Skip to content

Commit c76b490

Browse files
Alerting: Central alert history part4 (grafana#90088)
* Implement EventDetails for expanded rows and pagination on the events list * Add test for getPanelDataForRule function * prettier * refactor EventState component * create interfaces for props * Add missing translations * Update some comments * Add plus button in alertrulename , to add it into the filter * Add plus button to add filters from the list labels and alert name * Add clear labels filter button * run prettier * fix RBAC checks * Update AlertLabels onLabelClick functionality * add limit=0 in useCombinedRule call * Add filter by state * remove plus button in labels * Fix state filter * Add filter by previous state * fix some errors after solving conflicts * Add comments and remove some type assertions * Update the number of transitions calculation to be for each instance * Add tests for state filters * remove type assertion * Address review comments * Update returnTo prop in alert list view url * Update translations * address review comments * prettier * update cursor to pointer * Address Deyan review comments * address review pr comments from Deyan * fix label styles * Visualize expanded row as a state graph and address some pr review comments * Add warning when limit of events is reached and rename onClickLabel * Update texts * Fix translations * Update some Labels in the expanded states visualization * move getPanelDataForRule to a separate file * Add header to the list of events * Move HistoryErrorMessage to a separate file * remove getPanelDataForRule function and test * add comment * fitler by instance label results shown inthe state chart * remove defaults.ini changes * fix having single event on time state chart --------- Co-authored-by: Gilles De Mey <[email protected]>
1 parent 92ada4e commit c76b490

18 files changed

+860
-289
lines changed

pkg/services/navtree/navtreeimpl/navtree.go

+9-7
Original file line numberDiff line numberDiff line change
@@ -409,13 +409,15 @@ func (s *ServiceImpl) buildAlertNavLinks(c *contextmodel.ReqContext) *navtree.Na
409409
}
410410

411411
if s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagAlertingCentralAlertHistory) {
412-
alertChildNavs = append(alertChildNavs, &navtree.NavLink{
413-
Text: "History",
414-
SubTitle: "History of events that were generated by your Grafana-managed alert rules. Silences and Mute timings are ignored.",
415-
Id: "alerts-history",
416-
Url: s.cfg.AppSubURL + "/alerting/history",
417-
Icon: "history",
418-
})
412+
if hasAccess(ac.EvalAny(ac.EvalPermission(ac.ActionAlertingRuleRead))) {
413+
alertChildNavs = append(alertChildNavs, &navtree.NavLink{
414+
Text: "History",
415+
SubTitle: "View a history of all alert events generated by your Grafana-managed alert rules. All alert events are displayed regardless of whether silences or mute timings are set.",
416+
Id: "alerts-history",
417+
Url: s.cfg.AppSubURL + "/alerting/history",
418+
Icon: "history",
419+
})
420+
}
419421
}
420422

