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 index 03f547f75..d97647c55 100644 --- 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 @@ -93,7 +93,6 @@ describe('Vis augmenter - existing dashboards work as expected', () => { beforeEach(() => { cy.visitDashboard(dashboardName); - cy.wait(5000); }); after(() => { 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/vis-augmenter/commands.js b/cypress/utils/dashboards/vis-augmenter/commands.js index 18b18767b..e0ee418de 100644 --- a/cypress/utils/dashboards/vis-augmenter/commands.js +++ b/cypress/utils/dashboards/vis-augmenter/commands.js @@ -3,7 +3,6 @@ * SPDX-License-Identifier: Apache-2.0 */ -import _ from 'lodash'; import { BASE_PATH } from '../../constants'; Cypress.Commands.add('getVisPanelByTitle', (title) => @@ -26,7 +25,6 @@ Cypress.Commands.add( .find('button') .contains(text) .click() - .then(() => cy.get('.euiContextMenu')) ); Cypress.Commands.add('getMenuItems', { prevSubject: 'optional' }, (menu) => @@ -35,9 +33,16 @@ Cypress.Commands.add('getMenuItems', { prevSubject: 'optional' }, (menu) => Cypress.Commands.add('visitDashboard', (dashboardName) => { cy.visit(`${BASE_PATH}/app/dashboards`); + cy.wait(2000); cy.get('.euiFieldSearch').type(dashboardName); - cy.wait(1000); + 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/helpers.js b/cypress/utils/dashboards/vis-augmenter/helpers.js index e8a555e92..73ebbf12e 100644 --- a/cypress/utils/dashboards/vis-augmenter/helpers.js +++ b/cypress/utils/dashboards/vis-augmenter/helpers.js @@ -314,3 +314,12 @@ export const bootstrapDashboard = ( 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/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); +};