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/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/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/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/cypress.config.ts b/cypress.config.ts
index 67e7b4f5039b..d1363c2bf7ca 100644
--- a/cypress.config.ts
+++ b/cypress.config.ts
@@ -27,6 +27,7 @@ module.exports = defineConfig({
},
e2e: {
baseUrl: 'http://localhost:5601',
+ supportFile: 'cypress/support/e2e.{js,jsx,ts,tsx}',
specPattern: 'cypress/integration/**/*_spec.{js,jsx,ts,tsx}',
testIsolation: false,
setupNodeEvents,
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..02016d17e455
--- /dev/null
+++ b/cypress/integration/core_opensearch_dashboards/filter_for_value_spec.js
@@ -0,0 +1,60 @@
+/*
+ * 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');
+ DataExplorerPage.checkDocTableFirstFieldFilterForAndOutButton(false);
+ });
+ // filter actions should not exist for PPL
+ it('PPL', () => {
+ DataExplorerPage.selectIndexDataset('PPL');
+ DataExplorerPage.checkDocTableFirstFieldFilterForAndOutButton(false);
+ });
+ });
+ });
+});
diff --git a/cypress/support/e2e.js b/cypress/support/e2e.js
index fa35cf4214b4..fc5a308e4134 100644
--- a/cypress/support/e2e.js
+++ b/cypress/support/e2e.js
@@ -3,4 +3,17 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import '../utils/commands';
+import '../utils/commands.js';
+import '../utils/dashboards/data_explorer/commands.js';
+
+// Alternatively you can use CommonJS syntax:
+// require('./commands')
+
+const scopedHistoryNavigationError =
+ /^[^(ScopedHistory instance has fell out of navigation scope)]/;
+Cypress.on('uncaught:exception', (err) => {
+ /* returning false here prevents Cypress from failing the test */
+ if (scopedHistoryNavigationError.test(err.message)) {
+ return false;
+ }
+});
diff --git a/cypress/utils/commands.js b/cypress/utils/commands.js
index 56a1fd0cff0e..d30308576a80 100644
--- a/cypress/utils/commands.js
+++ b/cypress/utils/commands.js
@@ -3,13 +3,51 @@
* 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.
+ */
Cypress.Commands.add('getElementByTestId', (testId, options = {}) => {
return cy.get(`[data-test-subj="${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) => {
+ 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..f1d2f30605a2
--- /dev/null
+++ b/cypress/utils/dashboards/data_explorer/data_explorer_page.po.js
@@ -0,0 +1,265 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { DATA_EXPLORER_PAGE_ELEMENTS } from './elements.js';
+import { INDEX_CLUSTER_NAME, INDEX_NAME, 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 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 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);
+ }
+
+ /**
+ * 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) {
+ DataExplorerPage.getDatasetLanguageSelector().select(datasetLanguage);
+ switch (datasetLanguage) {
+ case 'PPL':
+ DataExplorerPage.selectDatasetTimeField("I don't want to use the time filter");
+ break;
+ }
+ DataExplorerPage.getDatasetSelectDataButton().click();
+ }
+
+ /**
+ * Select an index dataset.
+ * @param datasetLanguage Index supports "OpenSearch SQL" and "PPL"
+ */
+ static selectIndexDataset(datasetLanguage) {
+ DataExplorerPage.openDatasetExplorerWindow();
+ DataExplorerPage.getDatasetExplorerWindow().contains('Indexes').click();
+ DataExplorerPage.getDatasetExplorerWindow()
+ .contains(INDEX_CLUSTER_NAME, { timeout: 10000 })
+ .click();
+ DataExplorerPage.getDatasetExplorerWindow().contains(INDEX_NAME, { timeout: 10000 }).click();
+ DataExplorerPage.getDatasetExplorerNextButton().click();
+ DataExplorerPage.selectIndexDatasetLanguage(datasetLanguage);
+ }
+
+ /**
+ * 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');
+ }
+}
diff --git a/cypress/utils/dashboards/data_explorer/elements.js b/cypress/utils/dashboards/data_explorer/elements.js
new file mode 100644
index 000000000000..0ac45ad63b0c
--- /dev/null
+++ b/cypress/utils/dashboards/data_explorer/elements.js
@@ -0,0 +1,27 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+export const DATA_EXPLORER_PAGE_ELEMENTS = {
+ 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_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_SUBMIT_BUTTON: 'querySubmitButton',
+ GLOBAL_QUERY_EDITOR_FILTER_VALUE: 'globalFilterLabelValue',
+ GLOBAL_FILTER_BAR: 'globalFilterBar',
+};
diff --git a/package.json b/package.json
index 298ce702e854..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",
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/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/ui/dataset_selector/dataset_explorer.tsx b/src/plugins/data/public/ui/dataset_selector/dataset_explorer.tsx
index 7861dd836cd1..ec8e118157b1 100644
--- a/src/plugins/data/public/ui/dataset_selector/dataset_explorer.tsx
+++ b/src/plugins/data/public/ui/dataset_selector/dataset_explorer.tsx
@@ -152,6 +152,7 @@ export const DatasetExplorer = ({
(
-
+
{
:
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/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/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/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..29e49b00eab0 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;
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..84cf19bec50c 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
*
@@ -92,7 +101,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 4f21c30e1e52..69ddeeeabd5b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -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"
@@ -5525,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"
@@ -6997,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"
@@ -7726,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"
@@ -9010,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"
@@ -9562,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"
@@ -11500,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==
@@ -14158,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"
@@ -14180,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"
@@ -15561,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"
@@ -15648,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"
@@ -17540,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"