Skip to content

Commit

Permalink
Flyouts for APM alert annotations (#101632)
Browse files Browse the repository at this point in the history
  • Loading branch information
smith authored Jun 15, 2021
1 parent 35dd70b commit c151426
Show file tree
Hide file tree
Showing 16 changed files with 350 additions and 327 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
} from '@elastic/charts';
import { EuiTitle } from '@elastic/eui';
import d3 from 'd3';
import React from 'react';
import React, { Suspense, useState } from 'react';
import { RULE_ID } from '@kbn/rule-data-utils/target/technical_field_names';
import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context';
import { APIReturnType } from '../../../../services/rest/createCallApmApi';
Expand All @@ -27,6 +27,7 @@ import { useTheme } from '../../../../hooks/use_theme';
import { AlertType } from '../../../../../common/alert_types';
import { getAlertAnnotations } from '../../../shared/charts/helper/get_alert_annotations';
import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context';
import { LazyAlertsFlyout } from '../../../../../../observability/public';

type ErrorDistributionAPIResponse = APIReturnType<'GET /api/apm/services/{serviceName}/errors/distribution'>;

Expand Down Expand Up @@ -68,6 +69,9 @@ export function ErrorDistribution({ distribution, title }: Props) {
const { observabilityRuleTypeRegistry } = useApmPluginContext();
const { alerts } = useApmServiceContext();
const { getFormatter } = observabilityRuleTypeRegistry;
const [selectedAlertId, setSelectedAlertId] = useState<string | undefined>(
undefined
);

const tooltipProps: SettingsSpec['tooltip'] = {
headerFormatter: (tooltip: TooltipValue) => {
Expand Down Expand Up @@ -122,8 +126,21 @@ export function ErrorDistribution({ distribution, title }: Props) {
),
chartStartTime: buckets[0].x0,
getFormatter,
selectedAlertId,
setSelectedAlertId,
theme,
})}
<Suspense fallback={null}>
<LazyAlertsFlyout
alerts={alerts}
isInApp={true}
observabilityRuleTypeRegistry={observabilityRuleTypeRegistry}
onClose={() => {
setSelectedAlertId(undefined);
}}
selectedAlertId={selectedAlertId}
/>
</Suspense>
</Chart>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,21 @@ const getFormatter: ObservabilityRuleTypeRegistry['getFormatter'] = () => () =>
link: '/',
reason: 'a good reason',
});
const selectedAlertId = undefined;
const setSelectedAlertId = jest.fn();

