[RAM] Add the "updated at" feature in new alerts table (#136949)

* first commit

* fix and add test

* Update x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/toolbar/components/last_updated_at/translations.ts

Co-authored-by: Xavier Mouligneau <xavier.mouligneau@elastic.co>

* Update x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/toolbar/components/last_updated_at/translations.ts

Co-authored-by: Xavier Mouligneau <xavier.mouligneau@elastic.co>

Co-authored-by: Xavier Mouligneau <xavier.mouligneau@elastic.co>
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Julian Gernun 2022-07-25 10:38:03 +02:00 committed by GitHub
parent 1377ef2b33
commit 8b21e25ecf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 181 additions and 25 deletions

View file

@ -11,8 +11,9 @@ import userEvent from '@testing-library/user-event';
import { EcsFieldsResponse } from '@kbn/rule-registry-plugin/common/search_strategy';
import { AlertsTable } from './alerts_table';
import { AlertsField } from '../../../types';
import { AlertsField, AlertsTableProps } from '../../../types';
import { EuiButtonIcon, EuiFlexItem } from '@elastic/eui';
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
jest.mock('@kbn/data-plugin/public');
@ -88,11 +89,18 @@ describe('AlertsTable', () => {
useFetchAlertsData,
visibleColumns: columns.map((c) => c.id),
'data-test-subj': 'testTable',
updatedAt: Date.now(),
};
const AlertsTableWithLocale: React.FunctionComponent<AlertsTableProps> = (props) => (
<IntlProvider locale="en">
<AlertsTable {...props} />
</IntlProvider>
);
describe('Alerts table UI', () => {
it('should support sorting', async () => {
const renderResult = render(<AlertsTable {...tableProps} />);
const renderResult = render(<AlertsTableWithLocale {...tableProps} />);
userEvent.click(renderResult.container.querySelector('.euiDataGridHeaderCell__button')!);
userEvent.click(renderResult.getByTestId(`dataGridHeaderCellActionGroup-${columns[0].id}`));
userEvent.click(renderResult.getByTitle('Sort A-Z'));
@ -102,14 +110,19 @@ describe('AlertsTable', () => {
});
it('should support pagination', async () => {
const renderResult = render(<AlertsTable {...tableProps} />);
const renderResult = render(<AlertsTableWithLocale {...tableProps} />);
userEvent.click(renderResult.getByTestId('pagination-button-1'));
expect(fetchAlertsData.onPageChange).toHaveBeenCalledWith({ pageIndex: 1, pageSize: 1 });
});
it('should show when it was updated', () => {
const { getByTestId } = render(<AlertsTableWithLocale {...tableProps} />);
expect(getByTestId('toolbar-updated-at')).not.toBe(null);
});
describe('leading control columns', () => {
it('should return at least the flyout action control', async () => {
const wrapper = render(<AlertsTable {...tableProps} />);
const wrapper = render(<AlertsTableWithLocale {...tableProps} />);
expect(wrapper.getByTestId('expandColumnHeaderLabel').textContent).toBe('Actions');
});
@ -125,7 +138,7 @@ describe('AlertsTable', () => {
},
],
};
const wrapper = render(<AlertsTable {...customTableProps} />);
const wrapper = render(<AlertsTableWithLocale {...customTableProps} />);
expect(wrapper.queryByTestId('testHeader')).not.toBe(null);
expect(wrapper.queryByTestId('testCell')).not.toBe(null);
});
@ -168,7 +181,7 @@ describe('AlertsTable', () => {
},
};
const { queryByTestId } = render(<AlertsTable {...customTableProps} />);
const { queryByTestId } = render(<AlertsTableWithLocale {...customTableProps} />);
expect(queryByTestId('testActionColumn')).not.toBe(null);
expect(queryByTestId('testActionColumn2')).not.toBe(null);
expect(queryByTestId('expandColumnCellOpenFlyoutButton-0')).not.toBe(null);
@ -211,7 +224,7 @@ describe('AlertsTable', () => {
},
};
const { queryByTestId } = render(<AlertsTable {...customTableProps} />);
const { queryByTestId } = render(<AlertsTableWithLocale {...customTableProps} />);
expect(queryByTestId('testActionColumn')).not.toBe(null);
expect(queryByTestId('testActionColumn2')).not.toBe(null);
expect(queryByTestId('expandColumnCellOpenFlyoutButton-0')).toBe(null);
@ -223,7 +236,7 @@ describe('AlertsTable', () => {
showExpandToDetails: false,
};
const { queryByTestId } = render(<AlertsTable {...customTableProps} />);
const { queryByTestId } = render(<AlertsTableWithLocale {...customTableProps} />);
expect(queryByTestId('expandColumnHeaderLabel')).toBe(null);
expect(queryByTestId('expandColumnCellOpenFlyoutButton')).toBe(null);
});

View file

@ -72,8 +72,10 @@ const AlertsTable: React.FunctionComponent<AlertsTableProps> = (props: AlertsTab
alertsCount,
rowSelection,
alerts: alertsData.alerts,
updatedAt: props.updatedAt,
isLoading,
});
}, [bulkActionsState, bulkActions, alertsCount, alertsData.alerts])();
}, [bulkActionsState, bulkActions, alertsCount, alertsData.alerts, props.updatedAt, isLoading])();
const {
pagination,

View file

@ -19,9 +19,10 @@ import {
} from '../../../types';
import { PLUGIN_ID } from '../../../common/constants';
import { TypeRegistry } from '../../type_registry';
import AlertsTableState from './alerts_table_state';
import AlertsTableState, { AlertsTableStateProps } from './alerts_table_state';
import { useFetchAlerts } from './hooks/use_fetch_alerts';
import { DefaultSort } from './hooks';
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
jest.mock('./hooks/use_fetch_alerts');
jest.mock('@kbn/kibana-utils-plugin/public');
@ -103,6 +104,12 @@ hookUseFetchAlerts.mockImplementation(() => [
},
]);
const AlertsTableWithLocale: React.FunctionComponent<AlertsTableStateProps> = (props) => (
<IntlProvider locale="en">
<AlertsTableState {...props} />
</IntlProvider>
);
describe('AlertsTableState', () => {
const tableProps = {
alertsTableConfigurationRegistry: alertsTableConfigurationRegistryMock,
@ -120,14 +127,14 @@ describe('AlertsTableState', () => {
describe('Alerts table configuration registry', () => {
it('should read the configuration from the registry', async () => {
render(<AlertsTableState {...tableProps} />);
render(<AlertsTableWithLocale {...tableProps} />);
expect(hasMock).toHaveBeenCalledWith(PLUGIN_ID);
expect(getMock).toHaveBeenCalledWith(PLUGIN_ID);
});
it('should render an empty error state when the plugin id owner is not registered', async () => {
const props = { ...tableProps, configurationId: 'none' };
const result = render(<AlertsTableState {...props} />);
const result = render(<AlertsTableWithLocale {...props} />);
expect(result.getByTestId('alertsTableNoConfiguration')).toBeTruthy();
});
});
@ -137,7 +144,7 @@ describe('AlertsTableState', () => {
hookUseFetchAlerts.mockClear();
});
it('should show a flyout when selecting an alert', async () => {
const wrapper = render(<AlertsTableState {...tableProps} />);
const wrapper = render(<AlertsTableWithLocale {...tableProps} />);
userEvent.click(wrapper.queryByTestId('expandColumnCellOpenFlyoutButton-0')!);
const result = await wrapper.findAllByTestId('alertsFlyout');
@ -158,7 +165,7 @@ describe('AlertsTableState', () => {
it('should refetch data if flyout pagination exceeds the current page', async () => {
const wrapper = render(
<AlertsTableState
<AlertsTableWithLocale
{...{
...tableProps,
pageSize: 1,
@ -210,7 +217,7 @@ describe('AlertsTableState', () => {
});
it('should render an empty screen if there are no alerts', async () => {
const result = render(<AlertsTableState {...tableProps} />);
const result = render(<AlertsTableWithLocale {...tableProps} />);
expect(result.getByTestId('alertsStateTableEmptyState')).toBeTruthy();
});
});

View file

@ -149,7 +149,14 @@ const AlertsTableState = ({
const [
isLoading,
{ alerts, isInitializing, getInspectQuery, refetch: refresh, totalAlerts: alertsCount },
{
alerts,
isInitializing,
getInspectQuery,
refetch: refresh,
totalAlerts: alertsCount,
updatedAt,
},
] = useFetchAlerts({
fields: columns.map((col) => ({ field: col.id, include_unmapped: true })),
featureIds,
@ -215,6 +222,7 @@ const AlertsTableState = ({
onSortChange,
refresh,
sort,
updatedAt,
};
}, [
alerts,
@ -228,6 +236,7 @@ const AlertsTableState = ({
pagination.pageIndex,
refresh,
sort,
updatedAt,
]);
const tableProps = useMemo(
@ -246,6 +255,7 @@ const AlertsTableState = ({
useFetchAlertsData,
visibleColumns: storageAlertsTable.current.visibleColumns ?? [],
'data-test-subj': 'internalAlertsState',
updatedAt,
}),
[
alertsTableConfiguration,
@ -254,6 +264,7 @@ const AlertsTableState = ({
pagination.pageSize,
showExpandToDetails,
useFetchAlertsData,
updatedAt,
]
);

View file

@ -14,6 +14,7 @@ import { BulkActionsContext } from './context';
import { AlertsTable } from '../alerts_table';
import { AlertsField, AlertsTableProps, BulkActionsState } from '../../../../types';
import { bulkActionsReducer } from './reducer';
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
jest.mock('@kbn/data-plugin/public');
jest.mock('@kbn/kibana-react-plugin/public/ui_settings/use_ui_setting', () => ({
@ -92,6 +93,7 @@ describe('AlertsTable.BulkActions', () => {
useFetchAlertsData: () => alertsData,
visibleColumns: columns.map((c) => c.id),
'data-test-subj': 'testTable',
updatedAt: Date.now(),
};
const tablePropsWithBulkActions = {
@ -127,22 +129,32 @@ describe('AlertsTable.BulkActions', () => {
);
return (
<BulkActionsContext.Provider value={initialBulkActionsState}>
<AlertsTable {...props} />
</BulkActionsContext.Provider>
<IntlProvider locale="en">
<BulkActionsContext.Provider value={initialBulkActionsState}>
<AlertsTable {...props} />
</BulkActionsContext.Provider>
</IntlProvider>
);
};
describe('when the bulk action hook is not set', () => {
it('should not show the bulk actions column', () => {
const { queryByTestId } = render(<AlertsTable {...tableProps} />);
const { queryByTestId } = render(
<IntlProvider locale="en">
<AlertsTable {...tableProps} />
</IntlProvider>
);
expect(queryByTestId('bulk-actions-header')).toBeNull();
});
});
describe('when the bulk action hook is set', () => {
it('should show the bulk actions column', () => {
const { getByTestId } = render(<AlertsTable {...tablePropsWithBulkActions} />);
const { getByTestId } = render(
<IntlProvider locale="en">
<AlertsTable {...tablePropsWithBulkActions} />
</IntlProvider>
);
expect(getByTestId('bulk-actions-header')).toBeDefined();
});

View file

@ -0,0 +1,79 @@
/*
* 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 { EuiText, EuiToolTip } from '@elastic/eui';
import { FormattedRelative } from '@kbn/i18n-react';
import React, { useEffect, useMemo, useState } from 'react';
import * as i18n from './translations';
export interface LastUpdatedAtProps {
compact?: boolean;
updatedAt: number;
showUpdating?: boolean;
}
const Updated = React.memo<{ date: number; prefix: string; updatedAt: number }>(
({ date, prefix, updatedAt }) => (
<>
{prefix}
{
<FormattedRelative
data-test-subj="last-updated-at-date"
key={`formatedRelative-${date}`}
value={new Date(updatedAt)}
/>
}
</>
)
);
Updated.displayName = 'Updated';
const prefix = ` ${i18n.UPDATED} `;
export const LastUpdatedAt = React.memo<LastUpdatedAtProps>(
({ compact = false, updatedAt, showUpdating = false }) => {
const [date, setDate] = useState(Date.now());
function tick() {
setDate(Date.now());
}
useEffect(() => {
const timerID = setInterval(() => tick(), 10000);
return () => {
clearInterval(timerID);
};
}, []);
const updateText = useMemo(() => {
if (showUpdating) {
return <span> {i18n.UPDATING}</span>;
}
if (!compact) {
return <Updated date={date} prefix={prefix} updatedAt={updatedAt} />;
}
return null;
}, [compact, date, showUpdating, updatedAt]);
return (
<EuiToolTip content={<Updated date={date} prefix={prefix} updatedAt={updatedAt} />}>
<EuiText color="subdued" size="xs" data-test-subj="toolbar-updated-at">
{updateText}
</EuiText>
</EuiToolTip>
);
}
);
LastUpdatedAt.displayName = 'LastUpdatedAt';
// eslint-disable-next-line import/no-default-export
export { LastUpdatedAt as default };

View file

@ -0,0 +1,16 @@
/*
* 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 { i18n } from '@kbn/i18n';
export const UPDATING = i18n.translate('xpack.triggersActionsUI.alertsTable.lastUpdated.updating', {
defaultMessage: 'Updating...',
});
export const UPDATED = i18n.translate('xpack.triggersActionsUI.alertsTable.lastUpdated.updated', {
defaultMessage: 'Updated',
});

View file

@ -9,31 +9,46 @@ import { EuiDataGridToolBarVisibilityOptions } from '@elastic/eui';
import { EcsFieldsResponse } from '@kbn/rule-registry-plugin/common/search_strategy';
import React, { lazy, Suspense } from 'react';
import { BulkActionsConfig } from '../../../../types';
import { LastUpdatedAt } from './components/last_updated_at';
const BulkActionsToolbar = lazy(() => import('../bulk_actions/components/toolbar'));
const getDefaultVisibility = (updatedAt: number) => {
return {
showColumnSelector: true,
showSortSelector: true,
additionalControls: {
right: <LastUpdatedAt updatedAt={updatedAt} />,
},
};
};
export const getToolbarVisibility = ({
bulkActions,
alertsCount,
rowSelection,
alerts,
isLoading,
updatedAt,
}: {
bulkActions: BulkActionsConfig[];
alertsCount: number;
rowSelection: Set<number>;
alerts: EcsFieldsResponse[];
isLoading: boolean;
updatedAt: number;
}): EuiDataGridToolBarVisibilityOptions => {
const selectedRowsCount = rowSelection.size;
const defaultVisibility = getDefaultVisibility(updatedAt);
if (selectedRowsCount === 0 || selectedRowsCount === undefined || bulkActions.length === 0)
return {
showColumnSelector: true,
showSortSelector: true,
};
return defaultVisibility;
const options = {
showColumnSelector: false,
showSortSelector: false,
additionalControls: {
...defaultVisibility.additionalControls,
left: {
append: (
<Suspense fallback={null}>

View file

@ -418,6 +418,7 @@ export interface AlertsTableProps {
useFetchAlertsData: () => FetchAlertData;
visibleColumns: string[];
'data-test-subj': string;
updatedAt: number;
}
// TODO We need to create generic type between our plugin, right now we have different one because of the old alerts table