diff --git a/CHANGELOG.md b/CHANGELOG.md index 9fe60ba28007..a86899cb5dc2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) ### 📈 Features/Enhancements * Add updated_at column to objects' tables ([#1218](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/1218)) +* [Viz Builder] State validation before dispatching and loading ([#2351](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2351)) ### 🐛 Bug Fixes diff --git a/package.json b/package.json index 1dac0ee93307..f7ee743124bc 100644 --- a/package.json +++ b/package.json @@ -145,6 +145,7 @@ "@types/yauzl": "^2.9.1", "JSONStream": "1.3.5", "abortcontroller-polyfill": "^1.4.0", + "ajv": "^8.11.0", "angular": "^1.8.2", "angular-elastic": "^2.5.1", "angular-sanitize": "^1.8.0", diff --git a/src/plugins/wizard/public/application/utils/schema.json b/src/plugins/wizard/public/application/utils/schema.json new file mode 100644 index 000000000000..9effed97b2be --- /dev/null +++ b/src/plugins/wizard/public/application/utils/schema.json @@ -0,0 +1,28 @@ +{ + "type": "object", + "properties": { + "styleState": { + "type": "object" + }, + "visualizationState": { + "type": "object", + "properties": { + "activeVisualization": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "aggConfigParams": { "type": "array" } + }, + "required": ["name", "aggConfigParams"], + "additionalProperties": false + }, + "indexPattern": { "type": "string" }, + "searchField": { "type": "string" } + }, + "required": ["searchField"], + "additionalProperties": false + } + }, + "required": ["styleState", "visualizationState"], + "additionalProperties": false +} \ No newline at end of file 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 db17e478a41c..36ed26b15d04 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 @@ -7,6 +7,7 @@ import { i18n } from '@osd/i18n'; import { useEffect, useState } from 'react'; import { SavedObject } from '../../../../../saved_objects/public'; import { + InvalidJSONProperty, redirectWhenMissing, SavedObjectNotFound, } from '../../../../../opensearch_dashboards_utils/public'; @@ -23,6 +24,7 @@ import { } from '../state_management'; import { useOpenSearchDashboards } from '../../../../../opensearch_dashboards_react/public'; import { setEditorState } from '../state_management/metadata_slice'; +import { validateWizardState } from '../wizard_state_validation'; // This function can be used when instantiating a saved vis or creating a new one // using url parameters, embedding and destroying it in DOM @@ -39,6 +41,14 @@ export const useSavedWizardVis = (visualizationIdFromUrl: string | undefined) => http: { basePath }, toastNotifications, } = services; + const toastNotification = (message) => { + toastNotifications.addDanger({ + title: i18n.translate('visualize.createVisualization.failedToLoadErrorMessage', { + defaultMessage: 'Failed to load the visualization', + }), + text: message, + }); + }; const loadSavedWizardVis = async () => { try { const savedWizardVis = await getSavedWizardVis(services, visualizationIdFromUrl); @@ -58,7 +68,16 @@ export const useSavedWizardVis = (visualizationIdFromUrl: string | undefined) => activeVisualization: vizStateWithoutIndex.activeVisualization, indexPattern: savedWizardVis.searchSourceFields.index, }; - // TODO: Add validation and transformation, throw/handle errors + + const validateResult = validateWizardState({ styleState, visualizationState }); + if (!validateResult.valid) { + const err = validateResult.errors; + if (err) { + const errMsg = err[0].instancePath + ' ' + err[0].message; + throw new InvalidJSONProperty(errMsg); + } + } + dispatch(setStyleState(styleState)); dispatch(setVisualizationState(visualizationState)); } @@ -83,14 +102,12 @@ export const useSavedWizardVis = (visualizationIdFromUrl: string | undefined) => mapping: managementRedirectTarget, })(error); } + if (error instanceof InvalidJSONProperty) { + toastNotification(error.message); + } } catch (e) { const message = e instanceof Error ? e.message : ''; - toastNotifications.addWarning({ - title: i18n.translate('visualize.createVisualization.failedToLoadErrorMessage', { - defaultMessage: 'Failed to load the visualization', - }), - text: message, - }); + toastNotification(message); history.replace(EDIT_PATH); } } diff --git a/src/plugins/wizard/public/application/utils/wizard_state_validation.test.ts b/src/plugins/wizard/public/application/utils/wizard_state_validation.test.ts new file mode 100644 index 000000000000..6b02d9707438 --- /dev/null +++ b/src/plugins/wizard/public/application/utils/wizard_state_validation.test.ts @@ -0,0 +1,43 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { validateWizardState } from './wizard_state_validation'; + +describe('wizard state validation', () => { + const validStyleState = { + addLegend: true, + addTooltip: true, + legendPosition: '', + type: 'metric', + }; + const validVisualizationState = { + activeVisualization: { + name: 'metric', + aggConfigParams: [], + }, + indexPattern: '', + searchField: '', + }; + describe('correct return when validation suceeds', () => { + test('with correct wizard state', () => { + const validationResult = validateWizardState({ + styleState: validStyleState, + visualizationState: validVisualizationState, + }); + expect(validationResult.valid).toBeTruthy(); + expect(validationResult.errors).toBeNull(); + }); + }); + describe('correct return with errors when validation fails', () => { + test('with non object type styleStyle', () => { + const validationResult = validateWizardState({ + styleState: [], + visualizationState: validVisualizationState, + }); + expect(validationResult.valid).toBeFalsy(); + expect(validationResult.errors).toBeDefined(); + }); + }); +}); diff --git a/src/plugins/wizard/public/application/utils/wizard_state_validation.ts b/src/plugins/wizard/public/application/utils/wizard_state_validation.ts new file mode 100644 index 000000000000..6bcdf973e371 --- /dev/null +++ b/src/plugins/wizard/public/application/utils/wizard_state_validation.ts @@ -0,0 +1,19 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import Ajv from 'ajv'; +import wizardStateSchema from './schema.json'; + +const ajv = new Ajv(); +const validateState = ajv.compile(wizardStateSchema); + +export const validateWizardState = (wizardState) => { + const isWizardStateValid = validateState(wizardState); + + return { + valid: isWizardStateValid, + errors: validateState.errors, + }; +}; diff --git a/yarn.lock b/yarn.lock index e4fd8689c493..2914733af902 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4368,7 +4368,7 @@ ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.11.0, ajv@^6.12.3, ajv@^6.12.4, ajv json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ajv@^8.0.1, ajv@^8.6.2: +ajv@^8.0.1, ajv@^8.11.0, ajv@^8.6.2: version "8.11.0" resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.11.0.tgz#977e91dd96ca669f54a11e23e378e33b884a565f" integrity sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg== @@ -11911,10 +11911,10 @@ leaflet-responsive-popup@0.6.4: resolved "https://registry.yarnpkg.com/leaflet-responsive-popup/-/leaflet-responsive-popup-0.6.4.tgz#b93d9368ef9f96d6dc911cf5b96d90e08601c6b3" integrity sha512-2D8G9aQA6NHkulDBPN9kqbUCkCpWQQ6dF0xFL11AuEIWIbsL4UC/ZPP5m8GYM0dpU6YTlmyyCh1Tz+cls5Q4dg== -"leaflet-vega@npm:@amoo-miki/leaflet-vega@0.8.7": - version "0.8.7" - resolved "https://registry.yarnpkg.com/@amoo-miki/leaflet-vega/-/leaflet-vega-0.8.7.tgz#8faca1b4b8e2ef7d48667ac6faad9204f4da7153" - integrity sha512-T4M5yziwj3Fi9Adsbce+cdWqPjON0BRwEjwqLlPMoirU1vhifA6YKrlZkVzJrK0IIm+hdfMCLkBz33gD8fdxzQ== +"leaflet-vega@npm:@amoo-miki/leaflet-vega@0.8.8": + version "0.8.8" + resolved "https://registry.yarnpkg.com/@amoo-miki/leaflet-vega/-/leaflet-vega-0.8.8.tgz#675abf37d72fbea859755e982f4fd19dea776557" + integrity sha512-W2gGgFDxzy/XUx+fQJfz0NYVXsKl7V+G6QywiMcOV5NEodDId9c60up7NNf+cfM7ggpo+5BuLqrKmosuGO1CsA== dependencies: vega-spec-injector "^0.0.2" @@ -18353,10 +18353,10 @@ vega-hierarchy@~4.1.0: vega-dataflow "^5.7.3" vega-util "^1.15.2" -"vega-interpreter@npm:@amoo-miki/vega-forced-csp-compliant-interpreter@1.0.5": - version "1.0.5" - resolved "https://registry.yarnpkg.com/@amoo-miki/vega-forced-csp-compliant-interpreter/-/vega-forced-csp-compliant-interpreter-1.0.5.tgz#49970be9b00ca7e45ced0617fbf373c77a28aab4" - integrity sha512-lfeU77lVoUbSCC6N1ywdKg+I6K08xpkd82TLon+LebtKyC8aLCe7P5Dd/89zAPyFwRyobKftHu8z0xpV7R7a4Q== +"vega-interpreter@npm:@amoo-miki/vega-forced-csp-compliant-interpreter@1.0.6": + version "1.0.6" + resolved "https://registry.yarnpkg.com/@amoo-miki/vega-forced-csp-compliant-interpreter/-/vega-forced-csp-compliant-interpreter-1.0.6.tgz#5cffdf12b7fe12dc936194edd9e8519506c38716" + integrity sha512-9S5nTTVd8JVKobcWp5iwirIeePiamwH1J9uSZPuG5kcF0TUBvGu++ERKjNdst5Qck7e4R6/7vjx2wVf58XUarg== vega-label@~1.2.0: version "1.2.0"