From 4fa6e8289d89be268272abc8865bba465acd3b93 Mon Sep 17 00:00:00 2001 From: Rinku Kumar <103560761+rinku-kumar-psl@users.noreply.github.com> Date: Wed, 8 Jun 2022 05:38:38 +0530 Subject: [PATCH] Feature/error toast on invalid valueoption selection 666 (#736) * rendered default axes selected and added error toasts on Save and Apply click if invalid value option selected Signed-off-by: rinku-kumar-psl * Cypress changes for error toasts on invalid value options selected Signed-off-by: rinku-kumar-psl * added constant VIZ_CONTAIN_XY_AXIS for x, y value axis Signed-off-by: rinku-kumar-psl --- .../integration/1_event_analytics.spec.js | 44 ++++++++++++++++++ .../common/constants/explorer.ts | 3 ++ .../common/constants/shared.ts | 26 ++++++++++- .../common/types/explorer.ts | 1 + .../__snapshots__/utils.test.tsx.snap | 24 ++++++++++ .../event_analytics/explorer/explorer.tsx | 9 ++++ .../__snapshots__/config_panel.test.tsx.snap | 30 +++++++++++++ .../__tests__/config_panel.test.tsx | 3 +- .../config_panel/config_panel.tsx | 45 ++++++++++++++++++- .../explorer/visualizations/index.tsx | 3 ++ .../charts/helpers/viz_types.ts | 7 +-- 11 files changed, 188 insertions(+), 7 deletions(-) diff --git a/dashboards-observability/.cypress/integration/1_event_analytics.spec.js b/dashboards-observability/.cypress/integration/1_event_analytics.spec.js index 7a3a45352..d8dbde877 100644 --- a/dashboards-observability/.cypress/integration/1_event_analytics.spec.js +++ b/dashboards-observability/.cypress/integration/1_event_analytics.spec.js @@ -615,4 +615,48 @@ describe('Renders data view', () => { cy.get('[data-test-subj="workspace__dataTableViewSwitch"]').click(); cy.get('[data-test-subj="workspace__dataTable"]').should('not.exist'); }); +}); + +describe('Renders chart and verify Toast message if X-axis and Y-axis values are empty', () => { + beforeEach(() => { + landOnEventVisualizations(); + }); + + it('Renders chart, clear X-axis and Y-axis value and click on Apply button, Toast message should display with error message', () => { + querySearch(TEST_QUERIES[4].query, TEST_QUERIES[4].dateRangeDOM); + cy.get('[data-test-subj="configPane__vizTypeSelector"] [data-test-subj="comboBoxInput"]') + .type('Bar') + .type('{enter}'); + cy.wait(delay); + cy.get('#configPanel__value_options [data-test-subj="comboBoxClearButton"]').eq(0).click({force:true}); + cy.get('#configPanel__value_options [data-test-subj="comboBoxToggleListButton"]').eq(0).click(); + cy.wait(delay) + cy.get('#configPanel__value_options [data-test-subj="comboBoxClearButton"]').click({multiple:true}); + cy.get('#configPanel__value_options [data-test-subj="comboBoxToggleListButton"]').eq(1).click(); + cy.get('#configPanel__value_options [data-test-subj="comboBoxInput"]').eq(0).should('have.value', ''); + cy.get('#configPanel__value_options [data-test-subj="comboBoxInput"]').eq(1).should('have.value', ''); + cy.get('[data-test-subj="visualizeEditorRenderButton"]').click(); + cy.get('[data-test-subj="euiToastHeader"]').contains('Invalid value options configuration selected.').should('exist'); + }); + + it('Renders chart, clear X-axis and Y-axis value and try to save visulization, Toast message should display with error message', () => { + querySearch(TEST_QUERIES[4].query, TEST_QUERIES[4].dateRangeDOM); + cy.get('[data-test-subj="configPane__vizTypeSelector"] [data-test-subj="comboBoxInput"]') + .type('Bar') + .type('{enter}'); + cy.wait(delay); + cy.get('#configPanel__value_options [data-test-subj="comboBoxClearButton"]').eq(0).click({force:true}); + cy.get('#configPanel__value_options [data-test-subj="comboBoxToggleListButton"]').eq(0).click(); + cy.wait(delay) + cy.get('#configPanel__value_options [data-test-subj="comboBoxClearButton"]').click({multiple:true}); + cy.get('#configPanel__value_options [data-test-subj="comboBoxInput"]').eq(0).should('have.value', ''); + cy.get('#configPanel__value_options [data-test-subj="comboBoxInput"]').eq(1).should('have.value', ''); + cy.get('[data-test-subj="eventExplorer__saveManagementPopover"]').click(); + cy.get('[data-test-subj="eventExplorer__querySaveComboBox"]').click(); + cy.get('.euiComboBoxOptionsList__rowWrap .euiFilterSelectItem').eq(0).click(); + cy.get('.euiPopover__panel .euiFormControlLayoutIcons [data-test-subj="comboBoxToggleListButton"]').eq(0).click(); + cy.get('.euiPopover__panel input').eq(1).type(`Test visulization_`); + cy.get('[data-test-subj="eventExplorer__querySaveConfirm"]').click(); + cy.get('[data-test-subj="euiToastHeader"]').contains('Invalid value options configuration selected.').should('exist'); + }); }); \ No newline at end of file diff --git a/dashboards-observability/common/constants/explorer.ts b/dashboards-observability/common/constants/explorer.ts index fbe09f13b..2aac321d4 100644 --- a/dashboards-observability/common/constants/explorer.ts +++ b/dashboards-observability/common/constants/explorer.ts @@ -3,6 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { visChartTypes } from "./shared"; export const EVENT_ANALYTICS_DOCUMENTATION_URL = 'https://opensearch.org/docs/latest/observability-plugin/event-analytics/'; export const OPEN_TELEMETRY_LOG_CORRELATION_LINK = @@ -77,3 +78,5 @@ export const REDUX_EXPL_SLICE_COUNT_DISTRIBUTION = 'countDistributionVisualizati export const PLOTLY_GAUGE_COLUMN_NUMBER = 5; export const APP_ANALYTICS_TAB_ID_REGEX = /application-analytics-tab.+/; export const DEFAULT_AVAILABILITY_QUERY = 'stats count() by span( timestamp, 1h )'; + +export const VIZ_CONTAIN_XY_AXIS = [visChartTypes.Bar, visChartTypes.Histogram, visChartTypes.Line, visChartTypes.Pie]; \ No newline at end of file diff --git a/dashboards-observability/common/constants/shared.ts b/dashboards-observability/common/constants/shared.ts index 19c0fb8fd..c27d2bab1 100644 --- a/dashboards-observability/common/constants/shared.ts +++ b/dashboards-observability/common/constants/shared.ts @@ -3,6 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { IField } from '../../common/types/explorer'; import CSS from 'csstype'; // Client route @@ -71,9 +72,32 @@ export const pageStyles: CSS.Properties = { maxWidth: '1130px', }; + +export enum visChartTypes { + Bar = 'bar', + HorizontalBar = 'horizontal_bar', + Line = 'line', + Pie = 'pie', + HeatMap = 'heatmap', + Text = 'text', + Gauge = 'gauge', + Histogram = 'histogram', + TreeMap = 'tree_map' +} + +export interface ValueOptionsAxes { + xaxis ?: IField[]; + yaxis ?: IField[]; + zaxis ?: IField[]; + childField?: IField[]; + valueField?: IField[]; + series?: IField[]; + value?: IField[]; +} + export const NUMERICAL_FIELDS = ['short', 'integer', 'long', 'float', 'double']; -export const ENABLED_VIS_TYPES = ['bar', 'horizontal_bar', 'line', 'pie', 'heatmap', 'text']; +export const ENABLED_VIS_TYPES = [visChartTypes.Bar, visChartTypes.HorizontalBar, visChartTypes.Line, visChartTypes.Pie, visChartTypes.HeatMap, visChartTypes.Text]; //Live tail constants export const LIVE_OPTIONS = [ diff --git a/dashboards-observability/common/types/explorer.ts b/dashboards-observability/common/types/explorer.ts index d43aba751..f373929c3 100644 --- a/dashboards-observability/common/types/explorer.ts +++ b/dashboards-observability/common/types/explorer.ts @@ -31,6 +31,7 @@ export interface IQueryTab { export interface IField { name: string; type: string; + label?: string; } export interface ITabQueryResults { diff --git a/dashboards-observability/public/components/custom_panels/helpers/__tests__/__snapshots__/utils.test.tsx.snap b/dashboards-observability/public/components/custom_panels/helpers/__tests__/__snapshots__/utils.test.tsx.snap index 5a513f839..ba0e43feb 100644 --- a/dashboards-observability/public/components/custom_panels/helpers/__tests__/__snapshots__/utils.test.tsx.snap +++ b/dashboards-observability/public/components/custom_panels/helpers/__tests__/__snapshots__/utils.test.tsx.snap @@ -10,12 +10,14 @@ exports[`Utils helper functions renders displayVisualization function 1`] = ` "defaultAxes": Object { "xaxis": Array [ Object { + "label": "Carrier", "name": "Carrier", "type": "keyword", }, ], "yaxis": Array [ Object { + "label": "avg(FlightDelayMin)", "name": "avg(FlightDelayMin)", "type": "double", }, @@ -224,12 +226,14 @@ exports[`Utils helper functions renders displayVisualization function 1`] = ` "defaultAxes": Object { "xaxis": Array [ Object { + "label": "Carrier", "name": "Carrier", "type": "keyword", }, ], "yaxis": Array [ Object { + "label": "avg(FlightDelayMin)", "name": "avg(FlightDelayMin)", "type": "double", }, @@ -461,12 +465,14 @@ exports[`Utils helper functions renders displayVisualization function 1`] = ` "defaultAxes": Object { "xaxis": Array [ Object { + "label": "Carrier", "name": "Carrier", "type": "keyword", }, ], "yaxis": Array [ Object { + "label": "avg(FlightDelayMin)", "name": "avg(FlightDelayMin)", "type": "double", }, @@ -853,12 +859,14 @@ exports[`Utils helper functions renders displayVisualization function 2`] = ` "defaultAxes": Object { "xaxis": Array [ Object { + "label": "Carrier", "name": "Carrier", "type": "keyword", }, ], "yaxis": Array [ Object { + "label": "avg(FlightDelayMin)", "name": "avg(FlightDelayMin)", "type": "double", }, @@ -1084,12 +1092,14 @@ exports[`Utils helper functions renders displayVisualization function 2`] = ` "defaultAxes": Object { "xaxis": Array [ Object { + "label": "Carrier", "name": "Carrier", "type": "keyword", }, ], "yaxis": Array [ Object { + "label": "avg(FlightDelayMin)", "name": "avg(FlightDelayMin)", "type": "double", }, @@ -1369,12 +1379,14 @@ exports[`Utils helper functions renders displayVisualization function 2`] = ` "defaultAxes": Object { "xaxis": Array [ Object { + "label": "Carrier", "name": "Carrier", "type": "keyword", }, ], "yaxis": Array [ Object { + "label": "avg(FlightDelayMin)", "name": "avg(FlightDelayMin)", "type": "double", }, @@ -1788,12 +1800,14 @@ exports[`Utils helper functions renders displayVisualization function 3`] = ` "defaultAxes": Object { "xaxis": Array [ Object { + "label": "Carrier", "name": "Carrier", "type": "keyword", }, ], "yaxis": Array [ Object { + "label": "avg(FlightDelayMin)", "name": "avg(FlightDelayMin)", "type": "double", }, @@ -1996,12 +2010,14 @@ exports[`Utils helper functions renders displayVisualization function 3`] = ` "defaultAxes": Object { "xaxis": Array [ Object { + "label": "Carrier", "name": "Carrier", "type": "keyword", }, ], "yaxis": Array [ Object { + "label": "avg(FlightDelayMin)", "name": "avg(FlightDelayMin)", "type": "double", }, @@ -2227,12 +2243,14 @@ exports[`Utils helper functions renders displayVisualization function 3`] = ` "defaultAxes": Object { "xaxis": Array [ Object { + "label": "Carrier", "name": "Carrier", "type": "keyword", }, ], "yaxis": Array [ Object { + "label": "avg(FlightDelayMin)", "name": "avg(FlightDelayMin)", "type": "double", }, @@ -2613,12 +2631,14 @@ exports[`Utils helper functions renders displayVisualization function 4`] = ` "defaultAxes": Object { "xaxis": Array [ Object { + "label": "span(timestamp,1h)", "name": "span(timestamp,1h)", "type": "timestamp", }, ], "yaxis": Array [ Object { + "label": "count('ip')", "name": "count('ip')", "type": "integer", }, @@ -2794,12 +2814,14 @@ exports[`Utils helper functions renders displayVisualization function 4`] = ` "defaultAxes": Object { "xaxis": Array [ Object { + "label": "span(timestamp,1h)", "name": "span(timestamp,1h)", "type": "timestamp", }, ], "yaxis": Array [ Object { + "label": "count('ip')", "name": "count('ip')", "type": "integer", }, @@ -2998,12 +3020,14 @@ exports[`Utils helper functions renders displayVisualization function 4`] = ` "defaultAxes": Object { "xaxis": Array [ Object { + "label": "span(timestamp,1h)", "name": "span(timestamp,1h)", "type": "timestamp", }, ], "yaxis": Array [ Object { + "label": "count('ip')", "name": "count('ip')", "type": "integer", }, diff --git a/dashboards-observability/public/components/event_analytics/explorer/explorer.tsx b/dashboards-observability/public/components/event_analytics/explorer/explorer.tsx index a7fd42262..015bf48f5 100644 --- a/dashboards-observability/public/components/event_analytics/explorer/explorer.tsx +++ b/dashboards-observability/public/components/event_analytics/explorer/explorer.tsx @@ -141,6 +141,7 @@ export const Explorer = ({ const [browserTabFocus, setBrowserTabFocus] = useState(true); const [liveTimestamp, setLiveTimestamp] = useState(DATE_PICKER_FORMAT); const [triggerAvailability, setTriggerAvailability] = useState(false); + const [isValidDataConfigOptionSelected, setIsValidDataConfigOptionSelected] = useState(false); const queryRef = useRef(); const appBasedRef = useRef(''); @@ -727,6 +728,9 @@ export const Explorer = ({ } }; + const changeIsValidConfigOptionState = (isValidConfig: Boolean) => + setIsValidDataConfigOptionSelected(isValidConfig); + const getExplorerVis = () => { return ( ); }; @@ -835,6 +840,10 @@ export const Explorer = ({ setToast('Name field cannot be empty.', 'danger'); return; } + if (!isValidDataConfigOptionSelected) { + setToast('Invalid value options configuration selected.', 'danger'); + return; + } setIsPanelTextFieldInvalid(false); if (isEqual(selectedContentTabId, TAB_EVENT_ID)) { const isTabMatchingSavedType = isEqual(currQuery![SAVED_OBJECT_TYPE], SAVED_QUERY); diff --git a/dashboards-observability/public/components/event_analytics/explorer/visualizations/config_panel/__tests__/__snapshots__/config_panel.test.tsx.snap b/dashboards-observability/public/components/event_analytics/explorer/visualizations/config_panel/__tests__/__snapshots__/config_panel.test.tsx.snap index 065ec0e9c..babb55222 100644 --- a/dashboards-observability/public/components/event_analytics/explorer/visualizations/config_panel/__tests__/__snapshots__/config_panel.test.tsx.snap +++ b/dashboards-observability/public/components/event_analytics/explorer/visualizations/config_panel/__tests__/__snapshots__/config_panel.test.tsx.snap @@ -2,6 +2,21 @@ exports[`Config panel component Renders config panel with visualization data 1`] = ` , "id": "data-panel", "name": "Data", @@ -2962,6 +2982,11 @@ exports[`Config panel component Renders config panel with visualization data 1`] }, } } + vizState={ + Object { + "valueOptions": Object {}, + } + } />, "id": "data-panel", "name": "Data", @@ -3830,6 +3855,11 @@ exports[`Config panel component Renders config panel with visualization data 1`] }, } } + vizState={ + Object { + "valueOptions": Object {}, + } + } >
{ const tabId = 'query-panel-1'; const curVisId = 'bar'; const pplService = new PPLService(httpClientMock); + const mockChangeIsValidConfigOptionState = jest.fn(); const wrapper = mount( { pplService: pplService, }} > - + ); diff --git a/dashboards-observability/public/components/event_analytics/explorer/visualizations/config_panel/config_panel.tsx b/dashboards-observability/public/components/event_analytics/explorer/visualizations/config_panel/config_panel.tsx index 9c5b180bf..d1e8d25a8 100644 --- a/dashboards-observability/public/components/event_analytics/explorer/visualizations/config_panel/config_panel.tsx +++ b/dashboards-observability/public/components/event_analytics/explorer/visualizations/config_panel/config_panel.tsx @@ -25,7 +25,8 @@ import { getDefaultSpec } from '../visualization_specs/default_spec'; import { TabContext } from '../../../hooks'; import { DefaultEditorControls } from './config_panel_footer'; import { getVisType } from '../../../../visualizations/charts/vis_types'; -import { ENABLED_VIS_TYPES } from '../../../../../../common/constants/shared'; +import { ENABLED_VIS_TYPES, ValueOptionsAxes, visChartTypes } from '../../../../../../common/constants/shared'; +import { VIZ_CONTAIN_XY_AXIS } from '../../../../../../common/constants/explorer'; const CONFIG_LAYOUT_TEMPLATE = ` { @@ -60,13 +61,26 @@ interface PanelTabType { content?: any; } -export const ConfigPanel = ({ visualizations, setCurVisId, callback }: any) => { +export const ConfigPanel = ({ visualizations, setCurVisId, callback, changeIsValidConfigOptionState }: any) => { const { tabId, curVisId, dispatch, changeVisualizationConfig, setToast } = useContext( TabContext ); const { data, vis } = visualizations; const { userConfigs } = data; + const getDefaultAxisSelected = () => { + let chartBasedAxes: ValueOptionsAxes = {}; + const [valueField] = data.defaultAxes?.yaxis ?? []; + if (curVisId === visChartTypes.TreeMap) { + chartBasedAxes["childField"] = data.defaultAxes.xaxis ?? []; + chartBasedAxes["valueField"] = [valueField]; + } else { + chartBasedAxes = { ...data.defaultAxes }; + } + return { + valueOptions: { ...(chartBasedAxes && chartBasedAxes) } + } + } const [vizConfigs, setVizConfigs] = useState({ dataConfig: {}, layoutConfig: userConfigs?.layoutConfig @@ -78,6 +92,7 @@ export const ConfigPanel = ({ visualizations, setCurVisId, callback }: any) => { useEffect(() => { setVizConfigs({ ...userConfigs, + dataConfig: { ...vizConfigs.dataConfig, ...(userConfigs?.dataConfig ? userConfigs.dataConfig : getDefaultAxisSelected()) }, layoutConfig: userConfigs?.layoutConfig ? hjson.stringify({ ...userConfigs.layoutConfig }, HJSON_STRINGIFY_OPTIONS) : getDefaultSpec(), @@ -95,8 +110,34 @@ export const ConfigPanel = ({ visualizations, setCurVisId, callback }: any) => { [] ); + // To check, If user empty any of the value options + const isValidValueOptionConfigSelected = useMemo(() => { + const valueOptions = vizConfigs.dataConfig?.valueOptions; + const { TreeMap, Gauge, HeatMap } = visChartTypes; + const isValidValueOptionsXYAxes = VIZ_CONTAIN_XY_AXIS.includes(curVisId) && + valueOptions?.xaxis?.length !== 0 && valueOptions?.yaxis?.length !== 0; + + const isValid_valueOptions: { [key: string]: boolean } = { + tree_map: curVisId === TreeMap && valueOptions?.childField?.length !== 0 && + valueOptions?.valueField?.length !== 0, + gauge: Boolean(curVisId === Gauge && valueOptions?.series && valueOptions.series?.length !== 0 && + valueOptions?.value && valueOptions.value?.length !== 0), + heatmap: Boolean(curVisId === HeatMap && valueOptions?.zaxis && valueOptions.zaxis?.length !== 0), + bar: isValidValueOptionsXYAxes, + line: isValidValueOptionsXYAxes, + histogram: isValidValueOptionsXYAxes, + pie: isValidValueOptionsXYAxes + } + return isValid_valueOptions[curVisId]; + }, [vizConfigs.dataConfig]); + + useEffect(() => changeIsValidConfigOptionState(Boolean(isValidValueOptionConfigSelected)), [isValidValueOptionConfigSelected]); + const handleConfigUpdate = useCallback(() => { try { + if (!isValidValueOptionConfigSelected) { + setToast(`Invalid value options configuration selected.`, 'danger'); + } dispatch( changeVisualizationConfig({ tabId, diff --git a/dashboards-observability/public/components/event_analytics/explorer/visualizations/index.tsx b/dashboards-observability/public/components/event_analytics/explorer/visualizations/index.tsx index 16132957a..8e8be634a 100644 --- a/dashboards-observability/public/components/event_analytics/explorer/visualizations/index.tsx +++ b/dashboards-observability/public/components/event_analytics/explorer/visualizations/index.tsx @@ -27,6 +27,7 @@ interface IExplorerVisualizationsProps { visualizations: IVisualizationContainerProps; handleOverrideTimestamp: (field: IField) => void; callback?: any; + changeIsValidConfigOptionState: (isValidConfigOptionSelected: Boolean) => void; } export const ExplorerVisualizations = ({ @@ -41,6 +42,7 @@ export const ExplorerVisualizations = ({ visualizations, handleOverrideTimestamp, callback, + changeIsValidConfigOptionState }: IExplorerVisualizationsProps) => { return ( @@ -76,6 +78,7 @@ export const ExplorerVisualizations = ({ curVisId={curVisId} setCurVisId={setCurVisId} callback={callback} + changeIsValidConfigOptionState={changeIsValidConfigOptionState} /> diff --git a/dashboards-observability/public/components/visualizations/charts/helpers/viz_types.ts b/dashboards-observability/public/components/visualizations/charts/helpers/viz_types.ts index 4b7eb2332..5fbec3e02 100644 --- a/dashboards-observability/public/components/visualizations/charts/helpers/viz_types.ts +++ b/dashboards-observability/public/components/visualizations/charts/helpers/viz_types.ts @@ -20,11 +20,12 @@ interface IVizContainerProps { }; } -const getDefaultXYAxisLabels = (vizFields: string[]) => { +const getDefaultXYAxisLabels = (vizFields: IField[]) => { if (isEmpty(vizFields)) return {}; + const vizFieldsWithLabel = vizFields.map(vizField => ({ ...vizField, label: vizField.name })); return { - xaxis: [vizFields[vizFields.length - 1]] || [], - yaxis: take(vizFields, vizFields.length - 1 > 0 ? vizFields.length - 1 : 1) || [], + xaxis: [vizFieldsWithLabel[vizFieldsWithLabel.length - 1]] || [], + yaxis: take(vizFieldsWithLabel, vizFieldsWithLabel.length - 1 > 0 ? vizFieldsWithLabel.length - 1 : 1) || [], }; };