From dfd1962c65a05bf694bbca312f60b4a40a97648a Mon Sep 17 00:00:00 2001 From: Argus Li Date: Mon, 2 Dec 2024 16:19:58 -0800 Subject: [PATCH 01/31] Add utils to select a data source. --- cypress.config.js | 0 .../filter_for_value_spec.js | 25 +++++++ cypress/support/e2e.js | 6 ++ cypress/utils/commands.js | 27 +++++++ .../data_explorer_elements.js | 15 ++++ .../data_explorer_page/data_explorer_page.js | 74 +++++++++++++++++++ .../ui/dataset_selector/dataset_explorer.tsx | 1 + 7 files changed, 148 insertions(+) create mode 100644 cypress.config.js create mode 100644 cypress/integration/core_opensearch_dashboards/filter_for_value_spec.js create mode 100644 cypress/utils/data_explorer_page/data_explorer_elements.js create mode 100644 cypress/utils/data_explorer_page/data_explorer_page.js diff --git a/cypress.config.js b/cypress.config.js new file mode 100644 index 000000000000..e69de29bb2d1 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..096735339dc2 --- /dev/null +++ b/cypress/integration/core_opensearch_dashboards/filter_for_value_spec.js @@ -0,0 +1,25 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { MiscUtils } from '@opensearch-dashboards-test/opensearch-dashboards-test-library'; +import { DataExplorerPage } from '../../utils/data_explorer_page/data_explorer_page'; + +const miscUtils = new MiscUtils(cy); +const dataExplorerPage = new DataExplorerPage(cy); + +describe('filter for value spec', () => { + before(() => { + cy.localLogin(Cypress.env('username'), Cypress.env('password')); + miscUtils.visitPage('app/data-explorer/discover'); + }); + + beforeEach(() => { + dataExplorerPage.clickNewSearchButton(); + }); + + it('filter actions in table field', () => { + dataExplorerPage.selectIndexDataset('OpenSearch SQL'); + }); +}); diff --git a/cypress/support/e2e.js b/cypress/support/e2e.js index fa35cf4214b4..474948b47550 100644 --- a/cypress/support/e2e.js +++ b/cypress/support/e2e.js @@ -4,3 +4,9 @@ */ import '../utils/commands'; + +// eslint-disable-next-line no-unused-vars +Cypress.on('uncaught:exception', (_err) => { + // returning false here prevents Cypress from failing the test + return false; +}); diff --git a/cypress/utils/commands.js b/cypress/utils/commands.js index 56a1fd0cff0e..4a6d3bc261a1 100644 --- a/cypress/utils/commands.js +++ b/cypress/utils/commands.js @@ -3,6 +3,14 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { + MiscUtils, + LoginPage, +} from '@opensearch-dashboards-test/opensearch-dashboards-test-library'; + +const miscUtils = new MiscUtils(cy); +const loginPage = new LoginPage(cy); + // --- Typed commands -- Cypress.Commands.add('getElementByTestId', (testId, options = {}) => { @@ -13,3 +21,22 @@ Cypress.Commands.add('getElementsByTestIds', (testIds, options = {}) => { const selectors = [testIds].flat(Infinity).map((testId) => `[data-test-subj="${testId}"]`); return cy.get(selectors.join(','), options); }); + +Cypress.Commands.add('localLogin', (username, password) => { + miscUtils.visitPage('/app/login'); + loginPage.enterUserName(username); + loginPage.enterPassword(password); + loginPage.submit(); +}); + +Cypress.Commands.add('waitForLoader', () => { + const opts = { log: false }; + + Cypress.log({ + name: 'waitForPageLoad', + displayName: 'wait', + message: 'page load', + }); + cy.wait(Cypress.env('WAIT_FOR_LOADER_BUFFER_MS')); + cy.getElementByTestId('recentItemsSectionButton', opts); // Update to `homeLoader` once useExpandedHeader is enabled +}); diff --git a/cypress/utils/data_explorer_page/data_explorer_elements.js b/cypress/utils/data_explorer_page/data_explorer_elements.js new file mode 100644 index 000000000000..e1815e072eb9 --- /dev/null +++ b/cypress/utils/data_explorer_page/data_explorer_elements.js @@ -0,0 +1,15 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export const DATA_EXPLORER_PAGE_ELEMENTS = { + NEW_SEARCH_BUTTON: '[data-test-subj="discoverNewButton"]', + DATASET_SELECTOR_BUTTON: '[data-test-subj="datasetSelectorButton"]', + ALL_DATASETS_BUTTON: '[data-test-subj="datasetSelectorAdvancedButton"]', + DATASET_EXPLORER_WINDOW: '[data-test-subj="datasetExplorerWindow"]', + DATASET_SELECTOR_NEXT_BUTTON: '[data-test-subj="datasetSelectorNext"]', + DATASET_SELECTOR_LANGUAGE_SELECTOR: '[data-test-subj="advancedSelectorLanguageSelect"]', + DATASET_SELECTOR_TIME_SELECTOR: '[data-test-subj="advancedSelectorTimeFieldSelect"]', + DATASET_SELECTOR_SELECT_DATA_BUTTON: '[data-test-subj="advancedSelectorConfirmButton"]', +}; diff --git a/cypress/utils/data_explorer_page/data_explorer_page.js b/cypress/utils/data_explorer_page/data_explorer_page.js new file mode 100644 index 000000000000..121724907203 --- /dev/null +++ b/cypress/utils/data_explorer_page/data_explorer_page.js @@ -0,0 +1,74 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { DATA_EXPLORER_PAGE_ELEMENTS } from './data_explorer_elements.js'; + +export class DataExplorerPage { + constructor(inputTestRunner) { + this.testRunner = inputTestRunner; + } + + /** + * Click on the New Search button. + */ + clickNewSearchButton() { + this.testRunner + .get(DATA_EXPLORER_PAGE_ELEMENTS.NEW_SEARCH_BUTTON, { timeout: 10000 }) + .should('be.visible') + .click(); + } + + /** + * Open window to select Dataset + */ + openDatasetExplorerWindow() { + this.testRunner.get(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_SELECTOR_BUTTON).click(); + this.testRunner.get(DATA_EXPLORER_PAGE_ELEMENTS.ALL_DATASETS_BUTTON).click(); + } + + /** + * Select a Time Field in the Dataset Selector + */ + selectDatasetTimeField(timeField) { + this.testRunner + .get(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_SELECTOR_TIME_SELECTOR) + .select(timeField); + } + /** + * Select a language in the Dataset Selector + */ + selectDatasetLanguage(datasetLanguage) { + this.testRunner + .get(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_SELECTOR_LANGUAGE_SELECTOR) + .select(datasetLanguage); + switch (datasetLanguage) { + case 'PPL': + this.selectDatasetTimeField("I don't want to use the time filter"); + break; + } + this.testRunner.get(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_SELECTOR_SELECT_DATA_BUTTON).click(); + } + + /** + * Select an index dataset. + */ + selectIndexDataset(datasetLanguage) { + this.openDatasetExplorerWindow(); + this.testRunner + .get(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_EXPLORER_WINDOW) + .contains('Indexes') + .click(); + this.testRunner + .get(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_EXPLORER_WINDOW) + .contains(Cypress.env('INDEX_CLUSTER_NAME'), { timeout: 10000 }) + .click(); + this.testRunner + .get(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_EXPLORER_WINDOW) + .contains(Cypress.env('INDEX_NAME'), { timeout: 10000 }) + .click(); + this.testRunner.get(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_SELECTOR_NEXT_BUTTON).click(); + this.selectDatasetLanguage(datasetLanguage); + } +} 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 = ({
Date: Tue, 3 Dec 2024 21:58:09 -0800 Subject: [PATCH 02/31] Add support for different languages when creating dataset. Filter Out is almost complete, just working on removing the filter. --- .../filter_for_value_spec.js | 47 +++++-- cypress/utils/commands.js | 3 +- .../data_explorer_elements.js | 12 ++ .../data_explorer_page/data_explorer_page.js | 115 +++++++++++++++++- .../filter_editor/lib/filter_label.tsx | 6 +- 5 files changed, 171 insertions(+), 12 deletions(-) diff --git a/cypress/integration/core_opensearch_dashboards/filter_for_value_spec.js b/cypress/integration/core_opensearch_dashboards/filter_for_value_spec.js index 096735339dc2..490be6529a03 100644 --- a/cypress/integration/core_opensearch_dashboards/filter_for_value_spec.js +++ b/cypress/integration/core_opensearch_dashboards/filter_for_value_spec.js @@ -10,16 +10,49 @@ const miscUtils = new MiscUtils(cy); const dataExplorerPage = new DataExplorerPage(cy); describe('filter for value spec', () => { - before(() => { + beforeEach(() => { cy.localLogin(Cypress.env('username'), Cypress.env('password')); miscUtils.visitPage('app/data-explorer/discover'); - }); - - beforeEach(() => { dataExplorerPage.clickNewSearchButton(); }); - - it('filter actions in table field', () => { - dataExplorerPage.selectIndexDataset('OpenSearch SQL'); + describe('filter actions in table field', () => { + describe('index pattern dataset', () => { + // filter actions should not exist for DQL + it.only('DQL', () => { + dataExplorerPage.selectIndexPatternDataset('DQL'); + dataExplorerPage.setSearchDateRange('15', 'Years ago'); + dataExplorerPage.checkDocTableFirstFieldFilterForAndOutButton(true); + dataExplorerPage.checkDocTableFirstFieldFilterForButtonFiltersCorrectField(); + }); + // filter actions should not exist for PPL + it('Lucene', () => { + dataExplorerPage.selectIndexPatternDataset('Lucene'); + dataExplorerPage.setSearchDateRange('15', 'Years ago'); + dataExplorerPage.checkDocTableFirstFieldFilterForAndOutButton(true); + }); + // 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.setSearchDateRange('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/utils/commands.js b/cypress/utils/commands.js index 4a6d3bc261a1..162c5c4ac7b9 100644 --- a/cypress/utils/commands.js +++ b/cypress/utils/commands.js @@ -23,10 +23,11 @@ Cypress.Commands.add('getElementsByTestIds', (testIds, options = {}) => { }); Cypress.Commands.add('localLogin', (username, password) => { - miscUtils.visitPage('/app/login'); + miscUtils.visitPage('/app/home'); loginPage.enterUserName(username); loginPage.enterPassword(password); loginPage.submit(); + cy.url().should('contain', '/app/home'); }); Cypress.Commands.add('waitForLoader', () => { diff --git a/cypress/utils/data_explorer_page/data_explorer_elements.js b/cypress/utils/data_explorer_page/data_explorer_elements.js index e1815e072eb9..41f9299d4677 100644 --- a/cypress/utils/data_explorer_page/data_explorer_elements.js +++ b/cypress/utils/data_explorer_page/data_explorer_elements.js @@ -5,6 +5,7 @@ export const DATA_EXPLORER_PAGE_ELEMENTS = { NEW_SEARCH_BUTTON: '[data-test-subj="discoverNewButton"]', + DISCOVER_QUERY_HITS: '[data-test-subj="discoverQueryHits"]', DATASET_SELECTOR_BUTTON: '[data-test-subj="datasetSelectorButton"]', ALL_DATASETS_BUTTON: '[data-test-subj="datasetSelectorAdvancedButton"]', DATASET_EXPLORER_WINDOW: '[data-test-subj="datasetExplorerWindow"]', @@ -12,4 +13,15 @@ export const DATA_EXPLORER_PAGE_ELEMENTS = { DATASET_SELECTOR_LANGUAGE_SELECTOR: '[data-test-subj="advancedSelectorLanguageSelect"]', DATASET_SELECTOR_TIME_SELECTOR: '[data-test-subj="advancedSelectorTimeFieldSelect"]', DATASET_SELECTOR_SELECT_DATA_BUTTON: '[data-test-subj="advancedSelectorConfirmButton"]', + DOC_TABLE: '[data-test-subj="docTable"]', + DOC_TABLE_ROW_FIELD: '[data-test-subj="docTableField"]', + TABLE_FIELD_FILTER_FOR_BUTTON: '[data-test-subj="filterForValue"]', + TABLE_FIELD_FILTER_OUT_BUTTON: '[data-test-subj="filterOutValue"]', + SEARCH_DATE_PICKER_BUTTON: '[data-test-subj="superDatePickerShowDatesButton"]', + SEARCH_DATE_PICKER_RELATIVE_TAB: '[data-test-subj="superDatePickerRelativeTab"]', + SEARCH_DATE_RELATIVE_PICKER_INPUT: '[data-test-subj="superDatePickerRelativeDateInputNumber"]', + SEARCH_DATE_RELATIVE_PICKER_UNIT_SELECTOR: + '[data-test-subj="superDatePickerRelativeDateInputUnitSelector"]', + QUERY_SUBMIT_BUTTON: '[data-test-subj="querySubmitButton"]', + GLOBAL_QUERY_EDITOR_FILTER_VALUE: '[data-test-subj="globalFilterLabelValue"]', }; diff --git a/cypress/utils/data_explorer_page/data_explorer_page.js b/cypress/utils/data_explorer_page/data_explorer_page.js index 121724907203..b84bf3c96c99 100644 --- a/cypress/utils/data_explorer_page/data_explorer_page.js +++ b/cypress/utils/data_explorer_page/data_explorer_page.js @@ -37,9 +37,9 @@ export class DataExplorerPage { .select(timeField); } /** - * Select a language in the Dataset Selector + * Select a language in the Dataset Selector for Index */ - selectDatasetLanguage(datasetLanguage) { + selectIndexDatasetLanguage(datasetLanguage) { this.testRunner .get(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_SELECTOR_LANGUAGE_SELECTOR) .select(datasetLanguage); @@ -51,6 +51,16 @@ export class DataExplorerPage { this.testRunner.get(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_SELECTOR_SELECT_DATA_BUTTON).click(); } + /** + * Select a language in the Dataset Selector for Index Pattern + */ + selectIndexPatternDatasetLanguage(datasetLanguage) { + this.testRunner + .get(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_SELECTOR_LANGUAGE_SELECTOR) + .select(datasetLanguage); + this.testRunner.get(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_SELECTOR_SELECT_DATA_BUTTON).click(); + } + /** * Select an index dataset. */ @@ -69,6 +79,105 @@ export class DataExplorerPage { .contains(Cypress.env('INDEX_NAME'), { timeout: 10000 }) .click(); this.testRunner.get(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_SELECTOR_NEXT_BUTTON).click(); - this.selectDatasetLanguage(datasetLanguage); + this.selectIndexDatasetLanguage(datasetLanguage); + } + + /** + * Select an index pattern dataset. + */ + selectIndexPatternDataset(datasetLanguage) { + this.openDatasetExplorerWindow(); + this.testRunner + .get(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_EXPLORER_WINDOW) + .contains('Index Patterns') + .click(); + this.testRunner + .get(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_EXPLORER_WINDOW) + .contains(Cypress.env('INDEX_PATTERN_NAME'), { timeout: 10000 }) + .click(); + this.testRunner.get(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_SELECTOR_NEXT_BUTTON).click(); + this.selectIndexPatternDatasetLanguage(datasetLanguage); + } + + /** + * set search Date range + */ + setSearchDateRange(relativeNumber, relativeUnit) { + this.testRunner.get(DATA_EXPLORER_PAGE_ELEMENTS.SEARCH_DATE_PICKER_BUTTON).click(); + this.testRunner.get(DATA_EXPLORER_PAGE_ELEMENTS.SEARCH_DATE_PICKER_RELATIVE_TAB).click(); + this.testRunner + .get(DATA_EXPLORER_PAGE_ELEMENTS.SEARCH_DATE_RELATIVE_PICKER_INPUT) + .clear() + .type(relativeNumber); + this.testRunner + .get(DATA_EXPLORER_PAGE_ELEMENTS.SEARCH_DATE_RELATIVE_PICKER_UNIT_SELECTOR) + .select(relativeUnit); + this.testRunner.get(DATA_EXPLORER_PAGE_ELEMENTS.QUERY_SUBMIT_BUTTON).click(); + } + + /** + * check for the first Table Field's Filter For and Filter Out button. + */ + checkDocTableFirstFieldFilterForAndOutButton(isExists) { + const shouldText = isExists ? 'exist' : 'not.exist'; + this.testRunner + .get(DATA_EXPLORER_PAGE_ELEMENTS.DOC_TABLE) + .get('tbody tr') + .first() + .within(() => { + this.testRunner + .get(DATA_EXPLORER_PAGE_ELEMENTS.TABLE_FIELD_FILTER_FOR_BUTTON) + .should(shouldText); + this.testRunner + .get(DATA_EXPLORER_PAGE_ELEMENTS.TABLE_FIELD_FILTER_OUT_BUTTON) + .should(shouldText); + }); + } + + /** + * Check the Doc Table first Field's Filter For button filters the correct value. + */ + checkDocTableFirstFieldFilterForButtonFiltersCorrectField() { + this.testRunner + .get(DATA_EXPLORER_PAGE_ELEMENTS.DOC_TABLE) + .find('tbody tr') + .first() + .find(DATA_EXPLORER_PAGE_ELEMENTS.DOC_TABLE_ROW_FIELD) + .then(($field) => { + const fieldText = $field.find('span').find('span').text(); + $field.find(DATA_EXPLORER_PAGE_ELEMENTS.TABLE_FIELD_FILTER_FOR_BUTTON).click(); + this.testRunner + .get(DATA_EXPLORER_PAGE_ELEMENTS.GLOBAL_QUERY_EDITOR_FILTER_VALUE, { timeout: 10000 }) + .should('have.text', fieldText); + this.testRunner + .get(DATA_EXPLORER_PAGE_ELEMENTS.DOC_TABLE) + .find('tbody tr') + .first() + .find(DATA_EXPLORER_PAGE_ELEMENTS.DOC_TABLE_ROW_FIELD) + .find('span') + .find('span') + .should('have.text', fieldText); + this.testRunner + .get(DATA_EXPLORER_PAGE_ELEMENTS.DISCOVER_QUERY_HITS) + .should('have.text', '1'); + }); + } + + /** + * Check the Doc Table first Field's Filter Out button filters the correct value. + */ + checkDocTableFirstFieldFilterOutButtonFiltersCorrectField() { + this.testRunner + .get(DATA_EXPLORER_PAGE_ELEMENTS.DOC_TABLE) + .find('tbody tr') + .first() + .find(DATA_EXPLORER_PAGE_ELEMENTS.DOC_TABLE_ROW_FIELD) + .then(($field) => { + const fieldText = $field.find('span').find('span').text(); + $field.find(DATA_EXPLORER_PAGE_ELEMENTS.TABLE_FIELD_FILTER_OUT_BUTTON).click(); + this.testRunner + .get(DATA_EXPLORER_PAGE_ELEMENTS.GLOBAL_QUERY_EDITOR_FILTER_VALUE, { timeout: 10000 }) + .should('have.text', fieldText); + }); } } 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) { From 79ad0e220be58023a3dd729ce8a0acb2255345da Mon Sep 17 00:00:00 2001 From: Miki Date: Mon, 25 Nov 2024 22:43:34 -0800 Subject: [PATCH 03/31] Mitigate the incorrect layout of Discover due to a race condition between loading column definition and data (#8928) Signed-off-by: Miki --- .../default_discover_table.tsx | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) 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 && ( From 7f8889a2462658c7d0995cade957e3645a8c00e4 Mon Sep 17 00:00:00 2001 From: Tianyu Gao Date: Wed, 27 Nov 2024 17:29:58 +0800 Subject: [PATCH 04/31] [Workspace] feat: optimize recent items and filter out items whose workspace is deleted (#8900) * feat: optimize recent items and filter out items whose workspace is deleted Signed-off-by: tygao * Changeset file for PR #8900 created/updated * seperate link Signed-off-by: tygao * update filter sequence Signed-off-by: tygao --------- Signed-off-by: tygao Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> --- changelogs/fragments/8900.yml | 2 + .../chrome/ui/header/recent_items.test.tsx | 33 ++++++--- .../public/chrome/ui/header/recent_items.tsx | 68 ++++++++++++------- 3 files changed, 67 insertions(+), 36 deletions(-) create mode 100644 changelogs/fragments/8900.yml 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/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', + })} )} From ba397e69680f858495a0d754afcf4d3b6c04532f Mon Sep 17 00:00:00 2001 From: Paul Sebastian Date: Wed, 27 Nov 2024 03:23:45 -0800 Subject: [PATCH 05/31] [Auto Suggest] SQL Syntax Highlighting fix (#8951) Fixes SQL monaco monarch tokens by separating the states for single quoted and double quoted strings so that both can appear properly --------- Signed-off-by: Paul Sebastian Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> --- changelogs/fragments/8951.yml | 2 ++ .../osd-monaco/src/xjson/lexer_rules/opensearchsql.ts | 10 +++++++--- 2 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 changelogs/fragments/8951.yml 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/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: [ From 851e6158cd2ddeec98ddbfa02761692de10ba829 Mon Sep 17 00:00:00 2001 From: Miki Date: Wed, 27 Nov 2024 12:33:46 -0800 Subject: [PATCH 06/31] Bump `@opensearch-project/opensearch` from 2.9.0 to 2.13.0 (#8886) * Bump `@opensearch-project/opensearch` from 2.9.0 to 2.13.0 Signed-off-by: Miki * Changeset file for PR #8886 created/updated --------- Signed-off-by: Miki Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> --- changelogs/fragments/8886.yml | 2 ++ package.json | 3 +-- packages/osd-opensearch-archiver/package.json | 2 +- packages/osd-opensearch/package.json | 2 +- scripts/postinstall.js | 9 --------- yarn.lock | 12 ++++++------ 6 files changed, 11 insertions(+), 19 deletions(-) create mode 100644 changelogs/fragments/8886.yml 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/package.json b/package.json index 298ce702e854..0a103b9fdab1 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", @@ -166,7 +165,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/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/yarn.lock b/yarn.lock index 4f21c30e1e52..537af6f3662e 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" @@ -11500,7 +11500,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== From d26248cf78ef2ab939ab09e569907789898357c8 Mon Sep 17 00:00:00 2001 From: Qxisylolo Date: Thu, 28 Nov 2024 17:17:28 +0800 Subject: [PATCH 07/31] [workspace]fix: Change some of the http link in settings page to https link (#8919) * page_references_insecure Signed-off-by: Qxisylolo * typo Signed-off-by: Qxisylolo * Changeset file for PR #8919 created/updated * add https://numeraljs.com/ to lycheeignore Signed-off-by: Qxisylolo * change https://numeraljs.com/ to http Signed-off-by: Qxisylolo --------- Signed-off-by: Qxisylolo Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> --- changelogs/fragments/8919.yml | 2 ++ src/core/server/ui_settings/settings/date_formats.ts | 2 +- src/plugins/maps_legacy/server/ui_settings.ts | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 changelogs/fragments/8919.yml 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/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/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', { From 0c8102c4e989159c0fe2b8c6dd83d545d405bf71 Mon Sep 17 00:00:00 2001 From: Hailong Cui Date: Thu, 28 Nov 2024 17:18:42 +0800 Subject: [PATCH 08/31] [Workspace]Support search dev tools by its category name (#8920) * support search dev tools by category name Signed-off-by: Hailong Cui * Changeset file for PR #8920 created/updated * address review comments Signed-off-by: Hailong Cui --------- Signed-off-by: Hailong Cui Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> --- changelogs/fragments/8920.yml | 2 ++ .../search_devtool_command.test.tsx | 17 ++++++++++++++++- .../global_search/search_devtool_command.tsx | 12 +++++++++--- 3 files changed, 27 insertions(+), 4 deletions(-) create mode 100644 changelogs/fragments/8920.yml 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/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: [ { From f4eb1cf80c63960c5ab1cea9840952d95918f450 Mon Sep 17 00:00:00 2001 From: yuboluo Date: Mon, 2 Dec 2024 15:56:34 +0800 Subject: [PATCH 09/31] [Workspace] Isolate objects based on workspace when calling get/bulkGet (#8888) * Isolate objects based on workspace when calling get/bulkGet Signed-off-by: yubonluo * Changeset file for PR #8888 created/updated * add integration tests Signed-off-by: yubonluo * optimize the code Signed-off-by: yubonluo * optimize the code Signed-off-by: yubonluo * optimize the code Signed-off-by: yubonluo * optimize the function name Signed-off-by: yubonluo * add data source validate Signed-off-by: yubonluo * optimize the code Signed-off-by: yubonluo --------- Signed-off-by: yubonluo Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> --- changelogs/fragments/8888.yml | 2 + .../workspace_id_consumer_wrapper.test.ts | 154 ++++++ .../workspace_id_consumer_wrapper.test.ts | 492 ++++++++++++++++++ .../workspace_id_consumer_wrapper.ts | 98 +++- ...space_saved_objects_client_wrapper.test.ts | 281 ---------- .../workspace_saved_objects_client_wrapper.ts | 58 --- 6 files changed, 744 insertions(+), 341 deletions(-) create mode 100644 changelogs/fragments/8888.yml 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/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..c762d08cedff 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(); }); @@ -312,5 +318,153 @@ 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).toEqual([ + createdBarWorkspace.id, + ]); + 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/workspace_id_consumer_wrapper.test.ts b/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.test.ts index 570d701d7c63..ca19ffc927ad 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(); @@ -196,4 +197,495 @@ 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: {}, + references: [], + 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: {}, + references: [], + 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 {}, + "id": "dashboard_id", + "references": Array [], + "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", + "workspaces": Array [ + "bar", + ], + }, + 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 {}, + "id": "dashboard_id", + "references": Array [], + "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", + "workspaces": Array [ + "bar", + ], + }, + 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 {}, + "id": "dashboard_id", + "references": Array [], + "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", + "workspaces": Array [ + "bar", + ], + }, + 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..43393da03ef5 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,26 @@ import { OpenSearchDashboardsRequest, SavedObjectsFindOptions, SavedObjectsErrorHelpers, + 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,6 +61,36 @@ export class WorkspaceIdConsumerWrapper { return type === UI_SETTINGS_SAVED_OBJECTS_TYPE; } + 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, @@ -126,8 +169,59 @@ export class WorkspaceIdConsumerWrapper { } return wrapperOptions.client.find(finalOptions); }, - bulkGet: wrapperOptions.client.bulkGet, - get: wrapperOptions.client.get, + 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'); + } + + 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 + : { + ...object, + error: { + ...generateSavedObjectsForbiddenError().output.payload, + }, + }; + }), + }; + } + + 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; + }, 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, From 0fa9db87abcb079cb8b51a361efd1379a8217209 Mon Sep 17 00:00:00 2001 From: Sean Li Date: Mon, 2 Dec 2024 16:54:14 -0800 Subject: [PATCH 10/31] [Discover] Fix Initialization if No Saved Query (#8930) * replace default query with current query Signed-off-by: Sean Li * Changeset file for PR #8930 created/updated * adding unit tests Signed-off-by: Sean Li --------- Signed-off-by: Sean Li Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> --- changelogs/fragments/8930.yml | 2 + .../view_components/utils/use_search.test.tsx | 59 ++++++++++++++++++- .../view_components/utils/use_search.ts | 5 +- 3 files changed, 62 insertions(+), 4 deletions(-) create mode 100644 changelogs/fragments/8930.yml 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/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) { From 678392becaee8e7cc2aa90fba7b535c81c1b9e9f Mon Sep 17 00:00:00 2001 From: yuboluo Date: Tue, 3 Dec 2024 11:24:56 +0800 Subject: [PATCH 11/31] [Workspace][Bug] Check if workspaces exists when creating saved objects (#8739) * Check if workspaces exists when creating saved objects Signed-off-by: yubonluo * Changeset file for PR #8739 created/updated * optimize the code Signed-off-by: yubonluo * fix test error Signed-off-by: yubonluo * optimize the code Signed-off-by: yubonluo * fix test errors Signed-off-by: yubonluo * add integration tests Signed-off-by: yubonluo --------- Signed-off-by: yubonluo Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> --- changelogs/fragments/8739.yml | 2 + .../workspace_id_consumer_wrapper.test.ts | 60 ++++++++- ...space_saved_objects_client_wrapper.test.ts | 35 ++---- .../workspace_id_consumer_wrapper.test.ts | 67 +++++++++- .../workspace_id_consumer_wrapper.ts | 115 ++++++++++-------- 5 files changed, 199 insertions(+), 80 deletions(-) create mode 100644 changelogs/fragments/8739.yml 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/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 c762d08cedff..f597dd369272 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 @@ -150,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 @@ -184,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 @@ -288,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 () => { 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 ca19ffc927ad..fcef67870523 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 @@ -38,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', }); @@ -68,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', @@ -88,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', () => { @@ -174,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); }); 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 43393da03ef5..f6efb690c5cd 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,6 +14,7 @@ import { OpenSearchDashboardsRequest, SavedObjectsFindOptions, SavedObjectsErrorHelpers, + SavedObjectsClientWrapperOptions, SavedObject, SavedObjectsBulkGetObject, SavedObjectsBulkResponse, @@ -61,6 +62,52 @@ 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, @@ -94,22 +141,21 @@ export class WorkspaceIdConsumerWrapper { 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 = {} @@ -127,46 +173,7 @@ 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) - ); - } - } - - if (!isAllTargetWorkspaceExisting) { - throw SavedObjectsErrorHelpers.decorateBadRequestError( - new Error( - i18n.translate('workspace.id_consumer.invalid', { - defaultMessage: 'Invalid workspaces', - }) - ) - ); - } - } + await this.checkWorkspacesExist(finalOptions?.workspaces, wrapperOptions); return wrapperOptions.client.find(finalOptions); }, bulkGet: async ( From 67c3c056664373fe9225222501f45817686923c7 Mon Sep 17 00:00:00 2001 From: Joshua Li Date: Mon, 2 Dec 2024 20:01:48 -0800 Subject: [PATCH 12/31] [Discover] fix PPL to not throw error if aggregation query fails (#8992) Signed-off-by: Joshua Li --- .../query_enhancements/common/utils.test.ts | 6 +- .../query_enhancements/common/utils.ts | 2 +- .../search/ppl_async_search_strategy.ts | 6 +- .../server/search/ppl_search_strategy.test.ts | 372 ++++++++++++++++++ .../server/search/ppl_search_strategy.ts | 6 +- .../search/sql_async_search_strategy.ts | 6 +- .../server/search/sql_search_strategy.ts | 4 +- 7 files changed, 387 insertions(+), 15 deletions(-) create mode 100644 src/plugins/query_enhancements/server/search/ppl_search_strategy.test.ts 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/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, From 3dc82ba7b373c0d0edcbb48269caef28bfd74259 Mon Sep 17 00:00:00 2001 From: Argus Li Date: Wed, 4 Dec 2024 12:29:36 -0800 Subject: [PATCH 13/31] Reformat to match OSD-functional-tests. Reformat for TS. --- .../filter_for_value_spec.js | 38 ++-- cypress/support/{e2e.js => e2e.ts} | 5 +- cypress/utils/{commands.js => commands.ts} | 4 + .../dashboards/data_explorer/commands.ts | 171 ++++++++++++++++ .../dashboards/data_explorer/elements.ts | 26 +++ .../data_explorer_elements.js | 27 --- .../data_explorer_page/data_explorer_page.js | 183 ------------------ 7 files changed, 223 insertions(+), 231 deletions(-) rename cypress/support/{e2e.js => e2e.ts} (66%) rename cypress/utils/{commands.js => commands.ts} (90%) create mode 100644 cypress/utils/dashboards/data_explorer/commands.ts create mode 100644 cypress/utils/dashboards/data_explorer/elements.ts delete mode 100644 cypress/utils/data_explorer_page/data_explorer_elements.js delete mode 100644 cypress/utils/data_explorer_page/data_explorer_page.js diff --git a/cypress/integration/core_opensearch_dashboards/filter_for_value_spec.js b/cypress/integration/core_opensearch_dashboards/filter_for_value_spec.js index 490be6529a03..4f82d5f3d95e 100644 --- a/cypress/integration/core_opensearch_dashboards/filter_for_value_spec.js +++ b/cypress/integration/core_opensearch_dashboards/filter_for_value_spec.js @@ -4,54 +4,52 @@ */ import { MiscUtils } from '@opensearch-dashboards-test/opensearch-dashboards-test-library'; -import { DataExplorerPage } from '../../utils/data_explorer_page/data_explorer_page'; const miscUtils = new MiscUtils(cy); -const dataExplorerPage = new DataExplorerPage(cy); describe('filter for value spec', () => { beforeEach(() => { cy.localLogin(Cypress.env('username'), Cypress.env('password')); miscUtils.visitPage('app/data-explorer/discover'); - dataExplorerPage.clickNewSearchButton(); + cy.clickNewSearchButton(); }); describe('filter actions in table field', () => { describe('index pattern dataset', () => { // filter actions should not exist for DQL - it.only('DQL', () => { - dataExplorerPage.selectIndexPatternDataset('DQL'); - dataExplorerPage.setSearchDateRange('15', 'Years ago'); - dataExplorerPage.checkDocTableFirstFieldFilterForAndOutButton(true); - dataExplorerPage.checkDocTableFirstFieldFilterForButtonFiltersCorrectField(); + it('DQL', () => { + cy.selectIndexPatternDataset('DQL'); + cy.setSearchRelativeDateRange('15', 'Years ago'); + cy.checkDocTableFirstFieldFilterForAndOutButton(true); + cy.checkDocTableFirstFieldFilterForButtonFiltersCorrectField(); }); // filter actions should not exist for PPL it('Lucene', () => { - dataExplorerPage.selectIndexPatternDataset('Lucene'); - dataExplorerPage.setSearchDateRange('15', 'Years ago'); - dataExplorerPage.checkDocTableFirstFieldFilterForAndOutButton(true); + cy.selectIndexPatternDataset('Lucene'); + cy.setSearchRelativeDateRange('15', 'Years ago'); + cy.checkDocTableFirstFieldFilterForAndOutButton(true); }); // filter actions should not exist for SQL it('SQL', () => { - dataExplorerPage.selectIndexPatternDataset('OpenSearch SQL'); - dataExplorerPage.checkDocTableFirstFieldFilterForAndOutButton(false); + cy.selectIndexPatternDataset('OpenSearch SQL'); + cy.checkDocTableFirstFieldFilterForAndOutButton(false); }); // filter actions should not exist for PPL it('PPL', () => { - dataExplorerPage.selectIndexPatternDataset('PPL'); - dataExplorerPage.setSearchDateRange('15', 'Years ago'); - dataExplorerPage.checkDocTableFirstFieldFilterForAndOutButton(false); + cy.selectIndexPatternDataset('PPL'); + cy.setSearchRelativeDateRange('15', 'Years ago'); + cy.checkDocTableFirstFieldFilterForAndOutButton(false); }); }); describe('index dataset', () => { // filter actions should not exist for SQL it('SQL', () => { - dataExplorerPage.selectIndexDataset('OpenSearch SQL'); - dataExplorerPage.checkDocTableFirstFieldFilterForAndOutButton(false); + cy.selectIndexDataset('OpenSearch SQL'); + cy.checkDocTableFirstFieldFilterForAndOutButton(false); }); // filter actions should not exist for PPL it('PPL', () => { - dataExplorerPage.selectIndexDataset('PPL'); - dataExplorerPage.checkDocTableFirstFieldFilterForAndOutButton(false); + cy.selectIndexDataset('PPL'); + cy.checkDocTableFirstFieldFilterForAndOutButton(false); }); }); }); diff --git a/cypress/support/e2e.js b/cypress/support/e2e.ts similarity index 66% rename from cypress/support/e2e.js rename to cypress/support/e2e.ts index 474948b47550..ae89c76268a3 100644 --- a/cypress/support/e2e.js +++ b/cypress/support/e2e.ts @@ -4,8 +4,11 @@ */ import '../utils/commands'; +import '../utils/dashboards/data_explorer/commands'; + +// Alternatively you can use CommonJS syntax: +// require('./commands') -// eslint-disable-next-line no-unused-vars Cypress.on('uncaught:exception', (_err) => { // returning false here prevents Cypress from failing the test return false; diff --git a/cypress/utils/commands.js b/cypress/utils/commands.ts similarity index 90% rename from cypress/utils/commands.js rename to cypress/utils/commands.ts index 162c5c4ac7b9..ea07f3ed4406 100644 --- a/cypress/utils/commands.js +++ b/cypress/utils/commands.ts @@ -22,6 +22,10 @@ Cypress.Commands.add('getElementsByTestIds', (testIds, options = {}) => { return cy.get(selectors.join(','), options); }); +Cypress.Commands.add('findElementByTestId', (testId, options = {}) => { + return cy.find(`[data-test-subj="${testId}"]`, options); +}); + Cypress.Commands.add('localLogin', (username, password) => { miscUtils.visitPage('/app/home'); loginPage.enterUserName(username); diff --git a/cypress/utils/dashboards/data_explorer/commands.ts b/cypress/utils/dashboards/data_explorer/commands.ts new file mode 100644 index 000000000000..19a9cf62b3ba --- /dev/null +++ b/cypress/utils/dashboards/data_explorer/commands.ts @@ -0,0 +1,171 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { DATA_EXPLORER_PAGE_ELEMENTS } from './elements.js'; + +/** + * Click on the New Search button. + */ +Cypress.Commands.add('clickNewSearchButton', () => { + cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.NEW_SEARCH_BUTTON, { timeout: 10000 }) + .should('be.visible') + .click(); +}); + +/** + * Open window to select Dataset + */ +Cypress.Commands.add('openDatasetExplorerWindow', () => { + cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_SELECTOR_BUTTON).click(); + cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.ALL_DATASETS_BUTTON).click(); +}); + +/** + * Select a Time Field in the Dataset Selector + */ +Cypress.Commands.add('selectDatasetTimeField', (timeField) => { + cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_SELECTOR_TIME_SELECTOR).select( + timeField + ); +}); + +/** + * Select a language in the Dataset Selector for Index + */ +Cypress.Commands.add('selectIndexDatasetLanguage', (datasetLanguage) => { + cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_SELECTOR_LANGUAGE_SELECTOR).select( + datasetLanguage + ); + switch (datasetLanguage) { + case 'PPL': + this.selectDatasetTimeField("I don't want to use the time filter"); + break; + } + cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_SELECTOR_SELECT_DATA_BUTTON).click(); +}); + +/** + * Select a language in the Dataset Selector for Index Pattern + */ +Cypress.Commands.add('selectIndexPatternDatasetLanguage', (datasetLanguage) => { + cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_SELECTOR_LANGUAGE_SELECTOR).select( + datasetLanguage + ); + cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_SELECTOR_SELECT_DATA_BUTTON).click(); +}); + +/** + * Select an index dataset. + */ +Cypress.Commands.add('selectIndexDataset', (datasetLanguage) => { + this.openDatasetExplorerWindow(); + cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_EXPLORER_WINDOW) + .contains('Indexes') + .click(); + cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_EXPLORER_WINDOW) + .contains(Cypress.env('INDEX_CLUSTER_NAME'), { timeout: 10000 }) + .click(); + cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_EXPLORER_WINDOW) + .contains(Cypress.env('INDEX_NAME'), { timeout: 10000 }) + .click(); + cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_SELECTOR_NEXT_BUTTON).click(); + this.selectIndexDatasetLanguage(datasetLanguage); +}); + +/** + * Select an index pattern dataset. + */ +Cypress.Commands.add('selectIndexPatternDataset', (datasetLanguage) => { + this.openDatasetExplorerWindow(); + cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_EXPLORER_WINDOW) + .contains('Index Patterns') + .click(); + cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_EXPLORER_WINDOW) + .contains(Cypress.env('INDEX_PATTERN_NAME'), { timeout: 10000 }) + .click(); + cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_SELECTOR_NEXT_BUTTON).click(); + this.selectIndexPatternDatasetLanguage(datasetLanguage); +}); + +/** + * set search Date range + */ +Cypress.Commands.add('setSearchRelativeDateRange', (relativeNumber, relativeUnit) => { + cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.SEARCH_DATE_PICKER_BUTTON).click(); + cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.SEARCH_DATE_PICKER_RELATIVE_TAB).click(); + cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.SEARCH_DATE_RELATIVE_PICKER_INPUT) + .clear() + .type(relativeNumber); + cy.getElementByTestId( + DATA_EXPLORER_PAGE_ELEMENTS.SEARCH_DATE_RELATIVE_PICKER_UNIT_SELECTOR + ).select(relativeUnit); + cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.QUERY_SUBMIT_BUTTON).click(); +}); + +/** + * check for the first Table Field's Filter For and Filter Out button. + */ +Cypress.Commands.add('checkDocTableFirstFieldFilterForAndOutButton', (isExists) => { + const shouldText = isExists ? 'exist' : 'not.exist'; + cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DOC_TABLE) + .get('tbody tr') + .first() + .within(() => { + cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.TABLE_FIELD_FILTER_FOR_BUTTON).should( + shouldText + ); + cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.TABLE_FIELD_FILTER_OUT_BUTTON).should( + shouldText + ); + }); +}); + +/** + * Check the Doc Table first Field's Filter For button filters the correct value. + */ +Cypress.Commands.add('checkDocTableFirstFieldFilterForButtonFiltersCorrectField', () => { + cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DOC_TABLE) + .find('tbody tr') + .first() + .findElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DOC_TABLE_ROW_FIELD) + .then(($field) => { + const fieldText = $field.find('span span').text(); + $field + .findElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.TABLE_FIELD_FILTER_FOR_BUTTON) + .trigger(click); + cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.GLOBAL_QUERY_EDITOR_FILTER_VALUE, { + timeout: 10000, + }).should('have.text', fieldText); + cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DOC_TABLE) + .find('tbody tr') + .first() + .findElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DOC_TABLE_ROW_FIELD) + .find('span span') + .should('have.text', fieldText); + cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DISCOVER_QUERY_HITS).should( + 'have.text', + '1' + ); + }); +}); + +/** + * Check the Doc Table first Field's Filter Out button filters the correct value. + */ +Cypress.Commands.add('checkDocTableFirstFieldFilterOutButtonFiltersCorrectField', () => { + cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DOC_TABLE) + .find('tbody tr') + .first() + .findElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DOC_TABLE_ROW_FIELD) + .then(($field) => { + const fieldText = $field.find('span span').text(); + $field + .findElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.TABLE_FIELD_FILTER_OUT_BUTTON) + .trigger(click); + cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.GLOBAL_QUERY_EDITOR_FILTER_VALUE, { + timeout: 10000, + }).should('have.text', fieldText); + }); +}); diff --git a/cypress/utils/dashboards/data_explorer/elements.ts b/cypress/utils/dashboards/data_explorer/elements.ts new file mode 100644 index 000000000000..5b28bbef59cb --- /dev/null +++ b/cypress/utils/dashboards/data_explorer/elements.ts @@ -0,0 +1,26 @@ +/* + * 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', +}; diff --git a/cypress/utils/data_explorer_page/data_explorer_elements.js b/cypress/utils/data_explorer_page/data_explorer_elements.js deleted file mode 100644 index 41f9299d4677..000000000000 --- a/cypress/utils/data_explorer_page/data_explorer_elements.js +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -export const DATA_EXPLORER_PAGE_ELEMENTS = { - NEW_SEARCH_BUTTON: '[data-test-subj="discoverNewButton"]', - DISCOVER_QUERY_HITS: '[data-test-subj="discoverQueryHits"]', - DATASET_SELECTOR_BUTTON: '[data-test-subj="datasetSelectorButton"]', - ALL_DATASETS_BUTTON: '[data-test-subj="datasetSelectorAdvancedButton"]', - DATASET_EXPLORER_WINDOW: '[data-test-subj="datasetExplorerWindow"]', - DATASET_SELECTOR_NEXT_BUTTON: '[data-test-subj="datasetSelectorNext"]', - DATASET_SELECTOR_LANGUAGE_SELECTOR: '[data-test-subj="advancedSelectorLanguageSelect"]', - DATASET_SELECTOR_TIME_SELECTOR: '[data-test-subj="advancedSelectorTimeFieldSelect"]', - DATASET_SELECTOR_SELECT_DATA_BUTTON: '[data-test-subj="advancedSelectorConfirmButton"]', - DOC_TABLE: '[data-test-subj="docTable"]', - DOC_TABLE_ROW_FIELD: '[data-test-subj="docTableField"]', - TABLE_FIELD_FILTER_FOR_BUTTON: '[data-test-subj="filterForValue"]', - TABLE_FIELD_FILTER_OUT_BUTTON: '[data-test-subj="filterOutValue"]', - SEARCH_DATE_PICKER_BUTTON: '[data-test-subj="superDatePickerShowDatesButton"]', - SEARCH_DATE_PICKER_RELATIVE_TAB: '[data-test-subj="superDatePickerRelativeTab"]', - SEARCH_DATE_RELATIVE_PICKER_INPUT: '[data-test-subj="superDatePickerRelativeDateInputNumber"]', - SEARCH_DATE_RELATIVE_PICKER_UNIT_SELECTOR: - '[data-test-subj="superDatePickerRelativeDateInputUnitSelector"]', - QUERY_SUBMIT_BUTTON: '[data-test-subj="querySubmitButton"]', - GLOBAL_QUERY_EDITOR_FILTER_VALUE: '[data-test-subj="globalFilterLabelValue"]', -}; diff --git a/cypress/utils/data_explorer_page/data_explorer_page.js b/cypress/utils/data_explorer_page/data_explorer_page.js deleted file mode 100644 index b84bf3c96c99..000000000000 --- a/cypress/utils/data_explorer_page/data_explorer_page.js +++ /dev/null @@ -1,183 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { DATA_EXPLORER_PAGE_ELEMENTS } from './data_explorer_elements.js'; - -export class DataExplorerPage { - constructor(inputTestRunner) { - this.testRunner = inputTestRunner; - } - - /** - * Click on the New Search button. - */ - clickNewSearchButton() { - this.testRunner - .get(DATA_EXPLORER_PAGE_ELEMENTS.NEW_SEARCH_BUTTON, { timeout: 10000 }) - .should('be.visible') - .click(); - } - - /** - * Open window to select Dataset - */ - openDatasetExplorerWindow() { - this.testRunner.get(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_SELECTOR_BUTTON).click(); - this.testRunner.get(DATA_EXPLORER_PAGE_ELEMENTS.ALL_DATASETS_BUTTON).click(); - } - - /** - * Select a Time Field in the Dataset Selector - */ - selectDatasetTimeField(timeField) { - this.testRunner - .get(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_SELECTOR_TIME_SELECTOR) - .select(timeField); - } - /** - * Select a language in the Dataset Selector for Index - */ - selectIndexDatasetLanguage(datasetLanguage) { - this.testRunner - .get(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_SELECTOR_LANGUAGE_SELECTOR) - .select(datasetLanguage); - switch (datasetLanguage) { - case 'PPL': - this.selectDatasetTimeField("I don't want to use the time filter"); - break; - } - this.testRunner.get(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_SELECTOR_SELECT_DATA_BUTTON).click(); - } - - /** - * Select a language in the Dataset Selector for Index Pattern - */ - selectIndexPatternDatasetLanguage(datasetLanguage) { - this.testRunner - .get(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_SELECTOR_LANGUAGE_SELECTOR) - .select(datasetLanguage); - this.testRunner.get(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_SELECTOR_SELECT_DATA_BUTTON).click(); - } - - /** - * Select an index dataset. - */ - selectIndexDataset(datasetLanguage) { - this.openDatasetExplorerWindow(); - this.testRunner - .get(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_EXPLORER_WINDOW) - .contains('Indexes') - .click(); - this.testRunner - .get(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_EXPLORER_WINDOW) - .contains(Cypress.env('INDEX_CLUSTER_NAME'), { timeout: 10000 }) - .click(); - this.testRunner - .get(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_EXPLORER_WINDOW) - .contains(Cypress.env('INDEX_NAME'), { timeout: 10000 }) - .click(); - this.testRunner.get(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_SELECTOR_NEXT_BUTTON).click(); - this.selectIndexDatasetLanguage(datasetLanguage); - } - - /** - * Select an index pattern dataset. - */ - selectIndexPatternDataset(datasetLanguage) { - this.openDatasetExplorerWindow(); - this.testRunner - .get(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_EXPLORER_WINDOW) - .contains('Index Patterns') - .click(); - this.testRunner - .get(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_EXPLORER_WINDOW) - .contains(Cypress.env('INDEX_PATTERN_NAME'), { timeout: 10000 }) - .click(); - this.testRunner.get(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_SELECTOR_NEXT_BUTTON).click(); - this.selectIndexPatternDatasetLanguage(datasetLanguage); - } - - /** - * set search Date range - */ - setSearchDateRange(relativeNumber, relativeUnit) { - this.testRunner.get(DATA_EXPLORER_PAGE_ELEMENTS.SEARCH_DATE_PICKER_BUTTON).click(); - this.testRunner.get(DATA_EXPLORER_PAGE_ELEMENTS.SEARCH_DATE_PICKER_RELATIVE_TAB).click(); - this.testRunner - .get(DATA_EXPLORER_PAGE_ELEMENTS.SEARCH_DATE_RELATIVE_PICKER_INPUT) - .clear() - .type(relativeNumber); - this.testRunner - .get(DATA_EXPLORER_PAGE_ELEMENTS.SEARCH_DATE_RELATIVE_PICKER_UNIT_SELECTOR) - .select(relativeUnit); - this.testRunner.get(DATA_EXPLORER_PAGE_ELEMENTS.QUERY_SUBMIT_BUTTON).click(); - } - - /** - * check for the first Table Field's Filter For and Filter Out button. - */ - checkDocTableFirstFieldFilterForAndOutButton(isExists) { - const shouldText = isExists ? 'exist' : 'not.exist'; - this.testRunner - .get(DATA_EXPLORER_PAGE_ELEMENTS.DOC_TABLE) - .get('tbody tr') - .first() - .within(() => { - this.testRunner - .get(DATA_EXPLORER_PAGE_ELEMENTS.TABLE_FIELD_FILTER_FOR_BUTTON) - .should(shouldText); - this.testRunner - .get(DATA_EXPLORER_PAGE_ELEMENTS.TABLE_FIELD_FILTER_OUT_BUTTON) - .should(shouldText); - }); - } - - /** - * Check the Doc Table first Field's Filter For button filters the correct value. - */ - checkDocTableFirstFieldFilterForButtonFiltersCorrectField() { - this.testRunner - .get(DATA_EXPLORER_PAGE_ELEMENTS.DOC_TABLE) - .find('tbody tr') - .first() - .find(DATA_EXPLORER_PAGE_ELEMENTS.DOC_TABLE_ROW_FIELD) - .then(($field) => { - const fieldText = $field.find('span').find('span').text(); - $field.find(DATA_EXPLORER_PAGE_ELEMENTS.TABLE_FIELD_FILTER_FOR_BUTTON).click(); - this.testRunner - .get(DATA_EXPLORER_PAGE_ELEMENTS.GLOBAL_QUERY_EDITOR_FILTER_VALUE, { timeout: 10000 }) - .should('have.text', fieldText); - this.testRunner - .get(DATA_EXPLORER_PAGE_ELEMENTS.DOC_TABLE) - .find('tbody tr') - .first() - .find(DATA_EXPLORER_PAGE_ELEMENTS.DOC_TABLE_ROW_FIELD) - .find('span') - .find('span') - .should('have.text', fieldText); - this.testRunner - .get(DATA_EXPLORER_PAGE_ELEMENTS.DISCOVER_QUERY_HITS) - .should('have.text', '1'); - }); - } - - /** - * Check the Doc Table first Field's Filter Out button filters the correct value. - */ - checkDocTableFirstFieldFilterOutButtonFiltersCorrectField() { - this.testRunner - .get(DATA_EXPLORER_PAGE_ELEMENTS.DOC_TABLE) - .find('tbody tr') - .first() - .find(DATA_EXPLORER_PAGE_ELEMENTS.DOC_TABLE_ROW_FIELD) - .then(($field) => { - const fieldText = $field.find('span').find('span').text(); - $field.find(DATA_EXPLORER_PAGE_ELEMENTS.TABLE_FIELD_FILTER_OUT_BUTTON).click(); - this.testRunner - .get(DATA_EXPLORER_PAGE_ELEMENTS.GLOBAL_QUERY_EDITOR_FILTER_VALUE, { timeout: 10000 }) - .should('have.text', fieldText); - }); - } -} From 1da2863337d6be79299a12ed0917b37867c520bb Mon Sep 17 00:00:00 2001 From: Daniel Rowe <51932404+d-rowe@users.noreply.github.com> Date: Tue, 3 Dec 2024 12:31:10 -0800 Subject: [PATCH 14/31] Upgrade Cypress to v12 (#8995) * Update Cypress to v12 (#8926) * Update cypress to v12 Signed-off-by: Daniel Rowe * Add required e2e.js Signed-off-by: Daniel Rowe * Changeset file for PR #8926 created/updated * Update license header Signed-off-by: Daniel Rowe <51932404+d-rowe@users.noreply.github.com> * Update license in e2e.js Signed-off-by: Daniel Rowe <51932404+d-rowe@users.noreply.github.com> --------- Signed-off-by: Daniel Rowe Signed-off-by: Daniel Rowe <51932404+d-rowe@users.noreply.github.com> Co-authored-by: Daniel Rowe Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> * fix: support imports without extensions in cypress webpack build (#8993) * fix: support imports without extensions in cypress webpack build Signed-off-by: Daniel Rowe * Changeset file for PR #8993 created/updated * use typescript config Signed-off-by: Daniel Rowe <51932404+d-rowe@users.noreply.github.com> * fix lint Signed-off-by: Daniel Rowe <51932404+d-rowe@users.noreply.github.com> * disable new test isolation feature This isolation was causing regressions Signed-off-by: Daniel Rowe <51932404+d-rowe@users.noreply.github.com> --------- Signed-off-by: Daniel Rowe Signed-off-by: Daniel Rowe <51932404+d-rowe@users.noreply.github.com> Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> --------- Signed-off-by: Daniel Rowe Signed-off-by: Daniel Rowe <51932404+d-rowe@users.noreply.github.com> Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> --- cypress/support/e2e.js | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 cypress/support/e2e.js diff --git a/cypress/support/e2e.js b/cypress/support/e2e.js new file mode 100644 index 000000000000..fa35cf4214b4 --- /dev/null +++ b/cypress/support/e2e.js @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import '../utils/commands'; From a727acb1805d1a984fcbccdf559f3226d55fe760 Mon Sep 17 00:00:00 2001 From: Joshua Li Date: Tue, 3 Dec 2024 13:22:27 -0800 Subject: [PATCH 15/31] [Query enhancements] use status 503 if search strategy throws 500 (#8876) * [Query enhancements] use status 503 if opensearch throws 500 Signed-off-by: Joshua Li * update unit tests Signed-off-by: Joshua Li --------- Signed-off-by: Joshua Li --- .../server/routes/index.test.ts | 21 +++++++++++++++++++ .../query_enhancements/server/routes/index.ts | 11 +++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 src/plugins/query_enhancements/server/routes/index.test.ts 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, }); } From 788d2c717f616dc814c96ced0fe0edb9955ceb82 Mon Sep 17 00:00:00 2001 From: Tyler Ohlsen Date: Wed, 4 Dec 2024 09:48:20 -0800 Subject: [PATCH 16/31] Revert "[augmenter] do not support datasources with no version (#8915)" (#8925) This reverts commit 539675e688061e689b362801bcb05a3ef78431b2. --- changelogs/fragments/8915.yml | 2 - src/plugins/vis_augmenter/public/plugin.ts | 2 - src/plugins/vis_augmenter/public/services.ts | 6 +- .../vis_augmenter/public/utils/utils.test.ts | 129 ++---------------- .../vis_augmenter/public/utils/utils.ts | 26 +--- .../actions/view_events_option_action.tsx | 2 +- .../public/line_to_expression.ts | 2 +- .../public/embeddable/visualize_embeddable.ts | 2 +- 8 files changed, 19 insertions(+), 152 deletions(-) delete mode 100644 changelogs/fragments/8915.yml 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/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. From 502467880fc1df44ac543a107f00f24135d0d224 Mon Sep 17 00:00:00 2001 From: Argus Li Date: Wed, 4 Dec 2024 23:36:11 -0800 Subject: [PATCH 17/31] Complete test suite filter actions in table field. Refactor to match OSD-functional-tests-layout. --- cypress.config.ts | 3 +- .../filter_for_value_spec.js | 9 +- cypress/support/e2e.js | 12 +- cypress/support/e2e.ts | 15 -- cypress/utils/{commands.ts => commands.js} | 38 ++-- .../dashboards/data_explorer/commands.js | 209 ++++++++++++++++++ .../dashboards/data_explorer/commands.ts | 171 -------------- .../dashboards/data_explorer/constants.js | 8 + .../{elements.ts => elements.js} | 1 + .../data/public/ui/filter_bar/filter_bar.tsx | 7 +- 10 files changed, 263 insertions(+), 210 deletions(-) delete mode 100644 cypress/support/e2e.ts rename cypress/utils/{commands.ts => commands.js} (62%) create mode 100644 cypress/utils/dashboards/data_explorer/commands.js delete mode 100644 cypress/utils/dashboards/data_explorer/commands.ts create mode 100644 cypress/utils/dashboards/data_explorer/constants.js rename cypress/utils/dashboards/data_explorer/{elements.ts => elements.js} (96%) diff --git a/cypress.config.ts b/cypress.config.ts index 67e7b4f5039b..52eddacb6e99 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -27,8 +27,9 @@ 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, + testIsolation: true, 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 index 4f82d5f3d95e..4abbce49b11b 100644 --- a/cypress/integration/core_opensearch_dashboards/filter_for_value_spec.js +++ b/cypress/integration/core_opensearch_dashboards/filter_for_value_spec.js @@ -11,22 +11,25 @@ describe('filter for value spec', () => { beforeEach(() => { cy.localLogin(Cypress.env('username'), Cypress.env('password')); miscUtils.visitPage('app/data-explorer/discover'); - cy.clickNewSearchButton(); + cy.getNewSearchButton().click(); }); describe('filter actions in table field', () => { describe('index pattern dataset', () => { - // filter actions should not exist for DQL + // filter actions should exist for DQL it('DQL', () => { cy.selectIndexPatternDataset('DQL'); cy.setSearchRelativeDateRange('15', 'Years ago'); cy.checkDocTableFirstFieldFilterForAndOutButton(true); cy.checkDocTableFirstFieldFilterForButtonFiltersCorrectField(); + cy.checkDocTableFirstFieldFilterOutButtonFiltersCorrectField(); }); - // filter actions should not exist for PPL + // filter actions should exist for Lucene it('Lucene', () => { cy.selectIndexPatternDataset('Lucene'); cy.setSearchRelativeDateRange('15', 'Years ago'); cy.checkDocTableFirstFieldFilterForAndOutButton(true); + cy.checkDocTableFirstFieldFilterForButtonFiltersCorrectField(); + cy.checkDocTableFirstFieldFilterOutButtonFiltersCorrectField(); }); // filter actions should not exist for SQL it('SQL', () => { diff --git a/cypress/support/e2e.js b/cypress/support/e2e.js index fa35cf4214b4..b19e490d7080 100644 --- a/cypress/support/e2e.js +++ b/cypress/support/e2e.js @@ -3,4 +3,14 @@ * 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') + +// eslint-disable-next-line no-unused-vars +Cypress.on('uncaught:exception', (_err) => { + // returning false here prevents Cypress from failing the test + return false; +}); diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.ts deleted file mode 100644 index ae89c76268a3..000000000000 --- a/cypress/support/e2e.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import '../utils/commands'; -import '../utils/dashboards/data_explorer/commands'; - -// Alternatively you can use CommonJS syntax: -// require('./commands') - -Cypress.on('uncaught:exception', (_err) => { - // returning false here prevents Cypress from failing the test - return false; -}); diff --git a/cypress/utils/commands.ts b/cypress/utils/commands.js similarity index 62% rename from cypress/utils/commands.ts rename to cypress/utils/commands.js index ea07f3ed4406..1ab606ef788b 100644 --- a/cypress/utils/commands.ts +++ b/cypress/utils/commands.js @@ -11,21 +11,35 @@ import { const miscUtils = new MiscUtils(cy); const loginPage = new LoginPage(cy); -// --- Typed commands -- - +/** + * 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); }); -Cypress.Commands.add('findElementByTestId', (testId, options = {}) => { - return cy.find(`[data-test-subj="${testId}"]`, 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. + */ Cypress.Commands.add('localLogin', (username, password) => { miscUtils.visitPage('/app/home'); loginPage.enterUserName(username); @@ -33,15 +47,3 @@ Cypress.Commands.add('localLogin', (username, password) => { loginPage.submit(); cy.url().should('contain', '/app/home'); }); - -Cypress.Commands.add('waitForLoader', () => { - const opts = { log: false }; - - Cypress.log({ - name: 'waitForPageLoad', - displayName: 'wait', - message: 'page load', - }); - cy.wait(Cypress.env('WAIT_FOR_LOADER_BUFFER_MS')); - cy.getElementByTestId('recentItemsSectionButton', opts); // Update to `homeLoader` once useExpandedHeader is enabled -}); diff --git a/cypress/utils/dashboards/data_explorer/commands.js b/cypress/utils/dashboards/data_explorer/commands.js new file mode 100644 index 000000000000..f8a46dcfb0a3 --- /dev/null +++ b/cypress/utils/dashboards/data_explorer/commands.js @@ -0,0 +1,209 @@ +/* + * 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'; + +/** + * Get the New Search button. + */ +Cypress.Commands.add('getNewSearchButton', () => { + return cy + .getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.NEW_SEARCH_BUTTON, { timeout: 10000 }) + .should('be.visible'); +}); + +/** + * Open window to select Dataset + */ +Cypress.Commands.add('openDatasetExplorerWindow', () => { + cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_SELECTOR_BUTTON).click(); + cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.ALL_DATASETS_BUTTON).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" + */ +Cypress.Commands.add('selectDatasetTimeField', (timeField) => { + cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_SELECTOR_TIME_SELECTOR).select( + timeField + ); +}); + +/** + * Select a language in the Dataset Selector for Index + * @param datasetLanguage Index supports "OpenSearch SQL" and "PPL" + */ +Cypress.Commands.add('selectIndexDatasetLanguage', (datasetLanguage) => { + cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_SELECTOR_LANGUAGE_SELECTOR).select( + datasetLanguage + ); + switch (datasetLanguage) { + case 'PPL': + cy.selectDatasetTimeField("I don't want to use the time filter"); + break; + } + cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_SELECTOR_SELECT_DATA_BUTTON).click(); +}); + +/** + * Select an index dataset. + * @param datasetLanguage Index supports "DQL", "Lucene", "OpenSearch SQL" and "PPL" + */ +Cypress.Commands.add('selectIndexDataset', (datasetLanguage) => { + cy.openDatasetExplorerWindow(); + cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_EXPLORER_WINDOW) + .contains('Indexes') + .click(); + cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_EXPLORER_WINDOW) + .contains(INDEX_CLUSTER_NAME, { timeout: 10000 }) + .click(); + cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_EXPLORER_WINDOW) + .contains(INDEX_NAME, { timeout: 10000 }) + .click(); + cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_SELECTOR_NEXT_BUTTON).click(); + cy.selectIndexDatasetLanguage(datasetLanguage); +}); + +/** + * Select a language in the Dataset Selector for Index Pattern + * @param datasetLanguage Index supports "DQL", "Lucene", "OpenSearch SQL" and "PPL" + */ +Cypress.Commands.add('selectIndexPatternDatasetLanguage', (datasetLanguage) => { + cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_SELECTOR_LANGUAGE_SELECTOR).select( + datasetLanguage + ); + cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_SELECTOR_SELECT_DATA_BUTTON).click(); +}); + +/** + * Select an index pattern dataset. + * @param datasetLanguage Index supports "OpenSearch SQL" and "PPL" + */ +Cypress.Commands.add('selectIndexPatternDataset', (datasetLanguage) => { + cy.openDatasetExplorerWindow(); + cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_EXPLORER_WINDOW) + .contains('Index Patterns') + .click(); + cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_EXPLORER_WINDOW) + .contains(INDEX_PATTERN_NAME, { timeout: 10000 }) + .click(); + cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_SELECTOR_NEXT_BUTTON).click(); + cy.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') + */ +Cypress.Commands.add('setSearchRelativeDateRange', (relativeNumber, relativeUnit) => { + cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.SEARCH_DATE_PICKER_BUTTON).click(); + cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.SEARCH_DATE_PICKER_RELATIVE_TAB).click(); + cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.SEARCH_DATE_RELATIVE_PICKER_INPUT) + .clear() + .type(relativeNumber); + cy.getElementByTestId( + DATA_EXPLORER_PAGE_ELEMENTS.SEARCH_DATE_RELATIVE_PICKER_UNIT_SELECTOR + ).select(relativeUnit); + cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.QUERY_SUBMIT_BUTTON).click(); +}); + +/** + * Get specific row of DocTable. + * @param rowNumber Integer starts from 0 for the first row + */ +Cypress.Commands.add('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 + */ +Cypress.Commands.add('getDocTableField', (columnNumber, rowNumber) => { + return cy + .getDocTableRow(rowNumber) + .findElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DOC_TABLE_ROW_FIELD) + .eq(columnNumber); +}); + +/** + * Check the filter pill text matches expectedFilterText. + * @param expectedFilterText expected text in filter pill. + */ +Cypress.Commands.add('checkFilterPillText', (expectedFilterText) => { + cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.GLOBAL_QUERY_EDITOR_FILTER_VALUE, { + timeout: 10000, + }).should('have.text', expectedFilterText); +}); + +/** + * Check the query hit text matches expectedQueryHitText. + * @param expectedQueryHitText expected text for query hits + */ +Cypress.Commands.add('checkQueryHitText', (expectedQueryHitText) => { + cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DISCOVER_QUERY_HITS).should( + 'have.text', + expectedQueryHitText + ); +}); + +/** + * Check for the first Table Field's Filter For and Filter Out button. + * @param isExists Boolean determining if these button should exist + */ +Cypress.Commands.add('checkDocTableFirstFieldFilterForAndOutButton', (isExists) => { + const shouldText = isExists ? 'exist' : 'not.exist'; + cy.getDocTableField(0, 0).within(() => { + cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.TABLE_FIELD_FILTER_FOR_BUTTON).should( + shouldText + ); + cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.TABLE_FIELD_FILTER_OUT_BUTTON).should( + shouldText + ); + }); +}); + +/** + * Check the Doc Table first Field's Filter For button filters the correct value. + */ +Cypress.Commands.add('checkDocTableFirstFieldFilterForButtonFiltersCorrectField', () => { + cy.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(); + cy.checkFilterPillText(filterFieldText); + cy.checkQueryHitText('1'); // checkQueryHitText must be in front of checking first line text to give time for DocTable to update. + cy.getDocTableField(0, 0).find('span span').should('have.text', filterFieldText); + }); + cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.GLOBAL_FILTER_BAR) + .find('[aria-label="Delete"]') + .click(); + cy.checkQueryHitText('10,000'); +}); + +/** + * Check the Doc Table first Field's Filter Out button filters the correct value. + */ +Cypress.Commands.add('checkDocTableFirstFieldFilterOutButtonFiltersCorrectField', () => { + cy.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(); + cy.checkFilterPillText(filterFieldText); + cy.checkQueryHitText('9,999'); // checkQueryHitText must be in front of checking first line text to give time for DocTable to update. + cy.getDocTableField(0, 0).find('span span').should('not.have.text', filterFieldText); + }); + cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.GLOBAL_FILTER_BAR) + .find('[aria-label="Delete"]') + .click(); + cy.checkQueryHitText('10,000'); +}); diff --git a/cypress/utils/dashboards/data_explorer/commands.ts b/cypress/utils/dashboards/data_explorer/commands.ts deleted file mode 100644 index 19a9cf62b3ba..000000000000 --- a/cypress/utils/dashboards/data_explorer/commands.ts +++ /dev/null @@ -1,171 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { DATA_EXPLORER_PAGE_ELEMENTS } from './elements.js'; - -/** - * Click on the New Search button. - */ -Cypress.Commands.add('clickNewSearchButton', () => { - cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.NEW_SEARCH_BUTTON, { timeout: 10000 }) - .should('be.visible') - .click(); -}); - -/** - * Open window to select Dataset - */ -Cypress.Commands.add('openDatasetExplorerWindow', () => { - cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_SELECTOR_BUTTON).click(); - cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.ALL_DATASETS_BUTTON).click(); -}); - -/** - * Select a Time Field in the Dataset Selector - */ -Cypress.Commands.add('selectDatasetTimeField', (timeField) => { - cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_SELECTOR_TIME_SELECTOR).select( - timeField - ); -}); - -/** - * Select a language in the Dataset Selector for Index - */ -Cypress.Commands.add('selectIndexDatasetLanguage', (datasetLanguage) => { - cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_SELECTOR_LANGUAGE_SELECTOR).select( - datasetLanguage - ); - switch (datasetLanguage) { - case 'PPL': - this.selectDatasetTimeField("I don't want to use the time filter"); - break; - } - cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_SELECTOR_SELECT_DATA_BUTTON).click(); -}); - -/** - * Select a language in the Dataset Selector for Index Pattern - */ -Cypress.Commands.add('selectIndexPatternDatasetLanguage', (datasetLanguage) => { - cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_SELECTOR_LANGUAGE_SELECTOR).select( - datasetLanguage - ); - cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_SELECTOR_SELECT_DATA_BUTTON).click(); -}); - -/** - * Select an index dataset. - */ -Cypress.Commands.add('selectIndexDataset', (datasetLanguage) => { - this.openDatasetExplorerWindow(); - cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_EXPLORER_WINDOW) - .contains('Indexes') - .click(); - cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_EXPLORER_WINDOW) - .contains(Cypress.env('INDEX_CLUSTER_NAME'), { timeout: 10000 }) - .click(); - cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_EXPLORER_WINDOW) - .contains(Cypress.env('INDEX_NAME'), { timeout: 10000 }) - .click(); - cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_SELECTOR_NEXT_BUTTON).click(); - this.selectIndexDatasetLanguage(datasetLanguage); -}); - -/** - * Select an index pattern dataset. - */ -Cypress.Commands.add('selectIndexPatternDataset', (datasetLanguage) => { - this.openDatasetExplorerWindow(); - cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_EXPLORER_WINDOW) - .contains('Index Patterns') - .click(); - cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_EXPLORER_WINDOW) - .contains(Cypress.env('INDEX_PATTERN_NAME'), { timeout: 10000 }) - .click(); - cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_SELECTOR_NEXT_BUTTON).click(); - this.selectIndexPatternDatasetLanguage(datasetLanguage); -}); - -/** - * set search Date range - */ -Cypress.Commands.add('setSearchRelativeDateRange', (relativeNumber, relativeUnit) => { - cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.SEARCH_DATE_PICKER_BUTTON).click(); - cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.SEARCH_DATE_PICKER_RELATIVE_TAB).click(); - cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.SEARCH_DATE_RELATIVE_PICKER_INPUT) - .clear() - .type(relativeNumber); - cy.getElementByTestId( - DATA_EXPLORER_PAGE_ELEMENTS.SEARCH_DATE_RELATIVE_PICKER_UNIT_SELECTOR - ).select(relativeUnit); - cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.QUERY_SUBMIT_BUTTON).click(); -}); - -/** - * check for the first Table Field's Filter For and Filter Out button. - */ -Cypress.Commands.add('checkDocTableFirstFieldFilterForAndOutButton', (isExists) => { - const shouldText = isExists ? 'exist' : 'not.exist'; - cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DOC_TABLE) - .get('tbody tr') - .first() - .within(() => { - cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.TABLE_FIELD_FILTER_FOR_BUTTON).should( - shouldText - ); - cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.TABLE_FIELD_FILTER_OUT_BUTTON).should( - shouldText - ); - }); -}); - -/** - * Check the Doc Table first Field's Filter For button filters the correct value. - */ -Cypress.Commands.add('checkDocTableFirstFieldFilterForButtonFiltersCorrectField', () => { - cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DOC_TABLE) - .find('tbody tr') - .first() - .findElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DOC_TABLE_ROW_FIELD) - .then(($field) => { - const fieldText = $field.find('span span').text(); - $field - .findElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.TABLE_FIELD_FILTER_FOR_BUTTON) - .trigger(click); - cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.GLOBAL_QUERY_EDITOR_FILTER_VALUE, { - timeout: 10000, - }).should('have.text', fieldText); - cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DOC_TABLE) - .find('tbody tr') - .first() - .findElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DOC_TABLE_ROW_FIELD) - .find('span span') - .should('have.text', fieldText); - cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DISCOVER_QUERY_HITS).should( - 'have.text', - '1' - ); - }); -}); - -/** - * Check the Doc Table first Field's Filter Out button filters the correct value. - */ -Cypress.Commands.add('checkDocTableFirstFieldFilterOutButtonFiltersCorrectField', () => { - cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DOC_TABLE) - .find('tbody tr') - .first() - .findElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DOC_TABLE_ROW_FIELD) - .then(($field) => { - const fieldText = $field.find('span span').text(); - $field - .findElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.TABLE_FIELD_FILTER_OUT_BUTTON) - .trigger(click); - cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.GLOBAL_QUERY_EDITOR_FILTER_VALUE, { - timeout: 10000, - }).should('have.text', fieldText); - }); -}); 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/elements.ts b/cypress/utils/dashboards/data_explorer/elements.js similarity index 96% rename from cypress/utils/dashboards/data_explorer/elements.ts rename to cypress/utils/dashboards/data_explorer/elements.js index 5b28bbef59cb..0ac45ad63b0c 100644 --- a/cypress/utils/dashboards/data_explorer/elements.ts +++ b/cypress/utils/dashboards/data_explorer/elements.js @@ -23,4 +23,5 @@ export const DATA_EXPLORER_PAGE_ELEMENTS = { SEARCH_DATE_RELATIVE_PICKER_UNIT_SELECTOR: 'superDatePickerRelativeDateInputUnitSelector', QUERY_SUBMIT_BUTTON: 'querySubmitButton', GLOBAL_QUERY_EDITOR_FILTER_VALUE: 'globalFilterLabelValue', + GLOBAL_FILTER_BAR: 'globalFilterBar', }; diff --git a/src/plugins/data/public/ui/filter_bar/filter_bar.tsx b/src/plugins/data/public/ui/filter_bar/filter_bar.tsx index 822f962698e2..26fb97606001 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_bar.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_bar.tsx @@ -78,7 +78,12 @@ function FilterBarUI(props: Props) { function renderItems() { return props.filters.map((filter, i) => ( - + Date: Thu, 5 Dec 2024 00:33:39 -0800 Subject: [PATCH 18/31] Fix filter_label.test.tsx failing due to added data-test-subj --- .../ui/filter_bar/filter_editor/lib/filter_label.test.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_label.test.tsx b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_label.test.tsx index 48fcb25dc388..7606fe29fdc7 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_label.test.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_label.test.tsx @@ -95,6 +95,7 @@ test('alias with warning status', () => { : 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 From 77e69b57a9866e10b47bb2af1dd09647dcf996b5 Mon Sep 17 00:00:00 2001 From: yuboluo Date: Thu, 5 Dec 2024 17:06:58 +0800 Subject: [PATCH 19/31] [Workspace] Clear the attribute of error objects (#9003) * clear the attribute of error objects Signed-off-by: yubonluo * Changeset file for PR #9003 created/updated * Changeset file for PR #9003 deleted --------- Signed-off-by: yubonluo Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> --- .../workspace_id_consumer_wrapper.test.ts | 4 +- .../workspace_id_consumer_wrapper.test.ts | 41 ++++++++++--------- .../workspace_id_consumer_wrapper.ts | 5 ++- 3 files changed, 27 insertions(+), 23 deletions(-) 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 f597dd369272..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 @@ -485,9 +485,7 @@ describe('workspace_id_consumer integration test', () => { ]); expect(bulkGetResultWithWorkspace.body.saved_objects[0]?.error).toBeUndefined(); expect(bulkGetResultWithWorkspace.body.saved_objects[1].id).toEqual('bar'); - expect(bulkGetResultWithWorkspace.body.saved_objects[1].workspaces).toEqual([ - createdBarWorkspace.id, - ]); + expect(bulkGetResultWithWorkspace.body.saved_objects[1].workspaces).toBeUndefined(); expect(bulkGetResultWithWorkspace.body.saved_objects[1]?.error).toMatchInlineSnapshot(` Object { "error": "Forbidden", 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 fcef67870523..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 @@ -432,8 +432,8 @@ describe('WorkspaceIdConsumerWrapper', () => { { type: 'dashboard', id: 'dashboard_id', - attributes: {}, - references: [], + attributes: { description: 'description' }, + references: ['reference_id'], workspaces: ['foo'], }, { @@ -450,8 +450,8 @@ describe('WorkspaceIdConsumerWrapper', () => { { type: 'visualization', id: 'visualization_id', - attributes: {}, - references: [], + attributes: { description: 'description' }, + references: ['reference_id'], workspaces: ['bar'], }, { @@ -493,9 +493,13 @@ describe('WorkspaceIdConsumerWrapper', () => { Object { "saved_objects": Array [ Object { - "attributes": Object {}, + "attributes": Object { + "description": "description", + }, "id": "dashboard_id", - "references": Array [], + "references": Array [ + "reference_id", + ], "type": "dashboard", "workspaces": Array [ "foo", @@ -522,9 +526,6 @@ describe('WorkspaceIdConsumerWrapper', () => { "id": "visualization_id", "references": Array [], "type": "visualization", - "workspaces": Array [ - "bar", - ], }, Object { "attributes": Object {}, @@ -571,9 +572,13 @@ describe('WorkspaceIdConsumerWrapper', () => { Object { "saved_objects": Array [ Object { - "attributes": Object {}, + "attributes": Object { + "description": "description", + }, "id": "dashboard_id", - "references": Array [], + "references": Array [ + "reference_id", + ], "type": "dashboard", "workspaces": Array [ "foo", @@ -600,9 +605,6 @@ describe('WorkspaceIdConsumerWrapper', () => { "id": "visualization_id", "references": Array [], "type": "visualization", - "workspaces": Array [ - "bar", - ], }, Object { "attributes": Object {}, @@ -688,9 +690,13 @@ describe('WorkspaceIdConsumerWrapper', () => { Object { "saved_objects": Array [ Object { - "attributes": Object {}, + "attributes": Object { + "description": "description", + }, "id": "dashboard_id", - "references": Array [], + "references": Array [ + "reference_id", + ], "type": "dashboard", "workspaces": Array [ "foo", @@ -717,9 +723,6 @@ describe('WorkspaceIdConsumerWrapper', () => { "id": "visualization_id", "references": Array [], "type": "visualization", - "workspaces": Array [ - "bar", - ], }, Object { "attributes": Object {}, 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 f6efb690c5cd..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 @@ -195,7 +195,10 @@ export class WorkspaceIdConsumerWrapper { return this.validateObjectInAWorkspace(object, workspaces[0], wrapperOptions.request) ? object : { - ...object, + id: object.id, + type: object.type, + attributes: {} as T, + references: [], error: { ...generateSavedObjectsForbiddenError().output.payload, }, From 4d096c460070e95b4dc062d7fa25ae489ed13383 Mon Sep 17 00:00:00 2001 From: Joshua Li Date: Thu, 5 Dec 2024 09:04:14 -0800 Subject: [PATCH 20/31] bump `url` to 0.11.4 (#8611) * bump url to 0.11.4 Signed-off-by: Joshua Li * Changeset file for PR #8611 created/updated --------- Signed-off-by: Joshua Li Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> --- changelogs/fragments/8611.yml | 2 + package.json | 1 + yarn.lock | 108 +++++++++++++++++++++++++++------- 3 files changed, 90 insertions(+), 21 deletions(-) create mode 100644 changelogs/fragments/8611.yml 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/package.json b/package.json index 0a103b9fdab1..7c3bb252ecef 100644 --- a/package.json +++ b/package.json @@ -124,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" diff --git a/yarn.lock b/yarn.lock index 537af6f3662e..69ddeeeabd5b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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" @@ -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" From d0d5665b1dd325a8c7906cb8ba85f8d8181ca069 Mon Sep 17 00:00:00 2001 From: Argus Li Date: Thu, 5 Dec 2024 12:12:18 -0800 Subject: [PATCH 21/31] Address comments. Change testIsolation to be false, ignore uncaught errors to be more selective. --- cypress.config.ts | 2 +- cypress/support/e2e.js | 11 +++++++---- cypress/utils/commands.js | 14 +++++++++----- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/cypress.config.ts b/cypress.config.ts index 52eddacb6e99..d1363c2bf7ca 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -29,7 +29,7 @@ module.exports = defineConfig({ baseUrl: 'http://localhost:5601', supportFile: 'cypress/support/e2e.{js,jsx,ts,tsx}', specPattern: 'cypress/integration/**/*_spec.{js,jsx,ts,tsx}', - testIsolation: true, + testIsolation: false, setupNodeEvents, }, }); diff --git a/cypress/support/e2e.js b/cypress/support/e2e.js index b19e490d7080..fc5a308e4134 100644 --- a/cypress/support/e2e.js +++ b/cypress/support/e2e.js @@ -9,8 +9,11 @@ import '../utils/dashboards/data_explorer/commands.js'; // Alternatively you can use CommonJS syntax: // require('./commands') -// eslint-disable-next-line no-unused-vars -Cypress.on('uncaught:exception', (_err) => { - // returning false here prevents Cypress from failing the test - return false; +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 1ab606ef788b..d30308576a80 100644 --- a/cypress/utils/commands.js +++ b/cypress/utils/commands.js @@ -38,12 +38,16 @@ Cypress.Commands.add( ); /** - * Go to the local instance of OSD's home page and login. + * Go to the local instance of OSD's home page and login if needed. */ Cypress.Commands.add('localLogin', (username, password) => { miscUtils.visitPage('/app/home'); - loginPage.enterUserName(username); - loginPage.enterPassword(password); - loginPage.submit(); - cy.url().should('contain', '/app/home'); + cy.url().then(($url) => { + if ($url.includes('login')) { + loginPage.enterUserName(username); + loginPage.enterPassword(password); + loginPage.submit(); + } + cy.url().should('contain', '/app/home'); + }); }); From 5e9ab7398a14719a7f09c7b09114b293ec539ce9 Mon Sep 17 00:00:00 2001 From: Joshua Li Date: Thu, 5 Dec 2024 10:32:08 -0800 Subject: [PATCH 22/31] [Discover] use roundUp when converting timestamp for PPL (#8935) Signed-off-by: Joshua Li Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> --- changelogs/fragments/8935.yml | 2 ++ packages/opensearch-datemath/index.d.ts | 2 ++ .../data/common/data_frames/utils.test.ts | 27 +++++++++++++++++++ src/plugins/data/common/data_frames/utils.ts | 6 ++--- 4 files changed, 34 insertions(+), 3 deletions(-) create mode 100644 changelogs/fragments/8935.yml create mode 100644 src/plugins/data/common/data_frames/utils.test.ts 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/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/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 }; }; From 7d1c48c8bf9bed061b498a0668d608b6708cfe18 Mon Sep 17 00:00:00 2001 From: Argus Li Date: Thu, 5 Dec 2024 15:36:14 -0800 Subject: [PATCH 23/31] Reformat to use POM. Search bar objects that are non-page specific have been kept as commands. --- .../filter_for_value_spec.js | 39 +-- .../dashboards/data_explorer/commands.js | 197 ++----------- .../data_explorer/data_explorer_page.po.js | 265 ++++++++++++++++++ 3 files changed, 309 insertions(+), 192 deletions(-) create mode 100644 cypress/utils/dashboards/data_explorer/data_explorer_page.po.js diff --git a/cypress/integration/core_opensearch_dashboards/filter_for_value_spec.js b/cypress/integration/core_opensearch_dashboards/filter_for_value_spec.js index 4abbce49b11b..02016d17e455 100644 --- a/cypress/integration/core_opensearch_dashboards/filter_for_value_spec.js +++ b/cypress/integration/core_opensearch_dashboards/filter_for_value_spec.js @@ -4,6 +4,7 @@ */ 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); @@ -17,42 +18,42 @@ describe('filter for value spec', () => { describe('index pattern dataset', () => { // filter actions should exist for DQL it('DQL', () => { - cy.selectIndexPatternDataset('DQL'); - cy.setSearchRelativeDateRange('15', 'Years ago'); - cy.checkDocTableFirstFieldFilterForAndOutButton(true); - cy.checkDocTableFirstFieldFilterForButtonFiltersCorrectField(); - cy.checkDocTableFirstFieldFilterOutButtonFiltersCorrectField(); + DataExplorerPage.selectIndexPatternDataset('DQL'); + DataExplorerPage.setSearchRelativeDateRange('15', 'Years ago'); + DataExplorerPage.checkDocTableFirstFieldFilterForAndOutButton(true); + DataExplorerPage.checkDocTableFirstFieldFilterForButtonFiltersCorrectField(); + DataExplorerPage.checkDocTableFirstFieldFilterOutButtonFiltersCorrectField(); }); // filter actions should exist for Lucene it('Lucene', () => { - cy.selectIndexPatternDataset('Lucene'); - cy.setSearchRelativeDateRange('15', 'Years ago'); - cy.checkDocTableFirstFieldFilterForAndOutButton(true); - cy.checkDocTableFirstFieldFilterForButtonFiltersCorrectField(); - cy.checkDocTableFirstFieldFilterOutButtonFiltersCorrectField(); + DataExplorerPage.selectIndexPatternDataset('Lucene'); + DataExplorerPage.setSearchRelativeDateRange('15', 'Years ago'); + DataExplorerPage.checkDocTableFirstFieldFilterForAndOutButton(true); + DataExplorerPage.checkDocTableFirstFieldFilterForButtonFiltersCorrectField(); + DataExplorerPage.checkDocTableFirstFieldFilterOutButtonFiltersCorrectField(); }); // filter actions should not exist for SQL it('SQL', () => { - cy.selectIndexPatternDataset('OpenSearch SQL'); - cy.checkDocTableFirstFieldFilterForAndOutButton(false); + DataExplorerPage.selectIndexPatternDataset('OpenSearch SQL'); + DataExplorerPage.checkDocTableFirstFieldFilterForAndOutButton(false); }); // filter actions should not exist for PPL it('PPL', () => { - cy.selectIndexPatternDataset('PPL'); - cy.setSearchRelativeDateRange('15', 'Years ago'); - cy.checkDocTableFirstFieldFilterForAndOutButton(false); + DataExplorerPage.selectIndexPatternDataset('PPL'); + DataExplorerPage.setSearchRelativeDateRange('15', 'Years ago'); + DataExplorerPage.checkDocTableFirstFieldFilterForAndOutButton(false); }); }); describe('index dataset', () => { // filter actions should not exist for SQL it('SQL', () => { - cy.selectIndexDataset('OpenSearch SQL'); - cy.checkDocTableFirstFieldFilterForAndOutButton(false); + DataExplorerPage.selectIndexDataset('OpenSearch SQL'); + DataExplorerPage.checkDocTableFirstFieldFilterForAndOutButton(false); }); // filter actions should not exist for PPL it('PPL', () => { - cy.selectIndexDataset('PPL'); - cy.checkDocTableFirstFieldFilterForAndOutButton(false); + DataExplorerPage.selectIndexDataset('PPL'); + DataExplorerPage.checkDocTableFirstFieldFilterForAndOutButton(false); }); }); }); diff --git a/cypress/utils/dashboards/data_explorer/commands.js b/cypress/utils/dashboards/data_explorer/commands.js index f8a46dcfb0a3..37c785c74537 100644 --- a/cypress/utils/dashboards/data_explorer/commands.js +++ b/cypress/utils/dashboards/data_explorer/commands.js @@ -4,7 +4,6 @@ */ import { DATA_EXPLORER_PAGE_ELEMENTS } from './elements.js'; -import { INDEX_CLUSTER_NAME, INDEX_NAME, INDEX_PATTERN_NAME } from './constants.js'; /** * Get the New Search button. @@ -16,194 +15,46 @@ Cypress.Commands.add('getNewSearchButton', () => { }); /** - * Open window to select Dataset + * Get the Query Submit button. */ -Cypress.Commands.add('openDatasetExplorerWindow', () => { - cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_SELECTOR_BUTTON).click(); - cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.ALL_DATASETS_BUTTON).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" - */ -Cypress.Commands.add('selectDatasetTimeField', (timeField) => { - cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_SELECTOR_TIME_SELECTOR).select( - timeField - ); -}); - -/** - * Select a language in the Dataset Selector for Index - * @param datasetLanguage Index supports "OpenSearch SQL" and "PPL" - */ -Cypress.Commands.add('selectIndexDatasetLanguage', (datasetLanguage) => { - cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_SELECTOR_LANGUAGE_SELECTOR).select( - datasetLanguage - ); - switch (datasetLanguage) { - case 'PPL': - cy.selectDatasetTimeField("I don't want to use the time filter"); - break; - } - cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_SELECTOR_SELECT_DATA_BUTTON).click(); -}); - -/** - * Select an index dataset. - * @param datasetLanguage Index supports "DQL", "Lucene", "OpenSearch SQL" and "PPL" - */ -Cypress.Commands.add('selectIndexDataset', (datasetLanguage) => { - cy.openDatasetExplorerWindow(); - cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_EXPLORER_WINDOW) - .contains('Indexes') - .click(); - cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_EXPLORER_WINDOW) - .contains(INDEX_CLUSTER_NAME, { timeout: 10000 }) - .click(); - cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_EXPLORER_WINDOW) - .contains(INDEX_NAME, { timeout: 10000 }) - .click(); - cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_SELECTOR_NEXT_BUTTON).click(); - cy.selectIndexDatasetLanguage(datasetLanguage); -}); - -/** - * Select a language in the Dataset Selector for Index Pattern - * @param datasetLanguage Index supports "DQL", "Lucene", "OpenSearch SQL" and "PPL" - */ -Cypress.Commands.add('selectIndexPatternDatasetLanguage', (datasetLanguage) => { - cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_SELECTOR_LANGUAGE_SELECTOR).select( - datasetLanguage - ); - cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_SELECTOR_SELECT_DATA_BUTTON).click(); -}); - -/** - * Select an index pattern dataset. - * @param datasetLanguage Index supports "OpenSearch SQL" and "PPL" - */ -Cypress.Commands.add('selectIndexPatternDataset', (datasetLanguage) => { - cy.openDatasetExplorerWindow(); - cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_EXPLORER_WINDOW) - .contains('Index Patterns') - .click(); - cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_EXPLORER_WINDOW) - .contains(INDEX_PATTERN_NAME, { timeout: 10000 }) - .click(); - cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_SELECTOR_NEXT_BUTTON).click(); - cy.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') - */ -Cypress.Commands.add('setSearchRelativeDateRange', (relativeNumber, relativeUnit) => { - cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.SEARCH_DATE_PICKER_BUTTON).click(); - cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.SEARCH_DATE_PICKER_RELATIVE_TAB).click(); - cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.SEARCH_DATE_RELATIVE_PICKER_INPUT) - .clear() - .type(relativeNumber); - cy.getElementByTestId( - DATA_EXPLORER_PAGE_ELEMENTS.SEARCH_DATE_RELATIVE_PICKER_UNIT_SELECTOR - ).select(relativeUnit); - cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.QUERY_SUBMIT_BUTTON).click(); -}); - -/** - * Get specific row of DocTable. - * @param rowNumber Integer starts from 0 for the first row - */ -Cypress.Commands.add('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 - */ -Cypress.Commands.add('getDocTableField', (columnNumber, rowNumber) => { +Cypress.Commands.add('getQuerySubmitButton', () => { return cy - .getDocTableRow(rowNumber) - .findElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DOC_TABLE_ROW_FIELD) - .eq(columnNumber); -}); - -/** - * Check the filter pill text matches expectedFilterText. - * @param expectedFilterText expected text in filter pill. - */ -Cypress.Commands.add('checkFilterPillText', (expectedFilterText) => { - cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.GLOBAL_QUERY_EDITOR_FILTER_VALUE, { - timeout: 10000, - }).should('have.text', expectedFilterText); + .getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.QUERY_SUBMIT_BUTTON) + .should('be.visible'); }); /** - * Check the query hit text matches expectedQueryHitText. - * @param expectedQueryHitText expected text for query hits + * Get the Search Bar Date Picker button. */ -Cypress.Commands.add('checkQueryHitText', (expectedQueryHitText) => { - cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DISCOVER_QUERY_HITS).should( - 'have.text', - expectedQueryHitText - ); +Cypress.Commands.add('getSearchDatePickerButton', () => { + return cy + .getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.SEARCH_DATE_PICKER_BUTTON) + .should('be.visible'); }); /** - * Check for the first Table Field's Filter For and Filter Out button. - * @param isExists Boolean determining if these button should exist + * Get the Relative Date tab in the Search Bar Date Picker. */ -Cypress.Commands.add('checkDocTableFirstFieldFilterForAndOutButton', (isExists) => { - const shouldText = isExists ? 'exist' : 'not.exist'; - cy.getDocTableField(0, 0).within(() => { - cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.TABLE_FIELD_FILTER_FOR_BUTTON).should( - shouldText - ); - cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.TABLE_FIELD_FILTER_OUT_BUTTON).should( - shouldText - ); - }); +Cypress.Commands.add('getDatePickerRelativeTab', () => { + return cy + .getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.SEARCH_DATE_PICKER_RELATIVE_TAB) + .should('be.visible'); }); /** - * Check the Doc Table first Field's Filter For button filters the correct value. + * Get the Relative Date Input in the Search Bar Date Picker. */ -Cypress.Commands.add('checkDocTableFirstFieldFilterForButtonFiltersCorrectField', () => { - cy.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(); - cy.checkFilterPillText(filterFieldText); - cy.checkQueryHitText('1'); // checkQueryHitText must be in front of checking first line text to give time for DocTable to update. - cy.getDocTableField(0, 0).find('span span').should('have.text', filterFieldText); - }); - cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.GLOBAL_FILTER_BAR) - .find('[aria-label="Delete"]') - .click(); - cy.checkQueryHitText('10,000'); +Cypress.Commands.add('getDatePickerRelativeInput', () => { + return cy + .getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.SEARCH_DATE_RELATIVE_PICKER_INPUT) + .should('be.visible'); }); /** - * Check the Doc Table first Field's Filter Out button filters the correct value. + * Get the Relative Date Unit selector in the Search Bar Date Picker. */ -Cypress.Commands.add('checkDocTableFirstFieldFilterOutButtonFiltersCorrectField', () => { - cy.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(); - cy.checkFilterPillText(filterFieldText); - cy.checkQueryHitText('9,999'); // checkQueryHitText must be in front of checking first line text to give time for DocTable to update. - cy.getDocTableField(0, 0).find('span span').should('not.have.text', filterFieldText); - }); - cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.GLOBAL_FILTER_BAR) - .find('[aria-label="Delete"]') - .click(); - cy.checkQueryHitText('10,000'); +Cypress.Commands.add('getDatePickerRelativeUnitSelector', () => { + return cy + .getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.SEARCH_DATE_RELATIVE_PICKER_UNIT_SELECTOR) + .should('be.visible'); }); 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'); + } +} From 4d403091f00d57e442a1f2332e9d8e82974c4b15 Mon Sep 17 00:00:00 2001 From: Argus Li Date: Tue, 10 Dec 2024 10:04:34 -0800 Subject: [PATCH 24/31] Add support for expanded row filters. Getting disabled filter buttons still not working. --- .../filter_for_value_spec.js | 33 ++++ cypress/utils/commands.js | 32 ++++ .../dashboards/data_explorer/commands.js | 26 +--- .../data_explorer/data_explorer_page.po.js | 141 +++++++++++++++++- .../dashboards/data_explorer/elements.js | 4 + 5 files changed, 217 insertions(+), 19 deletions(-) diff --git a/cypress/integration/core_opensearch_dashboards/filter_for_value_spec.js b/cypress/integration/core_opensearch_dashboards/filter_for_value_spec.js index 02016d17e455..a363fcec75a1 100644 --- a/cypress/integration/core_opensearch_dashboards/filter_for_value_spec.js +++ b/cypress/integration/core_opensearch_dashboards/filter_for_value_spec.js @@ -57,4 +57,37 @@ describe('filter for value spec', () => { }); }); }); + + describe('filter actions in expanded table', () => { + describe('index pattern dataset', () => { + // filter actions should exist for DQL + it('DQL', () => { + DataExplorerPage.selectIndexPatternDataset('DQL'); + DataExplorerPage.setSearchRelativeDateRange('15', 'Years ago'); + DataExplorerPage.checkDocTableFirstExpandedFieldFirstRowFilterForAndOutButtons(true); + DataExplorerPage.checkDocTableFirstExpandedFieldFirstRowFilterForButtonFiltersCorrectField(); + DataExplorerPage.checkDocTableFirstExpandedFieldFirstRowFilterOutButtonFiltersCorrectField(); + }); + // filter actions should exist for Lucene + it('Lucene', () => {}); + // filter actions should not exist for SQL + it('SQL', () => { + DataExplorerPage.selectIndexPatternDataset('DQL'); + DataExplorerPage.setSearchRelativeDateRange('15', 'Years ago'); + DataExplorerPage.checkDocTableFirstExpandedFieldFirstRowFilterForAndOutButtons(false); + }); + // filter actions should not exist for PPL + it('PPL', () => { + DataExplorerPage.selectIndexPatternDataset('DQL'); + DataExplorerPage.setSearchRelativeDateRange('15', 'Years ago'); + DataExplorerPage.checkDocTableFirstExpandedFieldFirstRowFilterForAndOutButtons(false); + }); + }); + describe('index dataset', () => { + // filter actions should not exist for SQL + it('SQL', () => {}); + // filter actions should not exist for PPL + it('PPL', () => {}); + }); + }); }); diff --git a/cypress/utils/commands.js b/cypress/utils/commands.js index d30308576a80..58645e3002af 100644 --- a/cypress/utils/commands.js +++ b/cypress/utils/commands.js @@ -26,6 +26,38 @@ Cypress.Commands.add('getElementsByTestIds', (testIds, options = {}) => { return cy.get(selectors.join(','), options); }); +/** + * Find DOM elements with a data-test-subj id containing the testId. + * @param testId data-test-subj value. + * @param options get options. Default: {} + * @example + * // returns all DOM elements that has a data-test-subj including the string 'table' + * cy.findElementsByTestIdLike('table') + */ +Cypress.Commands.add( + 'findElementsByTestIdLike', + { prevSubject: true }, + (subject, partialTestId, options = {}) => { + return cy.wrap(subject).find(`[data-test-subj*="${partialTestId}"]`, options); + } +); + +/** + * Find DOM element with a data-test-subj id containing the testId. + * @param testId data-test-subj value. + * @param options get options. Default: {} + * @example + * // returns all DOM elements that has a data-test-subj including the string 'table' + * cy.findElementsByTestIdLike('table') + */ +Cypress.Commands.add( + 'findElementByTestId', + { prevSubject: true }, + (subject, partialTestId, options = {}) => { + return cy.wrap(subject).find(`[data-test-subj="${partialTestId}"]`, options); + } +); + /** * Find element from previous chained element by data-test-subj id. */ diff --git a/cypress/utils/dashboards/data_explorer/commands.js b/cypress/utils/dashboards/data_explorer/commands.js index 37c785c74537..b76e5d6f8d1c 100644 --- a/cypress/utils/dashboards/data_explorer/commands.js +++ b/cypress/utils/dashboards/data_explorer/commands.js @@ -9,52 +9,42 @@ 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 }) - .should('be.visible'); + 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) - .should('be.visible'); + 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) - .should('be.visible'); + 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) - .should('be.visible'); + 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) - .should('be.visible'); + 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) - .should('be.visible'); + return cy.getElementByTestId( + DATA_EXPLORER_PAGE_ELEMENTS.SEARCH_DATE_RELATIVE_PICKER_UNIT_SELECTOR + ); }); diff --git a/cypress/utils/dashboards/data_explorer/data_explorer_page.po.js b/cypress/utils/dashboards/data_explorer/data_explorer_page.po.js index f1d2f30605a2..179818ed0bcd 100644 --- a/cypress/utils/dashboards/data_explorer/data_explorer_page.po.js +++ b/cypress/utils/dashboards/data_explorer/data_explorer_page.po.js @@ -115,6 +115,87 @@ export class DataExplorerPage { return cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.GLOBAL_FILTER_BAR); } + /** + * Get Toggle Button for Column in Doc Table Field. + */ + static getDocTableExpandColumnToggleButton() { + return cy + .getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DOC_TABLE_EXPAND_TOGGLE_COLUMN_BUTTON) + .find('button'); + } + + /** + * find all Rows in Doc Table Field Expanded Document. + * @param expandedDocument cypress representation of the Doc Table Field Expanded Document + */ + static findDocTableExpandedDocRows(expandedDocument) { + return expandedDocument.findElementsByTestIdLike( + DATA_EXPLORER_PAGE_ELEMENTS.DOC_TABLE_EXPANDED_DOC_COLUMN_ROW_PREFIX + ); + } + + /** + * Get Row for Column by fieldName in Doc Table Field Expanded Document. + * @param fieldName Field name for row in Expanded Document. + * @example getDocTableExpandedDocColumnRow('id') + */ + static getDocTableExpandedDocRow(fieldName) { + return cy.getElementByTestId( + DATA_EXPLORER_PAGE_ELEMENTS.DOC_TABLE_EXPANDED_DOC_COLUMN_ROW_PREFIX + fieldName + ); + } + + /** + * Get Filter For Button in Doc Table Field Expanded Document Row. + */ + static getDocTableExpandedDocRowFilterForButton() { + return cy.getElementByTestId( + DATA_EXPLORER_PAGE_ELEMENTS.DOC_TABLE_EXPANDED_DOC_COLUMN_ADD_INCLUSIVE_FILTER_BUTTON + ); + } + + /** + * Get Filter Out Button in Doc Table Field Expanded Document Row. + */ + static getDocTableExpandedDocRowFilterOutButton() { + return cy.getElementByTestId( + DATA_EXPLORER_PAGE_ELEMENTS.DOC_TABLE_EXPANDED_DOC_COLUMN_REMOVE_INCLUSIVE_FILTER_BUTTON + ); + } + + /** + * Get the "expandedDocumentRowNumber"th row from the expanded document from the "docTableRowNumber"th row of the DocTable. + * @param docTableRowNumber Integer starts from 0 for the first row + * @param expandedDocumentRowNumber Integer starts from 0 for the first row + * @example + * // returns the first row from the expanded document from the second row of the DocTable. + * getExpandedDocRow(1, 0); + */ + static getExpandedDocRow(docTableRowNumber, expandedDocumentRowNumber) { + return DataExplorerPage.findDocTableExpandedDocRows( + DataExplorerPage.getDocTableRow(docTableRowNumber + 1) + ).eq(expandedDocumentRowNumber); + } + + /** + * Get the value for the "expandedDocumentRowNumber"th row from the expanded document from the "docTableRowNumber"th row of the DocTable. + * @param docTableRowNumber Integer starts from 0 for the first row + * @param expandedDocumentRowNumber Integer starts from 0 for the first row + * @example + * // returns the value of the field from the first row from the expanded document from the second row of the DocTable. + * getExpandedDocRow(1, 0); + */ + static getExpandedDocRowValue(docTableRowNumber, expandedDocumentRowNumber) { + return DataExplorerPage.findDocTableExpandedDocRows( + DataExplorerPage.getDocTableRow(docTableRowNumber + 1) + ) + .eq(expandedDocumentRowNumber) + .find( + `[data-test-subj*="${DATA_EXPLORER_PAGE_ELEMENTS.DOC_TABLE_EXPANDED_DOC_COLUMN_ROW_PREFIX}"]` + ) + .find('span'); + } + /** * Open window to select Dataset */ @@ -197,6 +278,16 @@ export class DataExplorerPage { cy.getQuerySubmitButton().click(); } + /** + * Expand rowNumber of Doc Table. + * @param rowNumber rowNumber of Doc Table starts at 0 for row 1. + */ + static expandDocTableRow(rowNumber) { + DataExplorerPage.getDocTableRow(rowNumber).within(() => { + DataExplorerPage.getDocTableExpandColumnToggleButton().click(); + }); + } + /** * Check the filter pill text matches expectedFilterText. * @param expectedFilterText expected text in filter pill. @@ -214,7 +305,7 @@ export class DataExplorerPage { } /** - * Check for the first Table Field's Filter For and Filter Out button. + * Check if the first Table Field's Filter For and Filter Out buttons exists. * @param isExists Boolean determining if these button should exist */ static checkDocTableFirstFieldFilterForAndOutButton(isExists) { @@ -262,4 +353,52 @@ export class DataExplorerPage { DataExplorerPage.getFilterBar().find('[aria-label="Delete"]').click(); DataExplorerPage.checkQueryHitsText('10,000'); } + + /** + * Check if the first expanded Doc Table Field's first row's Filter For and Filter Out button are disabled. + * @param isEnabled Boolean determining if these buttons are disabled + */ + static checkDocTableFirstExpandedFieldFirstRowFilterForAndOutButtons(isEnabled) { + const shouldText = isEnabled ? 'be.enabled' : 'not.be.enabled'; + DataExplorerPage.expandDocTableRow(0); + DataExplorerPage.getExpandedDocRow(0, 0).within(() => { + DataExplorerPage.getDocTableExpandedDocRowFilterForButton().should(shouldText); + DataExplorerPage.getDocTableExpandedDocRowFilterForButton().should(shouldText); + }); + } + + /** + * Check the first expanded Doc Table Field's first row's Filter For button filters the correct value. + */ + static checkDocTableFirstExpandedFieldFirstRowFilterForButtonFiltersCorrectField() { + DataExplorerPage.getExpandedDocRowValue(0, 0).then(($expandedDocumentRowValue) => { + const filterFieldText = $expandedDocumentRowValue.text(); + DataExplorerPage.getExpandedDocRow(0, 0).within(() => { + DataExplorerPage.getDocTableExpandedDocRowFilterForButton().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.getExpandedDocRowValue(0, 0).should('have.text', filterFieldText); + }); + DataExplorerPage.getFilterBar().find('[aria-label="Delete"]').click(); + DataExplorerPage.checkQueryHitsText('10,000'); + } + + /** + * Check the first expanded Doc Table Field's first row's Filter Out button filters the correct value. + */ + static checkDocTableFirstExpandedFieldFirstRowFilterOutButtonFiltersCorrectField() { + DataExplorerPage.getExpandedDocRowValue(0, 0).then(($expandedDocumentRowValue) => { + const filterFieldText = $expandedDocumentRowValue.text(); + DataExplorerPage.getExpandedDocRow(0, 0).within(() => { + DataExplorerPage.getDocTableExpandedDocRowFilterOutButton().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.expandDocTableRow(0); + DataExplorerPage.getExpandedDocRowValue(0, 0).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 index 0ac45ad63b0c..764c27f1a142 100644 --- a/cypress/utils/dashboards/data_explorer/elements.js +++ b/cypress/utils/dashboards/data_explorer/elements.js @@ -15,6 +15,10 @@ export const DATA_EXPLORER_PAGE_ELEMENTS = { DATASET_SELECTOR_SELECT_DATA_BUTTON: 'advancedSelectorConfirmButton', DOC_TABLE: 'docTable', DOC_TABLE_ROW_FIELD: 'docTableField', + DOC_TABLE_EXPAND_TOGGLE_COLUMN_BUTTON: 'docTableExpandToggleColumn', + DOC_TABLE_EXPANDED_DOC_COLUMN_ROW_PREFIX: 'tableDocViewRow-', + DOC_TABLE_EXPANDED_DOC_COLUMN_ADD_INCLUSIVE_FILTER_BUTTON: 'addInclusiveFilterButton', + DOC_TABLE_EXPANDED_DOC_COLUMN_REMOVE_INCLUSIVE_FILTER_BUTTON: 'removeInclusiveFilterButton', TABLE_FIELD_FILTER_FOR_BUTTON: 'filterForValue', TABLE_FIELD_FILTER_OUT_BUTTON: 'filterOutValue', SEARCH_DATE_PICKER_BUTTON: 'superDatePickerShowDatesButton', From a4ec7c21d1b6759618ad7db55fe035617c6806ec Mon Sep 17 00:00:00 2001 From: Argus Li Date: Tue, 10 Dec 2024 11:04:15 -0800 Subject: [PATCH 25/31] Change constants to discussed constants. --- cypress/utils/dashboards/data_explorer/constants.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cypress/utils/dashboards/data_explorer/constants.js b/cypress/utils/dashboards/data_explorer/constants.js index 657e3201f680..0d06882f205e 100644 --- a/cypress/utils/dashboards/data_explorer/constants.js +++ b/cypress/utils/dashboards/data_explorer/constants.js @@ -3,6 +3,6 @@ * 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*'; +export const INDEX_CLUSTER_NAME = 'data-logs-1'; +export const INDEX_NAME = 'data_logs_small_time_1'; +export const INDEX_PATTERN_NAME = 'data-logs-1::data_logs_small_time_1*'; From 998b496f452d843acb45c2c9805a01f5fe71e87a Mon Sep 17 00:00:00 2001 From: Argus Li Date: Tue, 10 Dec 2024 13:49:50 -0800 Subject: [PATCH 26/31] Refactor to use absolute dates, move the constants to the spec and use new index and index pattern constants. Signed-off-by: Argus Li --- .../constants.js | 2 + .../filter_for_value_spec.js | 37 ++++++---- .../dashboards/data_explorer/commands.js | 72 +++++++++++++++++++ .../data_explorer/data_explorer_page.po.js | 30 +++----- .../dashboards/data_explorer/elements.js | 4 ++ 5 files changed, 109 insertions(+), 36 deletions(-) rename cypress/{utils/dashboards/data_explorer => integration}/constants.js (64%) diff --git a/cypress/utils/dashboards/data_explorer/constants.js b/cypress/integration/constants.js similarity index 64% rename from cypress/utils/dashboards/data_explorer/constants.js rename to cypress/integration/constants.js index 0d06882f205e..9253f0104f6e 100644 --- a/cypress/utils/dashboards/data_explorer/constants.js +++ b/cypress/integration/constants.js @@ -6,3 +6,5 @@ export const INDEX_CLUSTER_NAME = 'data-logs-1'; export const INDEX_NAME = 'data_logs_small_time_1'; export const INDEX_PATTERN_NAME = 'data-logs-1::data_logs_small_time_1*'; +export const SEARCH_ABSOLUTE_START_DATE = 'Dec 31, 2020 @ 16:00:00.000'; +export const SEARCH_ABSOLUTE_END_DATE = 'Dec 31, 2022 @ 14:14:42.801'; diff --git a/cypress/integration/core_opensearch_dashboards/filter_for_value_spec.js b/cypress/integration/core_opensearch_dashboards/filter_for_value_spec.js index a363fcec75a1..a626d01f2533 100644 --- a/cypress/integration/core_opensearch_dashboards/filter_for_value_spec.js +++ b/cypress/integration/core_opensearch_dashboards/filter_for_value_spec.js @@ -5,6 +5,13 @@ import { MiscUtils } from '@opensearch-dashboards-test/opensearch-dashboards-test-library'; import { DataExplorerPage } from '../../utils/dashboards/data_explorer/data_explorer_page.po'; +import { + INDEX_CLUSTER_NAME, + INDEX_NAME, + INDEX_PATTERN_NAME, + SEARCH_ABSOLUTE_START_DATE, + SEARCH_ABSOLUTE_END_DATE, +} from '../constants.js'; const miscUtils = new MiscUtils(cy); @@ -18,41 +25,41 @@ describe('filter for value spec', () => { describe('index pattern dataset', () => { // filter actions should exist for DQL it('DQL', () => { - DataExplorerPage.selectIndexPatternDataset('DQL'); - DataExplorerPage.setSearchRelativeDateRange('15', 'Years ago'); + DataExplorerPage.selectIndexPatternDataset(INDEX_PATTERN_NAME, 'DQL'); + cy.setSearchAbsoluteDateRange(SEARCH_ABSOLUTE_START_DATE, SEARCH_ABSOLUTE_END_DATE); DataExplorerPage.checkDocTableFirstFieldFilterForAndOutButton(true); DataExplorerPage.checkDocTableFirstFieldFilterForButtonFiltersCorrectField(); DataExplorerPage.checkDocTableFirstFieldFilterOutButtonFiltersCorrectField(); }); // filter actions should exist for Lucene it('Lucene', () => { - DataExplorerPage.selectIndexPatternDataset('Lucene'); - DataExplorerPage.setSearchRelativeDateRange('15', 'Years ago'); + DataExplorerPage.selectIndexPatternDataset(INDEX_PATTERN_NAME, 'Lucene'); + cy.setSearchAbsoluteDateRange(SEARCH_ABSOLUTE_START_DATE, SEARCH_ABSOLUTE_END_DATE); DataExplorerPage.checkDocTableFirstFieldFilterForAndOutButton(true); DataExplorerPage.checkDocTableFirstFieldFilterForButtonFiltersCorrectField(); DataExplorerPage.checkDocTableFirstFieldFilterOutButtonFiltersCorrectField(); }); // filter actions should not exist for SQL it('SQL', () => { - DataExplorerPage.selectIndexPatternDataset('OpenSearch SQL'); + DataExplorerPage.selectIndexPatternDataset(INDEX_PATTERN_NAME, 'OpenSearch SQL'); DataExplorerPage.checkDocTableFirstFieldFilterForAndOutButton(false); }); // filter actions should not exist for PPL it('PPL', () => { - DataExplorerPage.selectIndexPatternDataset('PPL'); - DataExplorerPage.setSearchRelativeDateRange('15', 'Years ago'); + DataExplorerPage.selectIndexPatternDataset(INDEX_PATTERN_NAME, 'PPL'); + cy.setSearchAbsoluteDateRange(SEARCH_ABSOLUTE_START_DATE, SEARCH_ABSOLUTE_END_DATE); DataExplorerPage.checkDocTableFirstFieldFilterForAndOutButton(false); }); }); describe('index dataset', () => { // filter actions should not exist for SQL it('SQL', () => { - DataExplorerPage.selectIndexDataset('OpenSearch SQL'); + DataExplorerPage.selectIndexDataset(INDEX_CLUSTER_NAME, INDEX_NAME, 'OpenSearch SQL'); DataExplorerPage.checkDocTableFirstFieldFilterForAndOutButton(false); }); // filter actions should not exist for PPL it('PPL', () => { - DataExplorerPage.selectIndexDataset('PPL'); + DataExplorerPage.selectIndexDataset(INDEX_CLUSTER_NAME, INDEX_NAME, 'PPL'); DataExplorerPage.checkDocTableFirstFieldFilterForAndOutButton(false); }); }); @@ -62,8 +69,8 @@ describe('filter for value spec', () => { describe('index pattern dataset', () => { // filter actions should exist for DQL it('DQL', () => { - DataExplorerPage.selectIndexPatternDataset('DQL'); - DataExplorerPage.setSearchRelativeDateRange('15', 'Years ago'); + DataExplorerPage.selectIndexPatternDataset(INDEX_PATTERN_NAME, 'DQL'); + cy.setSearchAbsoluteDateRange(SEARCH_ABSOLUTE_START_DATE, SEARCH_ABSOLUTE_END_DATE); DataExplorerPage.checkDocTableFirstExpandedFieldFirstRowFilterForAndOutButtons(true); DataExplorerPage.checkDocTableFirstExpandedFieldFirstRowFilterForButtonFiltersCorrectField(); DataExplorerPage.checkDocTableFirstExpandedFieldFirstRowFilterOutButtonFiltersCorrectField(); @@ -72,14 +79,14 @@ describe('filter for value spec', () => { it('Lucene', () => {}); // filter actions should not exist for SQL it('SQL', () => { - DataExplorerPage.selectIndexPatternDataset('DQL'); - DataExplorerPage.setSearchRelativeDateRange('15', 'Years ago'); + DataExplorerPage.selectIndexPatternDataset(INDEX_PATTERN_NAME, 'DQL'); + cy.setSearchAbsoluteDateRange(SEARCH_ABSOLUTE_START_DATE, SEARCH_ABSOLUTE_END_DATE); DataExplorerPage.checkDocTableFirstExpandedFieldFirstRowFilterForAndOutButtons(false); }); // filter actions should not exist for PPL it('PPL', () => { - DataExplorerPage.selectIndexPatternDataset('DQL'); - DataExplorerPage.setSearchRelativeDateRange('15', 'Years ago'); + DataExplorerPage.selectIndexPatternDataset(INDEX_PATTERN_NAME, 'DQL'); + cy.setSearchAbsoluteDateRange(SEARCH_ABSOLUTE_START_DATE, SEARCH_ABSOLUTE_END_DATE); DataExplorerPage.checkDocTableFirstExpandedFieldFirstRowFilterForAndOutButtons(false); }); }); diff --git a/cypress/utils/dashboards/data_explorer/commands.js b/cypress/utils/dashboards/data_explorer/commands.js index b76e5d6f8d1c..e9a3b6daf3f8 100644 --- a/cypress/utils/dashboards/data_explorer/commands.js +++ b/cypress/utils/dashboards/data_explorer/commands.js @@ -26,6 +26,20 @@ Cypress.Commands.add('getSearchDatePickerButton', () => { return cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.SEARCH_DATE_PICKER_BUTTON); }); +/** + * Get the Search Bar Date Picker Start Date button. + */ +Cypress.Commands.add('getSearchDatePickerStartDateButton', () => { + return cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.SEARCH_DATE_PICKER_START_DATE_BUTTON); +}); + +/** + * Get the Search Bar Date Picker End Date button. + */ +Cypress.Commands.add('getSearchDatePickerEndDateButton', () => { + return cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.SEARCH_DATE_PICKER_END_DATE_BUTTON); +}); + /** * Get the Relative Date tab in the Search Bar Date Picker. */ @@ -40,6 +54,13 @@ Cypress.Commands.add('getDatePickerRelativeInput', () => { return cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.SEARCH_DATE_RELATIVE_PICKER_INPUT); }); +/** + * Get the Absolute Date Input in the Search Bar Date Picker. + */ +Cypress.Commands.add('getDatePickerAbsoluteInput', () => { + return cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.SEARCH_DATE_PICKER_ABSOLUTE_DATE_INPUT); +}); + /** * Get the Relative Date Unit selector in the Search Bar Date Picker. */ @@ -48,3 +69,54 @@ Cypress.Commands.add('getDatePickerRelativeUnitSelector', () => { DATA_EXPLORER_PAGE_ELEMENTS.SEARCH_DATE_RELATIVE_PICKER_UNIT_SELECTOR ); }); + +/** + * Get the Absolute Date tab in the Search Bar Date Picker. + */ +Cypress.Commands.add('getDatePickerAbsoluteTab', () => { + return cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.SEARCH_DATE_PICKER_ABSOLUTE_TAB); +}); + +/** + * Get the Absolute Date Input in the Absolute Tab of the Search Bar Date Picker. + */ +Cypress.Commands.add('getDatePickerAbsoluteDateInput', () => { + return cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.SEARCH_DATE_PICKER_ABSOLUTE_DATE_INPUT); +}); + +/** + * Set search to start from a relative Date + * @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 + * // sets search to return results from 15 years ago to now + * setSearchRelativeDateRange('15', 'years ago') + */ +Cypress.Commands.add('setSearchRelativeDateRange', (relativeNumber, relativeUnit) => { + cy.getSearchDatePickerButton().click(); + cy.getDatePickerRelativeTab().click(); + cy.getDatePickerRelativeInput().clear().type(relativeNumber); + cy.getDatePickerRelativeUnitSelector().select(relativeUnit); + cy.getQuerySubmitButton().click(); +}); + +/** + * Set search to an absolute Date + * @param absoluteStartDate String for Absolute Datetime for start date in format 'MMM dd, yyyy @ HH:mm:ss.SSS'. + * @param absoluteEndDate String for Absolute Datetime for end date in format 'MMM dd, yyyy @ HH:mm:ss.SSS'. + * @example setSearchRelativeDateRange('Dec 31, 2020 @ 16:00:00.000', 'Dec 31, 2022 @ 14:14:42.801') + */ +Cypress.Commands.add('setSearchAbsoluteDateRange', (absoluteStartDate, absoluteEndDate) => { + cy.getSearchDatePickerButton().click(); + cy.getDatePickerAbsoluteTab().click(); + cy.getDatePickerAbsoluteInput().clear().type(absoluteStartDate); + cy.getSearchDatePickerEndDateButton().click(); + cy.getDatePickerAbsoluteTab() + .should(($elements) => { + // This prevents there being 2 Absolute Tabs returned. + expect($elements).to.have.length(1); + }) + .click(); + cy.getDatePickerAbsoluteInput().clear().type(absoluteEndDate); + cy.getQuerySubmitButton().click(); +}); diff --git a/cypress/utils/dashboards/data_explorer/data_explorer_page.po.js b/cypress/utils/dashboards/data_explorer/data_explorer_page.po.js index 179818ed0bcd..5d98ab1a8aab 100644 --- a/cypress/utils/dashboards/data_explorer/data_explorer_page.po.js +++ b/cypress/utils/dashboards/data_explorer/data_explorer_page.po.js @@ -4,7 +4,6 @@ */ import { DATA_EXPLORER_PAGE_ELEMENTS } from './elements.js'; -import { INDEX_CLUSTER_NAME, INDEX_NAME, INDEX_PATTERN_NAME } from './constants.js'; export class DataExplorerPage { /** @@ -228,15 +227,17 @@ export class DataExplorerPage { /** * Select an index dataset. - * @param datasetLanguage Index supports "OpenSearch SQL" and "PPL" + * @param indexClusterName Name of the cluster to be used for the Index. + * @param indexName Name of the index dataset to be used. + * @param datasetLanguage Index supports "OpenSearch SQL" and "PPL". */ - static selectIndexDataset(datasetLanguage) { + static selectIndexDataset(indexClusterName, indexName, datasetLanguage) { DataExplorerPage.openDatasetExplorerWindow(); DataExplorerPage.getDatasetExplorerWindow().contains('Indexes').click(); DataExplorerPage.getDatasetExplorerWindow() - .contains(INDEX_CLUSTER_NAME, { timeout: 10000 }) + .contains(indexClusterName, { timeout: 10000 }) .click(); - DataExplorerPage.getDatasetExplorerWindow().contains(INDEX_NAME, { timeout: 10000 }).click(); + DataExplorerPage.getDatasetExplorerWindow().contains(indexName, { timeout: 10000 }).click(); DataExplorerPage.getDatasetExplorerNextButton().click(); DataExplorerPage.selectIndexDatasetLanguage(datasetLanguage); } @@ -252,32 +253,19 @@ export class DataExplorerPage { /** * Select an index pattern dataset. + * @param indexPatternName Name of the index pattern to be used. * @param datasetLanguage Index Pattern supports "DQL", "Lucene", "OpenSearch SQL" and "PPL" */ - static selectIndexPatternDataset(datasetLanguage) { + static selectIndexPatternDataset(indexPatternName, datasetLanguage) { DataExplorerPage.openDatasetExplorerWindow(); DataExplorerPage.getDatasetExplorerWindow().contains('Index Patterns').click(); DataExplorerPage.getDatasetExplorerWindow() - .contains(INDEX_PATTERN_NAME, { timeout: 10000 }) + .contains(indexPatternName, { 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(); - } - /** * Expand rowNumber of Doc Table. * @param rowNumber rowNumber of Doc Table starts at 0 for row 1. diff --git a/cypress/utils/dashboards/data_explorer/elements.js b/cypress/utils/dashboards/data_explorer/elements.js index 764c27f1a142..407a0cfb5560 100644 --- a/cypress/utils/dashboards/data_explorer/elements.js +++ b/cypress/utils/dashboards/data_explorer/elements.js @@ -22,9 +22,13 @@ export const DATA_EXPLORER_PAGE_ELEMENTS = { TABLE_FIELD_FILTER_FOR_BUTTON: 'filterForValue', TABLE_FIELD_FILTER_OUT_BUTTON: 'filterOutValue', SEARCH_DATE_PICKER_BUTTON: 'superDatePickerShowDatesButton', + SEARCH_DATE_PICKER_START_DATE_BUTTON: 'superDatePickerstartDatePopoverButton', + SEARCH_DATE_PICKER_END_DATE_BUTTON: 'superDatePickerendDatePopoverButton', SEARCH_DATE_PICKER_RELATIVE_TAB: 'superDatePickerRelativeTab', SEARCH_DATE_RELATIVE_PICKER_INPUT: 'superDatePickerRelativeDateInputNumber', SEARCH_DATE_RELATIVE_PICKER_UNIT_SELECTOR: 'superDatePickerRelativeDateInputUnitSelector', + SEARCH_DATE_PICKER_ABSOLUTE_TAB: 'superDatePickerAbsoluteTab', + SEARCH_DATE_PICKER_ABSOLUTE_DATE_INPUT: 'superDatePickerAbsoluteDateInput', QUERY_SUBMIT_BUTTON: 'querySubmitButton', GLOBAL_QUERY_EDITOR_FILTER_VALUE: 'globalFilterLabelValue', GLOBAL_FILTER_BAR: 'globalFilterBar', From 28d9b41b6e63d6d7674fd43370260d5e2c355ef3 Mon Sep 17 00:00:00 2001 From: Argus Li Date: Tue, 10 Dec 2024 14:03:30 -0800 Subject: [PATCH 27/31] Fix Buttons not detecting if disabled. Signed-off-by: Argus Li --- .../core_opensearch_dashboards/filter_for_value_spec.js | 6 ++---- .../utils/dashboards/data_explorer/data_explorer_page.po.js | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/cypress/integration/core_opensearch_dashboards/filter_for_value_spec.js b/cypress/integration/core_opensearch_dashboards/filter_for_value_spec.js index a626d01f2533..d038a21d96eb 100644 --- a/cypress/integration/core_opensearch_dashboards/filter_for_value_spec.js +++ b/cypress/integration/core_opensearch_dashboards/filter_for_value_spec.js @@ -79,14 +79,12 @@ describe('filter for value spec', () => { it('Lucene', () => {}); // filter actions should not exist for SQL it('SQL', () => { - DataExplorerPage.selectIndexPatternDataset(INDEX_PATTERN_NAME, 'DQL'); - cy.setSearchAbsoluteDateRange(SEARCH_ABSOLUTE_START_DATE, SEARCH_ABSOLUTE_END_DATE); + DataExplorerPage.selectIndexPatternDataset(INDEX_PATTERN_NAME, 'SQL'); DataExplorerPage.checkDocTableFirstExpandedFieldFirstRowFilterForAndOutButtons(false); }); // filter actions should not exist for PPL it('PPL', () => { - DataExplorerPage.selectIndexPatternDataset(INDEX_PATTERN_NAME, 'DQL'); - cy.setSearchAbsoluteDateRange(SEARCH_ABSOLUTE_START_DATE, SEARCH_ABSOLUTE_END_DATE); + DataExplorerPage.selectIndexPatternDataset(INDEX_PATTERN_NAME, 'PPL'); DataExplorerPage.checkDocTableFirstExpandedFieldFirstRowFilterForAndOutButtons(false); }); }); diff --git a/cypress/utils/dashboards/data_explorer/data_explorer_page.po.js b/cypress/utils/dashboards/data_explorer/data_explorer_page.po.js index 5d98ab1a8aab..0b4f29fe83c5 100644 --- a/cypress/utils/dashboards/data_explorer/data_explorer_page.po.js +++ b/cypress/utils/dashboards/data_explorer/data_explorer_page.po.js @@ -347,7 +347,7 @@ export class DataExplorerPage { * @param isEnabled Boolean determining if these buttons are disabled */ static checkDocTableFirstExpandedFieldFirstRowFilterForAndOutButtons(isEnabled) { - const shouldText = isEnabled ? 'be.enabled' : 'not.be.enabled'; + const shouldText = isEnabled ? 'be.enabled' : 'be.disabled'; DataExplorerPage.expandDocTableRow(0); DataExplorerPage.getExpandedDocRow(0, 0).within(() => { DataExplorerPage.getDocTableExpandedDocRowFilterForButton().should(shouldText); From 825d88ac1f05c09a6ef4acc1d882cd3566a10880 Mon Sep 17 00:00:00 2001 From: Argus Li Date: Tue, 10 Dec 2024 17:00:30 -0800 Subject: [PATCH 28/31] Add support for expanded table toggleColumnButton Checks. Signed-off-by: Argus Li --- .../filter_for_value_spec.js | 31 +++++- .../data_explorer/data_explorer_page.po.js | 94 +++++++++++++++++-- .../dashboards/data_explorer/elements.js | 5 + 3 files changed, 119 insertions(+), 11 deletions(-) diff --git a/cypress/integration/core_opensearch_dashboards/filter_for_value_spec.js b/cypress/integration/core_opensearch_dashboards/filter_for_value_spec.js index d038a21d96eb..a9a2093165c2 100644 --- a/cypress/integration/core_opensearch_dashboards/filter_for_value_spec.js +++ b/cypress/integration/core_opensearch_dashboards/filter_for_value_spec.js @@ -71,28 +71,53 @@ describe('filter for value spec', () => { it('DQL', () => { DataExplorerPage.selectIndexPatternDataset(INDEX_PATTERN_NAME, 'DQL'); cy.setSearchAbsoluteDateRange(SEARCH_ABSOLUTE_START_DATE, SEARCH_ABSOLUTE_END_DATE); + DataExplorerPage.toggleDocTableRow(0); DataExplorerPage.checkDocTableFirstExpandedFieldFirstRowFilterForAndOutButtons(true); DataExplorerPage.checkDocTableFirstExpandedFieldFirstRowFilterForButtonFiltersCorrectField(); DataExplorerPage.checkDocTableFirstExpandedFieldFirstRowFilterOutButtonFiltersCorrectField(); + DataExplorerPage.checkDocTableFirstExpandedFieldFirstRowToggleColumnButtonHasIntendedBehavior(); }); // filter actions should exist for Lucene - it('Lucene', () => {}); + it('Lucene', () => { + DataExplorerPage.selectIndexPatternDataset(INDEX_PATTERN_NAME, 'Lucene'); + cy.setSearchAbsoluteDateRange(SEARCH_ABSOLUTE_START_DATE, SEARCH_ABSOLUTE_END_DATE); + DataExplorerPage.toggleDocTableRow(0); + DataExplorerPage.checkDocTableFirstExpandedFieldFirstRowFilterForAndOutButtons(true); + DataExplorerPage.checkDocTableFirstExpandedFieldFirstRowFilterForButtonFiltersCorrectField(); + DataExplorerPage.checkDocTableFirstExpandedFieldFirstRowFilterOutButtonFiltersCorrectField(); + DataExplorerPage.checkDocTableFirstExpandedFieldFirstRowToggleColumnButtonHasIntendedBehavior(); + }); // filter actions should not exist for SQL it('SQL', () => { DataExplorerPage.selectIndexPatternDataset(INDEX_PATTERN_NAME, 'SQL'); + DataExplorerPage.toggleDocTableRow(0); DataExplorerPage.checkDocTableFirstExpandedFieldFirstRowFilterForAndOutButtons(false); + DataExplorerPage.checkDocTableFirstExpandedFieldFirstRowToggleColumnButtonHasIntendedBehavior(); }); // filter actions should not exist for PPL it('PPL', () => { DataExplorerPage.selectIndexPatternDataset(INDEX_PATTERN_NAME, 'PPL'); + cy.setSearchAbsoluteDateRange(SEARCH_ABSOLUTE_START_DATE, SEARCH_ABSOLUTE_END_DATE); + DataExplorerPage.toggleDocTableRow(0); DataExplorerPage.checkDocTableFirstExpandedFieldFirstRowFilterForAndOutButtons(false); + DataExplorerPage.checkDocTableFirstExpandedFieldFirstRowToggleColumnButtonHasIntendedBehavior(); }); }); describe('index dataset', () => { // filter actions should not exist for SQL - it('SQL', () => {}); + it('SQL', () => { + DataExplorerPage.selectIndexDataset(INDEX_CLUSTER_NAME, INDEX_NAME, 'OpenSearch SQL'); + DataExplorerPage.toggleDocTableRow(0); + DataExplorerPage.checkDocTableFirstExpandedFieldFirstRowFilterForAndOutButtons(false); + DataExplorerPage.checkDocTableFirstExpandedFieldFirstRowToggleColumnButtonHasIntendedBehavior(); + }); // filter actions should not exist for PPL - it('PPL', () => {}); + it('PPL', () => { + DataExplorerPage.selectIndexDataset(INDEX_CLUSTER_NAME, INDEX_NAME, 'PPL'); + DataExplorerPage.toggleDocTableRow(0); + DataExplorerPage.checkDocTableFirstExpandedFieldFirstRowFilterForAndOutButtons(false); + DataExplorerPage.checkDocTableFirstExpandedFieldFirstRowToggleColumnButtonHasIntendedBehavior(); + }); }); }); }); diff --git a/cypress/utils/dashboards/data_explorer/data_explorer_page.po.js b/cypress/utils/dashboards/data_explorer/data_explorer_page.po.js index 0b4f29fe83c5..58809268db8e 100644 --- a/cypress/utils/dashboards/data_explorer/data_explorer_page.po.js +++ b/cypress/utils/dashboards/data_explorer/data_explorer_page.po.js @@ -185,16 +185,72 @@ export class DataExplorerPage { * getExpandedDocRow(1, 0); */ static getExpandedDocRowValue(docTableRowNumber, expandedDocumentRowNumber) { - return DataExplorerPage.findDocTableExpandedDocRows( - DataExplorerPage.getDocTableRow(docTableRowNumber + 1) - ) - .eq(expandedDocumentRowNumber) + return DataExplorerPage.getExpandedDocRow(docTableRowNumber, expandedDocumentRowNumber) .find( `[data-test-subj*="${DATA_EXPLORER_PAGE_ELEMENTS.DOC_TABLE_EXPANDED_DOC_COLUMN_ROW_PREFIX}"]` ) .find('span'); } + /** + * Get the field name for the "expandedDocumentRowNumber"th row from the expanded document from the "docTableRowNumber"th row of the DocTable. + * @param docTableRowNumber Integer starts from 0 for the first row + * @param expandedDocumentRowNumber Integer starts from 0 for the first row + * @example + * // returns the name of the field from the first row from the expanded document from the second row of the DocTable. + * getExpandedDocRow(1, 0); + */ + static getExpandedDocRowFieldName(docTableRowNumber, expandedDocumentRowNumber) { + return DataExplorerPage.getExpandedDocRow(docTableRowNumber, expandedDocumentRowNumber) + .find('td') + .eq(1) // Field name is in the second column. + .find('span[class*="textTruncate"]'); + } + + /** + * Get Toggle Column Button in Doc Table Field Expanded Document Row. + */ + static getDocTableExpandedDocRowToggleColumnButton() { + return cy.getElementByTestId( + DATA_EXPLORER_PAGE_ELEMENTS.DOC_TABLE_EXPANDED_DOC_TOGGLE_COLUMN_BUTTON + ); + } + + /** + * Get Selected fields list in sidebar. + */ + static getSideBarSelectedFieldsList() { + return cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.SIDE_BAR_SELECTED_FIELDS_LIST); + } + + /** + * Get fieldName in sidebar. + * @param fieldName Field name for row in Expanded Document. + */ + static getSideBarField(fieldName) { + return cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.SIDE_BAR_FIELD_PREFIX + fieldName); + } + + /** + * Get field remove button in sidebar selected fields. + * @param fieldName Field name for row in Expanded Document. + */ + static getSideBarSelectedFieldRemoveButton(fieldName) { + return cy.getElementByTestId( + DATA_EXPLORER_PAGE_ELEMENTS.SIDE_BAR_SELECTED_FIELD_REMOVE_BUTTON_PREFIX + fieldName + ); + } + + /** + * Get header from Document Table. + * @param headerName Header name from Document Table. + */ + static getDocTableHeader(headerName) { + return cy.getElementByTestId( + DATA_EXPLORER_PAGE_ELEMENTS.DOC_TABLE_HEADER_FIELD_PREFIX + headerName + ); + } + /** * Open window to select Dataset */ @@ -267,10 +323,10 @@ export class DataExplorerPage { } /** - * Expand rowNumber of Doc Table. + * Toggle expansion of row rowNumber of Doc Table. * @param rowNumber rowNumber of Doc Table starts at 0 for row 1. */ - static expandDocTableRow(rowNumber) { + static toggleDocTableRow(rowNumber) { DataExplorerPage.getDocTableRow(rowNumber).within(() => { DataExplorerPage.getDocTableExpandColumnToggleButton().click(); }); @@ -348,7 +404,6 @@ export class DataExplorerPage { */ static checkDocTableFirstExpandedFieldFirstRowFilterForAndOutButtons(isEnabled) { const shouldText = isEnabled ? 'be.enabled' : 'be.disabled'; - DataExplorerPage.expandDocTableRow(0); DataExplorerPage.getExpandedDocRow(0, 0).within(() => { DataExplorerPage.getDocTableExpandedDocRowFilterForButton().should(shouldText); DataExplorerPage.getDocTableExpandedDocRowFilterForButton().should(shouldText); @@ -383,10 +438,33 @@ export class DataExplorerPage { }); 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.expandDocTableRow(0); + DataExplorerPage.toggleDocTableRow(0); DataExplorerPage.getExpandedDocRowValue(0, 0).should('not.have.text', filterFieldText); }); DataExplorerPage.getFilterBar().find('[aria-label="Delete"]').click(); DataExplorerPage.checkQueryHitsText('10,000'); + DataExplorerPage.toggleDocTableRow(0); + } + + /** + * Check the first expanded Doc Table Field's first row's Toggle Column button filters the correct value. + */ + static checkDocTableFirstExpandedFieldFirstRowToggleColumnButtonHasIntendedBehavior() { + DataExplorerPage.getExpandedDocRowFieldName(0, 0).then(($expandedDocumentRowFieldText) => { + const fieldText = $expandedDocumentRowFieldText.text(); + DataExplorerPage.getExpandedDocRow(0, 0).within(() => { + DataExplorerPage.getDocTableHeader(fieldText).should('not.exist'); + DataExplorerPage.getDocTableExpandedDocRowToggleColumnButton().click(); + }); + DataExplorerPage.getSideBarSelectedFieldsList().within(() => { + DataExplorerPage.getSideBarField(fieldText).should('exist'); + }); + DataExplorerPage.getDocTableHeader(fieldText).should('exist'); + DataExplorerPage.getSideBarSelectedFieldRemoveButton(fieldText).click(); + DataExplorerPage.getSideBarSelectedFieldsList().within(() => { + DataExplorerPage.getSideBarField(fieldText).should('not.exist'); + }); + DataExplorerPage.getDocTableHeader(fieldText).should('not.exist'); + }); } } diff --git a/cypress/utils/dashboards/data_explorer/elements.js b/cypress/utils/dashboards/data_explorer/elements.js index 407a0cfb5560..dbed87949ddb 100644 --- a/cypress/utils/dashboards/data_explorer/elements.js +++ b/cypress/utils/dashboards/data_explorer/elements.js @@ -17,8 +17,10 @@ export const DATA_EXPLORER_PAGE_ELEMENTS = { DOC_TABLE_ROW_FIELD: 'docTableField', DOC_TABLE_EXPAND_TOGGLE_COLUMN_BUTTON: 'docTableExpandToggleColumn', DOC_TABLE_EXPANDED_DOC_COLUMN_ROW_PREFIX: 'tableDocViewRow-', + DOC_TABLE_EXPANDED_DOC_TOGGLE_COLUMN_BUTTON: 'toggleColumnButton', DOC_TABLE_EXPANDED_DOC_COLUMN_ADD_INCLUSIVE_FILTER_BUTTON: 'addInclusiveFilterButton', DOC_TABLE_EXPANDED_DOC_COLUMN_REMOVE_INCLUSIVE_FILTER_BUTTON: 'removeInclusiveFilterButton', + DOC_TABLE_HEADER_FIELD_PREFIX: 'docTableHeader-', TABLE_FIELD_FILTER_FOR_BUTTON: 'filterForValue', TABLE_FIELD_FILTER_OUT_BUTTON: 'filterOutValue', SEARCH_DATE_PICKER_BUTTON: 'superDatePickerShowDatesButton', @@ -29,6 +31,9 @@ export const DATA_EXPLORER_PAGE_ELEMENTS = { SEARCH_DATE_RELATIVE_PICKER_UNIT_SELECTOR: 'superDatePickerRelativeDateInputUnitSelector', SEARCH_DATE_PICKER_ABSOLUTE_TAB: 'superDatePickerAbsoluteTab', SEARCH_DATE_PICKER_ABSOLUTE_DATE_INPUT: 'superDatePickerAbsoluteDateInput', + SIDE_BAR_SELECTED_FIELDS_LIST: 'fieldList-selected', + SIDE_BAR_FIELD_PREFIX: 'field-', + SIDE_BAR_SELECTED_FIELD_REMOVE_BUTTON_PREFIX: 'fieldToggle-', QUERY_SUBMIT_BUTTON: 'querySubmitButton', GLOBAL_QUERY_EDITOR_FILTER_VALUE: 'globalFilterLabelValue', GLOBAL_FILTER_BAR: 'globalFilterBar', From 444dbbe040c414a05771149533987ad847ee7d2d Mon Sep 17 00:00:00 2001 From: Argus Li Date: Wed, 11 Dec 2024 15:27:34 -0800 Subject: [PATCH 29/31] Add Exists Filter functionality. Signed-off-by: Argus Li --- ...pec.js => field_display_filtering_spec.js} | 26 ++++++++--- .../data_explorer/data_explorer_page.po.js | 45 +++++++++++++++++-- .../dashboards/data_explorer/elements.js | 1 + 3 files changed, 62 insertions(+), 10 deletions(-) rename cypress/integration/core_opensearch_dashboards/{filter_for_value_spec.js => field_display_filtering_spec.js} (90%) diff --git a/cypress/integration/core_opensearch_dashboards/filter_for_value_spec.js b/cypress/integration/core_opensearch_dashboards/field_display_filtering_spec.js similarity index 90% rename from cypress/integration/core_opensearch_dashboards/filter_for_value_spec.js rename to cypress/integration/core_opensearch_dashboards/field_display_filtering_spec.js index a9a2093165c2..b80de3068da8 100644 --- a/cypress/integration/core_opensearch_dashboards/filter_for_value_spec.js +++ b/cypress/integration/core_opensearch_dashboards/field_display_filtering_spec.js @@ -72,26 +72,34 @@ describe('filter for value spec', () => { DataExplorerPage.selectIndexPatternDataset(INDEX_PATTERN_NAME, 'DQL'); cy.setSearchAbsoluteDateRange(SEARCH_ABSOLUTE_START_DATE, SEARCH_ABSOLUTE_END_DATE); DataExplorerPage.toggleDocTableRow(0); - DataExplorerPage.checkDocTableFirstExpandedFieldFirstRowFilterForAndOutButtons(true); + DataExplorerPage.checkDocTableFirstExpandedFieldFirstRowFilterForFilterOutExistsFilterButtons( + true + ); DataExplorerPage.checkDocTableFirstExpandedFieldFirstRowFilterForButtonFiltersCorrectField(); DataExplorerPage.checkDocTableFirstExpandedFieldFirstRowFilterOutButtonFiltersCorrectField(); DataExplorerPage.checkDocTableFirstExpandedFieldFirstRowToggleColumnButtonHasIntendedBehavior(); + DataExplorerPage.checkDocTableFirstExpandedFieldFirstRowExistsFilterButtonFiltersCorrectField(); }); // filter actions should exist for Lucene it('Lucene', () => { DataExplorerPage.selectIndexPatternDataset(INDEX_PATTERN_NAME, 'Lucene'); cy.setSearchAbsoluteDateRange(SEARCH_ABSOLUTE_START_DATE, SEARCH_ABSOLUTE_END_DATE); DataExplorerPage.toggleDocTableRow(0); - DataExplorerPage.checkDocTableFirstExpandedFieldFirstRowFilterForAndOutButtons(true); + DataExplorerPage.checkDocTableFirstExpandedFieldFirstRowFilterForFilterOutExistsFilterButtons( + true + ); DataExplorerPage.checkDocTableFirstExpandedFieldFirstRowFilterForButtonFiltersCorrectField(); DataExplorerPage.checkDocTableFirstExpandedFieldFirstRowFilterOutButtonFiltersCorrectField(); DataExplorerPage.checkDocTableFirstExpandedFieldFirstRowToggleColumnButtonHasIntendedBehavior(); + DataExplorerPage.checkDocTableFirstExpandedFieldFirstRowExistsFilterButtonFiltersCorrectField(); }); // filter actions should not exist for SQL it('SQL', () => { DataExplorerPage.selectIndexPatternDataset(INDEX_PATTERN_NAME, 'SQL'); DataExplorerPage.toggleDocTableRow(0); - DataExplorerPage.checkDocTableFirstExpandedFieldFirstRowFilterForAndOutButtons(false); + DataExplorerPage.checkDocTableFirstExpandedFieldFirstRowFilterForFilterOutExistsFilterButtons( + false + ); DataExplorerPage.checkDocTableFirstExpandedFieldFirstRowToggleColumnButtonHasIntendedBehavior(); }); // filter actions should not exist for PPL @@ -99,7 +107,9 @@ describe('filter for value spec', () => { DataExplorerPage.selectIndexPatternDataset(INDEX_PATTERN_NAME, 'PPL'); cy.setSearchAbsoluteDateRange(SEARCH_ABSOLUTE_START_DATE, SEARCH_ABSOLUTE_END_DATE); DataExplorerPage.toggleDocTableRow(0); - DataExplorerPage.checkDocTableFirstExpandedFieldFirstRowFilterForAndOutButtons(false); + DataExplorerPage.checkDocTableFirstExpandedFieldFirstRowFilterForFilterOutExistsFilterButtons( + false + ); DataExplorerPage.checkDocTableFirstExpandedFieldFirstRowToggleColumnButtonHasIntendedBehavior(); }); }); @@ -108,14 +118,18 @@ describe('filter for value spec', () => { it('SQL', () => { DataExplorerPage.selectIndexDataset(INDEX_CLUSTER_NAME, INDEX_NAME, 'OpenSearch SQL'); DataExplorerPage.toggleDocTableRow(0); - DataExplorerPage.checkDocTableFirstExpandedFieldFirstRowFilterForAndOutButtons(false); + DataExplorerPage.checkDocTableFirstExpandedFieldFirstRowFilterForFilterOutExistsFilterButtons( + false + ); DataExplorerPage.checkDocTableFirstExpandedFieldFirstRowToggleColumnButtonHasIntendedBehavior(); }); // filter actions should not exist for PPL it('PPL', () => { DataExplorerPage.selectIndexDataset(INDEX_CLUSTER_NAME, INDEX_NAME, 'PPL'); DataExplorerPage.toggleDocTableRow(0); - DataExplorerPage.checkDocTableFirstExpandedFieldFirstRowFilterForAndOutButtons(false); + DataExplorerPage.checkDocTableFirstExpandedFieldFirstRowFilterForFilterOutExistsFilterButtons( + false + ); DataExplorerPage.checkDocTableFirstExpandedFieldFirstRowToggleColumnButtonHasIntendedBehavior(); }); }); diff --git a/cypress/utils/dashboards/data_explorer/data_explorer_page.po.js b/cypress/utils/dashboards/data_explorer/data_explorer_page.po.js index 58809268db8e..03b1e476a370 100644 --- a/cypress/utils/dashboards/data_explorer/data_explorer_page.po.js +++ b/cypress/utils/dashboards/data_explorer/data_explorer_page.po.js @@ -251,6 +251,15 @@ export class DataExplorerPage { ); } + /** + * Get Exists Filter Button in Doc Table Field Expanded Document Row. + */ + static getDocTableExpandedDocRowExistsFilterButton() { + return cy.getElementByTestId( + DATA_EXPLORER_PAGE_ELEMENTS.DOC_TABLE_EXPANDED_DOC_COLUMN_EXISTS_FILTER_BUTTON + ); + } + /** * Open window to select Dataset */ @@ -340,6 +349,17 @@ export class DataExplorerPage { DataExplorerPage.getGlobalQueryEditorFilterValue().should('have.text', expectedFilterText); } + /** + * Check the entire filter pill text matches expectedFilterText. + * @param expectedFilterText expected text in filter pill. + */ + static checkFullFilterPillText(expectedFilterText) { + // GLOBAL_QUERY_EDITOR_FILTER_VALUE gives the inner element, but we may want all the text in the filter pill + DataExplorerPage.getGlobalQueryEditorFilterValue() + .parent() + .should('have.text', expectedFilterText); + } + /** * Check the query hit text matches expectedQueryHitText. * @param expectedQueryHitsText expected text for query hits @@ -399,14 +419,15 @@ export class DataExplorerPage { } /** - * Check if the first expanded Doc Table Field's first row's Filter For and Filter Out button are disabled. + * Check if the first expanded Doc Table Field's first row's Filter For, Filter Out and Exists Filter buttons are disabled. * @param isEnabled Boolean determining if these buttons are disabled */ - static checkDocTableFirstExpandedFieldFirstRowFilterForAndOutButtons(isEnabled) { + static checkDocTableFirstExpandedFieldFirstRowFilterForFilterOutExistsFilterButtons(isEnabled) { const shouldText = isEnabled ? 'be.enabled' : 'be.disabled'; DataExplorerPage.getExpandedDocRow(0, 0).within(() => { DataExplorerPage.getDocTableExpandedDocRowFilterForButton().should(shouldText); - DataExplorerPage.getDocTableExpandedDocRowFilterForButton().should(shouldText); + DataExplorerPage.getDocTableExpandedDocRowFilterOutButton().should(shouldText); + DataExplorerPage.getDocTableExpandedDocRowExistsFilterButton().should(shouldText); }); } @@ -447,7 +468,7 @@ export class DataExplorerPage { } /** - * Check the first expanded Doc Table Field's first row's Toggle Column button filters the correct value. + * Check the first expanded Doc Table Field's first row's Toggle Column button has intended behavior. */ static checkDocTableFirstExpandedFieldFirstRowToggleColumnButtonHasIntendedBehavior() { DataExplorerPage.getExpandedDocRowFieldName(0, 0).then(($expandedDocumentRowFieldText) => { @@ -467,4 +488,20 @@ export class DataExplorerPage { DataExplorerPage.getDocTableHeader(fieldText).should('not.exist'); }); } + + /** + * Check the first expanded Doc Table Field's first row's Exists Filter button filters the correct Field. + */ + static checkDocTableFirstExpandedFieldFirstRowExistsFilterButtonFiltersCorrectField() { + DataExplorerPage.getExpandedDocRowFieldName(0, 0).then(($expandedDocumentRowField) => { + const filterFieldText = $expandedDocumentRowField.text(); + DataExplorerPage.getExpandedDocRow(0, 0).within(() => { + DataExplorerPage.getDocTableExpandedDocRowExistsFilterButton().click(); + }); + DataExplorerPage.checkFullFilterPillText(filterFieldText + ': ' + 'exists'); + DataExplorerPage.checkQueryHitsText('10,000'); + }); + 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 index dbed87949ddb..2613b747271c 100644 --- a/cypress/utils/dashboards/data_explorer/elements.js +++ b/cypress/utils/dashboards/data_explorer/elements.js @@ -20,6 +20,7 @@ export const DATA_EXPLORER_PAGE_ELEMENTS = { DOC_TABLE_EXPANDED_DOC_TOGGLE_COLUMN_BUTTON: 'toggleColumnButton', DOC_TABLE_EXPANDED_DOC_COLUMN_ADD_INCLUSIVE_FILTER_BUTTON: 'addInclusiveFilterButton', DOC_TABLE_EXPANDED_DOC_COLUMN_REMOVE_INCLUSIVE_FILTER_BUTTON: 'removeInclusiveFilterButton', + DOC_TABLE_EXPANDED_DOC_COLUMN_EXISTS_FILTER_BUTTON: 'addExistsFilterButton', DOC_TABLE_HEADER_FIELD_PREFIX: 'docTableHeader-', TABLE_FIELD_FILTER_FOR_BUTTON: 'filterForValue', TABLE_FIELD_FILTER_OUT_BUTTON: 'filterOutValue', From 3a5166ad939816b3adf1021e39ef18ad242bf1f7 Mon Sep 17 00:00:00 2001 From: Argus Li Date: Wed, 11 Dec 2024 15:53:03 -0800 Subject: [PATCH 30/31] Remove conflicting Cypress config file. Signed-off-by: Argus Li --- cypress.config.js | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 cypress.config.js diff --git a/cypress.config.js b/cypress.config.js deleted file mode 100644 index e69de29bb2d1..000000000000 From e48a294424467a2b8d04a1018b808c321377cb3b Mon Sep 17 00:00:00 2001 From: Argus Li Date: Thu, 12 Dec 2024 17:03:38 -0800 Subject: [PATCH 31/31] Reorganize tests within spec to reduce code duplication. Make small changes to reduce test flakiness. Add retries to improve test flakiness. Signed-off-by: Argus Li --- cypress.config.ts | 1 + .../field_display_filtering_spec.js | 120 ++++++++---------- cypress/utils/commands.js | 12 ++ .../data_explorer/data_explorer_page.po.js | 16 ++- 4 files changed, 79 insertions(+), 70 deletions(-) diff --git a/cypress.config.ts b/cypress.config.ts index d1363c2bf7ca..3432b574dc4b 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -12,6 +12,7 @@ module.exports = defineConfig({ responseTimeout: 60000, viewportWidth: 2000, viewportHeight: 1320, + retries: 2, env: { openSearchUrl: 'http://localhost:9200', SECURITY_ENABLED: false, diff --git a/cypress/integration/core_opensearch_dashboards/field_display_filtering_spec.js b/cypress/integration/core_opensearch_dashboards/field_display_filtering_spec.js index b80de3068da8..cc93518e7c5d 100644 --- a/cypress/integration/core_opensearch_dashboards/field_display_filtering_spec.js +++ b/cypress/integration/core_opensearch_dashboards/field_display_filtering_spec.js @@ -15,6 +15,50 @@ import { const miscUtils = new MiscUtils(cy); +function selectDataSet(datasetType, language) { + switch (datasetType) { + case 'index': + DataExplorerPage.selectIndexDataset(INDEX_CLUSTER_NAME, INDEX_NAME, language); + break; + case 'index_pattern': + DataExplorerPage.selectIndexPatternDataset(INDEX_PATTERN_NAME, language); + if (language !== 'OpenSearch SQL') { + cy.setSearchAbsoluteDateRange(SEARCH_ABSOLUTE_START_DATE, SEARCH_ABSOLUTE_END_DATE); + } + break; + } +} + +function checkTableFieldFilterActions(datasetType, language, isEnabled) { + selectDataSet(datasetType, language); + + DataExplorerPage.getDiscoverQueryHits().should('not.exist'); // To ensure it waits until a full table is loaded into the DOM, instead of a bug where table only has 1 hit. + + DataExplorerPage.checkDocTableFirstFieldFilterForAndOutButton(isEnabled); + + if (isEnabled) { + DataExplorerPage.checkDocTableFirstFieldFilterForButtonFiltersCorrectField(); + DataExplorerPage.checkDocTableFirstFieldFilterOutButtonFiltersCorrectField(); + } +} + +function checkExpandedTableFilterActions(datasetType, language, isEnabled) { + selectDataSet(datasetType, language); + + DataExplorerPage.getDiscoverQueryHits().should('not.exist'); // To ensure it waits until a full table is loaded into the DOM, instead of a bug where table only has 1 hit. + DataExplorerPage.toggleDocTableRow(0); + DataExplorerPage.checkDocTableFirstExpandedFieldFirstRowFilterForFilterOutExistsFilterButtons( + isEnabled + ); + DataExplorerPage.checkDocTableFirstExpandedFieldFirstRowToggleColumnButtonHasIntendedBehavior(); + + if (isEnabled) { + DataExplorerPage.checkDocTableFirstExpandedFieldFirstRowFilterForButtonFiltersCorrectField(); + DataExplorerPage.checkDocTableFirstExpandedFieldFirstRowFilterOutButtonFiltersCorrectField(); + DataExplorerPage.checkDocTableFirstExpandedFieldFirstRowExistsFilterButtonFiltersCorrectField(); + } +} + describe('filter for value spec', () => { beforeEach(() => { cy.localLogin(Cypress.env('username'), Cypress.env('password')); @@ -25,42 +69,29 @@ describe('filter for value spec', () => { describe('index pattern dataset', () => { // filter actions should exist for DQL it('DQL', () => { - DataExplorerPage.selectIndexPatternDataset(INDEX_PATTERN_NAME, 'DQL'); - cy.setSearchAbsoluteDateRange(SEARCH_ABSOLUTE_START_DATE, SEARCH_ABSOLUTE_END_DATE); - DataExplorerPage.checkDocTableFirstFieldFilterForAndOutButton(true); - DataExplorerPage.checkDocTableFirstFieldFilterForButtonFiltersCorrectField(); - DataExplorerPage.checkDocTableFirstFieldFilterOutButtonFiltersCorrectField(); + checkTableFieldFilterActions('index_pattern', 'DQL', true); }); // filter actions should exist for Lucene it('Lucene', () => { - DataExplorerPage.selectIndexPatternDataset(INDEX_PATTERN_NAME, 'Lucene'); - cy.setSearchAbsoluteDateRange(SEARCH_ABSOLUTE_START_DATE, SEARCH_ABSOLUTE_END_DATE); - DataExplorerPage.checkDocTableFirstFieldFilterForAndOutButton(true); - DataExplorerPage.checkDocTableFirstFieldFilterForButtonFiltersCorrectField(); - DataExplorerPage.checkDocTableFirstFieldFilterOutButtonFiltersCorrectField(); + checkTableFieldFilterActions('index_pattern', 'Lucene', true); }); // filter actions should not exist for SQL it('SQL', () => { - DataExplorerPage.selectIndexPatternDataset(INDEX_PATTERN_NAME, 'OpenSearch SQL'); - DataExplorerPage.checkDocTableFirstFieldFilterForAndOutButton(false); + checkTableFieldFilterActions('index_pattern', 'OpenSearch SQL', false); }); // filter actions should not exist for PPL it('PPL', () => { - DataExplorerPage.selectIndexPatternDataset(INDEX_PATTERN_NAME, 'PPL'); - cy.setSearchAbsoluteDateRange(SEARCH_ABSOLUTE_START_DATE, SEARCH_ABSOLUTE_END_DATE); - DataExplorerPage.checkDocTableFirstFieldFilterForAndOutButton(false); + checkTableFieldFilterActions('index_pattern', 'PPL', false); }); }); describe('index dataset', () => { // filter actions should not exist for SQL it('SQL', () => { - DataExplorerPage.selectIndexDataset(INDEX_CLUSTER_NAME, INDEX_NAME, 'OpenSearch SQL'); - DataExplorerPage.checkDocTableFirstFieldFilterForAndOutButton(false); + checkTableFieldFilterActions('index', 'OpenSearch SQL', false); }); // filter actions should not exist for PPL it('PPL', () => { - DataExplorerPage.selectIndexDataset(INDEX_CLUSTER_NAME, INDEX_NAME, 'PPL'); - DataExplorerPage.checkDocTableFirstFieldFilterForAndOutButton(false); + checkTableFieldFilterActions('index', 'PPL', false); }); }); }); @@ -69,68 +100,29 @@ describe('filter for value spec', () => { describe('index pattern dataset', () => { // filter actions should exist for DQL it('DQL', () => { - DataExplorerPage.selectIndexPatternDataset(INDEX_PATTERN_NAME, 'DQL'); - cy.setSearchAbsoluteDateRange(SEARCH_ABSOLUTE_START_DATE, SEARCH_ABSOLUTE_END_DATE); - DataExplorerPage.toggleDocTableRow(0); - DataExplorerPage.checkDocTableFirstExpandedFieldFirstRowFilterForFilterOutExistsFilterButtons( - true - ); - DataExplorerPage.checkDocTableFirstExpandedFieldFirstRowFilterForButtonFiltersCorrectField(); - DataExplorerPage.checkDocTableFirstExpandedFieldFirstRowFilterOutButtonFiltersCorrectField(); - DataExplorerPage.checkDocTableFirstExpandedFieldFirstRowToggleColumnButtonHasIntendedBehavior(); - DataExplorerPage.checkDocTableFirstExpandedFieldFirstRowExistsFilterButtonFiltersCorrectField(); + checkExpandedTableFilterActions('index_pattern', 'DQL', true); }); // filter actions should exist for Lucene it('Lucene', () => { - DataExplorerPage.selectIndexPatternDataset(INDEX_PATTERN_NAME, 'Lucene'); - cy.setSearchAbsoluteDateRange(SEARCH_ABSOLUTE_START_DATE, SEARCH_ABSOLUTE_END_DATE); - DataExplorerPage.toggleDocTableRow(0); - DataExplorerPage.checkDocTableFirstExpandedFieldFirstRowFilterForFilterOutExistsFilterButtons( - true - ); - DataExplorerPage.checkDocTableFirstExpandedFieldFirstRowFilterForButtonFiltersCorrectField(); - DataExplorerPage.checkDocTableFirstExpandedFieldFirstRowFilterOutButtonFiltersCorrectField(); - DataExplorerPage.checkDocTableFirstExpandedFieldFirstRowToggleColumnButtonHasIntendedBehavior(); - DataExplorerPage.checkDocTableFirstExpandedFieldFirstRowExistsFilterButtonFiltersCorrectField(); + checkExpandedTableFilterActions('index_pattern', 'Lucene', true); }); // filter actions should not exist for SQL it('SQL', () => { - DataExplorerPage.selectIndexPatternDataset(INDEX_PATTERN_NAME, 'SQL'); - DataExplorerPage.toggleDocTableRow(0); - DataExplorerPage.checkDocTableFirstExpandedFieldFirstRowFilterForFilterOutExistsFilterButtons( - false - ); - DataExplorerPage.checkDocTableFirstExpandedFieldFirstRowToggleColumnButtonHasIntendedBehavior(); + checkExpandedTableFilterActions('index_pattern', 'OpenSearch SQL', false); }); // filter actions should not exist for PPL it('PPL', () => { - DataExplorerPage.selectIndexPatternDataset(INDEX_PATTERN_NAME, 'PPL'); - cy.setSearchAbsoluteDateRange(SEARCH_ABSOLUTE_START_DATE, SEARCH_ABSOLUTE_END_DATE); - DataExplorerPage.toggleDocTableRow(0); - DataExplorerPage.checkDocTableFirstExpandedFieldFirstRowFilterForFilterOutExistsFilterButtons( - false - ); - DataExplorerPage.checkDocTableFirstExpandedFieldFirstRowToggleColumnButtonHasIntendedBehavior(); + checkExpandedTableFilterActions('index_pattern', 'PPL', false); }); }); describe('index dataset', () => { // filter actions should not exist for SQL it('SQL', () => { - DataExplorerPage.selectIndexDataset(INDEX_CLUSTER_NAME, INDEX_NAME, 'OpenSearch SQL'); - DataExplorerPage.toggleDocTableRow(0); - DataExplorerPage.checkDocTableFirstExpandedFieldFirstRowFilterForFilterOutExistsFilterButtons( - false - ); - DataExplorerPage.checkDocTableFirstExpandedFieldFirstRowToggleColumnButtonHasIntendedBehavior(); + checkExpandedTableFilterActions('index', 'OpenSearch SQL', false); }); // filter actions should not exist for PPL it('PPL', () => { - DataExplorerPage.selectIndexDataset(INDEX_CLUSTER_NAME, INDEX_NAME, 'PPL'); - DataExplorerPage.toggleDocTableRow(0); - DataExplorerPage.checkDocTableFirstExpandedFieldFirstRowFilterForFilterOutExistsFilterButtons( - false - ); - DataExplorerPage.checkDocTableFirstExpandedFieldFirstRowToggleColumnButtonHasIntendedBehavior(); + checkExpandedTableFilterActions('index', 'OpenSearch SQL', false); }); }); }); diff --git a/cypress/utils/commands.js b/cypress/utils/commands.js index 58645e3002af..e315b89f70c2 100644 --- a/cypress/utils/commands.js +++ b/cypress/utils/commands.js @@ -26,6 +26,18 @@ Cypress.Commands.add('getElementsByTestIds', (testIds, options = {}) => { return cy.get(selectors.join(','), options); }); +/** + * Get DOM elements with a data-test-subj id containing the testId. + * @param testId data-test-subj value. + * @param options get options. Default: {} + * @example + * // returns all DOM elements that has a data-test-subj including the string 'table' + * cy.getElementsByTestIdLike('table') + */ +Cypress.Commands.add('getElementsByTestIdLike', (partialTestId, options = {}) => { + return cy.get(`[data-test-subj*="${partialTestId}"]`, options); +}); + /** * Find DOM elements with a data-test-subj id containing the testId. * @param testId data-test-subj value. diff --git a/cypress/utils/dashboards/data_explorer/data_explorer_page.po.js b/cypress/utils/dashboards/data_explorer/data_explorer_page.po.js index 03b1e476a370..3ca18687295f 100644 --- a/cypress/utils/dashboards/data_explorer/data_explorer_page.po.js +++ b/cypress/utils/dashboards/data_explorer/data_explorer_page.po.js @@ -55,15 +55,19 @@ export class DataExplorerPage { return cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_SELECTOR_NEXT_BUTTON); } + /** + * Get Doc Table + */ + static getDocTable() { + return cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DOC_TABLE); + } + /** * 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); + return DataExplorerPage.getDocTable().get('tbody tr').eq(rowNumber); } /** @@ -127,7 +131,7 @@ export class DataExplorerPage { * find all Rows in Doc Table Field Expanded Document. * @param expandedDocument cypress representation of the Doc Table Field Expanded Document */ - static findDocTableExpandedDocRows(expandedDocument) { + static findDocTableExpandedDocRowsIn(expandedDocument) { return expandedDocument.findElementsByTestIdLike( DATA_EXPLORER_PAGE_ELEMENTS.DOC_TABLE_EXPANDED_DOC_COLUMN_ROW_PREFIX ); @@ -171,7 +175,7 @@ export class DataExplorerPage { * getExpandedDocRow(1, 0); */ static getExpandedDocRow(docTableRowNumber, expandedDocumentRowNumber) { - return DataExplorerPage.findDocTableExpandedDocRows( + return DataExplorerPage.findDocTableExpandedDocRowsIn( DataExplorerPage.getDocTableRow(docTableRowNumber + 1) ).eq(expandedDocumentRowNumber); }