From a7463000d43f61313d8a701831e5bddad1b05940 Mon Sep 17 00:00:00 2001 From: Igor Zaytsev Date: Mon, 9 Sep 2019 04:50:52 -0400 Subject: [PATCH 1/2] Events Feature first draft --- .../elastic_chart/helpers/info_tooltip.tsx | 48 +++++ .../components/elastic_chart/helpers/types.ts | 30 +++ .../components/elastic_chart/helpers/utils.ts | 35 ++++ .../public/components/elastic_chart/index.tsx | 187 ++++++++++++++++++ .../components/elasticsearch/node/advanced.js | 24 +-- x-pack/tsconfig.json | 5 +- 6 files changed, 306 insertions(+), 23 deletions(-) create mode 100644 x-pack/legacy/plugins/monitoring/public/components/elastic_chart/helpers/info_tooltip.tsx create mode 100644 x-pack/legacy/plugins/monitoring/public/components/elastic_chart/helpers/types.ts create mode 100644 x-pack/legacy/plugins/monitoring/public/components/elastic_chart/helpers/utils.ts create mode 100644 x-pack/legacy/plugins/monitoring/public/components/elastic_chart/index.tsx diff --git a/x-pack/legacy/plugins/monitoring/public/components/elastic_chart/helpers/info_tooltip.tsx b/x-pack/legacy/plugins/monitoring/public/components/elastic_chart/helpers/info_tooltip.tsx new file mode 100644 index 0000000000000..2ce0555f2caba --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/elastic_chart/helpers/info_tooltip.tsx @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { Series } from './types'; + +interface Props { + series: Series[]; + bucketSize: string; +} + +export function InfoTooltip({ series, bucketSize }: Props) { + const tableRows = series.map((item: Series, index: number) => { + return ( + + {item.metric.label} + {item.metric.description} + + ); + }); + + return ( + + + + + + + {tableRows} + +
+ + {bucketSize}
+ ); +} diff --git a/x-pack/legacy/plugins/monitoring/public/components/elastic_chart/helpers/types.ts b/x-pack/legacy/plugins/monitoring/public/components/elastic_chart/helpers/types.ts new file mode 100644 index 0000000000000..b9fed4d4ee86b --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/elastic_chart/helpers/types.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface Metric { + app: string; + description: string; + field: string; + format: string; + hasCalculation?: boolean; + isDerivative?: boolean; + label: string; + metricAgg: string; + title: string; + units: string; +} + +export interface Series { + bucket_size: string; + timeRange: { + min: number; + max: number; + }; + metric: Metric; + data: Array<[number, number]>; +} + +export type FormatMethod = (val: number) => string; diff --git a/x-pack/legacy/plugins/monitoring/public/components/elastic_chart/helpers/utils.ts b/x-pack/legacy/plugins/monitoring/public/components/elastic_chart/helpers/utils.ts new file mode 100644 index 0000000000000..b4653ee518a5a --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/elastic_chart/helpers/utils.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { get } from 'lodash'; +import numeral from '@elastic/numeral'; +import moment from 'moment'; + +import { Series, FormatMethod } from './types'; + +export const getUnits = (series: Series): string => { + const units: string = get(series, '.metric.units', 'B'); + return units !== 'B' ? units : ''; +}; + +export const formatTicksValues = (series: Series): FormatMethod => { + const format: string = get(series, '.metric.format', '0,0.0'); + return (val: number) => `${numeral(val).format(format)} ${getUnits(series)}`; +}; + +export const formatTimeValues: FormatMethod = (val: number) => `${moment.utc(val).format('HH:mm')}`; + +export const getTitle = (series: Series[]): string => { + for (const s of series) { + const { metric } = s; + const { title, label } = metric; + const sTitle = title || label; + if (sTitle) { + return sTitle; + } + } + return ''; +}; diff --git a/x-pack/legacy/plugins/monitoring/public/components/elastic_chart/index.tsx b/x-pack/legacy/plugins/monitoring/public/components/elastic_chart/index.tsx new file mode 100644 index 0000000000000..84d2907c241b7 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/elastic_chart/index.tsx @@ -0,0 +1,187 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import chrome from 'ui/chrome'; +import { + Axis, + Chart, + getAxisId, + getSpecId, + LineSeries, + Settings, + ScaleType, + Theme, + LIGHT_THEME, + DARK_THEME, +} from '@elastic/charts'; +import { + EuiPageContent, + EuiSpacer, + EuiFlexGrid, + EuiFlexItem, + EuiFlexGroup, + EuiTitle, + EuiScreenReaderOnly, + EuiIconTip, +} from '@elastic/eui'; +import { get } from 'lodash'; +import { getTitle, getUnits, formatTicksValues, formatTimeValues } from './helpers/utils'; +import { InfoTooltip } from './helpers/info_tooltip'; +import { Series } from './helpers/types'; + +interface PropsSeries { + series: Series[]; +} +type OptionalComponent = JSX.Element | null; +type StaticComponent = JSX.Element; + +const getBaseTheme = (): Theme => { + const modeTheme: Theme = chrome.getUiSettingsClient().get('theme:darkMode') + ? DARK_THEME + : LIGHT_THEME; + return { + ...modeTheme, + lineSeriesStyle: { + line: { + strokeWidth: 2, + visible: true, + opacity: 1, + }, + point: { + strokeWidth: 1, + visible: true, + radius: 2, + opacity: 1, + }, + }, + }; +}; + +const gridLines = { + showGridLines: true, + gridLineStyle: { + stroke: 'black', + strokeWidth: 0.5, + opacity: 0.1, + }, +}; + +const MonitoringChart = ({ series }: PropsSeries): OptionalComponent => { + if (!series) { + return null; + } + + const firstSeries: Series = series[0]; + const { timeRange } = firstSeries; + const { min, max } = timeRange; + return ( + + + + + {series.map((item: any, index: number) => ( + + ))} + + ); +}; + +export const MonitoringTimeseriesContainer = ({ series }: PropsSeries): OptionalComponent => { + if (!series) { + return null; + } + + const seriesTitle: string = getTitle(series); + const titleForAriaIds: string = seriesTitle.replace(/\s+/, '--'); + const units: string = getUnits(series[0]); + const title: string = `${seriesTitle}${units ? ` (${units})` : ''}`; + const bucketSize: string = get(series[0], 'bucket_size'); + const seriesScreenReaderTextList: string[] = [ + i18n.translate('xpack.monitoring.chart.seriesScreenReaderListDescription', { + defaultMessage: 'Interval: {bucketSize}', + values: { + bucketSize, + }, + }), + ].concat(series.map((item: any) => `${item.metric.label}: ${item.metric.description}`)); + + return ( + + + + + + <> + {title} + + + + + + + + + + } + /> + + + {seriesScreenReaderTextList.join('. ')} + + + + {/* zoom button was here */} + + + + + + + ); +}; + +export const MonitoringCharts = ({ metrics }: { metrics: Series[][] }): StaticComponent => { + return ( + <> + + + + {metrics.map((series: Series[], index: number) => ( + + + + + ))} + + + + ); +}; diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/node/advanced.js b/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/node/advanced.js index bcc7a1dd47df4..4fbbc13e9e429 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/node/advanced.js +++ b/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/node/advanced.js @@ -7,20 +7,15 @@ import React from 'react'; import { EuiPage, - EuiPageContent, EuiPageBody, EuiPanel, - EuiSpacer, - EuiFlexGrid, - EuiFlexItem, } from '@elastic/eui'; import { NodeDetailStatus } from '../node_detail_status'; -import { MonitoringTimeseriesContainer } from '../../chart'; +import { MonitoringCharts } from '../../elastic_chart'; export const AdvancedNode = ({ nodeSummary, - metrics, - ...props + metrics }) => { const metricsToShow = [ metrics.node_gc, @@ -46,20 +41,7 @@ export const AdvancedNode = ({ - - - - {metricsToShow.map((metric, index) => ( - - - - - ))} - - + ); diff --git a/x-pack/tsconfig.json b/x-pack/tsconfig.json index 11983b9db9ccd..cd9b661640060 100644 --- a/x-pack/tsconfig.json +++ b/x-pack/tsconfig.json @@ -31,8 +31,9 @@ "test_utils/*": [ "x-pack/test_utils/*" ], - "monitoring/common/*": [ - "x-pack/monitoring/common/*" + "plugins/monitoring/*": [ + "x-pack/monitoring/common/*", + "x-pack/legacy/plugins/monitoring/public/components/elastic-chart/*" ] }, "types": [ From a78dea968c2340d3abb67396dcaf7d66395876c7 Mon Sep 17 00:00:00 2001 From: Igor Zaytsev Date: Fri, 13 Sep 2019 04:27:42 -0400 Subject: [PATCH 2/2] Synced crosshair to all charts --- .../public/components/elastic_chart/index.tsx | 52 +++++++++++++++---- 1 file changed, 42 insertions(+), 10 deletions(-) diff --git a/x-pack/legacy/plugins/monitoring/public/components/elastic_chart/index.tsx b/x-pack/legacy/plugins/monitoring/public/components/elastic_chart/index.tsx index 84d2907c241b7..37c8383c36c36 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/elastic_chart/index.tsx +++ b/x-pack/legacy/plugins/monitoring/public/components/elastic_chart/index.tsx @@ -17,9 +17,13 @@ import { Settings, ScaleType, Theme, + CursorEvent, LIGHT_THEME, DARK_THEME, + CrosshairStyle, } from '@elastic/charts'; +import { CursorUpdateListener } from '@elastic/charts/dist/chart_types/xy_chart/store/chart_state'; +import { AxisSpec } from '@elastic/charts/dist/chart_types/xy_chart/utils/specs'; import { EuiPageContent, EuiSpacer, @@ -37,7 +41,10 @@ import { Series } from './helpers/types'; interface PropsSeries { series: Series[]; + onCursorUpdate?: CursorUpdateListener; + chartRef: React.RefObject; } + type OptionalComponent = JSX.Element | null; type StaticComponent = JSX.Element; @@ -47,6 +54,10 @@ const getBaseTheme = (): Theme => { : LIGHT_THEME; return { ...modeTheme, + crosshair: { + band: { fill: 'red', visible: true }, + line: {} as CrosshairStyle['line'], + }, lineSeriesStyle: { line: { strokeWidth: 2, @@ -63,16 +74,16 @@ const getBaseTheme = (): Theme => { }; }; -const gridLines = { +const gridLines: AxisSpec = { showGridLines: true, gridLineStyle: { stroke: 'black', strokeWidth: 0.5, opacity: 0.1, }, -}; +} as AxisSpec; -const MonitoringChart = ({ series }: PropsSeries): OptionalComponent => { +const MonitoringChart = ({ series, onCursorUpdate, chartRef }: PropsSeries): OptionalComponent => { if (!series) { return null; } @@ -81,8 +92,14 @@ const MonitoringChart = ({ series }: PropsSeries): OptionalComponent => { const { timeRange } = firstSeries; const { min, max } = timeRange; return ( - - + + { tickFormat={formatTimeValues} /> - {series.map((item: any, index: number) => ( + {series.map((item: Series, index: number) => ( { ); }; -export const MonitoringTimeseriesContainer = ({ series }: PropsSeries): OptionalComponent => { +const MonitoringTimeseriesContainer = (props: PropsSeries): OptionalComponent => { + const { series } = props; if (!series) { return null; } @@ -124,7 +142,7 @@ export const MonitoringTimeseriesContainer = ({ series }: PropsSeries): Optional bucketSize, }, }), - ].concat(series.map((item: any) => `${item.metric.label}: ${item.metric.description}`)); + ].concat(series.map((item: Series) => `${item.metric.label}: ${item.metric.description}`)); return ( @@ -162,13 +180,25 @@ export const MonitoringTimeseriesContainer = ({ series }: PropsSeries): Optional - + ); }; export const MonitoringCharts = ({ metrics }: { metrics: Series[][] }): StaticComponent => { + const chartRefs: Array> = metrics.map(() => React.createRef()); + const timers: NodeJS.Timeout[] = []; + const onCursorUpdate: CursorUpdateListener = (event: CursorEvent | undefined): void => { + chartRefs.forEach((ref: React.RefObject, i: number) => { + clearTimeout(timers[i]); + timers[i] = setTimeout( + () => ref.current && ref.current!.dispatchExternalCursorEvent(event), + i * 5 + ); + }); + }; + return ( <> @@ -176,7 +206,9 @@ export const MonitoringCharts = ({ metrics }: { metrics: Series[][] }): StaticCo {metrics.map((series: Series[], index: number) => ( - + ))}