From f8a6f97e3a3c8420dd499f50b07c662ba096c480 Mon Sep 17 00:00:00 2001 From: Jatin Kathuria Date: Wed, 11 Sep 2024 16:02:19 +0200 Subject: [PATCH] [Security Solution] Migration old format of Page Controls to new format (#192555) ## Summary PR #190561 introduces a breaking change in the format of page controls data. Because of this there was conflict between the value of page controls stored in local storage v/s a new format. > [!WARNING] > All new users of `v8.16` will encounter the error on the alerts page because of this conflict. To resolve this, they will have to clear local storage which not a great UX. ## Desk Testing 1. Checkout to `v8.15` branch by running `git checkout 8.15`. 2. Go to the alert page and do some modifications to the page controls. This store `v8.15` page controls in local storage. - You can, for example, delete one page control. - Change selected value for one page control. - Additionally, you can also add a custom control. 3. Checkout `main` now and repeat the above steps. 4. Your changes should be retained on the alert page and there should not be any error. ### Checklist Delete any items that are not applicable to this PR. - [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 --------- Co-authored-by: Elastic Machine --- .../public/detections/index.ts | 3 + .../public/detections/migrations.ts | 20 ++ .../migrate_alert_page_contorls.test.ts | 255 ++++++++++++++++++ .../migrate_alert_page_controls.ts | 137 ++++++++++ 4 files changed, 415 insertions(+) create mode 100644 x-pack/plugins/security_solution/public/detections/migrations.ts create mode 100644 x-pack/plugins/security_solution/public/timelines/containers/local_storage/migrate_alert_page_contorls.test.ts create mode 100644 x-pack/plugins/security_solution/public/timelines/containers/local_storage/migrate_alert_page_controls.ts diff --git a/x-pack/plugins/security_solution/public/detections/index.ts b/x-pack/plugins/security_solution/public/detections/index.ts index b93481c0c253b..77d131377f9f5 100644 --- a/x-pack/plugins/security_solution/public/detections/index.ts +++ b/x-pack/plugins/security_solution/public/detections/index.ts @@ -11,6 +11,7 @@ import type { TableIdLiteral } from '@kbn/securitysolution-data-table'; import { getDataTablesInStorageByIds } from '../timelines/containers/local_storage'; import { routes } from './routes'; import type { SecuritySubPlugin } from '../app/types'; +import { runDetectionMigrations } from './migrations'; export const DETECTIONS_TABLE_IDS: TableIdLiteral[] = [ TableId.alertsOnRuleDetailsPage, @@ -21,6 +22,8 @@ export class Detections { public setup() {} public start(storage: Storage): SecuritySubPlugin { + runDetectionMigrations(); + return { storageDataTables: { tableById: getDataTablesInStorageByIds(storage, DETECTIONS_TABLE_IDS), diff --git a/x-pack/plugins/security_solution/public/detections/migrations.ts b/x-pack/plugins/security_solution/public/detections/migrations.ts new file mode 100644 index 0000000000000..81009f63747a6 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/migrations.ts @@ -0,0 +1,20 @@ +/* + * 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 { Storage } from '@kbn/kibana-utils-plugin/public'; +import { migrateAlertPageControlsTo816 } from '../timelines/containers/local_storage/migrate_alert_page_controls'; + +type LocalStorageMigrator = (storage: Storage) => void; + +const runLocalStorageMigration = (fn: LocalStorageMigrator) => { + const storage = new Storage(localStorage); + fn(storage); +}; + +export const runDetectionMigrations = () => { + runLocalStorageMigration(migrateAlertPageControlsTo816); +}; diff --git a/x-pack/plugins/security_solution/public/timelines/containers/local_storage/migrate_alert_page_contorls.test.ts b/x-pack/plugins/security_solution/public/timelines/containers/local_storage/migrate_alert_page_contorls.test.ts new file mode 100644 index 0000000000000..575d5bcd6dab3 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/containers/local_storage/migrate_alert_page_contorls.test.ts @@ -0,0 +1,255 @@ +/* + * 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 { Storage } from '@kbn/kibana-utils-plugin/public'; +import { + PAGE_FILTER_STORAGE_KEY, + migrateAlertPageControlsTo816, +} from './migrate_alert_page_controls'; + +const OLD_FORMAT = { + viewMode: 'view', + id: '5bc0ef0f-c6a9-4eaf-9fc5-9703fcb85482', + panels: { + '0': { + type: 'optionsListControl', + order: 0, + grow: true, + width: 'small', + explicitInput: { + id: '0', + dataViewId: 'security_solution_alerts_dv', + fieldName: 'kibana.alert.workflow_status', + title: 'Status', + hideExclude: true, + hideSort: true, + hidePanelTitles: true, + placeholder: '', + ignoreParentSettings: { + ignoreValidations: true, + }, + selectedOptions: ['open'], + hideActionBar: true, + persist: true, + hideExists: true, + existsSelected: false, + exclude: false, + }, + }, + '1': { + type: 'optionsListControl', + order: 1, + grow: true, + width: 'small', + explicitInput: { + id: '1', + dataViewId: 'security_solution_alerts_dv', + fieldName: 'kibana.alert.severity', + title: 'Severity', + hideExclude: true, + hideSort: true, + hidePanelTitles: true, + placeholder: '', + ignoreParentSettings: { + ignoreValidations: true, + }, + selectedOptions: [], + hideActionBar: true, + hideExists: true, + existsSelected: false, + exclude: false, + }, + }, + '2': { + type: 'optionsListControl', + order: 2, + grow: true, + width: 'small', + explicitInput: { + id: '2', + dataViewId: 'security_solution_alerts_dv', + fieldName: 'user.name', + title: 'User', + hideExclude: true, + hideSort: true, + hidePanelTitles: true, + placeholder: '', + ignoreParentSettings: { + ignoreValidations: true, + }, + selectedOptions: [], + existsSelected: false, + exclude: false, + }, + }, + '3': { + type: 'optionsListControl', + order: 3, + grow: true, + width: 'small', + explicitInput: { + id: '3', + dataViewId: 'security_solution_alerts_dv', + fieldName: 'host.name', + title: 'Host', + hideExclude: true, + hideSort: true, + hidePanelTitles: true, + placeholder: '', + ignoreParentSettings: { + ignoreValidations: true, + }, + selectedOptions: [], + existsSelected: false, + exclude: false, + }, + }, + }, + defaultControlWidth: 'small', + defaultControlGrow: true, + controlStyle: 'oneLine', + chainingSystem: 'HIERARCHICAL', + showApplySelections: false, + ignoreParentSettings: { + ignoreFilters: false, + ignoreQuery: false, + ignoreTimerange: false, + ignoreValidations: false, + }, + timeRange: { + from: '2024-09-10T22:00:00.000Z', + to: '2024-09-11T21:59:59.999Z', + mode: 'absolute', + }, + filters: [ + { + meta: { + alias: null, + negate: true, + disabled: false, + type: 'exists', + key: 'kibana.alert.building_block_type', + index: 'security-solution-default', + }, + query: { + exists: { + field: 'kibana.alert.building_block_type', + }, + }, + }, + ], + query: { + query: '', + language: 'kuery', + }, +}; + +const NEW_FORMAT = { + initialChildControlState: { + '0': { + type: 'optionsListControl', + order: 0, + hideExclude: true, + hideSort: true, + placeholder: '', + width: 'small', + dataViewId: 'security_solution_alerts_dv', + title: 'Status', + fieldName: 'kibana.alert.workflow_status', + selectedOptions: ['open'], + hideActionBar: true, + persist: true, + hideExists: true, + }, + '1': { + type: 'optionsListControl', + order: 1, + hideExclude: true, + hideSort: true, + placeholder: '', + width: 'small', + dataViewId: 'security_solution_alerts_dv', + title: 'Severity', + fieldName: 'kibana.alert.severity', + selectedOptions: [], + hideActionBar: true, + hideExists: true, + }, + '2': { + type: 'optionsListControl', + order: 2, + hideExclude: true, + hideSort: true, + placeholder: '', + width: 'small', + dataViewId: 'security_solution_alerts_dv', + title: 'User', + fieldName: 'user.name', + }, + '3': { + type: 'optionsListControl', + order: 3, + hideExclude: true, + hideSort: true, + placeholder: '', + width: 'small', + dataViewId: 'security_solution_alerts_dv', + title: 'Host', + fieldName: 'host.name', + }, + }, + labelPosition: 'oneLine', + chainingSystem: 'HIERARCHICAL', + autoApplySelections: false, + ignoreParentSettings: { + ignoreValidations: false, + }, + editorConfig: { + hideWidthSettings: true, + hideDataViewSelector: true, + hideAdditionalSettings: true, + }, +}; +const storage = new Storage(localStorage); + +describe('migrateAlertPageControlsTo816', () => { + beforeEach(() => { + storage.clear(); + }); + it('should migrate the old format to the new format', () => { + storage.set(PAGE_FILTER_STORAGE_KEY, OLD_FORMAT); + migrateAlertPageControlsTo816(storage); + const migrated = storage.get(PAGE_FILTER_STORAGE_KEY); + expect(migrated).toMatchObject(NEW_FORMAT); + }); + + it('should be a no-op if the new format already exists', () => { + storage.set(PAGE_FILTER_STORAGE_KEY, NEW_FORMAT); + migrateAlertPageControlsTo816(storage); + const migrated = storage.get(PAGE_FILTER_STORAGE_KEY); + expect(migrated).toMatchObject(NEW_FORMAT); + }); + + it('should be a no-op if no value is present in localstorage for page filters ', () => { + migrateAlertPageControlsTo816(storage); + const migrated = storage.get(PAGE_FILTER_STORAGE_KEY); + expect(migrated).toBeNull(); + }); + + it('should convert custom old format correctly', () => { + const MODIFIED_OLD_FORMAT = structuredClone(OLD_FORMAT); + MODIFIED_OLD_FORMAT.panels['0'].explicitInput.hideExists = true; + MODIFIED_OLD_FORMAT.chainingSystem = 'NONE'; + storage.set(PAGE_FILTER_STORAGE_KEY, MODIFIED_OLD_FORMAT); + migrateAlertPageControlsTo816(storage); + const migrated = storage.get(PAGE_FILTER_STORAGE_KEY); + const EXPECTED_NEW_FORMAT = structuredClone(NEW_FORMAT); + EXPECTED_NEW_FORMAT.initialChildControlState['0'].hideExists = true; + EXPECTED_NEW_FORMAT.chainingSystem = 'NONE'; + expect(migrated).toMatchObject(EXPECTED_NEW_FORMAT); + }); +}); diff --git a/x-pack/plugins/security_solution/public/timelines/containers/local_storage/migrate_alert_page_controls.ts b/x-pack/plugins/security_solution/public/timelines/containers/local_storage/migrate_alert_page_controls.ts new file mode 100644 index 0000000000000..d3e17f47983d9 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/containers/local_storage/migrate_alert_page_controls.ts @@ -0,0 +1,137 @@ +/* + * 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 type { ControlGroupRuntimeState } from '@kbn/controls-plugin/public'; +import type { DefaultControlState } from '@kbn/controls-plugin/public/react_controls/controls/types'; +import type { Storage } from '@kbn/kibana-utils-plugin/public'; + +export const PAGE_FILTER_STORAGE_KEY = 'siem.default.pageFilters'; + +interface OldFormat { + viewMode: string; + id: string; + panels: { + [key: string]: { + type: string; + order: number; + grow: boolean; + width: string; + explicitInput: { + id: string; + dataViewId: string; + fieldName: string; + title: string; + hideExclude: boolean; + hideSort: boolean; + hidePanelTitles: boolean; + placeholder: string; + ignoreParentSettings: { + ignoreValidations: boolean; + }; + selectedOptions: string[]; + hideActionBar: boolean; + persist: boolean; + hideExists: boolean; + existsSelected: boolean; + exclude: boolean; + }; + }; + }; + defaultControlWidth: string; + defaultControlGrow: boolean; + controlStyle: string; + chainingSystem: string; + showApplySelections: boolean; + ignoreParentSettings: { + ignoreFilters: boolean; + ignoreQuery: boolean; + ignoreTimerange: boolean; + ignoreValidations: boolean; + }; + timeRange: { + from: string; + to: string; + mode: string; + }; + filters: Array<{ + meta: { + alias: null; + negate: boolean; + disabled: boolean; + type: string; + key: string; + index: string; + }; + query: { + exists: { + field: string; + }; + }; + }>; + query: { + query: string; + }; +} + +interface NewFormatExplicitInput { + dataViewId: string; + fieldName: string; + title: string; + hideExclude: boolean; + hideSort: boolean; + placeholder: string; + selectedOptions: string[]; + hideActionBar: boolean; + persist: boolean; + hideExists: boolean; +} + +/** + * Ref PR : https://github.com/elastic/kibana/pull/190561 + * + * The above PR breaks the local storage format of page filters controls. + * This migration script is to migrate the old format to the new format. + * + */ +export function migrateAlertPageControlsTo816(storage: Storage) { + const oldFormat: OldFormat = storage.get(PAGE_FILTER_STORAGE_KEY); + if (oldFormat && Object.keys(oldFormat).includes('panels')) { + // Only run when it is old format + const newFormat: ControlGroupRuntimeState = { + initialChildControlState: {}, + labelPosition: oldFormat.controlStyle as ControlGroupRuntimeState['labelPosition'], + chainingSystem: oldFormat.chainingSystem as ControlGroupRuntimeState['chainingSystem'], + autoApplySelections: oldFormat.showApplySelections ?? true, + ignoreParentSettings: oldFormat.ignoreParentSettings, + editorConfig: { + hideWidthSettings: true, + hideDataViewSelector: true, + hideAdditionalSettings: true, + }, + }; + + for (const [key, value] of Object.entries(oldFormat.panels)) { + newFormat.initialChildControlState[key] = { + type: 'optionsListControl', + order: value.order, + hideExclude: value.explicitInput.hideExclude ?? true, + hideSort: value.explicitInput.hideSort ?? true, + placeholder: value.explicitInput.placeholder ?? '', + width: value.width as DefaultControlState['width'], + dataViewId: value.explicitInput.dataViewId ?? 'security_solution_alerts_dv', + title: value.explicitInput.title, + fieldName: value.explicitInput.fieldName, + selectedOptions: value.explicitInput.selectedOptions, + hideActionBar: value.explicitInput.hideActionBar, + persist: value.explicitInput.persist, + hideExists: value.explicitInput.hideExists, + }; + } + + storage.set(PAGE_FILTER_STORAGE_KEY, newFormat); + } +}