|
@@ -1,26 +1,48 @@
|
|
import React from 'react';
|
|
import React from 'react';
|
|
|
|
+import styled from '@emotion/styled';
|
|
import color from 'color';
|
|
import color from 'color';
|
|
import moment from 'moment';
|
|
import moment from 'moment';
|
|
|
|
|
|
|
|
+import {Client} from 'app/api';
|
|
|
|
+import Feature from 'app/components/acl/feature';
|
|
|
|
+import Button from 'app/components/button';
|
|
import Graphic from 'app/components/charts/components/graphic';
|
|
import Graphic from 'app/components/charts/components/graphic';
|
|
import MarkLine from 'app/components/charts/components/markLine';
|
|
import MarkLine from 'app/components/charts/components/markLine';
|
|
|
|
+import EventsRequest from 'app/components/charts/eventsRequest';
|
|
import LineChart, {LineChartSeries} from 'app/components/charts/lineChart';
|
|
import LineChart, {LineChartSeries} from 'app/components/charts/lineChart';
|
|
|
|
+import {Panel, PanelBody, PanelFooter} from 'app/components/panels';
|
|
|
|
+import Placeholder from 'app/components/placeholder';
|
|
|
|
+import {IconCheckmark, IconFire, IconWarning} from 'app/icons';
|
|
|
|
+import {t} from 'app/locale';
|
|
import space from 'app/styles/space';
|
|
import space from 'app/styles/space';
|
|
-import {ReactEchartsRef, Series} from 'app/types/echarts';
|
|
|
|
|
|
+import {AvatarProject, Organization, Project} from 'app/types';
|
|
|
|
+import {ReactEchartsRef} from 'app/types/echarts';
|
|
import theme from 'app/utils/theme';
|
|
import theme from 'app/utils/theme';
|
|
-import {Trigger} from 'app/views/settings/incidentRules/types';
|
|
|
|
|
|
+import {makeDefaultCta} from 'app/views/settings/incidentRules/incidentRulePresets';
|
|
|
|
+import {IncidentRule} from 'app/views/settings/incidentRules/types';
|
|
|
|
|
|
-import {Incident} from '../../types';
|
|
|
|
|
|
+import {Incident, IncidentActivityType, IncidentStatus} from '../../types';
|
|
|
|
+import {getIncidentRuleMetricPreset} from '../../utils';
|
|
|
|
|
|
const X_AXIS_BOUNDARY_GAP = 15;
|
|
const X_AXIS_BOUNDARY_GAP = 15;
|
|
const VERTICAL_PADDING = 22;
|
|
const VERTICAL_PADDING = 22;
|
|
|
|
|
|
type Props = {
|
|
type Props = {
|
|
- data: Series[];
|
|
|
|
- ruleChangeThreshold?: string;
|
|
|
|
|
|
+ api: Client;
|
|
|
|
+ rule?: IncidentRule;
|
|
incidents?: Incident[];
|
|
incidents?: Incident[];
|
|
- warningTrigger?: Trigger;
|
|
|
|
- criticalTrigger?: Trigger;
|
|
|
|
|
|
+ timePeriod: {
|
|
|
|
+ start: string;
|
|
|
|
+ end: string;
|
|
|
|
+ label: string;
|
|
|
|
+ custom?: boolean;
|
|
|
|
+ };
|
|
|
|
+ organization: Organization;
|
|
|
|
+ projects: Project[] | AvatarProject[];
|
|
|
|
+ metricText: React.ReactNode;
|
|
|
|
+ interval: string;
|
|
|
|
+ query: string;
|
|
|
|
+ orgId: string;
|
|
};
|
|
};
|
|
|
|
|
|
type State = {
|
|
type State = {
|
|
@@ -41,6 +63,47 @@ function createThresholdSeries(lineColor: string, threshold: number): LineChartS
|
|
};
|
|
};
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+function createStatusAreaSeries(
|
|
|
|
+ lineColor: string,
|
|
|
|
+ startTime: number,
|
|
|
|
+ endTime: number
|
|
|
|
+): LineChartSeries {
|
|
|
|
+ return {
|
|
|
|
+ seriesName: 'Status Area',
|
|
|
|
+ type: 'line',
|
|
|
|
+ markLine: MarkLine({
|
|
|
|
+ silent: true,
|
|
|
|
+ lineStyle: {color: lineColor, type: 'solid', width: 4},
|
|
|
|
+ data: [[{coord: [startTime, 0]}, {coord: [endTime, 0]}] as any],
|
|
|
|
+ }),
|
|
|
|
+ data: [],
|
|
|
|
+ };
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+function createIncidentSeries(
|
|
|
|
+ lineColor: string,
|
|
|
|
+ incidentTimestamp: number,
|
|
|
|
+ label?: string
|
|
|
|
+) {
|
|
|
|
+ return {
|
|
|
|
+ seriesName: 'Incident Line',
|
|
|
|
+ type: 'line',
|
|
|
|
+ markLine: MarkLine({
|
|
|
|
+ silent: true,
|
|
|
|
+ lineStyle: {color: lineColor, type: 'solid'},
|
|
|
|
+ data: [{xAxis: incidentTimestamp}] as any,
|
|
|
|
+ label: {
|
|
|
|
+ show: !!label,
|
|
|
|
+ position: 'insideEndBottom',
|
|
|
|
+ formatter: label || '-',
|
|
|
|
+ color: lineColor,
|
|
|
|
+ fontSize: 10,
|
|
|
|
+ } as any,
|
|
|
|
+ }),
|
|
|
|
+ data: [],
|
|
|
|
+ };
|
|
|
|
+}
|
|
|
|
+
|
|
export default class MetricChart extends React.PureComponent<Props, State> {
|
|
export default class MetricChart extends React.PureComponent<Props, State> {
|
|
state = {
|
|
state = {
|
|
width: -1,
|
|
width: -1,
|
|
@@ -49,6 +112,11 @@ export default class MetricChart extends React.PureComponent<Props, State> {
|
|
|
|
|
|
ref: null | ReactEchartsRef = null;
|
|
ref: null | ReactEchartsRef = null;
|
|
|
|
|
|
|
|
+ get metricPreset() {
|
|
|
|
+ const {rule} = this.props;
|
|
|
|
+ return rule ? getIncidentRuleMetricPreset(rule) : undefined;
|
|
|
|
+ }
|
|
|
|
+
|
|
/**
|
|
/**
|
|
* Syncs component state with the chart's width/heights
|
|
* Syncs component state with the chart's width/heights
|
|
*/
|
|
*/
|
|
@@ -79,18 +147,18 @@ export default class MetricChart extends React.PureComponent<Props, State> {
|
|
}
|
|
}
|
|
};
|
|
};
|
|
|
|
|
|
- getRuleCreatedThresholdElements = () => {
|
|
|
|
|
|
+ getRuleChangeThresholdElements = data => {
|
|
const {height, width} = this.state;
|
|
const {height, width} = this.state;
|
|
- const {data, ruleChangeThreshold} = this.props;
|
|
|
|
|
|
+ const {dateModified} = this.props.rule || {};
|
|
|
|
|
|
- if (!data.length || !data[0].data.length) {
|
|
|
|
|
|
+ if (!data.length || !data[0].data.length || !dateModified) {
|
|
return [];
|
|
return [];
|
|
}
|
|
}
|
|
|
|
|
|
const seriesData = data[0].data;
|
|
const seriesData = data[0].data;
|
|
const seriesStart = seriesData[0].name as number;
|
|
const seriesStart = seriesData[0].name as number;
|
|
const seriesEnd = seriesData[seriesData.length - 1].name as number;
|
|
const seriesEnd = seriesData[seriesData.length - 1].name as number;
|
|
- const ruleChanged = moment(ruleChangeThreshold).valueOf();
|
|
|
|
|
|
+ const ruleChanged = moment(dateModified).valueOf();
|
|
|
|
|
|
if (ruleChanged < seriesStart) {
|
|
if (ruleChanged < seriesStart) {
|
|
return [];
|
|
return [];
|
|
@@ -128,102 +196,66 @@ export default class MetricChart extends React.PureComponent<Props, State> {
|
|
];
|
|
];
|
|
};
|
|
};
|
|
|
|
|
|
- render() {
|
|
|
|
- const {data, incidents, warningTrigger, criticalTrigger} = this.props;
|
|
|
|
-
|
|
|
|
- const series: LineChartSeries[] = [...data];
|
|
|
|
- // Ensure series data appears above incident lines
|
|
|
|
- series[0].z = 100;
|
|
|
|
- const dataArr = data[0].data;
|
|
|
|
- const maxSeriesValue = dataArr.reduce(
|
|
|
|
- (currMax, coord) => Math.max(currMax, coord.value),
|
|
|
|
- 0
|
|
|
|
- );
|
|
|
|
- const firstPoint = Number(dataArr[0].name);
|
|
|
|
- const lastPoint = dataArr[dataArr.length - 1].name;
|
|
|
|
- const resolvedArea = {
|
|
|
|
- seriesName: 'Resolved Area',
|
|
|
|
- type: 'line',
|
|
|
|
- markLine: MarkLine({
|
|
|
|
- silent: true,
|
|
|
|
- lineStyle: {color: theme.green300, type: 'solid', width: 4},
|
|
|
|
- data: [[{coord: [firstPoint, 0]}, {coord: [lastPoint, 0]}] as any],
|
|
|
|
- }),
|
|
|
|
- data: [],
|
|
|
|
|
|
+ renderChartActions(
|
|
|
|
+ totalDuration: number,
|
|
|
|
+ criticalDuration: number,
|
|
|
|
+ warningDuration: number
|
|
|
|
+ ) {
|
|
|
|
+ const {rule, orgId, projects, timePeriod} = this.props;
|
|
|
|
+ const preset = this.metricPreset;
|
|
|
|
+ const ctaOpts = {
|
|
|
|
+ orgSlug: orgId,
|
|
|
|
+ projects: projects as Project[],
|
|
|
|
+ rule,
|
|
|
|
+ start: timePeriod.start,
|
|
|
|
+ end: timePeriod.end,
|
|
};
|
|
};
|
|
- series.push(resolvedArea);
|
|
|
|
- if (incidents) {
|
|
|
|
- // select incidents that fall within the graph range
|
|
|
|
- const periodStart = moment.utc(firstPoint);
|
|
|
|
- const filteredIncidents = incidents.filter(incident => {
|
|
|
|
- return !incident.dateClosed || moment(incident.dateClosed).isAfter(periodStart);
|
|
|
|
- });
|
|
|
|
|
|
|
|
- const criticalLines = filteredIncidents.map(incident => {
|
|
|
|
- const detectTime = Math.max(moment(incident.dateStarted).valueOf(), firstPoint);
|
|
|
|
- let resolveTime;
|
|
|
|
- if (incident.dateClosed) {
|
|
|
|
- resolveTime = moment(incident.dateClosed).valueOf();
|
|
|
|
- } else {
|
|
|
|
- resolveTime = lastPoint;
|
|
|
|
- }
|
|
|
|
- return [{coord: [detectTime, 0]}, {coord: [resolveTime, 0]}];
|
|
|
|
- });
|
|
|
|
- const criticalArea = {
|
|
|
|
- seriesName: 'Critical Area',
|
|
|
|
- type: 'line',
|
|
|
|
- markLine: MarkLine({
|
|
|
|
- silent: true,
|
|
|
|
- lineStyle: {color: theme.red300, type: 'solid', width: 4},
|
|
|
|
- data: criticalLines as any,
|
|
|
|
- }),
|
|
|
|
- data: [],
|
|
|
|
- };
|
|
|
|
- series.push(criticalArea);
|
|
|
|
-
|
|
|
|
- const incidentValueMap: Record<number, string> = {};
|
|
|
|
- const incidentLines = filteredIncidents.map(({dateStarted, identifier}) => {
|
|
|
|
- const incidentStart = moment(dateStarted).valueOf();
|
|
|
|
- incidentValueMap[incidentStart] = identifier;
|
|
|
|
- return {xAxis: incidentStart};
|
|
|
|
- });
|
|
|
|
- const incidentLinesSeries = {
|
|
|
|
- seriesName: 'Incident Line',
|
|
|
|
- type: 'line',
|
|
|
|
- markLine: MarkLine({
|
|
|
|
- silent: true,
|
|
|
|
- lineStyle: {color: theme.red300, type: 'solid'},
|
|
|
|
- data: incidentLines as any,
|
|
|
|
- label: {
|
|
|
|
- show: true,
|
|
|
|
- position: 'insideEndBottom',
|
|
|
|
- formatter: ({value}) => {
|
|
|
|
- return incidentValueMap[value] ?? '-';
|
|
|
|
- },
|
|
|
|
- color: theme.red300,
|
|
|
|
- fontSize: 10,
|
|
|
|
- } as any,
|
|
|
|
- }),
|
|
|
|
- data: [],
|
|
|
|
- };
|
|
|
|
- series.push(incidentLinesSeries);
|
|
|
|
- }
|
|
|
|
|
|
+ const {buttonText, ...props} = preset
|
|
|
|
+ ? preset.makeCtaParams(ctaOpts)
|
|
|
|
+ : makeDefaultCta(ctaOpts);
|
|
|
|
|
|
- let maxThresholdValue = 0;
|
|
|
|
- if (warningTrigger?.alertThreshold) {
|
|
|
|
- const {alertThreshold} = warningTrigger;
|
|
|
|
- const warningThresholdLine = createThresholdSeries(theme.yellow300, alertThreshold);
|
|
|
|
- series.push(warningThresholdLine);
|
|
|
|
- maxThresholdValue = Math.max(maxThresholdValue, alertThreshold);
|
|
|
|
- }
|
|
|
|
|
|
+ const resolvedPercent = (
|
|
|
|
+ (100 * (totalDuration - criticalDuration - warningDuration)) /
|
|
|
|
+ totalDuration
|
|
|
|
+ ).toFixed(2);
|
|
|
|
+ const criticalPercent = ((100 * criticalDuration) / totalDuration).toFixed(2);
|
|
|
|
+ const warningPercent = ((100 * warningDuration) / totalDuration).toFixed(2);
|
|
|
|
|
|
- if (criticalTrigger?.alertThreshold) {
|
|
|
|
- const {alertThreshold} = criticalTrigger;
|
|
|
|
- const criticalThresholdLine = createThresholdSeries(theme.red300, alertThreshold);
|
|
|
|
- series.push(criticalThresholdLine);
|
|
|
|
- maxThresholdValue = Math.max(maxThresholdValue, alertThreshold);
|
|
|
|
- }
|
|
|
|
|
|
+ return (
|
|
|
|
+ <ChartActions>
|
|
|
|
+ <ChartSummary>
|
|
|
|
+ <SummaryText>{t('SUMMARY')}</SummaryText>
|
|
|
|
+ <SummaryStats>
|
|
|
|
+ <StatItem>
|
|
|
|
+ <IconCheckmark color="green300" isCircled />
|
|
|
|
+ <StatCount>{resolvedPercent}%</StatCount>
|
|
|
|
+ </StatItem>
|
|
|
|
+ <StatItem>
|
|
|
|
+ <IconWarning color="yellow300" />
|
|
|
|
+ <StatCount>{warningPercent}%</StatCount>
|
|
|
|
+ </StatItem>
|
|
|
|
+ <StatItem>
|
|
|
|
+ <IconFire color="red300" />
|
|
|
|
+ <StatCount>{criticalPercent}%</StatCount>
|
|
|
|
+ </StatItem>
|
|
|
|
+ </SummaryStats>
|
|
|
|
+ </ChartSummary>
|
|
|
|
+ <Feature features={['discover-basic']}>
|
|
|
|
+ <Button size="small" disabled={!rule} {...props}>
|
|
|
|
+ {buttonText}
|
|
|
|
+ </Button>
|
|
|
|
+ </Feature>
|
|
|
|
+ </ChartActions>
|
|
|
|
+ );
|
|
|
|
+ }
|
|
|
|
|
|
|
|
+ renderChart(
|
|
|
|
+ data: LineChartSeries[],
|
|
|
|
+ series: LineChartSeries[],
|
|
|
|
+ maxThresholdValue: number,
|
|
|
|
+ maxSeriesValue: number
|
|
|
|
+ ) {
|
|
return (
|
|
return (
|
|
<LineChart
|
|
<LineChart
|
|
isGroupedByDate
|
|
isGroupedByDate
|
|
@@ -238,7 +270,7 @@ export default class MetricChart extends React.PureComponent<Props, State> {
|
|
yAxis={maxThresholdValue > maxSeriesValue ? {max: maxThresholdValue} : undefined}
|
|
yAxis={maxThresholdValue > maxSeriesValue ? {max: maxThresholdValue} : undefined}
|
|
series={series}
|
|
series={series}
|
|
graphic={Graphic({
|
|
graphic={Graphic({
|
|
- elements: this.getRuleCreatedThresholdElements(),
|
|
|
|
|
|
+ elements: this.getRuleChangeThresholdElements(data),
|
|
})}
|
|
})}
|
|
onFinished={() => {
|
|
onFinished={() => {
|
|
// We want to do this whenever the chart finishes re-rendering so that we can update the dimensions of
|
|
// We want to do this whenever the chart finishes re-rendering so that we can update the dimensions of
|
|
@@ -248,4 +280,242 @@ export default class MetricChart extends React.PureComponent<Props, State> {
|
|
/>
|
|
/>
|
|
);
|
|
);
|
|
}
|
|
}
|
|
|
|
+
|
|
|
|
+ renderEmpty() {
|
|
|
|
+ return (
|
|
|
|
+ <ChartPanel>
|
|
|
|
+ <PanelBody withPadding>
|
|
|
|
+ <Placeholder height="200px" />
|
|
|
|
+ </PanelBody>
|
|
|
|
+ </ChartPanel>
|
|
|
|
+ );
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ render() {
|
|
|
|
+ const {
|
|
|
|
+ api,
|
|
|
|
+ rule,
|
|
|
|
+ organization,
|
|
|
|
+ timePeriod,
|
|
|
|
+ projects,
|
|
|
|
+ interval,
|
|
|
|
+ metricText,
|
|
|
|
+ query,
|
|
|
|
+ incidents,
|
|
|
|
+ } = this.props;
|
|
|
|
+
|
|
|
|
+ if (!rule) {
|
|
|
|
+ return this.renderEmpty();
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ const criticalTrigger = rule.triggers.find(({label}) => label === 'critical');
|
|
|
|
+ const warningTrigger = rule.triggers.find(({label}) => label === 'warning');
|
|
|
|
+
|
|
|
|
+ return (
|
|
|
|
+ <EventsRequest
|
|
|
|
+ api={api}
|
|
|
|
+ organization={organization}
|
|
|
|
+ query={query}
|
|
|
|
+ environment={rule.environment ? [rule.environment] : undefined}
|
|
|
|
+ project={(projects as Project[]).map(project => Number(project.id))}
|
|
|
|
+ interval={interval}
|
|
|
|
+ start={timePeriod.start}
|
|
|
|
+ end={timePeriod.end}
|
|
|
|
+ yAxis={rule.aggregate}
|
|
|
|
+ includePrevious={false}
|
|
|
|
+ currentSeriesName={rule.aggregate}
|
|
|
|
+ partial={false}
|
|
|
|
+ >
|
|
|
|
+ {({loading, timeseriesData}) => {
|
|
|
|
+ if (loading || !timeseriesData) {
|
|
|
|
+ return this.renderEmpty();
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ const series: LineChartSeries[] = [...timeseriesData];
|
|
|
|
+ // Ensure series data appears above incident lines
|
|
|
|
+ series[0].z = 100;
|
|
|
|
+ const dataArr = timeseriesData[0].data;
|
|
|
|
+ const maxSeriesValue = dataArr.reduce(
|
|
|
|
+ (currMax, coord) => Math.max(currMax, coord.value),
|
|
|
|
+ 0
|
|
|
|
+ );
|
|
|
|
+ const firstPoint = Number(dataArr[0].name);
|
|
|
|
+ const lastPoint = dataArr[dataArr.length - 1].name as number;
|
|
|
|
+ const totalDuration = lastPoint - firstPoint;
|
|
|
|
+ let criticalDuration = 0;
|
|
|
|
+ let warningDuration = 0;
|
|
|
|
+
|
|
|
|
+ series.push(createStatusAreaSeries(theme.green300, firstPoint, lastPoint));
|
|
|
|
+ if (incidents) {
|
|
|
|
+ // select incidents that fall within the graph range
|
|
|
|
+ const periodStart = moment.utc(firstPoint);
|
|
|
|
+
|
|
|
|
+ incidents
|
|
|
|
+ .filter(
|
|
|
|
+ incident =>
|
|
|
|
+ !incident.dateClosed || moment(incident.dateClosed).isAfter(periodStart)
|
|
|
|
+ )
|
|
|
|
+ .forEach(incident => {
|
|
|
|
+ const statusChanges = incident.activities
|
|
|
|
+ ?.filter(
|
|
|
|
+ ({type, value}) =>
|
|
|
|
+ type === IncidentActivityType.STATUS_CHANGE &&
|
|
|
|
+ value &&
|
|
|
|
+ [
|
|
|
|
+ `${IncidentStatus.WARNING}`,
|
|
|
|
+ `${IncidentStatus.CRITICAL}`,
|
|
|
|
+ ].includes(value)
|
|
|
|
+ )
|
|
|
|
+ .sort(
|
|
|
|
+ (a, b) =>
|
|
|
|
+ moment(a.dateCreated).valueOf() - moment(b.dateCreated).valueOf()
|
|
|
|
+ );
|
|
|
|
+
|
|
|
|
+ const incidentEnd = incident.dateClosed ?? moment().valueOf();
|
|
|
|
+
|
|
|
|
+ const timeWindowMs = rule.timeWindow * 60 * 1000;
|
|
|
|
+
|
|
|
|
+ series.push(
|
|
|
|
+ createIncidentSeries(
|
|
|
|
+ warningTrigger &&
|
|
|
|
+ statusChanges &&
|
|
|
|
+ !statusChanges.find(
|
|
|
|
+ ({value}) => value === `${IncidentStatus.CRITICAL}`
|
|
|
|
+ )
|
|
|
|
+ ? theme.yellow300
|
|
|
|
+ : theme.red300,
|
|
|
|
+ moment(incident.dateStarted).valueOf(),
|
|
|
|
+ incident.identifier
|
|
|
|
+ )
|
|
|
|
+ );
|
|
|
|
+ const areaStart = moment(incident.dateStarted).valueOf();
|
|
|
|
+ const areaEnd =
|
|
|
|
+ statusChanges?.length && statusChanges[0].dateCreated
|
|
|
|
+ ? moment(statusChanges[0].dateCreated).valueOf() - timeWindowMs
|
|
|
|
+ : moment(incidentEnd).valueOf();
|
|
|
|
+ const areaColor = warningTrigger ? theme.yellow300 : theme.red300;
|
|
|
|
+ series.push(createStatusAreaSeries(areaColor, areaStart, areaEnd));
|
|
|
|
+ if (areaColor === theme.yellow300) {
|
|
|
|
+ warningDuration += areaEnd - areaStart;
|
|
|
|
+ } else {
|
|
|
|
+ criticalDuration += areaEnd - areaStart;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ statusChanges?.forEach((activity, idx) => {
|
|
|
|
+ const statusAreaStart =
|
|
|
|
+ moment(activity.dateCreated).valueOf() - timeWindowMs;
|
|
|
|
+ const statusAreaColor =
|
|
|
|
+ activity.value === `${IncidentStatus.CRITICAL}`
|
|
|
|
+ ? theme.red300
|
|
|
|
+ : theme.yellow300;
|
|
|
|
+ const statusAreaEnd =
|
|
|
|
+ idx === statusChanges.length - 1
|
|
|
|
+ ? moment(incidentEnd).valueOf()
|
|
|
|
+ : moment(statusChanges[idx + 1].dateCreated).valueOf() -
|
|
|
|
+ timeWindowMs;
|
|
|
|
+ series.push(
|
|
|
|
+ createStatusAreaSeries(statusAreaColor, areaStart, statusAreaEnd)
|
|
|
|
+ );
|
|
|
|
+ if (statusAreaColor === theme.yellow300) {
|
|
|
|
+ warningDuration += statusAreaEnd - statusAreaStart;
|
|
|
|
+ } else {
|
|
|
|
+ criticalDuration += statusAreaEnd - statusAreaStart;
|
|
|
|
+ }
|
|
|
|
+ });
|
|
|
|
+ });
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ let maxThresholdValue = 0;
|
|
|
|
+ if (warningTrigger?.alertThreshold) {
|
|
|
|
+ const {alertThreshold} = warningTrigger;
|
|
|
|
+ const warningThresholdLine = createThresholdSeries(
|
|
|
|
+ theme.yellow300,
|
|
|
|
+ alertThreshold
|
|
|
|
+ );
|
|
|
|
+ series.push(warningThresholdLine);
|
|
|
|
+ maxThresholdValue = Math.max(maxThresholdValue, alertThreshold);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (criticalTrigger?.alertThreshold) {
|
|
|
|
+ const {alertThreshold} = criticalTrigger;
|
|
|
|
+ const criticalThresholdLine = createThresholdSeries(
|
|
|
|
+ theme.red300,
|
|
|
|
+ alertThreshold
|
|
|
|
+ );
|
|
|
|
+ series.push(criticalThresholdLine);
|
|
|
|
+ maxThresholdValue = Math.max(maxThresholdValue, alertThreshold);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return (
|
|
|
|
+ <ChartPanel>
|
|
|
|
+ <PanelBody withPadding>
|
|
|
|
+ <ChartHeader>
|
|
|
|
+ <PresetName>{this.metricPreset?.name ?? t('Custom metric')}</PresetName>
|
|
|
|
+ {metricText}
|
|
|
|
+ </ChartHeader>
|
|
|
|
+ {this.renderChart(
|
|
|
|
+ timeseriesData,
|
|
|
|
+ series,
|
|
|
|
+ maxThresholdValue,
|
|
|
|
+ maxSeriesValue
|
|
|
|
+ )}
|
|
|
|
+ </PanelBody>
|
|
|
|
+ {this.renderChartActions(totalDuration, criticalDuration, warningDuration)}
|
|
|
|
+ </ChartPanel>
|
|
|
|
+ );
|
|
|
|
+ }}
|
|
|
|
+ </EventsRequest>
|
|
|
|
+ );
|
|
|
|
+ }
|
|
}
|
|
}
|
|
|
|
+
|
|
|
|
+const ChartPanel = styled(Panel)`
|
|
|
|
+ margin-top: ${space(2)};
|
|
|
|
+`;
|
|
|
|
+
|
|
|
|
+const ChartHeader = styled('header')`
|
|
|
|
+ margin-bottom: ${space(1)};
|
|
|
|
+ display: flex;
|
|
|
|
+ flex-direction: row;
|
|
|
|
+`;
|
|
|
|
+
|
|
|
|
+const PresetName = styled('div')`
|
|
|
|
+ text-transform: capitalize;
|
|
|
|
+ margin-right: ${space(0.5)};
|
|
|
|
+`;
|
|
|
|
+
|
|
|
|
+const ChartActions = styled(PanelFooter)`
|
|
|
|
+ display: flex;
|
|
|
|
+ justify-content: flex-end;
|
|
|
|
+ align-items: center;
|
|
|
|
+ padding: ${space(1)} 20px;
|
|
|
|
+`;
|
|
|
|
+
|
|
|
|
+const ChartSummary = styled('div')`
|
|
|
|
+ display: flex;
|
|
|
|
+ margin-right: auto;
|
|
|
|
+`;
|
|
|
|
+
|
|
|
|
+const SummaryText = styled('span')`
|
|
|
|
+ margin-top: ${space(0.25)};
|
|
|
|
+ font-weight: bold;
|
|
|
|
+ font-size: ${p => p.theme.fontSizeSmall};
|
|
|
|
+`;
|
|
|
|
+
|
|
|
|
+const SummaryStats = styled('div')`
|
|
|
|
+ display: flex;
|
|
|
|
+ align-items: center;
|
|
|
|
+ margin: 0 ${space(2)};
|
|
|
|
+`;
|
|
|
|
+
|
|
|
|
+const StatItem = styled('div')`
|
|
|
|
+ display: flex;
|
|
|
|
+ align-items: center;
|
|
|
|
+ margin: 0 ${space(2)} 0 0;
|
|
|
|
+`;
|
|
|
|
+
|
|
|
|
+const StatCount = styled('span')`
|
|
|
|
+ margin-left: ${space(0.5)};
|
|
|
|
+ margin-top: ${space(0.25)};
|
|
|
|
+ color: ${p => p.theme.textColor};
|
|
|
|
+`;
|