import moment from 'moment'; import MarkLine from 'app/components/charts/components/markLine'; import MarkPoint from 'app/components/charts/components/markPoint'; import LineChart, {LineChartSeries} from 'app/components/charts/lineChart'; import {t} from 'app/locale'; import space from 'app/styles/space'; import theme from 'app/utils/theme'; import {Trigger} from 'app/views/alerts/incidentRules/types'; import closedSymbol from './closedSymbol'; import startedSymbol from './startedSymbol'; type Truthy = T extends false | '' | 0 | null | undefined ? never : T; function truthy(value: T): value is Truthy { return !!value; } type Data = [number, {count: number}[]]; /** * So we'll have to see how this looks with real data, but echarts requires * an explicit (x,y) value to draw a symbol (incident started/closed bubble). * * This uses the closest date *without* going over. * * AFAICT we can't give it an x-axis value and have it draw on the line, * so we probably need to calculate the y-axis value ourselves if we want it placed * at the exact time. * * @param data Data array * @param needle the target timestamp */ function getNearbyIndex(data: Data[], needle: number) { // `data` is sorted, return the first index whose value (timestamp) is > `needle` const index = data.findIndex(([ts]) => ts > needle); // this shouldn't happen, as we try to buffer dates before start/end dates if (index === 0) { return 0; } return index !== -1 ? index - 1 : data.length - 1; } type Props = { data: Data[]; aggregate: string; started: string; closed?: string; triggers?: Trigger[]; resolveThreshold?: number | '' | null; }; const Chart = (props: Props) => { const {aggregate, data, started, closed, triggers, resolveThreshold} = props; const startedTs = started && moment.utc(started).unix(); const closedTs = closed && moment.utc(closed).unix(); const chartData = data.map(([ts, val]) => [ ts * 1000, val.length ? val.reduce((acc, {count} = {count: 0}) => acc + count, 0) : 0, ]); const startedCoordinate = startedTs ? chartData[getNearbyIndex(data, startedTs)] : undefined; const showClosedMarker = data && closedTs && data[data.length - 1] && data[data.length - 1][0] >= closedTs ? true : false; const closedCoordinate = closedTs && showClosedMarker ? chartData[getNearbyIndex(data, closedTs)] : undefined; const seriesName = aggregate; const warningTrigger = triggers?.find(trig => trig.label === 'warning'); const criticalTrigger = triggers?.find(trig => trig.label === 'critical'); const warningTriggerAlertThreshold = typeof warningTrigger?.alertThreshold === 'number' ? warningTrigger?.alertThreshold : undefined; const criticalTriggerAlertThreshold = typeof criticalTrigger?.alertThreshold === 'number' ? criticalTrigger?.alertThreshold : undefined; const alertResolveThreshold = typeof resolveThreshold === 'number' ? resolveThreshold : undefined; const marklinePrecision = Math.max( ...[ warningTriggerAlertThreshold, criticalTriggerAlertThreshold, alertResolveThreshold, ].map(decimal => { if (!decimal || !isFinite(decimal)) return 0; let e = 1; let p = 0; while (Math.round(decimal * e) / e !== decimal) { e *= 10; p += 1; } return p; }) ); const lineSeries: LineChartSeries[] = [ { // e.g. Events or Users seriesName, dataArray: chartData, data: [], markPoint: MarkPoint({ data: [ { labelForValue: seriesName, seriesName, symbol: `image://${startedSymbol}`, name: t('Alert Triggered'), coord: startedCoordinate, }, ...(closedTs ? [ { labelForValue: seriesName, seriesName, symbol: `image://${closedSymbol}`, symbolSize: 24, name: t('Alert Resolved'), coord: closedCoordinate, }, ] : []), ] as any, // TODO(ts): data on this type is likely incomplete (needs @types/echarts@4.6.2) }), }, warningTrigger && warningTriggerAlertThreshold && { seriesName: 'Warning Alert', type: 'line', markLine: MarkLine({ silent: true, lineStyle: {color: theme.yellow300}, data: [ { yAxis: warningTriggerAlertThreshold, } as any, // TODO(ts): data on this type is likely incomplete (needs @types/echarts@4.6.2) ], precision: marklinePrecision, label: { show: true, position: 'insideEndTop', formatter: 'WARNING', color: theme.yellow300, fontSize: 10, } as any, // TODO(ts): Color is not an exposed option for label, }), data: [], }, criticalTrigger && criticalTriggerAlertThreshold && { seriesName: 'Critical Alert', type: 'line', markLine: MarkLine({ silent: true, lineStyle: {color: theme.red200}, data: [ { yAxis: criticalTriggerAlertThreshold, } as any, // TODO(ts): data on this type is likely incomplete (needs @types/echarts@4.6.2) ], precision: marklinePrecision, label: { show: true, position: 'insideEndTop', formatter: 'CRITICAL', color: theme.red300, fontSize: 10, } as any, // TODO(ts): Color is not an exposed option for label, }), data: [], }, criticalTrigger && alertResolveThreshold && { seriesName: 'Critical Resolve', type: 'line', markLine: MarkLine({ silent: true, lineStyle: {color: theme.gray200}, data: [ { yAxis: alertResolveThreshold, } as any, // TODO(ts): data on this type is likely incomplete (needs @types/echarts@4.6.2) ], precision: marklinePrecision, label: { show: true, position: 'insideEndBottom', formatter: 'CRITICAL RESOLUTION', color: theme.gray200, fontSize: 10, } as any, // TODO(ts): Color is not an option for label, }), data: [], }, ].filter(truthy); return ( ); }; export default Chart;