mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[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:
parent
1377ef2b33
commit
8b21e25ecf
9 changed files with 181 additions and 25 deletions
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
]
|
||||
);
|
||||
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
||||
|
|
|
@ -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 };
|
|
@ -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',
|
||||
});
|
|
@ -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}>
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue