diff --git a/changelogs/fragments/8611.yml b/changelogs/fragments/8611.yml
new file mode 100644
index 000000000000..2f7ec1677a58
--- /dev/null
+++ b/changelogs/fragments/8611.yml
@@ -0,0 +1,2 @@
+fix:
+- Bump url to 0.11.4 ([#8611](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8611))
\ No newline at end of file
diff --git a/changelogs/fragments/8739.yml b/changelogs/fragments/8739.yml
new file mode 100644
index 000000000000..563d6c0cacac
--- /dev/null
+++ b/changelogs/fragments/8739.yml
@@ -0,0 +1,2 @@
+fix:
+- [Workspace] [Bug] Check if workspaces exists when creating saved objects. ([#8739](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8739))
\ No newline at end of file
diff --git a/changelogs/fragments/8842.yml b/changelogs/fragments/8842.yml
new file mode 100644
index 000000000000..b9973f347f9e
--- /dev/null
+++ b/changelogs/fragments/8842.yml
@@ -0,0 +1,2 @@
+fix:
+- [Workspace]Fix error toasts in sample data page ([#8842](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8842))
\ No newline at end of file
diff --git a/changelogs/fragments/8886.yml b/changelogs/fragments/8886.yml
new file mode 100644
index 000000000000..74b3b404d8f5
--- /dev/null
+++ b/changelogs/fragments/8886.yml
@@ -0,0 +1,2 @@
+chore:
+- Bump `@opensearch-project/opensearch` from 2.9.0 to 2.13.0 ([#8886](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8886))
\ No newline at end of file
diff --git a/changelogs/fragments/8888.yml b/changelogs/fragments/8888.yml
new file mode 100644
index 000000000000..cf22e39bf062
--- /dev/null
+++ b/changelogs/fragments/8888.yml
@@ -0,0 +1,2 @@
+refactor:
+- [Workspace] Isolate objects based on workspace when calling get/bulkGet ([#8888](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8888))
\ No newline at end of file
diff --git a/changelogs/fragments/8900.yml b/changelogs/fragments/8900.yml
new file mode 100644
index 000000000000..78ae369755a7
--- /dev/null
+++ b/changelogs/fragments/8900.yml
@@ -0,0 +1,2 @@
+feat:
+- Optimize recent items and filter out items whose workspace is deleted ([#8900](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8900))
\ No newline at end of file
diff --git a/changelogs/fragments/8915.yml b/changelogs/fragments/8915.yml
deleted file mode 100644
index 46c124d3f25f..000000000000
--- a/changelogs/fragments/8915.yml
+++ /dev/null
@@ -1,2 +0,0 @@
-fix:
-- Do not support data sources with no version for vis augmenter ([#8915](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8915))
\ No newline at end of file
diff --git a/changelogs/fragments/8919.yml b/changelogs/fragments/8919.yml
new file mode 100644
index 000000000000..f18d457de271
--- /dev/null
+++ b/changelogs/fragments/8919.yml
@@ -0,0 +1,2 @@
+fix:
+- Change some of the http link in settings page to https link ([#8919](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8919))
\ No newline at end of file
diff --git a/changelogs/fragments/8920.yml b/changelogs/fragments/8920.yml
new file mode 100644
index 000000000000..f25a3042d437
--- /dev/null
+++ b/changelogs/fragments/8920.yml
@@ -0,0 +1,2 @@
+feat:
+- [workspace]support search dev tools by its category name ([#8920](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8920))
\ No newline at end of file
diff --git a/changelogs/fragments/8926.yml b/changelogs/fragments/8926.yml
new file mode 100644
index 000000000000..b99f449c54ca
--- /dev/null
+++ b/changelogs/fragments/8926.yml
@@ -0,0 +1,2 @@
+chore:
+- Update cypress to v12 ([#8926](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8926))
\ No newline at end of file
diff --git a/changelogs/fragments/8930.yml b/changelogs/fragments/8930.yml
new file mode 100644
index 000000000000..50551ecb2956
--- /dev/null
+++ b/changelogs/fragments/8930.yml
@@ -0,0 +1,2 @@
+fix:
+- Update saved search initialization logic to use current query instead of default query ([#8930](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8930))
\ No newline at end of file
diff --git a/changelogs/fragments/8932.yml b/changelogs/fragments/8932.yml
new file mode 100644
index 000000000000..a048de0a102a
--- /dev/null
+++ b/changelogs/fragments/8932.yml
@@ -0,0 +1,2 @@
+feat:
+- Support custom logic to insert time filter based on dataset type ([#8932](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8932))
\ No newline at end of file
diff --git a/changelogs/fragments/8935.yml b/changelogs/fragments/8935.yml
new file mode 100644
index 000000000000..84922a039ffc
--- /dev/null
+++ b/changelogs/fragments/8935.yml
@@ -0,0 +1,2 @@
+fix:
+- Use roundUp when converting timestamp for PPL ([#8935](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8935))
\ No newline at end of file
diff --git a/changelogs/fragments/8951.yml b/changelogs/fragments/8951.yml
new file mode 100644
index 000000000000..da724b7d3c66
--- /dev/null
+++ b/changelogs/fragments/8951.yml
@@ -0,0 +1,2 @@
+fix:
+- SQL syntax highlighting double quotes ([#8951](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8951))
\ No newline at end of file
diff --git a/changelogs/fragments/8993.yml b/changelogs/fragments/8993.yml
new file mode 100644
index 000000000000..dac519c8b746
--- /dev/null
+++ b/changelogs/fragments/8993.yml
@@ -0,0 +1,2 @@
+fix:
+- Support imports without extensions in cypress webpack build ([#8993](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8993))
\ No newline at end of file
diff --git a/cypress.config.ts b/cypress.config.ts
new file mode 100644
index 000000000000..1a9d8cc98028
--- /dev/null
+++ b/cypress.config.ts
@@ -0,0 +1,66 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { defineConfig } from 'cypress';
+import webpackPreprocessor from '@cypress/webpack-preprocessor';
+
+module.exports = defineConfig({
+ retries: 2,
+ defaultCommandTimeout: 60000,
+ requestTimeout: 60000,
+ responseTimeout: 60000,
+ viewportWidth: 2000,
+ viewportHeight: 1320,
+ env: {
+ openSearchUrl: 'http://localhost:9200',
+ SECURITY_ENABLED: false,
+ AGGREGATION_VIEW: false,
+ username: 'admin',
+ password: 'myStrongPassword123!',
+ ENDPOINT_WITH_PROXY: false,
+ MANAGED_SERVICE_ENDPOINT: false,
+ VISBUILDER_ENABLED: true,
+ DATASOURCE_MANAGEMENT_ENABLED: false,
+ ML_COMMONS_DASHBOARDS_ENABLED: true,
+ WAIT_FOR_LOADER_BUFFER_MS: 0,
+ },
+ e2e: {
+ baseUrl: 'http://localhost:5601',
+ specPattern: 'cypress/integration/**/*_spec.{js,jsx,ts,tsx}',
+ testIsolation: false,
+ setupNodeEvents,
+ },
+});
+
+function setupNodeEvents(
+ on: Cypress.PluginEvents,
+ config: Cypress.PluginConfigOptions
+): Cypress.PluginConfigOptions {
+ const { webpackOptions } = webpackPreprocessor.defaultOptions;
+
+ /**
+ * By default, cypress' internal webpack preprocessor doesn't allow imports without file extensions.
+ * This makes our life a bit hard since if any file in our testing dependency graph has an import without
+ * the .js extension our cypress build will fail.
+ *
+ * This extra rule relaxes this a bit by allowing imports without file extension
+ * ex. import module from './module'
+ */
+ webpackOptions!.module!.rules.unshift({
+ test: /\.m?js/,
+ resolve: {
+ enforceExtension: false,
+ },
+ });
+
+ on(
+ 'file:preprocessor',
+ webpackPreprocessor({
+ webpackOptions,
+ })
+ );
+
+ return config;
+}
diff --git a/cypress.json b/cypress.json
deleted file mode 100644
index 46e8c7e8ea16..000000000000
--- a/cypress.json
+++ /dev/null
@@ -1,21 +0,0 @@
-{
- "defaultCommandTimeout": 60000,
- "requestTimeout": 60000,
- "responseTimeout": 60000,
- "baseUrl": "http://localhost:5601",
- "viewportWidth": 2000,
- "viewportHeight": 1320,
- "env": {
- "openSearchUrl": "http://localhost:9200",
- "SECURITY_ENABLED": false,
- "AGGREGATION_VIEW": false,
- "username": "admin",
- "password": "myStrongPassword123!",
- "ENDPOINT_WITH_PROXY": false,
- "MANAGED_SERVICE_ENDPOINT": false,
- "VISBUILDER_ENABLED": true,
- "DATASOURCE_MANAGEMENT_ENABLED": false,
- "ML_COMMONS_DASHBOARDS_ENABLED": true,
- "WAIT_FOR_LOADER_BUFFER_MS": 0
- }
-}
diff --git a/cypress/integration/core_opensearch_dashboards/filter_for_value_spec.js b/cypress/integration/core_opensearch_dashboards/filter_for_value_spec.js
new file mode 100644
index 000000000000..43305f08bf60
--- /dev/null
+++ b/cypress/integration/core_opensearch_dashboards/filter_for_value_spec.js
@@ -0,0 +1,63 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { MiscUtils } from '@opensearch-dashboards-test/opensearch-dashboards-test-library';
+import { DataExplorerPage } from '../../utils/dashboards/data_explorer/data_explorer_page.po';
+
+const miscUtils = new MiscUtils(cy);
+
+describe('filter for value spec', () => {
+ beforeEach(() => {
+ cy.localLogin(Cypress.env('username'), Cypress.env('password'));
+ miscUtils.visitPage('app/data-explorer/discover');
+ cy.getNewSearchButton().click();
+ });
+ describe('filter actions in table field', () => {
+ describe('index pattern dataset', () => {
+ // filter actions should exist for DQL
+ it('DQL', () => {
+ DataExplorerPage.selectIndexPatternDataset('DQL');
+ DataExplorerPage.setSearchRelativeDateRange('15', 'Years ago');
+ DataExplorerPage.checkDocTableFirstFieldFilterForAndOutButton(true);
+ DataExplorerPage.checkDocTableFirstFieldFilterForButtonFiltersCorrectField();
+ DataExplorerPage.checkDocTableFirstFieldFilterOutButtonFiltersCorrectField();
+ });
+ // filter actions should exist for Lucene
+ it('Lucene', () => {
+ DataExplorerPage.selectIndexPatternDataset('Lucene');
+ DataExplorerPage.setSearchRelativeDateRange('15', 'Years ago');
+ DataExplorerPage.checkDocTableFirstFieldFilterForAndOutButton(true);
+ DataExplorerPage.checkDocTableFirstFieldFilterForButtonFiltersCorrectField();
+ DataExplorerPage.checkDocTableFirstFieldFilterOutButtonFiltersCorrectField();
+ });
+ // filter actions should not exist for SQL
+ it('SQL', () => {
+ DataExplorerPage.selectIndexPatternDataset('OpenSearch SQL');
+ DataExplorerPage.checkDocTableFirstFieldFilterForAndOutButton(false);
+ });
+ // filter actions should not exist for PPL
+ it('PPL', () => {
+ DataExplorerPage.selectIndexPatternDataset('PPL');
+ DataExplorerPage.setSearchRelativeDateRange('15', 'Years ago');
+ DataExplorerPage.checkDocTableFirstFieldFilterForAndOutButton(false);
+ });
+ });
+ describe('index dataset', () => {
+ // filter actions should not exist for SQL
+ it('SQL', () => {
+ DataExplorerPage.selectIndexDataset(
+ 'OpenSearch SQL',
+ "I don't want to use the time filter"
+ );
+ DataExplorerPage.checkDocTableFirstFieldFilterForAndOutButton(false);
+ });
+ // filter actions should not exist for PPL
+ it('PPL', () => {
+ DataExplorerPage.selectIndexDataset('PPL', "I don't want to use the time filter");
+ DataExplorerPage.checkDocTableFirstFieldFilterForAndOutButton(false);
+ });
+ });
+ });
+});
diff --git a/cypress/integration/core_opensearch_dashboards/sidebar_test_spec.js b/cypress/integration/core_opensearch_dashboards/sidebar_test_spec.js
new file mode 100644
index 000000000000..b90d212e0ef7
--- /dev/null
+++ b/cypress/integration/core_opensearch_dashboards/sidebar_test_spec.js
@@ -0,0 +1,266 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { MiscUtils } from '@opensearch-dashboards-test/opensearch-dashboards-test-library';
+import { DataExplorerPage } from '../../utils/dashboards/data_explorer/data_explorer_page.po';
+
+const miscUtils = new MiscUtils(cy);
+
+describe('sidebar spec', function () {
+ beforeEach(function () {
+ cy.localLogin(Cypress.env('username'), Cypress.env('password'));
+ miscUtils.visitPage('app/data-explorer/discover');
+ });
+
+ describe('sidebar fields', function () {
+ describe('add fields', function () {
+ function addFields(
+ testFields,
+ expectedValues,
+ pplQuery,
+ sqlQuery,
+ indexPattern = true,
+ nested = false
+ ) {
+ const offset = indexPattern ? 1 : 0; // defines starting column
+ const dataColumnOffset = nested ? -1 : 0;
+ if (indexPattern) {
+ DataExplorerPage.selectIndexPatternDataset('DQL');
+ DataExplorerPage.setQueryEditorLanguage('DQL');
+ DataExplorerPage.setSearchRelativeDateRange('15', 'Years ago');
+ } else {
+ if (nested) {
+ DataExplorerPage.selectIndexDataset(
+ 'OpenSearch SQL',
+ "I don't want to use the time filter",
+ 'cypress-test-os',
+ 'opensearch_dashboards_sample_data_ecommerce'
+ );
+ } else {
+ DataExplorerPage.selectIndexDataset(
+ 'OpenSearch SQL',
+ "I don't want to use the time filter",
+ 'cypress-test-os',
+ 'vis-builder'
+ );
+ }
+ }
+ // Check default column
+ DataExplorerPage.getDocTableHeader(0 + offset).should('have.text', '_source');
+ // Select some fields
+ testFields.forEach((field) => {
+ DataExplorerPage.getFieldBtnByName(field).click();
+ });
+ // Check that the default column no longer exists
+ DataExplorerPage.getDocTableHeader(0 + offset).should('not.have.text', '_source');
+ // Check table headers persistence between DQL and PPL
+ DataExplorerPage.checkTableHeadersByArray(testFields, offset);
+ DataExplorerPage.setQueryEditorLanguage('PPL');
+ DataExplorerPage.checkTableHeadersByArray(testFields, offset);
+ // Remove some fields
+ const firstTestField = testFields[0];
+ const secondTestField = testFields[1];
+ DataExplorerPage.getFieldBtnByName(firstTestField).click();
+ DataExplorerPage.getFieldBtnByName(secondTestField).click();
+ DataExplorerPage.getDocTableHeader(0 + offset).should('not.have.text', firstTestField);
+ DataExplorerPage.getDocTableHeader(1 + offset).should('not.have.text', secondTestField);
+ // Remove all fields
+ const thirdTestField = testFields[2];
+ const fourthTestField = testFields[3];
+ DataExplorerPage.getFieldBtnByName(thirdTestField).click();
+ DataExplorerPage.getFieldBtnByName(fourthTestField).click();
+ DataExplorerPage.getDocTableHeader(0 + offset).should('have.text', '_source');
+ DataExplorerPage.getDocTableHeader(1 + offset).should('not.exist');
+ // Select some fields
+ testFields.forEach((field) => {
+ DataExplorerPage.getFieldBtnByName(field).click();
+ });
+ // Check default column again
+ DataExplorerPage.getDocTableHeader(0 + offset).should('not.have.text', '_source');
+ // Check the columns match the selected fields
+ DataExplorerPage.checkTableHeadersByArray(testFields, offset);
+ if (indexPattern) {
+ // Validate default hits
+ DataExplorerPage.checkQueryHitsText('10,000');
+ }
+ // Send PPL query
+ cy.intercept('/api/enhancements/search/ppl').as('pplQuery');
+ DataExplorerPage.sendQueryOnMultilineEditor(pplQuery);
+ cy.wait('@pplQuery').then(function () {
+ // Check table headers persistence after PPL query
+ DataExplorerPage.checkTableHeadersByArray(testFields, offset);
+ if (indexPattern) {
+ // Check filter was correctly applied
+ DataExplorerPage.checkQueryHitsText('6,588');
+ }
+ // Validate the first 5 rows on the _id column
+ DataExplorerPage.checkDocTableColumnByArr(expectedValues, 1 + offset + dataColumnOffset);
+ });
+ // Send SQL query
+ DataExplorerPage.setQueryEditorLanguage('OpenSearch SQL');
+ cy.intercept('/api/enhancements/search/sql').as('sqlQuery');
+ DataExplorerPage.sendQueryOnMultilineEditor(sqlQuery);
+ cy.wait('@sqlQuery').then(function () {
+ // Check table headers persistence after SQL query
+ DataExplorerPage.checkTableHeadersByArray(testFields, offset);
+ // Validate the first 5 rows on the _id column
+ DataExplorerPage.checkDocTableColumnByArr(expectedValues, 1 + offset + dataColumnOffset);
+ });
+ }
+
+ const pplQuery = 'source = vis-builder* | where age > 40';
+ const sqlQuery = 'SELECT * FROM vis-builder* WHERE age > 40';
+ const testFields = ['_id', 'age', 'birthdate', 'salary'];
+ const expectedIdValues = ['50', '57', '52', '66', '46'];
+ it('add field in index pattern: DQL to PPL and SQL', function () {
+ addFields(testFields, expectedIdValues, pplQuery, sqlQuery);
+ });
+
+ it('add field in index: SQL and PPL', function () {
+ addFields(testFields, expectedIdValues, pplQuery, sqlQuery, false);
+ });
+
+ const nestedTestFields = [
+ 'geoip.region_name',
+ 'products.quantity',
+ 'event.dataset',
+ 'products.taxful_price',
+ ];
+ const expectedRegionValues = [
+ 'Cairo Governorate',
+ 'Dubai',
+ 'California',
+ ' - ',
+ 'Cairo Governorate',
+ ];
+ it.skip('add nested field in index pattern: DQL to PPL and SQL', function () {
+ addFields(nestedTestFields, expectedRegionValues, pplQuery, sqlQuery, true, true);
+ });
+
+ it('add nested field in index: SQL and PPL', function () {
+ addFields(nestedTestFields, expectedRegionValues, pplQuery, sqlQuery, false, true);
+ });
+ });
+
+ describe('filter fields', function () {
+ function filterFields() {
+ DataExplorerPage.checkSidebarFilterBarResults('equal', 'categories');
+ DataExplorerPage.checkSidebarFilterBarResults('include', 'a');
+ DataExplorerPage.checkSidebarFilterBarResults('include', 'ag');
+ DataExplorerPage.checkSidebarFilterBarNegativeResults('non-existent field');
+ }
+
+ it('index pattern: DQL, PPL and SQL', function () {
+ DataExplorerPage.selectIndexPatternDataset('DQL');
+ DataExplorerPage.setQueryEditorLanguage('DQL');
+ DataExplorerPage.setSearchRelativeDateRange('15', 'Years ago');
+ filterFields();
+ DataExplorerPage.setQueryEditorLanguage('PPL');
+ filterFields();
+ DataExplorerPage.setQueryEditorLanguage('OpenSearch SQL');
+ filterFields();
+ });
+
+ it('index: PPL and SQL', function () {
+ DataExplorerPage.selectIndexDataset(
+ 'PPL',
+ "I don't want to use the time filter",
+ 'cypress-test-os',
+ 'vis-builder'
+ );
+ DataExplorerPage.setQueryEditorLanguage('PPL');
+ filterFields();
+ DataExplorerPage.setQueryEditorLanguage('OpenSearch SQL');
+ filterFields();
+ DataExplorerPage.setQueryEditorLanguage('Lucene');
+ filterFields();
+ });
+ });
+
+ describe('side panel collapse/expand', function () {
+ function collapseAndExpand() {
+ DataExplorerPage.getSidebar().should('be.visible');
+ DataExplorerPage.collapseSidebar();
+ DataExplorerPage.getSidebar().should('not.be.visible');
+ DataExplorerPage.expandSidebar();
+ DataExplorerPage.getSidebar().should('be.visible');
+ }
+
+ function checkCollapseAndExpand(indexPattern = true) {
+ if (indexPattern) {
+ DataExplorerPage.setQueryEditorLanguage('DQL');
+ collapseAndExpand();
+ }
+ DataExplorerPage.setQueryEditorLanguage('PPL');
+ collapseAndExpand();
+ DataExplorerPage.setQueryEditorLanguage('OpenSearch SQL');
+ collapseAndExpand();
+ if (indexPattern) {
+ DataExplorerPage.setQueryEditorLanguage('DQL');
+ DataExplorerPage.getSidebar().should('be.visible');
+ }
+ }
+
+ function checkCollapse(indexPattern = true) {
+ if (indexPattern) {
+ DataExplorerPage.setQueryEditorLanguage('DQL');
+ DataExplorerPage.collapseSidebar();
+ DataExplorerPage.getSidebar().should('not.be.visible');
+ }
+ DataExplorerPage.setQueryEditorLanguage('PPL');
+ if (!indexPattern) {
+ DataExplorerPage.collapseSidebar();
+ }
+ DataExplorerPage.getSidebar().should('not.be.visible');
+ DataExplorerPage.setQueryEditorLanguage('OpenSearch SQL');
+ DataExplorerPage.getSidebar().should('not.be.visible');
+ if (indexPattern) {
+ DataExplorerPage.setQueryEditorLanguage('DQL');
+ DataExplorerPage.getSidebar().should('not.be.visible');
+ }
+ }
+
+ it('index pattern: collapse and expand for DQL, PPL and SQL', function () {
+ // this test case does three things:
+ // 1. checks the persistence of the sidebar state accross query languages
+ // 2. checks that the default state is expanded (first iteration of collapseAndExpand())
+ // 3. collapses and expands the sidebar for every language
+ DataExplorerPage.selectIndexPatternDataset('DQL');
+ checkCollapseAndExpand();
+ });
+
+ it('index pattern: check collapsed state for DQL, PPL and SQL', function () {
+ // this test case checks that the sidebar remains collapsed accross query languages
+ DataExplorerPage.selectIndexPatternDataset('DQL');
+ checkCollapse();
+ });
+
+ it('index: collapse and expand for PPL and SQL', function () {
+ // this test case does three things:
+ // 1. checks the persistence of the sidebar state accross query languages
+ // 2. checks that the default state is expanded (first iteration of collapseAndExpand())
+ // 3. collapses and expands the sidebar for every language
+ DataExplorerPage.selectIndexDataset(
+ 'PPL',
+ "I don't want to use the time filter",
+ 'cypress-test-os',
+ 'vis-builder'
+ );
+ checkCollapseAndExpand(false);
+ });
+
+ it('index: check collapsed state for PPL and SQL', function () {
+ // this test case checks that the sidebar remains collapsed accross query languages
+ DataExplorerPage.selectIndexDataset(
+ 'PPL',
+ "I don't want to use the time filter",
+ 'cypress-test-os',
+ 'vis-builder'
+ );
+ checkCollapse(false);
+ });
+ });
+ });
+});
diff --git a/cypress/plugins/index.js b/cypress/plugins/index.js
new file mode 100644
index 000000000000..59b2bab6e4e6
--- /dev/null
+++ b/cypress/plugins/index.js
@@ -0,0 +1,22 @@
+///
+// ***********************************************************
+// This example plugins/index.js can be used to load plugins
+//
+// You can change the location of this file or turn off loading
+// the plugins file with the 'pluginsFile' configuration option.
+//
+// You can read more here:
+// https://on.cypress.io/plugins-guide
+// ***********************************************************
+
+// This function is called when a project is opened or re-opened (e.g. due to
+// the project's config changing)
+
+/**
+ * @type {Cypress.PluginConfig}
+ */
+// eslint-disable-next-line no-unused-vars
+module.exports = (on, config) => {
+ // `on` is used to hook into various events Cypress emits
+ // `config` is the resolved Cypress config
+}
diff --git a/cypress/support/e2e.js b/cypress/support/e2e.js
new file mode 100644
index 000000000000..bc7c1b25b8eb
--- /dev/null
+++ b/cypress/support/e2e.js
@@ -0,0 +1,7 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import '../utils/commands';
+import '../utils/dashboards/data_explorer/commands';
diff --git a/cypress/utils/commands.js b/cypress/utils/commands.js
index 56a1fd0cff0e..c67cd85e1020 100644
--- a/cypress/utils/commands.js
+++ b/cypress/utils/commands.js
@@ -3,13 +3,71 @@
* SPDX-License-Identifier: Apache-2.0
*/
-// --- Typed commands --
+import {
+ MiscUtils,
+ LoginPage,
+} from '@opensearch-dashboards-test/opensearch-dashboards-test-library';
+const miscUtils = new MiscUtils(cy);
+const loginPage = new LoginPage(cy);
+
+/**
+ * Get DOM element by data-test-subj id.
+ * @param testId data-test-subj value.
+ * @param options cy.get() options. Default: {}
+ */
Cypress.Commands.add('getElementByTestId', (testId, options = {}) => {
return cy.get(`[data-test-subj="${testId}"]`, options);
});
+/**
+ * Get DOM element by partial data-test-subj id.
+ * @param testId data-test-subj value.
+ * @param options cy.get() options. Default: {}
+ * @comparisonType choose a partial data-test-subj comparison type. Accepted values: 'beginning', 'ending', 'substring'.
+ */
+Cypress.Commands.add('getElementByTestIdLike', (testId, comparisonType, options = {}) => {
+ const comparison = {
+ beginning: '^',
+ ending: '$',
+ substring: '*',
+ };
+ const chosenType = comparison[comparisonType] || '';
+ return cy.get(`[data-test-subj${chosenType}="${testId}"]`, options);
+});
+
+/**
+ * Get multiple DOM elements by data-test-subj ids.
+ */
Cypress.Commands.add('getElementsByTestIds', (testIds, options = {}) => {
const selectors = [testIds].flat(Infinity).map((testId) => `[data-test-subj="${testId}"]`);
return cy.get(selectors.join(','), options);
});
+
+/**
+ * Find element from previous chained element by data-test-subj id.
+ */
+Cypress.Commands.add(
+ 'findElementByTestId',
+ { prevSubject: true },
+ (subject, testId, options = {}) => {
+ return cy.wrap(subject).find(`[data-test-subj="${testId}"]`, options);
+ }
+);
+
+/**
+ * Go to the local instance of OSD's home page and login if needed.
+ */
+Cypress.Commands.add('localLogin', (username, password) => {
+ cy.session('test_automation', function () {
+ miscUtils.visitPage('/app/home');
+ cy.url().then(($url) => {
+ if ($url.includes('login')) {
+ loginPage.enterUserName(username);
+ loginPage.enterPassword(password);
+ loginPage.submit();
+ }
+ cy.url().should('contain', '/app/home');
+ });
+ });
+});
diff --git a/cypress/utils/dashboards/data_explorer/commands.js b/cypress/utils/dashboards/data_explorer/commands.js
new file mode 100644
index 000000000000..b76e5d6f8d1c
--- /dev/null
+++ b/cypress/utils/dashboards/data_explorer/commands.js
@@ -0,0 +1,50 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { DATA_EXPLORER_PAGE_ELEMENTS } from './elements.js';
+
+/**
+ * Get the New Search button.
+ */
+Cypress.Commands.add('getNewSearchButton', () => {
+ return cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.NEW_SEARCH_BUTTON, { timeout: 10000 });
+});
+
+/**
+ * Get the Query Submit button.
+ */
+Cypress.Commands.add('getQuerySubmitButton', () => {
+ return cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.QUERY_SUBMIT_BUTTON);
+});
+
+/**
+ * Get the Search Bar Date Picker button.
+ */
+Cypress.Commands.add('getSearchDatePickerButton', () => {
+ return cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.SEARCH_DATE_PICKER_BUTTON);
+});
+
+/**
+ * Get the Relative Date tab in the Search Bar Date Picker.
+ */
+Cypress.Commands.add('getDatePickerRelativeTab', () => {
+ return cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.SEARCH_DATE_PICKER_RELATIVE_TAB);
+});
+
+/**
+ * Get the Relative Date Input in the Search Bar Date Picker.
+ */
+Cypress.Commands.add('getDatePickerRelativeInput', () => {
+ return cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.SEARCH_DATE_RELATIVE_PICKER_INPUT);
+});
+
+/**
+ * Get the Relative Date Unit selector in the Search Bar Date Picker.
+ */
+Cypress.Commands.add('getDatePickerRelativeUnitSelector', () => {
+ return cy.getElementByTestId(
+ DATA_EXPLORER_PAGE_ELEMENTS.SEARCH_DATE_RELATIVE_PICKER_UNIT_SELECTOR
+ );
+});
diff --git a/cypress/utils/dashboards/data_explorer/constants.js b/cypress/utils/dashboards/data_explorer/constants.js
new file mode 100644
index 000000000000..657e3201f680
--- /dev/null
+++ b/cypress/utils/dashboards/data_explorer/constants.js
@@ -0,0 +1,8 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+export const INDEX_CLUSTER_NAME = 'cypress-test-os';
+export const INDEX_NAME = 'vis-builder';
+export const INDEX_PATTERN_NAME = 'cypress-test-os::vis-builder*';
diff --git a/cypress/utils/dashboards/data_explorer/data_explorer_page.po.js b/cypress/utils/dashboards/data_explorer/data_explorer_page.po.js
new file mode 100644
index 000000000000..1ed080833f3f
--- /dev/null
+++ b/cypress/utils/dashboards/data_explorer/data_explorer_page.po.js
@@ -0,0 +1,440 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { DATA_EXPLORER_PAGE_ELEMENTS } from './elements.js';
+import { INDEX_PATTERN_NAME } from './constants.js';
+
+export class DataExplorerPage {
+ /**
+ * Get the Dataset selector button
+ */
+ static getDatasetSelectorButton() {
+ return cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_SELECTOR_BUTTON);
+ }
+
+ /**
+ * Get the all Datasets button in the Datasets popup.
+ */
+ static getAllDatasetsButton() {
+ return cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.ALL_DATASETS_BUTTON);
+ }
+
+ /**
+ * Get the Time Selector in the Dataset Selector.
+ */
+ static getDatasetTimeSelector() {
+ return cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_SELECTOR_TIME_SELECTOR);
+ }
+
+ /**
+ * Get the Language Selector in the Dataset Selector.
+ */
+ static getDatasetLanguageSelector() {
+ return cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_SELECTOR_LANGUAGE_SELECTOR);
+ }
+
+ /**
+ * Get the Select Dataset button in the Dataset Selector.
+ */
+ static getDatasetSelectDataButton() {
+ return cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_SELECTOR_SELECT_DATA_BUTTON);
+ }
+
+ /**
+ * Get the Dataset Explorer Window.
+ */
+ static getDatasetExplorerWindow() {
+ return cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_EXPLORER_WINDOW);
+ }
+
+ /**
+ * Get the Next button in the Dataset Selector.
+ */
+ static getDatasetExplorerNextButton() {
+ return cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_SELECTOR_NEXT_BUTTON);
+ }
+
+ /**
+ * Get specific DocTable column header.
+ * @param index Integer starts from 0 for the first column header.
+ */
+ static getDocTableHeader(index) {
+ return cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DOC_TABLE_HEADER_FIELD).eq(index);
+ }
+
+ /**
+ * Get specific row of DocTable.
+ * @param rowNumber Integer starts from 0 for the first row
+ */
+ static getDocTableRow(rowNumber) {
+ return cy
+ .getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DOC_TABLE)
+ .get('tbody tr')
+ .eq(rowNumber);
+ }
+
+ /**
+ * Get specific field of DocTable.
+ * @param columnNumber Integer starts from 0 for the first column
+ * @param rowNumber Integer starts from 0 for the first row
+ */
+ static getDocTableField(columnNumber, rowNumber) {
+ return DataExplorerPage.getDocTableRow(rowNumber)
+ .findElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DOC_TABLE_ROW_FIELD)
+ .eq(columnNumber);
+ }
+
+ /**
+ * Get page header.
+ */
+ static getPageHeader() {
+ return cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.HEADER_GLOBAL_NAV);
+ }
+
+ /**
+ * Get query multiline editor element.
+ */
+ static getQueryMultilineEditor() {
+ DataExplorerPage.getPageHeader().click();
+ return cy.get('.view-line');
+ }
+
+ /**
+ * Selects the query submit button over the query multiline editor.
+ */
+ static getQuerySubmitBtn() {
+ return cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.QUERY_SUBMIT_BUTTON);
+ }
+
+ /**
+ *
+ * @param expectedValues array of expected values. E.g. ['50', '57', '52']
+ * @param columnNumber column index beginning at 0
+ */
+ static checkDocTableColumnByArr(expectedValues, columnNumber) {
+ let currentRow = 0;
+ expectedValues.forEach((value) => {
+ DataExplorerPage.getDocTableField(columnNumber, currentRow).should('have.text', value);
+ currentRow++;
+ });
+ }
+
+ /**
+ * Clears the query multiline editor content.
+ * Default cy.clear() will not work.
+ * @param del true/false. true: Deletes character to the right of the cursor; false: Deletes character to the left of the cursor
+ * @see https://docs.cypress.io/api/commands/type#Arguments
+ */
+ static clearQueryMultilineEditor() {
+ DataExplorerPage.getQueryMultilineEditor()
+ .invoke('text')
+ .then(function ($content) {
+ const contentLen = $content.length + 1;
+ DataExplorerPage.getQueryMultilineEditor().type('a');
+ DataExplorerPage.getQueryMultilineEditor().type('{backspace}'.repeat(contentLen));
+ });
+ }
+
+ /**
+ * Sends a new query via the query multiline editor.
+ * @param del true/false. true: Deletes character to the right of the cursor; false: Deletes character to the left of the cursor
+ * @see https://docs.cypress.io/api/commands/type#Arguments
+ */
+ static sendQueryOnMultilineEditor(query) {
+ DataExplorerPage.clearQueryMultilineEditor();
+ DataExplorerPage.getQueryMultilineEditor().type(query);
+ DataExplorerPage.getQuerySubmitBtn().click();
+ }
+
+ /**
+ * Set the query editor language
+ * @param language Accepted values: 'DQL', 'Lucene', 'OpenSearch SQL', 'PPL'
+ */
+ static setQueryEditorLanguage(language) {
+ DataExplorerPage.getPageHeader().click(); // remove helper message
+
+ cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.QUERY_EDITOR_LANGUAGE_SELECTOR).click();
+
+ cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.QUERY_EDITOR_LANGUAGE_OPTIONS)
+ .find('button')
+ .contains(language)
+ .click();
+ }
+
+ /**
+ * Get filter pill value.
+ */
+ static getGlobalQueryEditorFilterValue() {
+ return cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.GLOBAL_QUERY_EDITOR_FILTER_VALUE, {
+ timeout: 10000,
+ });
+ }
+
+ /**
+ * Get query hits.
+ */
+ static getDiscoverQueryHits() {
+ return cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DISCOVER_QUERY_HITS);
+ }
+
+ /**
+ * Get Table Field Filter Out Button.
+ */
+ static getTableFieldFilterOutButton() {
+ return cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.TABLE_FIELD_FILTER_OUT_BUTTON);
+ }
+
+ /**
+ * Get Table Field Filter For Button.
+ */
+ static getTableFieldFilterForButton() {
+ return cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.TABLE_FIELD_FILTER_FOR_BUTTON);
+ }
+
+ /**
+ * Get Filter Bar.
+ */
+ static getFilterBar() {
+ return cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.GLOBAL_FILTER_BAR);
+ }
+
+ /**
+ * Get sidebar filter bar.
+ */
+ static getSidebarFilterBar() {
+ return cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.SIDEBAR_FILTER_BAR);
+ }
+
+ /**
+ * Click on the "Clear input" button on the sidebar filter bar.
+ */
+ static clearSidebarFilterBar() {
+ return cy.get('button[aria-label="Clear input"]').click();
+ }
+
+ /**
+ * Get sidebar add field button by index.
+ * @param index Integer that starts at 0 for the first add button.
+ */
+ static getFieldBtnByIndex(index) {
+ return cy.getElementByTestIdLike('fieldToggle-', 'beginning').eq(index);
+ }
+
+ /**
+ * Get sidebar add field button by name.
+ */
+ static getFieldBtnByName(name) {
+ return cy.getElementByTestId('fieldToggle-' + name);
+ }
+
+ /**
+ * Get all sidebar add field button.
+ */
+ static getAllSidebarAddFields() {
+ return cy.get('[data-test-subj^="field-"]:not([data-test-subj$="showDetails"])');
+ }
+
+ static getSidebar() {
+ return cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.SIDEBAR_PANEL_OWNREFERENCE);
+ }
+
+ static getResizeableBar() {
+ return cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.SIDEBAR_PANEL_RESIZEABLE_BAR);
+ }
+
+ static getResizeableToggleButton() {
+ return cy.get('.euiResizableToggleButton');
+ }
+
+ static collapseSidebar() {
+ DataExplorerPage.getResizeableBar().trigger('mouseover').click();
+ DataExplorerPage.getResizeableToggleButton().click({ force: true });
+ }
+
+ static expandSidebar() {
+ DataExplorerPage.getResizeableToggleButton().click();
+ }
+
+ /**
+ * Check the results of the sidebar filter bar search.
+ * @param search string to look up
+ * @param assertion the type of assertion that is going to be performed. Example: 'eq', 'include'
+ */
+ static checkSidebarFilterBarResults(assertion, search) {
+ DataExplorerPage.getSidebarFilterBar().type(search, { force: true });
+ DataExplorerPage.getAllSidebarAddFields().each(function ($field) {
+ cy.wrap($field)
+ .should('be.visible')
+ .invoke('text')
+ .then(function ($fieldTxt) {
+ cy.wrap($fieldTxt).should(assertion, search);
+ });
+ });
+ DataExplorerPage.clearSidebarFilterBar();
+ }
+
+ /**
+ * Checks that the searched non-existent field does not appear on the DOM.
+ * @param search non-existent field
+ */
+ static checkSidebarFilterBarNegativeResults(search) {
+ DataExplorerPage.getSidebarFilterBar().type(search);
+ DataExplorerPage.getAllSidebarAddFields().should('not.exist');
+ DataExplorerPage.clearSidebarFilterBar();
+ }
+
+ /**
+ * Open window to select Dataset
+ */
+ static openDatasetExplorerWindow() {
+ DataExplorerPage.getDatasetSelectorButton().click();
+ DataExplorerPage.getAllDatasetsButton().click();
+ }
+
+ /**
+ * Select a Time Field in the Dataset Selector
+ * @param timeField Timefield for Language specific Time field. PPL allows "birthdate", "timestamp" and "I don't want to use the time filter"
+ */
+ static selectDatasetTimeField(timeField) {
+ DataExplorerPage.getDatasetTimeSelector().select(timeField);
+ }
+
+ /**
+ * Select a language in the Dataset Selector for Index
+ * @param datasetLanguage Index supports "OpenSearch SQL" and "PPL"
+ */
+ static selectIndexDatasetLanguage(datasetLanguage, timeField) {
+ DataExplorerPage.getDatasetLanguageSelector().select(datasetLanguage);
+ DataExplorerPage.selectDatasetTimeField(timeField);
+ DataExplorerPage.getDatasetSelectDataButton().click();
+ }
+
+ /**
+ * Select an index dataset.
+ * @param datasetLanguage Index supports "OpenSearch SQL" and "PPL"
+ */
+ static selectIndexDataset(datasetLanguage, timeField, indexCluster, indexName) {
+ DataExplorerPage.openDatasetExplorerWindow();
+ DataExplorerPage.getDatasetExplorerWindow().contains('Indexes').click();
+ DataExplorerPage.getDatasetExplorerWindow().contains(indexCluster, { timeout: 10000 }).click();
+ DataExplorerPage.getDatasetExplorerWindow().contains(indexName, { timeout: 10000 }).click();
+ DataExplorerPage.getDatasetExplorerNextButton().click();
+ DataExplorerPage.selectIndexDatasetLanguage(datasetLanguage, timeField);
+ }
+
+ /**
+ * Select a language in the Dataset Selector for Index Pattern
+ * @param datasetLanguage Index Pattern supports "DQL", "Lucene", "OpenSearch SQL" and "PPL"
+ */
+ static selectIndexPatternDatasetLanguage(datasetLanguage) {
+ DataExplorerPage.getDatasetLanguageSelector().select(datasetLanguage);
+ DataExplorerPage.getDatasetSelectDataButton().click();
+ }
+
+ /**
+ * Select an index pattern dataset.
+ * @param datasetLanguage Index Pattern supports "DQL", "Lucene", "OpenSearch SQL" and "PPL"
+ */
+ static selectIndexPatternDataset(datasetLanguage) {
+ DataExplorerPage.openDatasetExplorerWindow();
+ DataExplorerPage.getDatasetExplorerWindow().contains('Index Patterns').click();
+ DataExplorerPage.getDatasetExplorerWindow()
+ .contains(INDEX_PATTERN_NAME, { timeout: 10000 })
+ .click();
+ DataExplorerPage.getDatasetExplorerNextButton().click();
+ DataExplorerPage.selectIndexPatternDatasetLanguage(datasetLanguage);
+ }
+
+ /**
+ * Set search Date range
+ * @param relativeNumber Relative integer string to set date range
+ * @param relativeUnit Unit for number. Accepted Units: seconds/Minutes/Hours/Days/Weeks/Months/Years ago/from now
+ * @example setSearchRelativeDateRange('15', 'years ago')
+ */
+ static setSearchRelativeDateRange(relativeNumber, relativeUnit) {
+ cy.getSearchDatePickerButton().click();
+ cy.getDatePickerRelativeTab().click();
+ cy.getDatePickerRelativeInput().clear().type(relativeNumber);
+ cy.getDatePickerRelativeUnitSelector().select(relativeUnit);
+ cy.getQuerySubmitButton().click();
+ }
+
+ /**
+ * Check the filter pill text matches expectedFilterText.
+ * @param expectedFilterText expected text in filter pill.
+ */
+ static checkFilterPillText(expectedFilterText) {
+ DataExplorerPage.getGlobalQueryEditorFilterValue().should('have.text', expectedFilterText);
+ }
+
+ /**
+ * Check the query hit text matches expectedQueryHitText.
+ * @param expectedQueryHitsText expected text for query hits
+ */
+ static checkQueryHitsText(expectedQueryHitsText) {
+ DataExplorerPage.getDiscoverQueryHits().should('have.text', expectedQueryHitsText);
+ }
+
+ /**
+ * Check for the first Table Field's Filter For and Filter Out button.
+ * @param isExists Boolean determining if these button should exist
+ */
+ static checkDocTableFirstFieldFilterForAndOutButton(isExists) {
+ const shouldText = isExists ? 'exist' : 'not.exist';
+ DataExplorerPage.getDocTableField(0, 0).within(() => {
+ DataExplorerPage.getTableFieldFilterForButton().should(shouldText);
+ DataExplorerPage.getTableFieldFilterOutButton().should(shouldText);
+ });
+ }
+
+ /**
+ * Check the Doc Table first Field's Filter For button filters the correct value.
+ */
+ static checkDocTableFirstFieldFilterForButtonFiltersCorrectField() {
+ DataExplorerPage.getDocTableField(0, 0).then(($field) => {
+ const filterFieldText = $field.find('span span').text();
+ $field
+ .find(`[data-test-subj="${DATA_EXPLORER_PAGE_ELEMENTS.TABLE_FIELD_FILTER_FOR_BUTTON}"]`)
+ .click();
+ DataExplorerPage.checkFilterPillText(filterFieldText);
+ DataExplorerPage.checkQueryHitsText('1'); // checkQueryHitText must be in front of checking first line text to give time for DocTable to update.
+ DataExplorerPage.getDocTableField(0, 0)
+ .find('span span')
+ .should('have.text', filterFieldText);
+ });
+ DataExplorerPage.getFilterBar().find('[aria-label="Delete"]').click();
+ DataExplorerPage.checkQueryHitsText('10,000');
+ }
+
+ /**
+ * Check the Doc Table first Field's Filter Out button filters the correct value.
+ */
+ static checkDocTableFirstFieldFilterOutButtonFiltersCorrectField() {
+ DataExplorerPage.getDocTableField(0, 0).then(($field) => {
+ const filterFieldText = $field.find('span span').text();
+ $field
+ .find(`[data-test-subj="${DATA_EXPLORER_PAGE_ELEMENTS.TABLE_FIELD_FILTER_OUT_BUTTON}"]`)
+ .click();
+ DataExplorerPage.checkFilterPillText(filterFieldText);
+ DataExplorerPage.checkQueryHitsText('9,999'); // checkQueryHitText must be in front of checking first line text to give time for DocTable to update.
+ DataExplorerPage.getDocTableField(0, 0)
+ .find('span span')
+ .should('not.have.text', filterFieldText);
+ });
+ DataExplorerPage.getFilterBar().find('[aria-label="Delete"]').click();
+ DataExplorerPage.checkQueryHitsText('10,000');
+ }
+
+ /**
+ *
+ * @param expectedHeaders array containing the expected header names
+ * @param offset used to adjust the index of the table headers being checked. Set to 1 by default, which means the method starts checking headers from an index that is 1 higher than the current loop index (i + offset).
+ */
+ static checkTableHeadersByArray(expectedHeaders, offset = 1) {
+ for (let i = 0; i < expectedHeaders.length; i++) {
+ DataExplorerPage.getDocTableHeader(i + offset).should('have.text', expectedHeaders[i]);
+ }
+ }
+}
diff --git a/cypress/utils/dashboards/data_explorer/elements.js b/cypress/utils/dashboards/data_explorer/elements.js
new file mode 100644
index 000000000000..3346fe2a585f
--- /dev/null
+++ b/cypress/utils/dashboards/data_explorer/elements.js
@@ -0,0 +1,35 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+export const DATA_EXPLORER_PAGE_ELEMENTS = {
+ HEADER_GLOBAL_NAV: 'headerGlobalNav',
+ NEW_SEARCH_BUTTON: 'discoverNewButton',
+ DISCOVER_QUERY_HITS: 'discoverQueryHits',
+ DATASET_SELECTOR_BUTTON: 'datasetSelectorButton',
+ ALL_DATASETS_BUTTON: 'datasetSelectorAdvancedButton',
+ DATASET_EXPLORER_WINDOW: 'datasetExplorerWindow',
+ DATASET_SELECTOR_NEXT_BUTTON: 'datasetSelectorNext',
+ DATASET_SELECTOR_LANGUAGE_SELECTOR: 'advancedSelectorLanguageSelect',
+ DATASET_SELECTOR_TIME_SELECTOR: 'advancedSelectorTimeFieldSelect',
+ DATASET_SELECTOR_SELECT_DATA_BUTTON: 'advancedSelectorConfirmButton',
+ DOC_TABLE: 'docTable',
+ DOC_TABLE_HEADER_FIELD: 'docTableHeaderField',
+ DOC_TABLE_ROW_FIELD: 'docTableField',
+ TABLE_FIELD_FILTER_FOR_BUTTON: 'filterForValue',
+ TABLE_FIELD_FILTER_OUT_BUTTON: 'filterOutValue',
+ SEARCH_DATE_PICKER_BUTTON: 'superDatePickerShowDatesButton',
+ SEARCH_DATE_PICKER_RELATIVE_TAB: 'superDatePickerRelativeTab',
+ SEARCH_DATE_RELATIVE_PICKER_INPUT: 'superDatePickerRelativeDateInputNumber',
+ SEARCH_DATE_RELATIVE_PICKER_UNIT_SELECTOR: 'superDatePickerRelativeDateInputUnitSelector',
+ QUERY_EDITOR_LANGUAGE_SELECTOR: 'queryEditorLanguageSelector',
+ QUERY_EDITOR_LANGUAGE_OPTIONS: 'queryEditorLanguageOptions',
+ QUERY_SUBMIT_BUTTON: 'querySubmitButton',
+ QUERY_EDITOR_MULTILINE: 'osdQueryEditor__multiLine',
+ GLOBAL_QUERY_EDITOR_FILTER_VALUE: 'globalFilterLabelValue',
+ GLOBAL_FILTER_BAR: 'globalFilterBar',
+ SIDEBAR_PANEL_OWNREFERENCE: 'sidebarPanel',
+ SIDEBAR_PANEL_RESIZEABLE_BAR: 'euiResizableButton',
+ SIDEBAR_FILTER_BAR: 'fieldFilterSearchInput',
+};
diff --git a/docs/_sidebar.md b/docs/_sidebar.md
index 2584d106beb0..8fc7e380e6b6 100644
--- a/docs/_sidebar.md
+++ b/docs/_sidebar.md
@@ -149,6 +149,7 @@
- [Opensearch dashboards.release notes 1.3.17](../release-notes/opensearch-dashboards.release-notes-1.3.17.md)
- [Opensearch dashboards.release notes 1.3.19](../release-notes/opensearch-dashboards.release-notes-1.3.19.md)
- [Opensearch dashboards.release notes 1.3.2](../release-notes/opensearch-dashboards.release-notes-1.3.2.md)
+ - [Opensearch dashboards.release notes 1.3.20](../release-notes/opensearch-dashboards.release-notes-1.3.20.md)
- [Opensearch dashboards.release notes 1.3.3](../release-notes/opensearch-dashboards.release-notes-1.3.3.md)
- [Opensearch dashboards.release notes 1.3.5](../release-notes/opensearch-dashboards.release-notes-1.3.5.md)
- [Opensearch dashboards.release notes 1.3.6](../release-notes/opensearch-dashboards.release-notes-1.3.6.md)
diff --git a/package.json b/package.json
index 5bd2a4a5d09f..7c3bb252ecef 100644
--- a/package.json
+++ b/package.json
@@ -111,7 +111,6 @@
"**/jest-config": "npm:@amoo-miki/jest-config@27.5.1",
"**/jest-jasmine2": "npm:@amoo-miki/jest-jasmine2@27.5.1",
"**/joi/hoek": "npm:@amoo-miki/hoek@6.1.3",
- "**/json11": "^2.0.0",
"**/json-schema": "^0.4.0",
"**/kind-of": ">=6.0.3",
"**/load-bmfont/phin": "^3.7.1",
@@ -125,6 +124,7 @@
"**/trim": "^0.0.3",
"**/typescript": "4.6.4",
"**/unset-value": "^2.0.1",
+ "**/url": "^0.11.4",
"**/watchpack-chokidar2/chokidar": "^3.5.3",
"**/xml2js": "^0.5.0",
"**/yaml": "^2.2.2"
@@ -166,7 +166,7 @@
"@hapi/vision": "^6.1.0",
"@hapi/wreck": "^17.1.0",
"@opensearch-dashboards-test/opensearch-dashboards-test-library": "https://github.com/opensearch-project/opensearch-dashboards-test-library/archive/refs/tags/1.0.6.tar.gz",
- "@opensearch-project/opensearch": "^2.9.0",
+ "@opensearch-project/opensearch": "^2.13.0",
"@opensearch/datemath": "5.0.3",
"@osd/ace": "1.0.0",
"@osd/analytics": "1.0.0",
@@ -260,6 +260,7 @@
"@babel/plugin-transform-class-static-block": "^7.24.4",
"@babel/register": "^7.22.9",
"@babel/types": "^7.22.9",
+ "@cypress/webpack-preprocessor": "^5.17.1",
"@elastic/apm-rum": "^5.6.1",
"@elastic/charts": "31.1.0",
"@elastic/ems-client": "7.10.0",
@@ -384,7 +385,7 @@
"chromedriver": "^121.0.1",
"classnames": "^2.3.1",
"compare-versions": "3.5.1",
- "cypress": "9.5.4",
+ "cypress": "12.17.4",
"d3": "3.5.17",
"d3-cloud": "1.2.5",
"dedent": "^0.7.0",
diff --git a/packages/opensearch-datemath/index.d.ts b/packages/opensearch-datemath/index.d.ts
index 0706d7d0dccf..fde4b10013a7 100644
--- a/packages/opensearch-datemath/index.d.ts
+++ b/packages/opensearch-datemath/index.d.ts
@@ -47,6 +47,8 @@ declare const datemath: {
/**
* Parses a string into a moment object. The string can be something like "now - 15m".
+ * @param options.roundUp - If true, rounds the parsed date to the end of the
+ * unit. Only works for string with "/" like "now/d".
* @param options.forceNow If this optional parameter is supplied, "now" will be treated as this
* date, rather than the real "now".
*/
diff --git a/packages/osd-monaco/src/xjson/lexer_rules/opensearchsql.ts b/packages/osd-monaco/src/xjson/lexer_rules/opensearchsql.ts
index 0ff29b71c09d..6697b3592c15 100644
--- a/packages/osd-monaco/src/xjson/lexer_rules/opensearchsql.ts
+++ b/packages/osd-monaco/src/xjson/lexer_rules/opensearchsql.ts
@@ -134,18 +134,22 @@ export const lexerRules = {
[new RegExp(operators.join('|')), 'operator'],
[/[0-9]+(\.[0-9]+)?/, 'number'],
[/'([^'\\]|\\.)*$/, 'string.invalid'], // non-terminated string
- [/'/, 'string', '@string'],
- [/"/, 'string', '@string'],
+ [/'/, 'string', '@stringSingle'],
+ [/"/, 'string', '@stringDouble'],
],
whitespace: [
[/[ \t\r\n]+/, 'white'],
[/\/\*/, 'comment', '@comment'],
[/--.*$/, 'comment'],
],
- string: [
+ stringSingle: [
[/[^'\\]+/, 'string'],
[/\\./, 'string.escape'],
[/'/, 'string', '@pop'],
+ ],
+ stringDouble: [
+ [/[^"\\]+/, 'string'],
+ [/\\./, 'string.escape'],
[/"/, 'string', '@pop'],
],
comment: [
diff --git a/packages/osd-opensearch-archiver/package.json b/packages/osd-opensearch-archiver/package.json
index d1e9174299fa..bc4e8b227b30 100644
--- a/packages/osd-opensearch-archiver/package.json
+++ b/packages/osd-opensearch-archiver/package.json
@@ -13,7 +13,7 @@
"dependencies": {
"@osd/dev-utils": "1.0.0",
"@osd/std": "1.0.0",
- "@opensearch-project/opensearch": "^2.9.0"
+ "@opensearch-project/opensearch": "^2.13.0"
},
"devDependencies": {}
}
diff --git a/packages/osd-opensearch/package.json b/packages/osd-opensearch/package.json
index 4459c846c6c2..a70263e8af6d 100644
--- a/packages/osd-opensearch/package.json
+++ b/packages/osd-opensearch/package.json
@@ -12,7 +12,7 @@
"osd:watch": "../../scripts/use_node scripts/build --watch"
},
"dependencies": {
- "@opensearch-project/opensearch": "^2.9.0",
+ "@opensearch-project/opensearch": "^2.13.0",
"@osd/dev-utils": "1.0.0",
"abort-controller": "^3.0.0",
"chalk": "^4.1.0",
diff --git a/packages/osd-plugin-generator/template/public/components/app.tsx.ejs b/packages/osd-plugin-generator/template/public/components/app.tsx.ejs
index 2029a69dd8db..876b3f8c5e75 100644
--- a/packages/osd-plugin-generator/template/public/components/app.tsx.ejs
+++ b/packages/osd-plugin-generator/template/public/components/app.tsx.ejs
@@ -4,7 +4,7 @@ import { FormattedMessage, I18nProvider } from '@osd/i18n/react';
import { BrowserRouter as Router } from 'react-router-dom';
import {
-EuiSmallButton,
+EuiButton,
EuiHorizontalRule,
EuiPage,
EuiPageBody,
diff --git a/scripts/postinstall.js b/scripts/postinstall.js
index 59be50284dca..7865473ee494 100644
--- a/scripts/postinstall.js
+++ b/scripts/postinstall.js
@@ -84,15 +84,6 @@ const run = async () => {
},
])
);
- //ToDo: Remove when opensearch-js is released to include https://github.com/opensearch-project/opensearch-js/pull/889
- promises.push(
- patchFile('node_modules/@opensearch-project/opensearch/lib/Serializer.js', [
- {
- from: 'val < Number.MAX_SAFE_INTEGER',
- to: 'val < Number.MIN_SAFE_INTEGER',
- },
- ])
- );
await Promise.all(promises);
};
diff --git a/src/core/public/chrome/ui/header/recent_items.test.tsx b/src/core/public/chrome/ui/header/recent_items.test.tsx
index d01912e9c27f..28bae880fcfa 100644
--- a/src/core/public/chrome/ui/header/recent_items.test.tsx
+++ b/src/core/public/chrome/ui/header/recent_items.test.tsx
@@ -18,7 +18,7 @@ jest.mock('./nav_link', () => ({
}),
}));
-const mockRecentlyAccessed = new BehaviorSubject([
+const mockRecentlyAccessed$ = new BehaviorSubject([
{
id: '6ef856c0-5f86-11ef-b7df-1bb1cf26ce5b',
label: 'visualizeMock',
@@ -28,7 +28,7 @@ const mockRecentlyAccessed = new BehaviorSubject([
},
]);
-const mockWorkspaceList = new BehaviorSubject([
+const mockWorkspaceList$ = new BehaviorSubject([
{
id: 'workspace_1',
name: 'WorkspaceMock_1',
@@ -49,7 +49,14 @@ const defaultMockProps = {
navigateToUrl: applicationServiceMock.createStartContract().navigateToUrl,
workspaceList$: new BehaviorSubject([]),
recentlyAccessed$: new BehaviorSubject([]),
- navLinks$: new BehaviorSubject([]),
+ navLinks$: new BehaviorSubject([
+ {
+ id: '',
+ title: '',
+ baseUrl: '',
+ href: '',
+ },
+ ]),
basePath: httpServiceMock.createStartContract().basePath,
http: httpServiceMock.createSetupContract(),
renderBreadcrumbs: <>>,
@@ -85,7 +92,8 @@ describe('Recent items', () => {
it('should be able to render recent works', async () => {
const mockProps = {
...defaultMockProps,
- recentlyAccessed$: mockRecentlyAccessed,
+ recentlyAccessed$: mockRecentlyAccessed$,
+ workspaceList$: mockWorkspaceList$,
};
await act(async () => {
@@ -97,11 +105,11 @@ describe('Recent items', () => {
expect(screen.getByText('visualizeMock')).toBeInTheDocument();
});
- it('shoulde be able to display workspace name if the asset is attched to a workspace and render it with brackets wrapper ', async () => {
+ it('should be able to display workspace name if the asset is attched to a workspace and render it with brackets wrapper ', async () => {
const mockProps = {
...defaultMockProps,
- recentlyAccessed$: mockRecentlyAccessed,
- workspaceList$: mockWorkspaceList,
+ recentlyAccessed$: mockRecentlyAccessed$,
+ workspaceList$: mockWorkspaceList$,
};
await act(async () => {
@@ -116,8 +124,8 @@ describe('Recent items', () => {
it('should call navigateToUrl with link generated from createRecentNavLink when clicking a recent item', async () => {
const mockProps = {
...defaultMockProps,
- recentlyAccessed$: mockRecentlyAccessed,
- workspaceList$: mockWorkspaceList,
+ recentlyAccessed$: mockRecentlyAccessed$,
+ workspaceList$: mockWorkspaceList$,
};
const navigateToUrl = jest.fn();
@@ -137,7 +145,7 @@ describe('Recent items', () => {
it('should be able to display the preferences popover setting when clicking Preferences button', async () => {
const mockProps = {
...defaultMockProps,
- recentlyAccessed$: mockRecentlyAccessed,
+ recentlyAccessed$: mockRecentlyAccessed$,
};
await act(async () => {
@@ -158,4 +166,9 @@ describe('Recent items', () => {
);
expect(baseElement).toMatchSnapshot();
});
+
+ it('should show not display item if it is in a workspace which is not available', () => {
+ render();
+ expect(screen.queryByText('visualizeMock')).not.toBeInTheDocument();
+ });
});
diff --git a/src/core/public/chrome/ui/header/recent_items.tsx b/src/core/public/chrome/ui/header/recent_items.tsx
index 7efd276b8fa9..298bf51d2bc6 100644
--- a/src/core/public/chrome/ui/header/recent_items.tsx
+++ b/src/core/public/chrome/ui/header/recent_items.tsx
@@ -143,7 +143,9 @@ export const RecentItems = ({
setIsPreferencesPopoverOpen((IsPreferencesPopoverOpe) => !IsPreferencesPopoverOpe);
}}
>
- Preferences
+ {i18n.translate('core.header.recent.preferences', {
+ defaultMessage: 'Preferences',
+ })}
}
isOpen={isPreferencesPopoverOpen}
@@ -152,7 +154,11 @@ export const RecentItems = ({
setIsPreferencesPopoverOpen(false);
}}
>
- Preferences
+
+ {i18n.translate('core.header.recent.preferences.title', {
+ defaultMessage: 'Preferences',
+ })}
+
Recents,
+ children: (
+
+ {i18n.translate('core.header.recent.preferences.legend', {
+ defaultMessage: 'Recents',
+ })}
+
+ ),
}}
/>
@@ -208,15 +220,20 @@ export const RecentItems = ({
useEffect(() => {
const savedObjects = recentlyAccessedItems
- .filter((item) => item.meta?.type)
+ .filter(
+ (item) =>
+ item.meta?.type &&
+ (!item.workspaceId ||
+ // If the workspace id is existing but the workspace is deleted, filter the item
+ (item.workspaceId &&
+ !!workspaceList.find((workspace) => workspace.id === item.workspaceId)))
+ )
.map((item) => ({
type: item.meta?.type || '',
id: item.id,
}));
-
if (savedObjects.length) {
bulkGetDetail(savedObjects, http).then((res) => {
- const filteredNavLinks = navLinks.filter((link) => !link.hidden);
const formatDetailedSavedObjects = res.map((obj) => {
const recentAccessItem = recentlyAccessedItems.find(
(item) => item.id === obj.id
@@ -225,33 +242,21 @@ export const RecentItems = ({
const findWorkspace = workspaceList.find(
(workspace) => workspace.id === recentAccessItem.workspaceId
);
+
return {
...recentAccessItem,
...obj,
...recentAccessItem.meta,
updatedAt: moment(obj?.updated_at).valueOf(),
workspaceName: findWorkspace?.name,
- link: createRecentNavLink(recentAccessItem, filteredNavLinks, basePath, navigateToUrl)
- .href,
};
});
- // here I write this argument to avoid Unnecessary re-rendering
- if (JSON.stringify(formatDetailedSavedObjects) !== JSON.stringify(detailedSavedObjects)) {
- setDetailedSavedObjects(formatDetailedSavedObjects);
- }
+ setDetailedSavedObjects(formatDetailedSavedObjects);
});
}
- }, [
- navLinks,
- basePath,
- navigateToUrl,
- recentlyAccessedItems,
- http,
- workspaceList,
- detailedSavedObjects,
- ]);
+ }, [recentlyAccessedItems, http, workspaceList]);
- const selectedRecentsItems = useMemo(() => {
+ const selectedRecentItems = useMemo(() => {
return detailedSavedObjects.slice(0, Number(recentsRadioIdSelected));
}, [detailedSavedObjects, recentsRadioIdSelected]);
@@ -283,11 +288,20 @@ export const RecentItems = ({
- {selectedRecentsItems.length > 0 ? (
+ {selectedRecentItems.length > 0 ? (
- {selectedRecentsItems.map((item) => (
+ {selectedRecentItems.map((item) => (
handleItemClick(item.link)}
+ onClick={() =>
+ handleItemClick(
+ createRecentNavLink(
+ item,
+ navLinks.filter((link) => !link.hidden),
+ basePath,
+ navigateToUrl
+ ).href
+ )
+ }
key={item.link}
style={{ padding: '1px' }}
label={
@@ -309,7 +323,9 @@ export const RecentItems = ({
) : (
- No recently viewed items
+ {i18n.translate('core.header.recent.no.recents', {
+ defaultMessage: 'No recently viewed items',
+ })}
)}
diff --git a/src/core/server/ui_settings/settings/date_formats.ts b/src/core/server/ui_settings/settings/date_formats.ts
index 804d3bb3b58a..b426b76a6dbb 100644
--- a/src/core/server/ui_settings/settings/date_formats.ts
+++ b/src/core/server/ui_settings/settings/date_formats.ts
@@ -122,7 +122,7 @@ export const getDateFormatSettings = (): Record => {
'core.ui_settings.params.dateFormat.scaled.intervalsLinkText',
values: {
intervalsLink:
- '' +
+ '' +
i18n.translate('core.ui_settings.params.dateFormat.scaled.intervalsLinkText', {
defaultMessage: 'ISO8601 intervals',
}) +
diff --git a/src/plugins/data/common/data_frames/utils.test.ts b/src/plugins/data/common/data_frames/utils.test.ts
new file mode 100644
index 000000000000..5ba877c963c2
--- /dev/null
+++ b/src/plugins/data/common/data_frames/utils.test.ts
@@ -0,0 +1,27 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import datemath from '@opensearch/datemath';
+import { formatTimePickerDate } from '.';
+
+describe('formatTimePickerDate', () => {
+ const mockDateFormat = 'YYYY-MM-DD HH:mm:ss';
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should handle date range with rounding', () => {
+ jest.spyOn(datemath, 'parse');
+
+ const result = formatTimePickerDate({ from: 'now/d', to: 'now/d' }, mockDateFormat);
+
+ expect(result.fromDate).not.toEqual(result.toDate);
+
+ expect(datemath.parse).toHaveBeenCalledTimes(2);
+ expect(datemath.parse).toHaveBeenCalledWith('now/d', { roundUp: undefined });
+ expect(datemath.parse).toHaveBeenCalledWith('now/d', { roundUp: true });
+ });
+});
diff --git a/src/plugins/data/common/data_frames/utils.ts b/src/plugins/data/common/data_frames/utils.ts
index fdee757bfabb..7e280478630a 100644
--- a/src/plugins/data/common/data_frames/utils.ts
+++ b/src/plugins/data/common/data_frames/utils.ts
@@ -156,13 +156,13 @@ export const getTimeField = (
* the `dateFormat` parameter
*/
export const formatTimePickerDate = (dateRange: TimeRange, dateFormat: string) => {
- const dateMathParse = (date: string) => {
- const parsedDate = datemath.parse(date);
+ const dateMathParse = (date: string, roundUp?: boolean) => {
+ const parsedDate = datemath.parse(date, { roundUp });
return parsedDate ? parsedDate.utc().format(dateFormat) : '';
};
const fromDate = dateMathParse(dateRange.from);
- const toDate = dateMathParse(dateRange.to);
+ const toDate = dateMathParse(dateRange.to, true);
return { fromDate, toDate };
};
diff --git a/src/plugins/data/public/query/query_string/dataset_service/dataset_service.mock.ts b/src/plugins/data/public/query/query_string/dataset_service/dataset_service.mock.ts
index df5521078feb..ba491cb51191 100644
--- a/src/plugins/data/public/query/query_string/dataset_service/dataset_service.mock.ts
+++ b/src/plugins/data/public/query/query_string/dataset_service/dataset_service.mock.ts
@@ -43,6 +43,9 @@ const createSetupDatasetServiceMock = (): jest.Mocked =>
fetchOptions: jest.fn(),
getRecentDatasets: jest.fn(),
addRecentDataset: jest.fn(),
+ clearCache: jest.fn(),
+ getLastCacheTime: jest.fn(),
+ removeFromRecentDatasets: jest.fn(),
};
};
diff --git a/src/plugins/data/public/query/query_string/dataset_service/types.ts b/src/plugins/data/public/query/query_string/dataset_service/types.ts
index 65c322acec6f..d97afec8abb6 100644
--- a/src/plugins/data/public/query/query_string/dataset_service/types.ts
+++ b/src/plugins/data/public/query/query_string/dataset_service/types.ts
@@ -43,6 +43,13 @@ export interface DatasetTypeConfig {
id: string;
/** Human-readable title for the dataset type */
title: string;
+ languageOverrides?: {
+ [language: string]: {
+ /** The override transfers the responsibility of handling the input from
+ * the language interceptor to the dataset type search strategy. */
+ hideDatePicker?: boolean;
+ };
+ };
/** Metadata for UI representation */
meta: {
/** Icon to represent the dataset type */
@@ -51,7 +58,7 @@ export interface DatasetTypeConfig {
tooltip?: string;
/** Optional preference for search on page load else defaulted to true */
searchOnLoad?: boolean;
- /** Optional supportsTimeFilter determines if a time filter is needed */
+ /** Optional supportsTimeFilter determines if a time field is supported */
supportsTimeFilter?: boolean;
/** Optional isFieldLoadAsync determines if field loads are async */
isFieldLoadAsync?: boolean;
diff --git a/src/plugins/data/public/ui/dataset_selector/configurator.test.tsx b/src/plugins/data/public/ui/dataset_selector/configurator.test.tsx
index 462c6298a0a3..38d4e4e12183 100644
--- a/src/plugins/data/public/ui/dataset_selector/configurator.test.tsx
+++ b/src/plugins/data/public/ui/dataset_selector/configurator.test.tsx
@@ -3,14 +3,14 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import { render, screen, fireEvent, waitFor } from '@testing-library/react';
-import { Configurator } from './configurator';
import '@testing-library/jest-dom';
+import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import React from 'react';
-import { setQueryService, setIndexPatterns } from '../../services';
import { IntlProvider } from 'react-intl';
-import { Query } from '../../../../data/public';
import { Dataset } from 'src/plugins/data/common';
+import { Query } from '../../../../data/public';
+import { setIndexPatterns, setQueryService } from '../../services';
+import { Configurator } from './configurator';
const getQueryMock = jest.fn().mockReturnValue({
query: '',
@@ -358,4 +358,68 @@ describe('Configurator Component', () => {
expect(submitButton).toBeEnabled();
});
});
+
+ it('should show the date picker if supportsTimeFilter is undefined', async () => {
+ const mockDataset = {
+ ...mockBaseDataset,
+ timeFieldName: undefined,
+ type: 'index',
+ };
+ const { container } = render(
+
+
+
+ );
+
+ expect(
+ container.querySelector(`[data-test-subj="advancedSelectorTimeFieldSelect"]`)
+ ).toBeTruthy();
+ });
+
+ it('should hide the date picker if supportsTimeFilter is false', async () => {
+ const mockDataset = {
+ ...mockBaseDataset,
+ timeFieldName: undefined,
+ type: 'index',
+ };
+ const datasetTypeConfig = mockServices
+ .getQueryService()
+ .queryString.getDatasetService()
+ .getType();
+ mockServices
+ .getQueryService()
+ .queryString.getDatasetService()
+ .getType.mockReturnValue({
+ ...datasetTypeConfig,
+ meta: {
+ supportsTimeFilter: false,
+ },
+ });
+ const { container } = render(
+
+
+
+ );
+
+ expect(
+ container.querySelector(`[data-test-subj="advancedSelectorTimeFieldSelect"]`)
+ ).toBeFalsy();
+
+ mockServices
+ .getQueryService()
+ .queryString.getDatasetService()
+ .getType.mockReturnValue(datasetTypeConfig);
+ });
});
diff --git a/src/plugins/data/public/ui/dataset_selector/configurator.tsx b/src/plugins/data/public/ui/dataset_selector/configurator.tsx
index 0dba9107934c..4906bec2ef84 100644
--- a/src/plugins/data/public/ui/dataset_selector/configurator.tsx
+++ b/src/plugins/data/public/ui/dataset_selector/configurator.tsx
@@ -69,6 +69,7 @@ export const Configurator = ({
const [selectedIndexedView, setSelectedIndexedView] = useState();
const [indexedViews, setIndexedViews] = useState([]);
const [isLoadingIndexedViews, setIsLoadingIndexedViews] = useState(false);
+ const [timeFieldsLoading, setTimeFieldsLoading] = useState(false);
useEffect(() => {
let isMounted = true;
@@ -91,23 +92,26 @@ export const Configurator = ({
const submitDisabled = useMemo(() => {
return (
- timeFieldName === undefined &&
- !(
- languageService.getLanguage(language)?.hideDatePicker ||
- dataset.type === DEFAULT_DATA.SET_TYPES.INDEX_PATTERN
- ) &&
- timeFields &&
- timeFields.length > 0
+ timeFieldsLoading ||
+ (timeFieldName === undefined &&
+ !(dataset.type === DEFAULT_DATA.SET_TYPES.INDEX_PATTERN) &&
+ timeFields &&
+ timeFields.length > 0)
);
- }, [dataset, language, timeFieldName, timeFields, languageService]);
+ }, [dataset, timeFieldName, timeFields, timeFieldsLoading]);
useEffect(() => {
const fetchFields = async () => {
- const datasetFields = await queryString
- .getDatasetService()
- .getType(baseDataset.type)
- ?.fetchFields(baseDataset);
+ const datasetType = queryString.getDatasetService().getType(baseDataset.type);
+ if (!datasetType) {
+ setTimeFields([]);
+ return;
+ }
+ setTimeFieldsLoading(true);
+ const datasetFields = await datasetType
+ .fetchFields(baseDataset)
+ .finally(() => setTimeFieldsLoading(false));
const dateFields = datasetFields?.filter((field) => field.type === 'date');
setTimeFields(dateFields || []);
};
@@ -152,6 +156,16 @@ export const Configurator = ({
};
}, [indexedViewsService, selectedIndexedView, dataset]);
+ const shouldRenderDatePickerField = useCallback(() => {
+ const datasetType = queryString.getDatasetService().getType(dataset.type);
+
+ const supportsTimeField = datasetType?.meta?.supportsTimeFilter;
+ if (supportsTimeField !== undefined) {
+ return Boolean(supportsTimeField);
+ }
+ return true;
+ }, [dataset.type, queryString]);
+
return (
<>
@@ -256,7 +270,7 @@ export const Configurator = ({
data-test-subj="advancedSelectorLanguageSelect"
/>
- {!languageService.getLanguage(language)?.hideDatePicker &&
+ {shouldRenderDatePickerField() &&
(dataset.type === DEFAULT_DATA.SET_TYPES.INDEX_PATTERN ? (
(
-
+
{
:
Warning
@@ -125,6 +126,7 @@ test('alias with error status', () => {
:
Error
@@ -141,6 +143,7 @@ test('warning', () => {
:
Warning
@@ -157,6 +160,7 @@ test('error', () => {
:
Error
diff --git a/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_label.tsx b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_label.tsx
index 529053ffd042..32f14b3eba34 100644
--- a/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_label.tsx
+++ b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_label.tsx
@@ -59,7 +59,11 @@ export default function FilterLabel({ filter, valueLabel, filterLabelStatus }: F
);
const getValue = (text?: string) => {
- return {text};
+ return (
+
+ {text}
+
+ );
};
if (filter.meta.alias !== null) {
diff --git a/src/plugins/data/public/ui/query_editor/editors/default_editor/index.tsx b/src/plugins/data/public/ui/query_editor/editors/default_editor/index.tsx
index 1eaf373f2c8e..f642548bafc3 100644
--- a/src/plugins/data/public/ui/query_editor/editors/default_editor/index.tsx
+++ b/src/plugins/data/public/ui/query_editor/editors/default_editor/index.tsx
@@ -32,7 +32,7 @@ export const DefaultInput: React.FC = ({
provideCompletionItems,
}) => {
return (
-
+
{
)}
size="s"
items={languageOptionsMenu}
+ data-test-subj="queryEditorLanguageOptions"
/>
);
diff --git a/src/plugins/data/public/ui/query_editor/query_editor_top_row.test.tsx b/src/plugins/data/public/ui/query_editor/query_editor_top_row.test.tsx
index 62fe653bfd45..284cbe8d4ff0 100644
--- a/src/plugins/data/public/ui/query_editor/query_editor_top_row.test.tsx
+++ b/src/plugins/data/public/ui/query_editor/query_editor_top_row.test.tsx
@@ -3,15 +3,16 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import { Query, UI_SETTINGS } from '../../../common';
-import { coreMock } from '../../../../../core/public/mocks';
-import { dataPluginMock } from '../../mocks';
-import React from 'react';
import { I18nProvider } from '@osd/i18n/react';
-import { createEditor, DQLBody, QueryEditorTopRow, SingleLineInput } from '../';
-import { OpenSearchDashboardsContextProvider } from 'src/plugins/opensearch_dashboards_react/public';
import { cleanup, render, waitFor } from '@testing-library/react';
-import { LanguageConfig } from '../../query';
+import React from 'react';
+import { OpenSearchDashboardsContextProvider } from 'src/plugins/opensearch_dashboards_react/public';
+import { createEditor, DQLBody, QueryEditorTopRow, SingleLineInput } from '../';
+import { coreMock } from '../../../../../core/public/mocks';
+import { Query, UI_SETTINGS } from '../../../common';
+import { dataPluginMock } from '../../mocks';
+import { DatasetTypeConfig, LanguageConfig } from '../../query';
+import { datasetServiceMock } from '../../query/query_string/dataset_service/dataset_service.mock';
import { getQueryService } from '../../services';
const startMock = coreMock.createStart();
@@ -66,6 +67,7 @@ const createMockStorage = () => ({
});
const dataPlugin = dataPluginMock.createStartContract(true);
+const datasetService = datasetServiceMock.createStartContract();
function wrapQueryEditorTopRowInContext(testProps: any) {
const defaultOptions = {
@@ -111,6 +113,7 @@ describe('QueryEditorTopRow', () => {
beforeEach(() => {
jest.clearAllMocks();
(getQueryService as jest.Mock).mockReturnValue(dataPlugin.query);
+ dataPlugin.query.queryString.getDatasetService = jest.fn().mockReturnValue(datasetService);
});
afterEach(() => {
@@ -155,4 +158,49 @@ describe('QueryEditorTopRow', () => {
await waitFor(() => expect(container.querySelector(QUERY_EDITOR)).toBeTruthy());
expect(container.querySelector(DATE_PICKER)).toBeFalsy();
});
+
+ it('Should not render date picker if dataset type does not support time field', async () => {
+ const query: Query = {
+ query: 'test query',
+ dataset: datasetService.getDefault(),
+ language: 'test-language',
+ };
+ dataPlugin.query.queryString.getQuery = jest.fn().mockReturnValue(query);
+ datasetService.getType.mockReturnValue({
+ meta: { supportsTimeFilter: false },
+ } as DatasetTypeConfig);
+
+ const { container } = render(
+ wrapQueryEditorTopRowInContext({
+ query,
+ showQueryEditor: false,
+ showDatePicker: true,
+ })
+ );
+ await waitFor(() => expect(container.querySelector(QUERY_EDITOR)).toBeTruthy());
+ expect(container.querySelector(DATE_PICKER)).toBeFalsy();
+ });
+
+ it('Should render date picker if dataset overrides hideDatePicker to false', async () => {
+ const query: Query = {
+ query: 'test query',
+ dataset: datasetService.getDefault(),
+ language: 'test-language',
+ };
+ dataPlugin.query.queryString.getQuery = jest.fn().mockReturnValue(query);
+ datasetService.getType.mockReturnValue(({
+ meta: { supportsTimeFilter: true },
+ languageOverrides: { 'test-language': { hideDatePicker: false } },
+ } as unknown) as DatasetTypeConfig);
+
+ const { container } = render(
+ wrapQueryEditorTopRowInContext({
+ query,
+ showQueryEditor: false,
+ showDatePicker: true,
+ })
+ );
+ await waitFor(() => expect(container.querySelector(QUERY_EDITOR)).toBeTruthy());
+ expect(container.querySelector(DATE_PICKER)).toBeTruthy();
+ });
});
diff --git a/src/plugins/data/public/ui/query_editor/query_editor_top_row.tsx b/src/plugins/data/public/ui/query_editor/query_editor_top_row.tsx
index ea15fbfeeaa1..ad22750207ed 100644
--- a/src/plugins/data/public/ui/query_editor/query_editor_top_row.tsx
+++ b/src/plugins/data/public/ui/query_editor/query_editor_top_row.tsx
@@ -224,18 +224,53 @@ export default function QueryEditorTopRow(props: QueryEditorTopRowProps) {
);
}
+ /**
+ * Determines if the date picker should be rendered based on UI settings, dataset configuration, and language settings.
+ *
+ * @returns {boolean} Whether the date picker should be rendered
+ *
+ * UI Settings permutations (isDatePickerEnabled):
+ * - showDatePicker=true || showAutoRefreshOnly=true => true
+ * - showDatePicker=false && showAutoRefreshOnly=false => false
+ * - both undefined => true (default)
+ * If isDatePickerEnabled is false, returns false immediately
+ *
+ * Dataset Type permutations (datasetType?.meta?.supportsTimeFilter):
+ * - supportsTimeFilter=false => false
+ *
+ * Language permutations (when dataset.meta.supportsTimeFilter is undefined or true):
+ * - queryLanguage=undefined => true (shows date picker)
+ * - queryLanguage exists:
+ * - languageOverrides[queryLanguage].hideDatePicker=true => false
+ * - languageOverrides[queryLanguage].hideDatePicker=false => true
+ * - hideDatePicker=true => false
+ * - hideDatePicker=false => true
+ * - hideDatePicker=undefined => true
+ */
function shouldRenderDatePicker(): boolean {
- return (
- Boolean((props.showDatePicker || props.showAutoRefreshOnly) ?? true) &&
- !(
- queryLanguage &&
- data.query.queryString.getLanguageService().getLanguage(queryLanguage)?.hideDatePicker
- ) &&
- (props.query?.dataset
- ? data.query.queryString.getDatasetService().getType(props.query.dataset.type)?.meta
- ?.supportsTimeFilter !== false
- : true)
+ const { queryString } = data.query;
+ const datasetService = queryString.getDatasetService();
+ const languageService = queryString.getLanguageService();
+ const isDatePickerEnabled = Boolean(
+ (props.showDatePicker || props.showAutoRefreshOnly) ?? true
);
+ if (!isDatePickerEnabled) return false;
+
+ // Get dataset type configuration
+ const datasetType = props.query?.dataset
+ ? datasetService.getType(props.query?.dataset.type)
+ : undefined;
+ // Check if dataset type explicitly configures the `supportsTimeFilter` option
+ if (datasetType?.meta?.supportsTimeFilter === false) return false;
+
+ if (
+ queryLanguage &&
+ datasetType?.languageOverrides?.[queryLanguage]?.hideDatePicker !== undefined
+ ) {
+ return Boolean(!datasetType.languageOverrides[queryLanguage].hideDatePicker);
+ }
+
+ return Boolean(!(queryLanguage && languageService.getLanguage(queryLanguage)?.hideDatePicker));
}
function shouldRenderQueryEditor(): boolean {
diff --git a/src/plugins/data_explorer/public/components/sidebar/index.tsx b/src/plugins/data_explorer/public/components/sidebar/index.tsx
index 6a4ef0585e33..2aa747abca86 100644
--- a/src/plugins/data_explorer/public/components/sidebar/index.tsx
+++ b/src/plugins/data_explorer/public/components/sidebar/index.tsx
@@ -125,6 +125,7 @@ export const Sidebar: FC = ({ children, datasetSelectorRef }) => {
className="eui-yScroll deSidebar_panel"
hasBorder={true}
borderRadius="l"
+ data-test-subj="sidebarPanel"
>
{isEnhancementEnabled && }
diff --git a/src/plugins/dev_tools/public/global_search/search_devtool_command.test.tsx b/src/plugins/dev_tools/public/global_search/search_devtool_command.test.tsx
index 883584e49e08..9a5ce520e8f1 100644
--- a/src/plugins/dev_tools/public/global_search/search_devtool_command.test.tsx
+++ b/src/plugins/dev_tools/public/global_search/search_devtool_command.test.tsx
@@ -32,6 +32,17 @@ describe('DevtoolSearchCommand', () => {
expect(searchResult).toHaveLength(0);
});
+ it('searchForDevTools matches category', async () => {
+ const searchResult = await searchForDevTools('dev', {
+ devTools: devToolsFn,
+ title: 'Dev tools',
+ uiActionsApi: uiActionsApiFn,
+ });
+
+ // match all sub apps
+ expect(searchResult).toHaveLength(2);
+ });
+
it('searchForDevTools with match tool', async () => {
const searchResult = await searchForDevTools('console', {
devTools: devToolsFn,
@@ -56,7 +67,11 @@ describe('DevtoolSearchCommand', () => {
/>
- Dev tools
+
+ Dev tools
+
,
},
diff --git a/src/plugins/dev_tools/public/global_search/search_devtool_command.tsx b/src/plugins/dev_tools/public/global_search/search_devtool_command.tsx
index 7bb8a9cb7238..03efbb751807 100644
--- a/src/plugins/dev_tools/public/global_search/search_devtool_command.tsx
+++ b/src/plugins/dev_tools/public/global_search/search_devtool_command.tsx
@@ -33,12 +33,18 @@ export const searchForDevTools = async (
- {props.title}
+
+ {props.title}
+
);
- return tools
- .filter((tool) => tool.title.toLowerCase().includes(query.toLowerCase()))
+ const titleMatched = props.title.toLowerCase().includes(query.toLowerCase());
+ const matchedTools = titleMatched
+ ? tools
+ : tools.filter((tool) => tool.title.toLowerCase().includes(query.toLowerCase()));
+
+ return matchedTools
.map((tool) => ({
breadcrumbs: [
{
diff --git a/src/plugins/discover/public/application/components/default_discover_table/default_discover_table.tsx b/src/plugins/discover/public/application/components/default_discover_table/default_discover_table.tsx
index 1e92858157bc..5dcd040d8e76 100644
--- a/src/plugins/discover/public/application/components/default_discover_table/default_discover_table.tsx
+++ b/src/plugins/discover/public/application/components/default_discover_table/default_discover_table.tsx
@@ -186,6 +186,17 @@ const DefaultDiscoverTableUI = ({
// Allow auto column-sizing using the initially rendered rows and then convert to fixed
const tableLayoutRequestFrameRef = useRef(0);
+ /* In asynchronous data loading, column metadata may arrive before the corresponding data, resulting in
+ layout being calculated for the new column definitions using the old data. To mitigate this issue, we
+ additionally trigger a recalculation when a change is observed in the index that the data attributes
+ itself to. This ensures a re-layout is performed when new data is loaded or the column definitions
+ change, effectively addressing the symptoms of the race condition.
+ */
+ const indexOfRenderedData = rows?.[0]?._index;
+ const timeFromFirstRow =
+ typeof indexPattern?.timeFieldName === 'string' &&
+ rows?.[0]?._source?.[indexPattern.timeFieldName];
+
useEffect(() => {
if (tableElement) {
// Load the first batch of rows and adjust the columns to the contents
@@ -214,7 +225,7 @@ const DefaultDiscoverTableUI = ({
}
return () => cancelAnimationFrame(tableLayoutRequestFrameRef.current);
- }, [columns, tableElement]);
+ }, [columns, tableElement, indexOfRenderedData, timeFromFirstRow]);
return (
indexPattern && (
diff --git a/src/plugins/discover/public/application/view_components/utils/use_search.test.tsx b/src/plugins/discover/public/application/view_components/utils/use_search.test.tsx
index b76651899b61..f5021b90c1e7 100644
--- a/src/plugins/discover/public/application/view_components/utils/use_search.test.tsx
+++ b/src/plugins/discover/public/application/view_components/utils/use_search.test.tsx
@@ -18,12 +18,37 @@ jest.mock('./use_index_pattern', () => ({
useIndexPattern: jest.fn(),
}));
+const mockQuery = {
+ query: 'test query',
+ language: 'test language',
+};
+
+const mockDefaultQuery = {
+ query: 'default query',
+ language: 'default language',
+};
+
const mockSavedSearch = {
id: 'test-saved-search',
title: 'Test Saved Search',
searchSource: {
setField: jest.fn(),
- getField: jest.fn(),
+ getField: jest.fn().mockReturnValue(mockQuery),
+ fetch: jest.fn(),
+ getSearchRequestBody: jest.fn().mockResolvedValue({}),
+ getOwnField: jest.fn(),
+ getDataFrame: jest.fn(() => ({ name: 'test-pattern' })),
+ },
+ getFullPath: jest.fn(),
+ getOpenSearchType: jest.fn(),
+};
+
+const mockSavedSearchEmptyQuery = {
+ id: 'test-saved-search',
+ title: 'Test Saved Search',
+ searchSource: {
+ setField: jest.fn(),
+ getField: jest.fn().mockReturnValue(undefined),
fetch: jest.fn(),
getSearchRequestBody: jest.fn().mockResolvedValue({}),
getOwnField: jest.fn(),
@@ -215,4 +240,36 @@ describe('useSearch', () => {
expect.objectContaining({ status: ResultStatus.LOADING, rows: [] })
);
});
+
+ it('should load saved search', async () => {
+ const services = createMockServices();
+ services.data.query.queryString.setQuery = jest.fn();
+
+ const { waitForNextUpdate } = renderHook(() => useSearch(services), {
+ wrapper,
+ });
+
+ await act(async () => {
+ await waitForNextUpdate();
+ });
+
+ expect(services.data.query.queryString.setQuery).toBeCalledWith(mockQuery);
+ });
+
+ it('if no saved search, use get query', async () => {
+ const services = createMockServices();
+ services.getSavedSearchById = jest.fn().mockResolvedValue(mockSavedSearchEmptyQuery);
+ services.data.query.queryString.getQuery = jest.fn().mockReturnValue(mockDefaultQuery);
+ services.data.query.queryString.setQuery = jest.fn();
+
+ const { waitForNextUpdate } = renderHook(() => useSearch(services), {
+ wrapper,
+ });
+
+ await act(async () => {
+ await waitForNextUpdate();
+ });
+
+ expect(services.data.query.queryString.setQuery).toBeCalledWith(mockDefaultQuery);
+ });
});
diff --git a/src/plugins/discover/public/application/view_components/utils/use_search.ts b/src/plugins/discover/public/application/view_components/utils/use_search.ts
index 158a9cd46074..7923f0e717c2 100644
--- a/src/plugins/discover/public/application/view_components/utils/use_search.ts
+++ b/src/plugins/discover/public/application/view_components/utils/use_search.ts
@@ -392,8 +392,7 @@ export const useSearch = (services: DiscoverViewServices) => {
const savedSearchInstance = await getSavedSearchById(savedSearchId);
const query =
- savedSearchInstance.searchSource.getField('query') ||
- data.query.queryString.getDefaultQuery();
+ savedSearchInstance.searchSource.getField('query') || data.query.queryString.getQuery();
const isEnhancementsEnabled = await uiSettings.get('query:enhancements:enabled');
if (isEnhancementsEnabled && query.dataset) {
@@ -432,7 +431,7 @@ export const useSearch = (services: DiscoverViewServices) => {
}
filterManager.setAppFilters(actualFilters);
- data.query.queryString.setQuery(savedQuery ? data.query.queryString.getQuery() : query);
+ data.query.queryString.setQuery(query);
setSavedSearch(savedSearchInstance);
if (savedSearchInstance?.id) {
diff --git a/src/plugins/home/public/application/opensearch_dashboards_services.ts b/src/plugins/home/public/application/opensearch_dashboards_services.ts
index 1107e46ecf2e..eb4b085d86ae 100644
--- a/src/plugins/home/public/application/opensearch_dashboards_services.ts
+++ b/src/plugins/home/public/application/opensearch_dashboards_services.ts
@@ -37,6 +37,7 @@ import {
SavedObjectsClientContract,
IUiSettingsClient,
ApplicationStart,
+ WorkspacesSetup,
} from 'opensearch-dashboards/public';
import { UiStatsMetricType } from '@osd/analytics';
import { TelemetryPluginStart } from '../../../telemetry/public';
@@ -77,6 +78,7 @@ export interface HomeOpenSearchDashboardsServices {
};
dataSource?: DataSourcePluginStart;
sectionTypes: SectionTypeService;
+ workspaces?: WorkspacesSetup;
}
let services: HomeOpenSearchDashboardsServices | null = null;
diff --git a/src/plugins/home/public/application/sample_data_client.js b/src/plugins/home/public/application/sample_data_client.js
index 045736c428f6..b2adaf44cf81 100644
--- a/src/plugins/home/public/application/sample_data_client.js
+++ b/src/plugins/home/public/application/sample_data_client.js
@@ -41,11 +41,26 @@ export async function listSampleDataSets(dataSourceId) {
return await getServices().http.get(sampleDataUrl, { query });
}
+const canUpdateUISetting = () => {
+ const {
+ application: { capabilities },
+ workspaces,
+ } = getServices();
+ if (
+ capabilities.workspaces &&
+ capabilities.workspaces.enabled &&
+ capabilities.workspaces.permissionEnabled
+ ) {
+ return !!workspaces?.currentWorkspace$.getValue()?.owner;
+ }
+ return true;
+};
+
export async function installSampleDataSet(id, sampleDataDefaultIndex, dataSourceId) {
const query = buildQuery(dataSourceId);
await getServices().http.post(`${sampleDataUrl}/${id}`, { query });
- if (getServices().uiSettings.isDefault('defaultIndex')) {
+ if (canUpdateUISetting() && getServices().uiSettings.isDefault('defaultIndex')) {
getServices().uiSettings.set('defaultIndex', sampleDataDefaultIndex);
}
@@ -59,6 +74,7 @@ export async function uninstallSampleDataSet(id, sampleDataDefaultIndex, dataSou
const uiSettings = getServices().uiSettings;
if (
+ canUpdateUISetting() &&
!uiSettings.isDefault('defaultIndex') &&
uiSettings.get('defaultIndex') === sampleDataDefaultIndex
) {
diff --git a/src/plugins/home/public/application/sample_data_client.test.js b/src/plugins/home/public/application/sample_data_client.test.js
new file mode 100644
index 000000000000..35f86efef729
--- /dev/null
+++ b/src/plugins/home/public/application/sample_data_client.test.js
@@ -0,0 +1,167 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { BehaviorSubject } from 'rxjs';
+import { setServices } from '../application/opensearch_dashboards_services';
+import { installSampleDataSet, uninstallSampleDataSet } from './sample_data_client';
+
+const mockHttp = {
+ post: jest.fn(),
+ delete: jest.fn(),
+};
+
+const mockUiSettings = {
+ isDefault: jest.fn(),
+ set: jest.fn(),
+ get: jest.fn(),
+};
+
+const mockApplication = {
+ capabilities: {
+ workspaces: {
+ enabled: false,
+ permissionEnabled: false,
+ },
+ },
+};
+
+const mockIndexPatternService = {
+ clearCache: jest.fn(),
+};
+
+const mockWorkspace = {
+ currentWorkspace$: new BehaviorSubject(),
+};
+
+const mockServices = {
+ workspaces: mockWorkspace,
+ http: mockHttp,
+ uiSettings: mockUiSettings,
+ application: mockApplication,
+ indexPatternService: mockIndexPatternService,
+};
+
+setServices(mockServices);
+
+describe('installSampleDataSet', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ mockUiSettings.isDefault.mockReturnValue(true);
+ setServices(mockServices);
+ });
+
+ it('should install the sample data set and set the default index', async () => {
+ const id = 'sample-data-id';
+ const sampleDataDefaultIndex = 'sample-data-index';
+ const dataSourceId = 'data-source-id';
+
+ await installSampleDataSet(id, sampleDataDefaultIndex, dataSourceId);
+
+ expect(mockHttp.post).toHaveBeenCalledWith(`/api/sample_data/${id}`, {
+ query: expect.anything(),
+ });
+ expect(mockUiSettings.set).toHaveBeenCalledWith('defaultIndex', sampleDataDefaultIndex);
+ expect(mockIndexPatternService.clearCache).toHaveBeenCalled();
+ });
+
+ it('should install the sample data set and not set the default index when workspace is enabled', async () => {
+ const id = 'sample-data-id';
+ const sampleDataDefaultIndex = 'sample-data-index';
+ const dataSourceId = 'data-source-id';
+
+ setServices({
+ ...mockServices,
+ workspaces: {
+ currentWorkspace$: new BehaviorSubject(),
+ },
+ application: {
+ capabilities: {
+ workspaces: {
+ enabled: true,
+ permissionEnabled: true,
+ },
+ },
+ },
+ });
+
+ await installSampleDataSet(id, sampleDataDefaultIndex, dataSourceId);
+
+ expect(mockHttp.post).toHaveBeenCalledWith(`/api/sample_data/${id}`, {
+ query: expect.anything(),
+ });
+ expect(mockUiSettings.set).not.toHaveBeenCalled();
+ expect(mockIndexPatternService.clearCache).toHaveBeenCalled();
+ });
+});
+
+describe('uninstallSampleDataSet', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ mockUiSettings.isDefault.mockReturnValue(false);
+ setServices(mockServices);
+ });
+
+ it('should uninstall the sample data set and clear the default index', async () => {
+ const id = 'sample-data-id';
+ const sampleDataDefaultIndex = 'sample-data-index';
+ const dataSourceId = 'data-source-id';
+
+ mockUiSettings.get.mockReturnValue(sampleDataDefaultIndex);
+
+ await uninstallSampleDataSet(id, sampleDataDefaultIndex, dataSourceId);
+
+ expect(mockHttp.delete).toHaveBeenCalledWith(`/api/sample_data/${id}`, {
+ query: expect.anything(),
+ });
+ expect(mockUiSettings.set).toHaveBeenCalledWith('defaultIndex', null);
+ expect(mockIndexPatternService.clearCache).toHaveBeenCalled();
+ });
+
+ it('should uninstall the sample data set and not clear the default index when workspace is enabled', async () => {
+ const id = 'sample-data-id';
+ const sampleDataDefaultIndex = 'sample-data-index';
+ const dataSourceId = 'data-source-id';
+
+ setServices({
+ ...mockServices,
+ workspaces: {
+ currentWorkspace$: new BehaviorSubject(),
+ },
+ application: {
+ capabilities: {
+ workspaces: {
+ enabled: true,
+ permissionEnabled: true,
+ },
+ },
+ },
+ });
+
+ await uninstallSampleDataSet(id, sampleDataDefaultIndex, dataSourceId);
+
+ expect(mockHttp.delete).toHaveBeenCalledWith(`/api/sample_data/${id}`, {
+ query: expect.anything(),
+ });
+ expect(mockUiSettings.set).not.toHaveBeenCalled();
+ expect(mockIndexPatternService.clearCache).toHaveBeenCalled();
+ });
+
+ it('should uninstall the sample data set and not clear the default index when it is not the sample data index', async () => {
+ const id = 'sample-data-id';
+ const sampleDataDefaultIndex = 'sample-data-index';
+ const dataSourceId = 'data-source-id';
+
+ mockUiSettings.isDefault.mockReturnValue(false);
+ mockUiSettings.get.mockReturnValue('other-index');
+
+ await uninstallSampleDataSet(id, sampleDataDefaultIndex, dataSourceId);
+
+ expect(mockHttp.delete).toHaveBeenCalledWith(`/api/sample_data/${id}`, {
+ query: expect.anything(),
+ });
+ expect(mockUiSettings.set).not.toHaveBeenCalled();
+ expect(mockIndexPatternService.clearCache).toHaveBeenCalled();
+ });
+});
diff --git a/src/plugins/home/public/plugin.ts b/src/plugins/home/public/plugin.ts
index 435c7d4d3b9f..6d9771c724ef 100644
--- a/src/plugins/home/public/plugin.ts
+++ b/src/plugins/home/public/plugin.ts
@@ -156,6 +156,7 @@ export class HomePublicPlugin
injectedMetadata: coreStart.injectedMetadata,
dataSource,
sectionTypes: this.sectionTypeService,
+ workspaces: core.workspaces,
...homeOpenSearchDashboardsServices,
});
};
diff --git a/src/plugins/home/server/services/sample_data/routes/uninstall.ts b/src/plugins/home/server/services/sample_data/routes/uninstall.ts
index 3e4636c32486..da8dea3c2fe3 100644
--- a/src/plugins/home/server/services/sample_data/routes/uninstall.ts
+++ b/src/plugins/home/server/services/sample_data/routes/uninstall.ts
@@ -62,27 +62,10 @@ export function createUninstallRoute(
return response.notFound();
}
- const caller = dataSourceId
- ? context.dataSource.opensearch.legacy.getClient(dataSourceId).callAPI
- : context.core.opensearch.legacy.client.callAsCurrentUser;
-
- for (let i = 0; i < sampleDataset.dataIndices.length; i++) {
- const dataIndexConfig = sampleDataset.dataIndices[i];
- const index =
- dataIndexConfig.indexName ?? createIndexName(sampleDataset.id, dataIndexConfig.id);
-
- try {
- await caller('indices.delete', { index });
- } catch (err) {
- return response.customError({
- statusCode: err.status,
- body: {
- message: `Unable to delete sample data index "${index}", error: ${err.message}`,
- },
- });
- }
- }
-
+ /**
+ * Delete saved objects before removing the data index to avoid partial deletion
+ * of sample data when a read-only workspace user attempts to remove sample data.
+ */
const savedObjectsList = getFinalSavedObjects({
dataset: sampleDataset,
workspaceId,
@@ -99,7 +82,7 @@ export function createUninstallRoute(
// ignore 404s since users could have deleted some of the saved objects via the UI
if (_.get(err, 'output.statusCode') !== 404) {
return response.customError({
- statusCode: err.status,
+ statusCode: err.status || _.get(err, 'output.statusCode'),
body: {
message: `Unable to delete sample dataset saved objects, error: ${err.message}`,
},
@@ -107,6 +90,27 @@ export function createUninstallRoute(
}
}
+ const caller = dataSourceId
+ ? context.dataSource.opensearch.legacy.getClient(dataSourceId).callAPI
+ : context.core.opensearch.legacy.client.callAsCurrentUser;
+
+ for (let i = 0; i < sampleDataset.dataIndices.length; i++) {
+ const dataIndexConfig = sampleDataset.dataIndices[i];
+ const index =
+ dataIndexConfig.indexName ?? createIndexName(sampleDataset.id, dataIndexConfig.id);
+
+ try {
+ await caller('indices.delete', { index });
+ } catch (err) {
+ return response.customError({
+ statusCode: err.status,
+ body: {
+ message: `Unable to delete sample data index "${index}", error: ${err.message}`,
+ },
+ });
+ }
+ }
+
// track the usage operation in a non-blocking way
usageTracker.addUninstall(request.params.id);
diff --git a/src/plugins/maps_legacy/server/ui_settings.ts b/src/plugins/maps_legacy/server/ui_settings.ts
index 3209723da939..9b708749dc03 100644
--- a/src/plugins/maps_legacy/server/ui_settings.ts
+++ b/src/plugins/maps_legacy/server/ui_settings.ts
@@ -95,7 +95,7 @@ export function getUiSettings(): Record> {
'maps_legacy.advancedSettings.visualization.tileMap.wmsDefaults.propertiesLinkText',
values: {
propertiesLink:
- '' +
+ '' +
i18n.translate(
'maps_legacy.advancedSettings.visualization.tileMap.wmsDefaults.propertiesLinkText',
{
diff --git a/src/plugins/query_enhancements/common/types.ts b/src/plugins/query_enhancements/common/types.ts
index 1bb977527d4a..2f73ca52d496 100644
--- a/src/plugins/query_enhancements/common/types.ts
+++ b/src/plugins/query_enhancements/common/types.ts
@@ -4,7 +4,7 @@
*/
import { CoreSetup } from 'opensearch-dashboards/public';
-import { PollQueryResultsParams } from '../../data/common';
+import { PollQueryResultsParams, TimeRange } from '../../data/common';
export interface QueryAggConfig {
[key: string]: {
@@ -26,7 +26,10 @@ export interface EnhancedFetchContext {
http: CoreSetup['http'];
path: string;
signal?: AbortSignal;
- body?: { pollQueryResultsParams: PollQueryResultsParams };
+ body?: {
+ pollQueryResultsParams?: PollQueryResultsParams;
+ timeRange?: TimeRange;
+ };
}
export interface QueryStatusOptions {
diff --git a/src/plugins/query_enhancements/common/utils.test.ts b/src/plugins/query_enhancements/common/utils.test.ts
index 39bbdc258bea..787cebb0c082 100644
--- a/src/plugins/query_enhancements/common/utils.test.ts
+++ b/src/plugins/query_enhancements/common/utils.test.ts
@@ -3,7 +3,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import { handleFacetError } from './utils';
+import { throwFacetError } from './utils';
describe('handleFacetError', () => {
const error = new Error('mock-error');
@@ -16,9 +16,9 @@ describe('handleFacetError', () => {
data: error,
};
- expect(() => handleFacetError(response)).toThrowError();
+ expect(() => throwFacetError(response)).toThrowError();
try {
- handleFacetError(response);
+ throwFacetError(response);
} catch (err: any) {
expect(err.message).toBe('test error message');
expect(err.name).toBe('400');
diff --git a/src/plugins/query_enhancements/common/utils.ts b/src/plugins/query_enhancements/common/utils.ts
index 9b2bb9e3aacf..634a56b84603 100644
--- a/src/plugins/query_enhancements/common/utils.ts
+++ b/src/plugins/query_enhancements/common/utils.ts
@@ -42,7 +42,7 @@ export const removeKeyword = (queryString: string | undefined) => {
return queryString?.replace(new RegExp('.keyword'), '') ?? '';
};
-export const handleFacetError = (response: any) => {
+export const throwFacetError = (response: any) => {
const error = new Error(response.data.body?.message ?? response.data.body ?? response.data);
error.name = response.data.status ?? response.status ?? response.data.statusCode;
(error as any).status = error.name;
@@ -55,6 +55,7 @@ export const fetch = (context: EnhancedFetchContext, query: Query, aggConfig?: Q
query: { ...query, format: 'jdbc' },
aggConfig,
pollQueryResultsParams: context.body?.pollQueryResultsParams,
+ timeRange: context.body?.timeRange,
});
return from(
http.fetch({
diff --git a/src/plugins/query_enhancements/public/search/ppl_search_interceptor.ts b/src/plugins/query_enhancements/public/search/ppl_search_interceptor.ts
index 57152dbe98ea..ecfe32ff8a75 100644
--- a/src/plugins/query_enhancements/public/search/ppl_search_interceptor.ts
+++ b/src/plugins/query_enhancements/public/search/ppl_search_interceptor.ts
@@ -50,6 +50,7 @@ export class PPLSearchInterceptor extends SearchInterceptor {
signal,
body: {
pollQueryResultsParams: request.params?.pollQueryResultsParams,
+ timeRange: request.params?.body?.timeRange,
},
};
@@ -68,15 +69,33 @@ export class PPLSearchInterceptor extends SearchInterceptor {
.getDatasetService()
.getType(datasetType);
strategy = datasetTypeConfig?.getSearchOptions?.().strategy ?? strategy;
+
+ if (
+ dataset?.timeFieldName &&
+ datasetTypeConfig?.languageOverrides?.PPL?.hideDatePicker === false
+ ) {
+ request.params = {
+ ...request.params,
+ body: {
+ ...request.params.body,
+ timeRange: this.queryService.timefilter.timefilter.getTime(),
+ },
+ };
+ }
}
return this.runSearch(request, options.abortSignal, strategy);
}
private buildQuery() {
- const query: Query = this.queryService.queryString.getQuery();
+ const { queryString } = this.queryService;
+ const query: Query = queryString.getQuery();
const dataset = query.dataset;
if (!dataset || !dataset.timeFieldName) return query;
+ const datasetService = queryString.getDatasetService();
+ if (datasetService.getType(dataset.type)?.languageOverrides?.PPL?.hideDatePicker === false)
+ return query;
+
const [baseQuery, ...afterPipeParts] = query.query.split('|');
const afterPipe = afterPipeParts.length > 0 ? ` | ${afterPipeParts.join('|').trim()}` : '';
const timeFilter = this.getTimeFilter(dataset.timeFieldName);
diff --git a/src/plugins/query_enhancements/public/search/sql_search_interceptor.ts b/src/plugins/query_enhancements/public/search/sql_search_interceptor.ts
index 9fe17fc79322..9f93dd067cb3 100644
--- a/src/plugins/query_enhancements/public/search/sql_search_interceptor.ts
+++ b/src/plugins/query_enhancements/public/search/sql_search_interceptor.ts
@@ -42,6 +42,7 @@ export class SQLSearchInterceptor extends SearchInterceptor {
signal,
body: {
pollQueryResultsParams: request.params?.pollQueryResultsParams,
+ timeRange: request.params?.body?.timeRange,
},
};
@@ -62,6 +63,16 @@ export class SQLSearchInterceptor extends SearchInterceptor {
.getDatasetService()
.getType(datasetType);
strategy = datasetTypeConfig?.getSearchOptions?.().strategy ?? strategy;
+
+ if (datasetTypeConfig?.languageOverrides?.SQL?.hideDatePicker === false) {
+ request.params = {
+ ...request.params,
+ body: {
+ ...request.params.body,
+ timeRange: this.queryService.timefilter.timefilter.getTime(),
+ },
+ };
+ }
}
return this.runSearch(request, options.abortSignal, strategy);
diff --git a/src/plugins/query_enhancements/server/routes/index.test.ts b/src/plugins/query_enhancements/server/routes/index.test.ts
new file mode 100644
index 000000000000..9c7c7a56de2e
--- /dev/null
+++ b/src/plugins/query_enhancements/server/routes/index.test.ts
@@ -0,0 +1,21 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { coerceStatusCode } from '.';
+
+describe('coerceStatusCode', () => {
+ it('should return 503 when input is 500', () => {
+ expect(coerceStatusCode(500)).toBe(503);
+ });
+
+ it('should return the input status code when it is not 500', () => {
+ expect(coerceStatusCode(404)).toBe(404);
+ });
+
+ it('should return 503 when input is undefined or null', () => {
+ expect(coerceStatusCode((undefined as unknown) as number)).toBe(503);
+ expect(coerceStatusCode((null as unknown) as number)).toBe(503);
+ });
+});
diff --git a/src/plugins/query_enhancements/server/routes/index.ts b/src/plugins/query_enhancements/server/routes/index.ts
index 79b93a279272..2cda4a9f0cbf 100644
--- a/src/plugins/query_enhancements/server/routes/index.ts
+++ b/src/plugins/query_enhancements/server/routes/index.ts
@@ -16,6 +16,15 @@ import { API } from '../../common';
import { registerQueryAssistRoutes } from './query_assist';
import { registerDataSourceConnectionsRoutes } from './data_source_connection';
+/**
+ * Coerce status code to 503 for 500 errors from dependency services. Only use
+ * this function to handle errors throw by other services, and not from OSD.
+ */
+export const coerceStatusCode = (statusCode: number) => {
+ if (statusCode === 500) return 503;
+ return statusCode || 503;
+};
+
/**
* @experimental
*
@@ -77,6 +86,7 @@ export function defineSearchStrategyRouteProvider(logger: Logger, router: IRoute
sessionId: schema.maybe(schema.string()),
})
),
+ timeRange: schema.maybe(schema.object({}, { unknowns: 'allow' })),
}),
},
},
@@ -92,7 +102,7 @@ export function defineSearchStrategyRouteProvider(logger: Logger, router: IRoute
error = err;
}
return res.custom({
- statusCode: error.status || err.status,
+ statusCode: coerceStatusCode(error.status || err.status),
body: err.message,
});
}
diff --git a/src/plugins/query_enhancements/server/search/ppl_async_search_strategy.ts b/src/plugins/query_enhancements/server/search/ppl_async_search_strategy.ts
index 309c5fd522b6..2af66fb427c2 100644
--- a/src/plugins/query_enhancements/server/search/ppl_async_search_strategy.ts
+++ b/src/plugins/query_enhancements/server/search/ppl_async_search_strategy.ts
@@ -13,7 +13,7 @@ import {
Query,
} from '../../../data/common';
import { ISearchStrategy, SearchUsage } from '../../../data/server';
-import { buildQueryStatusConfig, getFields, handleFacetError, SEARCH_STRATEGY } from '../../common';
+import { buildQueryStatusConfig, getFields, throwFacetError, SEARCH_STRATEGY } from '../../common';
import { Facet } from '../utils';
export const pplAsyncSearchStrategyProvider = (
@@ -45,7 +45,7 @@ export const pplAsyncSearchStrategyProvider = (
request.body = { ...request.body, lang: SEARCH_STRATEGY.PPL };
const rawResponse: any = await pplAsyncFacet.describeQuery(context, request);
- if (!rawResponse.success) handleFacetError(rawResponse);
+ if (!rawResponse.success) throwFacetError(rawResponse);
const statusConfig = buildQueryStatusConfig(rawResponse);
@@ -60,7 +60,7 @@ export const pplAsyncSearchStrategyProvider = (
request.params = { queryId: inProgressQueryId };
const queryStatusResponse = await pplAsyncJobsFacet.describeQuery(context, request);
- if (!queryStatusResponse.success) handleFacetError(queryStatusResponse);
+ if (!queryStatusResponse.success) throwFacetError(queryStatusResponse);
const queryStatus = queryStatusResponse.data?.status;
logger.info(`pplAsyncSearchStrategy: JOB: ${inProgressQueryId} - STATUS: ${queryStatus}`);
diff --git a/src/plugins/query_enhancements/server/search/ppl_search_strategy.test.ts b/src/plugins/query_enhancements/server/search/ppl_search_strategy.test.ts
new file mode 100644
index 000000000000..ae8105180db8
--- /dev/null
+++ b/src/plugins/query_enhancements/server/search/ppl_search_strategy.test.ts
@@ -0,0 +1,372 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {
+ ILegacyClusterClient,
+ Logger,
+ RequestHandlerContext,
+ SharedGlobalConfig,
+} from 'opensearch-dashboards/server';
+import { Observable, of } from 'rxjs';
+import { DATA_FRAME_TYPES, IOpenSearchDashboardsSearchRequest } from '../../../data/common';
+import { SearchUsage } from '../../../data/server';
+import * as utils from '../../common/utils';
+import * as facet from '../utils/facet';
+import { pplSearchStrategyProvider } from './ppl_search_strategy';
+
+jest.mock('../../common/utils', () => ({
+ ...jest.requireActual('../../common/utils'),
+ getFields: jest.fn(),
+}));
+
+describe('pplSearchStrategyProvider', () => {
+ let config$: Observable;
+ let logger: Logger;
+ let client: ILegacyClusterClient;
+ let usage: SearchUsage;
+ const emptyRequestHandlerContext = ({} as unknown) as RequestHandlerContext;
+
+ beforeEach(() => {
+ config$ = of({} as SharedGlobalConfig);
+ logger = ({
+ error: jest.fn(),
+ } as unknown) as Logger;
+ client = {} as ILegacyClusterClient;
+ usage = {
+ trackSuccess: jest.fn(),
+ trackError: jest.fn(),
+ } as SearchUsage;
+ });
+
+ it('should return an object with a search method', () => {
+ const strategy = pplSearchStrategyProvider(config$, logger, client, usage);
+ expect(strategy).toHaveProperty('search');
+ expect(typeof strategy.search).toBe('function');
+ });
+
+ it('should handle successful search response', async () => {
+ const mockResponse = {
+ success: true,
+ data: {
+ schema: [
+ { name: 'field1', type: 'long' },
+ { name: 'field2', type: 'text' },
+ ],
+ datarows: [
+ [1, 'value1'],
+ [2, 'value2'],
+ ],
+ },
+ took: 100,
+ };
+ const mockFacet = ({
+ describeQuery: jest.fn().mockResolvedValue(mockResponse),
+ } as unknown) as facet.Facet;
+ jest.spyOn(facet, 'Facet').mockImplementation(() => mockFacet);
+ (utils.getFields as jest.Mock).mockReturnValue([
+ { name: 'field1', type: 'long' },
+ { name: 'field2', type: 'text' },
+ ]);
+
+ const strategy = pplSearchStrategyProvider(config$, logger, client, usage);
+ const result = await strategy.search(
+ emptyRequestHandlerContext,
+ ({
+ body: { query: { query: 'source = table', dataset: { id: 'test-dataset' } } },
+ } as unknown) as IOpenSearchDashboardsSearchRequest,
+ {}
+ );
+
+ expect(result).toEqual({
+ type: DATA_FRAME_TYPES.DEFAULT,
+ body: {
+ name: 'test-dataset',
+ fields: [
+ { name: 'field1', type: 'long', values: [] },
+ { name: 'field2', type: 'text', values: [] },
+ ],
+ schema: [
+ { name: 'field1', type: 'long', values: [] },
+ { name: 'field2', type: 'text', values: [] },
+ ],
+ size: 2,
+ },
+ took: 100,
+ });
+ expect(usage.trackSuccess).toHaveBeenCalledWith(100);
+ });
+
+ it('should handle failed search response', async () => {
+ const mockResponse = {
+ success: false,
+ data: { cause: 'Query failed' },
+ took: 50,
+ };
+ const mockFacet = ({
+ describeQuery: jest.fn().mockResolvedValue(mockResponse),
+ } as unknown) as facet.Facet;
+ jest.spyOn(facet, 'Facet').mockImplementation(() => mockFacet);
+
+ const strategy = pplSearchStrategyProvider(config$, logger, client, usage);
+ await expect(
+ strategy.search(
+ emptyRequestHandlerContext,
+ ({
+ body: { query: { query: 'source = table' } },
+ } as unknown) as IOpenSearchDashboardsSearchRequest,
+ {}
+ )
+ ).rejects.toThrow();
+ });
+
+ it('should handle exceptions', async () => {
+ const mockError = new Error('Something went wrong');
+ const mockFacet = ({
+ describeQuery: jest.fn().mockRejectedValue(mockError),
+ } as unknown) as facet.Facet;
+ jest.spyOn(facet, 'Facet').mockImplementation(() => mockFacet);
+
+ const strategy = pplSearchStrategyProvider(config$, logger, client, usage);
+ await expect(
+ strategy.search(
+ emptyRequestHandlerContext,
+ ({
+ body: { query: { query: 'source = table' } },
+ } as unknown) as IOpenSearchDashboardsSearchRequest,
+ {}
+ )
+ ).rejects.toThrow(mockError);
+ expect(logger.error).toHaveBeenCalledWith(`pplSearchStrategy: ${mockError.message}`);
+ expect(usage.trackError).toHaveBeenCalled();
+ });
+
+ it('should throw error when describeQuery success is false', async () => {
+ const mockError = new Error('Something went wrong');
+ const mockFacet = ({
+ describeQuery: jest.fn().mockResolvedValue({ success: false, data: mockError }),
+ } as unknown) as facet.Facet;
+ jest.spyOn(facet, 'Facet').mockImplementation(() => mockFacet);
+
+ const strategy = pplSearchStrategyProvider(config$, logger, client, usage);
+ await expect(
+ strategy.search(
+ emptyRequestHandlerContext,
+ ({
+ body: { query: { query: 'source = table' } },
+ } as unknown) as IOpenSearchDashboardsSearchRequest,
+ {}
+ )
+ ).rejects.toThrowError();
+ expect(logger.error).toHaveBeenCalledWith(expect.stringContaining(mockError.message));
+ expect(usage.trackError).toHaveBeenCalled();
+ });
+
+ it('should handle empty search response', async () => {
+ const mockResponse = {
+ success: true,
+ data: {
+ schema: [
+ { name: 'field1', type: 'long' },
+ { name: 'field2', type: 'text' },
+ ],
+ datarows: [],
+ },
+ took: 10,
+ };
+ const mockFacet = ({
+ describeQuery: jest.fn().mockResolvedValue(mockResponse),
+ } as unknown) as facet.Facet;
+ jest.spyOn(facet, 'Facet').mockImplementation(() => mockFacet);
+ (utils.getFields as jest.Mock).mockReturnValue([
+ { name: 'field1', type: 'long' },
+ { name: 'field2', type: 'text' },
+ ]);
+
+ const strategy = pplSearchStrategyProvider(config$, logger, client, usage);
+ const result = await strategy.search(
+ emptyRequestHandlerContext,
+ ({
+ body: { query: { query: 'source = empty_table', dataset: { id: 'empty-dataset' } } },
+ } as unknown) as IOpenSearchDashboardsSearchRequest,
+ {}
+ );
+
+ expect(result).toEqual({
+ type: DATA_FRAME_TYPES.DEFAULT,
+ body: {
+ name: 'empty-dataset',
+ fields: [
+ { name: 'field1', type: 'long', values: [] },
+ { name: 'field2', type: 'text', values: [] },
+ ],
+ schema: [
+ { name: 'field1', type: 'long', values: [] },
+ { name: 'field2', type: 'text', values: [] },
+ ],
+ size: 0,
+ },
+ took: 10,
+ });
+ expect(usage.trackSuccess).toHaveBeenCalledWith(10);
+ });
+
+ it('should handle aggConfig when response succeeds', async () => {
+ const mockResponse = {
+ success: true,
+ data: {
+ schema: [
+ { name: 'field1', type: 'long' },
+ { name: 'field2', type: 'text' },
+ ],
+ datarows: [
+ [1, 'value1'],
+ [2, 'value2'],
+ ],
+ },
+ took: 10,
+ };
+ const mockFacet = ({
+ describeQuery: jest.fn().mockResolvedValue(mockResponse),
+ } as unknown) as facet.Facet;
+ jest.spyOn(facet, 'Facet').mockImplementation(() => mockFacet);
+ (utils.getFields as jest.Mock).mockReturnValue([
+ { name: 'field1', type: 'long' },
+ { name: 'field2', type: 'text' },
+ ]);
+
+ const strategy = pplSearchStrategyProvider(config$, logger, client, usage);
+ const result = await strategy.search(
+ emptyRequestHandlerContext,
+ ({
+ body: {
+ query: { query: 'source = empty_table', dataset: { id: 'empty-dataset' } },
+ aggConfig: {
+ date_histogram: {
+ field: 'timestamp',
+ fixed_interval: '12h',
+ time_zone: 'America/Los_Angeles',
+ min_doc_count: 1,
+ },
+ qs: {
+ '2': 'source = empty_table | stats count() by span(timestamp, 12h)',
+ },
+ },
+ },
+ } as unknown) as IOpenSearchDashboardsSearchRequest,
+ {}
+ );
+
+ expect(result).toEqual({
+ type: DATA_FRAME_TYPES.DEFAULT,
+ body: {
+ name: 'empty-dataset',
+ fields: [
+ { name: 'field1', type: 'long', values: [] },
+ { name: 'field2', type: 'text', values: [] },
+ ],
+ schema: [
+ { name: 'field1', type: 'long', values: [] },
+ { name: 'field2', type: 'text', values: [] },
+ ],
+ aggs: {
+ '2': [
+ { key: 'value1', value: 1 },
+ { key: 'value2', value: 2 },
+ ],
+ },
+ meta: {
+ date_histogram: {
+ field: 'timestamp',
+ fixed_interval: '12h',
+ time_zone: 'America/Los_Angeles',
+ min_doc_count: 1,
+ },
+ qs: { '2': 'source = empty_table | stats count() by span(timestamp, 12h)' },
+ },
+ size: 2,
+ },
+ took: 10,
+ });
+ expect(usage.trackSuccess).toHaveBeenCalledWith(10);
+ });
+
+ it('should handle aggConfig when aggregation fails', async () => {
+ const mockResponse = {
+ success: true,
+ data: {
+ schema: [
+ { name: 'field1', type: 'long' },
+ { name: 'field2', type: 'text' },
+ ],
+ datarows: [
+ [1, 'value1'],
+ [2, 'value2'],
+ ],
+ },
+ took: 10,
+ };
+ const mockError = new Error('Something went wrong');
+ const mockFacet = ({
+ describeQuery: jest
+ .fn()
+ .mockResolvedValueOnce(mockResponse)
+ .mockResolvedValue({ success: false, data: mockError }),
+ } as unknown) as facet.Facet;
+ jest.spyOn(facet, 'Facet').mockImplementation(() => mockFacet);
+ (utils.getFields as jest.Mock).mockReturnValue([
+ { name: 'field1', type: 'long' },
+ { name: 'field2', type: 'text' },
+ ]);
+
+ const strategy = pplSearchStrategyProvider(config$, logger, client, usage);
+ const result = await strategy.search(
+ emptyRequestHandlerContext,
+ ({
+ body: {
+ query: { query: 'source = empty_table', dataset: { id: 'empty-dataset' } },
+ aggConfig: {
+ date_histogram: {
+ field: 'timestamp',
+ fixed_interval: '12h',
+ time_zone: 'America/Los_Angeles',
+ min_doc_count: 1,
+ },
+ qs: {
+ '2': 'source = empty_table | stats count() by span(timestamp, 12h)',
+ },
+ },
+ },
+ } as unknown) as IOpenSearchDashboardsSearchRequest,
+ {}
+ );
+
+ expect(result).toEqual({
+ type: DATA_FRAME_TYPES.DEFAULT,
+ body: {
+ name: 'empty-dataset',
+ fields: [
+ { name: 'field1', type: 'long', values: [] },
+ { name: 'field2', type: 'text', values: [] },
+ ],
+ schema: [
+ { name: 'field1', type: 'long', values: [] },
+ { name: 'field2', type: 'text', values: [] },
+ ],
+ meta: {
+ date_histogram: {
+ field: 'timestamp',
+ fixed_interval: '12h',
+ time_zone: 'America/Los_Angeles',
+ min_doc_count: 1,
+ },
+ qs: { '2': 'source = empty_table | stats count() by span(timestamp, 12h)' },
+ },
+ size: 2,
+ },
+ took: 10,
+ });
+ expect(usage.trackSuccess).toHaveBeenCalledWith(10);
+ });
+});
diff --git a/src/plugins/query_enhancements/server/search/ppl_search_strategy.ts b/src/plugins/query_enhancements/server/search/ppl_search_strategy.ts
index d71ae6810fad..d47d2ca41c4a 100644
--- a/src/plugins/query_enhancements/server/search/ppl_search_strategy.ts
+++ b/src/plugins/query_enhancements/server/search/ppl_search_strategy.ts
@@ -14,7 +14,7 @@ import {
Query,
createDataFrame,
} from '../../../data/common';
-import { getFields, handleFacetError } from '../../common/utils';
+import { getFields, throwFacetError } from '../../common/utils';
import { Facet } from '../utils';
import { QueryAggConfig } from '../../common';
@@ -39,7 +39,7 @@ export const pplSearchStrategyProvider = (
const aggConfig: QueryAggConfig | undefined = request.body.aggConfig;
const rawResponse: any = await pplFacet.describeQuery(context, request);
- if (!rawResponse.success) handleFacetError(rawResponse);
+ if (!rawResponse.success) throwFacetError(rawResponse);
const dataFrame = createDataFrame({
name: query.dataset?.id,
@@ -56,7 +56,7 @@ export const pplSearchStrategyProvider = (
for (const [key, aggQueryString] of Object.entries(aggConfig.qs)) {
request.body.query.query = aggQueryString;
const rawAggs: any = await pplFacet.describeQuery(context, request);
- if (!rawAggs.success) handleFacetError(rawResponse);
+ if (!rawAggs.success) continue;
(dataFrame as IDataFrameWithAggs).aggs = {};
(dataFrame as IDataFrameWithAggs).aggs[key] = rawAggs.data.datarows?.map((hit: any) => {
return {
diff --git a/src/plugins/query_enhancements/server/search/sql_async_search_strategy.ts b/src/plugins/query_enhancements/server/search/sql_async_search_strategy.ts
index bc25f69a70f6..76642b9dbac5 100644
--- a/src/plugins/query_enhancements/server/search/sql_async_search_strategy.ts
+++ b/src/plugins/query_enhancements/server/search/sql_async_search_strategy.ts
@@ -13,7 +13,7 @@ import {
Query,
} from '../../../data/common';
import { ISearchStrategy, SearchUsage } from '../../../data/server';
-import { buildQueryStatusConfig, getFields, handleFacetError, SEARCH_STRATEGY } from '../../common';
+import { buildQueryStatusConfig, getFields, throwFacetError, SEARCH_STRATEGY } from '../../common';
import { Facet } from '../utils';
export const sqlAsyncSearchStrategyProvider = (
@@ -45,7 +45,7 @@ export const sqlAsyncSearchStrategyProvider = (
request.body = { ...request.body, lang: SEARCH_STRATEGY.SQL };
const rawResponse: any = await sqlAsyncFacet.describeQuery(context, request);
- if (!rawResponse.success) handleFacetError(rawResponse);
+ if (!rawResponse.success) throwFacetError(rawResponse);
const statusConfig = buildQueryStatusConfig(rawResponse);
@@ -60,7 +60,7 @@ export const sqlAsyncSearchStrategyProvider = (
request.params = { queryId: inProgressQueryId };
const queryStatusResponse = await sqlAsyncJobsFacet.describeQuery(context, request);
- if (!queryStatusResponse.success) handleFacetError(queryStatusResponse);
+ if (!queryStatusResponse.success) throwFacetError(queryStatusResponse);
const queryStatus = queryStatusResponse.data?.status;
logger.info(`sqlAsyncSearchStrategy: JOB: ${inProgressQueryId} - STATUS: ${queryStatus}`);
diff --git a/src/plugins/query_enhancements/server/search/sql_search_strategy.ts b/src/plugins/query_enhancements/server/search/sql_search_strategy.ts
index 8fa945c8809e..09f2775d0fe2 100644
--- a/src/plugins/query_enhancements/server/search/sql_search_strategy.ts
+++ b/src/plugins/query_enhancements/server/search/sql_search_strategy.ts
@@ -13,7 +13,7 @@ import {
Query,
createDataFrame,
} from '../../../data/common';
-import { getFields, handleFacetError } from '../../common/utils';
+import { getFields, throwFacetError } from '../../common/utils';
import { Facet } from '../utils';
export const sqlSearchStrategyProvider = (
@@ -36,7 +36,7 @@ export const sqlSearchStrategyProvider = (
const query: Query = request.body.query;
const rawResponse: any = await sqlFacet.describeQuery(context, request);
- if (!rawResponse.success) handleFacetError(rawResponse);
+ if (!rawResponse.success) throwFacetError(rawResponse);
const dataFrame = createDataFrame({
name: query.dataset?.id,
diff --git a/src/plugins/vis_augmenter/public/plugin.ts b/src/plugins/vis_augmenter/public/plugin.ts
index bd6e45a3967b..9760bfd75b2d 100644
--- a/src/plugins/vis_augmenter/public/plugin.ts
+++ b/src/plugins/vis_augmenter/public/plugin.ts
@@ -13,7 +13,6 @@ import {
setUiActions,
setEmbeddable,
setQueryService,
- setIndexPatterns,
setVisualizations,
setCore,
} from './services';
@@ -63,7 +62,6 @@ export class VisAugmenterPlugin
setUiActions(uiActions);
setEmbeddable(embeddable);
setQueryService(data.query);
- setIndexPatterns(data.indexPatterns);
setVisualizations(visualizations);
setCore(core);
setFlyoutState(VIEW_EVENTS_FLYOUT_STATE.CLOSED);
diff --git a/src/plugins/vis_augmenter/public/services.ts b/src/plugins/vis_augmenter/public/services.ts
index 44a7ea8b424b..1d7f3e2111db 100644
--- a/src/plugins/vis_augmenter/public/services.ts
+++ b/src/plugins/vis_augmenter/public/services.ts
@@ -8,7 +8,7 @@ import { IUiSettingsClient } from '../../../core/public';
import { SavedObjectLoaderAugmentVis } from './saved_augment_vis';
import { EmbeddableStart } from '../../embeddable/public';
import { UiActionsStart } from '../../ui_actions/public';
-import { DataPublicPluginStart, IndexPatternsContract } from '../../../plugins/data/public';
+import { DataPublicPluginStart } from '../../../plugins/data/public';
import { VisualizationsStart } from '../../visualizations/public';
import { CoreStart } from '../../../core/public';
@@ -26,10 +26,6 @@ export const [getQueryService, setQueryService] = createGetterSetter<
DataPublicPluginStart['query']
>('Query');
-export const [getIndexPatterns, setIndexPatterns] = createGetterSetter(
- 'IndexPatterns'
-);
-
export const [getVisualizations, setVisualizations] = createGetterSetter(
'visualizations'
);
diff --git a/src/plugins/vis_augmenter/public/utils/utils.test.ts b/src/plugins/vis_augmenter/public/utils/utils.test.ts
index 05f90522fe4a..f831deef3955 100644
--- a/src/plugins/vis_augmenter/public/utils/utils.test.ts
+++ b/src/plugins/vis_augmenter/public/utils/utils.test.ts
@@ -21,12 +21,11 @@ import {
PluginResource,
VisLayerErrorTypes,
SavedObjectLoaderAugmentVis,
- isEligibleForDataSource,
} from '../';
import { PLUGIN_AUGMENTATION_ENABLE_SETTING } from '../../common/constants';
import { AggConfigs } from '../../../data/common';
import { uiSettingsServiceMock } from '../../../../core/public/mocks';
-import { setIndexPatterns, setUISettings } from '../services';
+import { setUISettings } from '../services';
import {
STUB_INDEX_PATTERN_WITH_FIELDS,
TYPES_REGISTRY,
@@ -36,7 +35,6 @@ import {
createPointInTimeEventsVisLayer,
createVisLayer,
} from '../mocks';
-import { dataPluginMock } from 'src/plugins/data/public/mocks';
describe('utils', () => {
const uiSettingsMock = uiSettingsServiceMock.createStartContract();
@@ -62,7 +60,7 @@ describe('utils', () => {
aggs: VALID_AGGS,
},
} as unknown) as Vis;
- expect(await isEligibleForVisLayers(vis)).toEqual(false);
+ expect(isEligibleForVisLayers(vis)).toEqual(false);
});
it('vis is ineligible with no date_histogram', async () => {
const invalidConfigStates = [
@@ -89,7 +87,7 @@ describe('utils', () => {
invalidAggs,
},
} as unknown) as Vis;
- expect(await isEligibleForVisLayers(vis)).toEqual(false);
+ expect(isEligibleForVisLayers(vis)).toEqual(false);
});
it('vis is ineligible with invalid aggs counts', async () => {
const invalidConfigStates = [
@@ -113,7 +111,7 @@ describe('utils', () => {
invalidAggs,
},
} as unknown) as Vis;
- expect(await isEligibleForVisLayers(vis)).toEqual(false);
+ expect(isEligibleForVisLayers(vis)).toEqual(false);
});
it('vis is ineligible with no metric aggs', async () => {
const invalidConfigStates = [
@@ -135,7 +133,7 @@ describe('utils', () => {
invalidAggs,
},
} as unknown) as Vis;
- expect(await isEligibleForVisLayers(vis)).toEqual(false);
+ expect(isEligibleForVisLayers(vis)).toEqual(false);
});
it('vis is ineligible with series param is not line type', async () => {
const vis = ({
@@ -156,7 +154,7 @@ describe('utils', () => {
aggs: VALID_AGGS,
},
} as unknown) as Vis;
- expect(await isEligibleForVisLayers(vis)).toEqual(false);
+ expect(isEligibleForVisLayers(vis)).toEqual(false);
});
it('vis is ineligible with series param not all being line type', async () => {
const vis = ({
@@ -180,7 +178,7 @@ describe('utils', () => {
aggs: VALID_AGGS,
},
} as unknown) as Vis;
- expect(await isEligibleForVisLayers(vis)).toEqual(false);
+ expect(isEligibleForVisLayers(vis)).toEqual(false);
});
it('vis is ineligible with invalid x-axis due to no segment aggregation', async () => {
const badConfigStates = [
@@ -218,7 +216,7 @@ describe('utils', () => {
badAggs,
},
} as unknown) as Vis;
- expect(await isEligibleForVisLayers(invalidVis)).toEqual(false);
+ expect(isEligibleForVisLayers(invalidVis)).toEqual(false);
});
it('vis is ineligible with xaxis not on bottom', async () => {
const invalidVis = ({
@@ -239,7 +237,7 @@ describe('utils', () => {
aggs: VALID_AGGS,
},
} as unknown) as Vis;
- expect(await isEligibleForVisLayers(invalidVis)).toEqual(false);
+ expect(isEligibleForVisLayers(invalidVis)).toEqual(false);
});
it('vis is ineligible with no seriesParams', async () => {
const invalidVis = ({
@@ -255,16 +253,16 @@ describe('utils', () => {
aggs: VALID_AGGS,
},
} as unknown) as Vis;
- expect(await isEligibleForVisLayers(invalidVis)).toEqual(false);
+ expect(isEligibleForVisLayers(invalidVis)).toEqual(false);
});
it('vis is ineligible with valid type and disabled setting', async () => {
uiSettingsMock.get.mockImplementation((key: string) => {
return key !== PLUGIN_AUGMENTATION_ENABLE_SETTING;
});
- expect(await isEligibleForVisLayers(VALID_VIS)).toEqual(false);
+ expect(isEligibleForVisLayers(VALID_VIS)).toEqual(false);
});
it('vis is eligible with valid type', async () => {
- expect(await isEligibleForVisLayers(VALID_VIS)).toEqual(true);
+ expect(isEligibleForVisLayers(VALID_VIS)).toEqual(true);
});
});
@@ -662,107 +660,4 @@ describe('utils', () => {
expect(mockDeleteFn).toHaveBeenCalledTimes(1);
});
});
-
- describe('isEligibleForDataSource', () => {
- beforeEach(() => {
- jest.clearAllMocks();
- });
- it('returns true if the Vis indexPattern does not have a dataSourceRef', async () => {
- const indexPatternsMock = dataPluginMock.createStartContract().indexPatterns;
- indexPatternsMock.getDataSource = jest.fn().mockReturnValue(undefined);
- setIndexPatterns(indexPatternsMock);
- const vis = {
- data: {
- indexPattern: {
- id: '123',
- },
- },
- } as Vis;
- expect(await isEligibleForDataSource(vis)).toEqual(true);
- });
- it('returns true if the Vis indexPattern has a dataSourceRef with a compatible version', async () => {
- const indexPatternsMock = dataPluginMock.createStartContract().indexPatterns;
- indexPatternsMock.getDataSource = jest.fn().mockReturnValue({
- id: '456',
- attributes: {
- dataSourceVersion: '1.2.3',
- },
- });
- setIndexPatterns(indexPatternsMock);
- const vis = {
- data: {
- indexPattern: {
- id: '123',
- dataSourceRef: {
- id: '456',
- },
- },
- },
- } as Vis;
- expect(await isEligibleForDataSource(vis)).toEqual(true);
- });
- it('returns false if the Vis indexPattern has a dataSourceRef with an incompatible version', async () => {
- const indexPatternsMock = dataPluginMock.createStartContract().indexPatterns;
- indexPatternsMock.getDataSource = jest.fn().mockReturnValue({
- id: '456',
- attributes: {
- dataSourceVersion: '.0',
- },
- });
- setIndexPatterns(indexPatternsMock);
- const vis = {
- data: {
- indexPattern: {
- id: '123',
- dataSourceRef: {
- id: '456',
- },
- },
- },
- } as Vis;
- expect(await isEligibleForDataSource(vis)).toEqual(false);
- });
- it('returns false if the Vis indexPattern has a dataSourceRef with an undefined version', async () => {
- const indexPatternsMock = dataPluginMock.createStartContract().indexPatterns;
- indexPatternsMock.getDataSource = jest.fn().mockReturnValue({
- id: '456',
- attributes: {
- dataSourceVersion: undefined,
- },
- });
- setIndexPatterns(indexPatternsMock);
- const vis = {
- data: {
- indexPattern: {
- id: '123',
- dataSourceRef: {
- id: '456',
- },
- },
- },
- } as Vis;
- expect(await isEligibleForDataSource(vis)).toEqual(false);
- });
- it('returns false if the Vis indexPattern has a dataSourceRef with an empty string version', async () => {
- const indexPatternsMock = dataPluginMock.createStartContract().indexPatterns;
- indexPatternsMock.getDataSource = jest.fn().mockReturnValue({
- id: '456',
- attributes: {
- dataSourceVersion: '',
- },
- });
- setIndexPatterns(indexPatternsMock);
- const vis = {
- data: {
- indexPattern: {
- id: '123',
- dataSourceRef: {
- id: '456',
- },
- },
- },
- } as Vis;
- expect(await isEligibleForDataSource(vis)).toEqual(false);
- });
- });
});
diff --git a/src/plugins/vis_augmenter/public/utils/utils.ts b/src/plugins/vis_augmenter/public/utils/utils.ts
index 0ae3c9ec93aa..ce44964e6173 100644
--- a/src/plugins/vis_augmenter/public/utils/utils.ts
+++ b/src/plugins/vis_augmenter/public/utils/utils.ts
@@ -4,7 +4,6 @@
*/
import { get, isEmpty } from 'lodash';
-import semver from 'semver';
import { Vis } from '../../../../plugins/visualizations/public';
import {
formatExpression,
@@ -21,13 +20,10 @@ import {
VisLayerErrorTypes,
} from '../';
import { PLUGIN_AUGMENTATION_ENABLE_SETTING } from '../../common/constants';
-import { getUISettings, getIndexPatterns } from '../services';
+import { getUISettings } from '../services';
import { IUiSettingsClient } from '../../../../core/public';
-export const isEligibleForVisLayers = async (
- vis: Vis,
- uiSettingsClient?: IUiSettingsClient
-): Promise => {
+export const isEligibleForVisLayers = (vis: Vis, uiSettingsClient?: IUiSettingsClient): boolean => {
// Only support a date histogram
const dateHistograms = vis.data?.aggs?.byTypeName?.('date_histogram');
if (!Array.isArray(dateHistograms) || dateHistograms.length !== 1) return false;
@@ -57,9 +53,6 @@ export const isEligibleForVisLayers = async (
)
return false;
- // Check if the vis datasource is eligible for the augmentation
- if (!(await isEligibleForDataSource(vis))) return false;
-
// Checks if the augmentation setting is enabled
const config = uiSettingsClient ?? getUISettings();
return config.get(PLUGIN_AUGMENTATION_ENABLE_SETTING);
@@ -170,6 +163,7 @@ export const getAnyErrors = (visLayers: VisLayer[], visTitle: string): Error | u
* @param visLayers the produced VisLayers containing details if the resource has been deleted
* @param visualizationsLoader the visualizations saved object loader to handle deletion
*/
+
export const cleanupStaleObjects = (
augmentVisSavedObjs: ISavedAugmentVis[],
visLayers: VisLayer[],
@@ -193,17 +187,3 @@ export const cleanupStaleObjects = (
loader?.delete(objIdsToDelete);
}
};
-
-/**
- * Returns true if the Vis is eligible to be used with the DataSource feature.
- * @param vis - The Vis to check
- * @returns true if the Vis is eligible for the DataSource feature, false otherwise
- */
-export const isEligibleForDataSource = async (vis: Vis) => {
- const dataSourceRef = vis.data.indexPattern?.dataSourceRef;
- if (!dataSourceRef) return true;
- const dataSource = await getIndexPatterns().getDataSource(dataSourceRef.id);
- if (!dataSource || !dataSource.attributes) return false;
- const version = semver.coerce(dataSource.attributes.dataSourceVersion);
- return version ? semver.satisfies(version, '>=1.0.0') : false;
-};
diff --git a/src/plugins/vis_augmenter/public/view_events_flyout/actions/view_events_option_action.tsx b/src/plugins/vis_augmenter/public/view_events_flyout/actions/view_events_option_action.tsx
index f83f0e0b77d6..ac7f795c586e 100644
--- a/src/plugins/vis_augmenter/public/view_events_flyout/actions/view_events_option_action.tsx
+++ b/src/plugins/vis_augmenter/public/view_events_flyout/actions/view_events_option_action.tsx
@@ -46,7 +46,7 @@ export class ViewEventsOptionAction implements Action {
const vis = (embeddable as VisualizeEmbeddable).vis;
return (
vis !== undefined &&
- (await isEligibleForVisLayers(vis)) &&
+ isEligibleForVisLayers(vis) &&
!isEmpty((embeddable as VisualizeEmbeddable).visLayers)
);
}
diff --git a/src/plugins/vis_type_vislib/public/line_to_expression.ts b/src/plugins/vis_type_vislib/public/line_to_expression.ts
index e8d207017c00..8650c6013801 100644
--- a/src/plugins/vis_type_vislib/public/line_to_expression.ts
+++ b/src/plugins/vis_type_vislib/public/line_to_expression.ts
@@ -32,7 +32,7 @@ export const toExpressionAst = async (vis: Vis, params: any) => {
if (
params.visLayers == null ||
Object.keys(params.visLayers).length === 0 ||
- !(await isEligibleForVisLayers(vis))
+ !isEligibleForVisLayers(vis)
) {
// Render using vislib instead of vega-lite
const visConfig = { ...vis.params, dimensions };
diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts
index 7bf996c148ea..605c88067211 100644
--- a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts
+++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts
@@ -541,7 +541,7 @@ export class VisualizeEmbeddable
this.visAugmenterConfig?.visLayerResourceIds
);
- if (!isEmpty(augmentVisSavedObjs) && !aborted && (await isEligibleForVisLayers(this.vis))) {
+ if (!isEmpty(augmentVisSavedObjs) && !aborted && isEligibleForVisLayers(this.vis)) {
const visLayersPipeline = buildPipelineFromAugmentVisSavedObjs(augmentVisSavedObjs);
// The initial input for the pipeline will just be an empty arr of VisLayers. As plugin
// expression functions are ran, they will incrementally append their generated VisLayers to it.
diff --git a/src/plugins/workspace/server/saved_objects/integration_tests/workspace_id_consumer_wrapper.test.ts b/src/plugins/workspace/server/saved_objects/integration_tests/workspace_id_consumer_wrapper.test.ts
index c8212d9cc6b1..eca47fbb5b72 100644
--- a/src/plugins/workspace/server/saved_objects/integration_tests/workspace_id_consumer_wrapper.test.ts
+++ b/src/plugins/workspace/server/saved_objects/integration_tests/workspace_id_consumer_wrapper.test.ts
@@ -36,6 +36,8 @@ describe('workspace_id_consumer integration test', () => {
let createdBarWorkspace: WorkspaceAttributes = {
id: '',
};
+ const deleteWorkspace = (workspaceId: string) =>
+ osdTestServer.request.delete(root, `/api/workspaces/${workspaceId}`);
beforeAll(async () => {
const { startOpenSearch, startOpenSearchDashboards } = osdTestServer.createTestServers({
adjustTimeout: (t: number) => jest.setTimeout(t),
@@ -75,6 +77,10 @@ describe('workspace_id_consumer integration test', () => {
}).then((resp) => resp.body.result);
}, 30000);
afterAll(async () => {
+ await Promise.all([
+ deleteWorkspace(createdFooWorkspace.id),
+ deleteWorkspace(createdBarWorkspace.id),
+ ]);
await root.shutdown();
await opensearchServer.stop();
});
@@ -144,10 +150,35 @@ describe('workspace_id_consumer integration test', () => {
`/api/saved_objects/${config.type}/${packageInfo.version}`
);
- // workspaces arrtibutes should not be append
+ // workspaces attributes should not be append
expect(!getConfigResult.body.workspaces).toEqual(true);
});
+ it('should return error when create with a not existing workspace', async () => {
+ await clearFooAndBar();
+ const createResultWithNonExistRequestWorkspace = await osdTestServer.request
+ .post(root, `/w/not_exist_workspace_id/api/saved_objects/${dashboard.type}`)
+ .send({
+ attributes: dashboard.attributes,
+ })
+ .expect(400);
+
+ expect(createResultWithNonExistRequestWorkspace.body.message).toEqual(
+ 'Exist invalid workspaces'
+ );
+
+ const createResultWithNonExistOptionsWorkspace = await osdTestServer.request
+ .post(root, `/api/saved_objects/${dashboard.type}`)
+ .send({
+ attributes: dashboard.attributes,
+ workspaces: ['not_exist_workspace_id'],
+ })
+ .expect(400);
+ expect(createResultWithNonExistOptionsWorkspace.body.message).toEqual(
+ 'Exist invalid workspaces'
+ );
+ });
+
it('bulk create', async () => {
await clearFooAndBar();
const createResultFoo = await osdTestServer.request
@@ -178,6 +209,37 @@ describe('workspace_id_consumer integration test', () => {
);
});
+ it('should return error when bulk create with a not existing workspace', async () => {
+ await clearFooAndBar();
+ const bulkCreateResultWithNonExistRequestWorkspace = await osdTestServer.request
+ .post(root, `/w/not_exist_workspace_id/api/saved_objects/_bulk_create`)
+ .send([
+ {
+ ...dashboard,
+ id: 'foo',
+ },
+ ])
+ .expect(400);
+
+ expect(bulkCreateResultWithNonExistRequestWorkspace.body.message).toEqual(
+ 'Exist invalid workspaces'
+ );
+
+ const bulkCreateResultWithNonExistOptionsWorkspace = await osdTestServer.request
+ .post(root, `/api/saved_objects/_bulk_create?workspaces=not_exist_workspace_id`)
+ .send([
+ {
+ ...dashboard,
+ id: 'foo',
+ },
+ ])
+ .expect(400);
+
+ expect(bulkCreateResultWithNonExistOptionsWorkspace.body.message).toEqual(
+ 'Exist invalid workspaces'
+ );
+ });
+
it('checkConflicts when importing ndjson', async () => {
await clearFooAndBar();
const createResultFoo = await osdTestServer.request
@@ -282,7 +344,7 @@ describe('workspace_id_consumer integration test', () => {
.get(root, `/w/not_exist_workspace_id/api/saved_objects/_find?type=${dashboard.type}`)
.expect(400);
- expect(findResult.body.message).toEqual('Invalid workspaces');
+ expect(findResult.body.message).toEqual('Exist invalid workspaces');
});
it('import within workspace', async () => {
@@ -312,5 +374,151 @@ describe('workspace_id_consumer integration test', () => {
expect(importWithWorkspacesResult.body.success).toEqual(true);
expect(findResult.body.saved_objects[0].workspaces).toEqual([createdFooWorkspace.id]);
});
+
+ it('get', async () => {
+ await clearFooAndBar();
+ await osdTestServer.request.delete(
+ root,
+ `/api/saved_objects/${config.type}/${packageInfo.version}`
+ );
+ const createResultFoo = await osdTestServer.request
+ .post(root, `/w/${createdFooWorkspace.id}/api/saved_objects/_bulk_create`)
+ .send([
+ {
+ ...dashboard,
+ id: 'foo',
+ },
+ ])
+ .expect(200);
+
+ const createResultBar = await osdTestServer.request
+ .post(root, `/w/${createdBarWorkspace.id}/api/saved_objects/_bulk_create`)
+ .send([
+ {
+ ...dashboard,
+ id: 'bar',
+ },
+ ])
+ .expect(200);
+
+ await osdTestServer.request
+ .post(root, `/api/saved_objects/${config.type}/${packageInfo.version}`)
+ .send({
+ attributes: {
+ legacyConfig: 'foo',
+ },
+ })
+ .expect(200);
+
+ const getResultWithRequestWorkspace = await osdTestServer.request
+ .get(root, `/w/${createdFooWorkspace.id}/api/saved_objects/${dashboard.type}/foo`)
+ .expect(200);
+ expect(getResultWithRequestWorkspace.body.id).toEqual('foo');
+ expect(getResultWithRequestWorkspace.body.workspaces).toEqual([createdFooWorkspace.id]);
+
+ const getResultWithoutRequestWorkspace = await osdTestServer.request
+ .get(root, `/api/saved_objects/${dashboard.type}/bar`)
+ .expect(200);
+ expect(getResultWithoutRequestWorkspace.body.id).toEqual('bar');
+
+ const getGlobalResultWithinWorkspace = await osdTestServer.request
+ .get(
+ root,
+ `/w/${createdFooWorkspace.id}/api/saved_objects/${config.type}/${packageInfo.version}`
+ )
+ .expect(200);
+ expect(getGlobalResultWithinWorkspace.body.id).toEqual(packageInfo.version);
+
+ await osdTestServer.request
+ .get(root, `/w/${createdFooWorkspace.id}/api/saved_objects/${dashboard.type}/bar`)
+ .expect(403);
+
+ await Promise.all(
+ [...createResultFoo.body.saved_objects, ...createResultBar.body.saved_objects].map((item) =>
+ deleteItem({
+ type: item.type,
+ id: item.id,
+ })
+ )
+ );
+ await osdTestServer.request.delete(
+ root,
+ `/api/saved_objects/${config.type}/${packageInfo.version}`
+ );
+ });
+
+ it('bulk get', async () => {
+ await clearFooAndBar();
+ const createResultFoo = await osdTestServer.request
+ .post(root, `/w/${createdFooWorkspace.id}/api/saved_objects/_bulk_create`)
+ .send([
+ {
+ ...dashboard,
+ id: 'foo',
+ },
+ ])
+ .expect(200);
+
+ const createResultBar = await osdTestServer.request
+ .post(root, `/w/${createdBarWorkspace.id}/api/saved_objects/_bulk_create`)
+ .send([
+ {
+ ...dashboard,
+ id: 'bar',
+ },
+ ])
+ .expect(200);
+
+ const payload = [
+ { id: 'foo', type: 'dashboard' },
+ { id: 'bar', type: 'dashboard' },
+ ];
+ const bulkGetResultWithWorkspace = await osdTestServer.request
+ .post(root, `/w/${createdFooWorkspace.id}/api/saved_objects/_bulk_get`)
+ .send(payload)
+ .expect(200);
+
+ expect(bulkGetResultWithWorkspace.body.saved_objects.length).toEqual(2);
+ expect(bulkGetResultWithWorkspace.body.saved_objects[0].id).toEqual('foo');
+ expect(bulkGetResultWithWorkspace.body.saved_objects[0].workspaces).toEqual([
+ createdFooWorkspace.id,
+ ]);
+ expect(bulkGetResultWithWorkspace.body.saved_objects[0]?.error).toBeUndefined();
+ expect(bulkGetResultWithWorkspace.body.saved_objects[1].id).toEqual('bar');
+ expect(bulkGetResultWithWorkspace.body.saved_objects[1].workspaces).toBeUndefined();
+ expect(bulkGetResultWithWorkspace.body.saved_objects[1]?.error).toMatchInlineSnapshot(`
+ Object {
+ "error": "Forbidden",
+ "message": "Saved object does not belong to the workspace",
+ "statusCode": 403,
+ }
+ `);
+
+ const bulkGetResultWithoutWorkspace = await osdTestServer.request
+ .post(root, `/api/saved_objects/_bulk_get`)
+ .send(payload)
+ .expect(200);
+
+ expect(bulkGetResultWithoutWorkspace.body.saved_objects.length).toEqual(2);
+ expect(bulkGetResultWithoutWorkspace.body.saved_objects[0].id).toEqual('foo');
+ expect(bulkGetResultWithoutWorkspace.body.saved_objects[0].workspaces).toEqual([
+ createdFooWorkspace.id,
+ ]);
+ expect(bulkGetResultWithoutWorkspace.body.saved_objects[0]?.error).toBeUndefined();
+ expect(bulkGetResultWithoutWorkspace.body.saved_objects[1].id).toEqual('bar');
+ expect(bulkGetResultWithoutWorkspace.body.saved_objects[1].workspaces).toEqual([
+ createdBarWorkspace.id,
+ ]);
+ expect(bulkGetResultWithoutWorkspace.body.saved_objects[1]?.error).toBeUndefined();
+
+ await Promise.all(
+ [...createResultFoo.body.saved_objects, ...createResultBar.body.saved_objects].map((item) =>
+ deleteItem({
+ type: item.type,
+ id: item.id,
+ })
+ )
+ );
+ });
});
});
diff --git a/src/plugins/workspace/server/saved_objects/integration_tests/workspace_saved_objects_client_wrapper.test.ts b/src/plugins/workspace/server/saved_objects/integration_tests/workspace_saved_objects_client_wrapper.test.ts
index 82c943545aca..e3eddb443990 100644
--- a/src/plugins/workspace/server/saved_objects/integration_tests/workspace_saved_objects_client_wrapper.test.ts
+++ b/src/plugins/workspace/server/saved_objects/integration_tests/workspace_saved_objects_client_wrapper.test.ts
@@ -250,7 +250,7 @@ describe('WorkspaceSavedObjectsClientWrapper', () => {
perPage: 999,
page: 1,
})
- ).rejects.toMatchInlineSnapshot(`[Error: Invalid workspaces]`);
+ ).rejects.toMatchInlineSnapshot(`[Error: Exist invalid workspaces]`);
});
it('should return consistent inner workspace data when user permitted', async () => {
@@ -349,21 +349,16 @@ describe('WorkspaceSavedObjectsClientWrapper', () => {
});
describe('create', () => {
- it('should throw forbidden error when workspace not permitted and create called', async () => {
- let error;
- try {
- await notPermittedSavedObjectedClient.create(
+ it('should throw bad request error when workspace is invalid and create called', async () => {
+ await expect(
+ notPermittedSavedObjectedClient.create(
'dashboard',
{},
{
workspaces: ['workspace-1'],
}
- );
- } catch (e) {
- error = e;
- }
-
- expect(SavedObjectsErrorHelpers.isForbiddenError(error)).toBe(true);
+ )
+ ).rejects.toMatchInlineSnapshot(`[Error: Exist invalid workspaces]`);
});
it('should able to create saved objects into permitted workspaces after create called', async () => {
@@ -427,7 +422,7 @@ describe('WorkspaceSavedObjectsClientWrapper', () => {
expect(createResult.error).toBeUndefined();
});
- it('should throw forbidden error when user create a workspce and is not OSD admin', async () => {
+ it('should throw forbidden error when user create a workspace and is not OSD admin', async () => {
let error;
try {
await permittedSavedObjectedClient.create('workspace', {}, {});
@@ -468,17 +463,12 @@ describe('WorkspaceSavedObjectsClientWrapper', () => {
});
describe('bulkCreate', () => {
- it('should throw forbidden error when workspace not permitted and bulkCreate called', async () => {
- let error;
- try {
- await notPermittedSavedObjectedClient.bulkCreate([{ type: 'dashboard', attributes: {} }], {
+ it('should throw bad request error when workspace is invalid and bulkCreate called', async () => {
+ await expect(
+ notPermittedSavedObjectedClient.bulkCreate([{ type: 'dashboard', attributes: {} }], {
workspaces: ['workspace-1'],
- });
- } catch (e) {
- error = e;
- }
-
- expect(SavedObjectsErrorHelpers.isForbiddenError(error)).toBe(true);
+ })
+ ).rejects.toMatchInlineSnapshot(`[Error: Exist invalid workspaces]`);
});
it('should able to create saved objects into permitted workspaces after bulkCreate called', async () => {
@@ -506,7 +496,6 @@ describe('WorkspaceSavedObjectsClientWrapper', () => {
],
{
overwrite: true,
- workspaces: ['workspace-1'],
}
);
} catch (e) {
diff --git a/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.test.ts b/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.test.ts
index 570d701d7c63..5d9a4094336e 100644
--- a/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.test.ts
+++ b/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.test.ts
@@ -8,6 +8,7 @@ import { SavedObject } from '../../../../core/public';
import { httpServerMock, savedObjectsClientMock, coreMock } from '../../../../core/server/mocks';
import { WorkspaceIdConsumerWrapper } from './workspace_id_consumer_wrapper';
import { workspaceClientMock } from '../workspace_client.mock';
+import { SavedObjectsErrorHelpers } from '../../../../core/server';
describe('WorkspaceIdConsumerWrapper', () => {
const requestHandlerContext = coreMock.createRequestHandlerContext();
@@ -37,8 +38,15 @@ describe('WorkspaceIdConsumerWrapper', () => {
describe('create', () => {
beforeEach(() => {
mockedClient.create.mockClear();
+ mockedWorkspaceClient.get.mockClear();
+ mockedWorkspaceClient.list.mockClear();
});
it(`Should add workspaces parameters when create`, async () => {
+ mockedWorkspaceClient.get.mockImplementationOnce((requestContext, id) => {
+ return {
+ success: true,
+ };
+ });
await wrapperClient.create('dashboard', {
name: 'foo',
});
@@ -67,13 +75,54 @@ describe('WorkspaceIdConsumerWrapper', () => {
expect(mockedClient.create.mock.calls[0][2]?.hasOwnProperty('workspaces')).toEqual(false);
});
+
+ it(`Should throw error when passing in invalid workspaces`, async () => {
+ const workspaceIdConsumerWrapper = new WorkspaceIdConsumerWrapper(mockedWorkspaceClient);
+ const mockRequest = httpServerMock.createOpenSearchDashboardsRequest();
+ updateWorkspaceState(mockRequest, {});
+ const mockedWrapperClient = workspaceIdConsumerWrapper.wrapperFactory({
+ client: mockedClient,
+ typeRegistry: requestHandlerContext.savedObjects.typeRegistry,
+ request: mockRequest,
+ });
+
+ mockedWorkspaceClient.list.mockResolvedValueOnce({
+ success: true,
+ result: {
+ workspaces: [
+ {
+ id: 'foo',
+ },
+ ],
+ },
+ });
+
+ expect(
+ mockedWrapperClient.create(
+ 'dashboard',
+ {
+ name: 'foo',
+ },
+ { workspaces: ['zoo', 'noo'] }
+ )
+ ).rejects.toMatchInlineSnapshot(`[Error: Exist invalid workspaces]`);
+ expect(mockedWorkspaceClient.get).toBeCalledTimes(0);
+ expect(mockedWorkspaceClient.list).toBeCalledTimes(1);
+ });
});
describe('bulkCreate', () => {
beforeEach(() => {
mockedClient.bulkCreate.mockClear();
+ mockedWorkspaceClient.get.mockClear();
+ mockedWorkspaceClient.list.mockClear();
});
it(`Should add workspaces parameters when bulk create`, async () => {
+ mockedWorkspaceClient.get.mockImplementationOnce((requestContext, id) => {
+ return {
+ success: true,
+ };
+ });
await wrapperClient.bulkCreate([
getSavedObject({
id: 'foo',
@@ -87,6 +136,23 @@ describe('WorkspaceIdConsumerWrapper', () => {
}
);
});
+
+ it(`Should throw error when passing in invalid workspaces`, async () => {
+ mockedWorkspaceClient.get.mockImplementationOnce((requestContext, id) => {
+ return {
+ success: false,
+ };
+ });
+ expect(
+ wrapperClient.bulkCreate([
+ getSavedObject({
+ id: 'foo',
+ }),
+ ])
+ ).rejects.toMatchInlineSnapshot(`[Error: Exist invalid workspaces]`);
+ expect(mockedWorkspaceClient.get).toBeCalledTimes(1);
+ expect(mockedWorkspaceClient.list).toBeCalledTimes(0);
+ });
});
describe('checkConflict', () => {
@@ -173,7 +239,7 @@ describe('WorkspaceIdConsumerWrapper', () => {
type: ['dashboard', 'visualization'],
workspaces: ['foo', 'not-exist'],
})
- ).rejects.toMatchInlineSnapshot(`[Error: Invalid workspaces]`);
+ ).rejects.toMatchInlineSnapshot(`[Error: Exist invalid workspaces]`);
expect(mockedWorkspaceClient.get).toBeCalledTimes(0);
expect(mockedWorkspaceClient.list).toBeCalledTimes(1);
});
@@ -196,4 +262,498 @@ describe('WorkspaceIdConsumerWrapper', () => {
});
});
});
+
+ describe('get', () => {
+ beforeEach(() => {
+ mockedClient.get.mockClear();
+ });
+
+ it(`Should get object belonging to options.workspaces`, async () => {
+ const savedObject = {
+ type: 'dashboard',
+ id: 'dashboard_id',
+ attributes: {},
+ references: [],
+ workspaces: ['foo'],
+ };
+ mockedClient.get.mockResolvedValueOnce(savedObject);
+ const result = await wrapperClient.get(savedObject.type, savedObject.id, {
+ workspaces: savedObject.workspaces,
+ });
+ expect(mockedClient.get).toBeCalledWith(savedObject.type, savedObject.id, {
+ workspaces: savedObject.workspaces,
+ });
+ expect(result).toEqual(savedObject);
+ });
+
+ it(`Should get object belonging to the workspace in request`, async () => {
+ const savedObject = {
+ type: 'dashboard',
+ id: 'dashboard_id',
+ attributes: {},
+ references: [],
+ workspaces: ['foo'],
+ };
+ mockedClient.get.mockResolvedValueOnce(savedObject);
+ const result = await wrapperClient.get(savedObject.type, savedObject.id);
+ expect(mockedClient.get).toBeCalledWith(savedObject.type, savedObject.id, {});
+ expect(result).toEqual(savedObject);
+ });
+
+ it(`Should get object if the object type is workspace`, async () => {
+ const savedObject = {
+ type: 'workspace',
+ id: 'workspace_id',
+ attributes: {},
+ references: [],
+ };
+ mockedClient.get.mockResolvedValueOnce(savedObject);
+ const result = await wrapperClient.get(savedObject.type, savedObject.id);
+ expect(mockedClient.get).toBeCalledWith(savedObject.type, savedObject.id, {});
+ expect(result).toEqual(savedObject);
+ });
+
+ it(`Should get object if the object type is config`, async () => {
+ const savedObject = {
+ type: 'config',
+ id: 'config_id',
+ attributes: {},
+ references: [],
+ };
+ mockedClient.get.mockResolvedValueOnce(savedObject);
+ const result = await wrapperClient.get(savedObject.type, savedObject.id);
+ expect(mockedClient.get).toBeCalledWith(savedObject.type, savedObject.id, {});
+ expect(result).toEqual(savedObject);
+ });
+
+ it(`Should get object when there is no workspace in options/request`, async () => {
+ const workspaceIdConsumerWrapper = new WorkspaceIdConsumerWrapper(mockedWorkspaceClient);
+ const mockRequest = httpServerMock.createOpenSearchDashboardsRequest();
+ updateWorkspaceState(mockRequest, {});
+ const mockedWrapperClient = workspaceIdConsumerWrapper.wrapperFactory({
+ client: mockedClient,
+ typeRegistry: requestHandlerContext.savedObjects.typeRegistry,
+ request: mockRequest,
+ });
+ const savedObject = {
+ type: 'dashboard',
+ id: 'dashboard_id',
+ attributes: {},
+ references: [],
+ };
+ mockedClient.get.mockResolvedValueOnce(savedObject);
+ const result = await mockedWrapperClient.get(savedObject.type, savedObject.id);
+ expect(mockedClient.get).toBeCalledWith(savedObject.type, savedObject.id, {});
+ expect(result).toEqual(savedObject);
+ });
+
+ it(`Should throw error when the object is not belong to the workspace`, async () => {
+ const savedObject = {
+ type: 'dashboard',
+ id: 'dashboard_id',
+ attributes: {},
+ references: [],
+ workspaces: ['bar'],
+ };
+ mockedClient.get.mockResolvedValueOnce(savedObject);
+ expect(wrapperClient.get(savedObject.type, savedObject.id)).rejects.toMatchInlineSnapshot(
+ `[Error: Saved object does not belong to the workspace]`
+ );
+ expect(mockedClient.get).toBeCalledWith(savedObject.type, savedObject.id, {});
+ });
+
+ it(`Should throw error when the object does not exist`, async () => {
+ mockedClient.get.mockRejectedValueOnce(SavedObjectsErrorHelpers.createGenericNotFoundError());
+ expect(wrapperClient.get('type', 'id')).rejects.toMatchInlineSnapshot(`[Error: Not Found]`);
+ expect(mockedClient.get).toHaveBeenCalledTimes(1);
+ });
+
+ it(`Should throw error when the options.workspaces has more than one workspace.`, async () => {
+ const savedObject = {
+ type: 'dashboard',
+ id: 'dashboard_id',
+ attributes: {},
+ references: [],
+ workspaces: ['bar'],
+ };
+ const options = { workspaces: ['foo', 'bar'] };
+ expect(
+ wrapperClient.get(savedObject.type, savedObject.id, options)
+ ).rejects.toMatchInlineSnapshot(`[Error: Multiple workspace parameters: Bad Request]`);
+ expect(mockedClient.get).not.toBeCalled();
+ });
+
+ it(`Should get data source when user is data source admin`, async () => {
+ const workspaceIdConsumerWrapper = new WorkspaceIdConsumerWrapper(mockedWorkspaceClient);
+ const mockRequest = httpServerMock.createOpenSearchDashboardsRequest();
+ updateWorkspaceState(mockRequest, { isDataSourceAdmin: true, requestWorkspaceId: 'foo' });
+ const mockedWrapperClient = workspaceIdConsumerWrapper.wrapperFactory({
+ client: mockedClient,
+ typeRegistry: requestHandlerContext.savedObjects.typeRegistry,
+ request: mockRequest,
+ });
+ const savedObject = {
+ type: 'data-source',
+ id: 'data-source_id',
+ attributes: {},
+ references: [],
+ };
+ mockedClient.get.mockResolvedValueOnce(savedObject);
+ const result = await mockedWrapperClient.get(savedObject.type, savedObject.id);
+ expect(mockedClient.get).toBeCalledWith(savedObject.type, savedObject.id, {});
+ expect(result).toEqual(savedObject);
+ });
+
+ it(`Should throw error when the object is global data source`, async () => {
+ const savedObject = {
+ type: 'data-source',
+ id: 'data-source_id',
+ attributes: {},
+ references: [],
+ };
+ mockedClient.get.mockResolvedValueOnce(savedObject);
+ mockedClient.get.mockResolvedValueOnce(savedObject);
+ expect(wrapperClient.get(savedObject.type, savedObject.id)).rejects.toMatchInlineSnapshot(
+ `[Error: Saved object does not belong to the workspace]`
+ );
+ expect(mockedClient.get).toBeCalledWith(savedObject.type, savedObject.id, {});
+ });
+ });
+
+ describe('bulkGet', () => {
+ const payload = [
+ { id: 'dashboard_id', type: 'dashboard' },
+ { id: 'dashboard_error_id', type: 'dashboard' },
+ { id: 'visualization_id', type: 'visualization' },
+ { id: 'global_data_source_id', type: 'data-source' },
+ { id: 'data_source_id', type: 'data-source' },
+ ];
+ const savedObjects = [
+ {
+ type: 'dashboard',
+ id: 'dashboard_id',
+ attributes: { description: 'description' },
+ references: ['reference_id'],
+ workspaces: ['foo'],
+ },
+ {
+ type: 'dashboard',
+ id: 'dashboard_error_id',
+ attributes: {},
+ references: [],
+ error: {
+ statusCode: 404,
+ error: 'Not Found',
+ message: 'Saved object [dashboard/dashboard_error_id] not found',
+ },
+ },
+ {
+ type: 'visualization',
+ id: 'visualization_id',
+ attributes: { description: 'description' },
+ references: ['reference_id'],
+ workspaces: ['bar'],
+ },
+ {
+ type: 'config',
+ id: 'config_id',
+ attributes: {},
+ references: [],
+ },
+ {
+ type: 'workspace',
+ id: 'workspace_id',
+ attributes: {},
+ references: [],
+ },
+ {
+ type: 'data-source',
+ id: 'global_data_source_id',
+ attributes: {},
+ references: [],
+ },
+ {
+ type: 'data-source',
+ id: 'data_source_id',
+ attributes: {},
+ references: [],
+ workspaces: ['foo'],
+ },
+ ];
+ const options = { workspaces: ['foo'] };
+ beforeEach(() => {
+ mockedClient.bulkGet.mockClear();
+ });
+
+ it(`Should bulkGet objects belonging to options.workspaces`, async () => {
+ mockedClient.bulkGet.mockResolvedValueOnce({ saved_objects: savedObjects });
+ const result = await wrapperClient.bulkGet(payload, options);
+ expect(mockedClient.bulkGet).toBeCalledWith(payload, options);
+ expect(result).toMatchInlineSnapshot(`
+ Object {
+ "saved_objects": Array [
+ Object {
+ "attributes": Object {
+ "description": "description",
+ },
+ "id": "dashboard_id",
+ "references": Array [
+ "reference_id",
+ ],
+ "type": "dashboard",
+ "workspaces": Array [
+ "foo",
+ ],
+ },
+ Object {
+ "attributes": Object {},
+ "error": Object {
+ "error": "Not Found",
+ "message": "Saved object [dashboard/dashboard_error_id] not found",
+ "statusCode": 404,
+ },
+ "id": "dashboard_error_id",
+ "references": Array [],
+ "type": "dashboard",
+ },
+ Object {
+ "attributes": Object {},
+ "error": Object {
+ "error": "Forbidden",
+ "message": "Saved object does not belong to the workspace",
+ "statusCode": 403,
+ },
+ "id": "visualization_id",
+ "references": Array [],
+ "type": "visualization",
+ },
+ Object {
+ "attributes": Object {},
+ "id": "config_id",
+ "references": Array [],
+ "type": "config",
+ },
+ Object {
+ "attributes": Object {},
+ "id": "workspace_id",
+ "references": Array [],
+ "type": "workspace",
+ },
+ Object {
+ "attributes": Object {},
+ "error": Object {
+ "error": "Forbidden",
+ "message": "Saved object does not belong to the workspace",
+ "statusCode": 403,
+ },
+ "id": "global_data_source_id",
+ "references": Array [],
+ "type": "data-source",
+ },
+ Object {
+ "attributes": Object {},
+ "id": "data_source_id",
+ "references": Array [],
+ "type": "data-source",
+ "workspaces": Array [
+ "foo",
+ ],
+ },
+ ],
+ }
+ `);
+ });
+
+ it(`Should bulkGet objects belonging to the workspace in request`, async () => {
+ mockedClient.bulkGet.mockResolvedValueOnce({ saved_objects: savedObjects });
+ const result = await wrapperClient.bulkGet(payload);
+ expect(mockedClient.bulkGet).toBeCalledWith(payload, {});
+ expect(result).toMatchInlineSnapshot(`
+ Object {
+ "saved_objects": Array [
+ Object {
+ "attributes": Object {
+ "description": "description",
+ },
+ "id": "dashboard_id",
+ "references": Array [
+ "reference_id",
+ ],
+ "type": "dashboard",
+ "workspaces": Array [
+ "foo",
+ ],
+ },
+ Object {
+ "attributes": Object {},
+ "error": Object {
+ "error": "Not Found",
+ "message": "Saved object [dashboard/dashboard_error_id] not found",
+ "statusCode": 404,
+ },
+ "id": "dashboard_error_id",
+ "references": Array [],
+ "type": "dashboard",
+ },
+ Object {
+ "attributes": Object {},
+ "error": Object {
+ "error": "Forbidden",
+ "message": "Saved object does not belong to the workspace",
+ "statusCode": 403,
+ },
+ "id": "visualization_id",
+ "references": Array [],
+ "type": "visualization",
+ },
+ Object {
+ "attributes": Object {},
+ "id": "config_id",
+ "references": Array [],
+ "type": "config",
+ },
+ Object {
+ "attributes": Object {},
+ "id": "workspace_id",
+ "references": Array [],
+ "type": "workspace",
+ },
+ Object {
+ "attributes": Object {},
+ "error": Object {
+ "error": "Forbidden",
+ "message": "Saved object does not belong to the workspace",
+ "statusCode": 403,
+ },
+ "id": "global_data_source_id",
+ "references": Array [],
+ "type": "data-source",
+ },
+ Object {
+ "attributes": Object {},
+ "id": "data_source_id",
+ "references": Array [],
+ "type": "data-source",
+ "workspaces": Array [
+ "foo",
+ ],
+ },
+ ],
+ }
+ `);
+ });
+
+ it(`Should bulkGet objects when there is no workspace in options/request`, async () => {
+ const workspaceIdConsumerWrapper = new WorkspaceIdConsumerWrapper(mockedWorkspaceClient);
+ const mockRequest = httpServerMock.createOpenSearchDashboardsRequest();
+ updateWorkspaceState(mockRequest, {});
+ const mockedWrapperClient = workspaceIdConsumerWrapper.wrapperFactory({
+ client: mockedClient,
+ typeRegistry: requestHandlerContext.savedObjects.typeRegistry,
+ request: mockRequest,
+ });
+ mockedClient.bulkGet.mockResolvedValueOnce({ saved_objects: savedObjects });
+ const result = await mockedWrapperClient.bulkGet(payload);
+ expect(mockedClient.bulkGet).toBeCalledWith(payload, {});
+ expect(result).toEqual({ saved_objects: savedObjects });
+ });
+
+ it(`Should throw error when the objects do not exist`, async () => {
+ mockedClient.bulkGet.mockRejectedValueOnce(
+ SavedObjectsErrorHelpers.createGenericNotFoundError()
+ );
+ expect(wrapperClient.bulkGet(payload)).rejects.toMatchInlineSnapshot(`[Error: Not Found]`);
+ expect(mockedClient.bulkGet).toBeCalledWith(payload, {});
+ });
+
+ it(`Should throw error when the options.workspaces has more than one workspace.`, async () => {
+ expect(
+ wrapperClient.bulkGet(payload, { workspaces: ['foo', 'var'] })
+ ).rejects.toMatchInlineSnapshot(`[Error: Multiple workspace parameters: Bad Request]`);
+ expect(mockedClient.bulkGet).not.toBeCalled();
+ });
+
+ it(`Should bulkGet data source when user is data source admin`, async () => {
+ const workspaceIdConsumerWrapper = new WorkspaceIdConsumerWrapper(mockedWorkspaceClient);
+ const mockRequest = httpServerMock.createOpenSearchDashboardsRequest();
+ updateWorkspaceState(mockRequest, { isDataSourceAdmin: true, requestWorkspaceId: 'foo' });
+ const mockedWrapperClient = workspaceIdConsumerWrapper.wrapperFactory({
+ client: mockedClient,
+ typeRegistry: requestHandlerContext.savedObjects.typeRegistry,
+ request: mockRequest,
+ });
+
+ mockedClient.bulkGet.mockResolvedValueOnce({ saved_objects: savedObjects });
+ const result = await mockedWrapperClient.bulkGet(payload);
+ expect(mockedClient.bulkGet).toBeCalledWith(payload, {});
+ expect(result).toMatchInlineSnapshot(`
+ Object {
+ "saved_objects": Array [
+ Object {
+ "attributes": Object {
+ "description": "description",
+ },
+ "id": "dashboard_id",
+ "references": Array [
+ "reference_id",
+ ],
+ "type": "dashboard",
+ "workspaces": Array [
+ "foo",
+ ],
+ },
+ Object {
+ "attributes": Object {},
+ "error": Object {
+ "error": "Not Found",
+ "message": "Saved object [dashboard/dashboard_error_id] not found",
+ "statusCode": 404,
+ },
+ "id": "dashboard_error_id",
+ "references": Array [],
+ "type": "dashboard",
+ },
+ Object {
+ "attributes": Object {},
+ "error": Object {
+ "error": "Forbidden",
+ "message": "Saved object does not belong to the workspace",
+ "statusCode": 403,
+ },
+ "id": "visualization_id",
+ "references": Array [],
+ "type": "visualization",
+ },
+ Object {
+ "attributes": Object {},
+ "id": "config_id",
+ "references": Array [],
+ "type": "config",
+ },
+ Object {
+ "attributes": Object {},
+ "id": "workspace_id",
+ "references": Array [],
+ "type": "workspace",
+ },
+ Object {
+ "attributes": Object {},
+ "id": "global_data_source_id",
+ "references": Array [],
+ "type": "data-source",
+ },
+ Object {
+ "attributes": Object {},
+ "id": "data_source_id",
+ "references": Array [],
+ "type": "data-source",
+ "workspaces": Array [
+ "foo",
+ ],
+ },
+ ],
+ }
+ `);
+ });
+ });
});
diff --git a/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.ts b/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.ts
index 90820c835d47..b9edaecd2c9d 100644
--- a/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.ts
+++ b/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.ts
@@ -14,13 +14,27 @@ import {
OpenSearchDashboardsRequest,
SavedObjectsFindOptions,
SavedObjectsErrorHelpers,
+ SavedObjectsClientWrapperOptions,
+ SavedObject,
+ SavedObjectsBulkGetObject,
+ SavedObjectsBulkResponse,
} from '../../../../core/server';
import { IWorkspaceClientImpl } from '../types';
+import { validateIsWorkspaceDataSourceAndConnectionObjectType } from '../../common/utils';
const UI_SETTINGS_SAVED_OBJECTS_TYPE = 'config';
type WorkspaceOptions = Pick | undefined;
+const generateSavedObjectsForbiddenError = () =>
+ SavedObjectsErrorHelpers.decorateForbiddenError(
+ new Error(
+ i18n.translate('workspace.id_consumer.saved_objects.forbidden', {
+ defaultMessage: 'Saved object does not belong to the workspace',
+ })
+ )
+ );
+
export class WorkspaceIdConsumerWrapper {
private formatWorkspaceIdParams(
request: OpenSearchDashboardsRequest,
@@ -48,25 +62,100 @@ export class WorkspaceIdConsumerWrapper {
return type === UI_SETTINGS_SAVED_OBJECTS_TYPE;
}
+ private async checkWorkspacesExist(
+ workspaces: SavedObject['workspaces'] | null,
+ wrapperOptions: SavedObjectsClientWrapperOptions
+ ) {
+ if (workspaces?.length) {
+ let invalidWorkspaces: string[] = [];
+ // If only has one workspace, we should use get to optimize performance
+ if (workspaces.length === 1) {
+ const workspaceGet = await this.workspaceClient.get(
+ { request: wrapperOptions.request },
+ workspaces[0]
+ );
+ if (!workspaceGet.success) {
+ invalidWorkspaces = [workspaces[0]];
+ }
+ } else {
+ const workspaceList = await this.workspaceClient.list(
+ {
+ request: wrapperOptions.request,
+ },
+ {
+ perPage: 9999,
+ }
+ );
+ if (workspaceList.success) {
+ const workspaceIdsSet = new Set(
+ workspaceList.result.workspaces.map((workspace) => workspace.id)
+ );
+ invalidWorkspaces = workspaces.filter(
+ (targetWorkspace) => !workspaceIdsSet.has(targetWorkspace)
+ );
+ }
+ }
+
+ if (invalidWorkspaces.length > 0) {
+ throw SavedObjectsErrorHelpers.decorateBadRequestError(
+ new Error(
+ i18n.translate('workspace.id_consumer.invalid', {
+ defaultMessage: 'Exist invalid workspaces',
+ })
+ )
+ );
+ }
+ }
+ }
+
+ private validateObjectInAWorkspace(
+ object: SavedObject,
+ workspace: string,
+ request: OpenSearchDashboardsRequest
+ ) {
+ // Keep the original object error
+ if (!!object?.error) {
+ return true;
+ }
+ // Data source is a workspace level object, validate if the request has access to the data source within the requested workspace.
+ if (validateIsWorkspaceDataSourceAndConnectionObjectType(object.type)) {
+ if (!!getWorkspaceState(request).isDataSourceAdmin) {
+ return true;
+ }
+ // Deny access if the object is a global data source (no workspaces assigned)
+ if (!object.workspaces || object.workspaces.length === 0) {
+ return false;
+ }
+ }
+ /*
+ * Allow access if the requested workspace matches one of the object's assigned workspaces
+ * This ensures that the user can only access data sources within their current workspace
+ */
+ if (object.workspaces && object.workspaces.length > 0) {
+ return object.workspaces.includes(workspace);
+ }
+ // Allow access if the object is a global object (object.workspaces is null/[])
+ return true;
+ }
+
public wrapperFactory: SavedObjectsClientWrapperFactory = (wrapperOptions) => {
return {
...wrapperOptions.client,
- create: (type: string, attributes: T, options: SavedObjectsCreateOptions = {}) =>
- wrapperOptions.client.create(
- type,
- attributes,
- this.isConfigType(type)
- ? options
- : this.formatWorkspaceIdParams(wrapperOptions.request, options)
- ),
- bulkCreate: (
+ create: async (type: string, attributes: T, options: SavedObjectsCreateOptions = {}) => {
+ const finalOptions = this.isConfigType(type)
+ ? options
+ : this.formatWorkspaceIdParams(wrapperOptions.request, options);
+ await this.checkWorkspacesExist(finalOptions?.workspaces, wrapperOptions);
+ return wrapperOptions.client.create(type, attributes, finalOptions);
+ },
+ bulkCreate: async (
objects: Array>,
options: SavedObjectsCreateOptions = {}
- ) =>
- wrapperOptions.client.bulkCreate(
- objects,
- this.formatWorkspaceIdParams(wrapperOptions.request, options)
- ),
+ ) => {
+ const finalOptions = this.formatWorkspaceIdParams(wrapperOptions.request, options);
+ await this.checkWorkspacesExist(finalOptions?.workspaces, wrapperOptions);
+ return wrapperOptions.client.bulkCreate(objects, finalOptions);
+ },
checkConflicts: (
objects: SavedObjectsCheckConflictsObject[] = [],
options: SavedObjectsBaseOptions = {}
@@ -84,50 +173,65 @@ export class WorkspaceIdConsumerWrapper {
this.isConfigType(options.type as string) && options.sortField === 'buildNum'
? options
: this.formatWorkspaceIdParams(wrapperOptions.request, options);
- if (finalOptions.workspaces?.length) {
- let isAllTargetWorkspaceExisting = false;
- // If only has one workspace, we should use get to optimize performance
- if (finalOptions.workspaces.length === 1) {
- const workspaceGet = await this.workspaceClient.get(
- { request: wrapperOptions.request },
- finalOptions.workspaces[0]
- );
- if (workspaceGet.success) {
- isAllTargetWorkspaceExisting = true;
- }
- } else {
- const workspaceList = await this.workspaceClient.list(
- {
- request: wrapperOptions.request,
- },
- {
- perPage: 9999,
- }
- );
- if (workspaceList.success) {
- const workspaceIdsSet = new Set(
- workspaceList.result.workspaces.map((workspace) => workspace.id)
- );
- isAllTargetWorkspaceExisting = finalOptions.workspaces.every((targetWorkspace) =>
- workspaceIdsSet.has(targetWorkspace)
- );
- }
- }
+ await this.checkWorkspacesExist(finalOptions?.workspaces, wrapperOptions);
+ return wrapperOptions.client.find(finalOptions);
+ },
+ bulkGet: async (
+ objects: SavedObjectsBulkGetObject[] = [],
+ options: SavedObjectsBaseOptions = {}
+ ): Promise> => {
+ const { workspaces } = this.formatWorkspaceIdParams(wrapperOptions.request, options);
+ if (!!workspaces && workspaces.length > 1) {
+ // Version 2.18 does not support the passing of multiple workspaces.
+ throw SavedObjectsErrorHelpers.createBadRequestError('Multiple workspace parameters');
+ }
- if (!isAllTargetWorkspaceExisting) {
- throw SavedObjectsErrorHelpers.decorateBadRequestError(
- new Error(
- i18n.translate('workspace.id_consumer.invalid', {
- defaultMessage: 'Invalid workspaces',
- })
- )
- );
- }
+ const objectToBulkGet = await wrapperOptions.client.bulkGet(objects, options);
+
+ if (workspaces?.length === 1) {
+ return {
+ ...objectToBulkGet,
+ saved_objects: objectToBulkGet.saved_objects.map((object) => {
+ return this.validateObjectInAWorkspace(object, workspaces[0], wrapperOptions.request)
+ ? object
+ : {
+ id: object.id,
+ type: object.type,
+ attributes: {} as T,
+ references: [],
+ error: {
+ ...generateSavedObjectsForbiddenError().output.payload,
+ },
+ };
+ }),
+ };
}
- return wrapperOptions.client.find(finalOptions);
+
+ return objectToBulkGet;
+ },
+ get: async (
+ type: string,
+ id: string,
+ options: SavedObjectsBaseOptions = {}
+ ): Promise> => {
+ const { workspaces } = this.formatWorkspaceIdParams(wrapperOptions.request, options);
+ if (!!workspaces && workspaces.length > 1) {
+ // Version 2.18 does not support the passing of multiple workspaces.
+ throw SavedObjectsErrorHelpers.createBadRequestError('Multiple workspace parameters');
+ }
+
+ const objectToGet = await wrapperOptions.client.get(type, id, options);
+
+ if (
+ workspaces?.length === 1 &&
+ !this.validateObjectInAWorkspace(objectToGet, workspaces[0], wrapperOptions.request)
+ ) {
+ throw generateSavedObjectsForbiddenError();
+ }
+
+ // Allow access if no specific workspace is requested.
+ return objectToGet;
},
- bulkGet: wrapperOptions.client.bulkGet,
- get: wrapperOptions.client.get,
update: wrapperOptions.client.update,
bulkUpdate: wrapperOptions.client.bulkUpdate,
addToNamespaces: wrapperOptions.client.addToNamespaces,
diff --git a/src/plugins/workspace/server/saved_objects/workspace_saved_objects_client_wrapper.test.ts b/src/plugins/workspace/server/saved_objects/workspace_saved_objects_client_wrapper.test.ts
index e9f5c5c2a409..55098d6e2b27 100644
--- a/src/plugins/workspace/server/saved_objects/workspace_saved_objects_client_wrapper.test.ts
+++ b/src/plugins/workspace/server/saved_objects/workspace_saved_objects_client_wrapper.test.ts
@@ -652,127 +652,6 @@ describe('WorkspaceSavedObjectsClientWrapper', () => {
}
`);
});
-
- it('should validate data source or data connection workspace field', async () => {
- const { wrapper } = generateWorkspaceSavedObjectsClientWrapper();
- let errorCatched;
- try {
- await wrapper.get('data-source', 'workspace-1-data-source');
- } catch (e) {
- errorCatched = e;
- }
- expect(errorCatched?.message).toEqual(
- 'Invalid data source permission, please associate it to current workspace'
- );
-
- try {
- await wrapper.get('data-connection', 'workspace-1-data-connection');
- } catch (e) {
- errorCatched = e;
- }
- expect(errorCatched?.message).toEqual(
- 'Invalid data source permission, please associate it to current workspace'
- );
-
- let result = await wrapper.get('data-source', 'workspace-2-data-source');
- expect(result).toEqual(
- expect.objectContaining({
- attributes: {
- title: 'Workspace 2 data source',
- },
- id: 'workspace-2-data-source',
- type: 'data-source',
- workspaces: ['mock-request-workspace-id'],
- })
- );
- result = await wrapper.get('data-connection', 'workspace-2-data-connection');
- expect(result).toEqual(
- expect.objectContaining({
- attributes: {
- title: 'Workspace 2 data connection',
- },
- id: 'workspace-2-data-connection',
- type: 'data-connection',
- workspaces: ['mock-request-workspace-id'],
- })
- );
- });
-
- it('should not validate data source or data connection when not in workspace', async () => {
- const { wrapper, requestMock } = generateWorkspaceSavedObjectsClientWrapper();
- updateWorkspaceState(requestMock, { requestWorkspaceId: undefined });
- let result = await wrapper.get('data-source', 'workspace-1-data-source');
- expect(result).toEqual({
- type: DATA_SOURCE_SAVED_OBJECT_TYPE,
- id: 'workspace-1-data-source',
- attributes: { title: 'Workspace 1 data source' },
- workspaces: ['workspace-1'],
- references: [],
- });
- result = await wrapper.get('data-connection', 'workspace-1-data-connection');
- expect(result).toEqual({
- type: DATA_CONNECTION_SAVED_OBJECT_TYPE,
- id: 'workspace-1-data-connection',
- attributes: { title: 'Workspace 1 data connection' },
- workspaces: ['workspace-1'],
- references: [],
- });
- });
-
- it('should not validate data source when user is data source admin', async () => {
- const { wrapper } = generateWorkspaceSavedObjectsClientWrapper(DATASOURCE_ADMIN);
- const result = await wrapper.get('data-source', 'workspace-1-data-source');
- expect(result).toEqual({
- type: DATA_SOURCE_SAVED_OBJECT_TYPE,
- id: 'workspace-1-data-source',
- attributes: { title: 'Workspace 1 data source' },
- workspaces: ['workspace-1'],
- references: [],
- });
- });
-
- it('should throw permission error when tried to access a global data source or data connection', async () => {
- const { wrapper } = generateWorkspaceSavedObjectsClientWrapper();
- let errorCatched;
- try {
- await wrapper.get('data-source', 'global-data-source');
- } catch (e) {
- errorCatched = e;
- }
- expect(errorCatched?.message).toEqual(
- 'Invalid data source permission, please associate it to current workspace'
- );
- try {
- await wrapper.get('data-connection', 'global-data-connection');
- } catch (e) {
- errorCatched = e;
- }
- expect(errorCatched?.message).toEqual(
- 'Invalid data source permission, please associate it to current workspace'
- );
- });
-
- it('should throw permission error when tried to access a empty workspaces global data source or data connection', async () => {
- const { wrapper, requestMock } = generateWorkspaceSavedObjectsClientWrapper();
- updateWorkspaceState(requestMock, { requestWorkspaceId: undefined });
- let errorCatched;
- try {
- await wrapper.get('data-source', 'global-data-source-empty-workspaces');
- } catch (e) {
- errorCatched = e;
- }
- expect(errorCatched?.message).toEqual(
- 'Invalid data source permission, please associate it to current workspace'
- );
- try {
- await wrapper.get('data-connection', 'global-data-connection-empty-workspaces');
- } catch (e) {
- errorCatched = e;
- }
- expect(errorCatched?.message).toEqual(
- 'Invalid data source permission, please associate it to current workspace'
- );
- });
});
describe('bulk get', () => {
it("should call permission validate with object's workspace and throw permission error", async () => {
@@ -837,166 +716,6 @@ describe('WorkspaceSavedObjectsClientWrapper', () => {
{}
);
});
- it('should validate data source or data connection workspace field', async () => {
- const { wrapper } = generateWorkspaceSavedObjectsClientWrapper();
- let errorCatched;
- try {
- await wrapper.bulkGet([
- {
- type: 'data-source',
- id: 'workspace-1-data-source',
- },
- ]);
- } catch (e) {
- errorCatched = e;
- }
- expect(errorCatched?.message).toEqual(
- 'Invalid data source permission, please associate it to current workspace'
- );
-
- try {
- await wrapper.bulkGet([
- {
- type: 'data-connection',
- id: 'workspace-1-data-connection',
- },
- ]);
- } catch (e) {
- errorCatched = e;
- }
- expect(errorCatched?.message).toEqual(
- 'Invalid data source permission, please associate it to current workspace'
- );
-
- let result = await await wrapper.bulkGet([
- {
- type: 'data-source',
- id: 'workspace-2-data-source',
- },
- ]);
- expect(result).toEqual({
- saved_objects: [
- {
- attributes: {
- title: 'Workspace 2 data source',
- },
- id: 'workspace-2-data-source',
- type: 'data-source',
- workspaces: ['mock-request-workspace-id'],
- references: [],
- },
- ],
- });
-
- result = await await wrapper.bulkGet([
- {
- type: 'data-connection',
- id: 'workspace-2-data-connection',
- },
- ]);
- expect(result).toEqual({
- saved_objects: [
- {
- attributes: {
- title: 'Workspace 2 data connection',
- },
- id: 'workspace-2-data-connection',
- type: 'data-connection',
- workspaces: ['mock-request-workspace-id'],
- references: [],
- },
- ],
- });
- });
-
- it('should not validate data source or data connection when not in workspace', async () => {
- const { wrapper, requestMock } = generateWorkspaceSavedObjectsClientWrapper();
- updateWorkspaceState(requestMock, { requestWorkspaceId: undefined });
- let result = await wrapper.bulkGet([
- {
- type: 'data-source',
- id: 'workspace-1-data-source',
- },
- ]);
- expect(result).toEqual({
- saved_objects: [
- {
- attributes: {
- title: 'Workspace 1 data source',
- },
- id: 'workspace-1-data-source',
- type: 'data-source',
- workspaces: ['workspace-1'],
- references: [],
- },
- ],
- });
-
- result = await wrapper.bulkGet([
- {
- type: 'data-connection',
- id: 'workspace-1-data-connection',
- },
- ]);
- expect(result).toEqual({
- saved_objects: [
- {
- attributes: {
- title: 'Workspace 1 data connection',
- },
- id: 'workspace-1-data-connection',
- type: 'data-connection',
- workspaces: ['workspace-1'],
- references: [],
- },
- ],
- });
- });
-
- it('should throw permission error when tried to bulk get global data source or data connection', async () => {
- const { wrapper, requestMock } = generateWorkspaceSavedObjectsClientWrapper();
- updateWorkspaceState(requestMock, { requestWorkspaceId: undefined });
- let errorCatched;
- try {
- await wrapper.bulkGet([{ type: 'data-source', id: 'global-data-source' }]);
- } catch (e) {
- errorCatched = e;
- }
- expect(errorCatched?.message).toEqual(
- 'Invalid data source permission, please associate it to current workspace'
- );
- try {
- await wrapper.bulkGet([{ type: 'data-connection', id: 'global-data-connection' }]);
- } catch (e) {
- errorCatched = e;
- }
- expect(errorCatched?.message).toEqual(
- 'Invalid data source permission, please associate it to current workspace'
- );
- });
-
- it('should throw permission error when tried to bulk get a empty workspace global data source or data connection', async () => {
- const { wrapper, requestMock } = generateWorkspaceSavedObjectsClientWrapper();
- updateWorkspaceState(requestMock, { requestWorkspaceId: undefined });
- let errorCatched;
- try {
- await wrapper.bulkGet([
- { type: 'data-source', id: 'global-data-source-empty-workspaces' },
- ]);
- } catch (e) {
- errorCatched = e;
- }
- expect(errorCatched?.message).toEqual(
- 'Invalid data source permission, please associate it to current workspace'
- );
- try {
- await wrapper.bulkGet([
- { type: 'data-connection', id: 'global-data-connection-empty-workspaces' },
- ]);
- } catch (e) {
- errorCatched = e;
- }
- });
});
describe('find', () => {
it('should call client.find with consistent params when ACLSearchParams and workspaceOperator not provided', async () => {
diff --git a/src/plugins/workspace/server/saved_objects/workspace_saved_objects_client_wrapper.ts b/src/plugins/workspace/server/saved_objects/workspace_saved_objects_client_wrapper.ts
index 162f7a488ad2..0adc27b39a43 100644
--- a/src/plugins/workspace/server/saved_objects/workspace_saved_objects_client_wrapper.ts
+++ b/src/plugins/workspace/server/saved_objects/workspace_saved_objects_client_wrapper.ts
@@ -61,15 +61,6 @@ const generateSavedObjectsPermissionError = () =>
)
);
-const generateDataSourcePermissionError = () =>
- SavedObjectsErrorHelpers.decorateForbiddenError(
- new Error(
- i18n.translate('workspace.saved_objects.data_source.invalidate', {
- defaultMessage: 'Invalid data source permission, please associate it to current workspace',
- })
- )
- );
-
const generateOSDAdminPermissionError = () =>
SavedObjectsErrorHelpers.decorateForbiddenError(
new Error(
@@ -205,32 +196,6 @@ export class WorkspaceSavedObjectsClientWrapper {
return hasPermission;
}
- // Data source is a workspace level object, validate if the request has access to the data source within the requested workspace.
- private validateDataSourcePermissions = (
- object: SavedObject,
- request: OpenSearchDashboardsRequest
- ) => {
- const requestWorkspaceId = getWorkspaceState(request).requestWorkspaceId;
- // Deny access if the object is a global data source (no workspaces assigned)
- if (!object.workspaces || object.workspaces.length === 0) {
- return false;
- }
- /**
- * Allow access if no specific workspace is requested.
- * This typically occurs when retrieving data sources or performing operations
- * that don't require a specific workspace, such as pages within the
- * Data Administration navigation group that include a data source picker.
- */
- if (!requestWorkspaceId) {
- return true;
- }
- /*
- * Allow access if the requested workspace matches one of the object's assigned workspaces
- * This ensures that the user can only access data sources within their current workspace
- */
- return object.workspaces.includes(requestWorkspaceId);
- };
-
private getWorkspaceTypeEnabledClient(request: OpenSearchDashboardsRequest) {
return this.getScopedClient?.(request, {
includedHiddenTypes: [WORKSPACE_TYPE],
@@ -462,21 +427,6 @@ export class WorkspaceSavedObjectsClientWrapper {
): Promise> => {
const objectToGet = await wrapperOptions.client.get(type, id, options);
- if (validateIsWorkspaceDataSourceAndConnectionObjectType(objectToGet.type)) {
- if (isDataSourceAdmin) {
- ACLAuditor?.increment(ACLAuditorStateKey.VALIDATE_SUCCESS, 1);
- return objectToGet;
- }
- const hasPermission = this.validateDataSourcePermissions(
- objectToGet,
- wrapperOptions.request
- );
- if (!hasPermission) {
- ACLAuditor?.increment(ACLAuditorStateKey.VALIDATE_FAILURE, 1);
- throw generateDataSourcePermissionError();
- }
- }
-
if (
!(await this.validateWorkspacesAndSavedObjectsPermissions(
objectToGet,
@@ -504,14 +454,6 @@ export class WorkspaceSavedObjectsClientWrapper {
);
for (const object of objectToBulkGet.saved_objects) {
- if (validateIsWorkspaceDataSourceAndConnectionObjectType(object.type)) {
- const hasPermission = this.validateDataSourcePermissions(object, wrapperOptions.request);
- if (!hasPermission) {
- ACLAuditor?.increment(ACLAuditorStateKey.VALIDATE_FAILURE, 1);
- throw generateDataSourcePermissionError();
- }
- }
-
if (
!(await this.validateWorkspacesAndSavedObjectsPermissions(
object,
diff --git a/yarn.lock b/yarn.lock
index 5b3dec208a45..69ddeeeabd5b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1294,7 +1294,7 @@
resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9"
integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==
-"@cypress/request@^2.88.10":
+"@cypress/request@2.88.12":
version "2.88.12"
resolved "https://registry.yarnpkg.com/@cypress/request/-/request-2.88.12.tgz#ba4911431738494a85e93fb04498cb38bc55d590"
integrity sha512-tOn+0mDZxASFM+cuAP9szGUGPI1HwWVSvdzm7V4cCsPdFTx6qMj29CwaQmRAMIEhORIUBFBsYROYJcveK4uOjA==
@@ -2594,15 +2594,15 @@
version "1.0.6"
resolved "https://github.com/opensearch-project/opensearch-dashboards-test-library/archive/refs/tags/1.0.6.tar.gz#f2f489832a75191e243c6d2b42d49047265d9ce3"
-"@opensearch-project/opensearch@^2.9.0":
- version "2.9.0"
- resolved "https://registry.yarnpkg.com/@opensearch-project/opensearch/-/opensearch-2.9.0.tgz#319b4d174540b6d000c31477a56618e5054c6fcb"
- integrity sha512-BXPWSBME1rszZ8OvtBVQ9F6kLiZSENDSFPawbPa1fv0GouuQfWxkKSI9TcnfGLp869fgLTEIfeC5Qexd4RbAYw==
+"@opensearch-project/opensearch@^2.13.0":
+ version "2.13.0"
+ resolved "https://registry.yarnpkg.com/@opensearch-project/opensearch/-/opensearch-2.13.0.tgz#e60c1a3a3dd059562f1d901aa8d3659035cb1781"
+ integrity sha512-Bu3jJ7pKzumbMMeefu7/npAWAvFu5W9SlbBow1ulhluqUpqc7QoXe0KidDrMy7Dy3BQrkI6llR3cWL4lQTZOFw==
dependencies:
aws4 "^1.11.0"
debug "^4.3.1"
hpagent "^1.2.0"
- json11 "^1.0.4"
+ json11 "^2.0.0"
ms "^2.1.3"
secure-json-parse "^2.4.0"
@@ -2929,6 +2929,11 @@
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82"
integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==
+"@tootallnate/quickjs-emscripten@^0.23.0":
+ version "0.23.0"
+ resolved "https://registry.yarnpkg.com/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz#db4ecfd499a9765ab24002c3b696d02e6d32a12c"
+ integrity sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==
+
"@tsd/typescript@~4.7.3":
version "4.7.4"
resolved "https://registry.yarnpkg.com/@tsd/typescript/-/typescript-4.7.4.tgz#f1e4e6c3099a174a0cb7aa51cf53f34f6494e528"
@@ -3534,7 +3539,7 @@
dependencies:
"@types/node" "*"
-"@types/node@*", "@types/node@12.20.24", "@types/node@16.9.1", "@types/node@^14.14.31", "@types/node@~18.7.0":
+"@types/node@*", "@types/node@12.20.24", "@types/node@16.9.1", "@types/node@^16.18.39", "@types/node@~18.7.0":
version "18.7.23"
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.7.23.tgz#75c580983846181ebe5f4abc40fe9dfb2d65665f"
integrity sha512-DWNcCHolDq0ZKGizjx2DZjR/PqsYwAcYUJmfMWqtVU2MBMG5Mo+xFZrhGId5r/O5HOuMPyQEcM6KUBp5lBZZBg==
@@ -4367,6 +4372,13 @@ agent-base@6:
dependencies:
debug "4"
+agent-base@^7.0.2, agent-base@^7.1.0, agent-base@^7.1.1:
+ version "7.1.1"
+ resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.1.tgz#bdbded7dfb096b751a2a087eeeb9664725b2e317"
+ integrity sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==
+ dependencies:
+ debug "^4.3.4"
+
agentkeepalive@^3.4.1, agentkeepalive@^4.2.1, agentkeepalive@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.5.0.tgz#2673ad1389b3c418c5a20c5d7364f93ca04be923"
@@ -4815,6 +4827,13 @@ ast-types-flow@^0.0.7:
resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.7.tgz#f70b735c6bca1a5c9c22d982c3e39e7feba3bdad"
integrity sha1-9wtzXGvKGlycItmCw+Oef+ujva0=
+ast-types@^0.13.4:
+ version "0.13.4"
+ resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.13.4.tgz#ee0d77b343263965ecc3fb62da16e7222b2b6782"
+ integrity sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==
+ dependencies:
+ tslib "^2.0.1"
+
astral-regex@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9"
@@ -4927,7 +4946,7 @@ axe-core@^4.0.2, axe-core@^4.3.5:
resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.4.1.tgz#7dbdc25989298f9ad006645cd396782443757413"
integrity sha512-gd1kmb21kwNuWr6BQz8fv6GNECPBnUasepcoLbekws23NVBLODdsClRZ+bQ8+9Uomf3Sm3+Vwn0oYG9NvwnJCw==
-axios@^1.6.1, axios@^1.6.5:
+axios@^1.6.1:
version "1.7.7"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.7.tgz#2f554296f9892a72ac8d8e4c5b79c14a91d0a47f"
integrity sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==
@@ -4936,6 +4955,15 @@ axios@^1.6.1, axios@^1.6.5:
form-data "^4.0.0"
proxy-from-env "^1.1.0"
+axios@^1.7.4:
+ version "1.7.8"
+ resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.8.tgz#1997b1496b394c21953e68c14aaa51b7b5de3d6e"
+ integrity sha512-Uu0wb7KNqK2t5K+YQyVCLM76prD5sRFjKHbJYCP1J7JFGEQ6nN7HWn9+04LAeiJ3ji54lgS/gZCH1oxyrf1SPw==
+ dependencies:
+ follow-redirects "^1.15.6"
+ form-data "^4.0.0"
+ proxy-from-env "^1.1.0"
+
axobject-query@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be"
@@ -5123,6 +5151,11 @@ basic-auth@^2.0.1:
dependencies:
safe-buffer "5.1.2"
+basic-ftp@^5.0.2:
+ version "5.0.5"
+ resolved "https://registry.yarnpkg.com/basic-ftp/-/basic-ftp-5.0.5.tgz#14a474f5fffecca1f4f406f1c26b18f800225ac0"
+ integrity sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==
+
batch-processor@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/batch-processor/-/batch-processor-1.0.0.tgz#75c95c32b748e0850d10c2b168f6bdbe9891ace8"
@@ -5492,6 +5525,17 @@ call-bind@^1.0.0, call-bind@^1.0.2, call-bind@^1.0.4, call-bind@^1.0.5:
get-intrinsic "^1.2.1"
set-function-length "^1.1.1"
+call-bind@^1.0.7:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9"
+ integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==
+ dependencies:
+ es-define-property "^1.0.0"
+ es-errors "^1.3.0"
+ function-bind "^1.1.2"
+ get-intrinsic "^1.2.4"
+ set-function-length "^1.2.1"
+
callsites@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
@@ -5710,16 +5754,16 @@ chrome-trace-event@^1.0.2:
resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac"
integrity sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==
-chromedriver@^121.0.1:
- version "121.0.2"
- resolved "https://registry.yarnpkg.com/chromedriver/-/chromedriver-121.0.2.tgz#208909a61e9d510913107ea6faf34bcdd72cdced"
- integrity sha512-58MUSCEE3oB3G3Y/Jo3URJ2Oa1VLHcVBufyYt7vNfGrABSJm7ienQLF9IQ8LPDlPVgLUXt2OBfggK3p2/SlEBg==
+chromedriver@^131.0.1:
+ version "131.0.1"
+ resolved "https://registry.yarnpkg.com/chromedriver/-/chromedriver-131.0.1.tgz#bfbf47f6c2ad7a65c154ff47d321bd8c33b52a77"
+ integrity sha512-LHRh+oaNU1WowJjAkWsviN8pTzQYJDbv/FvJyrQ7XhjKdIzVh/s3GV1iU7IjMTsxIQnBsTjx+9jWjzCWIXC7ug==
dependencies:
"@testim/chrome-version" "^1.1.4"
- axios "^1.6.5"
+ axios "^1.7.4"
compare-versions "^6.1.0"
extract-zip "^2.0.1"
- https-proxy-agent "^5.0.1"
+ proxy-agent "^6.4.0"
proxy-from-env "^1.1.0"
tcp-port-used "^1.0.2"
@@ -6017,11 +6061,16 @@ commander@^4.0.1:
resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068"
integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==
-commander@^5.0.0, commander@^5.1.0:
+commander@^5.0.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae"
integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==
+commander@^6.2.1:
+ version "6.2.1"
+ resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c"
+ integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==
+
comment-stripper@^0.0.4:
version "0.0.4"
resolved "https://registry.yarnpkg.com/comment-stripper/-/comment-stripper-0.0.4.tgz#e8d61366d362779ea225c764f05cca6c950f8a2c"
@@ -6498,14 +6547,14 @@ cyclist@^1.0.1:
resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9"
integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=
-cypress@9.5.4:
- version "9.5.4"
- resolved "https://registry.yarnpkg.com/cypress/-/cypress-9.5.4.tgz#49d9272f62eba12f2314faf29c2a865610e87550"
- integrity sha512-6AyJAD8phe7IMvOL4oBsI9puRNOWxZjl8z1lgixJMcgJ85JJmyKeP6uqNA0dI1z14lmJ7Qklf2MOgP/xdAqJ/Q==
+cypress@12.17.4:
+ version "12.17.4"
+ resolved "https://registry.yarnpkg.com/cypress/-/cypress-12.17.4.tgz#b4dadf41673058493fa0d2362faa3da1f6ae2e6c"
+ integrity sha512-gAN8Pmns9MA5eCDFSDJXWKUpaL3IDd89N9TtIupjYnzLSmlpVr+ZR+vb4U/qaMp+lB6tBvAmt7504c3Z4RU5KQ==
dependencies:
- "@cypress/request" "^2.88.10"
+ "@cypress/request" "2.88.12"
"@cypress/xvfb" "^1.2.4"
- "@types/node" "^14.14.31"
+ "@types/node" "^16.18.39"
"@types/sinonjs__fake-timers" "8.1.1"
"@types/sizzle" "^2.3.2"
arch "^2.2.0"
@@ -6517,12 +6566,12 @@ cypress@9.5.4:
check-more-types "^2.24.0"
cli-cursor "^3.1.0"
cli-table3 "~0.6.1"
- commander "^5.1.0"
+ commander "^6.2.1"
common-tags "^1.8.0"
dayjs "^1.10.4"
- debug "^4.3.2"
+ debug "^4.3.4"
enquirer "^2.3.6"
- eventemitter2 "^6.4.3"
+ eventemitter2 "6.4.7"
execa "4.1.0"
executable "^4.1.1"
extract-zip "2.0.1"
@@ -6535,12 +6584,13 @@ cypress@9.5.4:
listr2 "^3.8.3"
lodash "^4.17.21"
log-symbols "^4.0.0"
- minimist "^1.2.6"
+ minimist "^1.2.8"
ospath "^1.2.2"
pretty-bytes "^5.6.0"
+ process "^0.11.10"
proxy-from-env "1.0.0"
request-progress "^3.0.0"
- semver "^7.3.2"
+ semver "^7.5.3"
supports-color "^8.1.1"
tmp "~0.2.1"
untildify "^4.0.0"
@@ -6769,6 +6819,11 @@ dashify@^0.1.0:
resolved "https://registry.yarnpkg.com/dashify/-/dashify-0.1.0.tgz#107daf9cca5e326e30a8b39ffa5048b6684922ea"
integrity sha1-EH2vnMpeMm4wqLOf+lBItmhJIuo=
+data-uri-to-buffer@^6.0.2:
+ version "6.0.2"
+ resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz#8a58bb67384b261a38ef18bea1810cb01badd28b"
+ integrity sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==
+
data-urls@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-2.0.0.tgz#156485a72963a970f5d5821aaf642bef2bf2db9b"
@@ -6953,6 +7008,15 @@ define-data-property@^1.0.1, define-data-property@^1.1.1:
gopd "^1.0.1"
has-property-descriptors "^1.0.0"
+define-data-property@^1.1.4:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e"
+ integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==
+ dependencies:
+ es-define-property "^1.0.0"
+ es-errors "^1.3.0"
+ gopd "^1.0.1"
+
define-properties@^1.1.3, define-properties@^1.1.4, define-properties@^1.2.0:
version "1.2.1"
resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c"
@@ -6967,6 +7031,15 @@ defined@^1.0.0:
resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693"
integrity sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=
+degenerator@^5.0.0:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/degenerator/-/degenerator-5.0.1.tgz#9403bf297c6dad9a1ece409b37db27954f91f2f5"
+ integrity sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==
+ dependencies:
+ ast-types "^0.13.4"
+ escodegen "^2.1.0"
+ esprima "^4.0.1"
+
del-cli@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/del-cli/-/del-cli-3.0.1.tgz#2d27ff260204b5104cadeda86f78f180a4ebe89a"
@@ -7673,6 +7746,18 @@ es-array-method-boxes-properly@^1.0.0:
resolved "https://registry.yarnpkg.com/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz#873f3e84418de4ee19c5be752990b2e44718d09e"
integrity sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==
+es-define-property@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845"
+ integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==
+ dependencies:
+ get-intrinsic "^1.2.4"
+
+es-errors@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f"
+ integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==
+
es-get-iterator@^1.1.1:
version "1.1.2"
resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.2.tgz#9234c54aba713486d7ebde0220864af5e2b283f7"
@@ -7808,6 +7893,17 @@ escodegen@^2.0.0:
optionalDependencies:
source-map "~0.6.1"
+escodegen@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-2.1.0.tgz#ba93bbb7a43986d29d6041f99f5262da773e2e17"
+ integrity sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==
+ dependencies:
+ esprima "^4.0.1"
+ estraverse "^5.2.0"
+ esutils "^2.0.2"
+ optionalDependencies:
+ source-map "~0.6.1"
+
eslint-config-prettier@^6.11.0:
version "6.15.0"
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-6.15.0.tgz#7f93f6cb7d45a92f1537a70ecc06366e1ac6fed9"
@@ -8197,10 +8293,10 @@ event-target-shim@^5.0.0:
resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789"
integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==
-eventemitter2@^6.4.3:
- version "6.4.9"
- resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.4.9.tgz#41f2750781b4230ed58827bc119d293471ecb125"
- integrity sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg==
+eventemitter2@6.4.7:
+ version "6.4.7"
+ resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.4.7.tgz#a7f6c4d7abf28a14c1ef3442f21cb306a054271d"
+ integrity sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==
eventemitter2@~0.4.13:
version "0.4.14"
@@ -8822,6 +8918,15 @@ fs-constants@^1.0.0:
resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad"
integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==
+fs-extra@^11.2.0:
+ version "11.2.0"
+ resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.2.0.tgz#e70e17dfad64232287d01929399e0ea7c86b0e5b"
+ integrity sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==
+ dependencies:
+ graceful-fs "^4.2.0"
+ jsonfile "^6.0.1"
+ universalify "^2.0.0"
+
fs-extra@^9.1.0:
version "9.1.0"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d"
@@ -8937,6 +9042,17 @@ get-intrinsic@^1.0.1, get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@
has-symbols "^1.0.3"
hasown "^2.0.0"
+get-intrinsic@^1.2.4:
+ version "1.2.4"
+ resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd"
+ integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==
+ dependencies:
+ es-errors "^1.3.0"
+ function-bind "^1.1.2"
+ has-proto "^1.0.1"
+ has-symbols "^1.0.3"
+ hasown "^2.0.0"
+
get-nonce@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/get-nonce/-/get-nonce-1.0.1.tgz#fdf3f0278073820d2ce9426c18f07481b1e0cdf3"
@@ -8977,6 +9093,16 @@ get-symbol-description@^1.0.0:
call-bind "^1.0.2"
get-intrinsic "^1.1.1"
+get-uri@^6.0.1:
+ version "6.0.3"
+ resolved "https://registry.yarnpkg.com/get-uri/-/get-uri-6.0.3.tgz#0d26697bc13cf91092e519aa63aa60ee5b6f385a"
+ integrity sha512-BzUrJBS9EcUb4cFol8r4W3v1cPsSyajLSthNkz5BxbpDcHN5tIrM10E2eNvfnvBn3DaT3DUgx0OpsBKkaOpanw==
+ dependencies:
+ basic-ftp "^5.0.2"
+ data-uri-to-buffer "^6.0.2"
+ debug "^4.3.4"
+ fs-extra "^11.2.0"
+
get-value@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/get-value/-/get-value-3.0.1.tgz#5efd2a157f1d6a516d7524e124ac52d0a39ef5a8"
@@ -9479,6 +9605,13 @@ has-property-descriptors@^1.0.0:
dependencies:
get-intrinsic "^1.1.1"
+has-property-descriptors@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854"
+ integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==
+ dependencies:
+ es-define-property "^1.0.0"
+
has-proto@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.1.tgz#1885c1305538958aff469fef37937c22795408e0"
@@ -9803,6 +9936,14 @@ http-proxy-agent@^4.0.1:
agent-base "6"
debug "4"
+http-proxy-agent@^7.0.0, http-proxy-agent@^7.0.1:
+ version "7.0.2"
+ resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz#9a8b1f246866c028509486585f62b8f2c18c270e"
+ integrity sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==
+ dependencies:
+ agent-base "^7.1.0"
+ debug "^4.3.4"
+
http-signature@~1.3.6:
version "1.3.6"
resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.3.6.tgz#cb6fbfdf86d1c974f343be94e87f7fc128662cf9"
@@ -9825,7 +9966,7 @@ https-browserify@^1.0.0:
resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73"
integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=
-https-proxy-agent@5.0.1, https-proxy-agent@^5.0.0, https-proxy-agent@^5.0.1:
+https-proxy-agent@5.0.1, https-proxy-agent@^5.0.0:
version "5.0.1"
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6"
integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==
@@ -9833,6 +9974,14 @@ https-proxy-agent@5.0.1, https-proxy-agent@^5.0.0, https-proxy-agent@^5.0.1:
agent-base "6"
debug "4"
+https-proxy-agent@^7.0.3, https-proxy-agent@^7.0.5:
+ version "7.0.5"
+ resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz#9e8b5013873299e11fab6fd548405da2d6c602b2"
+ integrity sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==
+ dependencies:
+ agent-base "^7.0.2"
+ debug "4"
+
human-signals@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3"
@@ -10128,6 +10277,14 @@ ip-address@^6.3.0:
lodash.repeat "4.1.0"
sprintf-js "1.1.2"
+ip-address@^9.0.5:
+ version "9.0.5"
+ resolved "https://registry.yarnpkg.com/ip-address/-/ip-address-9.0.5.tgz#117a960819b08780c3bd1f14ef3c1cc1d3f3ea5a"
+ integrity sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==
+ dependencies:
+ jsbn "1.1.0"
+ sprintf-js "^1.1.3"
+
ip-cidr@^2.1.0:
version "2.1.5"
resolved "https://registry.yarnpkg.com/ip-cidr/-/ip-cidr-2.1.5.tgz#67fd02ee001d6ac0f253a1d577e4170a8f7d480b"
@@ -11393,7 +11550,7 @@ json-stringify-safe@5.0.1, json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.
resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=
-json11@^1.0.4, json11@^2.0.0:
+json11@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/json11/-/json11-2.0.0.tgz#06c4ad0a40b50c5de99a87f6d3028593137e5641"
integrity sha512-VuKJKUSPEJape+daTm70Nx7vdcdorf4S6LCyN2z0jUVH4UrQ4ftXo2kC0bnHpCREmxHuHqCNVPA75BjI3CB6Ag==
@@ -12022,6 +12179,11 @@ lru-cache@^6.0.0:
dependencies:
yallist "^4.0.0"
+lru-cache@^7.14.1:
+ version "7.18.3"
+ resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.18.3.tgz#f793896e0fd0e954a59dfdd82f0773808df6aa89"
+ integrity sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==
+
lru-queue@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/lru-queue/-/lru-queue-0.1.0.tgz#2738bd9f0d3cf4f84490c5736c48699ac632cda3"
@@ -12418,6 +12580,11 @@ minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6:
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
+minimist@^1.2.8:
+ version "1.2.8"
+ resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
+ integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
+
minipass-collect@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/minipass-collect/-/minipass-collect-1.0.2.tgz#22b813bf745dc6edba2576b940022ad6edc8c617"
@@ -12724,6 +12891,11 @@ nested-error-stacks@^2.0.0, nested-error-stacks@^2.1.0:
resolved "https://registry.yarnpkg.com/nested-error-stacks/-/nested-error-stacks-2.1.1.tgz#26c8a3cee6cc05fbcf1e333cd2fc3e003326c0b5"
integrity sha512-9iN1ka/9zmX1ZvLV9ewJYEk9h7RyRRtqdK0woXcqohu8EWIerfPUjYJPg0ULy0UqP7cslmdGc8xKDJcojlKiaw==
+netmask@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/netmask/-/netmask-2.0.2.tgz#8b01a07644065d536383835823bc52004ebac5e7"
+ integrity sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==
+
newtype-ts@^0.2.4:
version "0.2.4"
resolved "https://registry.yarnpkg.com/newtype-ts/-/newtype-ts-0.2.4.tgz#a02a8f160a3d179f871848d687a93de73a964a41"
@@ -13338,6 +13510,28 @@ p-try@^2.0.0:
resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
+pac-proxy-agent@^7.0.1:
+ version "7.0.2"
+ resolved "https://registry.yarnpkg.com/pac-proxy-agent/-/pac-proxy-agent-7.0.2.tgz#0fb02496bd9fb8ae7eb11cfd98386daaac442f58"
+ integrity sha512-BFi3vZnO9X5Qt6NRz7ZOaPja3ic0PhlsmCRYLOpN11+mWBCR6XJDqW5RF3j8jm4WGGQZtBA+bTfxYzeKW73eHg==
+ dependencies:
+ "@tootallnate/quickjs-emscripten" "^0.23.0"
+ agent-base "^7.0.2"
+ debug "^4.3.4"
+ get-uri "^6.0.1"
+ http-proxy-agent "^7.0.0"
+ https-proxy-agent "^7.0.5"
+ pac-resolver "^7.0.1"
+ socks-proxy-agent "^8.0.4"
+
+pac-resolver@^7.0.1:
+ version "7.0.1"
+ resolved "https://registry.yarnpkg.com/pac-resolver/-/pac-resolver-7.0.1.tgz#54675558ea368b64d210fd9c92a640b5f3b8abb6"
+ integrity sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==
+ dependencies:
+ degenerator "^5.0.0"
+ netmask "^2.0.2"
+
package-hash@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/package-hash/-/package-hash-4.0.0.tgz#3537f654665ec3cc38827387fc904c163c54f506"
@@ -13938,6 +14132,20 @@ property-information@^5.0.0, property-information@^5.3.0:
dependencies:
xtend "^4.0.0"
+proxy-agent@^6.4.0:
+ version "6.4.0"
+ resolved "https://registry.yarnpkg.com/proxy-agent/-/proxy-agent-6.4.0.tgz#b4e2dd51dee2b377748aef8d45604c2d7608652d"
+ integrity sha512-u0piLU+nCOHMgGjRbimiXmA9kM/L9EHh3zL81xCdp7m+Y2pHIsnmbdDoEDoAz5geaonNR6q6+yOPQs6n4T6sBQ==
+ dependencies:
+ agent-base "^7.0.2"
+ debug "^4.3.4"
+ http-proxy-agent "^7.0.1"
+ https-proxy-agent "^7.0.3"
+ lru-cache "^7.14.1"
+ pac-proxy-agent "^7.0.1"
+ proxy-from-env "^1.1.0"
+ socks-proxy-agent "^8.0.2"
+
proxy-from-env@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.0.0.tgz#33c50398f70ea7eb96d21f7b817630a55791c7ee"
@@ -14000,20 +14208,15 @@ pumpify@^1.3.3, pumpify@^1.3.5:
inherits "^2.0.3"
pump "^2.0.0"
-punycode@1.3.2:
- version "1.3.2"
- resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d"
- integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=
-
punycode@2.x.x, punycode@^2.1.0, punycode@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
-punycode@^1.2.4:
+punycode@^1.2.4, punycode@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
- integrity sha1-wNWmOycYgArY4esPpSachN1BhF4=
+ integrity sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==
qs@^6.11.0:
version "6.11.0"
@@ -14022,6 +14225,13 @@ qs@^6.11.0:
dependencies:
side-channel "^1.0.4"
+qs@^6.12.3:
+ version "6.13.0"
+ resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906"
+ integrity sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==
+ dependencies:
+ side-channel "^1.0.6"
+
qs@~6.10.3:
version "6.10.5"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.5.tgz#974715920a80ff6a262264acd2c7e6c2a53282b4"
@@ -15403,6 +15613,18 @@ set-function-length@^1.1.1:
gopd "^1.0.1"
has-property-descriptors "^1.0.0"
+set-function-length@^1.2.1:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449"
+ integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==
+ dependencies:
+ define-data-property "^1.1.4"
+ es-errors "^1.3.0"
+ function-bind "^1.1.2"
+ get-intrinsic "^1.2.4"
+ gopd "^1.0.1"
+ has-property-descriptors "^1.0.2"
+
set-function-name@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/set-function-name/-/set-function-name-2.0.1.tgz#12ce38b7954310b9f61faa12701620a0c882793a"
@@ -15490,6 +15712,16 @@ side-channel@^1.0.3, side-channel@^1.0.4:
get-intrinsic "^1.0.2"
object-inspect "^1.9.0"
+side-channel@^1.0.6:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2"
+ integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==
+ dependencies:
+ call-bind "^1.0.7"
+ es-errors "^1.3.0"
+ get-intrinsic "^1.2.4"
+ object-inspect "^1.13.1"
+
signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7:
version "3.0.7"
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9"
@@ -15581,6 +15813,28 @@ slide@~1.1.3:
resolved "https://registry.yarnpkg.com/slide/-/slide-1.1.6.tgz#56eb027d65b4d2dce6cb2e2d32c4d4afc9e1d707"
integrity sha1-VusCfWW00tzmyy4tMsTUr8nh1wc=
+smart-buffer@^4.2.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae"
+ integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==
+
+socks-proxy-agent@^8.0.2, socks-proxy-agent@^8.0.4:
+ version "8.0.4"
+ resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-8.0.4.tgz#9071dca17af95f483300316f4b063578fa0db08c"
+ integrity sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSG+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRw==
+ dependencies:
+ agent-base "^7.1.1"
+ debug "^4.3.4"
+ socks "^2.8.3"
+
+socks@^2.8.3:
+ version "2.8.3"
+ resolved "https://registry.yarnpkg.com/socks/-/socks-2.8.3.tgz#1ebd0f09c52ba95a09750afe3f3f9f724a800cb5"
+ integrity sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==
+ dependencies:
+ ip-address "^9.0.5"
+ smart-buffer "^4.2.0"
+
sonic-boom@^1.0.2:
version "1.4.1"
resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-1.4.1.tgz#d35d6a74076624f12e6f917ade7b9d75e918f53e"
@@ -15793,6 +16047,11 @@ sprintf-js@1.1.2, sprintf-js@^1.1.1:
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.2.tgz#da1765262bf8c0f571749f2ad6c26300207ae673"
integrity sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==
+sprintf-js@^1.1.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.3.tgz#4914b903a2f8b685d17fdf78a70e917e872e444a"
+ integrity sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==
+
sprintf-js@~1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
@@ -16899,6 +17158,11 @@ tslib@^2.0.0, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.2.0, tslib@^2.3.0, tslib@^2.3
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae"
integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==
+tslib@^2.0.1:
+ version "2.8.1"
+ resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
+ integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
+
tslib@~2.4.0:
version "2.4.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.1.tgz#0d0bfbaac2880b91e22df0768e55be9753a5b17e"
@@ -17350,21 +17614,13 @@ url-parse@^1.5.10, url-parse@^1.5.3:
querystringify "^2.1.1"
requires-port "^1.0.0"
-url@0.10.3:
- version "0.10.3"
- resolved "https://registry.yarnpkg.com/url/-/url-0.10.3.tgz#021e4d9c7705f21bbf37d03ceb58767402774c64"
- integrity sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ==
- dependencies:
- punycode "1.3.2"
- querystring "0.2.0"
-
-url@^0.11.0:
- version "0.11.0"
- resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1"
- integrity sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=
+url@0.10.3, url@^0.11.0, url@^0.11.4:
+ version "0.11.4"
+ resolved "https://registry.yarnpkg.com/url/-/url-0.11.4.tgz#adca77b3562d56b72746e76b330b7f27b6721f3c"
+ integrity sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==
dependencies:
- punycode "1.3.2"
- querystring "0.2.0"
+ punycode "^1.4.1"
+ qs "^6.12.3"
use-callback-ref@^1.2.3, use-callback-ref@^1.2.5:
version "1.2.5"