From 073a9ff3bbf17ee4e6a85a794b759bb370899533 Mon Sep 17 00:00:00 2001 From: Miki Date: Mon, 25 Nov 2024 22:43:34 -0800 Subject: [PATCH 01/24] 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 c24d5bc8426d4572fc34351a860390fb66858137 Mon Sep 17 00:00:00 2001 From: Tianyu Gao Date: Wed, 27 Nov 2024 17:29:58 +0800 Subject: [PATCH 02/24] [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 8f58bceec42038554f84c6f7a5ad6be58f847cfb Mon Sep 17 00:00:00 2001 From: Paul Sebastian Date: Wed, 27 Nov 2024 03:23:45 -0800 Subject: [PATCH 03/24] [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 473c0aecf6ba2d660ec37a7285f07e8542a60499 Mon Sep 17 00:00:00 2001 From: Miki Date: Wed, 27 Nov 2024 12:33:46 -0800 Subject: [PATCH 04/24] 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 5bd2a4a5d09f..9d83eec7c6cf 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 5b3dec208a45..b19d1350a13f 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" @@ -11393,7 +11393,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 4dac5a79773f15a223f37309531164b112a3836c Mon Sep 17 00:00:00 2001 From: Qxisylolo Date: Thu, 28 Nov 2024 17:17:28 +0800 Subject: [PATCH 05/24] [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 1c744d675ebf0c93a9990ddc7f424636e539fff9 Mon Sep 17 00:00:00 2001 From: Hailong Cui Date: Thu, 28 Nov 2024 17:18:42 +0800 Subject: [PATCH 06/24] [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 b31206a83833cfb66867d0eb4f3a83f0ddb8ec0c Mon Sep 17 00:00:00 2001 From: yuboluo Date: Mon, 2 Dec 2024 15:56:34 +0800 Subject: [PATCH 07/24] [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 f6d345b46595b3860ba65e28dcb1499aa5a94424 Mon Sep 17 00:00:00 2001 From: Argus Li Date: Mon, 2 Dec 2024 16:19:58 -0800 Subject: [PATCH 08/24] Add utils to select a data source. --- cypress.config.js | 2 + .../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, 150 insertions(+) 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 index 8ac393867e20..f856f5c30843 100644 --- a/cypress.config.js +++ b/cypress.config.js @@ -23,6 +23,8 @@ module.exports = defineConfig({ DATASOURCE_MANAGEMENT_ENABLED: false, ML_COMMONS_DASHBOARDS_ENABLED: true, WAIT_FOR_LOADER_BUFFER_MS: 0, + INDEX_CLUSTER_NAME: 'cypress-test-os', + INDEX_NAME: 'vis-builder', }, e2e: { baseUrl: 'http://localhost:5601', 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: Mon, 2 Dec 2024 16:54:14 -0800 Subject: [PATCH 09/24] [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 340326ff3d3813a6ed308886a46b97638bee1564 Mon Sep 17 00:00:00 2001 From: yuboluo Date: Tue, 3 Dec 2024 11:24:56 +0800 Subject: [PATCH 10/24] [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 d5e0087f825faf89094ae639d88e26c18b3708c0 Mon Sep 17 00:00:00 2001 From: Joshua Li Date: Mon, 2 Dec 2024 20:01:48 -0800 Subject: [PATCH 11/24] [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 e07680b8f3f40e5d805be5396a4d3742ca7f0763 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 12/24] 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> --- changelogs/fragments/8926.yml | 2 + changelogs/fragments/8993.yml | 2 + cypress.config.ts | 65 +++++++++ cypress.json | 21 --- cypress/support/e2e.js | 6 + package.json | 3 +- yarn.lock | 242 ++++++++++++++++++++++++++++++---- 7 files changed, 293 insertions(+), 48 deletions(-) create mode 100644 changelogs/fragments/8926.yml create mode 100644 changelogs/fragments/8993.yml create mode 100644 cypress.config.ts delete mode 100644 cypress.json create mode 100644 cypress/support/e2e.js diff --git a/changelogs/fragments/8926.yml b/changelogs/fragments/8926.yml new file mode 100644 index 000000000000..b99f449c54ca --- /dev/null +++ b/changelogs/fragments/8926.yml @@ -0,0 +1,2 @@ +chore: +- Update cypress to v12 ([#8926](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8926)) \ No newline at end of file diff --git a/changelogs/fragments/8993.yml b/changelogs/fragments/8993.yml new file mode 100644 index 000000000000..dac519c8b746 --- /dev/null +++ b/changelogs/fragments/8993.yml @@ -0,0 +1,2 @@ +fix: +- Support imports without extensions in cypress webpack build ([#8993](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8993)) \ No newline at end of file diff --git a/cypress.config.ts b/cypress.config.ts new file mode 100644 index 000000000000..67e7b4f5039b --- /dev/null +++ b/cypress.config.ts @@ -0,0 +1,65 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { defineConfig } from 'cypress'; +import webpackPreprocessor from '@cypress/webpack-preprocessor'; + +module.exports = defineConfig({ + defaultCommandTimeout: 60000, + requestTimeout: 60000, + responseTimeout: 60000, + viewportWidth: 2000, + viewportHeight: 1320, + env: { + openSearchUrl: 'http://localhost:9200', + SECURITY_ENABLED: false, + AGGREGATION_VIEW: false, + username: 'admin', + password: 'myStrongPassword123!', + ENDPOINT_WITH_PROXY: false, + MANAGED_SERVICE_ENDPOINT: false, + VISBUILDER_ENABLED: true, + DATASOURCE_MANAGEMENT_ENABLED: false, + ML_COMMONS_DASHBOARDS_ENABLED: true, + WAIT_FOR_LOADER_BUFFER_MS: 0, + }, + e2e: { + baseUrl: 'http://localhost:5601', + specPattern: 'cypress/integration/**/*_spec.{js,jsx,ts,tsx}', + testIsolation: false, + setupNodeEvents, + }, +}); + +function setupNodeEvents( + on: Cypress.PluginEvents, + config: Cypress.PluginConfigOptions +): Cypress.PluginConfigOptions { + const { webpackOptions } = webpackPreprocessor.defaultOptions; + + /** + * By default, cypress' internal webpack preprocessor doesn't allow imports without file extensions. + * This makes our life a bit hard since if any file in our testing dependency graph has an import without + * the .js extension our cypress build will fail. + * + * This extra rule relaxes this a bit by allowing imports without file extension + * ex. import module from './module' + */ + webpackOptions!.module!.rules.unshift({ + test: /\.m?js/, + resolve: { + enforceExtension: false, + }, + }); + + on( + 'file:preprocessor', + webpackPreprocessor({ + webpackOptions, + }) + ); + + return config; +} diff --git a/cypress.json b/cypress.json deleted file mode 100644 index 46e8c7e8ea16..000000000000 --- a/cypress.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "defaultCommandTimeout": 60000, - "requestTimeout": 60000, - "responseTimeout": 60000, - "baseUrl": "http://localhost:5601", - "viewportWidth": 2000, - "viewportHeight": 1320, - "env": { - "openSearchUrl": "http://localhost:9200", - "SECURITY_ENABLED": false, - "AGGREGATION_VIEW": false, - "username": "admin", - "password": "myStrongPassword123!", - "ENDPOINT_WITH_PROXY": false, - "MANAGED_SERVICE_ENDPOINT": false, - "VISBUILDER_ENABLED": true, - "DATASOURCE_MANAGEMENT_ENABLED": false, - "ML_COMMONS_DASHBOARDS_ENABLED": true, - "WAIT_FOR_LOADER_BUFFER_MS": 0 - } -} diff --git a/cypress/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'; diff --git a/package.json b/package.json index 9d83eec7c6cf..0a103b9fdab1 100644 --- a/package.json +++ b/package.json @@ -259,6 +259,7 @@ "@babel/plugin-transform-class-static-block": "^7.24.4", "@babel/register": "^7.22.9", "@babel/types": "^7.22.9", + "@cypress/webpack-preprocessor": "^5.17.1", "@elastic/apm-rum": "^5.6.1", "@elastic/charts": "31.1.0", "@elastic/ems-client": "7.10.0", @@ -383,7 +384,7 @@ "chromedriver": "^121.0.1", "classnames": "^2.3.1", "compare-versions": "3.5.1", - "cypress": "9.5.4", + "cypress": "12.17.4", "d3": "3.5.17", "d3-cloud": "1.2.5", "dedent": "^0.7.0", diff --git a/yarn.lock b/yarn.lock index b19d1350a13f..537af6f3662e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1294,7 +1294,7 @@ resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== -"@cypress/request@^2.88.10": +"@cypress/request@2.88.12": version "2.88.12" resolved "https://registry.yarnpkg.com/@cypress/request/-/request-2.88.12.tgz#ba4911431738494a85e93fb04498cb38bc55d590" integrity sha512-tOn+0mDZxASFM+cuAP9szGUGPI1HwWVSvdzm7V4cCsPdFTx6qMj29CwaQmRAMIEhORIUBFBsYROYJcveK4uOjA== @@ -2929,6 +2929,11 @@ resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== +"@tootallnate/quickjs-emscripten@^0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz#db4ecfd499a9765ab24002c3b696d02e6d32a12c" + integrity sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA== + "@tsd/typescript@~4.7.3": version "4.7.4" resolved "https://registry.yarnpkg.com/@tsd/typescript/-/typescript-4.7.4.tgz#f1e4e6c3099a174a0cb7aa51cf53f34f6494e528" @@ -3534,7 +3539,7 @@ dependencies: "@types/node" "*" -"@types/node@*", "@types/node@12.20.24", "@types/node@16.9.1", "@types/node@^14.14.31", "@types/node@~18.7.0": +"@types/node@*", "@types/node@12.20.24", "@types/node@16.9.1", "@types/node@^16.18.39", "@types/node@~18.7.0": version "18.7.23" resolved "https://registry.yarnpkg.com/@types/node/-/node-18.7.23.tgz#75c580983846181ebe5f4abc40fe9dfb2d65665f" integrity sha512-DWNcCHolDq0ZKGizjx2DZjR/PqsYwAcYUJmfMWqtVU2MBMG5Mo+xFZrhGId5r/O5HOuMPyQEcM6KUBp5lBZZBg== @@ -4367,6 +4372,13 @@ agent-base@6: dependencies: debug "4" +agent-base@^7.0.2, agent-base@^7.1.0, agent-base@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.1.tgz#bdbded7dfb096b751a2a087eeeb9664725b2e317" + integrity sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA== + dependencies: + debug "^4.3.4" + agentkeepalive@^3.4.1, agentkeepalive@^4.2.1, agentkeepalive@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.5.0.tgz#2673ad1389b3c418c5a20c5d7364f93ca04be923" @@ -4815,6 +4827,13 @@ ast-types-flow@^0.0.7: resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.7.tgz#f70b735c6bca1a5c9c22d982c3e39e7feba3bdad" integrity sha1-9wtzXGvKGlycItmCw+Oef+ujva0= +ast-types@^0.13.4: + version "0.13.4" + resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.13.4.tgz#ee0d77b343263965ecc3fb62da16e7222b2b6782" + integrity sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w== + dependencies: + tslib "^2.0.1" + astral-regex@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9" @@ -4927,7 +4946,7 @@ axe-core@^4.0.2, axe-core@^4.3.5: resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.4.1.tgz#7dbdc25989298f9ad006645cd396782443757413" integrity sha512-gd1kmb21kwNuWr6BQz8fv6GNECPBnUasepcoLbekws23NVBLODdsClRZ+bQ8+9Uomf3Sm3+Vwn0oYG9NvwnJCw== -axios@^1.6.1, axios@^1.6.5: +axios@^1.6.1: version "1.7.7" resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.7.tgz#2f554296f9892a72ac8d8e4c5b79c14a91d0a47f" integrity sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q== @@ -4936,6 +4955,15 @@ axios@^1.6.1, axios@^1.6.5: form-data "^4.0.0" proxy-from-env "^1.1.0" +axios@^1.7.4: + version "1.7.8" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.8.tgz#1997b1496b394c21953e68c14aaa51b7b5de3d6e" + integrity sha512-Uu0wb7KNqK2t5K+YQyVCLM76prD5sRFjKHbJYCP1J7JFGEQ6nN7HWn9+04LAeiJ3ji54lgS/gZCH1oxyrf1SPw== + dependencies: + follow-redirects "^1.15.6" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + axobject-query@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be" @@ -5123,6 +5151,11 @@ basic-auth@^2.0.1: dependencies: safe-buffer "5.1.2" +basic-ftp@^5.0.2: + version "5.0.5" + resolved "https://registry.yarnpkg.com/basic-ftp/-/basic-ftp-5.0.5.tgz#14a474f5fffecca1f4f406f1c26b18f800225ac0" + integrity sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg== + batch-processor@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/batch-processor/-/batch-processor-1.0.0.tgz#75c95c32b748e0850d10c2b168f6bdbe9891ace8" @@ -5710,16 +5743,16 @@ chrome-trace-event@^1.0.2: resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac" integrity sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg== -chromedriver@^121.0.1: - version "121.0.2" - resolved "https://registry.yarnpkg.com/chromedriver/-/chromedriver-121.0.2.tgz#208909a61e9d510913107ea6faf34bcdd72cdced" - integrity sha512-58MUSCEE3oB3G3Y/Jo3URJ2Oa1VLHcVBufyYt7vNfGrABSJm7ienQLF9IQ8LPDlPVgLUXt2OBfggK3p2/SlEBg== +chromedriver@^131.0.1: + version "131.0.1" + resolved "https://registry.yarnpkg.com/chromedriver/-/chromedriver-131.0.1.tgz#bfbf47f6c2ad7a65c154ff47d321bd8c33b52a77" + integrity sha512-LHRh+oaNU1WowJjAkWsviN8pTzQYJDbv/FvJyrQ7XhjKdIzVh/s3GV1iU7IjMTsxIQnBsTjx+9jWjzCWIXC7ug== dependencies: "@testim/chrome-version" "^1.1.4" - axios "^1.6.5" + axios "^1.7.4" compare-versions "^6.1.0" extract-zip "^2.0.1" - https-proxy-agent "^5.0.1" + proxy-agent "^6.4.0" proxy-from-env "^1.1.0" tcp-port-used "^1.0.2" @@ -6017,11 +6050,16 @@ commander@^4.0.1: resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== -commander@^5.0.0, commander@^5.1.0: +commander@^5.0.0: version "5.1.0" resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae" integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg== +commander@^6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" + integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== + comment-stripper@^0.0.4: version "0.0.4" resolved "https://registry.yarnpkg.com/comment-stripper/-/comment-stripper-0.0.4.tgz#e8d61366d362779ea225c764f05cca6c950f8a2c" @@ -6498,14 +6536,14 @@ cyclist@^1.0.1: resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9" integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk= -cypress@9.5.4: - version "9.5.4" - resolved "https://registry.yarnpkg.com/cypress/-/cypress-9.5.4.tgz#49d9272f62eba12f2314faf29c2a865610e87550" - integrity sha512-6AyJAD8phe7IMvOL4oBsI9puRNOWxZjl8z1lgixJMcgJ85JJmyKeP6uqNA0dI1z14lmJ7Qklf2MOgP/xdAqJ/Q== +cypress@12.17.4: + version "12.17.4" + resolved "https://registry.yarnpkg.com/cypress/-/cypress-12.17.4.tgz#b4dadf41673058493fa0d2362faa3da1f6ae2e6c" + integrity sha512-gAN8Pmns9MA5eCDFSDJXWKUpaL3IDd89N9TtIupjYnzLSmlpVr+ZR+vb4U/qaMp+lB6tBvAmt7504c3Z4RU5KQ== dependencies: - "@cypress/request" "^2.88.10" + "@cypress/request" "2.88.12" "@cypress/xvfb" "^1.2.4" - "@types/node" "^14.14.31" + "@types/node" "^16.18.39" "@types/sinonjs__fake-timers" "8.1.1" "@types/sizzle" "^2.3.2" arch "^2.2.0" @@ -6517,12 +6555,12 @@ cypress@9.5.4: check-more-types "^2.24.0" cli-cursor "^3.1.0" cli-table3 "~0.6.1" - commander "^5.1.0" + commander "^6.2.1" common-tags "^1.8.0" dayjs "^1.10.4" - debug "^4.3.2" + debug "^4.3.4" enquirer "^2.3.6" - eventemitter2 "^6.4.3" + eventemitter2 "6.4.7" execa "4.1.0" executable "^4.1.1" extract-zip "2.0.1" @@ -6535,12 +6573,13 @@ cypress@9.5.4: listr2 "^3.8.3" lodash "^4.17.21" log-symbols "^4.0.0" - minimist "^1.2.6" + minimist "^1.2.8" ospath "^1.2.2" pretty-bytes "^5.6.0" + process "^0.11.10" proxy-from-env "1.0.0" request-progress "^3.0.0" - semver "^7.3.2" + semver "^7.5.3" supports-color "^8.1.1" tmp "~0.2.1" untildify "^4.0.0" @@ -6769,6 +6808,11 @@ dashify@^0.1.0: resolved "https://registry.yarnpkg.com/dashify/-/dashify-0.1.0.tgz#107daf9cca5e326e30a8b39ffa5048b6684922ea" integrity sha1-EH2vnMpeMm4wqLOf+lBItmhJIuo= +data-uri-to-buffer@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz#8a58bb67384b261a38ef18bea1810cb01badd28b" + integrity sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw== + data-urls@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-2.0.0.tgz#156485a72963a970f5d5821aaf642bef2bf2db9b" @@ -6967,6 +7011,15 @@ defined@^1.0.0: resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693" integrity sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM= +degenerator@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/degenerator/-/degenerator-5.0.1.tgz#9403bf297c6dad9a1ece409b37db27954f91f2f5" + integrity sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ== + dependencies: + ast-types "^0.13.4" + escodegen "^2.1.0" + esprima "^4.0.1" + del-cli@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/del-cli/-/del-cli-3.0.1.tgz#2d27ff260204b5104cadeda86f78f180a4ebe89a" @@ -7808,6 +7861,17 @@ escodegen@^2.0.0: optionalDependencies: source-map "~0.6.1" +escodegen@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-2.1.0.tgz#ba93bbb7a43986d29d6041f99f5262da773e2e17" + integrity sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w== + dependencies: + esprima "^4.0.1" + estraverse "^5.2.0" + esutils "^2.0.2" + optionalDependencies: + source-map "~0.6.1" + eslint-config-prettier@^6.11.0: version "6.15.0" resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-6.15.0.tgz#7f93f6cb7d45a92f1537a70ecc06366e1ac6fed9" @@ -8197,10 +8261,10 @@ event-target-shim@^5.0.0: resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== -eventemitter2@^6.4.3: - version "6.4.9" - resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.4.9.tgz#41f2750781b4230ed58827bc119d293471ecb125" - integrity sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg== +eventemitter2@6.4.7: + version "6.4.7" + resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.4.7.tgz#a7f6c4d7abf28a14c1ef3442f21cb306a054271d" + integrity sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg== eventemitter2@~0.4.13: version "0.4.14" @@ -8822,6 +8886,15 @@ fs-constants@^1.0.0: resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== +fs-extra@^11.2.0: + version "11.2.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.2.0.tgz#e70e17dfad64232287d01929399e0ea7c86b0e5b" + integrity sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + fs-extra@^9.1.0: version "9.1.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" @@ -8977,6 +9050,16 @@ get-symbol-description@^1.0.0: call-bind "^1.0.2" get-intrinsic "^1.1.1" +get-uri@^6.0.1: + version "6.0.3" + resolved "https://registry.yarnpkg.com/get-uri/-/get-uri-6.0.3.tgz#0d26697bc13cf91092e519aa63aa60ee5b6f385a" + integrity sha512-BzUrJBS9EcUb4cFol8r4W3v1cPsSyajLSthNkz5BxbpDcHN5tIrM10E2eNvfnvBn3DaT3DUgx0OpsBKkaOpanw== + dependencies: + basic-ftp "^5.0.2" + data-uri-to-buffer "^6.0.2" + debug "^4.3.4" + fs-extra "^11.2.0" + get-value@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/get-value/-/get-value-3.0.1.tgz#5efd2a157f1d6a516d7524e124ac52d0a39ef5a8" @@ -9803,6 +9886,14 @@ http-proxy-agent@^4.0.1: agent-base "6" debug "4" +http-proxy-agent@^7.0.0, http-proxy-agent@^7.0.1: + version "7.0.2" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz#9a8b1f246866c028509486585f62b8f2c18c270e" + integrity sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig== + dependencies: + agent-base "^7.1.0" + debug "^4.3.4" + http-signature@~1.3.6: version "1.3.6" resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.3.6.tgz#cb6fbfdf86d1c974f343be94e87f7fc128662cf9" @@ -9825,7 +9916,7 @@ https-browserify@^1.0.0: resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM= -https-proxy-agent@5.0.1, https-proxy-agent@^5.0.0, https-proxy-agent@^5.0.1: +https-proxy-agent@5.0.1, https-proxy-agent@^5.0.0: version "5.0.1" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== @@ -9833,6 +9924,14 @@ https-proxy-agent@5.0.1, https-proxy-agent@^5.0.0, https-proxy-agent@^5.0.1: agent-base "6" debug "4" +https-proxy-agent@^7.0.3, https-proxy-agent@^7.0.5: + version "7.0.5" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz#9e8b5013873299e11fab6fd548405da2d6c602b2" + integrity sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw== + dependencies: + agent-base "^7.0.2" + debug "4" + human-signals@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" @@ -10128,6 +10227,14 @@ ip-address@^6.3.0: lodash.repeat "4.1.0" sprintf-js "1.1.2" +ip-address@^9.0.5: + version "9.0.5" + resolved "https://registry.yarnpkg.com/ip-address/-/ip-address-9.0.5.tgz#117a960819b08780c3bd1f14ef3c1cc1d3f3ea5a" + integrity sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g== + dependencies: + jsbn "1.1.0" + sprintf-js "^1.1.3" + ip-cidr@^2.1.0: version "2.1.5" resolved "https://registry.yarnpkg.com/ip-cidr/-/ip-cidr-2.1.5.tgz#67fd02ee001d6ac0f253a1d577e4170a8f7d480b" @@ -12022,6 +12129,11 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" +lru-cache@^7.14.1: + version "7.18.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.18.3.tgz#f793896e0fd0e954a59dfdd82f0773808df6aa89" + integrity sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA== + lru-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/lru-queue/-/lru-queue-0.1.0.tgz#2738bd9f0d3cf4f84490c5736c48699ac632cda3" @@ -12418,6 +12530,11 @@ minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== +minimist@^1.2.8: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + minipass-collect@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/minipass-collect/-/minipass-collect-1.0.2.tgz#22b813bf745dc6edba2576b940022ad6edc8c617" @@ -12724,6 +12841,11 @@ nested-error-stacks@^2.0.0, nested-error-stacks@^2.1.0: resolved "https://registry.yarnpkg.com/nested-error-stacks/-/nested-error-stacks-2.1.1.tgz#26c8a3cee6cc05fbcf1e333cd2fc3e003326c0b5" integrity sha512-9iN1ka/9zmX1ZvLV9ewJYEk9h7RyRRtqdK0woXcqohu8EWIerfPUjYJPg0ULy0UqP7cslmdGc8xKDJcojlKiaw== +netmask@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/netmask/-/netmask-2.0.2.tgz#8b01a07644065d536383835823bc52004ebac5e7" + integrity sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg== + newtype-ts@^0.2.4: version "0.2.4" resolved "https://registry.yarnpkg.com/newtype-ts/-/newtype-ts-0.2.4.tgz#a02a8f160a3d179f871848d687a93de73a964a41" @@ -13338,6 +13460,28 @@ p-try@^2.0.0: resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== +pac-proxy-agent@^7.0.1: + version "7.0.2" + resolved "https://registry.yarnpkg.com/pac-proxy-agent/-/pac-proxy-agent-7.0.2.tgz#0fb02496bd9fb8ae7eb11cfd98386daaac442f58" + integrity sha512-BFi3vZnO9X5Qt6NRz7ZOaPja3ic0PhlsmCRYLOpN11+mWBCR6XJDqW5RF3j8jm4WGGQZtBA+bTfxYzeKW73eHg== + dependencies: + "@tootallnate/quickjs-emscripten" "^0.23.0" + agent-base "^7.0.2" + debug "^4.3.4" + get-uri "^6.0.1" + http-proxy-agent "^7.0.0" + https-proxy-agent "^7.0.5" + pac-resolver "^7.0.1" + socks-proxy-agent "^8.0.4" + +pac-resolver@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/pac-resolver/-/pac-resolver-7.0.1.tgz#54675558ea368b64d210fd9c92a640b5f3b8abb6" + integrity sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg== + dependencies: + degenerator "^5.0.0" + netmask "^2.0.2" + package-hash@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/package-hash/-/package-hash-4.0.0.tgz#3537f654665ec3cc38827387fc904c163c54f506" @@ -13938,6 +14082,20 @@ property-information@^5.0.0, property-information@^5.3.0: dependencies: xtend "^4.0.0" +proxy-agent@^6.4.0: + version "6.4.0" + resolved "https://registry.yarnpkg.com/proxy-agent/-/proxy-agent-6.4.0.tgz#b4e2dd51dee2b377748aef8d45604c2d7608652d" + integrity sha512-u0piLU+nCOHMgGjRbimiXmA9kM/L9EHh3zL81xCdp7m+Y2pHIsnmbdDoEDoAz5geaonNR6q6+yOPQs6n4T6sBQ== + dependencies: + agent-base "^7.0.2" + debug "^4.3.4" + http-proxy-agent "^7.0.1" + https-proxy-agent "^7.0.3" + lru-cache "^7.14.1" + pac-proxy-agent "^7.0.1" + proxy-from-env "^1.1.0" + socks-proxy-agent "^8.0.2" + proxy-from-env@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.0.0.tgz#33c50398f70ea7eb96d21f7b817630a55791c7ee" @@ -15581,6 +15739,28 @@ slide@~1.1.3: resolved "https://registry.yarnpkg.com/slide/-/slide-1.1.6.tgz#56eb027d65b4d2dce6cb2e2d32c4d4afc9e1d707" integrity sha1-VusCfWW00tzmyy4tMsTUr8nh1wc= +smart-buffer@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae" + integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== + +socks-proxy-agent@^8.0.2, socks-proxy-agent@^8.0.4: + version "8.0.4" + resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-8.0.4.tgz#9071dca17af95f483300316f4b063578fa0db08c" + integrity sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSG+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRw== + dependencies: + agent-base "^7.1.1" + debug "^4.3.4" + socks "^2.8.3" + +socks@^2.8.3: + version "2.8.3" + resolved "https://registry.yarnpkg.com/socks/-/socks-2.8.3.tgz#1ebd0f09c52ba95a09750afe3f3f9f724a800cb5" + integrity sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw== + dependencies: + ip-address "^9.0.5" + smart-buffer "^4.2.0" + sonic-boom@^1.0.2: version "1.4.1" resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-1.4.1.tgz#d35d6a74076624f12e6f917ade7b9d75e918f53e" @@ -15793,6 +15973,11 @@ sprintf-js@1.1.2, sprintf-js@^1.1.1: resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.2.tgz#da1765262bf8c0f571749f2ad6c26300207ae673" integrity sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug== +sprintf-js@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.3.tgz#4914b903a2f8b685d17fdf78a70e917e872e444a" + integrity sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA== + sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" @@ -16899,6 +17084,11 @@ tslib@^2.0.0, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.2.0, tslib@^2.3.0, tslib@^2.3 resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== +tslib@^2.0.1: + version "2.8.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" + integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== + tslib@~2.4.0: version "2.4.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.1.tgz#0d0bfbaac2880b91e22df0768e55be9753a5b17e" From fe616e7755faf7e263c55792d5062c597663847b Mon Sep 17 00:00:00 2001 From: Joshua Li Date: Tue, 3 Dec 2024 13:22:27 -0800 Subject: [PATCH 13/24] [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 4e54e2f941fc9f7a46f59f633dbab0f7b9c8846a Mon Sep 17 00:00:00 2001 From: Argus Li Date: Tue, 3 Dec 2024 21:58:09 -0800 Subject: [PATCH 14/24] Add support for different languages when creating dataset. Filter Out is almost complete, just working on removing the filter. --- cypress.config.js | 1 + .../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 +- 6 files changed, 172 insertions(+), 12 deletions(-) diff --git a/cypress.config.js b/cypress.config.js index f856f5c30843..c3a5c5445b07 100644 --- a/cypress.config.js +++ b/cypress.config.js @@ -25,6 +25,7 @@ module.exports = defineConfig({ WAIT_FOR_LOADER_BUFFER_MS: 0, INDEX_CLUSTER_NAME: 'cypress-test-os', INDEX_NAME: 'vis-builder', + INDEX_PATTERN_NAME: 'cypress-test-os::vis-builder*', }, e2e: { baseUrl: 'http://localhost:5601', 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 98e9042114a27fecb810e94f10e5b7fe8813917f Mon Sep 17 00:00:00 2001 From: Tyler Ohlsen Date: Wed, 4 Dec 2024 09:48:20 -0800 Subject: [PATCH 15/24] 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 5f95d74a74da19ba15473415c81ab70667690fb0 Mon Sep 17 00:00:00 2001 From: Argus Li Date: Wed, 4 Dec 2024 12:29:36 -0800 Subject: [PATCH 16/24] Reformat to match OSD-functional-tests. Reformat for TS. --- cypress.config.js | 34 ---- cypress.config.ts | 68 +++++++ .../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 ------------------ 9 files changed, 291 insertions(+), 265 deletions(-) delete mode 100644 cypress.config.js create mode 100644 cypress.config.ts 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.config.js b/cypress.config.js deleted file mode 100644 index c3a5c5445b07..000000000000 --- a/cypress.config.js +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -const { defineConfig } = require('cypress'); - -module.exports = defineConfig({ - defaultCommandTimeout: 60000, - requestTimeout: 60000, - responseTimeout: 60000, - viewportWidth: 2000, - viewportHeight: 1320, - env: { - openSearchUrl: 'http://localhost:9200', - SECURITY_ENABLED: false, - AGGREGATION_VIEW: false, - username: 'admin', - password: 'myStrongPassword123!', - ENDPOINT_WITH_PROXY: false, - MANAGED_SERVICE_ENDPOINT: false, - VISBUILDER_ENABLED: true, - DATASOURCE_MANAGEMENT_ENABLED: false, - ML_COMMONS_DASHBOARDS_ENABLED: true, - WAIT_FOR_LOADER_BUFFER_MS: 0, - INDEX_CLUSTER_NAME: 'cypress-test-os', - INDEX_NAME: 'vis-builder', - INDEX_PATTERN_NAME: 'cypress-test-os::vis-builder*', - }, - e2e: { - baseUrl: 'http://localhost:5601', - specPattern: 'cypress/integration/**/*_spec.{js,jsx,ts,tsx}', - }, -}); diff --git a/cypress.config.ts b/cypress.config.ts new file mode 100644 index 000000000000..66802069f200 --- /dev/null +++ b/cypress.config.ts @@ -0,0 +1,68 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { defineConfig } from 'cypress'; +import webpackPreprocessor from '@cypress/webpack-preprocessor'; + +module.exports = defineConfig({ + defaultCommandTimeout: 60000, + requestTimeout: 60000, + responseTimeout: 60000, + viewportWidth: 2000, + viewportHeight: 1320, + env: { + openSearchUrl: 'http://localhost:9200', + SECURITY_ENABLED: false, + AGGREGATION_VIEW: false, + username: 'admin', + password: 'myStrongPassword123!', + ENDPOINT_WITH_PROXY: false, + MANAGED_SERVICE_ENDPOINT: false, + VISBUILDER_ENABLED: true, + DATASOURCE_MANAGEMENT_ENABLED: false, + ML_COMMONS_DASHBOARDS_ENABLED: true, + WAIT_FOR_LOADER_BUFFER_MS: 0, + INDEX_CLUSTER_NAME: 'cypress-test-os', + INDEX_NAME: 'vis-builder', + INDEX_PATTERN_NAME: 'cypress-test-os::vis-builder*', + }, + e2e: { + baseUrl: 'http://localhost:5601', + specPattern: 'cypress/integration/**/*_spec.{js,jsx,ts,tsx}', + testIsolation: false, + setupNodeEvents, + }, +}); + +function setupNodeEvents( + on: Cypress.PluginEvents, + config: Cypress.PluginConfigOptions +): Cypress.PluginConfigOptions { + const { webpackOptions } = webpackPreprocessor.defaultOptions; + + /** + * By default, cypress' internal webpack preprocessor doesn't allow imports without file extensions. + * This makes our life a bit hard since if any file in our testing dependency graph has an import without + * the .js extension our cypress build will fail. + * + * This extra rule relaxes this a bit by allowing imports without file extension + * ex. import module from './module' + */ + webpackOptions!.module!.rules.unshift({ + test: /\.m?js/, + resolve: { + enforceExtension: false, + }, + }); + + on( + 'file:preprocessor', + webpackPreprocessor({ + webpackOptions, + }) + ); + + return config; +} diff --git a/cypress/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 cd6c3d8cb923e0885e0d0476eff4c1aa1d17e301 Mon Sep 17 00:00:00 2001 From: Argus Li Date: Wed, 4 Dec 2024 23:36:11 -0800 Subject: [PATCH 17/24] Complete test suite filter actions in table field. Refactor to match OSD-functional-tests-layout. --- cypress.config.ts | 6 +- .../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(+), 213 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 66802069f200..52eddacb6e99 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -24,14 +24,12 @@ module.exports = defineConfig({ DATASOURCE_MANAGEMENT_ENABLED: false, ML_COMMONS_DASHBOARDS_ENABLED: true, WAIT_FOR_LOADER_BUFFER_MS: 0, - INDEX_CLUSTER_NAME: 'cypress-test-os', - INDEX_NAME: 'vis-builder', - INDEX_PATTERN_NAME: 'cypress-test-os::vis-builder*', }, 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/24] 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 36bf5e83fe2b44b2221488d3c0149a58d5d4884a Mon Sep 17 00:00:00 2001 From: yuboluo Date: Thu, 5 Dec 2024 17:06:58 +0800 Subject: [PATCH 19/24] [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 41ac8ab6c9c5162b3ea419f22fa4173e6d6ad9e5 Mon Sep 17 00:00:00 2001 From: Joshua Li Date: Thu, 5 Dec 2024 09:04:14 -0800 Subject: [PATCH 20/24] 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 3f659387cc6fa1a9294348869d6132264d788d68 Mon Sep 17 00:00:00 2001 From: Joshua Li Date: Thu, 5 Dec 2024 10:32:08 -0800 Subject: [PATCH 21/24] [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 6118ee50bafa40cb0619264636b33ec14f517c80 Mon Sep 17 00:00:00 2001 From: Argus Li Date: Thu, 5 Dec 2024 12:12:18 -0800 Subject: [PATCH 22/24] 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 f78b3e56bf817af608610f0797a2a99e30a0ad77 Mon Sep 17 00:00:00 2001 From: Argus Li Date: Thu, 5 Dec 2024 15:36:14 -0800 Subject: [PATCH 23/24] 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 cde2eaf5b52f25673d4fe02a6358a4c31a54cddd Mon Sep 17 00:00:00 2001 From: Argus Li Date: Fri, 6 Dec 2024 08:56:52 -0800 Subject: [PATCH 24/24] Remove should be visible as each command should be a simple get. --- .../dashboards/data_explorer/commands.js | 26 ++++++------------- 1 file changed, 8 insertions(+), 18 deletions(-) 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 + ); });