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..1f02b0f90
--- /dev/null
+++ b/cypress/integration/plugins/anomaly-detection-dashboards-plugin/vis_augmenter/augment_vis_saved_object_spec.js
@@ -0,0 +1,105 @@
+/*
+ * 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');
+ });
+});
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..25b091c57
--- /dev/null
+++ b/cypress/utils/dashboards/vis-augmenter/helpers.js
@@ -0,0 +1,321 @@
+/*
+ * 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#/create');
+
+ // 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);
+};