Skip to content

Commit

Permalink
[ML] Custom sorting by message level on Notifications page (#153462)
Browse files Browse the repository at this point in the history
## Summary

Part of #143034

Adds custom sorting for the notification level.

- For descending sorting: `error` -> `warning` -> `info`
- For ascending sorting: `info` -> `warning` -> `error`

<img width="1431" alt="image"
src="https://user-images.githubusercontent.com/5236598/227580024-1584d319-f045-461e-9451-42a02ce406b2.png">


### Checklist

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
  • Loading branch information
darnautov authored Mar 27, 2023
1 parent c77b23d commit 04ed17d
Show file tree
Hide file tree
Showing 4 changed files with 101 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { FormattedMessage } from '@kbn/i18n-react';
import {
EuiBadge,
EuiCallOut,
EuiInMemoryTable,
EuiBasicTable,
EuiSearchBar,
EuiSpacer,
IconColor,
Expand Down Expand Up @@ -294,6 +294,12 @@ export const NotificationsList: FC = () => {

const newNotificationsCount = Object.values(notificationsCounts).reduce((a, b) => a + b);

const itemsPerPage = useMemo(() => {
const fromIndex = pagination.pageIndex * pagination.pageSize;
const toIndex = fromIndex + pagination.pageSize;
return items.slice(fromIndex, toIndex);
}, [items, pagination]);

return (
<>
<SavedObjectsWarning onCloseFlyout={fetchNotifications} forceRefresh={isLoading} />
Expand Down Expand Up @@ -382,12 +388,12 @@ export const NotificationsList: FC = () => {
</>
) : null}

<EuiInMemoryTable<NotificationItem>
<EuiBasicTable<NotificationItem>
columns={columns}
hasActions={false}
isExpandable={false}
isSelectable={false}
items={items}
items={itemsPerPage}
itemId={'id'}
loading={isLoading}
rowProps={(item) => ({
Expand All @@ -397,7 +403,7 @@ export const NotificationsList: FC = () => {
onChange={onTableChange}
sorting={sorting}
data-test-subj={isLoading ? 'mlNotificationsTable loading' : 'mlNotificationsTable loaded'}
message={
noItemsMessage={
<FormattedMessage
id="xpack.ml.notifications.noItemsFoundMessage"
defaultMessage="No notifications found"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,18 +154,35 @@ export class NotificationsService {
sortField: keyof NotificationItem,
sortDirection: 'asc' | 'desc'
): (a: NotificationItem, b: NotificationItem) => number {
if (sortField === 'timestamp') {
if (sortDirection === 'asc') {
return (a, b) => a.timestamp - b.timestamp;
} else {
return (a, b) => b.timestamp - a.timestamp;
}
} else {
if (sortDirection === 'asc') {
return (a, b) => (a[sortField] ?? '').localeCompare(b[sortField]);
} else {
return (a, b) => (b[sortField] ?? '').localeCompare(a[sortField]);
}
switch (sortField) {
case 'timestamp':
if (sortDirection === 'asc') {
return (a, b) => a.timestamp - b.timestamp;
} else {
return (a, b) => b.timestamp - a.timestamp;
}
case 'level':
if (sortDirection === 'asc') {
const levelOrder: Record<NotificationSource['level'], number> = {
error: 0,
warning: 1,
info: 2,
};
return (a, b) => levelOrder[b.level] - levelOrder[a.level];
} else {
const levelOrder: Record<NotificationSource['level'], number> = {
error: 2,
warning: 1,
info: 0,
};
return (a, b) => levelOrder[b.level] - levelOrder[a.level];
}
default:
if (sortDirection === 'asc') {
return (a, b) => (a[sortField] ?? '').localeCompare(b[sortField]);
} else {
return (a, b) => (b[sortField] ?? '').localeCompare(a[sortField]);
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@
* 2.0.
*/

import expect from '@kbn/expect';
import moment from 'moment';
import { FtrProviderContext } from '../../../../ftr_provider_context';

const timepickerFormat = 'MMM D, YYYY @ HH:mm:ss.SSS';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const PageObjects = getPageObjects(['common']);
const PageObjects = getPageObjects(['common', 'timePicker']);
const esArchiver = getService('esArchiver');
const ml = getService('ml');
const browser = getService('browser');
Expand Down Expand Up @@ -58,6 +61,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {

await ml.notifications.table.waitForTableToLoad();
await ml.notifications.table.assertRowsNumberPerPage(25);
await ml.notifications.table.assertTableSorting('timestamp', 0, 'desc');
});

it('does not show notifications from another space', async () => {
Expand Down Expand Up @@ -92,5 +96,25 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await browser.refresh();
await ml.notifications.assertNotificationErrorsCount(0);
});

it('supports custom sorting for notifications level', async () => {
await ml.navigation.navigateToNotifications();
await ml.notifications.table.waitForTableToLoad();

await PageObjects.timePicker.pauseAutoRefresh();
const fromTime = moment().subtract(1, 'week').format(timepickerFormat);
const toTime = moment().format(timepickerFormat);
await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime);

await ml.notifications.table.waitForTableToLoad();

await ml.notifications.table.sortByField('level', 1, 'desc');
const rowsDesc = await ml.notifications.table.parseTable();
expect(rowsDesc[0].level).to.eql('error');

await ml.notifications.table.sortByField('level', 1, 'asc');
const rowsAsc = await ml.notifications.table.parseTable();
expect(rowsAsc[0].level).to.eql('info');
});
});
}
37 changes: 37 additions & 0 deletions x-pack/test/functional/services/ml/common_table_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export type MlTableService = ReturnType<typeof MlTableServiceProvider>;
export function MlTableServiceProvider({ getPageObject, getService }: FtrProviderContext) {
const testSubjects = getService('testSubjects');
const commonPage = getPageObject('common');
const retry = getService('retry');

const TableService = class {
constructor(
Expand Down Expand Up @@ -101,6 +102,42 @@ export function MlTableServiceProvider({ getPageObject, getService }: FtrProvide
`Filtered table should have ${expectedRowCount} row(s) for filter '${queryString}' (got ${rows.length} matching items)`
);
}

public async assertTableSorting(
columnName: string,
columnIndex: number,
expectedDirection: 'desc' | 'asc'
) {
const actualDirection = await this.getCurrentSorting();
expect(actualDirection?.direction).to.eql(expectedDirection);
expect(actualDirection?.columnName).to.eql(columnName);
}

public async getCurrentSorting(): Promise<
{ columnName: string; direction: string } | undefined
> {
const table = await testSubjects.find(`~${this.tableTestSubj}`);
const headers = await table.findAllByClassName('euiTableHeaderCell');
for (const header of headers) {
const ariaSort = await header.getAttribute('aria-sort');
if (ariaSort !== 'none') {
const columnNameFragments = (await header.getAttribute('data-test-subj')).split('_');
const columnName = columnNameFragments.slice(1, columnNameFragments.length - 1).join('_');
return { columnName, direction: ariaSort.replace('ending', '') };
}
}
}

public async sortByField(columnName: string, columnIndex: number, direction: 'desc' | 'asc') {
const testSubjString = `tableHeaderCell_${columnName}_${columnIndex}`;

await retry.tryForTime(5000, async () => {
await testSubjects.click(testSubjString);
await this.waitForTableToStartLoading();
await this.waitForTableToLoad();
await this.assertTableSorting(columnName, columnIndex, direction);
});
}
};

return {
Expand Down

0 comments on commit 04ed17d

Please sign in to comment.