From 07a67d724710109bd95800dd5ce973e83dbbe10a Mon Sep 17 00:00:00 2001 From: Tyler Ohlsen Date: Tue, 11 Jul 2023 12:04:25 -0700 Subject: [PATCH] [Vis Augmenter / Feature Anywhere] Add tests in core OSD and AD plugin (#739) * [Vis Augmenter / Feature Anywhere] Add test suite for vanilla OSD + helper fns for plugins (#725) * feature anywhere initial tests Signed-off-by: Jovan Cvetkovic * Add test suite Signed-off-by: Tyler Ohlsen * Remove unnecessary test case Signed-off-by: Tyler Ohlsen * Optimize getters Signed-off-by: Tyler Ohlsen --------- Signed-off-by: Jovan Cvetkovic Signed-off-by: Tyler Ohlsen Co-authored-by: Jovan Cvetkovic * [Feature Anywhere / Vis Augmenter] Add test flows for integration with AD plugin (#727) * feature anywhere initial tests Signed-off-by: Jovan Cvetkovic * Add test suite Signed-off-by: Tyler Ohlsen * Add AD vis augmenter tests Signed-off-by: Tyler Ohlsen * More refactoring Signed-off-by: Tyler Ohlsen * More tests Signed-off-by: Tyler Ohlsen * Add test for AD cleanup scenario Signed-off-by: Tyler Ohlsen * Set up saved obj test suite Signed-off-by: Tyler Ohlsen * Add reminder TODO Signed-off-by: Tyler Ohlsen * Add tests regarding saved obj visibility Signed-off-by: Tyler Ohlsen * Add view events tests Signed-off-by: Tyler Ohlsen * cleanup Signed-off-by: Tyler Ohlsen * remove import Signed-off-by: Tyler Ohlsen --------- Signed-off-by: Jovan Cvetkovic Signed-off-by: Tyler Ohlsen Co-authored-by: Jovan Cvetkovic --------- Signed-off-by: Jovan Cvetkovic Signed-off-by: Tyler Ohlsen Co-authored-by: Jovan Cvetkovic --- .../sample-data-simple/data.json | 27 ++ .../index-pattern-fields.txt | 1 + .../sample-data-simple/index-settings.txt | 1 + .../apps/vis-augmenter/dashboard_spec.js | 129 +++++++ .../sample_detector_spec.js | 20 +- .../vis_augmenter/associate_detector_spec.js | 145 ++++++++ .../augment_vis_saved_object_spec.js | 119 +++++++ .../vis_augmenter/view_anomaly_events_spec.js | 111 ++++++ cypress/utils/dashboards/commands.js | 1 + cypress/utils/dashboards/constants.js | 1 + .../dashboards/vis-augmenter/commands.js | 48 +++ .../dashboards/vis-augmenter/constants.js | 15 + .../utils/dashboards/vis-augmenter/helpers.js | 325 ++++++++++++++++++ .../utils/dashboards/vis-augmenter/index.d.ts | 45 +++ cypress/utils/helpers.js | 1 + .../helpers.js | 116 +++++++ 16 files changed, 1086 insertions(+), 19 deletions(-) create mode 100644 cypress/fixtures/dashboard/opensearch_dashboards/vis-augmenter/sample-data-simple/data.json create mode 100644 cypress/fixtures/dashboard/opensearch_dashboards/vis-augmenter/sample-data-simple/index-pattern-fields.txt create mode 100644 cypress/fixtures/dashboard/opensearch_dashboards/vis-augmenter/sample-data-simple/index-settings.txt create mode 100644 cypress/integration/core-opensearch-dashboards/opensearch-dashboards/apps/vis-augmenter/dashboard_spec.js create mode 100644 cypress/integration/plugins/anomaly-detection-dashboards-plugin/vis_augmenter/associate_detector_spec.js create mode 100644 cypress/integration/plugins/anomaly-detection-dashboards-plugin/vis_augmenter/augment_vis_saved_object_spec.js create mode 100644 cypress/integration/plugins/anomaly-detection-dashboards-plugin/vis_augmenter/view_anomaly_events_spec.js create mode 100644 cypress/utils/dashboards/vis-augmenter/commands.js create mode 100644 cypress/utils/dashboards/vis-augmenter/constants.js create mode 100644 cypress/utils/dashboards/vis-augmenter/helpers.js create mode 100644 cypress/utils/dashboards/vis-augmenter/index.d.ts diff --git a/cypress/fixtures/dashboard/opensearch_dashboards/vis-augmenter/sample-data-simple/data.json b/cypress/fixtures/dashboard/opensearch_dashboards/vis-augmenter/sample-data-simple/data.json new file mode 100644 index 000000000..5124ea41e --- /dev/null +++ b/cypress/fixtures/dashboard/opensearch_dashboards/vis-augmenter/sample-data-simple/data.json @@ -0,0 +1,27 @@ +[ + { + "value1": 1, + "value2": 10, + "value3": 5 + }, + { + "value1": 5, + "value2": 1, + "value3": 3 + }, + { + "value1": 9, + "value2": 6, + "value3": 2 + }, + { + "value1": 2, + "value2": 1, + "value3": 1 + }, + { + "value1": 12, + "value2": 5, + "value3": 4 + } +] diff --git a/cypress/fixtures/dashboard/opensearch_dashboards/vis-augmenter/sample-data-simple/index-pattern-fields.txt b/cypress/fixtures/dashboard/opensearch_dashboards/vis-augmenter/sample-data-simple/index-pattern-fields.txt new file mode 100644 index 000000000..2e010deba --- /dev/null +++ b/cypress/fixtures/dashboard/opensearch_dashboards/vis-augmenter/sample-data-simple/index-pattern-fields.txt @@ -0,0 +1 @@ +[{"count":0,"name":"@timestamp","type":"date","esTypes":["date"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true},{"count":0,"name":"_id","type":"string","esTypes":["_id"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":false},{"count":0,"name":"_index","type":"string","esTypes":["_index"],"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":false},{"count":0,"name":"_score","type":"number","scripted":false,"searchable":false,"aggregatable":false,"readFromDocValues":false},{"count":0,"name":"_source","type":"_source","esTypes":["_source"],"scripted":false,"searchable":false,"aggregatable":false,"readFromDocValues":false},{"count":0,"name":"_type","type":"string","scripted":false,"searchable":false,"aggregatable":false,"readFromDocValues":false},{"count":0,"name":"value1","type":"number","scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":false},{"count":0,"name":"value2","type":"number","scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":false},{"count":0,"name":"value3","type":"number","scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":false}] \ No newline at end of file diff --git a/cypress/fixtures/dashboard/opensearch_dashboards/vis-augmenter/sample-data-simple/index-settings.txt b/cypress/fixtures/dashboard/opensearch_dashboards/vis-augmenter/sample-data-simple/index-settings.txt new file mode 100644 index 000000000..d4d78055d --- /dev/null +++ b/cypress/fixtures/dashboard/opensearch_dashboards/vis-augmenter/sample-data-simple/index-settings.txt @@ -0,0 +1 @@ +{"mappings":{"properties":{"value1":{"type":"integer"},"value2":{"type":"integer"},"value3":{"type":"integer"},"@timestamp":{"type":"date", "format":"epoch_millis"}}},"settings":{"index":{"number_of_shards":"1","number_of_replicas":"1"}}} \ No newline at end of file diff --git a/cypress/integration/core-opensearch-dashboards/opensearch-dashboards/apps/vis-augmenter/dashboard_spec.js b/cypress/integration/core-opensearch-dashboards/opensearch-dashboards/apps/vis-augmenter/dashboard_spec.js new file mode 100644 index 000000000..d97647c55 --- /dev/null +++ b/cypress/integration/core-opensearch-dashboards/opensearch-dashboards/apps/vis-augmenter/dashboard_spec.js @@ -0,0 +1,129 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + INDEX_PATTERN_FILEPATH_SIMPLE, + INDEX_SETTINGS_FILEPATH_SIMPLE, + SAMPLE_DATA_FILEPATH_SIMPLE, +} from '../../../../../utils/constants'; +import { + deleteVisAugmenterData, + bootstrapDashboard, +} from '../../../../../utils/dashboards/vis-augmenter/helpers'; + +describe('Vis augmenter - existing dashboards work as expected', () => { + describe('dashboard with ineligible, eligible, and vega visualizations', () => { + const indexName = 'vis-augmenter-sample-index'; + const indexPatternName = 'vis-augmenter-sample-*'; + const dashboardName = 'Vis Augmenter Dashboard'; + const visualizationSpecs = [ + { + name: 'count-agg-vis', + type: 'line', + indexPattern: indexPatternName, + metrics: [], + }, + { + name: 'single-metric-vis', + type: 'line', + indexPattern: indexPatternName, + metrics: [ + { + aggregation: 'Average', + field: 'value1', + }, + ], + }, + { + name: 'multi-metric-vis', + type: 'line', + indexPattern: indexPatternName, + metrics: [ + { + aggregation: 'Average', + field: 'value1', + }, + { + aggregation: 'Average', + field: 'value2', + }, + { + aggregation: 'Max', + field: 'value3', + }, + ], + }, + { + name: 'area-vis', + type: 'area', + indexPattern: indexPatternName, + metrics: [ + { + aggregation: 'Max', + field: 'value2', + }, + ], + }, + { + name: 'vega-vis', + type: 'vega', + indexPattern: indexPatternName, + metrics: [], + }, + ]; + + const visualizationNames = visualizationSpecs.map( + (visualizationSpec) => visualizationSpec.name + ); + + before(() => { + // Create a dashboard and add some visualizations + bootstrapDashboard( + INDEX_SETTINGS_FILEPATH_SIMPLE, + INDEX_PATTERN_FILEPATH_SIMPLE, + SAMPLE_DATA_FILEPATH_SIMPLE, + indexName, + indexPatternName, + dashboardName, + visualizationSpecs + ); + }); + + beforeEach(() => { + cy.visitDashboard(dashboardName); + }); + + after(() => { + deleteVisAugmenterData( + indexName, + indexPatternName, + visualizationNames, + dashboardName + ); + }); + + it('View events option does not exist for any visualization', () => { + visualizationNames.forEach((visualizationName) => { + cy.getVisPanelByTitle(visualizationName) + .openVisContextMenu() + .getMenuItems() + .contains('View Events') + .should('not.exist'); + }); + }); + + it('Validate non-vega visualizations are not rendered with vega under the hood', () => { + visualizationSpecs.forEach((visualizationSpec) => { + cy.getVisPanelByTitle(visualizationSpec.name).within(() => { + if (visualizationSpec.type === 'vega') { + cy.get('.vgaVis__view').should('exist'); + } else { + cy.get('.vgaVis__view').should('not.exist'); + } + }); + }); + }); + }); +}); diff --git a/cypress/integration/plugins/anomaly-detection-dashboards-plugin/sample_detector_spec.js b/cypress/integration/plugins/anomaly-detection-dashboards-plugin/sample_detector_spec.js index 8008eabbf..ebcab7e29 100644 --- a/cypress/integration/plugins/anomaly-detection-dashboards-plugin/sample_detector_spec.js +++ b/cypress/integration/plugins/anomaly-detection-dashboards-plugin/sample_detector_spec.js @@ -3,27 +3,9 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { AD_URL } from '../../../utils/constants'; +import { createSampleDetector } from '../../../utils/helpers'; context('Sample detectors', () => { - // Helper fn that takes in a button test ID to determine - // the sample detector to create - const createSampleDetector = (createButtonDataTestSubj) => { - cy.visit(AD_URL.OVERVIEW); - - cy.getElementByTestId('overviewTitle').should('exist'); - cy.getElementByTestId('viewSampleDetectorLink').should('not.exist'); - cy.getElementByTestId(createButtonDataTestSubj).click(); - cy.visit(AD_URL.OVERVIEW); - - // Check that the details page defaults to real-time, and shows detector is initializing - cy.getElementByTestId('viewSampleDetectorLink').click(); - cy.getElementByTestId('detectorNameHeader').should('exist'); - cy.getElementByTestId('sampleIndexDetailsCallout').should('exist'); - cy.getElementByTestId('realTimeResultsHeader').should('exist'); - cy.getElementByTestId('detectorStateInitializing').should('exist'); - }; - beforeEach(() => { cy.deleteAllIndices(); cy.deleteADSystemIndices(); diff --git a/cypress/integration/plugins/anomaly-detection-dashboards-plugin/vis_augmenter/associate_detector_spec.js b/cypress/integration/plugins/anomaly-detection-dashboards-plugin/vis_augmenter/associate_detector_spec.js new file mode 100644 index 000000000..7a9c8ee4d --- /dev/null +++ b/cypress/integration/plugins/anomaly-detection-dashboards-plugin/vis_augmenter/associate_detector_spec.js @@ -0,0 +1,145 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + deleteVisAugmenterData, + bootstrapDashboard, + openAddAnomalyDetectorFlyout, + openAssociatedDetectorsFlyout, + createDetectorFromVis, + associateDetectorFromVis, + unlinkDetectorFromVis, + ensureDetectorIsLinked, + ensureDetectorDetails, + openDetectorDetailsPageFromFlyout, +} from '../../../../utils/helpers'; +import { + INDEX_PATTERN_FILEPATH_SIMPLE, + INDEX_SETTINGS_FILEPATH_SIMPLE, + SAMPLE_DATA_FILEPATH_SIMPLE, +} from '../../../../utils/constants'; + +describe('Anomaly detection integration with vis augmenter', () => { + const indexName = 'ad-vis-augmenter-sample-index'; + const indexPatternName = 'ad-vis-augmenter-sample-*'; + const dashboardName = 'AD Vis Augmenter Dashboard'; + const detectorName = 'ad-vis-augmenter-detector'; + const visualizationName = 'single-metric-vis'; + const visualizationSpec = { + name: visualizationName, + type: 'line', + indexPattern: indexPatternName, + metrics: [ + { + aggregation: 'Average', + field: 'value1', + }, + ], + }; + + before(() => { + // Create a dashboard and add some visualizations + cy.wait(5000); + bootstrapDashboard( + INDEX_SETTINGS_FILEPATH_SIMPLE, + INDEX_PATTERN_FILEPATH_SIMPLE, + SAMPLE_DATA_FILEPATH_SIMPLE, + indexName, + indexPatternName, + dashboardName, + [visualizationSpec] + ); + }); + + after(() => { + deleteVisAugmenterData( + indexName, + indexPatternName, + [visualizationName], + dashboardName + ); + cy.deleteADSystemIndices(); + }); + + beforeEach(() => {}); + + afterEach(() => {}); + + it('Shows empty state when no associated detectors', () => { + openAssociatedDetectorsFlyout(dashboardName, visualizationName); + cy.getElementByTestId('emptyAssociatedDetectorFlyoutMessage'); + }); + + it('Create new detector from visualization', () => { + openAddAnomalyDetectorFlyout(dashboardName, visualizationName); + createDetectorFromVis(detectorName); + + ensureDetectorIsLinked(dashboardName, visualizationName, detectorName); + + // Since this detector is created based off of vis metrics, we assume here + // the number of features will equal the number of metrics we have specified. + ensureDetectorDetails(detectorName, visualizationSpec.metrics.length); + + unlinkDetectorFromVis(dashboardName, visualizationName, detectorName); + }); + + it('Associate existing detector - creation flow', () => { + openAddAnomalyDetectorFlyout(dashboardName, visualizationName); + + cy.get('.euiFlyout').find('.euiTitle').contains('Add anomaly detector'); + // ensuring the flyout is defaulting to detector creation vs. association + cy.getElementByTestId('adAnywhereCreateDetectorButton'); + cy.get('[id="add-anomaly-detector__existing"]').click(); + + associateDetectorFromVis(detectorName); + + ensureDetectorIsLinked(dashboardName, visualizationName, detectorName); + unlinkDetectorFromVis(dashboardName, visualizationName, detectorName); + }); + + it('Associate existing detector - associated detectors flow', () => { + openAssociatedDetectorsFlyout(dashboardName, visualizationName); + cy.getElementByTestId('associateDetectorButton').click(); + associateDetectorFromVis(detectorName); + + ensureDetectorIsLinked(dashboardName, visualizationName, detectorName); + unlinkDetectorFromVis(dashboardName, visualizationName, detectorName); + }); + + it('Deleting linked detector shows error once and removes from associated detectors list', () => { + openAssociatedDetectorsFlyout(dashboardName, visualizationName); + cy.getElementByTestId('associateDetectorButton').click(); + associateDetectorFromVis(detectorName); + ensureDetectorIsLinked(dashboardName, visualizationName, detectorName); + openDetectorDetailsPageFromFlyout(); + cy.getElementByTestId('configurationsTab').click(); + cy.getElementByTestId('detectorNameHeader').within(() => { + cy.contains(detectorName); + }); + + cy.getElementByTestId('actionsButton').click(); + cy.getElementByTestId('deleteDetectorItem').click(); + cy.getElementByTestId('typeDeleteField').type('delete', { force: true }); + cy.getElementByTestId('confirmButton').click(); + cy.wait(5000); + + cy.visitDashboard(dashboardName); + + // Expect an error message to show up + cy.getElementByTestId('errorToastMessage').parent().find('button').click(); + cy.get('.euiModal'); + cy.get('.euiModalFooter').find('button').click(); + cy.wait(2000); + + // Expect associated detector list to be empty (the association should be removed) + openAssociatedDetectorsFlyout(dashboardName, visualizationName); + cy.getElementByTestId('emptyAssociatedDetectorFlyoutMessage'); + cy.wait(2000); + + // Reload the dashboard - error toast shouldn't show anymore + cy.visitDashboard(dashboardName); + cy.getElementByTestId('errorToastMessage').should('not.exist'); + }); +}); diff --git a/cypress/integration/plugins/anomaly-detection-dashboards-plugin/vis_augmenter/augment_vis_saved_object_spec.js b/cypress/integration/plugins/anomaly-detection-dashboards-plugin/vis_augmenter/augment_vis_saved_object_spec.js new file mode 100644 index 000000000..6ad1b4302 --- /dev/null +++ b/cypress/integration/plugins/anomaly-detection-dashboards-plugin/vis_augmenter/augment_vis_saved_object_spec.js @@ -0,0 +1,119 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { CommonUI } from '@opensearch-dashboards-test/opensearch-dashboards-test-library'; +import { + deleteVisAugmenterData, + bootstrapDashboard, + openAddAnomalyDetectorFlyout, + createDetectorFromVis, + unlinkDetectorFromVis, + ensureDetectorIsLinked, + filterByObjectType, +} from '../../../../utils/helpers'; +import { + INDEX_PATTERN_FILEPATH_SIMPLE, + INDEX_SETTINGS_FILEPATH_SIMPLE, + SAMPLE_DATA_FILEPATH_SIMPLE, +} from '../../../../utils/constants'; + +describe('AD augment-vis saved objects', () => { + const commonUI = new CommonUI(cy); + const indexName = 'ad-vis-augmenter-sample-index'; + const indexPatternName = 'ad-vis-augmenter-sample-*'; + const dashboardName = 'AD Vis Augmenter Dashboard'; + const detectorName = 'ad-vis-augmenter-detector'; + const visualizationName = 'single-metric-vis'; + const visualizationSpec = { + name: visualizationName, + type: 'line', + indexPattern: indexPatternName, + metrics: [ + { + aggregation: 'Average', + field: 'value1', + }, + ], + }; + + before(() => { + // Create a dashboard and add some visualizations + cy.wait(5000); + bootstrapDashboard( + INDEX_SETTINGS_FILEPATH_SIMPLE, + INDEX_PATTERN_FILEPATH_SIMPLE, + SAMPLE_DATA_FILEPATH_SIMPLE, + indexName, + indexPatternName, + dashboardName, + [visualizationSpec] + ); + }); + + after(() => { + deleteVisAugmenterData( + indexName, + indexPatternName, + [visualizationName], + dashboardName + ); + cy.deleteADSystemIndices(); + }); + + beforeEach(() => {}); + + afterEach(() => {}); + + it('Associating a detector creates a visible saved object', () => { + openAddAnomalyDetectorFlyout(dashboardName, visualizationName); + createDetectorFromVis(detectorName); + ensureDetectorIsLinked(dashboardName, visualizationName, detectorName); + + cy.visitSavedObjectsManagement(); + filterByObjectType('augment-vis'); + cy.getElementByTestId('savedObjectsTable') + .find('.euiTableRow') + .should('have.length', 1); + }); + + it('Created AD saved object has correct fields', () => { + cy.visitSavedObjectsManagement(); + filterByObjectType('augment-vis'); + cy.getElementByTestId('savedObjectsTableAction-inspect').click(); + cy.contains('originPlugin'); + commonUI.checkElementExists('[value="anomalyDetectionDashboards"]', 1); + cy.contains('pluginResource.type'); + commonUI.checkElementExists('[value="Anomaly Detectors"]', 1); + cy.contains('pluginResource.id'); + cy.contains('visLayerExpressionFn.type'); + commonUI.checkElementExists('[value="PointInTimeEvents"]', 1); + cy.contains('visLayerExpressionFn.name'); + commonUI.checkElementExists('[value="overlay_anomalies"]', 1); + }); + + it('Removing an association deletes the saved object', () => { + unlinkDetectorFromVis(dashboardName, visualizationName, detectorName); + + cy.visitSavedObjectsManagement(); + filterByObjectType('augment-vis'); + cy.getElementByTestId('savedObjectsTable') + .find('.euiTableRow') + .contains('No items found'); + }); + + it('Deleting the visualization from the edit view deletes the saved object', () => { + cy.visitSavedObjectsManagement(); + filterByObjectType('visualization'); + cy.getElementByTestId('savedObjectsTableAction-inspect').click(); + cy.getElementByTestId('savedObjectEditDelete').click(); + cy.getElementByTestId('confirmModalConfirmButton').click(); + cy.wait(3000); + + filterByObjectType('augment-vis'); + cy.getElementByTestId('savedObjectsTable') + .find('.euiTableRow') + .contains('No items found'); + }); +}); diff --git a/cypress/integration/plugins/anomaly-detection-dashboards-plugin/vis_augmenter/view_anomaly_events_spec.js b/cypress/integration/plugins/anomaly-detection-dashboards-plugin/vis_augmenter/view_anomaly_events_spec.js new file mode 100644 index 000000000..40583a39f --- /dev/null +++ b/cypress/integration/plugins/anomaly-detection-dashboards-plugin/vis_augmenter/view_anomaly_events_spec.js @@ -0,0 +1,111 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + deleteVisAugmenterData, + bootstrapDashboard, + openAddAnomalyDetectorFlyout, + createDetectorFromVis, + unlinkDetectorFromVis, + ensureDetectorIsLinked, + openViewEventsFlyout, +} from '../../../../utils/helpers'; +import { + INDEX_PATTERN_FILEPATH_SIMPLE, + INDEX_SETTINGS_FILEPATH_SIMPLE, + SAMPLE_DATA_FILEPATH_SIMPLE, +} from '../../../../utils/constants'; + +describe('View anomaly events in flyout', () => { + const indexName = 'ad-vis-augmenter-sample-index'; + const indexPatternName = 'ad-vis-augmenter-sample-*'; + const dashboardName = 'AD Vis Augmenter Dashboard'; + const detectorName = 'ad-vis-augmenter-detector'; + const visualizationName = 'single-metric-vis'; + const visualizationSpec = { + name: visualizationName, + type: 'line', + indexPattern: indexPatternName, + metrics: [ + { + aggregation: 'Average', + field: 'value1', + }, + ], + }; + + before(() => { + // Create a dashboard and add some visualizations + cy.wait(5000); + bootstrapDashboard( + INDEX_SETTINGS_FILEPATH_SIMPLE, + INDEX_PATTERN_FILEPATH_SIMPLE, + SAMPLE_DATA_FILEPATH_SIMPLE, + indexName, + indexPatternName, + dashboardName, + [visualizationSpec] + ); + }); + + after(() => { + deleteVisAugmenterData( + indexName, + indexPatternName, + [visualizationName], + dashboardName + ); + cy.deleteADSystemIndices(); + }); + + beforeEach(() => {}); + + afterEach(() => {}); + + it('Action does not exist if there are no VisLayers for a visualization', () => { + cy.getVisPanelByTitle(visualizationName) + .openVisContextMenu() + .getMenuItems() + .contains('View Events') + .should('not.exist'); + }); + + it('Action does exist if there are VisLayers for a visualization', () => { + openAddAnomalyDetectorFlyout(dashboardName, visualizationName); + createDetectorFromVis(detectorName); + ensureDetectorIsLinked(dashboardName, visualizationName, detectorName); + + cy.visitDashboard(dashboardName); + cy.getVisPanelByTitle(visualizationName) + .openVisContextMenu() + .getMenuItems() + .contains('View Events') + .should('exist'); + }); + + it('Basic components show up in flyout', () => { + openViewEventsFlyout(dashboardName, visualizationName); + cy.get('.euiFlyoutHeader').contains(visualizationName); + cy.getElementByTestId('baseVis'); + cy.getElementByTestId('eventVis'); + cy.getElementByTestId('timelineVis'); + cy.getElementByTestId('pluginResourceDescription'); + cy.getElementByTestId('pluginResourceDescription').within(() => { + cy.contains(detectorName); + cy.get('.euiLink'); + cy.get(`[target="_blank"]`); + }); + }); + + it('Removing all VisLayers hides the view events action again', () => { + unlinkDetectorFromVis(dashboardName, visualizationName, detectorName); + cy.visitDashboard(dashboardName); + cy.getVisPanelByTitle(visualizationName) + .openVisContextMenu() + .getMenuItems() + .contains('View Events') + .should('not.exist'); + }); +}); diff --git a/cypress/utils/dashboards/commands.js b/cypress/utils/dashboards/commands.js index e7a4e39b1..54615c999 100644 --- a/cypress/utils/dashboards/commands.js +++ b/cypress/utils/dashboards/commands.js @@ -5,6 +5,7 @@ import './vis_builder/commands'; import './vis_type_table/commands'; +import './vis-augmenter/commands'; Cypress.Commands.add('waitForLoader', () => { const opts = { log: false }; diff --git a/cypress/utils/dashboards/constants.js b/cypress/utils/dashboards/constants.js index e16741d1e..c2c797c85 100644 --- a/cypress/utils/dashboards/constants.js +++ b/cypress/utils/dashboards/constants.js @@ -17,3 +17,4 @@ export const SAVED_OBJECTS_PATH = export * from './vis_builder/constants'; export * from './vis_type_table/constants'; +export * from './vis-augmenter/constants'; diff --git a/cypress/utils/dashboards/vis-augmenter/commands.js b/cypress/utils/dashboards/vis-augmenter/commands.js new file mode 100644 index 000000000..e0ee418de --- /dev/null +++ b/cypress/utils/dashboards/vis-augmenter/commands.js @@ -0,0 +1,48 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { BASE_PATH } from '../../constants'; + +Cypress.Commands.add('getVisPanelByTitle', (title) => + cy.get(`[data-title="${title}"]`).parents('.embPanel').should('be.visible') +); + +Cypress.Commands.add('openVisContextMenu', { prevSubject: true }, (panel) => + cy + .wrap(panel) + .find(`[data-test-subj="embeddablePanelContextMenuClosed"]`) + .click() + .then(() => cy.get('.euiContextMenu')) +); + +Cypress.Commands.add( + 'clickVisPanelMenuItem', + { prevSubject: 'optional' }, + (menu, text) => + (menu ? cy.wrap(menu) : cy.get('.euiContextMenu')) + .find('button') + .contains(text) + .click() +); + +Cypress.Commands.add('getMenuItems', { prevSubject: 'optional' }, (menu) => + (menu ? cy.wrap(menu) : cy.get('.euiContextMenu')).find('button') +); + +Cypress.Commands.add('visitDashboard', (dashboardName) => { + cy.visit(`${BASE_PATH}/app/dashboards`); + cy.wait(2000); + cy.get('.euiFieldSearch').type(dashboardName); + cy.wait(2000); + cy.get('[data-test-subj="itemsInMemTable"]').contains(dashboardName).click({ + force: true, + }); + cy.wait(5000); +}); + +Cypress.Commands.add('visitSavedObjectsManagement', () => { + cy.visit(`${BASE_PATH}/app/management/opensearch-dashboards/objects`); + cy.wait(5000); +}); diff --git a/cypress/utils/dashboards/vis-augmenter/constants.js b/cypress/utils/dashboards/vis-augmenter/constants.js new file mode 100644 index 000000000..6c7bfa365 --- /dev/null +++ b/cypress/utils/dashboards/vis-augmenter/constants.js @@ -0,0 +1,15 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +const SAMPLE_DATA_DIR_SIMPLE = + 'dashboard/opensearch_dashboards/vis-augmenter/sample-data-simple/'; + +export const SAMPLE_DATA_FILEPATH_SIMPLE = SAMPLE_DATA_DIR_SIMPLE + 'data.json'; + +export const INDEX_PATTERN_FILEPATH_SIMPLE = + SAMPLE_DATA_DIR_SIMPLE + 'index-pattern-fields.txt'; + +export const INDEX_SETTINGS_FILEPATH_SIMPLE = + SAMPLE_DATA_DIR_SIMPLE + 'index-settings.txt'; diff --git a/cypress/utils/dashboards/vis-augmenter/helpers.js b/cypress/utils/dashboards/vis-augmenter/helpers.js new file mode 100644 index 000000000..73ebbf12e --- /dev/null +++ b/cypress/utils/dashboards/vis-augmenter/helpers.js @@ -0,0 +1,325 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { isEmpty } from 'lodash'; +import { MiscUtils } from '@opensearch-dashboards-test/opensearch-dashboards-test-library'; + +const apiRequest = (url, method = 'POST', body = undefined, qs = undefined) => + cy.request({ + method: method, + failOnStatusCode: false, + url: url, + headers: { + 'content-type': 'application/json', + 'osd-xsrf': true, + }, + body: body, + qs: qs, + }); + +const devToolsRequest = ( + url, + method = 'POST', + body = undefined, + qs = undefined +) => + cy.request({ + method: 'POST', + form: false, + failOnStatusCode: false, + url: encodeURI(`api/console/proxy?path=${url}&method=${method}`), + headers: { + 'content-type': 'application/json;charset=UTF-8', + 'osd-xsrf': true, + }, + body: body, + qs: qs, + }); + +/** + * Cleans up the index & all associated saved objects (index pattern, visualizations, + * dashboards, etc.) created during the test run + */ +export const deleteVisAugmenterData = ( + indexName, + indexPatternName, + visualizationNames, + dashboardName +) => { + devToolsRequest(indexName, 'DELETE').then(() => { + apiRequest( + `api/saved_objects/index-pattern/${indexPatternName}`, + 'DELETE' + ).then(() => { + apiRequest( + ` +api/opensearch-dashboards/management/saved_objects/_find?perPage=5000&page=1&fields=id&type=config&type=url&type=index-pattern&type=query&type=dashboard&type=visualization&type=visualization-visbuilder&type=augment-vis&type=search&sortField=type`, + 'GET' + ).then((response) => { + response.body.saved_objects.forEach((obj) => { + if ( + obj.type !== 'config' && + [ + indexName, + indexPatternName, + ...visualizationNames, + dashboardName, + ].indexOf(obj.meta.title) !== -1 + ) { + apiRequest( + `api/saved_objects/${obj.type}/${obj.id}?force=true`, + 'DELETE' + ); + } + }); + }); + }); + }); +}; + +/** + * Fetch the fixtures to create and configure an index, index pattern, + * and ingesting sample data to the index + */ +export const ingestVisAugmenterData = ( + indexName, + indexPatternName, + indexSettingsFilepath, + indexPatternFieldsFilepath, + sampleDataFilepath +) => { + cy.fixture(indexSettingsFilepath).then((indexSettings) => + devToolsRequest(indexName, 'PUT', indexSettings) + ); + + cy.fixture(indexPatternFieldsFilepath).then((fields) => { + apiRequest( + `api/saved_objects/index-pattern/${indexPatternName}`, + 'POST', + JSON.stringify({ + attributes: { + fields: fields, + title: indexPatternName, + timeFieldName: '@timestamp', + }, + }) + ); + }); + + cy.fixture(sampleDataFilepath).then((indexData) => { + indexData.forEach((item, idx) => { + let date = new Date(); + item['@timestamp'] = date.setMinutes(date.getMinutes() - 1); + devToolsRequest(`${indexName}/_doc/${idx}`, 'POST', JSON.stringify(item)); + }); + }); +}; + +/** + * Creating a new visualization from a dashboard, and finishing at the + * vis edit page + */ +const bootstrapCreateFromDashboard = (visType, indexPatternName) => { + cy.getElementByTestId('dashboardAddNewPanelButton') + .should('be.visible') + .click(); + + cy.getElementByTestId(`visType-${visType}`).click(); + + // vega charts don't have a secondary modal to configure the + // index pattern / saved search. Skip those steps here + if (visType !== 'vega') { + cy.getElementByTestId('savedObjectFinderSearchInput').type( + `${indexPatternName}{enter}` + ); + cy.get(`[title="${indexPatternName}"]`).click(); + } +}; + +const setXAxisDateHistogram = () => { + cy.get('.euiTitle') + .contains('Buckets') + .parent() + .find('[data-test-subj="visEditorAdd_buckets"]') + .click(); + + cy.getElementByTestId('visEditorAdd_buckets_X-axis').click(); + + cy.get('.euiTitle') + .contains('Buckets') + .parent() + .within(() => { + cy.wait(1000); + cy.getElementByTestId('comboBoxInput') + .find('input') + .type('Date Histogram{enter}', { force: true }); + }); +}; + +/** + * From the vis edit view, return to the dashboard + */ +const saveVisualizationAndReturn = (visualizationName) => { + cy.getElementByTestId('visualizeEditorRenderButton').click({ + force: true, + }); + cy.getElementByTestId('visualizeSaveButton').click({ + force: true, + }); + cy.getElementByTestId('savedObjectTitle').type(visualizationName); + cy.getElementByTestId('confirmSaveSavedObjectButton').click({ + force: true, + }); +}; + +/** + * Creates a list of specified metrics under the y-axis section + * in the vis editor page + */ +const setYAxis = (metrics) => { + metrics.forEach((metric, index) => { + // There is always a default count metric populated. So, for the first + // added metric, we need to overwrite it. For additional metrics, we + // can just click the "Add" button + if (index === 0) { + cy.getElementByTestId('metricsAggGroup') + .find('.euiAccordion__button') + .click({ force: true }); + } else { + cy.getElementByTestId('visEditorAdd_metrics').click(); + cy.getElementByTestId('visEditorAdd_metrics_Y-axis').click(); + } + addMetric(metric, index); + }); +}; + +/** + * Adds a metric to the specified index in the vis editor page + */ +const addMetric = (metric, index) => { + cy.getElementByTestId('metricsAggGroup') + .find(`[data-test-subj="visEditorAggAccordion${index + 1}"]`) + .within(() => { + cy.wait(1000); + cy.getElementByTestId('comboBoxSearchInput').type( + `${metric.aggregation}{downarrow}{enter}`, + { + force: true, + } + ); + cy.contains(`${metric.aggregation}`).click({ force: true }); + }); + + // non-count aggregations will have an additional field value to set + if (metric.aggregation !== 'Count' && metric.aggregation !== 'count') { + cy.getElementByTestId('metricsAggGroup') + .find(`[data-test-subj="visEditorAggAccordion${index + 1}"]`) + .find('[data-test-subj="visDefaultEditorField"]') + .within(() => { + cy.wait(1000); + cy.getElementByTestId('comboBoxSearchInput').type( + `${metric.field}{downarrow}{enter}`, + { + force: true, + } + ); + }); + } + + // re-collapse the accordion + cy.getElementByTestId(`visEditorAggAccordion${index + 1}`) + .find('.euiAccordion__button') + .first() + .click({ force: true }); +}; + +/** + * Creates an individual visualization, assuming runner is + * starting from dashboard edit view. + */ +export const createVisualizationFromDashboard = ( + visType, + indexPatternName, + visualizationName, + metrics +) => { + bootstrapCreateFromDashboard(visType, indexPatternName); + + // Vega visualizations don't configure axes the same way, + // so ignore those here. Note we still want to support the vega type, + // but don't support any custom specs, as the default spec may be + // sufficient for now + if (visType !== 'vega') { + if (!isEmpty(metrics)) { + setYAxis(metrics); + } + setXAxisDateHistogram(); + } + saveVisualizationAndReturn(visualizationName); +}; + +/** + * Ingests the specified sample data, creates and saves a specified + * list of visualizations, and saves them all to a new dashboard + */ +export const bootstrapDashboard = ( + indexSettingsFilepath, + indexPatternFieldsFilepath, + sampleDataFilepath, + indexName, + indexPatternName, + dashboardName, + visualizationSpecs +) => { + const miscUtils = new MiscUtils(cy); + deleteVisAugmenterData( + indexName, + indexPatternName, + visualizationSpecs.map((visualizationSpec) => visualizationSpec.name), + dashboardName + ); + ingestVisAugmenterData( + indexName, + indexPatternName, + indexSettingsFilepath, + indexPatternFieldsFilepath, + sampleDataFilepath + ); + + miscUtils.visitPage('app/dashboards'); + + cy.getElementByTestId('createDashboardPromptButton') + .should('be.visible') + .click(); + + // Create several different visualizations + visualizationSpecs.forEach((visualizationSpec) => { + createVisualizationFromDashboard( + visualizationSpec.type, + visualizationSpec.indexPattern, + visualizationSpec.name, + visualizationSpec.metrics + ); + }); + + cy.getElementByTestId('dashboardSaveMenuItem').click({ + force: true, + }); + + cy.getElementByTestId('savedObjectTitle').type(dashboardName); + + cy.getElementByTestId('confirmSaveSavedObjectButton').click({ + force: true, + }); +}; + +export const filterByObjectType = (type) => { + cy.get('.euiFilterButton').click(); + cy.get('.euiFilterSelect__items') + .find('button') + .contains(type) + .click({ force: true }); + cy.wait(3000); +}; diff --git a/cypress/utils/dashboards/vis-augmenter/index.d.ts b/cypress/utils/dashboards/vis-augmenter/index.d.ts new file mode 100644 index 000000000..ded2d27d3 --- /dev/null +++ b/cypress/utils/dashboards/vis-augmenter/index.d.ts @@ -0,0 +1,45 @@ +// type definitions for custom commands like "createDefaultTodos" +/// + +declare namespace Cypress { + interface Chainable { + /** + * Returns visualization panel by title + * @example + * cy.getVisPanelByTitle('[Logs] Visitors by OS') + */ + getVisPanelByTitle(title: string): Chainable; + + /** + * Opens vis panel context menu + * @example + * cy.get('visPanel').openVisContextMenu() + */ + openVisContextMenu(): Chainable; + + /** + * Clicks vis panel context menu item + * @example + * cy.clickVisPanelMenuItem('Alerting') + */ + clickVisPanelMenuItem(text: string): Chainable; + + /** + * Gets all items in the context menu + * @example + * cy.getVisPanelByTitle('my-visualization') + .openVisContextMenu() + .getMenuItems() + .contains('View Events') + .should('exist'); + */ + getMenuItems(): Chainable; + + /** + * Visits a dashboard + * @example + * cy.visitDashboard('My-Dashboard') + */ + visitDashboard(dashboardName: string): Chainable; + } +} diff --git a/cypress/utils/helpers.js b/cypress/utils/helpers.js index a64e431fa..bb67116f6 100644 --- a/cypress/utils/helpers.js +++ b/cypress/utils/helpers.js @@ -4,3 +4,4 @@ */ export * from './plugins/anomaly-detection-dashboards-plugin/helpers'; +export * from './dashboards/vis-augmenter/helpers'; diff --git a/cypress/utils/plugins/anomaly-detection-dashboards-plugin/helpers.js b/cypress/utils/plugins/anomaly-detection-dashboards-plugin/helpers.js index 8388339d9..96bda3d93 100644 --- a/cypress/utils/plugins/anomaly-detection-dashboards-plugin/helpers.js +++ b/cypress/utils/plugins/anomaly-detection-dashboards-plugin/helpers.js @@ -3,6 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { AD_URL } from './constants'; + export const selectTopItemFromFilter = ( dataTestSubjectName, allowMultipleSelections = true @@ -21,3 +23,117 @@ export const selectTopItemFromFilter = ( .click(); } }; + +export const createSampleDetector = (createButtonDataTestSubj) => { + cy.visit(AD_URL.OVERVIEW); + + cy.getElementByTestId('overviewTitle').should('exist'); + cy.getElementByTestId('viewSampleDetectorLink').should('not.exist'); + cy.getElementByTestId(createButtonDataTestSubj).click(); + cy.visit(AD_URL.OVERVIEW); + + // Check that the details page defaults to real-time, and shows detector is initializing + cy.getElementByTestId('viewSampleDetectorLink').click(); + cy.getElementByTestId('detectorNameHeader').should('exist'); + cy.getElementByTestId('sampleIndexDetailsCallout').should('exist'); + cy.getElementByTestId('realTimeResultsHeader').should('exist'); + cy.getElementByTestId('detectorStateInitializing').should('exist'); +}; + +const openAnomalyDetectionPanel = (dashboardName, visualizationName) => { + cy.visitDashboard(dashboardName); + cy.getVisPanelByTitle(visualizationName) + .openVisContextMenu() + .clickVisPanelMenuItem('Anomaly Detection'); +}; + +export const openDetectorDetailsPageFromFlyout = () => { + cy.get('.euiBasicTable').find('.euiLink').click(); +}; + +export const openAddAnomalyDetectorFlyout = ( + dashboardName, + visualizationName +) => { + openAnomalyDetectionPanel(dashboardName, visualizationName); + cy.clickVisPanelMenuItem('Add anomaly detector'); + cy.wait(5000); +}; + +export const openAssociatedDetectorsFlyout = ( + dashboardName, + visualizationName +) => { + openAnomalyDetectionPanel(dashboardName, visualizationName); + cy.clickVisPanelMenuItem('Associated detectors'); +}; + +export const openViewEventsFlyout = (dashboardName, visualizationName) => { + cy.visitDashboard(dashboardName); + cy.getVisPanelByTitle(visualizationName) + .openVisContextMenu() + .clickVisPanelMenuItem('View Events'); + cy.wait(5000); +}; + +// expected context: on create detector flyout +export const createDetectorFromVis = (detectorName) => { + cy.get('[id="detectorDetailsAccordion"]') + .parent() + .find('[data-test-subj="accordionTitleButton"]') + .click(); + cy.getElementByTestId('detectorNameTextInputFlyout').clear(); + cy.getElementByTestId('detectorNameTextInputFlyout').type(detectorName); + cy.getElementByTestId('adAnywhereCreateDetectorButton').click(); + cy.wait(5000); +}; + +// expected context: on associate detector flyout +export const associateDetectorFromVis = (detectorName) => { + cy.wait(2000); + cy.getElementByTestId('comboBoxInput').type( + `${detectorName}{downArrow}{enter}` + ); + cy.wait(2000); + cy.getElementByTestId('adAnywhereAssociateDetectorButton').click(); + cy.wait(5000); +}; + +export const ensureDetectorIsLinked = ( + dashboardName, + visualizationName, + detectorName +) => { + openAssociatedDetectorsFlyout(dashboardName, visualizationName); + cy.wait(2000); + cy.get('.euiFieldSearch').type(detectorName); + cy.get('.euiBasicTable').find('.euiTableRow').should('have.length', 1); +}; + +export const ensureDetectorDetails = (detectorName, numFeatures) => { + openDetectorDetailsPageFromFlyout(); + cy.getElementByTestId('detectorNameHeader').within(() => { + cy.contains(detectorName); + }); + cy.getElementByTestId('resultsTab'); + cy.getElementByTestId('realTimeResultsHeader'); + cy.getElementByTestId('configurationsTab').click(); + cy.getElementByTestId('detectorSettingsHeader'); + cy.getElementByTestId('featureTable') + .find('.euiTableRow') + .should('have.length', numFeatures); +}; + +export const unlinkDetectorFromVis = ( + dashboardName, + visualizationName, + detectorName +) => { + openAssociatedDetectorsFlyout(dashboardName, visualizationName); + cy.wait(2000); + cy.get('.euiFieldSearch').type(detectorName); + cy.wait(1000); + cy.getElementByTestId('unlinkButton').click(); + cy.getElementByTestId('confirmUnlinkButton').click(); + cy.wait(5000); +};