Skip to content

Commit e61c666

Browse files
authored
Open/Closed filter for observability alerts page (#99217)
1 parent 5dc85c6 commit e61c666

File tree

16 files changed

+274
-25
lines changed

16 files changed

+274
-25
lines changed

src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,7 @@ kibana_vars=(
276276
xpack.reporting.roles.allow
277277
xpack.reporting.roles.enabled
278278
xpack.rollup.enabled
279-
xpack.ruleRegistry.unsafe.write.enabled
279+
xpack.ruleRegistry.write.enabled
280280
xpack.searchprofiler.enabled
281281
xpack.security.audit.enabled
282282
xpack.security.audit.appender.type

x-pack/plugins/observability/README.md

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ This will only enable the UI for these pages. In order to have alert data indexe
1919
you'll need to enable writing in the [Rule Registry plugin](../rule_registry/README.md):
2020
2121
```yaml
22-
xpack.ruleRegistry.unsafe.write.enabled: true
22+
xpack.ruleRegistry.write.enabled: true
2323
```
2424
2525
When both of the these are set to `true`, your alerts should show on the alerts page.
@@ -47,3 +47,41 @@ HTML coverage report can be found in target/coverage/jest after tests have run.
4747
```bash
4848
open target/coverage/jest/index.html
4949
```
50+
51+
## API integration testing
52+
53+
API tests are separated in two suites:
54+
55+
- a basic license test suite
56+
- a trial license test suite (the equivalent of gold+)
57+
58+
This requires separate test servers and test runners.
59+
60+
### Basic
61+
62+
```
63+
# Start server
64+
node scripts/functional_tests_server --config x-pack/test/observability_api_integration/basic/config.ts
65+
66+
# Run tests
67+
node scripts/functional_test_runner --config x-pack/test/observability_api_integration/basic/config.ts
68+
```
69+
70+
The API tests for "basic" are located in `x-pack/test/observability_api_integration/basic/tests`.
71+
72+
### Trial
73+
74+
```
75+
# Start server
76+
node scripts/functional_tests_server --config x-pack/test/observability_api_integration/trial/config.ts
77+
78+
# Run tests
79+
node scripts/functional_test_runner --config x-pack/test/observability_api_integration/trial/config.ts
80+
```
81+
82+
The API tests for "trial" are located in `x-pack/test/observability_api_integration/trial/tests`.
83+
84+
### API test tips
85+
86+
- For debugging access Elasticsearch on http://localhost:9220` (elastic/changeme)
87+
- To update snapshots append `--updateSnapshots` to the functional_test_runner command

x-pack/plugins/observability/common/typings.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,9 @@
44
* 2.0; you may not use this file except in compliance with the Elastic License
55
* 2.0.
66
*/
7+
import * as t from 'io-ts';
78

89
export type Maybe<T> = T | null | undefined;
10+
11+
export const alertStatusRt = t.union([t.literal('all'), t.literal('open'), t.literal('closed')]);
12+
export type AlertStatus = t.TypeOf<typeof alertStatusRt>;

x-pack/plugins/observability/public/pages/alerts/alerts_table.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,16 +66,16 @@ export function AlertsTable(props: AlertsTableProps) {
6666

6767
return active ? (
6868
<EuiIconTip
69-
content={i18n.translate('xpack.observability.alertsTable.statusActiveDescription', {
70-
defaultMessage: 'Active',
69+
content={i18n.translate('xpack.observability.alertsTable.statusOpenDescription', {
70+
defaultMessage: 'Open',
7171
})}
7272
color="danger"
7373
type="alert"
7474
/>
7575
) : (
7676
<EuiIconTip
77-
content={i18n.translate('xpack.observability.alertsTable.statusRecoveredDescription', {
78-
defaultMessage: 'Recovered',
77+
content={i18n.translate('xpack.observability.alertsTable.statusClosedDescription', {
78+
defaultMessage: 'Closed',
7979
})}
8080
type="check"
8181
/>

x-pack/plugins/observability/public/pages/alerts/index.tsx

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,23 @@ import {
1212
EuiFlexItem,
1313
EuiLink,
1414
EuiPageTemplate,
15+
EuiSpacer,
1516
} from '@elastic/eui';
1617
import { i18n } from '@kbn/i18n';
17-
import React from 'react';
18-
import { useHistory } from 'react-router-dom';
19-
import { format, parse } from 'url';
2018
import {
2119
ALERT_START,
22-
EVENT_ACTION,
20+
ALERT_STATUS,
2321
RULE_ID,
2422
RULE_NAME,
2523
} from '@kbn/rule-data-utils/target/technical_field_names';
24+
import React from 'react';
25+
import { useHistory } from 'react-router-dom';
26+
import { format, parse } from 'url';
2627
import {
2728
ParsedTechnicalFields,
2829
parseTechnicalFields,
2930
} from '../../../../rule_registry/common/parse_technical_fields';
31+
import type { AlertStatus } from '../../../common/typings';
3032
import { asDuration, asPercent } from '../../../common/utils/formatters';
3133
import { ExperimentalBadge } from '../../components/shared/experimental_badge';
3234
import { useFetcher } from '../../hooks/use_fetcher';
@@ -37,6 +39,7 @@ import type { ObservabilityAPIReturnType } from '../../services/call_observabili
3739
import { getAbsoluteDateRange } from '../../utils/date';
3840
import { AlertsSearchBar } from './alerts_search_bar';
3941
import { AlertsTable } from './alerts_table';
42+
import { StatusFilter } from './status_filter';
4043

4144
export type TopAlertResponse = ObservabilityAPIReturnType<'GET /api/observability/rules/alerts/top'>[number];
4245

@@ -57,7 +60,7 @@ export function AlertsPage({ routeParams }: AlertsPageProps) {
5760
const { prepend } = core.http.basePath;
5861
const history = useHistory();
5962
const {
60-
query: { rangeFrom = 'now-15m', rangeTo = 'now', kuery = '' },
63+
query: { rangeFrom = 'now-15m', rangeTo = 'now', kuery = '', status = 'open' },
6164
} = routeParams;
6265

6366
// In a future milestone we'll have a page dedicated to rule management in
@@ -81,6 +84,7 @@ export function AlertsPage({ routeParams }: AlertsPageProps) {
8184
start,
8285
end,
8386
kuery,
87+
status,
8488
},
8589
},
8690
}).then((alerts) => {
@@ -108,15 +112,24 @@ export function AlertsPage({ routeParams }: AlertsPageProps) {
108112
},
109113
})
110114
: undefined,
111-
active: parsedFields[EVENT_ACTION] !== 'close',
115+
active: parsedFields[ALERT_STATUS] !== 'closed',
112116
start: new Date(parsedFields[ALERT_START]!).getTime(),
113117
};
114118
});
115119
});
116120
},
117-
[kuery, observabilityRuleTypeRegistry, rangeFrom, rangeTo]
121+
[kuery, observabilityRuleTypeRegistry, rangeFrom, rangeTo, status]
118122
);
119123

124+
function setStatusFilter(value: AlertStatus) {
125+
const nextSearchParams = new URLSearchParams(history.location.search);
126+
nextSearchParams.set('status', value);
127+
history.push({
128+
...history.location,
129+
search: nextSearchParams.toString(),
130+
});
131+
}
132+
120133
return (
121134
<EuiPageTemplate
122135
pageHeader={{
@@ -179,9 +192,19 @@ export function AlertsPage({ routeParams }: AlertsPageProps) {
179192
}}
180193
/>
181194
</EuiFlexItem>
182-
<EuiFlexItem>
183-
<AlertsTable items={topAlerts ?? []} />
184-
</EuiFlexItem>
195+
<EuiSpacer size="s" />
196+
<EuiFlexGroup direction="column">
197+
<EuiFlexItem>
198+
<EuiFlexGroup justifyContent="flexEnd">
199+
<EuiFlexItem grow={false}>
200+
<StatusFilter status={status} onChange={setStatusFilter} />
201+
</EuiFlexItem>
202+
</EuiFlexGroup>
203+
</EuiFlexItem>
204+
<EuiFlexItem>
205+
<AlertsTable items={topAlerts ?? []} />
206+
</EuiFlexItem>
207+
</EuiFlexGroup>
185208
</EuiFlexGroup>
186209
</EuiPageTemplate>
187210
);
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import React, { ComponentProps, useState } from 'react';
9+
import type { AlertStatus } from '../../../common/typings';
10+
import { StatusFilter } from './status_filter';
11+
12+
type Args = ComponentProps<typeof StatusFilter>;
13+
14+
export default {
15+
title: 'app/Alerts/StatusFilter',
16+
component: StatusFilter,
17+
argTypes: {
18+
onChange: { action: 'change' },
19+
},
20+
};
21+
22+
export function Example({ onChange }: Args) {
23+
const [status, setStatus] = useState<AlertStatus>('open');
24+
25+
return (
26+
<StatusFilter
27+
status={status}
28+
onChange={(value) => {
29+
setStatus(value);
30+
onChange(value);
31+
}}
32+
/>
33+
);
34+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import { render } from '@testing-library/react';
9+
import React from 'react';
10+
import type { AlertStatus } from '../../../common/typings';
11+
import { StatusFilter } from './status_filter';
12+
13+
describe('StatusFilter', () => {
14+
describe('render', () => {
15+
it('renders', () => {
16+
const onChange = jest.fn();
17+
const status: AlertStatus = 'all';
18+
const props = { onChange, status };
19+
20+
expect(() => render(<StatusFilter {...props} />)).not.toThrowError();
21+
});
22+
23+
(['all', 'open', 'closed'] as AlertStatus[]).map((status) => {
24+
describe(`when clicking the ${status} button`, () => {
25+
it('calls the onChange callback with "${status}"', () => {
26+
const onChange = jest.fn();
27+
const props = { onChange, status };
28+
29+
const { getByTestId } = render(<StatusFilter {...props} />);
30+
const button = getByTestId(`StatusFilter ${status} button`);
31+
32+
button.click();
33+
34+
expect(onChange).toHaveBeenCalledWith(status);
35+
});
36+
});
37+
});
38+
});
39+
});
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import { EuiFilterButton, EuiFilterGroup } from '@elastic/eui';
9+
import { i18n } from '@kbn/i18n';
10+
import React from 'react';
11+
import type { AlertStatus } from '../../../common/typings';
12+
13+
export interface StatusFilterProps {
14+
status: AlertStatus;
15+
onChange: (value: AlertStatus) => void;
16+
}
17+
18+
export function StatusFilter({ status = 'open', onChange }: StatusFilterProps) {
19+
return (
20+
<EuiFilterGroup
21+
aria-label={i18n.translate('xpack.observability.alerts.statusFilterAriaLabel', {
22+
defaultMessage: 'Filter alerts by open and closed status',
23+
})}
24+
>
25+
<EuiFilterButton
26+
data-test-subj="StatusFilter open button"
27+
hasActiveFilters={status === 'open'}
28+
onClick={() => onChange('open')}
29+
withNext={true}
30+
>
31+
{i18n.translate('xpack.observability.alerts.statusFilter.openButtonLabel', {
32+
defaultMessage: 'Open',
33+
})}
34+
</EuiFilterButton>
35+
<EuiFilterButton
36+
data-test-subj="StatusFilter closed button"
37+
hasActiveFilters={status === 'closed'}
38+
onClick={() => onChange('closed')}
39+
withNext={true}
40+
>
41+
{i18n.translate('xpack.observability.alerts.statusFilter.closedButtonLabel', {
42+
defaultMessage: 'Closed',
43+
})}
44+
</EuiFilterButton>
45+
<EuiFilterButton
46+
data-test-subj="StatusFilter all button"
47+
hasActiveFilters={status === 'all'}
48+
onClick={() => onChange('all')}
49+
>
50+
{i18n.translate('xpack.observability.alerts.statusFilter.allButtonLabel', {
51+
defaultMessage: 'All',
52+
})}
53+
</EuiFilterButton>
54+
</EuiFilterGroup>
55+
);
56+
}

x-pack/plugins/observability/public/routes/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { jsonRt } from './json_rt';
1515
import { AlertsPage } from '../pages/alerts';
1616
import { CasesPage } from '../pages/cases';
1717
import { ExploratoryViewPage } from '../components/shared/exploratory_view';
18+
import { alertStatusRt } from '../../common/typings';
1819

1920
export type RouteParams<T extends keyof typeof routes> = DecodeParams<typeof routes[T]['params']>;
2021

@@ -105,6 +106,7 @@ export const routes = {
105106
rangeFrom: t.string,
106107
rangeTo: t.string,
107108
kuery: t.string,
109+
status: alertStatusRt,
108110
refreshPaused: jsonRt.pipe(t.boolean),
109111
refreshInterval: jsonRt.pipe(t.number),
110112
}),

x-pack/plugins/observability/server/lib/rules/get_top_alerts.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,26 +6,29 @@
66
*/
77
import { ALERT_UUID, TIMESTAMP } from '@kbn/rule-data-utils/target/technical_field_names';
88
import { RuleDataClient } from '../../../../rule_registry/server';
9-
import { kqlQuery, rangeQuery } from '../../utils/queries';
9+
import type { AlertStatus } from '../../../common/typings';
10+
import { kqlQuery, rangeQuery, alertStatusQuery } from '../../utils/queries';
1011

1112
export async function getTopAlerts({
1213
ruleDataClient,
1314
start,
1415
end,
1516
kuery,
1617
size,
18+
status,
1719
}: {
1820
ruleDataClient: RuleDataClient;
1921
start: number;
2022
end: number;
2123
kuery?: string;
2224
size: number;
25+
status: AlertStatus;
2326
}) {
2427
const response = await ruleDataClient.getReader().search({
2528
body: {
2629
query: {
2730
bool: {
28-
filter: [...rangeQuery(start, end), ...kqlQuery(kuery)],
31+
filter: [...rangeQuery(start, end), ...kqlQuery(kuery), ...alertStatusQuery(status)],
2932
},
3033
},
3134
fields: ['*'],

0 commit comments

Comments
 (0)