Make alerts list sortable by name and status (#93426) (#93959)

* Initial commit

* Update docs

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>

Co-authored-by: Mike Côté <mikecote@users.noreply.github.com>
This commit is contained in:
Kibana Machine 2021-03-08 13:27:27 -05:00 committed by GitHub
parent afb144d051
commit c4011f8b53
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 165 additions and 58 deletions

View file

@ -860,7 +860,7 @@
"description": [],
"source": {
"path": "x-pack/plugins/alerting/server/alerts_client/alerts_client.ts",
"lineNumber": 131
"lineNumber": 132
}
},
{
@ -871,7 +871,7 @@
"description": [],
"source": {
"path": "x-pack/plugins/alerting/server/alerts_client/alerts_client.ts",
"lineNumber": 132
"lineNumber": 133
}
},
{
@ -882,7 +882,7 @@
"description": [],
"source": {
"path": "x-pack/plugins/alerting/server/alerts_client/alerts_client.ts",
"lineNumber": 133
"lineNumber": 134
}
},
{
@ -893,7 +893,7 @@
"description": [],
"source": {
"path": "x-pack/plugins/alerting/server/alerts_client/alerts_client.ts",
"lineNumber": 134
"lineNumber": 135
},
"signature": [
"Pick<",
@ -910,7 +910,7 @@
],
"source": {
"path": "x-pack/plugins/alerting/server/alerts_client/alerts_client.ts",
"lineNumber": 130
"lineNumber": 131
},
"initialIsOpen": false
},

View file

