diff --git a/x-pack/plugins/ml/public/application/notifications/components/notifications_list.tsx b/x-pack/plugins/ml/public/application/notifications/components/notifications_list.tsx index a2a677670e2ea..7b08bcec0331f 100644 --- a/x-pack/plugins/ml/public/application/notifications/components/notifications_list.tsx +++ b/x-pack/plugins/ml/public/application/notifications/components/notifications_list.tsx @@ -12,7 +12,7 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { EuiBadge, EuiCallOut, - EuiInMemoryTable, + EuiBasicTable, EuiSearchBar, EuiSpacer, IconColor, @@ -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 ( <> @@ -382,12 +388,12 @@ export const NotificationsList: FC = () => { ) : null} - + columns={columns} hasActions={false} isExpandable={false} isSelectable={false} - items={items} + items={itemsPerPage} itemId={'id'} loading={isLoading} rowProps={(item) => ({ @@ -397,7 +403,7 @@ export const NotificationsList: FC = () => { onChange={onTableChange} sorting={sorting} data-test-subj={isLoading ? 'mlNotificationsTable loading' : 'mlNotificationsTable loaded'} - message={ + noItemsMessage={ 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 = { + error: 0, + warning: 1, + info: 2, + }; + return (a, b) => levelOrder[b.level] - levelOrder[a.level]; + } else { + const levelOrder: Record = { + 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]); + } } } diff --git a/x-pack/test/functional/apps/ml/short_tests/notifications/notification_list.ts b/x-pack/test/functional/apps/ml/short_tests/notifications/notification_list.ts index 68a6090bd99e2..eb0f99c97e3c0 100644 --- a/x-pack/test/functional/apps/ml/short_tests/notifications/notification_list.ts +++ b/x-pack/test/functional/apps/ml/short_tests/notifications/notification_list.ts @@ -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'); @@ -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 () => { @@ -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'); + }); }); } diff --git a/x-pack/test/functional/services/ml/common_table_service.ts b/x-pack/test/functional/services/ml/common_table_service.ts index 50a40ab43f35a..063c6095c85b5 100644 --- a/x-pack/test/functional/services/ml/common_table_service.ts +++ b/x-pack/test/functional/services/ml/common_table_service.ts @@ -14,6 +14,7 @@ export type MlTableService = ReturnType; export function MlTableServiceProvider({ getPageObject, getService }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const commonPage = getPageObject('common'); + const retry = getService('retry'); const TableService = class { constructor( @@ -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 {