[RAC] Store Alerts View table state in localStorage (#118207) (#119678)

* [RAC] Store Alerts View table state in localStorage

* Use Redux store subscriber instead of callback

* Fix typecheck

* Fix bad merge

* Add tests

* Remove persisting selected rows

* Fix bad merge

* onTGridStateChange => onStateChange

* Remove non-null assertion

* Put non-null assertion back because typescript hates me, personally

* Fix checks

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

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Zacqary Adam Xeper 2021-11-24 18:34:48 -06:00 committed by GitHub
parent 42decf18dc
commit 6c987e5d8b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 133 additions and 7 deletions

View file

@ -34,12 +34,18 @@ import {
import styled from 'styled-components';
import React, { Suspense, useMemo, useState, useCallback, useEffect } from 'react';
import usePrevious from 'react-use/lib/usePrevious';
import { get } from 'lodash';
import { get, pick } from 'lodash';
import {
getAlertsPermissions,
useGetUserAlertsPermissions,
} from '../../hooks/use_alert_permission';
import type { TimelinesUIStart, TGridType, SortDirection } from '../../../../timelines/public';
import type {
TimelinesUIStart,
TGridType,
TGridState,
TGridModel,
SortDirection,
} from '../../../../timelines/public';
import { useStatusBulkActionItems } from '../../../../timelines/public';
import type { TopAlert } from './';
import { useKibana } from '../../../../../../src/plugins/kibana_react/public';
@ -60,6 +66,8 @@ import { parseAlert } from './parse_alert';
import { CoreStart } from '../../../../../../src/core/public';
import { translations, paths } from '../../config';
const ALERT_TABLE_STATE_STORAGE_KEY = 'xpack.observability.alert.tableState';
interface AlertsTableTGridProps {
indexNames: string[];
rangeFrom: string;
@ -330,6 +338,9 @@ export function AlertsTableTGrid(props: AlertsTableTGridProps) {
} = useKibana<CoreStart & { timelines: TimelinesUIStart }>().services;
const [flyoutAlert, setFlyoutAlert] = useState<TopAlert | undefined>(undefined);
const [tGridState, setTGridState] = useState<Partial<TGridModel> | null>(
JSON.parse(localStorage.getItem(ALERT_TABLE_STATE_STORAGE_KEY) ?? 'null')
);
const casePermissions = useGetUserCasesPermissions();
@ -351,6 +362,20 @@ export function AlertsTableTGrid(props: AlertsTableTGridProps) {
}
}, [workflowStatus, prevWorkflowStatus]);
useEffect(() => {
if (tGridState) {
const newState = JSON.stringify({
...tGridState,
columns: tGridState.columns?.map((c) =>
pick(c, ['columnHeaderType', 'displayAsText', 'id', 'initialWidth', 'linkField'])
),
});
if (newState !== localStorage.getItem(ALERT_TABLE_STATE_STORAGE_KEY)) {
localStorage.setItem(ALERT_TABLE_STATE_STORAGE_KEY, newState);
}
}
}, [tGridState]);
const setEventsDeleted = useCallback<ObservabilityActionsProps['setEventsDeleted']>((action) => {
if (action.isDeleted) {
setDeletedEventIds((ids) => [...ids, ...action.eventIds]);
@ -379,6 +404,20 @@ export function AlertsTableTGrid(props: AlertsTableTGridProps) {
];
}, [workflowStatus, setEventsDeleted]);
const onStateChange = useCallback(
(state: TGridState) => {
const pickedState = pick(state.timelineById['standalone-t-grid'], [
'columns',
'sort',
'selectedEventIds',
]);
if (JSON.stringify(pickedState) !== JSON.stringify(tGridState)) {
setTGridState(pickedState);
}
},
[tGridState]
);
const tGridProps = useMemo(() => {
const type: TGridType = 'standalone';
const sortDirection: SortDirection = 'desc';
@ -387,7 +426,7 @@ export function AlertsTableTGrid(props: AlertsTableTGridProps) {
casesOwner: observabilityFeatureId,
casePermissions,
type,
columns,
columns: tGridState?.columns ?? columns,
deletedEventIds,
defaultCellActions: getDefaultCellActions({ addToQuery }),
disabledCellActions: FIELDS_WITHOUT_CELL_ACTIONS,
@ -398,6 +437,7 @@ export function AlertsTableTGrid(props: AlertsTableTGridProps) {
itemsPerPageOptions: [10, 25, 50],
loadingText: translations.alertsTable.loadingTextLabel,
footerText: translations.alertsTable.footerTextLabel,
onStateChange,
query: {
query: `${ALERT_WORKFLOW_STATUS}: ${workflowStatus}${kuery !== '' ? ` and ${kuery}` : ''}`,
language: 'kuery',
@ -408,7 +448,7 @@ export function AlertsTableTGrid(props: AlertsTableTGridProps) {
runtimeMappings: {},
start: rangeFrom,
setRefetch,
sort: [
sort: tGridState?.sort ?? [
{
columnId: '@timestamp',
columnType: 'date',
@ -432,6 +472,8 @@ export function AlertsTableTGrid(props: AlertsTableTGridProps) {
setRefetch,
leadingControlColumns,
deletedEventIds,
onStateChange,
tGridState,
]);
const handleFlyoutClose = () => setFlyoutAlert(undefined);

View file

@ -105,6 +105,7 @@ export interface TGridStandaloneProps {
itemsPerPageOptions: number[];
query: Query;
onRuleChange?: () => void;
onStateChange?: (state: State) => void;
renderCellValue: (props: CellValueElementProps) => React.ReactNode;
rowRenderers: RowRenderer[];
runtimeMappings: MappingRuntimeFields;

View file

@ -26,7 +26,7 @@ export type {
export { Direction } from '../common/search_strategy/common';
export { tGridReducer } from './store/t_grid/reducer';
export type { TGridModelForTimeline, TimelineState, TimelinesUIStart } from './types';
export type { TGridType, SortDirection } from './types';
export type { TGridType, SortDirection, State as TGridState, TGridModel } from './types';
export type { OnColumnFocused } from '../common/utils/accessibility';
export {
ARIA_COLINDEX_ATTRIBUTE,

View file

@ -5,7 +5,8 @@
* 2.0.
*/
import { Store } from 'redux';
import { Store, Unsubscribe } from 'redux';
import { throttle } from 'lodash';
import { Storage } from '../../../../src/plugins/kibana_utils/public';
import type { CoreSetup, Plugin, CoreStart } from '../../../../src/core/public';
@ -29,6 +30,7 @@ import { getHoverActions } from './components/hover_actions';
export class TimelinesPlugin implements Plugin<void, TimelinesUIStart> {
private _store: Store | undefined;
private _storage = new Storage(localStorage);
private _storeUnsubscribe: Unsubscribe | undefined;
public setup(core: CoreSetup) {}
@ -43,6 +45,13 @@ export class TimelinesPlugin implements Plugin<void, TimelinesUIStart> {
const state = getState();
if (state && state.app) {
this._store = undefined;
} else {
if (props.onStateChange) {
this._storeUnsubscribe = this._store.subscribe(
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
throttle(() => props.onStateChange!(getState()), 500)
);
}
}
}
return getTGridLazy(props, {
@ -118,5 +127,9 @@ export class TimelinesPlugin implements Plugin<void, TimelinesUIStart> {
this._store = store;
}
public stop() {}
public stop() {
if (this._storeUnsubscribe) {
this._storeUnsubscribe();
}
}
}

View file

@ -9,6 +9,8 @@ import { Storage } from '../../../../../../src/plugins/kibana_utils/public';
import type { ColumnHeaderOptions } from '../../../common';
import type { TGridModel, TGridModelSettings } from './model';
export type { TGridModel };
export interface AutoSavedWarningMsg {
timelineId: string | null;
newTimelineModel: TGridModel | null;

View file

@ -0,0 +1,67 @@
/*
* 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 expect from '@kbn/expect';
import { FtrProviderContext } from '../../../ftr_provider_context';
export default ({ getService, getPageObject }: FtrProviderContext) => {
describe('Observability alert table state storage', function () {
this.tags('includeFirefox');
const observability = getService('observability');
const esArchiver = getService('esArchiver');
const testSubjects = getService('testSubjects');
before(async () => {
await esArchiver.load('x-pack/test/functional/es_archives/observability/alerts');
await esArchiver.load('x-pack/test/functional/es_archives/infra/metrics_and_logs');
await observability.alerts.common.navigateToTimeWithData();
});
after(async () => {
await esArchiver.unload('x-pack/test/functional/es_archives/infra/metrics_and_logs');
await esArchiver.unload('x-pack/test/functional/es_archives/observability/alerts');
});
it('remembers column changes', async () => {
const durationColumnButton = await testSubjects.find(
'dataGridHeaderCellActionButton-kibana.alert.duration.us'
);
await durationColumnButton.click();
const columnMenu = await testSubjects.find(
'dataGridHeaderCellActionGroup-kibana.alert.duration.us'
);
const removeButton = await columnMenu.findByCssSelector('[title="Remove column"]');
await removeButton.click();
await observability.alerts.common.navigateToTimeWithData();
const durationColumnExists = await testSubjects.exists(
'dataGridHeaderCellActionButton-kibana.alert.duration.us'
);
expect(durationColumnExists).to.be(false);
});
it('remembers sorting changes', async () => {
const timestampColumnButton = await testSubjects.find(
'dataGridHeaderCellActionButton-@timestamp'
);
await timestampColumnButton.click();
const columnMenu = await testSubjects.find('dataGridHeaderCellActionGroup-@timestamp');
const sortButton = await columnMenu.findByCssSelector('[title="Sort Old-New"]');
await sortButton.click();
await observability.alerts.common.navigateToTimeWithData();
const timestampColumnHeading = await testSubjects.find('dataGridHeaderCell-@timestamp');
expect(await timestampColumnHeading.getAttribute('aria-sort')).to.be('ascending');
});
});
};

View file

@ -19,5 +19,6 @@ export default function ({ loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./alerts/add_to_case'));
loadTestFile(require.resolve('./alerts/state_synchronization'));
loadTestFile(require.resolve('./alerts/bulk_actions'));
loadTestFile(require.resolve('./alerts/table_storage'));
});
}