@ -942,7 +942,7 @@
"description": [],
"source": {
"path": "x-pack/plugins/triggers_actions_ui/public/types.ts",
"lineNumber": 156
"lineNumber": 161
}
},
{
@ -953,7 +953,7 @@
"description": [],
"source": {
"path": "x-pack/plugins/triggers_actions_ui/public/types.ts",
"lineNumber": 157
"lineNumber": 162
}
},
{
@ -964,7 +964,7 @@
"description": [],
"source": {
"path": "x-pack/plugins/triggers_actions_ui/public/types.ts",
"lineNumber": 158
"lineNumber": 163
},
"signature": [
"boolean | undefined"
@ -973,7 +973,7 @@
],
"source": {
"path": "x-pack/plugins/triggers_actions_ui/public/types.ts",
"lineNumber": 155
"lineNumber": 160
},
"initialIsOpen": false
},
@ -1130,7 +1130,7 @@
"description": [],
"source": {
"path": "x-pack/plugins/triggers_actions_ui/public/types.ts",
"lineNumber": 221
"lineNumber": 227
}
},
{
@ -1141,7 +1141,7 @@
"description": [],
"source": {
"path": "x-pack/plugins/triggers_actions_ui/public/types.ts",
"lineNumber": 222
"lineNumber": 228
}
},
{
@ -1152,7 +1152,7 @@
"description": [],
"source": {
"path": "x-pack/plugins/triggers_actions_ui/public/types.ts",
"lineNumber": 223
"lineNumber": 229
}
},
{
@ -1163,7 +1163,7 @@
"description": [],
"source": {
"path": "x-pack/plugins/triggers_actions_ui/public/types.ts",
"lineNumber": 224
"lineNumber": 230
},
"signature": [
"string | ((docLinks: ",
@ -1185,7 +1185,7 @@
"description": [],
"source": {
"path": "x-pack/plugins/triggers_actions_ui/public/types.ts",
"lineNumber": 225
"lineNumber": 231
},
"signature": [
"(alertParams: Params) => ",
@ -1206,7 +1206,7 @@
"description": [],
"source": {
"path": "x-pack/plugins/triggers_actions_ui/public/types.ts",
"lineNumber": 226
"lineNumber": 232
},
"signature": [
"React.FunctionComponent<any> | React.LazyExoticComponent<React.ComponentType<",
@ -1228,7 +1228,7 @@
"description": [],
"source": {
"path": "x-pack/plugins/triggers_actions_ui/public/types.ts",
"lineNumber": 229
"lineNumber": 235
}
},
{
@ -1239,7 +1239,7 @@
"description": [],
"source": {
"path": "x-pack/plugins/triggers_actions_ui/public/types.ts",
"lineNumber": 230
"lineNumber": 236
},
"signature": [
"string | undefined"
@ -1248,7 +1248,7 @@
],
"source": {
"path": "x-pack/plugins/triggers_actions_ui/public/types.ts",
"lineNumber": 220
"lineNumber": 226
},
"initialIsOpen": false
},
@ -1277,7 +1277,7 @@
"description": [],
"source": {
"path": "x-pack/plugins/triggers_actions_ui/public/types.ts",
"lineNumber": 203
"lineNumber": 209
},
"signature": [
"Params"
@ -1291,7 +1291,7 @@
"description": [],
"source": {
"path": "x-pack/plugins/triggers_actions_ui/public/types.ts",
"lineNumber": 204
"lineNumber": 210
}
},
{
@ -1302,7 +1302,7 @@
"description": [],
"source": {
"path": "x-pack/plugins/triggers_actions_ui/public/types.ts",
"lineNumber": 205
"lineNumber": 211
}
},
{
@ -1313,7 +1313,7 @@
"description": [],
"source": {
"path": "x-pack/plugins/triggers_actions_ui/public/types.ts",
"lineNumber": 206
"lineNumber": 212
},
"signature": [
"\"onActionGroupChange\" | \"onActiveAlert\" | \"onThrottleInterval\""
@ -1327,7 +1327,7 @@
"description": [],
"source": {
"path": "x-pack/plugins/triggers_actions_ui/public/types.ts",
"lineNumber": 207
"lineNumber": 213
},
"signature": [
"<Key extends keyof Params>(property: Key, value: Params[Key] | undefined) => void"
@ -1341,7 +1341,7 @@
"description": [],
"source": {
"path": "x-pack/plugins/triggers_actions_ui/public/types.ts",
"lineNumber": 208
"lineNumber": 214
},
"signature": [
"<Prop extends \"enabled\" | \"id\" | \"name\" | \"params\" | \"actions\" | \"muteAll\" | \"tags\" | \"alertTypeId\" | \"consumer\" | \"schedule\" | \"scheduledTaskId\" | \"createdBy\" | \"updatedBy\" | \"createdAt\" | \"updatedAt\" | \"apiKeyOwner\" | \"throttle\" | \"notifyWhen\" | \"mutedInstanceIds\" | \"executionStatus\">(key: Prop, value: Pick<",
@ -1363,7 +1363,7 @@
"description": [],
"source": {
"path": "x-pack/plugins/triggers_actions_ui/public/types.ts",
"lineNumber": 212
"lineNumber": 218
},
"signature": [
{
@ -1383,7 +1383,7 @@
"description": [],
"source": {
"path": "x-pack/plugins/triggers_actions_ui/public/types.ts",
"lineNumber": 213
"lineNumber": 219
}
},
{
@ -1394,7 +1394,7 @@
"description": [],
"source": {
"path": "x-pack/plugins/triggers_actions_ui/public/types.ts",
"lineNumber": 214
"lineNumber": 220
},
"signature": [
{
@ -1415,7 +1415,7 @@
"description": [],
"source": {
"path": "x-pack/plugins/triggers_actions_ui/public/types.ts",
"lineNumber": 215
"lineNumber": 221
},
"signature": [
"MetaData | undefined"
@ -1429,7 +1429,7 @@
"description": [],
"source": {
"path": "x-pack/plugins/triggers_actions_ui/public/types.ts",
"lineNumber": 216
"lineNumber": 222
},
"signature": [
{
@ -1449,7 +1449,7 @@
"description": [],
"source": {
"path": "x-pack/plugins/triggers_actions_ui/public/types.ts",
"lineNumber": 217
"lineNumber": 223
},
"signature": [
{
@ -1464,7 +1464,7 @@
],
"source": {
"path": "x-pack/plugins/triggers_actions_ui/public/types.ts",
"lineNumber": 198
"lineNumber": 204
},
"initialIsOpen": false
},
@ -1591,7 +1591,7 @@
"description": [],
"source": {
"path": "x-pack/plugins/triggers_actions_ui/public/types.ts",
"lineNumber": 234
"lineNumber": 240
},
"signature": [
"any"
@ -1600,7 +1600,7 @@
],
"source": {
"path": "x-pack/plugins/triggers_actions_ui/public/types.ts",
"lineNumber": 233
"lineNumber": 239
},
"initialIsOpen": false
},
@ -1924,7 +1924,7 @@
"description": [],
"source": {
"path": "x-pack/plugins/triggers_actions_ui/public/types.ts",
"lineNumber": 110
"lineNumber": 115
},
"signature": [
"Record<string, any>"
@ -1933,7 +1933,7 @@
],
"source": {
"path": "x-pack/plugins/triggers_actions_ui/public/types.ts",
"lineNumber": 109
"lineNumber": 114
},
"initialIsOpen": false
}
@ -1997,7 +1997,7 @@
"description": [],
"source": {
"path": "x-pack/plugins/triggers_actions_ui/public/types.ts",
"lineNumber": 142
"lineNumber": 147
},
"signature": [
"PreConfiguredActionConnector",
@ -2060,7 +2060,7 @@
"description": [],
"source": {
"path": "x-pack/plugins/triggers_actions_ui/public/types.ts",
"lineNumber": 166
"lineNumber": 171
},
"signature": [
"AsActionVariables<\"params\" | \"state\"> & Partial<AsActionVariables<\"context\">>"

View file

@ -57,6 +57,7 @@ import { partiallyUpdateAlert } from '../saved_objects';
import { markApiKeyForInvalidation } from '../invalidate_pending_api_keys/mark_api_key_for_invalidation';
import { alertAuditEvent, AlertAuditAction } from './audit_events';
import { nodeBuilder } from '../../../../../src/plugins/data/common';
import { mapSortField } from './lib';
export interface RegistryAlertTypeWithAuth extends RegistryAlertType {
authorizedConsumers: string[];
@ -465,6 +466,7 @@ export class AlertsClient {
saved_objects: data,
} = await this.unsecuredSavedObjectsClient.find<RawAlert>({
...options,
sortField: mapSortField(options.sortField),
filter:
(authorizationFilter && options.filter
? nodeBuilder.and([esKuery.fromKueryExpression(options.filter), authorizationFilter])

View file

@ -0,0 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export { mapSortField } from './map_sort_field';

View file

@ -0,0 +1,22 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { mapSortField } from './map_sort_field';
describe('mapSortField()', () => {
test('should return undefined when given undefined', () => {
expect(mapSortField(undefined)).toStrictEqual(undefined);
});
test('should return a mapped value when a mapping exists', () => {
expect(mapSortField('name')).toEqual('name.keyword');
});
test(`should return field when a mapping doesn't exist`, () => {
expect(mapSortField('tags')).toEqual('tags');
});
});

View file

@ -0,0 +1,14 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
const sortFieldMap: Record<string, string> = {
name: 'name.keyword',
};
export function mapSortField(field?: string): string | undefined {
return field ? sortFieldMap[field] || field : undefined;
}

View file

@ -55,6 +55,10 @@ beforeEach(() => {
setGlobalDate();
jest.mock('../lib/map_sort_field', () => ({
mapSortField: jest.fn(),
}));
describe('find()', () => {
const listedTypes = new Set<RegistryAlertType>([
{
@ -172,12 +176,19 @@ describe('find()', () => {
Object {
"fields": undefined,
"filter": undefined,
"sortField": undefined,
"type": "alert",
},
]
`);
});
test('calls mapSortField', async () => {
const alertsClient = new AlertsClient(alertsClientParams);
await alertsClient.find({ options: { sortField: 'name' } });
expect(jest.requireMock('../lib/map_sort_field').mapSortField).toHaveBeenCalledWith('name');
});
describe('authorization', () => {
test('ensures user is query filter types down to those the user is authorized to find', async () => {
const filter = esKuery.fromKueryExpression(

View file

@ -22653,7 +22653,7 @@
"xpack.triggersActionsUI.sections.alertsList.alertErrorReasonRunning": "アラートの実行中にエラーが発生しました。",
"xpack.triggersActionsUI.sections.alertsList.alertErrorReasonUnknown": "不明な理由でエラーが発生しました。",
"xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.actionsTex": "アクション",
"xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.actionsText": "アクション",
"xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.actionsCount": "アクション",
"xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.alertTypeTitle": "型",
"xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.intervalTitle": "次の間隔で実行",
"xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.nameTitle": "名前",

View file

@ -23004,7 +23004,7 @@
"xpack.triggersActionsUI.sections.alertsList.alertErrorReasonRunning": "运行告警时发生错误。",
"xpack.triggersActionsUI.sections.alertsList.alertErrorReasonUnknown": "由于未知原因发生错误。",
"xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.actionsTex": "操作",
"xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.actionsText": "操作",
"xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.actionsCount": "操作",
"xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.alertTypeTitle": "类型",
"xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.intervalTitle": "运行间隔",
"xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.nameTitle": "名称",

View file

@ -180,7 +180,7 @@ describe('loadAlerts', () => {
"per_page": 10,
"search": undefined,
"search_fields": undefined,
"sort_field": "name.keyword",
"sort_field": "name",
"sort_order": "asc",
},
},
@ -210,7 +210,7 @@ describe('loadAlerts', () => {
"per_page": 10,
"search": "apples",
"search_fields": "[\\"name\\",\\"tags\\"]",
"sort_field": "name.keyword",
"sort_field": "name",
"sort_order": "asc",
},
},
@ -244,7 +244,7 @@ describe('loadAlerts', () => {
"per_page": 10,
"search": "foo",
"search_fields": "[\\"name\\",\\"tags\\"]",
"sort_field": "name.keyword",
"sort_field": "name",
"sort_order": "asc",
},
},
@ -278,7 +278,7 @@ describe('loadAlerts', () => {
"per_page": 10,
"search": undefined,
"search_fields": undefined,
"sort_field": "name.keyword",
"sort_field": "name",
"sort_order": "asc",
},
},
@ -313,7 +313,7 @@ describe('loadAlerts', () => {
"per_page": 10,
"search": "baz",
"search_fields": "[\\"name\\",\\"tags\\"]",
"sort_field": "name.keyword",
"sort_field": "name",
"sort_order": "asc",
},
},
@ -348,7 +348,7 @@ describe('loadAlerts', () => {
"per_page": 10,
"search": "apples, foo, baz",
"search_fields": "[\\"name\\",\\"tags\\"]",
"sort_field": "name.keyword",
"sort_field": "name",
"sort_order": "asc",
},
},

View file

@ -19,6 +19,8 @@ import {
AlertUpdates,
AlertTaskState,
AlertInstanceSummary,
Pagination,
Sorting,
} from '../../types';
export async function loadAlertTypes({ http }: { http: HttpSetup }): Promise<AlertType[]> {
@ -103,13 +105,15 @@ export async function loadAlerts({
typesFilter,
actionTypesFilter,
alertStatusesFilter,
sort = { field: 'name', direction: 'asc' },
}: {
http: HttpSetup;
page: { index: number; size: number };
page: Pagination;
searchText?: string;
typesFilter?: string[];
actionTypesFilter?: string[];
alertStatusesFilter?: string[];
sort?: Sorting;
}): Promise<{
page: number;
perPage: number;
@ -125,8 +129,8 @@ export async function loadAlerts({
search: searchText,
filter: filters.length ? filters.join(' and ') : undefined,
default_search_operator: 'AND',
sort_field: 'name.keyword',
sort_order: 'asc',
sort_field: sort.field,
sort_order: sort.direction,
},
});
}

View file

@ -363,6 +363,28 @@ describe('alerts_list component with items', () => {
wrapper.find('EuiButton[data-test-subj="confirmModalConfirmButton"]').simulate('click');
expect(global.open).toHaveBeenCalled();
});
it('sorts alerts when clicking the name column', async () => {
await setup();
wrapper
.find('[data-test-subj="tableHeaderCell_name_0"] .euiTableHeaderButton')
.first()
.simulate('click');
await act(async () => {
await nextTick();
wrapper.update();
});
expect(loadAlerts).toHaveBeenCalledWith(
expect.objectContaining({
sort: {
field: 'name',
direction: 'desc',
},
})
);
});
});
describe('alerts_list component empty with show only capability', () => {

View file

@ -26,6 +26,7 @@ import {
EuiHealth,
EuiText,
EuiToolTip,
EuiTableSortingType,
} from '@elastic/eui';
import { useHistory } from 'react-router-dom';
@ -99,6 +100,10 @@ export const AlertsList: React.FunctionComponent = () => {
const [alertStatusesFilter, setAlertStatusesFilter] = useState<string[]>([]);
const [alertFlyoutVisible, setAlertFlyoutVisibility] = useState<boolean>(false);
const [dismissAlertErrors, setDismissAlertErrors] = useState<boolean>(false);
const [sort, setSort] = useState<EuiTableSortingType<AlertTableItem>['sort']>({
field: 'name',
direction: 'asc',
});
const [manageLicenseModalOpts, setManageLicenseModalOpts] = useState<{
licenseType: string;
alertTypeId: string;
@ -194,6 +199,7 @@ export const AlertsList: React.FunctionComponent = () => {
typesFilter,
actionTypesFilter,
alertStatusesFilter,
sort,
});
await loadAlertAggs();
setAlertsState({
@ -307,7 +313,7 @@ export const AlertsList: React.FunctionComponent = () => {
'xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.nameTitle',
{ defaultMessage: 'Name' }
),
sortable: false,
sortable: true,
truncateText: true,
width: '35%',
'data-test-subj': 'alertsTableCell-name',
@ -325,17 +331,17 @@ export const AlertsList: React.FunctionComponent = () => {
},
},
{
field: 'executionStatus',
field: 'executionStatus.status',
name: i18n.translate(
'xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.statusTitle',
{ defaultMessage: 'Status' }
),
sortable: false,
sortable: true,
truncateText: false,
width: '150px',
'data-test-subj': 'alertsTableCell-status',
render: (executionStatus: AlertExecutionStatus, item: AlertTableItem) => {
return renderAlertExecutionStatus(executionStatus, item);
return renderAlertExecutionStatus(item.executionStatus, item);
},
},
{
@ -348,9 +354,9 @@ export const AlertsList: React.FunctionComponent = () => {
'data-test-subj': 'alertsTableCell-tagsText',
},
{
field: 'actionsText',
field: 'actionsCount',
name: i18n.translate(
'xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.actionsText',
'xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.actionsCount',
{ defaultMessage: 'Actions' }
),
render: (count: number, item: AlertTableItem) => {
@ -361,7 +367,7 @@ export const AlertsList: React.FunctionComponent = () => {
);
},
sortable: false,
'data-test-subj': 'alertsTableCell-actionsText',
'data-test-subj': 'alertsTableCell-actionsCount',
},
{
field: 'alertType',
@ -655,6 +661,7 @@ export const AlertsList: React.FunctionComponent = () => {
}
itemId="id"
columns={alertsTableColumns}
sorting={{ sort }}
rowProps={(item: AlertTableItem) => ({
'data-test-subj': 'alert-row',
className: !alertTypesState.data.get(item.alertTypeId)?.enabledInLicense
@ -680,8 +687,19 @@ export const AlertsList: React.FunctionComponent = () => {
setSelectedIds(updatedSelectedItemsList.map((item) => item.id));
},
}}
onChange={({ page: changedPage }: { page: Pagination }) => {
setPage(changedPage);
onChange={({
page: changedPage,
sort: changedSort,
}: {
page?: Pagination;
sort?: EuiTableSortingType<AlertTableItem>['sort'];
}) => {
if (changedPage) {
setPage(changedPage);
}
if (changedSort) {
setSort(changedSort);
}
}}
/>
{manageLicenseModalOpts && (
@ -797,7 +815,7 @@ function convertAlertsToTableItems(
) {
return alerts.map((alert) => ({
...alert,
actionsText: alert.actions.length,
actionsCount: alert.actions.length,
tagsText: alert.tags.join(', '),
alertType: alertTypesIndex.get(alert.alertTypeId)?.name ?? alert.alertTypeId,
isEditable:

View file

@ -83,6 +83,11 @@ export interface Pagination {
size: number;
}
export interface Sorting {
field: string;
direction: string;
}
export interface ActionTypeModel<ActionConfig = any, ActionSecrets = any, ActionParams = any> {
id: string;
iconClass: string;
@ -191,6 +196,7 @@ export type AlertUpdates = Omit<Alert, 'id' | 'executionStatus'>;
export interface AlertTableItem extends Alert {
alertType: AlertType['name'];
tagsText: string;
actionsCount: number;
isEditable: boolean;
enabledInLicense: boolean;
}