From 428e832b8111ee74fe59978c5d2ff4b52327debc Mon Sep 17 00:00:00 2001 From: "Qingyang(Abby) Hu" Date: Tue, 6 Sep 2022 10:42:50 -0700 Subject: [PATCH] [D&D] Save index pattern using proper saved object structure (#2218) * Save index pattern as a proper saved object relationship, previously it is only saved as an id in the visualizationState. Signed-off-by: abbyhu2000 * remove index id from visualization when saving & add comments Signed-off-by: abbyhu2000 * add migration for existing wizard Signed-off-by: abbyhu2000 * add migration unit test Signed-off-by: abbyhu2000 * change wizard doc version to 2; change migration version to 2.3.0 Signed-off-by: abbyhu2000 Signed-off-by: abbyhu2000 --- .../application/utils/get_top_nav_config.tsx | 18 ++- .../utils/use/use_saved_wizard_vis.ts | 16 ++- .../public/saved_visualizations/_saved_vis.ts | 4 +- .../saved_visualization_references.ts | 20 ++++ .../wizard/server/saved_objects/wizard_app.ts | 9 +- .../saved_objects/wizard_migration.test.ts | 107 ++++++++++++++++++ .../server/saved_objects/wizard_migration.ts | 51 +++++++++ 7 files changed, 218 insertions(+), 7 deletions(-) create mode 100644 src/plugins/wizard/public/saved_visualizations/saved_visualization_references.ts create mode 100644 src/plugins/wizard/server/saved_objects/wizard_migration.test.ts create mode 100644 src/plugins/wizard/server/saved_objects/wizard_migration.ts diff --git a/src/plugins/wizard/public/application/utils/get_top_nav_config.tsx b/src/plugins/wizard/public/application/utils/get_top_nav_config.tsx index 6d0f5a5498ad..8d83807c058f 100644 --- a/src/plugins/wizard/public/application/utils/get_top_nav_config.tsx +++ b/src/plugins/wizard/public/application/utils/get_top_nav_config.tsx @@ -41,7 +41,6 @@ import { WizardVisSavedObject } from '../../types'; import { StyleState, VisualizationState, AppDispatch } from './state_management'; import { EDIT_PATH } from '../../../common'; import { setEditorState } from './state_management/metadata_slice'; - interface TopNavConfigParams { visualizationIdFromUrl: string; savedWizardVis: WizardVisSavedObject; @@ -60,7 +59,12 @@ export const getTopNavConfig = ( saveDisabledReason, dispatch, }: TopNavConfigParams, - { history, toastNotifications, i18n: { Context: I18nContext } }: WizardServices + { + history, + toastNotifications, + i18n: { Context: I18nContext }, + data: { indexPatterns }, + }: WizardServices ) => { const topNavConfig: TopNavMenuData[] = [ { @@ -96,7 +100,15 @@ export const getTopNavConfig = ( return; } const currentTitle = savedWizardVis.title; - savedWizardVis.visualizationState = JSON.stringify(visualizationState); + const indexPattern = await indexPatterns.get(visualizationState.indexPattern || ''); + savedWizardVis.searchSourceFields = { + index: indexPattern, + }; + const vizStateWithoutIndex = { + searchField: visualizationState.searchField, + activeVisualization: visualizationState.activeVisualization, + }; + savedWizardVis.visualizationState = JSON.stringify(vizStateWithoutIndex); savedWizardVis.styleState = JSON.stringify(styleState); savedWizardVis.title = newTitle; savedWizardVis.description = newDescription; diff --git a/src/plugins/wizard/public/application/utils/use/use_saved_wizard_vis.ts b/src/plugins/wizard/public/application/utils/use/use_saved_wizard_vis.ts index 56298b5821b7..db17e478a41c 100644 --- a/src/plugins/wizard/public/application/utils/use/use_saved_wizard_vis.ts +++ b/src/plugins/wizard/public/application/utils/use/use_saved_wizard_vis.ts @@ -15,10 +15,17 @@ import { WizardServices } from '../../../types'; import { MetricOptionsDefaults } from '../../../visualizations/metric/metric_viz_type'; import { getCreateBreadcrumbs, getEditBreadcrumbs } from '../breadcrumbs'; import { getSavedWizardVis } from '../get_saved_wizard_vis'; -import { useTypedDispatch, setStyleState, setVisualizationState } from '../state_management'; +import { + useTypedDispatch, + setStyleState, + setVisualizationState, + VisualizationState, +} from '../state_management'; import { useOpenSearchDashboards } from '../../../../../opensearch_dashboards_react/public'; import { setEditorState } from '../state_management/metadata_slice'; +// This function can be used when instantiating a saved vis or creating a new one +// using url parameters, embedding and destroying it in DOM export const useSavedWizardVis = (visualizationIdFromUrl: string | undefined) => { const { services } = useOpenSearchDashboards(); const [savedVisState, setSavedVisState] = useState(undefined); @@ -45,7 +52,12 @@ export const useSavedWizardVis = (visualizationIdFromUrl: string | undefined) => if (savedWizardVis.styleState !== '{}' && savedWizardVis.visualizationState !== '{}') { const styleState = JSON.parse(savedWizardVis.styleState); - const visualizationState = JSON.parse(savedWizardVis.visualizationState); + const vizStateWithoutIndex = JSON.parse(savedWizardVis.visualizationState); + const visualizationState: VisualizationState = { + searchField: vizStateWithoutIndex.searchField, + activeVisualization: vizStateWithoutIndex.activeVisualization, + indexPattern: savedWizardVis.searchSourceFields.index, + }; // TODO: Add validation and transformation, throw/handle errors dispatch(setStyleState(styleState)); dispatch(setVisualizationState(visualizationState)); diff --git a/src/plugins/wizard/public/saved_visualizations/_saved_vis.ts b/src/plugins/wizard/public/saved_visualizations/_saved_vis.ts index 66a02a974dd6..d269642bd8bc 100644 --- a/src/plugins/wizard/public/saved_visualizations/_saved_vis.ts +++ b/src/plugins/wizard/public/saved_visualizations/_saved_vis.ts @@ -8,6 +8,7 @@ import { SavedObjectOpenSearchDashboardsServices, } from '../../../saved_objects/public'; import { EDIT_PATH, PLUGIN_ID, WIZARD_SAVED_OBJECT } from '../../common'; +import { injectReferences } from './saved_visualization_references'; export function createSavedWizardVisClass(services: SavedObjectOpenSearchDashboardsServices) { const SavedObjectClass = createSavedObjectClass(services); @@ -32,6 +33,7 @@ export function createSavedWizardVisClass(services: SavedObjectOpenSearchDashboa super({ type: SavedWizardVis.type, mapping: SavedWizardVis.mapping, + injectReferences, // if this is null/undefined then the SavedObject will be assigned the defaults id, @@ -42,7 +44,7 @@ export function createSavedWizardVisClass(services: SavedObjectOpenSearchDashboa description: '', visualizationState: '{}', styleState: '{}', - version: 1, + version: 2, }, }); this.showInRecentlyAccessed = true; diff --git a/src/plugins/wizard/public/saved_visualizations/saved_visualization_references.ts b/src/plugins/wizard/public/saved_visualizations/saved_visualization_references.ts new file mode 100644 index 000000000000..81a2a54a01c6 --- /dev/null +++ b/src/plugins/wizard/public/saved_visualizations/saved_visualization_references.ts @@ -0,0 +1,20 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { SavedObjectReference } from '../../../../core/public'; +import { WizardVisSavedObject } from '../types'; +import { injectSearchSourceReferences } from '../../../data/public'; + +export function injectReferences( + savedObject: WizardVisSavedObject, + references: SavedObjectReference[] +) { + if (savedObject.searchSourceFields) { + savedObject.searchSourceFields = injectSearchSourceReferences( + savedObject.searchSourceFields as any, + references + ); + } +} diff --git a/src/plugins/wizard/server/saved_objects/wizard_app.ts b/src/plugins/wizard/server/saved_objects/wizard_app.ts index f5820d0f3f29..f9b57d53e3de 100644 --- a/src/plugins/wizard/server/saved_objects/wizard_app.ts +++ b/src/plugins/wizard/server/saved_objects/wizard_app.ts @@ -10,6 +10,7 @@ import { WizardSavedObjectAttributes, WIZARD_SAVED_OBJECT, } from '../../common'; +import { wizardSavedObjectTypeMigrations } from './wizard_migration'; export const wizardSavedObjectType: SavedObjectsType = { name: WIZARD_SAVED_OBJECT, @@ -29,7 +30,7 @@ export const wizardSavedObjectType: SavedObjectsType = { }; }, }, - migrations: {}, + migrations: wizardSavedObjectTypeMigrations, mappings: { properties: { title: { @@ -47,6 +48,12 @@ export const wizardSavedObjectType: SavedObjectsType = { index: false, }, version: { type: 'integer' }, + // Need to add a kibanaSavedObjectMeta attribute here to follow the current saved object flow + // When we save a saved object, the saved object plugin will extract the search source into two parts + // Some information will be put into kibanaSavedObjectMeta while others will be created as a reference object and pushed to the reference array + kibanaSavedObjectMeta: { + properties: { searchSourceJSON: { type: 'text', index: false } }, + }, }, }, }; diff --git a/src/plugins/wizard/server/saved_objects/wizard_migration.test.ts b/src/plugins/wizard/server/saved_objects/wizard_migration.test.ts new file mode 100644 index 000000000000..0e9248e1951f --- /dev/null +++ b/src/plugins/wizard/server/saved_objects/wizard_migration.test.ts @@ -0,0 +1,107 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { SavedObjectMigrationFn, SavedObjectMigrationContext } from '../../../../core/server'; +import { wizardSavedObjectTypeMigrations } from './wizard_migration'; + +const savedObjectMigrationContext = (null as unknown) as SavedObjectMigrationContext; + +describe('2.3.0', () => { + const migrate = (doc: any) => + wizardSavedObjectTypeMigrations['2.3.0']( + doc as Parameters[0], + savedObjectMigrationContext + ); + + it('should return original doc if visualizationState is not found', () => { + const migratedDoc = migrate({ + type: 'wizard', + attributes: {}, + }); + + expect(migratedDoc).toEqual({ + type: 'wizard', + attributes: {}, + }); + }); + + it('should return original doc if indexPattern is not found within visualizationState', () => { + const migratedDoc = migrate({ + type: 'wizard', + attributes: { + visualizationState: { + searchSource: '', + activeVisualization: {}, + }, + }, + }); + + expect(migratedDoc).toEqual({ + type: 'wizard', + attributes: { + visualizationState: { + searchSource: '', + activeVisualization: {}, + }, + }, + }); + }); + + it('should return original doc if references is not an array', () => { + const migratedDoc = migrate({ + type: 'wizard', + attributes: { + visualizationState: {}, + }, + references: {}, + }); + + expect(migratedDoc).toEqual({ + type: 'wizard', + attributes: { + visualizationState: {}, + }, + references: {}, + }); + }); + + it('should migrate the old version wizard saved object to new version wizard saved object', () => { + const migratedDoc = migrate({ + type: 'wizard', + attributes: { + visualizationState: JSON.stringify({ + searchFields: {}, + activeVisualization: {}, + indexPattern: 'indexPatternId', + }), + version: 1, + }, + references: [], + }); + + expect(migratedDoc).toEqual({ + type: 'wizard', + attributes: { + visualizationState: JSON.stringify({ + searchFields: {}, + activeVisualization: {}, + }), + version: 2, + kibanaSavedObjectMeta: { + searchSourceJSON: JSON.stringify({ + indexRefName: 'kibanaSavedObjectMeta.searchSourceJSON.index', + }), + }, + }, + references: [ + { + name: 'kibanaSavedObjectMeta.searchSourceJSON.index', + type: 'index-pattern', + id: 'indexPatternId', + }, + ], + }); + }); +}); diff --git a/src/plugins/wizard/server/saved_objects/wizard_migration.ts b/src/plugins/wizard/server/saved_objects/wizard_migration.ts new file mode 100644 index 000000000000..dcbaf8252218 --- /dev/null +++ b/src/plugins/wizard/server/saved_objects/wizard_migration.ts @@ -0,0 +1,51 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { get, flow } from 'lodash'; +import { SavedObjectMigrationFn } from '../../../../core/server'; + +const migrateIndexPattern: SavedObjectMigrationFn = (doc) => { + try { + const visualizationStateJSON = get(doc, 'attributes.visualizationState'); + const visualizationState = JSON.parse(visualizationStateJSON); + const indexPatternId = visualizationState.indexPattern; + const indexRefName = 'kibanaSavedObjectMeta.searchSourceJSON.index'; + + if (indexPatternId && Array.isArray(doc.references)) { + const searchSourceIndex = { + indexRefName, + }; + const visualizationWithoutIndex = { + searchFields: visualizationState.searchFields, + activeVisualization: visualizationState.activeVisualization, + }; + doc.attributes.visualizationState = JSON.stringify(visualizationWithoutIndex); + + doc.references.push({ + name: indexRefName, + type: 'index-pattern', + id: indexPatternId, + }); + doc.attributes.version = 2; + + return { + ...doc, + attributes: { + ...doc.attributes, + kibanaSavedObjectMeta: { + searchSourceJSON: JSON.stringify(searchSourceIndex), + }, + }, + }; + } + return doc; + } catch (e) { + return doc; + } +}; + +export const wizardSavedObjectTypeMigrations = { + '2.3.0': flow(migrateIndexPattern), +};