421423
if c.SignedInUser.GetOrgRole() == org.RoleAdmin {

public/app/features/alerting/routes.tsx

+1-4
Original file line numberDiff line numberDiff line change
@@ -168,10 +168,7 @@ export function getAlertingRoutes(cfg = config): RouteDescriptor[] {
168168
},
169169
{
170170
path: '/alerting/history/',
171-
roles: evaluateAccess([
172-
AccessControlAction.AlertingInstanceRead,
173-
AccessControlAction.AlertingInstancesExternalRead,
174-
]),
171+
roles: evaluateAccess([AccessControlAction.AlertingRuleRead]),
175172
component: importAlertingComponent(
176173
() =>
177174
import(

public/app/features/alerting/unified/components/AlertLabels.tsx

+25-12
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@ interface Props {
1515
labels: Record<string, string>;
1616
commonLabels?: Record<string, string>;
1717
size?: LabelSize;
18+
onClick?: (label: string, value: string) => void;
1819
}
1920

20-
export const AlertLabels = ({ labels, commonLabels = {}, size }: Props) => {
21+
export const AlertLabels = ({ labels, commonLabels = {}, size, onClick }: Props) => {
2122
const styles = useStyles2(getStyles, size);
2223
const [showCommonLabels, setShowCommonLabels] = useState(false);
2324

@@ -33,9 +34,19 @@ export const AlertLabels = ({ labels, commonLabels = {}, size }: Props) => {
3334

3435
return (
3536
<div className={styles.wrapper} role="list" aria-label="Labels">
36-
{labelsToShow.map(([label, value]) => (
37-
<Label key={label + value} size={size} label={label} value={value} color={getLabelColor(label)} />
38-
))}
37+
{labelsToShow.map(([label, value]) => {
38+
return (
39+
<Label
40+
key={label + value}
41+
size={size}
42+
label={label}
43+
value={value}
44+
color={getLabelColor(label)}
45+
onClick={onClick}
46+
/>
47+
);
48+
})}
49+
3950
{!showCommonLabels && hasCommonLabels && (
4051
<Button
4152
variant="secondary"
@@ -67,12 +78,14 @@ function getLabelColor(input: string): string {
6778
return getTagColorsFromName(input).color;
6879
}
6980

70-
const getStyles = (theme: GrafanaTheme2, size?: LabelSize) => ({
71-
wrapper: css({
72-
display: 'flex',
73-
flexWrap: 'wrap',
74-
alignItems: 'center',
81+
const getStyles = (theme: GrafanaTheme2, size?: LabelSize) => {
82+
return {
83+
wrapper: css({
84+
display: 'flex',
85+
flexWrap: 'wrap',
86+
alignItems: 'center',
7587

76-
gap: size === 'md' ? theme.spacing() : theme.spacing(0.5),
77-
}),
78-
});
88+
gap: size === 'md' ? theme.spacing() : theme.spacing(0.5),
89+
}),
90+
};
91+
};

public/app/features/alerting/unified/components/Label.tsx

+39-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { css } from '@emotion/css';
2-
import { CSSProperties, ReactNode } from 'react';
2+
import { CSSProperties, ReactNode, useMemo } from 'react';
33
import tinycolor2 from 'tinycolor2';
44

55
import { GrafanaTheme2, IconName } from '@grafana/data';
@@ -13,15 +13,18 @@ interface Props {
1313
value: ReactNode;
1414
color?: string;
1515
size?: LabelSize;
16+
onClick?: (label: string, value: string) => void;
1617
}
1718

1819
// TODO allow customization with color prop
19-
const Label = ({ label, value, icon, color, size = 'md' }: Props) => {
20+
const Label = ({ label, value, icon, color, size = 'md', onClick }: Props) => {
2021
const styles = useStyles2(getStyles, color, size);
2122
const ariaLabel = `${label}: ${value}`;
23+
const labelStr = label?.toString() ?? '';
24+
const valueStr = value?.toString() ?? '';
2225

23-
return (
24-
<div className={styles.wrapper} role="listitem" aria-label={ariaLabel} data-testid="label-value">
26+
const innerLabel = useMemo(
27+
() => (
2528
<Stack direction="row" gap={0} alignItems="stretch">
2629
<div className={styles.label}>
2730
<Stack direction="row" gap={0.5} alignItems="center">
@@ -37,6 +40,32 @@ const Label = ({ label, value, icon, color, size = 'md' }: Props) => {
3740
{value ?? '-'}
3841
</div>
3942
</Stack>
43+
),
44+
[icon, label, value, styles]
45+
);
46+
47+
return (
48+
<div className={styles.wrapper} role="listitem" aria-label={ariaLabel} data-testid="label-value">
49+
{onClick ? (
50+
<div
51+
className={styles.clickable}
52+
role="button" // role="button" and tabIndex={0} is needed for keyboard navigation
53+
tabIndex={0} // Make it focusable
54+
key={labelStr + valueStr}
55+
onClick={() => onClick(labelStr, valueStr)}
56+
onKeyDown={(e) => {
57+
// needed for accessiblity: handle keyboard navigation
58+
if (e.key === 'Enter') {
59+
onClick(labelStr, valueStr);
60+
e.preventDefault();
61+
}
62+
}}
63+
>
64+
{innerLabel}
65+
</div>
66+
) : (
67+
innerLabel
68+
)}
4069
</div>
4170
);
4271
};
@@ -94,6 +123,12 @@ const getStyles = (theme: GrafanaTheme2, color?: string, size?: string) => {
94123
borderTopLeftRadius: theme.shape.borderRadius(2),
95124
borderBottomLeftRadius: theme.shape.borderRadius(2),
96125
}),
126+
clickable: css({
127+
'&:hover': {
128+
opacity: 0.8,
129+
cursor: 'pointer',
130+
},
131+
}),
97132
value: css({
98133
color: 'inherit',
99134
padding: padding,

public/app/features/alerting/unified/components/rules/central-state-history/CentralAlertHistoryScene.tsx

+121-15
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { css } from '@emotion/css';
22

3+
import { GrafanaTheme2, VariableHide } from '@grafana/data';
34
import {
5+
CustomVariable,
46
EmbeddedScene,
57
PanelBuilders,
68
SceneControlsSpacer,
@@ -18,12 +20,14 @@ import {
1820
} from '@grafana/scenes';
1921
import { GraphDrawStyle, VisibilityMode } from '@grafana/schema/dist/esm/index';
2022
import {
23+
Button,
2124
GraphGradientMode,
2225
Icon,
2326
LegendDisplayMode,
2427
LineInterpolation,
2528
ScaleDistribution,
2629
StackingMode,
30+
Text,
2731
Tooltip,
2832
TooltipDisplayMode,
2933
useStyles2,
@@ -35,7 +39,9 @@ import { DataSourceInformation } from '../../../home/Insights';
3539
import { alertStateHistoryDatasource, useRegisterHistoryRuntimeDataSource } from './CentralHistoryRuntimeDataSource';
3640
import { HistoryEventsListObject } from './EventListSceneObject';
3741

38-
export const LABELS_FILTER = 'filter';
42+
export const LABELS_FILTER = 'labelsFilter';
43+
export const STATE_FILTER_TO = 'stateFilterTo';
44+
export const STATE_FILTER_FROM = 'stateFilterFrom';
3945
/**
4046
*
4147
* This scene shows the history of the alert state changes.
@@ -46,20 +52,56 @@ export const LABELS_FILTER = 'filter';
4652
* Both share time range and filter variable from the parent scene.
4753
*/
4854

55+
export const StateFilterValues = {
56+
all: 'all',
57+
firing: 'Alerting',
58+
normal: 'Normal',
59+
pending: 'Pending',
60+
} as const;
61+
4962
export const CentralAlertHistoryScene = () => {
50-
const filterVariable = new TextBoxVariable({
63+
// create the variables for the filters
64+
// textbox variable for filtering by labels
65+
const labelsFilterVariable = new TextBoxVariable({
5166
name: LABELS_FILTER,
52-
label: 'Filter by labels: ',
67+
label: 'Labels: ',
68+
});
69+
//custom variable for filtering by the current state
70+
const transitionsToFilterVariable = new CustomVariable({
71+
name: STATE_FILTER_TO,
72+
value: StateFilterValues.all,
73+
label: 'End state:',
74+
hide: VariableHide.dontHide,
75+
query: `All : ${StateFilterValues.all}, To Firing : ${StateFilterValues.firing},To Normal : ${StateFilterValues.normal},To Pending : ${StateFilterValues.pending}`,
76+
});
77+
//custom variable for filtering by the previous state
78+
const transitionsFromFilterVariable = new CustomVariable({
79+
name: STATE_FILTER_FROM,
80+
value: StateFilterValues.all,
81+
label: 'Start state:',
82+
hide: VariableHide.dontHide,
83+
query: `All : ${StateFilterValues.all}, From Firing : ${StateFilterValues.firing},From Normal : ${StateFilterValues.normal},From Pending : ${StateFilterValues.pending}`,
5384
});
5485

5586
useRegisterHistoryRuntimeDataSource(); // register the runtime datasource for the history api.
5687

5788
const scene = new EmbeddedScene({
5889
controls: [
90+
new SceneReactObject({
91+
component: LabelFilter,
92+
}),
5993
new SceneReactObject({
6094
component: FilterInfo,
6195
}),
6296
new VariableValueSelectors({}),
97+
new SceneReactObject({
98+
component: ClearFilterButton,
99+
props: {
100+
labelsFilterVariable,
101+
transitionsToFilterVariable,
102+
transitionsFromFilterVariable,
103+
},
104+
}),
63105
new SceneControlsSpacer(),
64106
new SceneTimePicker({}),
65107
new SceneRefreshPicker({}),
@@ -71,7 +113,7 @@ export const CentralAlertHistoryScene = () => {
71113
to: 'now',
72114
}),
73115
$variables: new SceneVariableSet({
74-
variables: [filterVariable],
116+
variables: [labelsFilterVariable, transitionsFromFilterVariable, transitionsToFilterVariable],
75117
}),
76118
body: new SceneFlexLayout({
77119
direction: 'column',
@@ -144,8 +186,10 @@ export function getEventsScenesFlexItem(datasource: DataSourceInformation) {
144186
return new SceneFlexItem({
145187
minHeight: 300,
146188
body: PanelBuilders.timeseries()
147-
.setTitle('Events')
148-
.setDescription('Alert events during the period of time.')
189+
.setTitle('Alert Events')
190+
.setDescription(
191+
'Each alert event represents an alert instance that changed its state at a particular point in time. The history of the data is displayed over a period of time.'
192+
)
149193
.setData(getSceneQuery(datasource))
150194
.setColor({ mode: 'continuous-BlPu' })
151195
.setCustomFieldConfig('fillOpacity', 100)
@@ -167,11 +211,66 @@ export function getEventsScenesFlexItem(datasource: DataSourceInformation) {
167211
.build(),
168212
});
169213
}
214+
/*
215+
* This component shows a button to clear the filters.
216+
* It is shown when the filters are active.
217+
* props:
218+
* labelsFilterVariable: the textbox variable for filtering by labels
219+
* transitionsToFilterVariable: the custom variable for filtering by the current state
220+
* transitionsFromFilterVariable: the custom variable for filtering by the previous state
221+
*/
170222

171-
export const FilterInfo = () => {
223+
function ClearFilterButton({
224+
labelsFilterVariable,
225+
transitionsToFilterVariable,
226+
transitionsFromFilterVariable,
227+
}: {
228+
labelsFilterVariable: TextBoxVariable;
229+
transitionsToFilterVariable: CustomVariable;
230+
transitionsFromFilterVariable: CustomVariable;
231+
}) {
232+
// get the current values of the filters
233+
const valueInLabelsFilter = labelsFilterVariable.getValue();
234+
//todo: use parsePromQLStyleMatcherLooseSafe to validate the label filter and check the lenghtof the result
235+
const valueInTransitionsFilter = transitionsToFilterVariable.getValue();
236+
const valueInTransitionsFromFilter = transitionsFromFilterVariable.getValue();
237+
// if no filter is active, return null
238+
if (
239+
!valueInLabelsFilter &&
240+
valueInTransitionsFilter === StateFilterValues.all &&
241+
valueInTransitionsFromFilter === StateFilterValues.all
242+
) {
243+
return null;
244+
}
245+
const onClearFilter = () => {
246+
labelsFilterVariable.setValue('');
247+
transitionsToFilterVariable.changeValueTo(StateFilterValues.all);
248+
transitionsFromFilterVariable.changeValueTo(StateFilterValues.all);
249+
};
250+
return (
251+
<Tooltip content="Clear filter">
252+
<Button variant={'secondary'} icon="times" onClick={onClearFilter}>
253+
<Trans i18nKey="alerting.central-alert-history.filter.clear">Clear filters</Trans>
254+
</Button>
255+
</Tooltip>
256+
);
257+
}
258+
259+
const LabelFilter = () => {
172260
const styles = useStyles2(getStyles);
173261
return (
174-
<div className={styles.container}>
262+
<div className={styles.filterLabelContainer}>
263+
<Text variant="body" weight="light" color="secondary">
264+
<Trans i18nKey="alerting.central-alert-history.filterBy">Filter by:</Trans>
265+
</Text>
266+
</div>
267+
);
268+
};
269+
270+
const FilterInfo = () => {
271+
const styles = useStyles2(getStyles);
272+
return (
273+
<div className={styles.filterInfoContainer}>
175274
<Tooltip
176275
content={
177276
<div>
@@ -180,7 +279,7 @@ export const FilterInfo = () => {
180279
</Trans>
181280
<pre>{`{severity="critical", instance=~"cluster-us-.+"}`}</pre>
182281
<Trans i18nKey="alerting.central-alert-history.filter.info.label2">Invalid use of spaces:</Trans>
183-
<pre>{`{severity= "alerting.critical"}`}</pre>
282+
<pre>{`{severity= "critical"}`}</pre>
184283
<pre>{`{severity ="critical"}`}</pre>
185284
<Trans i18nKey="alerting.central-alert-history.filter.info.label3">Valid use of spaces:</Trans>
186285
<pre>{`{severity=" critical"}`}</pre>
@@ -197,9 +296,16 @@ export const FilterInfo = () => {
197296
);
198297
};
199298

200-
const getStyles = () => ({
201-
container: css({
202-
padding: '0',
203-
alignSelf: 'center',
204-
}),
205-
});
299+
const getStyles = (theme: GrafanaTheme2) => {
300+
return {
301+
filterInfoContainer: css({
302+
padding: '0',
303+
alignSelf: 'center',
304+
marginRight: theme.spacing(-1),
305+
}),
306+
filterLabelContainer: css({
307+
padding: '0',
308+
alignSelf: 'center',
309+
}),
310+
};
311+
};

0 commit comments

Comments
 (0)