describe('getAlertAnnotations', () => {
describe('with no alerts', () => {
it('returns an empty array', () => {
expect(
getAlertAnnotations({ alerts: [], chartStartTime, getFormatter, theme })
getAlertAnnotations({
alerts: [],
chartStartTime,
getFormatter,
selectedAlertId,
setSelectedAlertId,
theme,
})
).toEqual([]);
});
});
Expand All @@ -66,6 +75,8 @@ describe('getAlertAnnotations', () => {
alerts: [alert],
chartStartTime,
getFormatter,
selectedAlertId,
setSelectedAlertId,
theme,
})![0].props.style.line.stroke
).toEqual(euiColorDanger);
Expand All @@ -77,6 +88,8 @@ describe('getAlertAnnotations', () => {
alerts: [alert],
chartStartTime,
getFormatter,
selectedAlertId,
setSelectedAlertId,
theme,
})![0].props.dataValues[0].header
).toEqual('Alert');
Expand All @@ -88,6 +101,8 @@ describe('getAlertAnnotations', () => {
alerts: [alert],
chartStartTime,
getFormatter,
selectedAlertId,
setSelectedAlertId,
theme,
})![0].props.dataValues[0].details
).toEqual('a good reason');
Expand All @@ -103,6 +118,8 @@ describe('getAlertAnnotations', () => {
alerts: [alert],
chartStartTime,
getFormatter: getNoFormatter,
selectedAlertId,
setSelectedAlertId,
theme,
})![0].props.dataValues[0].details
).toEqual(alert['rule.name']![0]);
Expand All @@ -118,6 +135,8 @@ describe('getAlertAnnotations', () => {
alerts: [alert],
chartStartTime: beforeChartStartTime,
getFormatter,
selectedAlertId,
setSelectedAlertId,
theme,
})![0].props.dataValues[0].dataValue
).toEqual(beforeChartStartTime);
Expand All @@ -137,6 +156,8 @@ describe('getAlertAnnotations', () => {
alerts: [warningAlert],
chartStartTime,
getFormatter,
selectedAlertId,
setSelectedAlertId,
theme,
})![0].props.style.line.stroke
).toEqual(euiColorWarning);
Expand All @@ -148,6 +169,8 @@ describe('getAlertAnnotations', () => {
alerts: [warningAlert],
chartStartTime,
getFormatter,
selectedAlertId,
setSelectedAlertId,
theme,
})![0].props.dataValues[0].header
).toEqual('Warning Alert');
Expand All @@ -166,6 +189,8 @@ describe('getAlertAnnotations', () => {
alerts: [criticalAlert],
chartStartTime,
getFormatter,
selectedAlertId,
setSelectedAlertId,
theme,
})![0].props.style.line.stroke
).toEqual(euiColorDanger);
Expand All @@ -177,6 +202,8 @@ describe('getAlertAnnotations', () => {
alerts: [criticalAlert],
chartStartTime,
getFormatter,
selectedAlertId,
setSelectedAlertId,
theme,
})![0].props.dataValues[0].header
).toEqual('Critical Alert');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
Position,
RectAnnotation,
} from '@elastic/charts';
import { EuiIcon } from '@elastic/eui';
import { EuiButtonIcon } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import {
ALERT_DURATION,
Expand All @@ -20,7 +20,7 @@ import {
RULE_ID,
RULE_NAME,
} from '@kbn/rule-data-utils/target/technical_field_names';
import React from 'react';
import React, { Dispatch, SetStateAction } from 'react';
import { EuiTheme } from 'src/plugins/kibana_react/common';
import { ValuesType } from 'utility-types';
import type { ObservabilityRuleTypeRegistry } from '../../../../../../observability/public';
Expand Down Expand Up @@ -68,15 +68,30 @@ function getAlertHeader({
}
}

/**
* Get the components needed to render alert annotations.
*
* You might be thinking, "Hey, this is a function that returns DOM.
* This should not be a function but a component."
*
* You would be correct, except for https://github.com/elastic/elastic-charts/issues/914,
* which makes it so if you construct a chart with its elements broken into
* different components it makes the whole chart disappear, which is not what
* we want.
*/
export function getAlertAnnotations({
alerts,
chartStartTime,
getFormatter,
selectedAlertId,
setSelectedAlertId,
theme,
}: {
alerts?: Alert[];
chartStartTime: number;
getFormatter: ObservabilityRuleTypeRegistry['getFormatter'];
selectedAlertId?: string;
setSelectedAlertId: Dispatch<SetStateAction<string | undefined>>;
theme: EuiTheme;
}) {
return alerts?.flatMap((alert) => {
Expand All @@ -100,6 +115,7 @@ export function getAlertAnnotations({
formatters: { asDuration, asPercent },
}) ?? {}),
};
const isSelected = uuid === selectedAlertId;

return [
<LineAnnotation
Expand All @@ -113,9 +129,29 @@ export function getAlertAnnotations({
domainType={AnnotationDomainType.XDomain}
id={`alert_${uuid}_line`}
key={`alert_${uuid}_line`}
marker={<EuiIcon type="alert" />}
marker={
<EuiButtonIcon
aria-label={i18n.translate(
'xpack.apm.alertAnnotationButtonAriaLabel',
{ defaultMessage: 'View alert details' }
)}
color={severityLevel === 'warning' ? 'warning' : 'danger'}
onClick={() => {
if (selectedAlertId === uuid) {
setSelectedAlertId(undefined);
} else {
setSelectedAlertId(uuid);
}
}}
iconSize={isSelected ? 'l' : 'm'}
iconType="alert"
size="xs"
/>
}
markerPosition={Position.Top}
style={{ line: { opacity: 1, strokeWidth: 2, stroke: color } }}
style={{
line: { opacity: 1, strokeWidth: isSelected ? 6 : 2, stroke: color },
}}
/>,
<RectAnnotation
key={`alert_${uuid}_area`}
Expand All @@ -128,7 +164,7 @@ export function getAlertAnnotations({
},
},
]}
style={{ fill: color }}
style={{ fill: color, opacity: isSelected ? 0.6 : 0.25 }}
/>,
];
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { StoryContext } from '@storybook/react';
import React, { ComponentType } from 'react';
import { MemoryRouter, Route } from 'react-router-dom';
import { EuiThemeProvider } from '../../../../../../../../src/plugins/kibana_react/common';
import { KibanaContextProvider } from '../../../../../../../../src/plugins/kibana_react/public';
import { LatencyAggregationType } from '../../../../../common/latency_aggregation_types';
import {
ApmPluginContext,
Expand Down Expand Up @@ -48,6 +49,7 @@ export default {
toasts: { addWarning: () => {}, addDanger: () => {} },
},
http: {
basePath: { prepend: () => {} },
get: (endpoint: string) => {
switch (endpoint) {
case '/api/apm/services/test-service/transactions/charts/latency':
Expand All @@ -59,7 +61,7 @@ export default {
}
},
},
uiSettings: { get: () => true },
uiSettings: { get: () => '' },
},
plugins: { observability: { isAlertingExperienceEnabled: () => true } },
observabilityRuleTypeRegistry: { getFormatter: () => undefined },
Expand All @@ -71,20 +73,24 @@ export default {
<ApmPluginContext.Provider value={apmPluginContextMock}>
<MemoryRouter initialEntries={[`/app/apm/services/test-service`]}>
<Route path="/app/apm/services/:serviceName">
<EuiThemeProvider>
<MockUrlParamsContextProvider
params={{
latencyAggregationType: LatencyAggregationType.avg,
transactionType: `${Math.random()}`, // So we don't memoize
}}
>
<ApmServiceContextProvider>
<ChartPointerEventContextProvider>
<Story />
</ChartPointerEventContextProvider>
</ApmServiceContextProvider>
</MockUrlParamsContextProvider>
</EuiThemeProvider>
<KibanaContextProvider
services={{ ...apmPluginContextMock.core }}
>
<EuiThemeProvider>
<MockUrlParamsContextProvider
params={{
latencyAggregationType: LatencyAggregationType.avg,
transactionType: `${Math.random()}`, // So we don't memoize
}}
>
<ApmServiceContextProvider>
<ChartPointerEventContextProvider>
<Story />
</ChartPointerEventContextProvider>
</ApmServiceContextProvider>
</MockUrlParamsContextProvider>
</EuiThemeProvider>
</KibanaContextProvider>
</Route>
</MemoryRouter>
</ApmPluginContext.Provider>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,12 @@ import {
} from '@elastic/charts';
import { EuiIcon } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import React, { Suspense, useState } from 'react';
import { useHistory } from 'react-router-dom';
import { useChartTheme } from '../../../../../observability/public';
import {
LazyAlertsFlyout,
useChartTheme,
} from '../../../../../observability/public';
import { asAbsoluteDateTime } from '../../../../common/utils/formatters';
import {
Coordinate,
Expand Down Expand Up @@ -88,6 +91,9 @@ export function TimeseriesChart({
const { setPointerEvent, chartRef } = useChartPointerEventContext();
const theme = useTheme();
const chartTheme = useChartTheme();
const [selectedAlertId, setSelectedAlertId] = useState<string | undefined>(
undefined
);

const xValues = timeseries.flatMap(({ data }) => data.map(({ x }) => x));

Expand Down Expand Up @@ -208,8 +214,21 @@ export function TimeseriesChart({
alerts,
chartStartTime: xValues[0],
getFormatter,
selectedAlertId,
setSelectedAlertId,
theme,
})}
<Suspense fallback={null}>
<LazyAlertsFlyout
alerts={alerts}
isInApp={true}
observabilityRuleTypeRegistry={observabilityRuleTypeRegistry}
onClose={() => {
setSelectedAlertId(undefined);
}}
selectedAlertId={selectedAlertId}
/>
</Suspense>
</Chart>
</ChartContainer>
);
Expand Down
Loading

0 comments on commit c151426

Please sign in to comment.