From 073a9ff3bbf17ee4e6a85a794b759bb370899533 Mon Sep 17 00:00:00 2001 From: Miki Date: Mon, 25 Nov 2024 22:43:34 -0800 Subject: [PATCH 01/80] 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/80] [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/80] [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/80] 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/80] [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/80] [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/80] [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 080b0db8142451af98b156d0faceed7a269bf558 Mon Sep 17 00:00:00 2001 From: Sean Li Date: Mon, 2 Dec 2024 16:54:14 -0800 Subject: [PATCH 08/80] [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 09/80] [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 10/80] [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 11/80] 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 12/80] [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 98e9042114a27fecb810e94f10e5b7fe8813917f Mon Sep 17 00:00:00 2001 From: Tyler Ohlsen Date: Wed, 4 Dec 2024 09:48:20 -0800 Subject: [PATCH 13/80] 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 36bf5e83fe2b44b2221488d3c0149a58d5d4884a Mon Sep 17 00:00:00 2001 From: yuboluo Date: Thu, 5 Dec 2024 17:06:58 +0800 Subject: [PATCH 14/80] [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 15/80] 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 16/80] [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 af429b6f5db5a1f973924ff3cff6743322ea4fe3 Mon Sep 17 00:00:00 2001 From: Lin Wang Date: Fri, 6 Dec 2024 09:24:20 +0800 Subject: [PATCH 17/80] [Workspace]Fix error toasts in sample data page (#8842) * Set default index pattern when workspace disabled Signed-off-by: Lin Wang * Move saved objects first to avoid partial deleted Signed-off-by: Lin Wang * Skip ui setting update for non workspace admin Signed-off-by: Lin Wang * Add UT for sample_data_client Signed-off-by: Lin Wang * Changeset file for PR #8842 created/updated --------- Signed-off-by: Lin Wang Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> --- changelogs/fragments/8842.yml | 2 + .../opensearch_dashboards_services.ts | 2 + .../public/application/sample_data_client.js | 18 +- .../application/sample_data_client.test.js | 167 ++++++++++++++++++ src/plugins/home/public/plugin.ts | 1 + .../services/sample_data/routes/uninstall.ts | 48 ++--- 6 files changed, 215 insertions(+), 23 deletions(-) create mode 100644 changelogs/fragments/8842.yml create mode 100644 src/plugins/home/public/application/sample_data_client.test.js diff --git a/changelogs/fragments/8842.yml b/changelogs/fragments/8842.yml new file mode 100644 index 000000000000..b9973f347f9e --- /dev/null +++ b/changelogs/fragments/8842.yml @@ -0,0 +1,2 @@ +fix: +- [Workspace]Fix error toasts in sample data page ([#8842](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8842)) \ No newline at end of file diff --git a/src/plugins/home/public/application/opensearch_dashboards_services.ts b/src/plugins/home/public/application/opensearch_dashboards_services.ts index 1107e46ecf2e..eb4b085d86ae 100644 --- a/src/plugins/home/public/application/opensearch_dashboards_services.ts +++ b/src/plugins/home/public/application/opensearch_dashboards_services.ts @@ -37,6 +37,7 @@ import { SavedObjectsClientContract, IUiSettingsClient, ApplicationStart, + WorkspacesSetup, } from 'opensearch-dashboards/public'; import { UiStatsMetricType } from '@osd/analytics'; import { TelemetryPluginStart } from '../../../telemetry/public'; @@ -77,6 +78,7 @@ export interface HomeOpenSearchDashboardsServices { }; dataSource?: DataSourcePluginStart; sectionTypes: SectionTypeService; + workspaces?: WorkspacesSetup; } let services: HomeOpenSearchDashboardsServices | null = null; diff --git a/src/plugins/home/public/application/sample_data_client.js b/src/plugins/home/public/application/sample_data_client.js index 045736c428f6..b2adaf44cf81 100644 --- a/src/plugins/home/public/application/sample_data_client.js +++ b/src/plugins/home/public/application/sample_data_client.js @@ -41,11 +41,26 @@ export async function listSampleDataSets(dataSourceId) { return await getServices().http.get(sampleDataUrl, { query }); } +const canUpdateUISetting = () => { + const { + application: { capabilities }, + workspaces, + } = getServices(); + if ( + capabilities.workspaces && + capabilities.workspaces.enabled && + capabilities.workspaces.permissionEnabled + ) { + return !!workspaces?.currentWorkspace$.getValue()?.owner; + } + return true; +}; + export async function installSampleDataSet(id, sampleDataDefaultIndex, dataSourceId) { const query = buildQuery(dataSourceId); await getServices().http.post(`${sampleDataUrl}/${id}`, { query }); - if (getServices().uiSettings.isDefault('defaultIndex')) { + if (canUpdateUISetting() && getServices().uiSettings.isDefault('defaultIndex')) { getServices().uiSettings.set('defaultIndex', sampleDataDefaultIndex); } @@ -59,6 +74,7 @@ export async function uninstallSampleDataSet(id, sampleDataDefaultIndex, dataSou const uiSettings = getServices().uiSettings; if ( + canUpdateUISetting() && !uiSettings.isDefault('defaultIndex') && uiSettings.get('defaultIndex') === sampleDataDefaultIndex ) { diff --git a/src/plugins/home/public/application/sample_data_client.test.js b/src/plugins/home/public/application/sample_data_client.test.js new file mode 100644 index 000000000000..35f86efef729 --- /dev/null +++ b/src/plugins/home/public/application/sample_data_client.test.js @@ -0,0 +1,167 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { BehaviorSubject } from 'rxjs'; +import { setServices } from '../application/opensearch_dashboards_services'; +import { installSampleDataSet, uninstallSampleDataSet } from './sample_data_client'; + +const mockHttp = { + post: jest.fn(), + delete: jest.fn(), +}; + +const mockUiSettings = { + isDefault: jest.fn(), + set: jest.fn(), + get: jest.fn(), +}; + +const mockApplication = { + capabilities: { + workspaces: { + enabled: false, + permissionEnabled: false, + }, + }, +}; + +const mockIndexPatternService = { + clearCache: jest.fn(), +}; + +const mockWorkspace = { + currentWorkspace$: new BehaviorSubject(), +}; + +const mockServices = { + workspaces: mockWorkspace, + http: mockHttp, + uiSettings: mockUiSettings, + application: mockApplication, + indexPatternService: mockIndexPatternService, +}; + +setServices(mockServices); + +describe('installSampleDataSet', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockUiSettings.isDefault.mockReturnValue(true); + setServices(mockServices); + }); + + it('should install the sample data set and set the default index', async () => { + const id = 'sample-data-id'; + const sampleDataDefaultIndex = 'sample-data-index'; + const dataSourceId = 'data-source-id'; + + await installSampleDataSet(id, sampleDataDefaultIndex, dataSourceId); + + expect(mockHttp.post).toHaveBeenCalledWith(`/api/sample_data/${id}`, { + query: expect.anything(), + }); + expect(mockUiSettings.set).toHaveBeenCalledWith('defaultIndex', sampleDataDefaultIndex); + expect(mockIndexPatternService.clearCache).toHaveBeenCalled(); + }); + + it('should install the sample data set and not set the default index when workspace is enabled', async () => { + const id = 'sample-data-id'; + const sampleDataDefaultIndex = 'sample-data-index'; + const dataSourceId = 'data-source-id'; + + setServices({ + ...mockServices, + workspaces: { + currentWorkspace$: new BehaviorSubject(), + }, + application: { + capabilities: { + workspaces: { + enabled: true, + permissionEnabled: true, + }, + }, + }, + }); + + await installSampleDataSet(id, sampleDataDefaultIndex, dataSourceId); + + expect(mockHttp.post).toHaveBeenCalledWith(`/api/sample_data/${id}`, { + query: expect.anything(), + }); + expect(mockUiSettings.set).not.toHaveBeenCalled(); + expect(mockIndexPatternService.clearCache).toHaveBeenCalled(); + }); +}); + +describe('uninstallSampleDataSet', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockUiSettings.isDefault.mockReturnValue(false); + setServices(mockServices); + }); + + it('should uninstall the sample data set and clear the default index', async () => { + const id = 'sample-data-id'; + const sampleDataDefaultIndex = 'sample-data-index'; + const dataSourceId = 'data-source-id'; + + mockUiSettings.get.mockReturnValue(sampleDataDefaultIndex); + + await uninstallSampleDataSet(id, sampleDataDefaultIndex, dataSourceId); + + expect(mockHttp.delete).toHaveBeenCalledWith(`/api/sample_data/${id}`, { + query: expect.anything(), + }); + expect(mockUiSettings.set).toHaveBeenCalledWith('defaultIndex', null); + expect(mockIndexPatternService.clearCache).toHaveBeenCalled(); + }); + + it('should uninstall the sample data set and not clear the default index when workspace is enabled', async () => { + const id = 'sample-data-id'; + const sampleDataDefaultIndex = 'sample-data-index'; + const dataSourceId = 'data-source-id'; + + setServices({ + ...mockServices, + workspaces: { + currentWorkspace$: new BehaviorSubject(), + }, + application: { + capabilities: { + workspaces: { + enabled: true, + permissionEnabled: true, + }, + }, + }, + }); + + await uninstallSampleDataSet(id, sampleDataDefaultIndex, dataSourceId); + + expect(mockHttp.delete).toHaveBeenCalledWith(`/api/sample_data/${id}`, { + query: expect.anything(), + }); + expect(mockUiSettings.set).not.toHaveBeenCalled(); + expect(mockIndexPatternService.clearCache).toHaveBeenCalled(); + }); + + it('should uninstall the sample data set and not clear the default index when it is not the sample data index', async () => { + const id = 'sample-data-id'; + const sampleDataDefaultIndex = 'sample-data-index'; + const dataSourceId = 'data-source-id'; + + mockUiSettings.isDefault.mockReturnValue(false); + mockUiSettings.get.mockReturnValue('other-index'); + + await uninstallSampleDataSet(id, sampleDataDefaultIndex, dataSourceId); + + expect(mockHttp.delete).toHaveBeenCalledWith(`/api/sample_data/${id}`, { + query: expect.anything(), + }); + expect(mockUiSettings.set).not.toHaveBeenCalled(); + expect(mockIndexPatternService.clearCache).toHaveBeenCalled(); + }); +}); diff --git a/src/plugins/home/public/plugin.ts b/src/plugins/home/public/plugin.ts index 435c7d4d3b9f..6d9771c724ef 100644 --- a/src/plugins/home/public/plugin.ts +++ b/src/plugins/home/public/plugin.ts @@ -156,6 +156,7 @@ export class HomePublicPlugin injectedMetadata: coreStart.injectedMetadata, dataSource, sectionTypes: this.sectionTypeService, + workspaces: core.workspaces, ...homeOpenSearchDashboardsServices, }); }; diff --git a/src/plugins/home/server/services/sample_data/routes/uninstall.ts b/src/plugins/home/server/services/sample_data/routes/uninstall.ts index 3e4636c32486..da8dea3c2fe3 100644 --- a/src/plugins/home/server/services/sample_data/routes/uninstall.ts +++ b/src/plugins/home/server/services/sample_data/routes/uninstall.ts @@ -62,27 +62,10 @@ export function createUninstallRoute( return response.notFound(); } - const caller = dataSourceId - ? context.dataSource.opensearch.legacy.getClient(dataSourceId).callAPI - : context.core.opensearch.legacy.client.callAsCurrentUser; - - for (let i = 0; i < sampleDataset.dataIndices.length; i++) { - const dataIndexConfig = sampleDataset.dataIndices[i]; - const index = - dataIndexConfig.indexName ?? createIndexName(sampleDataset.id, dataIndexConfig.id); - - try { - await caller('indices.delete', { index }); - } catch (err) { - return response.customError({ - statusCode: err.status, - body: { - message: `Unable to delete sample data index "${index}", error: ${err.message}`, - }, - }); - } - } - + /** + * Delete saved objects before removing the data index to avoid partial deletion + * of sample data when a read-only workspace user attempts to remove sample data. + */ const savedObjectsList = getFinalSavedObjects({ dataset: sampleDataset, workspaceId, @@ -99,7 +82,7 @@ export function createUninstallRoute( // ignore 404s since users could have deleted some of the saved objects via the UI if (_.get(err, 'output.statusCode') !== 404) { return response.customError({ - statusCode: err.status, + statusCode: err.status || _.get(err, 'output.statusCode'), body: { message: `Unable to delete sample dataset saved objects, error: ${err.message}`, }, @@ -107,6 +90,27 @@ export function createUninstallRoute( } } + const caller = dataSourceId + ? context.dataSource.opensearch.legacy.getClient(dataSourceId).callAPI + : context.core.opensearch.legacy.client.callAsCurrentUser; + + for (let i = 0; i < sampleDataset.dataIndices.length; i++) { + const dataIndexConfig = sampleDataset.dataIndices[i]; + const index = + dataIndexConfig.indexName ?? createIndexName(sampleDataset.id, dataIndexConfig.id); + + try { + await caller('indices.delete', { index }); + } catch (err) { + return response.customError({ + statusCode: err.status, + body: { + message: `Unable to delete sample data index "${index}", error: ${err.message}`, + }, + }); + } + } + // track the usage operation in a non-blocking way usageTracker.addUninstall(request.params.id); From afb260501bdf3f7f9a151af6db8cfef4c0dd0219 Mon Sep 17 00:00:00 2001 From: Justin Kim Date: Fri, 6 Dec 2024 12:37:55 -0800 Subject: [PATCH 18/80] fix: update the osd-plugin-generator template to reference the correct button (#9014) * fix: update the osd-plugin-generator template to reference the correct button Signed-off-by: Justin Kim * remove the size prop Signed-off-by: Justin Kim * change it to EuiButton Signed-off-by: Justin Kim --------- Signed-off-by: Justin Kim --- .../osd-plugin-generator/template/public/components/app.tsx.ejs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/osd-plugin-generator/template/public/components/app.tsx.ejs b/packages/osd-plugin-generator/template/public/components/app.tsx.ejs index 2029a69dd8db..876b3f8c5e75 100644 --- a/packages/osd-plugin-generator/template/public/components/app.tsx.ejs +++ b/packages/osd-plugin-generator/template/public/components/app.tsx.ejs @@ -4,7 +4,7 @@ import { FormattedMessage, I18nProvider } from '@osd/i18n/react'; import { BrowserRouter as Router } from 'react-router-dom'; import { -EuiSmallButton, +EuiButton, EuiHorizontalRule, EuiPage, EuiPageBody, From 7df73ddeea9eb8f0c462cc8a099dc32f49d14692 Mon Sep 17 00:00:00 2001 From: Joshua Li Date: Fri, 6 Dec 2024 13:25:54 -0800 Subject: [PATCH 19/80] [Discover] Support custom logic to insert time filter based on dataset type (#8932) * Pass time filter if language overrides hideDatePicker --------- Signed-off-by: Joshua Li Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> --- changelogs/fragments/8932.yml | 2 + .../dataset_service/dataset_service.mock.ts | 3 + .../query_string/dataset_service/types.ts | 9 ++- .../ui/dataset_selector/configurator.test.tsx | 72 +++++++++++++++++-- .../ui/dataset_selector/configurator.tsx | 40 +++++++---- .../query_editor_top_row.test.tsx | 62 ++++++++++++++-- .../ui/query_editor/query_editor_top_row.tsx | 55 +++++++++++--- .../query_enhancements/common/types.ts | 7 +- .../query_enhancements/common/utils.ts | 1 + .../public/search/ppl_search_interceptor.ts | 21 +++++- .../public/search/sql_search_interceptor.ts | 11 +++ .../query_enhancements/server/routes/index.ts | 1 + 12 files changed, 246 insertions(+), 38 deletions(-) create mode 100644 changelogs/fragments/8932.yml diff --git a/changelogs/fragments/8932.yml b/changelogs/fragments/8932.yml new file mode 100644 index 000000000000..a048de0a102a --- /dev/null +++ b/changelogs/fragments/8932.yml @@ -0,0 +1,2 @@ +feat: +- Support custom logic to insert time filter based on dataset type ([#8932](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8932)) \ No newline at end of file diff --git a/src/plugins/data/public/query/query_string/dataset_service/dataset_service.mock.ts b/src/plugins/data/public/query/query_string/dataset_service/dataset_service.mock.ts index df5521078feb..ba491cb51191 100644 --- a/src/plugins/data/public/query/query_string/dataset_service/dataset_service.mock.ts +++ b/src/plugins/data/public/query/query_string/dataset_service/dataset_service.mock.ts @@ -43,6 +43,9 @@ const createSetupDatasetServiceMock = (): jest.Mocked => fetchOptions: jest.fn(), getRecentDatasets: jest.fn(), addRecentDataset: jest.fn(), + clearCache: jest.fn(), + getLastCacheTime: jest.fn(), + removeFromRecentDatasets: jest.fn(), }; }; diff --git a/src/plugins/data/public/query/query_string/dataset_service/types.ts b/src/plugins/data/public/query/query_string/dataset_service/types.ts index 65c322acec6f..d97afec8abb6 100644 --- a/src/plugins/data/public/query/query_string/dataset_service/types.ts +++ b/src/plugins/data/public/query/query_string/dataset_service/types.ts @@ -43,6 +43,13 @@ export interface DatasetTypeConfig { id: string; /** Human-readable title for the dataset type */ title: string; + languageOverrides?: { + [language: string]: { + /** The override transfers the responsibility of handling the input from + * the language interceptor to the dataset type search strategy. */ + hideDatePicker?: boolean; + }; + }; /** Metadata for UI representation */ meta: { /** Icon to represent the dataset type */ @@ -51,7 +58,7 @@ export interface DatasetTypeConfig { tooltip?: string; /** Optional preference for search on page load else defaulted to true */ searchOnLoad?: boolean; - /** Optional supportsTimeFilter determines if a time filter is needed */ + /** Optional supportsTimeFilter determines if a time field is supported */ supportsTimeFilter?: boolean; /** Optional isFieldLoadAsync determines if field loads are async */ isFieldLoadAsync?: boolean; diff --git a/src/plugins/data/public/ui/dataset_selector/configurator.test.tsx b/src/plugins/data/public/ui/dataset_selector/configurator.test.tsx index 462c6298a0a3..38d4e4e12183 100644 --- a/src/plugins/data/public/ui/dataset_selector/configurator.test.tsx +++ b/src/plugins/data/public/ui/dataset_selector/configurator.test.tsx @@ -3,14 +3,14 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { render, screen, fireEvent, waitFor } from '@testing-library/react'; -import { Configurator } from './configurator'; import '@testing-library/jest-dom'; +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import React from 'react'; -import { setQueryService, setIndexPatterns } from '../../services'; import { IntlProvider } from 'react-intl'; -import { Query } from '../../../../data/public'; import { Dataset } from 'src/plugins/data/common'; +import { Query } from '../../../../data/public'; +import { setIndexPatterns, setQueryService } from '../../services'; +import { Configurator } from './configurator'; const getQueryMock = jest.fn().mockReturnValue({ query: '', @@ -358,4 +358,68 @@ describe('Configurator Component', () => { expect(submitButton).toBeEnabled(); }); }); + + it('should show the date picker if supportsTimeFilter is undefined', async () => { + const mockDataset = { + ...mockBaseDataset, + timeFieldName: undefined, + type: 'index', + }; + const { container } = render( + + + + ); + + expect( + container.querySelector(`[data-test-subj="advancedSelectorTimeFieldSelect"]`) + ).toBeTruthy(); + }); + + it('should hide the date picker if supportsTimeFilter is false', async () => { + const mockDataset = { + ...mockBaseDataset, + timeFieldName: undefined, + type: 'index', + }; + const datasetTypeConfig = mockServices + .getQueryService() + .queryString.getDatasetService() + .getType(); + mockServices + .getQueryService() + .queryString.getDatasetService() + .getType.mockReturnValue({ + ...datasetTypeConfig, + meta: { + supportsTimeFilter: false, + }, + }); + const { container } = render( + + + + ); + + expect( + container.querySelector(`[data-test-subj="advancedSelectorTimeFieldSelect"]`) + ).toBeFalsy(); + + mockServices + .getQueryService() + .queryString.getDatasetService() + .getType.mockReturnValue(datasetTypeConfig); + }); }); diff --git a/src/plugins/data/public/ui/dataset_selector/configurator.tsx b/src/plugins/data/public/ui/dataset_selector/configurator.tsx index 0dba9107934c..4906bec2ef84 100644 --- a/src/plugins/data/public/ui/dataset_selector/configurator.tsx +++ b/src/plugins/data/public/ui/dataset_selector/configurator.tsx @@ -69,6 +69,7 @@ export const Configurator = ({ const [selectedIndexedView, setSelectedIndexedView] = useState(); const [indexedViews, setIndexedViews] = useState([]); const [isLoadingIndexedViews, setIsLoadingIndexedViews] = useState(false); + const [timeFieldsLoading, setTimeFieldsLoading] = useState(false); useEffect(() => { let isMounted = true; @@ -91,23 +92,26 @@ export const Configurator = ({ const submitDisabled = useMemo(() => { return ( - timeFieldName === undefined && - !( - languageService.getLanguage(language)?.hideDatePicker || - dataset.type === DEFAULT_DATA.SET_TYPES.INDEX_PATTERN - ) && - timeFields && - timeFields.length > 0 + timeFieldsLoading || + (timeFieldName === undefined && + !(dataset.type === DEFAULT_DATA.SET_TYPES.INDEX_PATTERN) && + timeFields && + timeFields.length > 0) ); - }, [dataset, language, timeFieldName, timeFields, languageService]); + }, [dataset, timeFieldName, timeFields, timeFieldsLoading]); useEffect(() => { const fetchFields = async () => { - const datasetFields = await queryString - .getDatasetService() - .getType(baseDataset.type) - ?.fetchFields(baseDataset); + const datasetType = queryString.getDatasetService().getType(baseDataset.type); + if (!datasetType) { + setTimeFields([]); + return; + } + setTimeFieldsLoading(true); + const datasetFields = await datasetType + .fetchFields(baseDataset) + .finally(() => setTimeFieldsLoading(false)); const dateFields = datasetFields?.filter((field) => field.type === 'date'); setTimeFields(dateFields || []); }; @@ -152,6 +156,16 @@ export const Configurator = ({ }; }, [indexedViewsService, selectedIndexedView, dataset]); + const shouldRenderDatePickerField = useCallback(() => { + const datasetType = queryString.getDatasetService().getType(dataset.type); + + const supportsTimeField = datasetType?.meta?.supportsTimeFilter; + if (supportsTimeField !== undefined) { + return Boolean(supportsTimeField); + } + return true; + }, [dataset.type, queryString]); + return ( <> @@ -256,7 +270,7 @@ export const Configurator = ({ data-test-subj="advancedSelectorLanguageSelect" /> - {!languageService.getLanguage(language)?.hideDatePicker && + {shouldRenderDatePickerField() && (dataset.type === DEFAULT_DATA.SET_TYPES.INDEX_PATTERN ? ( ({ }); const dataPlugin = dataPluginMock.createStartContract(true); +const datasetService = datasetServiceMock.createStartContract(); function wrapQueryEditorTopRowInContext(testProps: any) { const defaultOptions = { @@ -111,6 +113,7 @@ describe('QueryEditorTopRow', () => { beforeEach(() => { jest.clearAllMocks(); (getQueryService as jest.Mock).mockReturnValue(dataPlugin.query); + dataPlugin.query.queryString.getDatasetService = jest.fn().mockReturnValue(datasetService); }); afterEach(() => { @@ -155,4 +158,49 @@ describe('QueryEditorTopRow', () => { await waitFor(() => expect(container.querySelector(QUERY_EDITOR)).toBeTruthy()); expect(container.querySelector(DATE_PICKER)).toBeFalsy(); }); + + it('Should not render date picker if dataset type does not support time field', async () => { + const query: Query = { + query: 'test query', + dataset: datasetService.getDefault(), + language: 'test-language', + }; + dataPlugin.query.queryString.getQuery = jest.fn().mockReturnValue(query); + datasetService.getType.mockReturnValue({ + meta: { supportsTimeFilter: false }, + } as DatasetTypeConfig); + + const { container } = render( + wrapQueryEditorTopRowInContext({ + query, + showQueryEditor: false, + showDatePicker: true, + }) + ); + await waitFor(() => expect(container.querySelector(QUERY_EDITOR)).toBeTruthy()); + expect(container.querySelector(DATE_PICKER)).toBeFalsy(); + }); + + it('Should render date picker if dataset overrides hideDatePicker to false', async () => { + const query: Query = { + query: 'test query', + dataset: datasetService.getDefault(), + language: 'test-language', + }; + dataPlugin.query.queryString.getQuery = jest.fn().mockReturnValue(query); + datasetService.getType.mockReturnValue(({ + meta: { supportsTimeFilter: true }, + languageOverrides: { 'test-language': { hideDatePicker: false } }, + } as unknown) as DatasetTypeConfig); + + const { container } = render( + wrapQueryEditorTopRowInContext({ + query, + showQueryEditor: false, + showDatePicker: true, + }) + ); + await waitFor(() => expect(container.querySelector(QUERY_EDITOR)).toBeTruthy()); + expect(container.querySelector(DATE_PICKER)).toBeTruthy(); + }); }); diff --git a/src/plugins/data/public/ui/query_editor/query_editor_top_row.tsx b/src/plugins/data/public/ui/query_editor/query_editor_top_row.tsx index ea15fbfeeaa1..ad22750207ed 100644 --- a/src/plugins/data/public/ui/query_editor/query_editor_top_row.tsx +++ b/src/plugins/data/public/ui/query_editor/query_editor_top_row.tsx @@ -224,18 +224,53 @@ export default function QueryEditorTopRow(props: QueryEditorTopRowProps) { ); } + /** + * Determines if the date picker should be rendered based on UI settings, dataset configuration, and language settings. + * + * @returns {boolean} Whether the date picker should be rendered + * + * UI Settings permutations (isDatePickerEnabled): + * - showDatePicker=true || showAutoRefreshOnly=true => true + * - showDatePicker=false && showAutoRefreshOnly=false => false + * - both undefined => true (default) + * If isDatePickerEnabled is false, returns false immediately + * + * Dataset Type permutations (datasetType?.meta?.supportsTimeFilter): + * - supportsTimeFilter=false => false + * + * Language permutations (when dataset.meta.supportsTimeFilter is undefined or true): + * - queryLanguage=undefined => true (shows date picker) + * - queryLanguage exists: + * - languageOverrides[queryLanguage].hideDatePicker=true => false + * - languageOverrides[queryLanguage].hideDatePicker=false => true + * - hideDatePicker=true => false + * - hideDatePicker=false => true + * - hideDatePicker=undefined => true + */ function shouldRenderDatePicker(): boolean { - return ( - Boolean((props.showDatePicker || props.showAutoRefreshOnly) ?? true) && - !( - queryLanguage && - data.query.queryString.getLanguageService().getLanguage(queryLanguage)?.hideDatePicker - ) && - (props.query?.dataset - ? data.query.queryString.getDatasetService().getType(props.query.dataset.type)?.meta - ?.supportsTimeFilter !== false - : true) + const { queryString } = data.query; + const datasetService = queryString.getDatasetService(); + const languageService = queryString.getLanguageService(); + const isDatePickerEnabled = Boolean( + (props.showDatePicker || props.showAutoRefreshOnly) ?? true ); + if (!isDatePickerEnabled) return false; + + // Get dataset type configuration + const datasetType = props.query?.dataset + ? datasetService.getType(props.query?.dataset.type) + : undefined; + // Check if dataset type explicitly configures the `supportsTimeFilter` option + if (datasetType?.meta?.supportsTimeFilter === false) return false; + + if ( + queryLanguage && + datasetType?.languageOverrides?.[queryLanguage]?.hideDatePicker !== undefined + ) { + return Boolean(!datasetType.languageOverrides[queryLanguage].hideDatePicker); + } + + return Boolean(!(queryLanguage && languageService.getLanguage(queryLanguage)?.hideDatePicker)); } function shouldRenderQueryEditor(): boolean { diff --git a/src/plugins/query_enhancements/common/types.ts b/src/plugins/query_enhancements/common/types.ts index 1bb977527d4a..2f73ca52d496 100644 --- a/src/plugins/query_enhancements/common/types.ts +++ b/src/plugins/query_enhancements/common/types.ts @@ -4,7 +4,7 @@ */ import { CoreSetup } from 'opensearch-dashboards/public'; -import { PollQueryResultsParams } from '../../data/common'; +import { PollQueryResultsParams, TimeRange } from '../../data/common'; export interface QueryAggConfig { [key: string]: { @@ -26,7 +26,10 @@ export interface EnhancedFetchContext { http: CoreSetup['http']; path: string; signal?: AbortSignal; - body?: { pollQueryResultsParams: PollQueryResultsParams }; + body?: { + pollQueryResultsParams?: PollQueryResultsParams; + timeRange?: TimeRange; + }; } export interface QueryStatusOptions { diff --git a/src/plugins/query_enhancements/common/utils.ts b/src/plugins/query_enhancements/common/utils.ts index 29e49b00eab0..634a56b84603 100644 --- a/src/plugins/query_enhancements/common/utils.ts +++ b/src/plugins/query_enhancements/common/utils.ts @@ -55,6 +55,7 @@ export const fetch = (context: EnhancedFetchContext, query: Query, aggConfig?: Q query: { ...query, format: 'jdbc' }, aggConfig, pollQueryResultsParams: context.body?.pollQueryResultsParams, + timeRange: context.body?.timeRange, }); return from( http.fetch({ diff --git a/src/plugins/query_enhancements/public/search/ppl_search_interceptor.ts b/src/plugins/query_enhancements/public/search/ppl_search_interceptor.ts index 57152dbe98ea..ecfe32ff8a75 100644 --- a/src/plugins/query_enhancements/public/search/ppl_search_interceptor.ts +++ b/src/plugins/query_enhancements/public/search/ppl_search_interceptor.ts @@ -50,6 +50,7 @@ export class PPLSearchInterceptor extends SearchInterceptor { signal, body: { pollQueryResultsParams: request.params?.pollQueryResultsParams, + timeRange: request.params?.body?.timeRange, }, }; @@ -68,15 +69,33 @@ export class PPLSearchInterceptor extends SearchInterceptor { .getDatasetService() .getType(datasetType); strategy = datasetTypeConfig?.getSearchOptions?.().strategy ?? strategy; + + if ( + dataset?.timeFieldName && + datasetTypeConfig?.languageOverrides?.PPL?.hideDatePicker === false + ) { + request.params = { + ...request.params, + body: { + ...request.params.body, + timeRange: this.queryService.timefilter.timefilter.getTime(), + }, + }; + } } return this.runSearch(request, options.abortSignal, strategy); } private buildQuery() { - const query: Query = this.queryService.queryString.getQuery(); + const { queryString } = this.queryService; + const query: Query = queryString.getQuery(); const dataset = query.dataset; if (!dataset || !dataset.timeFieldName) return query; + const datasetService = queryString.getDatasetService(); + if (datasetService.getType(dataset.type)?.languageOverrides?.PPL?.hideDatePicker === false) + return query; + const [baseQuery, ...afterPipeParts] = query.query.split('|'); const afterPipe = afterPipeParts.length > 0 ? ` | ${afterPipeParts.join('|').trim()}` : ''; const timeFilter = this.getTimeFilter(dataset.timeFieldName); diff --git a/src/plugins/query_enhancements/public/search/sql_search_interceptor.ts b/src/plugins/query_enhancements/public/search/sql_search_interceptor.ts index 9fe17fc79322..9f93dd067cb3 100644 --- a/src/plugins/query_enhancements/public/search/sql_search_interceptor.ts +++ b/src/plugins/query_enhancements/public/search/sql_search_interceptor.ts @@ -42,6 +42,7 @@ export class SQLSearchInterceptor extends SearchInterceptor { signal, body: { pollQueryResultsParams: request.params?.pollQueryResultsParams, + timeRange: request.params?.body?.timeRange, }, }; @@ -62,6 +63,16 @@ export class SQLSearchInterceptor extends SearchInterceptor { .getDatasetService() .getType(datasetType); strategy = datasetTypeConfig?.getSearchOptions?.().strategy ?? strategy; + + if (datasetTypeConfig?.languageOverrides?.SQL?.hideDatePicker === false) { + request.params = { + ...request.params, + body: { + ...request.params.body, + timeRange: this.queryService.timefilter.timefilter.getTime(), + }, + }; + } } return this.runSearch(request, options.abortSignal, strategy); diff --git a/src/plugins/query_enhancements/server/routes/index.ts b/src/plugins/query_enhancements/server/routes/index.ts index 84cf19bec50c..2cda4a9f0cbf 100644 --- a/src/plugins/query_enhancements/server/routes/index.ts +++ b/src/plugins/query_enhancements/server/routes/index.ts @@ -86,6 +86,7 @@ export function defineSearchStrategyRouteProvider(logger: Logger, router: IRoute sessionId: schema.maybe(schema.string()), }) ), + timeRange: schema.maybe(schema.object({}, { unknowns: 'allow' })), }), }, }, From 6a0484ae23251ecff347bb126b1a5dc9391bbe30 Mon Sep 17 00:00:00 2001 From: Argus Li <43020525+ArgusLi@users.noreply.github.com> Date: Tue, 10 Dec 2024 11:32:22 -0800 Subject: [PATCH 20/80] [Discover Test] TestID-69: Filtering. Add data-test-subj tags. (#9027) * 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> * Add data-test-subj tags. Signed-off-by: Argus Li --------- Signed-off-by: Daniel Rowe Signed-off-by: Daniel Rowe <51932404+d-rowe@users.noreply.github.com> Signed-off-by: Argus Li Co-authored-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> --- .../data/public/ui/dataset_selector/dataset_explorer.tsx | 1 + src/plugins/data/public/ui/filter_bar/filter_bar.tsx | 7 ++++++- .../ui/filter_bar/filter_editor/lib/filter_label.test.tsx | 4 ++++ .../ui/filter_bar/filter_editor/lib/filter_label.tsx | 6 +++++- 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/plugins/data/public/ui/dataset_selector/dataset_explorer.tsx b/src/plugins/data/public/ui/dataset_selector/dataset_explorer.tsx index 7861dd836cd1..ec8e118157b1 100644 --- a/src/plugins/data/public/ui/dataset_selector/dataset_explorer.tsx +++ b/src/plugins/data/public/ui/dataset_selector/dataset_explorer.tsx @@ -152,6 +152,7 @@ export const DatasetExplorer = ({
( - + { : Warning @@ -125,6 +126,7 @@ test('alias with error status', () => { : Error @@ -141,6 +143,7 @@ test('warning', () => { : Warning @@ -157,6 +160,7 @@ test('error', () => { : Error diff --git a/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_label.tsx b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_label.tsx index 529053ffd042..32f14b3eba34 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_label.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_label.tsx @@ -59,7 +59,11 @@ export default function FilterLabel({ filter, valueLabel, filterLabelStatus }: F ); const getValue = (text?: string) => { - return {text}; + return ( + + {text} + + ); }; if (filter.meta.alias !== null) { From 9f234422debc120bd9028ed8d202d7d3da7e565e Mon Sep 17 00:00:00 2001 From: Federico Silva Date: Tue, 10 Dec 2024 17:15:59 -0300 Subject: [PATCH 21/80] add queryEditorLanguageOptions data-test-subj on language_selector.tsx (#9029) Signed-off-by: Federico Silva Co-authored-by: Federico Silva --- src/plugins/data/public/ui/query_editor/language_selector.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/plugins/data/public/ui/query_editor/language_selector.tsx b/src/plugins/data/public/ui/query_editor/language_selector.tsx index 3047cefda31b..0177ede6f7fb 100644 --- a/src/plugins/data/public/ui/query_editor/language_selector.tsx +++ b/src/plugins/data/public/ui/query_editor/language_selector.tsx @@ -151,6 +151,7 @@ export const QueryLanguageSelector = (props: QueryLanguageSelectorProps) => { )} size="s" items={languageOptionsMenu} + data-test-subj="queryEditorLanguageOptions" /> ); From d79ea88a74c0875ab0d89cd40654fce963f04b4c Mon Sep 17 00:00:00 2001 From: Joanne Wang Date: Thu, 14 Nov 2024 14:54:57 -0800 Subject: [PATCH 22/80] [Discover] Add max height and scroll to error message body (#8867) * add max height and scroll Signed-off-by: Joanne Wang * Changeset file for PR #8867 created/updated * comments and update snapshot Signed-off-by: Joanne Wang --------- Signed-off-by: Joanne Wang Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> Signed-off-by: Federico Silva --- changelogs/fragments/8867.yml | 2 ++ .../lib/__snapshots__/query_result.test.tsx.snap | 2 ++ .../query/query_string/language_service/lib/query_result.tsx | 5 ++++- 3 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 changelogs/fragments/8867.yml diff --git a/changelogs/fragments/8867.yml b/changelogs/fragments/8867.yml new file mode 100644 index 000000000000..384f388393c4 --- /dev/null +++ b/changelogs/fragments/8867.yml @@ -0,0 +1,2 @@ +fix: +- Add max height and scroll to error message body ([#8867](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8867)) \ No newline at end of file diff --git a/src/plugins/data/public/query/query_string/language_service/lib/__snapshots__/query_result.test.tsx.snap b/src/plugins/data/public/query/query_string/language_service/lib/__snapshots__/query_result.test.tsx.snap index a0fd2861a2b4..f3d4e3df2c92 100644 --- a/src/plugins/data/public/query/query_string/language_service/lib/__snapshots__/query_result.test.tsx.snap +++ b/src/plugins/data/public/query/query_string/language_service/lib/__snapshots__/query_result.test.tsx.snap @@ -37,6 +37,8 @@ exports[`Query Result show error status with error message 2`] = ` className="eui-textBreakWord" style={ Object { + "maxHeight": "250px", + "overflowY": "auto", "width": "250px", } } diff --git a/src/plugins/data/public/query/query_string/language_service/lib/query_result.tsx b/src/plugins/data/public/query/query_string/language_service/lib/query_result.tsx index 5378cf8a111c..dff7faea36e3 100644 --- a/src/plugins/data/public/query/query_string/language_service/lib/query_result.tsx +++ b/src/plugins/data/public/query/query_string/language_service/lib/query_result.tsx @@ -143,7 +143,10 @@ export function QueryResult(props: { queryStatus: QueryStatus }) { data-test-subj="queryResultError" > ERRORS -
+

From 0acd8341be68dbbe677d590525995567af889e82 Mon Sep 17 00:00:00 2001 From: "Qingyang(Abby) Hu" Date: Thu, 14 Nov 2024 16:18:05 -0800 Subject: [PATCH 23/80] Keep previous query result if current query result in error (#8863) * keep previous result Signed-off-by: abbyhu2000 * Changeset file for PR #8863 created/updated * add some comment Signed-off-by: abbyhu2000 * invalid first query shows refresh data page Signed-off-by: abbyhu2000 --------- Signed-off-by: abbyhu2000 Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> Signed-off-by: Federico Silva --- changelogs/fragments/8863.yml | 2 ++ .../view_components/canvas/index.tsx | 23 ++++++++++--------- 2 files changed, 14 insertions(+), 11 deletions(-) create mode 100644 changelogs/fragments/8863.yml diff --git a/changelogs/fragments/8863.yml b/changelogs/fragments/8863.yml new file mode 100644 index 000000000000..51dc8d37cc2f --- /dev/null +++ b/changelogs/fragments/8863.yml @@ -0,0 +1,2 @@ +fix: +- Keep previous query result if current query result in error ([#8863](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8863)) \ No newline at end of file diff --git a/src/plugins/discover/public/application/view_components/canvas/index.tsx b/src/plugins/discover/public/application/view_components/canvas/index.tsx index 71d47446c75c..5fe1bac50891 100644 --- a/src/plugins/discover/public/application/view_components/canvas/index.tsx +++ b/src/plugins/discover/public/application/view_components/canvas/index.tsx @@ -84,8 +84,13 @@ export default function DiscoverCanvas({ setHeaderActionMenu, history, optionalR if (next.bucketInterval && next.bucketInterval !== fetchState.bucketInterval) shouldUpdateState = true; if (next.chartData && next.chartData !== fetchState.chartData) shouldUpdateState = true; - // we still want to show rows from the previous query while current query is loading - if (next.status !== ResultStatus.LOADING && next.rows && next.rows !== fetchState.rows) { + // we still want to show rows from the previous query while current query is loading or the current query results in error + if ( + next.status !== ResultStatus.LOADING && + next.status !== ResultStatus.ERROR && + next.rows && + next.rows !== fetchState.rows + ) { shouldUpdateState = true; setRows(next.rows); } @@ -152,20 +157,16 @@ export default function DiscoverCanvas({ setHeaderActionMenu, history, optionalR timeFieldName={timeField} /> )} - {fetchState.status === ResultStatus.ERROR && ( - - )} {fetchState.status === ResultStatus.UNINITIALIZED && ( refetch$.next()} /> )} {fetchState.status === ResultStatus.LOADING && !rows?.length && } + {fetchState.status === ResultStatus.ERROR && !rows?.length && ( + refetch$.next()} /> + )} {(fetchState.status === ResultStatus.READY || - (fetchState.status === ResultStatus.LOADING && !!rows?.length)) && + (fetchState.status === ResultStatus.LOADING && !!rows?.length) || + (fetchState.status === ResultStatus.ERROR && !!rows?.length)) && (isEnhancementsEnabled ? ( <> From 3965f67f9022180990e1a73965007a26c12b208d Mon Sep 17 00:00:00 2001 From: Miki Date: Fri, 15 Nov 2024 09:01:16 -0800 Subject: [PATCH 24/80] Fix a typo while inspecting values for large numerals in OSD and the JS client (#8839) * [@osd/std] Fix typo while inspecting values for large numerals Signed-off-by: Miki * Patch @opensearch-project/opensearch to fix a typo Ref: https://github.com/opensearch-project/opensearch-js/pull/889 Signed-off-by: Miki * Changeset file for PR #8839 created/updated --------- Signed-off-by: Miki Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> Signed-off-by: Federico Silva --- changelogs/fragments/8839.yml | 2 + packages/osd-std/src/json.test.ts | 84 +++++++++++++++++++++++++++++-- packages/osd-std/src/json.ts | 2 +- scripts/postinstall.js | 9 ++++ 4 files changed, 93 insertions(+), 4 deletions(-) create mode 100644 changelogs/fragments/8839.yml diff --git a/changelogs/fragments/8839.yml b/changelogs/fragments/8839.yml new file mode 100644 index 000000000000..27477e376254 --- /dev/null +++ b/changelogs/fragments/8839.yml @@ -0,0 +1,2 @@ +fix: +- Fix a typo while inspecting values for large numerals in OSD and the JS client ([#8839](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8839)) \ No newline at end of file diff --git a/packages/osd-std/src/json.test.ts b/packages/osd-std/src/json.test.ts index 33abd71d91d2..0d4b900e0ca5 100644 --- a/packages/osd-std/src/json.test.ts +++ b/packages/osd-std/src/json.test.ts @@ -3,6 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +import JSON11 from 'json11'; import { stringify, parse } from './json'; describe('json', () => { @@ -90,9 +91,55 @@ describe('json', () => { expect(stringify(input, replacer, 2)).toEqual(JSON.stringify(input, replacer, 2)); }); - it('can handle long numerals while parsing', () => { - const longPositive = BigInt(Number.MAX_SAFE_INTEGER) * 2n; - const longNegative = BigInt(Number.MIN_SAFE_INTEGER) * 2n; + it('can handle positive long numerals while parsing', () => { + const longPositiveA = BigInt(Number.MAX_SAFE_INTEGER) * 2n; + const longPositiveB = BigInt(Number.MAX_SAFE_INTEGER) * 2n + 1n; + const text = + `{` + + // The space before and after the values, and the lack of spaces before comma are intentional + `"\\":${longPositiveA}": "[ ${longPositiveB.toString()}, ${longPositiveA.toString()} ]", ` + + `"positive": ${longPositiveA.toString()}, ` + + `"array": [ ${longPositiveB.toString()}, ${longPositiveA.toString()} ], ` + + `"negative": ${longPositiveB.toString()},` + + `"number": 102931203123987` + + `}`; + + const result = parse(text); + expect(result.positive).toBe(longPositiveA); + expect(result.negative).toBe(longPositiveB); + expect(result.array).toEqual([longPositiveB, longPositiveA]); + expect(result['":' + longPositiveA]).toBe( + `[ ${longPositiveB.toString()}, ${longPositiveA.toString()} ]` + ); + expect(result.number).toBe(102931203123987); + }); + + it('can handle negative long numerals while parsing', () => { + const longNegativeA = BigInt(Number.MIN_SAFE_INTEGER) * 2n; + const longNegativeB = BigInt(Number.MIN_SAFE_INTEGER) * 2n - 1n; + const text = + `{` + + // The space before and after the values, and the lack of spaces before comma are intentional + `"\\":${longNegativeA}": "[ ${longNegativeB.toString()}, ${longNegativeA.toString()} ]", ` + + `"positive": ${longNegativeA.toString()}, ` + + `"array": [ ${longNegativeB.toString()}, ${longNegativeA.toString()} ], ` + + `"negative": ${longNegativeB.toString()},` + + `"number": 102931203123987` + + `}`; + + const result = parse(text); + expect(result.positive).toBe(longNegativeA); + expect(result.negative).toBe(longNegativeB); + expect(result.array).toEqual([longNegativeB, longNegativeA]); + expect(result['":' + longNegativeA]).toBe( + `[ ${longNegativeB.toString()}, ${longNegativeA.toString()} ]` + ); + expect(result.number).toBe(102931203123987); + }); + + it('can handle mixed long numerals while parsing', () => { + const longPositive = BigInt(Number.MAX_SAFE_INTEGER) * 2n + 1n; + const longNegative = BigInt(Number.MIN_SAFE_INTEGER) * 2n - 1n; const text = `{` + // The space before and after the values, and the lack of spaces before comma are intentional @@ -113,6 +160,37 @@ describe('json', () => { expect(result.number).toBe(102931203123987); }); + it('does not use JSON11 when not needed', () => { + const spyParse = jest.spyOn(JSON11, 'parse'); + + const longPositive = BigInt(Number.MAX_SAFE_INTEGER) * 2n + 1n; + const longNegative = BigInt(Number.MIN_SAFE_INTEGER) * 2n - 1n; + const text = + `{` + + `"\\":${longPositive}": "[ ${longNegative.toString()}, ${longPositive.toString()} ]", ` + + `"number": 102931203123987` + + `}`; + parse(text); + + expect(spyParse).not.toHaveBeenCalled(); + }); + + it('uses JSON11 when dealing with long numerals', () => { + const spyParse = jest.spyOn(JSON11, 'parse'); + + const longPositive = BigInt(Number.MAX_SAFE_INTEGER) * 2n + 1n; + const longNegative = BigInt(Number.MIN_SAFE_INTEGER) * 2n - 1n; + const text = + `{` + + `"\\":${longPositive}": "[ ${longNegative.toString()}, ${longPositive.toString()} ]", ` + + `"positive": ${longPositive.toString()}, ` + + `"number": 102931203123987` + + `}`; + parse(text); + + expect(spyParse).toHaveBeenCalled(); + }); + it('can handle BigInt values while stringifying', () => { const longPositive = BigInt(Number.MAX_SAFE_INTEGER) * 2n; const longNegative = BigInt(Number.MIN_SAFE_INTEGER) * 2n; diff --git a/packages/osd-std/src/json.ts b/packages/osd-std/src/json.ts index 4dcd3eb03e65..79a148f625f7 100644 --- a/packages/osd-std/src/json.ts +++ b/packages/osd-std/src/json.ts @@ -69,7 +69,7 @@ export const parse = ( numeralsAreNumbers && typeof val === 'number' && isFinite(val) && - (val < Number.MAX_SAFE_INTEGER || val > Number.MAX_SAFE_INTEGER) + (val < Number.MIN_SAFE_INTEGER || val > Number.MAX_SAFE_INTEGER) ) { numeralsAreNumbers = false; } diff --git a/scripts/postinstall.js b/scripts/postinstall.js index 7865473ee494..59be50284dca 100644 --- a/scripts/postinstall.js +++ b/scripts/postinstall.js @@ -84,6 +84,15 @@ 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); }; From bd26d1f879e916f17149a4e38201e9eb9eca78f7 Mon Sep 17 00:00:00 2001 From: Kawika Avilla Date: Fri, 15 Nov 2024 11:56:49 -0800 Subject: [PATCH 25/80] [Discover] Dataset search on page load issues (#8871) * Dataset search on page load issues Required a double click on search and then also potentially loading issue. Issue n/a Signed-off-by: Kawika Avilla * Clean up dependencies Signed-off-by: Kawika Avilla * Addresses issue Signed-off-by: Kawika Avilla * add some tests Signed-off-by: Kawika Avilla * Changeset file for PR #8871 created/updated --------- Signed-off-by: Kawika Avilla Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> Signed-off-by: Federico Silva --- changelogs/fragments/8871.yml | 2 + .../view_components/utils/use_search.test.tsx | 42 +++++++++++++++++++ .../view_components/utils/use_search.ts | 26 +++++------- 3 files changed, 54 insertions(+), 16 deletions(-) create mode 100644 changelogs/fragments/8871.yml diff --git a/changelogs/fragments/8871.yml b/changelogs/fragments/8871.yml new file mode 100644 index 000000000000..032a928fd5c0 --- /dev/null +++ b/changelogs/fragments/8871.yml @@ -0,0 +1,2 @@ +fix: +- Search on page load out of sync state when clicking submit. ([#8871](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8871)) \ 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 4a92bb5d37be..b76651899b61 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 @@ -110,6 +110,48 @@ describe('useSearch', () => { }); }); + it('should initialize with uninitialized state when dataset type config search on page load is disabled', async () => { + const services = createMockServices(); + (services.uiSettings.get as jest.Mock).mockReturnValueOnce(true); + (services.data.query.queryString.getDatasetService as jest.Mock).mockReturnValue({ + meta: { searchOnLoad: false }, + }); + (services.data.query.timefilter.timefilter.getRefreshInterval as jest.Mock).mockReturnValue({ + pause: true, + value: 10, + }); + + const { result, waitForNextUpdate } = renderHook(() => useSearch(services), { wrapper }); + expect(result.current.data$.getValue()).toEqual( + expect.objectContaining({ status: ResultStatus.UNINITIALIZED }) + ); + + await act(async () => { + await waitForNextUpdate(); + }); + }); + + it('should initialize with uninitialized state when dataset type config search on page load is enabled but the UI setting is disabled', async () => { + const services = createMockServices(); + (services.uiSettings.get as jest.Mock).mockReturnValueOnce(false); + (services.data.query.queryString.getDatasetService as jest.Mock).mockReturnValue({ + meta: { searchOnLoad: true }, + }); + (services.data.query.timefilter.timefilter.getRefreshInterval as jest.Mock).mockReturnValue({ + pause: true, + value: 10, + }); + + const { result, waitForNextUpdate } = renderHook(() => useSearch(services), { wrapper }); + expect(result.current.data$.getValue()).toEqual( + expect.objectContaining({ status: ResultStatus.UNINITIALIZED }) + ); + + await act(async () => { + await waitForNextUpdate(); + }); + }); + it('should update startTime when hook rerenders', async () => { const services = createMockServices(); 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 7f2270efc5a6..158a9cd46074 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 @@ -115,24 +115,23 @@ export const useSearch = (services: DiscoverViewServices) => { requests: new RequestAdapter(), }; - const getDatasetAutoSearchOnPageLoadPreference = () => { - // Checks the searchOnpageLoadPreference for the current dataset if not specifed defaults to true - const datasetType = data.query.queryString.getQuery().dataset?.type; - - const datasetService = data.query.queryString.getDatasetService(); - - return !datasetType || (datasetService?.getType(datasetType)?.meta?.searchOnLoad ?? true); - }; - const shouldSearchOnPageLoad = useCallback(() => { + // Checks the searchOnpageLoadPreference for the current dataset if not specifed defaults to UI Settings + const { queryString } = data.query; + const { dataset } = queryString.getQuery(); + const typeConfig = dataset ? queryString.getDatasetService().getType(dataset.type) : undefined; + const datasetPreference = + typeConfig?.meta?.searchOnLoad ?? uiSettings.get(SEARCH_ON_PAGE_LOAD_SETTING); + // A saved search is created on every page load, so we check the ID to see if we're loading a // previously saved search or if it is just transient return ( - services.uiSettings.get(SEARCH_ON_PAGE_LOAD_SETTING) || + datasetPreference || + uiSettings.get(SEARCH_ON_PAGE_LOAD_SETTING) || savedSearch?.id !== undefined || timefilter.getRefreshInterval().pause === false ); - }, [savedSearch, services.uiSettings, timefilter]); + }, [data.query, savedSearch, uiSettings, timefilter]); const startTime = Date.now(); const data$ = useMemo( @@ -346,9 +345,6 @@ export const useSearch = (services: DiscoverViewServices) => { ]); useEffect(() => { - if (!getDatasetAutoSearchOnPageLoadPreference()) { - skipInitialFetch.current = true; - } const fetch$ = merge( refetch$, filterManager.getFetches$(), @@ -379,8 +375,6 @@ export const useSearch = (services: DiscoverViewServices) => { return () => { subscription.unsubscribe(); }; - // disabling the eslint since we are not adding getDatasetAutoSearchOnPageLoadPreference since this changes when dataset changes and these chnages are already part of data.query.queryString - // eslint-disable-next-line react-hooks/exhaustive-deps }, [ data$, data.query.queryString, From 882fdf2afc95b22d9553c92aed69523db6e54f3f Mon Sep 17 00:00:00 2001 From: Sean Li Date: Fri, 15 Nov 2024 13:58:34 -0800 Subject: [PATCH 26/80] [Discover] Hide Date Picker For Unsupported Types (#8866) * initial commit for hiding date picker Signed-off-by: Sean Li * Changeset file for PR #8866 created/updated * adding tests for query_editor_top_row.tsx Signed-off-by: Sean Li * updating conditional 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> Signed-off-by: Federico Silva --- changelogs/fragments/8866.yml | 2 + .../language_service/language_service.mock.ts | 9 +- .../query_editor_top_row.test.tsx | 158 ++++++++++++++++++ .../ui/query_editor/query_editor_top_row.tsx | 14 +- 4 files changed, 178 insertions(+), 5 deletions(-) create mode 100644 changelogs/fragments/8866.yml create mode 100644 src/plugins/data/public/ui/query_editor/query_editor_top_row.test.tsx diff --git a/changelogs/fragments/8866.yml b/changelogs/fragments/8866.yml new file mode 100644 index 000000000000..9d328bf54e5b --- /dev/null +++ b/changelogs/fragments/8866.yml @@ -0,0 +1,2 @@ +fix: +- Hide Date Picker for Unsupported Types ([#8866](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8866)) \ No newline at end of file diff --git a/src/plugins/data/public/query/query_string/language_service/language_service.mock.ts b/src/plugins/data/public/query/query_string/language_service/language_service.mock.ts index 936ff690353d..e481932883ca 100644 --- a/src/plugins/data/public/query/query_string/language_service/language_service.mock.ts +++ b/src/plugins/data/public/query/query_string/language_service/language_service.mock.ts @@ -3,6 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { createEditor, DQLBody, SingleLineInput } from '../../../ui'; import { LanguageServiceContract } from './language_service'; import { LanguageConfig } from './types'; @@ -14,7 +15,7 @@ const createSetupLanguageServiceMock = (): jest.Mocked title: 'DQL', search: {} as any, getQueryString: jest.fn(), - editor: {} as any, + editor: createEditor(SingleLineInput, SingleLineInput, [], DQLBody), fields: { filterable: true, visualizable: true, @@ -28,7 +29,7 @@ const createSetupLanguageServiceMock = (): jest.Mocked title: 'Lucene', search: {} as any, getQueryString: jest.fn(), - editor: {} as any, + editor: createEditor(SingleLineInput, SingleLineInput, [], DQLBody), fields: { filterable: true, visualizable: true, @@ -42,7 +43,9 @@ const createSetupLanguageServiceMock = (): jest.Mocked return { __enhance: jest.fn(), - registerLanguage: jest.fn(), + registerLanguage: jest.fn((language: LanguageConfig) => { + languages.set(language.id, language); + }), getLanguage: jest.fn((id: string) => languages.get(id)), getLanguages: jest.fn(() => Array.from(languages.values())), getDefaultLanguage: jest.fn(() => languages.get('kuery') || languages.values().next().value), diff --git a/src/plugins/data/public/ui/query_editor/query_editor_top_row.test.tsx b/src/plugins/data/public/ui/query_editor/query_editor_top_row.test.tsx new file mode 100644 index 000000000000..62fe653bfd45 --- /dev/null +++ b/src/plugins/data/public/ui/query_editor/query_editor_top_row.test.tsx @@ -0,0 +1,158 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Query, UI_SETTINGS } from '../../../common'; +import { coreMock } from '../../../../../core/public/mocks'; +import { dataPluginMock } from '../../mocks'; +import React from 'react'; +import { I18nProvider } from '@osd/i18n/react'; +import { createEditor, DQLBody, QueryEditorTopRow, SingleLineInput } from '../'; +import { OpenSearchDashboardsContextProvider } from 'src/plugins/opensearch_dashboards_react/public'; +import { cleanup, render, waitFor } from '@testing-library/react'; +import { LanguageConfig } from '../../query'; +import { getQueryService } from '../../services'; + +const startMock = coreMock.createStart(); + +jest.mock('../../services', () => ({ + getQueryService: jest.fn(), +})); + +startMock.uiSettings.get.mockImplementation((key: string) => { + switch (key) { + case UI_SETTINGS.TIMEPICKER_QUICK_RANGES: + return [ + { + from: 'now/d', + to: 'now/d', + display: 'Today', + }, + ]; + case 'dateFormat': + return 'MMM D, YYYY @ HH:mm:ss.SSS'; + case UI_SETTINGS.HISTORY_LIMIT: + return 10; + case UI_SETTINGS.TIMEPICKER_TIME_DEFAULTS: + return { + from: 'now-15m', + to: 'now', + }; + case UI_SETTINGS.QUERY_ENHANCEMENTS_ENABLED: + return true; + case 'theme:darkMode': + return true; + default: + throw new Error(`Unexpected config key: ${key}`); + } +}); + +const createMockWebStorage = () => ({ + clear: jest.fn(), + getItem: jest.fn(), + key: jest.fn(), + removeItem: jest.fn(), + setItem: jest.fn(), + length: 0, +}); + +const createMockStorage = () => ({ + storage: createMockWebStorage(), + get: jest.fn(), + set: jest.fn(), + remove: jest.fn(), + clear: jest.fn(), +}); + +const dataPlugin = dataPluginMock.createStartContract(true); + +function wrapQueryEditorTopRowInContext(testProps: any) { + const defaultOptions = { + onSubmit: jest.fn(), + onChange: jest.fn(), + isDirty: true, + screenTitle: 'Another Screen', + }; + + const mockLanguage: LanguageConfig = { + id: 'test-language', + title: 'Test Language', + search: {} as any, + getQueryString: jest.fn(), + editor: createEditor(SingleLineInput, SingleLineInput, [], DQLBody), + fields: {}, + showDocLinks: true, + editorSupportedAppNames: ['discover'], + hideDatePicker: true, + }; + dataPlugin.query.queryString.getLanguageService().registerLanguage(mockLanguage); + + const services = { + ...startMock, + data: dataPlugin, + appName: 'discover', + storage: createMockStorage(), + }; + + return ( + + + + + + ); +} + +describe('QueryEditorTopRow', () => { + const QUERY_EDITOR = '.osdQueryEditor'; + const DATE_PICKER = '.osdQueryEditor__datePickerWrapper'; + + beforeEach(() => { + jest.clearAllMocks(); + (getQueryService as jest.Mock).mockReturnValue(dataPlugin.query); + }); + + afterEach(() => { + cleanup(); + jest.resetModules(); + }); + + it('Should render query editor', async () => { + const { container } = render( + wrapQueryEditorTopRowInContext({ + showQueryEditor: true, + }) + ); + await waitFor(() => expect(container.querySelector(QUERY_EDITOR)).toBeTruthy()); + expect(container.querySelector(DATE_PICKER)).toBeTruthy(); + }); + + it('Should not render date picker if showDatePicker is false', async () => { + const { container } = render( + wrapQueryEditorTopRowInContext({ + showQueryEditor: true, + showDatePicker: false, + }) + ); + await waitFor(() => expect(container.querySelector(QUERY_EDITOR)).toBeTruthy()); + expect(container.querySelector(DATE_PICKER)).toBeFalsy(); + }); + + it('Should not render date picker if language does not support time field', async () => { + const query: Query = { + query: 'test query', + language: 'test-language', + }; + dataPlugin.query.queryString.getQuery = jest.fn().mockReturnValue(query); + const { container } = render( + wrapQueryEditorTopRowInContext({ + query, + showQueryEditor: false, + showDatePicker: true, + }) + ); + await waitFor(() => expect(container.querySelector(QUERY_EDITOR)).toBeTruthy()); + expect(container.querySelector(DATE_PICKER)).toBeFalsy(); + }); +}); diff --git a/src/plugins/data/public/ui/query_editor/query_editor_top_row.tsx b/src/plugins/data/public/ui/query_editor/query_editor_top_row.tsx index ab9b8c50e038..ea15fbfeeaa1 100644 --- a/src/plugins/data/public/ui/query_editor/query_editor_top_row.tsx +++ b/src/plugins/data/public/ui/query_editor/query_editor_top_row.tsx @@ -72,7 +72,7 @@ export default function QueryEditorTopRow(props: QueryEditorTopRowProps) { const [isDateRangeInvalid, setIsDateRangeInvalid] = useState(false); const [isQueryEditorFocused, setIsQueryEditorFocused] = useState(false); const opensearchDashboards = useOpenSearchDashboards(); - const { uiSettings, storage, appName } = opensearchDashboards.services; + const { uiSettings, storage, appName, data } = opensearchDashboards.services; const queryLanguage = props.query && props.query.language; const persistedLog: PersistedLog | undefined = React.useMemo( @@ -225,7 +225,17 @@ export default function QueryEditorTopRow(props: QueryEditorTopRowProps) { } function shouldRenderDatePicker(): boolean { - return Boolean(props.showDatePicker ?? true) ?? (props.showAutoRefreshOnly && true); + return ( + Boolean((props.showDatePicker || props.showAutoRefreshOnly) ?? true) && + !( + queryLanguage && + data.query.queryString.getLanguageService().getLanguage(queryLanguage)?.hideDatePicker + ) && + (props.query?.dataset + ? data.query.queryString.getDatasetService().getType(props.query.dataset.type)?.meta + ?.supportsTimeFilter !== false + : true) + ); } function shouldRenderQueryEditor(): boolean { From 7441fef63300e41555a141e78854faa31fe70806 Mon Sep 17 00:00:00 2001 From: Anan Zhuang Date: Fri, 15 Nov 2024 14:06:38 -0800 Subject: [PATCH 27/80] [Bug] Make release note generation more resilient by gracefully handling invalid changelog fragments (#8780) Signed-off-by: Anan Zhuang Signed-off-by: Federico Silva --- src/dev/generate_release_note.ts | 100 +++++++++++++++++++++---------- 1 file changed, 70 insertions(+), 30 deletions(-) diff --git a/src/dev/generate_release_note.ts b/src/dev/generate_release_note.ts index 1c85995f814b..5cfa4503537d 100644 --- a/src/dev/generate_release_note.ts +++ b/src/dev/generate_release_note.ts @@ -3,6 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { ToolingLog } from '@osd/dev-utils'; import { join, resolve } from 'path'; import { readFileSync, writeFileSync, Dirent, rm, rename, promises as fsPromises } from 'fs'; import { load as loadYaml } from 'js-yaml'; @@ -19,6 +20,11 @@ import { filePath, } from './generate_release_note_helper'; +const log = new ToolingLog({ + level: 'info', + writeTo: process.stdout, +}); + // Function to add content after the 'Unreleased' section in the changelog function addContentAfterUnreleased(path: string, newContent: string): void { let fileContent = readFileSync(path, 'utf8'); @@ -60,35 +66,63 @@ async function readFragments() { ) as unknown) as Changelog; const fragmentPaths = await readdir(fragmentDirPath, { withFileTypes: true }); + const failedFragments: string[] = []; + for (const fragmentFilename of fragmentPaths) { // skip non yml or yaml files if (!/\.ya?ml$/i.test(fragmentFilename.name)) { - // eslint-disable-next-line no-console - console.warn(`Skipping non yml or yaml file ${fragmentFilename.name}`); + log.info(`Skipping non yml or yaml file ${fragmentFilename.name}`); continue; } - const fragmentPath = join(fragmentDirPath, fragmentFilename.name); - const fragmentContents = readFileSync(fragmentPath, { encoding: 'utf-8' }); - - validateFragment(fragmentContents); - - const fragmentContentLines = fragmentContents.split('\n'); - // Adding a quotes to the second line and escaping exisiting " within the line - fragmentContentLines[1] = fragmentContentLines[1].replace(/-\s*(.*)/, (match, p1) => { - // Escape any existing quotes in the content - const escapedContent = p1.replace(/"/g, '\\"'); - return `- "${escapedContent}"`; - }); - - const processedFragmentContent = fragmentContentLines.join('\n'); - - const fragmentYaml = loadYaml(processedFragmentContent) as Changelog; - for (const [sectionKey, entries] of Object.entries(fragmentYaml)) { - sections[sectionKey as SectionKey].push(...entries); + try { + const fragmentPath = join(fragmentDirPath, fragmentFilename.name); + const fragmentContents = readFileSync(fragmentPath, { encoding: 'utf-8' }); + + try { + validateFragment(fragmentContents); + } catch (validationError) { + log.info(`Validation failed for ${fragmentFilename.name}: ${validationError.message}`); + failedFragments.push( + `${fragmentFilename.name} (Validation Error: ${validationError.message})` + ); + continue; + } + + const fragmentContentLines = fragmentContents.split('\n'); + // Adding a quotes to the second line and escaping existing " within the line + fragmentContentLines[1] = fragmentContentLines[1].replace(/-\s*(.*)/, (match, p1) => { + // Escape any existing quotes in the content + const escapedContent = p1.replace(/"/g, '\\"'); + return `- "${escapedContent}"`; + }); + + const processedFragmentContent = fragmentContentLines.join('\n'); + + try { + const fragmentYaml = loadYaml(processedFragmentContent) as Changelog; + for (const [sectionKey, entries] of Object.entries(fragmentYaml)) { + sections[sectionKey as SectionKey].push(...entries); + } + } catch (yamlError) { + log.info(`Failed to parse YAML in ${fragmentFilename.name}: ${yamlError.message}`); + failedFragments.push(`${fragmentFilename.name} (YAML Parse Error: ${yamlError.message})`); + continue; + } + } catch (error) { + log.info(`Failed to process ${fragmentFilename.name}: ${error.message}`); + failedFragments.push(`${fragmentFilename.name} (Processing Error: ${error.message})`); + continue; } } - return { sections, fragmentPaths }; + + if (failedFragments.length > 0) { + log.info('\nThe following changelog fragments were skipped due to errors:'); + failedFragments.forEach((fragment) => log.info(`- ${fragment}`)); + log.info('\nPlease review and fix these fragments for inclusion in the next release.\n'); + } + + return { sections, fragmentPaths, failedFragments }; } async function moveFragments(fragmentPaths: Dirent[], fragmentTempDirPath: string): Promise { @@ -128,16 +162,22 @@ function generateReleaseNote(changelogSections: string[]) { } (async () => { - const { sections, fragmentPaths } = await readFragments(); - // create folder for temp fragments - const fragmentTempDirPath = await fsPromises.mkdtemp(join(fragmentDirPath, 'tmp_fragments-')); - // move fragments to temp fragments folder - await moveFragments(fragmentPaths, fragmentTempDirPath); + const { sections, fragmentPaths, failedFragments } = await readFragments(); - const changelogSections = generateChangelog(sections); + // Only proceed if we have some valid fragments + if (Object.values(sections).some((section) => section.length > 0)) { + // create folder for temp fragments + const fragmentTempDirPath = await fsPromises.mkdtemp(join(fragmentDirPath, 'tmp_fragments-')); + // move fragments to temp fragments folder + await moveFragments(fragmentPaths, fragmentTempDirPath); - generateReleaseNote(changelogSections); + const changelogSections = generateChangelog(sections); + generateReleaseNote(changelogSections); - // remove temp fragments folder - await deleteFragments(fragmentTempDirPath); + // remove temp fragments folder + await deleteFragments(fragmentTempDirPath); + } else { + log.error('No valid changelog entries were found. Release notes generation aborted.'); + process.exit(1); + } })(); From eede97bdd7cf47daf35d70589f6919d3e513fc75 Mon Sep 17 00:00:00 2001 From: Lu Yu Date: Mon, 18 Nov 2024 10:29:13 -0800 Subject: [PATCH 28/80] Fix openApi doc for bulk saved object API http method (#8885) * Fix openApi doc for bulk saved object API http method Signed-off-by: Lu Yu * Changeset file for PR #8885 created/updated --------- Signed-off-by: Lu Yu Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> Signed-off-by: Federico Silva --- changelogs/fragments/8885.yml | 2 ++ docs/openapi/saved_objects/saved_objects.yml | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 changelogs/fragments/8885.yml diff --git a/changelogs/fragments/8885.yml b/changelogs/fragments/8885.yml new file mode 100644 index 000000000000..5ba3f06558ca --- /dev/null +++ b/changelogs/fragments/8885.yml @@ -0,0 +1,2 @@ +doc: +- Fix OpenAPI documentation ([#8885](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8885)) \ No newline at end of file diff --git a/docs/openapi/saved_objects/saved_objects.yml b/docs/openapi/saved_objects/saved_objects.yml index bd1877545dc3..f54faa757072 100644 --- a/docs/openapi/saved_objects/saved_objects.yml +++ b/docs/openapi/saved_objects/saved_objects.yml @@ -423,7 +423,7 @@ paths: schema: type: object /api/saved_objects/_bulk_update: - post: + put: tags: - saved objects summary: Bulk update saved objects @@ -489,7 +489,7 @@ paths: schema: type: object /api/saved_objects/_bulk_get: - get: + post: tags: - saved objects summary: Bulk get saved objects From fc708ffd44b36637cac6328dc89dd496c66dff86 Mon Sep 17 00:00:00 2001 From: Miki Date: Mon, 18 Nov 2024 12:31:49 -0800 Subject: [PATCH 29/80] [CVE-2024-21538] Bump `cross-spawn` from 6.0.5 and 7.0.3 to 7.0.5 (#8882) * Bump `cross-spawn` from 6.0.5 and 7.0.3 to 7.0.5 Signed-off-by: Miki * Changeset file for PR #8882 created/updated --------- Signed-off-by: Miki Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> Signed-off-by: Federico Silva --- changelogs/fragments/8882.yml | 2 ++ package.json | 1 + yarn.lock | 45 +++++------------------------------ 3 files changed, 9 insertions(+), 39 deletions(-) create mode 100644 changelogs/fragments/8882.yml diff --git a/changelogs/fragments/8882.yml b/changelogs/fragments/8882.yml new file mode 100644 index 000000000000..d6fe67ac7888 --- /dev/null +++ b/changelogs/fragments/8882.yml @@ -0,0 +1,2 @@ +security: +- [CVE-2024-21538] Bump `cross-spawn` from 6.0.5 and 7.0.3 to 7.0.5 ([#8882](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8882)) \ No newline at end of file diff --git a/package.json b/package.json index 7c8b3ec5cfa8..5bd2a4a5d09f 100644 --- a/package.json +++ b/package.json @@ -102,6 +102,7 @@ "**/cpy/globby": "^10.0.1", "**/d3-color": "^3.1.0", "**/elasticsearch/agentkeepalive": "^4.5.0", + "**/eslint/cross-spawn": "^7.0.5", "**/es5-ext": "^0.10.63", "**/fetch-mock/path-to-regexp": "^3.3.0", "**/follow-redirects": "^1.15.4", diff --git a/yarn.lock b/yarn.lock index cc9f4490818d..5b3dec208a45 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6295,21 +6295,10 @@ create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7: safe-buffer "^5.0.1" sha.js "^2.4.8" -cross-spawn@^6.0.5: - version "6.0.5" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" - integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== - dependencies: - nice-try "^1.0.4" - path-key "^2.0.1" - semver "^5.5.0" - shebang-command "^1.2.0" - which "^1.2.9" - -cross-spawn@^7.0.0, cross-spawn@^7.0.3: - version "7.0.3" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" - integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== +cross-spawn@^6.0.5, cross-spawn@^7.0.0, cross-spawn@^7.0.3, cross-spawn@^7.0.5: + version "7.0.5" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.5.tgz#910aac880ff5243da96b728bc6521a5f6c2f2f82" + integrity sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug== dependencies: path-key "^3.1.0" shebang-command "^2.0.0" @@ -12753,11 +12742,6 @@ next-tick@1, next-tick@^1.1.0: resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.1.0.tgz#1836ee30ad56d67ef281b22bd199f709449b35eb" integrity sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ== -nice-try@^1.0.4: - version "1.0.5" - resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" - integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== - nise@^1.5.2: version "1.5.3" resolved "https://registry.yarnpkg.com/nise/-/nise-1.5.3.tgz#9d2cfe37d44f57317766c6e9408a359c5d3ac1f7" @@ -13528,11 +13512,6 @@ path-is-inside@^1.0.2: resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" integrity sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM= -path-key@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" - integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= - path-key@^3.0.0, path-key@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" @@ -15376,7 +15355,7 @@ selenium-webdriver@^4.0.0-alpha.7: rimraf "^2.7.1" tmp "0.0.30" -"semver@2 || 3 || 4 || 5", semver@7.3.2, semver@^5.3.0, semver@^5.5.0, semver@^5.6.0, semver@^5.7.0, semver@^5.7.1, semver@^5.7.2, semver@^6.0.0, semver@^6.1.0, semver@^6.1.2, semver@^6.3.0, semver@^6.3.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.8, semver@^7.5.3, semver@^7.5.4, semver@~7.3.0: +"semver@2 || 3 || 4 || 5", semver@7.3.2, semver@^5.3.0, semver@^5.6.0, semver@^5.7.0, semver@^5.7.1, semver@^5.7.2, semver@^6.0.0, semver@^6.1.0, semver@^6.1.2, semver@^6.3.0, semver@^6.3.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.8, semver@^7.5.3, semver@^7.5.4, semver@~7.3.0: version "7.5.3" resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.3.tgz#161ce8c2c6b4b3bdca6caadc9fa3317a4c4fe88e" integrity sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ== @@ -15481,13 +15460,6 @@ shallowequal@^1.1.0: resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8" integrity sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ== -shebang-command@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" - integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo= - dependencies: - shebang-regex "^1.0.0" - shebang-command@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" @@ -15495,11 +15467,6 @@ shebang-command@^2.0.0: dependencies: shebang-regex "^3.0.0" -shebang-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" - integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= - shebang-regex@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" @@ -18259,7 +18226,7 @@ which-typed-array@^1.1.11, which-typed-array@^1.1.13, which-typed-array@^1.1.2: gopd "^1.0.1" has-tostringtag "^1.0.0" -which@^1.2.14, which@^1.2.9, which@^1.3.1: +which@^1.2.14, which@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== From 7a6c7274fb8072c8a66e50e099086b28e2dddfc1 Mon Sep 17 00:00:00 2001 From: AWSHurneyt Date: Mon, 18 Nov 2024 14:01:19 -0800 Subject: [PATCH 30/80] Adjusted source of QueryStringManager functions for flyout. (#8864) * Change origin of query string management functions. Signed-off-by: AWSHurneyt * Added try/catch block. Signed-off-by: AWSHurneyt * Changed source of query string management functions. Signed-off-by: AWSHurneyt * Fixed typo. Signed-off-by: AWSHurneyt * Fixed import. Signed-off-by: AWSHurneyt * Fix lint errors. Signed-off-by: AWSHurneyt * Fixed lint error. Signed-off-by: AWSHurneyt * Fixed test mocks. Signed-off-by: AWSHurneyt --------- Signed-off-by: AWSHurneyt Signed-off-by: Federico Silva --- .../public/ui/filter_bar/filter_options.tsx | 4 +- .../open_saved_query_flyout.test.tsx | 15 +++--- .../open_saved_query_flyout.tsx | 54 ++++++++++--------- .../saved_query_management_component.tsx | 5 +- .../ui/search_bar/create_search_bar.tsx | 1 - .../data/public/ui/search_bar/search_bar.tsx | 10 +--- 6 files changed, 42 insertions(+), 47 deletions(-) diff --git a/src/plugins/data/public/ui/filter_bar/filter_options.tsx b/src/plugins/data/public/ui/filter_bar/filter_options.tsx index 4af53fa28df1..3cda39731fa7 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_options.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_options.tsx @@ -59,7 +59,7 @@ import { import { FilterEditor } from './filter_editor'; import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; import { SavedQueryManagementComponent } from '../saved_query_management'; -import { QueryStringManager, SavedQuery, SavedQueryService } from '../../query'; +import { SavedQuery, SavedQueryService } from '../../query'; import { SavedQueryMeta } from '../saved_query_form'; import { getUseNewSavedQueriesUI } from '../../services'; @@ -79,7 +79,6 @@ interface Props { useSaveQueryMenu: boolean; isQueryEditorControl: boolean; saveQuery: (savedQueryMeta: SavedQueryMeta, saveAsNew?: boolean) => Promise; - queryStringManager: QueryStringManager; } const maxFilterWidth = 600; @@ -311,7 +310,6 @@ const FilterOptionsUI = (props: Props) => { key={'savedQueryManagement'} useNewSavedQueryUI={getUseNewSavedQueriesUI()} saveQuery={props.saveQuery} - queryStringManager={props.queryStringManager} />, ]} data-test-subj="save-query-panel" diff --git a/src/plugins/data/public/ui/saved_query_flyouts/open_saved_query_flyout.test.tsx b/src/plugins/data/public/ui/saved_query_flyouts/open_saved_query_flyout.test.tsx index 8daaafe0fdcb..f004f6e7e5af 100644 --- a/src/plugins/data/public/ui/saved_query_flyouts/open_saved_query_flyout.test.tsx +++ b/src/plugins/data/public/ui/saved_query_flyouts/open_saved_query_flyout.test.tsx @@ -4,13 +4,14 @@ */ import React from 'react'; -import { render, screen, fireEvent, waitFor, act } from '@testing-library/react'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import { OpenSavedQueryFlyout } from './open_saved_query_flyout'; import { createSavedQueryService } from '../../../public/query/saved_query/saved_query_service'; import { applicationServiceMock, uiSettingsServiceMock } from '../../../../../core/public/mocks'; import { SavedQueryAttributes } from '../../../public/query/saved_query/types'; import '@testing-library/jest-dom'; import { queryStringManagerMock } from '../../../../data/public/query/query_string/query_string_manager.mock'; +import { getQueryService } from '../../services'; const savedQueryAttributesWithTemplate: SavedQueryAttributes = { title: 'foo', @@ -63,6 +64,10 @@ jest.mock('@osd/i18n', () => ({ }, })); +jest.mock('../../services', () => ({ + getQueryService: jest.fn(), +})); + const mockSavedQueryService = createSavedQueryService( // @ts-ignore mockSavedObjectsClient, @@ -100,6 +105,9 @@ jest.spyOn(mockSavedQueryService, 'getAllSavedQueries').mockResolvedValue(savedQ describe('OpenSavedQueryFlyout', () => { beforeEach(() => { jest.clearAllMocks(); + (getQueryService as jest.Mock).mockReturnValue({ + queryString: queryStringManagerMock.createSetupContract(), + }); }); it('should render the flyout with correct tabs and content', async () => { @@ -109,7 +117,6 @@ describe('OpenSavedQueryFlyout', () => { onClose={mockOnClose} onQueryOpen={mockOnQueryOpen} handleQueryDelete={mockHandleQueryDelete} - queryStringManager={queryStringManagerMock.createSetupContract()} /> ); @@ -141,7 +148,6 @@ describe('OpenSavedQueryFlyout', () => { onClose={mockOnClose} onQueryOpen={mockOnQueryOpen} handleQueryDelete={mockHandleQueryDelete} - queryStringManager={queryStringManagerMock.createSetupContract()} /> ); @@ -162,7 +168,6 @@ describe('OpenSavedQueryFlyout', () => { onClose={mockOnClose} onQueryOpen={mockOnQueryOpen} handleQueryDelete={mockHandleQueryDelete} - queryStringManager={queryStringManagerMock.createSetupContract()} /> ); @@ -181,7 +186,6 @@ describe('OpenSavedQueryFlyout', () => { onClose={mockOnClose} onQueryOpen={mockOnQueryOpen} handleQueryDelete={mockHandleQueryDelete} - queryStringManager={queryStringManagerMock.createSetupContract()} /> ); @@ -214,7 +218,6 @@ describe('OpenSavedQueryFlyout', () => { onClose={mockOnClose} onQueryOpen={mockOnQueryOpen} handleQueryDelete={mockHandleQueryDelete} - queryStringManager={queryStringManagerMock.createSetupContract()} /> ); diff --git a/src/plugins/data/public/ui/saved_query_flyouts/open_saved_query_flyout.tsx b/src/plugins/data/public/ui/saved_query_flyouts/open_saved_query_flyout.tsx index 41aa344bbaef..212e0228e626 100644 --- a/src/plugins/data/public/ui/saved_query_flyouts/open_saved_query_flyout.tsx +++ b/src/plugins/data/public/ui/saved_query_flyouts/open_saved_query_flyout.tsx @@ -24,16 +24,16 @@ import { } from '@elastic/eui'; import React, { useCallback, useEffect, useRef, useState } from 'react'; import { i18n } from '@osd/i18n'; -import { QueryStringManager, SavedQuery, SavedQueryService } from '../../query'; +import { SavedQuery, SavedQueryService } from '../../query'; import { SavedQueryCard } from './saved_query_card'; import { Query } from '../../../common'; +import { getQueryService } from '../../services'; export interface OpenSavedQueryFlyoutProps { savedQueryService: SavedQueryService; onClose: () => void; onQueryOpen: (query: SavedQuery) => void; handleQueryDelete: (query: SavedQuery) => Promise; - queryStringManager: QueryStringManager; } interface SavedQuerySearchableItem { @@ -50,7 +50,6 @@ export function OpenSavedQueryFlyout({ onClose, onQueryOpen, handleQueryDelete, - queryStringManager, }: OpenSavedQueryFlyoutProps) { const [selectedTabId, setSelectedTabId] = useState('mutable-saved-queries'); const [savedQueries, setSavedQueries] = useState([]); @@ -65,36 +64,43 @@ export function OpenSavedQueryFlyout({ const [searchQuery, setSearchQuery] = useState(EuiSearchBar.Query.MATCH_ALL); const [isLoading, setIsLoading] = useState(false); const currentTabIdRef = useRef(selectedTabId); + const queryStringManager = getQueryService().queryString; const fetchAllSavedQueriesForSelectedTab = useCallback(async () => { setIsLoading(true); - const query = queryStringManager.getQuery(); - let templateQueries: any[] = []; + try { + const query = queryStringManager.getQuery(); + let templateQueries: any[] = []; - // fetch sample query based on dataset type - if (query?.dataset?.type) { - templateQueries = - (await queryStringManager - .getDatasetService() - ?.getType(query.dataset.type) - ?.getSampleQueries?.()) || []; + // fetch sample query based on dataset type + if (query?.dataset?.type) { + templateQueries = + (await queryStringManager + .getDatasetService() + ?.getType(query.dataset.type) + ?.getSampleQueries?.()) || []; - // Check if any sample query has isTemplate set to true - const hasTemplates = templateQueries.some((q) => q?.attributes?.isTemplate); - setHasTemplateQueries(hasTemplates); - } + // Check if any sample query has isTemplate set to true + const hasTemplates = templateQueries.some((q) => q?.attributes?.isTemplate); + setHasTemplateQueries(hasTemplates); + } - // Set queries based on the current tab - if (currentTabIdRef.current === 'mutable-saved-queries') { - const allQueries = await savedQueryService.getAllSavedQueries(); - const mutableSavedQueries = allQueries.filter((q) => !q.attributes.isTemplate); + // Set queries based on the current tab if (currentTabIdRef.current === 'mutable-saved-queries') { - setSavedQueries(mutableSavedQueries); + const allQueries = await savedQueryService.getAllSavedQueries(); + const mutableSavedQueries = allQueries.filter((q) => !q.attributes.isTemplate); + if (currentTabIdRef.current === 'mutable-saved-queries') { + setSavedQueries(mutableSavedQueries); + } + } else if (currentTabIdRef.current === 'template-saved-queries') { + setSavedQueries(templateQueries); } - } else if (currentTabIdRef.current === 'template-saved-queries') { - setSavedQueries(templateQueries); + } catch (e) { + // eslint-disable-next-line no-console + console.error('Error occurred while retrieving saved queries.', e); + } finally { + setIsLoading(false); } - setIsLoading(false); }, [savedQueryService, currentTabIdRef, setSavedQueries, queryStringManager]); const updatePageIndex = useCallback((index: number) => { diff --git a/src/plugins/data/public/ui/saved_query_management/saved_query_management_component.tsx b/src/plugins/data/public/ui/saved_query_management/saved_query_management_component.tsx index 44c5ef384966..01f9b97e978f 100644 --- a/src/plugins/data/public/ui/saved_query_management/saved_query_management_component.tsx +++ b/src/plugins/data/public/ui/saved_query_management/saved_query_management_component.tsx @@ -45,7 +45,7 @@ import { import { i18n } from '@osd/i18n'; import React, { useCallback, useEffect, useState, Fragment, useRef } from 'react'; import { sortBy } from 'lodash'; -import { QueryStringManager, SavedQuery, SavedQueryService } from '../..'; +import { SavedQuery, SavedQueryService } from '../..'; import { SavedQueryListItem } from './saved_query_list_item'; import { toMountPoint, @@ -70,7 +70,6 @@ interface Props { onClearSavedQuery: () => void; closeMenuPopover: () => void; saveQuery: (savedQueryMeta: SavedQueryMeta, saveAsNew?: boolean) => Promise; - queryStringManager: QueryStringManager; } export function SavedQueryManagementComponent({ @@ -84,7 +83,6 @@ export function SavedQueryManagementComponent({ closeMenuPopover, useNewSavedQueryUI, saveQuery, - queryStringManager, }: Props) { const [savedQueries, setSavedQueries] = useState([] as SavedQuery[]); const [count, setTotalCount] = useState(0); @@ -258,7 +256,6 @@ export function SavedQueryManagementComponent({ onClose={() => openSavedQueryFlyout?.close().then()} onQueryOpen={onLoad} handleQueryDelete={handleDelete} - queryStringManager={queryStringManager} /> ) ); diff --git a/src/plugins/data/public/ui/search_bar/create_search_bar.tsx b/src/plugins/data/public/ui/search_bar/create_search_bar.tsx index d3f89d0f559d..f8b9694caabc 100644 --- a/src/plugins/data/public/ui/search_bar/create_search_bar.tsx +++ b/src/plugins/data/public/ui/search_bar/create_search_bar.tsx @@ -202,7 +202,6 @@ export function createSearchBar({ core, storage, data }: StatefulSearchBarDeps) isRefreshPaused={refreshInterval.pause} filters={filters} query={query} - queryStringManager={data.query.queryString} onFiltersUpdated={defaultFiltersUpdated(data.query)} onRefreshChange={defaultOnRefreshChange(data.query)} savedQuery={savedQuery} diff --git a/src/plugins/data/public/ui/search_bar/search_bar.tsx b/src/plugins/data/public/ui/search_bar/search_bar.tsx index 3cd6cdcca25e..1f1b20b8c952 100644 --- a/src/plugins/data/public/ui/search_bar/search_bar.tsx +++ b/src/plugins/data/public/ui/search_bar/search_bar.tsx @@ -38,13 +38,7 @@ import { withOpenSearchDashboards, } from '../../../../opensearch_dashboards_react/public'; import { Filter, IIndexPattern, Query, TimeRange, UI_SETTINGS } from '../../../common'; -import { - SavedQuery, - SavedQueryAttributes, - TimeHistoryContract, - QueryStatus, - QueryStringManager, -} from '../../query'; +import { SavedQuery, SavedQueryAttributes, TimeHistoryContract, QueryStatus } from '../../query'; import { IDataPluginServices } from '../../types'; import { FilterBar } from '../filter_bar/filter_bar'; import { QueryEditorTopRow } from '../query_editor'; @@ -101,7 +95,6 @@ export interface SearchBarOwnProps { onRefresh?: (payload: { dateRange: TimeRange }) => void; indicateNoData?: boolean; queryStatus?: QueryStatus; - queryStringManager: QueryStringManager; } export type SearchBarProps = SearchBarOwnProps & SearchBarInjectedDeps; @@ -474,7 +467,6 @@ class SearchBarUI extends Component { useSaveQueryMenu={useSaveQueryMenu} isQueryEditorControl={isQueryEditorControl} saveQuery={this.onSave} - queryStringManager={this.props.queryStringManager} /> ) ); From 8a202f03b7096c1fb5f06648f04548d7fbf8b9c3 Mon Sep 17 00:00:00 2001 From: Hailong Cui Date: Tue, 19 Nov 2024 10:18:15 +0800 Subject: [PATCH 31/80] [chore]upgrade actions/upload-artifact to v4 (#8855) * upgrade actioins/upload-artifact to v4 Signed-off-by: Hailong Cui * Changeset file for PR #8855 created/updated --------- Signed-off-by: Hailong Cui Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> Signed-off-by: Federico Silva --- .github/workflows/build_and_test_workflow.yml | 8 +++--- .github/workflows/cypress_workflow.yml | 26 +++++++++---------- .../workflows/release_cypress_workflow.yml | 16 ++++++------ changelogs/fragments/8855.yml | 2 ++ 4 files changed, 27 insertions(+), 25 deletions(-) create mode 100644 changelogs/fragments/8855.yml diff --git a/.github/workflows/build_and_test_workflow.yml b/.github/workflows/build_and_test_workflow.yml index 49cdbe165961..40c335dcca9c 100644 --- a/.github/workflows/build_and_test_workflow.yml +++ b/.github/workflows/build_and_test_workflow.yml @@ -282,7 +282,7 @@ jobs: JOB: ci${{ matrix.group }} CACHE_DIR: ciGroup${{ matrix.group }} - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 if: failure() with: name: failure-artifacts-ci${{ matrix.group }} @@ -393,7 +393,7 @@ jobs: id: plugin-ftr-tests run: node scripts/functional_tests.js --config test/plugin_functional/config.ts - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 if: failure() with: name: failure-artifacts-plugin-functional-${{ matrix.os }} @@ -506,7 +506,7 @@ jobs: - name: Build `${{ matrix.name }}` run: yarn ${{ matrix.script }} --release - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 if: success() with: name: ${{ matrix.suffix }}-${{ env.VERSION }} @@ -595,7 +595,7 @@ jobs: run: | ./bwctest.sh -s false -o ${{ env.OPENSEARCH_URL }} -d ${{ steps.download.outputs.download-path }}/opensearch-dashboards-${{ env.VERSION }}-linux-x64.tar.gz - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 if: ${{ failure() && steps.verify-opensearch-exists.outputs.version-exists == 'true' }} with: name: ${{ matrix.version }}-test-failures diff --git a/.github/workflows/cypress_workflow.yml b/.github/workflows/cypress_workflow.yml index 3d3b0b79b027..c15edeac5e35 100644 --- a/.github/workflows/cypress_workflow.yml +++ b/.github/workflows/cypress_workflow.yml @@ -265,50 +265,50 @@ jobs: # Screenshots are only captured on failure, will change this once we do visual regression tests - name: Upload FT repo screenshots - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: failure() && matrix.test_location == 'ftr' with: - name: ftr-cypress-screenshots + name: ftr-cypress-screenshots-${{ matrix.group }} path: ${{ env.FTR_PATH }}/cypress/screenshots retention-days: 1 - name: Upload FT repo videos - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: always() && matrix.test_location == 'ftr' with: - name: ftr-cypress-videos + name: ftr-cypress-videos-${{ matrix.group }} path: ${{ env.FTR_PATH }}/cypress/videos retention-days: 1 - name: Upload FT repo results - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: always() && matrix.test_location == 'ftr' with: - name: ftr-cypress-results + name: ftr-cypress-results-${{ matrix.group }} path: ${{ env.FTR_PATH }}/cypress/results retention-days: 1 - name: Upload Dashboards screenshots if: failure() && matrix.test_location == 'source' - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: dashboards-cypress-screenshots + name: dashboards-cypress-screenshots-${{ matrix.group }} path: cypress/screenshots retention-days: 1 - name: Upload Dashboards repo videos - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: always() && matrix.test_location == 'source' with: - name: dashboards-cypress-videos + name: dashboards-cypress-videos-${{ matrix.group }} path: cypress/videos retention-days: 1 - name: Upload Dashboards repo results - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: always() && matrix.test_location == 'source' with: - name: dashboards-cypress-results + name: dashboards-cypress-results-${{ matrix.group }} path: cypress/results retention-days: 1 @@ -346,6 +346,6 @@ jobs: '${{ env.SPEC }}' ``` - #### Link to results: + #### Link to results: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} edit-mode: replace diff --git a/.github/workflows/release_cypress_workflow.yml b/.github/workflows/release_cypress_workflow.yml index f58757b23f9f..bb9895d9f048 100644 --- a/.github/workflows/release_cypress_workflow.yml +++ b/.github/workflows/release_cypress_workflow.yml @@ -72,7 +72,7 @@ jobs: CI: 1 # avoid warnings like "tput: No value for $TERM and no -T specified" TERM: xterm - name: Run cypress tests (osd:ciGroup${{ matrix.spec_group }}) ${{ inputs.UNIQUE_ID}} + name: Run cypress tests (osd:ciGroup${{ matrix.spec_group }}) ${{ inputs.UNIQUE_ID}} steps: - name: Checkout code uses: actions/checkout@v2 @@ -130,7 +130,7 @@ jobs: mkdir -p $CWD/${{ env.OPENSEARCH_DIR }} source ${{ env.OSD_PATH }}/scripts/common/utils.sh open_artifact $CWD/${{ env.OPENSEARCH_DIR }} ${{ env.OPENSEARCH }} - + - name: Download and extract OpenSearch Dashboards artifacts run: | CWD=$(pwd) @@ -138,22 +138,22 @@ jobs: source ${{ env.OSD_PATH }}/scripts/common/utils.sh open_artifact $CWD/${{ env.DASHBOARDS_DIR }} ${{ env.DASHBOARDS }} - - name: Run Cypress tests + - name: Run Cypress tests run: | chown -R 1000:1000 `pwd` su `id -un 1000` -c "source ${{ env.OSD_PATH }}/scripts/cypress_tests.sh && run_dashboards_cypress_tests" # Screenshots are only captured on failures - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 if: failure() with: - name: release-osd-cypress-screenshots + name: release-osd-cypress-screenshots-${{ matrix.spec_group }} path: ${{ env.OSD_PATH }}/cypress/screenshots retention-days: 1 - - - uses: actions/upload-artifact@v3 + + - uses: actions/upload-artifact@v4 if: always() with: - name: release-osd-cypress-videos + name: release-osd-cypress-videos-${{ matrix.spec_group }} path: ${{ env.OSD_PATH }}/cypress/videos retention-days: 1 diff --git a/changelogs/fragments/8855.yml b/changelogs/fragments/8855.yml new file mode 100644 index 000000000000..ad9835ebe292 --- /dev/null +++ b/changelogs/fragments/8855.yml @@ -0,0 +1,2 @@ +fix: +- Upgrade actions/upload-artifact to v4 ([#8855](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8855)) \ No newline at end of file From 8da61f089d71d5aa53ae6bf97367d603f746c341 Mon Sep 17 00:00:00 2001 From: Amardeepsingh Siglani Date: Tue, 19 Nov 2024 12:22:35 -0800 Subject: [PATCH 32/80] Use currently selected data source when no source attached to saved query (#8883) Opening a saved query that has no dataset stored with it, resets the currently selected dataset in the picker which breaks the query experience since the user will need to reselect the dataset which will then reset the query. * use currently selected data source when no source attached to saved query Signed-off-by: Amardeepsingh Siglani * Changeset file for PR #8883 created/updated * refactored fix Signed-off-by: Amardeepsingh Siglani * revert license change Signed-off-by: Amardeepsingh Siglani --------- Signed-off-by: Amardeepsingh Siglani Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> Signed-off-by: Federico Silva --- changelogs/fragments/8883.yml | 2 ++ .../open_saved_query_flyout.tsx | 24 +++++++-------- .../saved_query_flyouts/save_query_flyout.tsx | 1 - .../public/ui/saved_query_form/helpers.tsx | 30 +------------------ .../ui/saved_query_form/save_query_form.tsx | 4 --- .../populate_state_from_saved_query.test.ts | 12 ++++++-- .../lib/populate_state_from_saved_query.ts | 6 +++- .../data/public/ui/search_bar/search_bar.tsx | 6 +--- 8 files changed, 29 insertions(+), 56 deletions(-) create mode 100644 changelogs/fragments/8883.yml diff --git a/changelogs/fragments/8883.yml b/changelogs/fragments/8883.yml new file mode 100644 index 000000000000..d9254d81c3cf --- /dev/null +++ b/changelogs/fragments/8883.yml @@ -0,0 +1,2 @@ +fix: +- Retain currently selected dataset when opening saved query without dataset info ([#8883](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8883)) \ No newline at end of file diff --git a/src/plugins/data/public/ui/saved_query_flyouts/open_saved_query_flyout.tsx b/src/plugins/data/public/ui/saved_query_flyouts/open_saved_query_flyout.tsx index 212e0228e626..099f3e1f0420 100644 --- a/src/plugins/data/public/ui/saved_query_flyouts/open_saved_query_flyout.tsx +++ b/src/plugins/data/public/ui/saved_query_flyouts/open_saved_query_flyout.tsx @@ -26,7 +26,6 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; import { i18n } from '@osd/i18n'; import { SavedQuery, SavedQueryService } from '../../query'; import { SavedQueryCard } from './saved_query_card'; -import { Query } from '../../../common'; import { getQueryService } from '../../services'; export interface OpenSavedQueryFlyoutProps { @@ -306,19 +305,16 @@ export function OpenSavedQueryFlyout({ fill onClick={() => { if (selectedQuery) { - if ( - // Template queries are not associated with data sources. Apply data source from current query - selectedQuery.attributes.isTemplate - ) { - const updatedQuery: Query = { - ...queryStringManager?.getQuery(), - query: selectedQuery.attributes.query.query, - language: selectedQuery.attributes.query.language, - }; - queryStringManager.setQuery(updatedQuery); - } else { - onQueryOpen(selectedQuery); - } + onQueryOpen({ + ...selectedQuery, + attributes: { + ...selectedQuery.attributes, + query: { + ...selectedQuery.attributes.query, + dataset: queryStringManager.getQuery().dataset, + }, + }, + }); onClose(); } }} diff --git a/src/plugins/data/public/ui/saved_query_flyouts/save_query_flyout.tsx b/src/plugins/data/public/ui/saved_query_flyouts/save_query_flyout.tsx index c0356b864485..f62a60f7e9c7 100644 --- a/src/plugins/data/public/ui/saved_query_flyouts/save_query_flyout.tsx +++ b/src/plugins/data/public/ui/saved_query_flyouts/save_query_flyout.tsx @@ -43,7 +43,6 @@ export function SaveQueryFlyout({ savedQueryService={savedQueryService} showFilterOption={showFilterOption} showTimeFilterOption={showTimeFilterOption} - showDataSourceOption={true} setSaveAsNew={(shouldSaveAsNew) => setSaveAsNew(shouldSaveAsNew)} savedQuery={saveAsNew ? undefined : savedQuery} saveAsNew={saveAsNew} diff --git a/src/plugins/data/public/ui/saved_query_form/helpers.tsx b/src/plugins/data/public/ui/saved_query_form/helpers.tsx index 467eac2de475..ad3de3acde3f 100644 --- a/src/plugins/data/public/ui/saved_query_form/helpers.tsx +++ b/src/plugins/data/public/ui/saved_query_form/helpers.tsx @@ -57,7 +57,6 @@ interface Props { formUiType: 'Modal' | 'Flyout'; showFilterOption?: boolean; showTimeFilterOption?: boolean; - showDataSourceOption?: boolean; saveAsNew?: boolean; setSaveAsNew?: (shouldSaveAsNew: boolean) => void; cannotBeOverwritten?: boolean; @@ -70,7 +69,6 @@ export function useSaveQueryFormContent({ onClose, showFilterOption = true, showTimeFilterOption = true, - showDataSourceOption = false, formUiType, saveAsNew, setSaveAsNew, @@ -81,7 +79,6 @@ export function useSaveQueryFormContent({ const [description, setDescription] = useState(''); const [savedQueries, setSavedQueries] = useState([]); const [shouldIncludeFilters, setShouldIncludeFilters] = useState(true); - const [shouldIncludeDataSource, setShouldIncludeDataSource] = useState(true); // Defaults to false because saved queries are meant to be as portable as possible and loading // a saved query with a time filter will override whatever the current value of the global timepicker // is. We expect this option to be used rarely and only when the user knows they want this behavior. @@ -96,7 +93,6 @@ export function useSaveQueryFormContent({ setDescription(savedQuery?.description || ''); setShouldIncludeFilters(savedQuery ? !!savedQuery.filters : true); setIncludeTimefilter(!!savedQuery?.timefilter); - setShouldIncludeDataSource(savedQuery ? !!savedQuery.query.dataset : true); setFormErrors([]); }, [savedQuery]); @@ -147,18 +143,9 @@ export function useSaveQueryFormContent({ description, shouldIncludeFilters, shouldIncludeTimeFilter, - shouldIncludeDataSource, }); } - }, [ - validate, - onSave, - title, - description, - shouldIncludeFilters, - shouldIncludeTimeFilter, - shouldIncludeDataSource, - ]); + }, [validate, onSave, title, description, shouldIncludeFilters, shouldIncludeTimeFilter]); const onInputChange = useCallback((event) => { setEnabledSaveButton(Boolean(event.target.value)); @@ -229,21 +216,6 @@ export function useSaveQueryFormContent({ data-test-subj="saveQueryFormDescription" /> - {showDataSourceOption && ( - - { - setShouldIncludeDataSource(!shouldIncludeDataSource); - }} - data-test-subj="saveQueryFormIncludeDataSourceOption" - /> - - )} {showFilterOption && ( void; showFilterOption?: boolean; showTimeFilterOption?: boolean; - showDataSourceOption?: boolean; saveAsNew?: boolean; cannotBeOverwritten?: boolean; } @@ -63,7 +62,6 @@ export interface SavedQueryMeta { description: string; shouldIncludeFilters: boolean; shouldIncludeTimeFilter: boolean; - shouldIncludeDataSource: boolean; } export function SaveQueryForm({ @@ -74,7 +72,6 @@ export function SaveQueryForm({ onClose, showFilterOption = true, showTimeFilterOption = true, - showDataSourceOption = false, saveAsNew, setSaveAsNew, cannotBeOverwritten, @@ -87,7 +84,6 @@ export function SaveQueryForm({ onClose, showFilterOption, showTimeFilterOption, - showDataSourceOption, saveAsNew, setSaveAsNew, cannotBeOverwritten, diff --git a/src/plugins/data/public/ui/search_bar/lib/populate_state_from_saved_query.test.ts b/src/plugins/data/public/ui/search_bar/lib/populate_state_from_saved_query.test.ts index 52c3f981296b..b172d8c42a76 100644 --- a/src/plugins/data/public/ui/search_bar/lib/populate_state_from_saved_query.test.ts +++ b/src/plugins/data/public/ui/search_bar/lib/populate_state_from_saved_query.test.ts @@ -47,6 +47,11 @@ describe('populateStateFromSavedQuery', () => { query: { query: 'test', language: 'kuery', + dataset: { + id: 'saved-query-dataset', + title: 'saved-query-dataset', + type: 'INDEX', + }, }, }, }; @@ -57,12 +62,15 @@ describe('populateStateFromSavedQuery', () => { dataMock.query.filterManager.getGlobalFilters = jest.fn().mockReturnValue([]); }); - it('should set query', async () => { + it('should set query with current dataset', async () => { const savedQuery: SavedQuery = { ...baseSavedQuery, }; populateStateFromSavedQuery(dataMock.query, savedQuery); - expect(dataMock.query.queryString.setQuery).toHaveBeenCalled(); + expect(dataMock.query.queryString.setQuery).toHaveBeenCalledWith({ + ...savedQuery.attributes.query, + dataset: dataMock.query.queryString.getQuery().dataset, + }); }); it('should set filters', async () => { diff --git a/src/plugins/data/public/ui/search_bar/lib/populate_state_from_saved_query.ts b/src/plugins/data/public/ui/search_bar/lib/populate_state_from_saved_query.ts index 382fd382ac01..abab61dfe82e 100644 --- a/src/plugins/data/public/ui/search_bar/lib/populate_state_from_saved_query.ts +++ b/src/plugins/data/public/ui/search_bar/lib/populate_state_from_saved_query.ts @@ -48,7 +48,11 @@ export const populateStateFromSavedQuery = (queryService: QueryStart, savedQuery } // query string - queryString.setQuery(savedQuery.attributes.query); + queryString.setQuery({ + ...savedQuery.attributes.query, + // We should keep the currently selected dataset intact + dataset: queryString.getQuery().dataset, + }); // filters const savedQueryFilters = savedQuery.attributes.filters || []; diff --git a/src/plugins/data/public/ui/search_bar/search_bar.tsx b/src/plugins/data/public/ui/search_bar/search_bar.tsx index 1f1b20b8c952..251a0dc86fa0 100644 --- a/src/plugins/data/public/ui/search_bar/search_bar.tsx +++ b/src/plugins/data/public/ui/search_bar/search_bar.tsx @@ -45,7 +45,6 @@ import { QueryEditorTopRow } from '../query_editor'; import QueryBarTopRow from '../query_string_input/query_bar_top_row'; import { SavedQueryMeta, SaveQueryForm } from '../saved_query_form'; import { FilterOptions } from '../filter_bar/filter_options'; -import { getUseNewSavedQueriesUI } from '../../services'; interface SearchBarInjectedDeps { opensearchDashboards: OpenSearchDashboardsReactContextValue; @@ -285,11 +284,8 @@ class SearchBarUI extends Component { public onSave = async (savedQueryMeta: SavedQueryMeta, saveAsNew = false) => { if (!this.state.query) return; - const query = cloneDeep(this.state.query); - if (getUseNewSavedQueriesUI() && !savedQueryMeta.shouldIncludeDataSource) { - delete query.dataset; - } + delete query.dataset; const savedQueryAttributes: SavedQueryAttributes = { title: savedQueryMeta.title, From d1996d3e7e655baddd0ee39aaa0d8674ee7e4361 Mon Sep 17 00:00:00 2001 From: Amardeepsingh Siglani Date: Tue, 19 Nov 2024 18:53:41 -0800 Subject: [PATCH 33/80] Added framework to get default query string using dataset and language combination (#8896) * added framework to get default query using dataset Signed-off-by: Amardeepsingh Siglani * Changeset file for PR #8896 created/updated * deduped code Signed-off-by: Amardeepsingh Siglani * added UTs; minor refactor Signed-off-by: Amardeepsingh Siglani --------- Signed-off-by: Amardeepsingh Siglani Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> Signed-off-by: Federico Silva --- changelogs/fragments/8896.yml | 2 ++ .../query_string/dataset_service/types.ts | 5 +++ .../query_string/query_string_manager.test.ts | 32 +++++++++++++++++++ .../query_string/query_string_manager.ts | 30 +++++++++++------ 4 files changed, 59 insertions(+), 10 deletions(-) create mode 100644 changelogs/fragments/8896.yml diff --git a/changelogs/fragments/8896.yml b/changelogs/fragments/8896.yml new file mode 100644 index 000000000000..a1a03c05f257 --- /dev/null +++ b/changelogs/fragments/8896.yml @@ -0,0 +1,2 @@ +feat: +- Added framework to get default query string using dataset and language combination ([#8896](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8896)) \ No newline at end of file diff --git a/src/plugins/data/public/query/query_string/dataset_service/types.ts b/src/plugins/data/public/query/query_string/dataset_service/types.ts index 0b8bcd402d15..65c322acec6f 100644 --- a/src/plugins/data/public/query/query_string/dataset_service/types.ts +++ b/src/plugins/data/public/query/query_string/dataset_service/types.ts @@ -8,6 +8,7 @@ import { DatasetField, DatasetSearchOptions, DataStructure, + Query, SavedObject, } from '../../../../common'; import { IDataPluginServices } from '../../../types'; @@ -106,4 +107,8 @@ export interface DatasetTypeConfig { * Service used for indexedViews related operations */ indexedViewsService?: DatasetIndexedViewsService; + /** + * Returns the initial query that is added to the query editor when a dataset is selected. + */ + getInitialQueryString?: (query: Query) => string | void; } diff --git a/src/plugins/data/public/query/query_string/query_string_manager.test.ts b/src/plugins/data/public/query/query_string/query_string_manager.test.ts index 758d658864ab..4bce5d7159db 100644 --- a/src/plugins/data/public/query/query_string/query_string_manager.test.ts +++ b/src/plugins/data/public/query/query_string/query_string_manager.test.ts @@ -308,5 +308,37 @@ describe('QueryStringManager', () => { expect(result.dataset).toEqual(currentDataset); expect(result.query).toBeDefined(); }); + + test('getInitialQueryByLanguage returns the initial query from the dataset config if present', () => { + service.getDatasetService().getType = jest.fn().mockReturnValue({ + supportedLanguages: jest.fn(), + getInitialQueryString: jest.fn().mockImplementation(({ language }) => { + switch (language) { + case 'sql': + return 'default sql dataset query'; + case 'ppl': + return 'default ppl dataset query'; + } + }), + }); + + const sqlQuery = service.getInitialQueryByLanguage('sql'); + expect(sqlQuery).toHaveProperty('query', 'default sql dataset query'); + + const pplQuery = service.getInitialQueryByLanguage('ppl'); + expect(pplQuery).toHaveProperty('query', 'default ppl dataset query'); + }); + + test('getInitialQueryByLanguage returns the initial query from the language config if dataset does not provide one', () => { + service.getDatasetService().getType = jest.fn().mockReturnValue({ + supportedLanguages: jest.fn(), + }); + service.getLanguageService().getLanguage = jest.fn().mockReturnValue({ + getQueryString: jest.fn().mockReturnValue('default-language-service-query'), + }); + + const sqlQuery = service.getInitialQueryByLanguage('sql'); + expect(sqlQuery).toHaveProperty('query', 'default-language-service-query'); + }); }); }); diff --git a/src/plugins/data/public/query/query_string/query_string_manager.ts b/src/plugins/data/public/query/query_string/query_string_manager.ts index 33bfc7d5d10b..47b6d536db6f 100644 --- a/src/plugins/data/public/query/query_string/query_string_manager.ts +++ b/src/plugins/data/public/query/query_string/query_string_manager.ts @@ -63,6 +63,21 @@ export class QueryStringManager { return this.storage.get('userQueryString') || ''; } + private getInitialDatasetQueryString(query: Query) { + const { language, dataset } = query; + + const languageConfig = this.languageService.getLanguage(language); + let typeConfig; + + if (dataset) { + typeConfig = this.datasetService.getType(dataset.type); + } + + return ( + typeConfig?.getInitialQueryString?.(query) ?? (languageConfig?.getQueryString(query) || '') + ); + } + public getDefaultQuery(): Query { const defaultLanguageId = this.getDefaultLanguage(); const defaultQuery = this.getDefaultQueryString(); @@ -79,13 +94,11 @@ export class QueryStringManager { defaultDataset && this.languageService ) { - const language = this.languageService.getLanguage(defaultLanguageId); const newQuery = { ...query, dataset: defaultDataset }; - const newQueryString = language?.getQueryString(newQuery) || ''; return { ...newQuery, - query: newQueryString, + query: this.getInitialDatasetQueryString(newQuery), }; } @@ -244,13 +257,12 @@ export class QueryStringManager { // Both language and dataset provided - generate fresh query if (language && dataset) { - const languageService = this.languageService.getLanguage(language); const newQuery = { language, dataset, query: '', }; - newQuery.query = languageService?.getQueryString(newQuery) || ''; + newQuery.query = this.getInitialDatasetQueryString(newQuery); return newQuery; } @@ -274,12 +286,12 @@ export class QueryStringManager { */ public getInitialQueryByLanguage = (languageId: string) => { const curQuery = this.query$.getValue(); - const language = this.languageService.getLanguage(languageId); const newQuery = { ...curQuery, language: languageId, }; - const queryString = language?.getQueryString(newQuery) || ''; + + const queryString = this.getInitialDatasetQueryString(newQuery); this.languageService.setUserQueryString(queryString); return { @@ -296,17 +308,15 @@ export class QueryStringManager { const curQuery = this.query$.getValue(); // Use dataset's preferred language or fallback to current language const languageId = newDataset.language || curQuery.language; - const language = this.languageService.getLanguage(languageId); const newQuery = { ...curQuery, language: languageId, dataset: newDataset, }; - const queryString = language?.getQueryString(newQuery) || ''; return { ...newQuery, - query: queryString, + query: this.getInitialDatasetQueryString(newQuery, newDataset), }; }; From 60102f3d0904759c7c22dc03c4abb6e6d16cf4b6 Mon Sep 17 00:00:00 2001 From: Amardeepsingh Siglani Date: Tue, 19 Nov 2024 20:50:57 -0800 Subject: [PATCH 34/80] Only support copy for query templates (#8899) * only support copy for query templates Signed-off-by: Amardeepsingh Siglani * Changeset file for PR #8899 created/updated * clear selected query on tab change; keep button disabled when query is not selected Signed-off-by: Amardeepsingh Siglani --------- Signed-off-by: Amardeepsingh Siglani Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> Signed-off-by: Federico Silva --- changelogs/fragments/8899.yml | 2 + .../open_saved_query_flyout.tsx | 78 +++++++++++++------ .../saved_query_management_component.tsx | 3 +- 3 files changed, 58 insertions(+), 25 deletions(-) create mode 100644 changelogs/fragments/8899.yml diff --git a/changelogs/fragments/8899.yml b/changelogs/fragments/8899.yml new file mode 100644 index 000000000000..11030aecb552 --- /dev/null +++ b/changelogs/fragments/8899.yml @@ -0,0 +1,2 @@ +fix: +- Only support copy action for query templates ([#8899](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8899)) \ No newline at end of file diff --git a/src/plugins/data/public/ui/saved_query_flyouts/open_saved_query_flyout.tsx b/src/plugins/data/public/ui/saved_query_flyouts/open_saved_query_flyout.tsx index 099f3e1f0420..d9c2941adc8d 100644 --- a/src/plugins/data/public/ui/saved_query_flyouts/open_saved_query_flyout.tsx +++ b/src/plugins/data/public/ui/saved_query_flyouts/open_saved_query_flyout.tsx @@ -21,15 +21,18 @@ import { EuiTablePagination, EuiTitle, Pager, + copyToClipboard, } from '@elastic/eui'; import React, { useCallback, useEffect, useRef, useState } from 'react'; import { i18n } from '@osd/i18n'; +import { NotificationsStart } from 'opensearch-dashboards/public'; import { SavedQuery, SavedQueryService } from '../../query'; import { SavedQueryCard } from './saved_query_card'; import { getQueryService } from '../../services'; export interface OpenSavedQueryFlyoutProps { savedQueryService: SavedQueryService; + notifications?: NotificationsStart; onClose: () => void; onQueryOpen: (query: SavedQuery) => void; handleQueryDelete: (query: SavedQuery) => Promise; @@ -44,13 +47,21 @@ interface SavedQuerySearchableItem { savedQuery: SavedQuery; } +enum OPEN_QUERY_TAB_ID { + SAVED_QUERIES = 'saved-queries', + QUERY_TEMPLATES = 'query-templates', +} + export function OpenSavedQueryFlyout({ savedQueryService, + notifications, onClose, onQueryOpen, handleQueryDelete, }: OpenSavedQueryFlyoutProps) { - const [selectedTabId, setSelectedTabId] = useState('mutable-saved-queries'); + const [selectedTabId, setSelectedTabId] = useState( + OPEN_QUERY_TAB_ID.SAVED_QUERIES + ); const [savedQueries, setSavedQueries] = useState([]); const [hasTemplateQueries, setHasTemplateQueries] = useState(false); const [itemsPerPage, setItemsPerPage] = useState(10); @@ -85,13 +96,13 @@ export function OpenSavedQueryFlyout({ } // Set queries based on the current tab - if (currentTabIdRef.current === 'mutable-saved-queries') { + if (currentTabIdRef.current === OPEN_QUERY_TAB_ID.SAVED_QUERIES) { const allQueries = await savedQueryService.getAllSavedQueries(); const mutableSavedQueries = allQueries.filter((q) => !q.attributes.isTemplate); - if (currentTabIdRef.current === 'mutable-saved-queries') { + if (currentTabIdRef.current === OPEN_QUERY_TAB_ID.SAVED_QUERIES) { setSavedQueries(mutableSavedQueries); } - } else if (currentTabIdRef.current === 'template-saved-queries') { + } else if (currentTabIdRef.current === OPEN_QUERY_TAB_ID.QUERY_TEMPLATES) { setSavedQueries(templateQueries); } } catch (e) { @@ -111,6 +122,7 @@ export function OpenSavedQueryFlyout({ fetchAllSavedQueriesForSelectedTab(); setSearchQuery(EuiSearchBar.Query.MATCH_ALL); updatePageIndex(0); + setSelectedQuery(undefined); }, [selectedTabId, fetchAllSavedQueriesForSelectedTab, updatePageIndex]); useEffect(() => { @@ -261,7 +273,7 @@ export function OpenSavedQueryFlyout({ const tabs = [ { - id: 'mutable-saved-queries', + id: OPEN_QUERY_TAB_ID.SAVED_QUERIES, name: 'Saved queries', content: flyoutBodyContent, }, @@ -269,12 +281,43 @@ export function OpenSavedQueryFlyout({ if (hasTemplateQueries) { tabs.push({ - id: 'template-saved-queries', + id: OPEN_QUERY_TAB_ID.QUERY_TEMPLATES, name: 'Templates', content: flyoutBodyContent, }); } + const onQueryAction = useCallback(() => { + if (!selectedQuery) { + return; + } + + if (selectedQuery?.attributes.isTemplate) { + copyToClipboard(selectedQuery.attributes.query.query as string); + notifications?.toasts.addSuccess({ + title: i18n.translate('data.openSavedQueryFlyout.queryCopied.title', { + defaultMessage: 'Query copied', + }), + text: i18n.translate('data.openSavedQueryFlyout.queryCopied.text', { + defaultMessage: 'Paste the query in the editor to modify and run.', + }), + }); + } else { + onQueryOpen({ + ...selectedQuery, + attributes: { + ...selectedQuery.attributes, + query: { + ...selectedQuery.attributes.query, + dataset: queryStringManager.getQuery().dataset, + }, + }, + }); + } + + onClose(); + }, [onClose, onQueryOpen, notifications, selectedQuery, queryStringManager]); + return ( @@ -287,8 +330,8 @@ export function OpenSavedQueryFlyout({ tabs={tabs} initialSelectedTab={tabs[0]} onTabClick={(tab) => { - setSelectedTabId(tab.id); - currentTabIdRef.current = tab.id; + setSelectedTabId(tab.id as OPEN_QUERY_TAB_ID); + currentTabIdRef.current = tab.id as OPEN_QUERY_TAB_ID; }} /> @@ -303,23 +346,10 @@ export function OpenSavedQueryFlyout({ { - if (selectedQuery) { - onQueryOpen({ - ...selectedQuery, - attributes: { - ...selectedQuery.attributes, - query: { - ...selectedQuery.attributes.query, - dataset: queryStringManager.getQuery().dataset, - }, - }, - }); - onClose(); - } - }} + onClick={onQueryAction} + data-testid="open-query-action-button" > - Open query + {selectedTabId === OPEN_QUERY_TAB_ID.SAVED_QUERIES ? 'Open' : 'Copy'} query diff --git a/src/plugins/data/public/ui/saved_query_management/saved_query_management_component.tsx b/src/plugins/data/public/ui/saved_query_management/saved_query_management_component.tsx index 01f9b97e978f..94898bfe57a2 100644 --- a/src/plugins/data/public/ui/saved_query_management/saved_query_management_component.tsx +++ b/src/plugins/data/public/ui/saved_query_management/saved_query_management_component.tsx @@ -89,7 +89,7 @@ export function SavedQueryManagementComponent({ const [activePage, setActivePage] = useState(0); const cancelPendingListingRequest = useRef<() => void>(() => {}); const { - services: { overlays }, + services: { overlays, notifications }, } = useOpenSearchDashboards(); useEffect(() => { @@ -253,6 +253,7 @@ export function SavedQueryManagementComponent({ toMountPoint( openSavedQueryFlyout?.close().then()} onQueryOpen={onLoad} handleQueryDelete={handleDelete} From ba259e438341c284567e9a3abf08b015c652212a Mon Sep 17 00:00:00 2001 From: Amardeepsingh Siglani Date: Wed, 20 Nov 2024 14:15:42 -0800 Subject: [PATCH 35/80] removed extra param getInitialDatasetQueryString in query_string_manager (#8902) * removed extra param Signed-off-by: Amardeepsingh Siglani * Changeset file for PR #8902 created/updated --------- Signed-off-by: Amardeepsingh Siglani Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> Signed-off-by: Federico Silva --- changelogs/fragments/8902.yml | 2 ++ .../data/public/query/query_string/query_string_manager.ts | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 changelogs/fragments/8902.yml diff --git a/changelogs/fragments/8902.yml b/changelogs/fragments/8902.yml new file mode 100644 index 000000000000..d4658d0296a7 --- /dev/null +++ b/changelogs/fragments/8902.yml @@ -0,0 +1,2 @@ +fix: +- Removed extra parameter ([#8902](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8902)) \ No newline at end of file diff --git a/src/plugins/data/public/query/query_string/query_string_manager.ts b/src/plugins/data/public/query/query_string/query_string_manager.ts index 47b6d536db6f..2bb5f41fbc19 100644 --- a/src/plugins/data/public/query/query_string/query_string_manager.ts +++ b/src/plugins/data/public/query/query_string/query_string_manager.ts @@ -316,7 +316,7 @@ export class QueryStringManager { return { ...newQuery, - query: this.getInitialDatasetQueryString(newQuery, newDataset), + query: this.getInitialDatasetQueryString(newQuery), }; }; From e0d284810ae1b53e61b91ae10558eb439e6e3eff Mon Sep 17 00:00:00 2001 From: Anan Zhuang Date: Thu, 21 Nov 2024 12:00:01 -0800 Subject: [PATCH 36/80] Remove filter out serverless cluster and add support to extract index name (#8872) * Remove filter out serverless cluster and add support to extract index name Allow extract index name for both serverless and non-serverless clusters Allow different key formats: - datasource-id::TIMESERIES:::0 - datasource-id:::0 - (non-serverless case) Signed-off-by: Anan Zhuang * fix PR comment Signed-off-by: Anan Zhuang * Changeset file for PR #8872 created/updated --------- Signed-off-by: Anan Zhuang Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> Signed-off-by: Federico Silva --- changelogs/fragments/8872.yml | 2 + .../dataset_service/lib/index_type.test.ts | 81 ++++++++++++++++--- .../dataset_service/lib/index_type.ts | 34 ++++---- .../public/datasets/s3_type.test.ts | 58 ++++++------- .../public/datasets/s3_type.ts | 28 +++---- 5 files changed, 133 insertions(+), 70 deletions(-) create mode 100644 changelogs/fragments/8872.yml diff --git a/changelogs/fragments/8872.yml b/changelogs/fragments/8872.yml new file mode 100644 index 000000000000..1e43b2ae770d --- /dev/null +++ b/changelogs/fragments/8872.yml @@ -0,0 +1,2 @@ +fix: +- Remove filter out serverless cluster and add support to extract index name ([#8872](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8872)) \ No newline at end of file diff --git a/src/plugins/data/public/query/query_string/dataset_service/lib/index_type.test.ts b/src/plugins/data/public/query/query_string/dataset_service/lib/index_type.test.ts index 7a3d9810fb7f..a645fd4acc7c 100644 --- a/src/plugins/data/public/query/query_string/dataset_service/lib/index_type.test.ts +++ b/src/plugins/data/public/query/query_string/dataset_service/lib/index_type.test.ts @@ -10,11 +10,18 @@ import { SavedObjectsClientContract } from 'opensearch-dashboards/public'; import { DATA_STRUCTURE_META_TYPES, DataStructure, Dataset } from '../../../../../common'; import * as services from '../../../../services'; import { IDataPluginServices } from 'src/plugins/data/public'; +import { of } from 'rxjs'; jest.mock('../../../../services', () => { + const mockSearchFunction = jest.fn(); + return { - getSearchService: jest.fn(), getIndexPatterns: jest.fn(), + getSearchService: jest.fn(() => ({ + getDefaultSearchInterceptor: () => ({ + search: mockSearchFunction, + }), + })), getQueryService: () => ({ queryString: { getLanguageService: () => ({ @@ -90,9 +97,7 @@ describe('indexTypeConfig', () => { test('should fetch data sources for unknown type', async () => { mockSavedObjectsClient.find = jest.fn().mockResolvedValue({ - savedObjects: [ - { id: 'ds1', attributes: { title: 'DataSource 1', dataSourceVersion: '3.0' } }, - ], + savedObjects: [{ id: 'ds1', attributes: { title: 'DataSource 1' } }], }); const result = await indexTypeConfig.fetch(mockServices as IDataPluginServices, [ @@ -104,18 +109,18 @@ describe('indexTypeConfig', () => { expect(result.hasNext).toBe(true); }); - test('should filter out data sources with versions lower than 1.0.0', async () => { + test('should NOT filter out data sources regardless of version', async () => { mockSavedObjectsClient.find = jest.fn().mockResolvedValue({ savedObjects: [ { id: 'ds1', attributes: { title: 'DataSource 1', dataSourceVersion: '1.0' } }, { id: 'ds2', - attributes: { title: 'DataSource 2', dataSourceVersion: '' }, + attributes: { title: 'DataSource 2', dataSourceVersion: '' }, // empty version }, { id: 'ds3', attributes: { title: 'DataSource 3', dataSourceVersion: '2.17.0' } }, { id: 'ds4', - attributes: { title: 'DataSource 4', dataSourceVersion: '.0' }, + attributes: { title: 'DataSource 4', dataSourceVersion: '.0' }, // invalid version }, ], }); @@ -124,10 +129,64 @@ describe('indexTypeConfig', () => { { id: 'unknown', title: 'Unknown', type: 'UNKNOWN' }, ]); - expect(result.children).toHaveLength(2); - expect(result.children?.[0].title).toBe('DataSource 1'); - expect(result.children?.[1].title).toBe('DataSource 3'); - expect(result.children?.some((child) => child.title === 'DataSource 2')).toBe(false); + // Verify all data sources are included regardless of version + expect(result.children).toHaveLength(4); + expect(result.children?.map((child) => child.title)).toEqual([ + 'DataSource 1', + 'DataSource 2', + 'DataSource 3', + 'DataSource 4', + ]); expect(result.hasNext).toBe(true); }); + + describe('fetchIndices', () => { + test('should extract index names correctly from different formats', async () => { + const mockResponse = { + rawResponse: { + aggregations: { + indices: { + buckets: [ + { key: '123::TIMESERIES::sample-index-1:0' }, + // Serverless format without TIMESERIES + { key: '123::sample-index-2:0' }, + // Non-serverless format + { key: 'simple-index' }, + ], + }, + }, + }, + }; + + const searchService = services.getSearchService(); + const interceptor = searchService.getDefaultSearchInterceptor(); + (interceptor.search as jest.Mock).mockReturnValue(of(mockResponse)); + + const result = await indexTypeConfig.fetch(mockServices as IDataPluginServices, [ + { id: 'datasource1', title: 'DataSource 1', type: 'DATA_SOURCE' }, + ]); + + expect(result.children).toEqual([ + { id: 'datasource1::sample-index-1', title: 'sample-index-1', type: 'INDEX' }, + { id: 'datasource1::sample-index-2', title: 'sample-index-2', type: 'INDEX' }, + { id: 'datasource1::simple-index', title: 'simple-index', type: 'INDEX' }, + ]); + }); + + test('should handle response without aggregations', async () => { + const mockResponse = { + rawResponse: {}, + }; + + const searchService = services.getSearchService(); + const interceptor = searchService.getDefaultSearchInterceptor(); + (interceptor.search as jest.Mock).mockReturnValue(of(mockResponse)); + + const result = await indexTypeConfig.fetch(mockServices as IDataPluginServices, [ + { id: 'datasource1', title: 'DataSource 1', type: 'DATA_SOURCE' }, + ]); + + expect(result.children).toEqual([]); + }); + }); }); diff --git a/src/plugins/data/public/query/query_string/dataset_service/lib/index_type.ts b/src/plugins/data/public/query/query_string/dataset_service/lib/index_type.ts index 13fc4ce14f72..31b331e1433d 100644 --- a/src/plugins/data/public/query/query_string/dataset_service/lib/index_type.ts +++ b/src/plugins/data/public/query/query_string/dataset_service/lib/index_type.ts @@ -6,7 +6,6 @@ import { SavedObjectsClientContract } from 'opensearch-dashboards/public'; import { map } from 'rxjs/operators'; import { i18n } from '@osd/i18n'; -import semver from 'semver'; import { DEFAULT_DATA, DataStructure, @@ -17,6 +16,8 @@ import { DatasetTypeConfig } from '../types'; import { getSearchService, getIndexPatterns } from '../../../../services'; import { injectMetaToDataStructures } from './utils'; +export const DELIMITER = '::'; + export const indexTypeConfig: DatasetTypeConfig = { id: DEFAULT_DATA.SET_TYPES.INDEX, title: 'Indexes', @@ -120,16 +121,11 @@ const fetchDataSources = async (client: SavedObjectsClientContract) => { type: 'data-source', perPage: 10000, }); - const dataSources: DataStructure[] = response.savedObjects - .filter((savedObject) => { - const coercedVersion = semver.coerce(savedObject.attributes.dataSourceVersion); - return coercedVersion ? semver.satisfies(coercedVersion, '>=1.0.0') : false; - }) - .map((savedObject) => ({ - id: savedObject.id, - title: savedObject.attributes.title, - type: 'DATA_SOURCE', - })); + const dataSources: DataStructure[] = response.savedObjects.map((savedObject) => ({ + id: savedObject.id, + title: savedObject.attributes.title, + type: 'DATA_SOURCE', + })); return injectMetaToDataStructures(dataSources); }; @@ -158,9 +154,19 @@ const fetchIndices = async (dataStructure: DataStructure): Promise => const searchResponseToArray = (response: any) => { const { rawResponse } = response; - return rawResponse.aggregations - ? rawResponse.aggregations.indices.buckets.map((bucket: { key: any }) => bucket.key) - : []; + if (!rawResponse.aggregations) { + return []; + } + + return rawResponse.aggregations.indices.buckets.map((bucket: { key: string }) => { + const key = bucket.key; + // Note: Index names cannot contain ':' or '::' in OpenSearch, so these delimiters + // are guaranteed not to be part of the regular format of index name + const parts = key.split(DELIMITER); + const lastPart = parts[parts.length - 1] || key; + // extract index name or return original key if pattern doesn't match + return lastPart.split(':')[0] || key; + }); }; return search diff --git a/src/plugins/query_enhancements/public/datasets/s3_type.test.ts b/src/plugins/query_enhancements/public/datasets/s3_type.test.ts index 6a2d5cc6182c..1a6066f72f14 100644 --- a/src/plugins/query_enhancements/public/datasets/s3_type.test.ts +++ b/src/plugins/query_enhancements/public/datasets/s3_type.test.ts @@ -141,9 +141,7 @@ describe('s3TypeConfig', () => { it('should fetch data sources for unknown type', async () => { mockSavedObjectsClient.find = jest.fn().mockResolvedValue({ - savedObjects: [ - { id: 'ds1', attributes: { title: 'DataSource 1', dataSourceVersion: '3.0' } }, - ], + savedObjects: [{ id: 'ds1', attributes: { title: 'DataSource 1' } }], }); const result = await s3TypeConfig.fetch(mockServices as IDataPluginServices, [ @@ -154,33 +152,37 @@ describe('s3TypeConfig', () => { expect(result.children?.[0].title).toBe('DataSource 1'); expect(result.hasNext).toBe(true); }); + }); - it('should filter out data sources with versions lower than 1.0.0', async () => { - mockSavedObjectsClient.find = jest.fn().mockResolvedValue({ - savedObjects: [ - { id: 'ds1', attributes: { title: 'DataSource 1', dataSourceVersion: '1.0' } }, - { - id: 'ds2', - attributes: { title: 'DataSource 2', dataSourceVersion: '' }, - }, - { id: 'ds3', attributes: { title: 'DataSource 3', dataSourceVersion: '2.17.0' } }, - { - id: 'ds4', - attributes: { title: 'DataSource 4', dataSourceVersion: '.0' }, - }, - ], - }); - - const result = await s3TypeConfig.fetch(mockServices as IDataPluginServices, [ - { id: 'unknown', title: 'Unknown', type: 'UNKNOWN' }, - ]); - - expect(result.children).toHaveLength(2); - expect(result.children?.[0].title).toBe('DataSource 1'); - expect(result.children?.[1].title).toBe('DataSource 3'); - expect(result.children?.some((child) => child.title === 'DataSource 2')).toBe(false); - expect(result.hasNext).toBe(true); + it('should NOT filter out data sources regardless of version', async () => { + mockSavedObjectsClient.find = jest.fn().mockResolvedValue({ + savedObjects: [ + { id: 'ds1', attributes: { title: 'DataSource 1', dataSourceVersion: '1.0' } }, + { + id: 'ds2', + attributes: { title: 'DataSource 2', dataSourceVersion: '' }, // empty version + }, + { id: 'ds3', attributes: { title: 'DataSource 3', dataSourceVersion: '2.17.0' } }, + { + id: 'ds4', + attributes: { title: 'DataSource 4', dataSourceVersion: '.0' }, // invalid version + }, + ], }); + + const result = await s3TypeConfig.fetch(mockServices as IDataPluginServices, [ + { id: 'unknown', title: 'Unknown', type: 'UNKNOWN' }, + ]); + + // Verify all data sources are included + expect(result.children).toHaveLength(4); + expect(result.children?.map((child) => child.title)).toEqual([ + 'DataSource 1', + 'DataSource 2', + 'DataSource 3', + 'DataSource 4', + ]); + expect(result.hasNext).toBe(true); }); test('fetchFields returns table fields', async () => { diff --git a/src/plugins/query_enhancements/public/datasets/s3_type.ts b/src/plugins/query_enhancements/public/datasets/s3_type.ts index c13b5e898670..4e8c41959f2d 100644 --- a/src/plugins/query_enhancements/public/datasets/s3_type.ts +++ b/src/plugins/query_enhancements/public/datasets/s3_type.ts @@ -6,7 +6,6 @@ import { i18n } from '@osd/i18n'; import { trimEnd } from 'lodash'; import { HttpSetup, SavedObjectsClientContract } from 'opensearch-dashboards/public'; -import semver from 'semver'; import { DATA_STRUCTURE_META_TYPES, DEFAULT_DATA, @@ -198,22 +197,17 @@ const fetchDataSources = async (client: SavedObjectsClientContract): Promise { - const coercedVersion = semver.coerce(savedObject.attributes.dataSourceVersion); - return coercedVersion ? semver.satisfies(coercedVersion, '>=1.0.0') : false; - }) - .map((savedObject) => ({ - id: savedObject.id, - title: savedObject.attributes.title, - type: 'DATA_SOURCE', - meta: { - query: { - id: savedObject.id, - }, - type: DATA_STRUCTURE_META_TYPES.CUSTOM, - } as DataStructureCustomMeta, - })); + const dataSources: DataStructure[] = resp.savedObjects.map((savedObject) => ({ + id: savedObject.id, + title: savedObject.attributes.title, + type: 'DATA_SOURCE', + meta: { + query: { + id: savedObject.id, + }, + type: DATA_STRUCTURE_META_TYPES.CUSTOM, + } as DataStructureCustomMeta, + })); return dataSources; }; From e046f3214ed0ba58b1e4073f9b642089d656a89e Mon Sep 17 00:00:00 2001 From: Anan Zhuang Date: Thu, 21 Nov 2024 12:18:08 -0800 Subject: [PATCH 37/80] Re-enable serverless mds support for index pattern (#8909) Signed-off-by: Anan Zhuang Signed-off-by: Federico Silva --- .../query_string/dataset_service/lib/index_pattern_type.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/plugins/data/public/query/query_string/dataset_service/lib/index_pattern_type.ts b/src/plugins/data/public/query/query_string/dataset_service/lib/index_pattern_type.ts index 5d230e0396bf..db2abe58a375 100644 --- a/src/plugins/data/public/query/query_string/dataset_service/lib/index_pattern_type.ts +++ b/src/plugins/data/public/query/query_string/dataset_service/lib/index_pattern_type.ts @@ -66,9 +66,6 @@ export const indexPatternTypeConfig: DatasetTypeConfig = { }, supportedLanguages: (dataset): string[] => { - if (dataset.dataSource?.type === 'OpenSearch Serverless') { - return ['kuery', 'lucene']; - } return ['kuery', 'lucene', 'PPL', 'SQL']; }, From e8281b15861bdc543457353f4dd4842caee2b4aa Mon Sep 17 00:00:00 2001 From: Anan Zhuang Date: Thu, 21 Nov 2024 13:27:00 -0800 Subject: [PATCH 38/80] enable s3 (#8911) Signed-off-by: Anan Zhuang Signed-off-by: Federico Silva --- .../public/datasets/s3_type.test.ts | 58 +++++++++---------- .../public/datasets/s3_type.ts | 28 +++++---- 2 files changed, 45 insertions(+), 41 deletions(-) diff --git a/src/plugins/query_enhancements/public/datasets/s3_type.test.ts b/src/plugins/query_enhancements/public/datasets/s3_type.test.ts index 1a6066f72f14..6a2d5cc6182c 100644 --- a/src/plugins/query_enhancements/public/datasets/s3_type.test.ts +++ b/src/plugins/query_enhancements/public/datasets/s3_type.test.ts @@ -141,7 +141,9 @@ describe('s3TypeConfig', () => { it('should fetch data sources for unknown type', async () => { mockSavedObjectsClient.find = jest.fn().mockResolvedValue({ - savedObjects: [{ id: 'ds1', attributes: { title: 'DataSource 1' } }], + savedObjects: [ + { id: 'ds1', attributes: { title: 'DataSource 1', dataSourceVersion: '3.0' } }, + ], }); const result = await s3TypeConfig.fetch(mockServices as IDataPluginServices, [ @@ -152,37 +154,33 @@ describe('s3TypeConfig', () => { expect(result.children?.[0].title).toBe('DataSource 1'); expect(result.hasNext).toBe(true); }); - }); - it('should NOT filter out data sources regardless of version', async () => { - mockSavedObjectsClient.find = jest.fn().mockResolvedValue({ - savedObjects: [ - { id: 'ds1', attributes: { title: 'DataSource 1', dataSourceVersion: '1.0' } }, - { - id: 'ds2', - attributes: { title: 'DataSource 2', dataSourceVersion: '' }, // empty version - }, - { id: 'ds3', attributes: { title: 'DataSource 3', dataSourceVersion: '2.17.0' } }, - { - id: 'ds4', - attributes: { title: 'DataSource 4', dataSourceVersion: '.0' }, // invalid version - }, - ], - }); + it('should filter out data sources with versions lower than 1.0.0', async () => { + mockSavedObjectsClient.find = jest.fn().mockResolvedValue({ + savedObjects: [ + { id: 'ds1', attributes: { title: 'DataSource 1', dataSourceVersion: '1.0' } }, + { + id: 'ds2', + attributes: { title: 'DataSource 2', dataSourceVersion: '' }, + }, + { id: 'ds3', attributes: { title: 'DataSource 3', dataSourceVersion: '2.17.0' } }, + { + id: 'ds4', + attributes: { title: 'DataSource 4', dataSourceVersion: '.0' }, + }, + ], + }); + + const result = await s3TypeConfig.fetch(mockServices as IDataPluginServices, [ + { id: 'unknown', title: 'Unknown', type: 'UNKNOWN' }, + ]); - const result = await s3TypeConfig.fetch(mockServices as IDataPluginServices, [ - { id: 'unknown', title: 'Unknown', type: 'UNKNOWN' }, - ]); - - // Verify all data sources are included - expect(result.children).toHaveLength(4); - expect(result.children?.map((child) => child.title)).toEqual([ - 'DataSource 1', - 'DataSource 2', - 'DataSource 3', - 'DataSource 4', - ]); - expect(result.hasNext).toBe(true); + expect(result.children).toHaveLength(2); + expect(result.children?.[0].title).toBe('DataSource 1'); + expect(result.children?.[1].title).toBe('DataSource 3'); + expect(result.children?.some((child) => child.title === 'DataSource 2')).toBe(false); + expect(result.hasNext).toBe(true); + }); }); test('fetchFields returns table fields', async () => { diff --git a/src/plugins/query_enhancements/public/datasets/s3_type.ts b/src/plugins/query_enhancements/public/datasets/s3_type.ts index 4e8c41959f2d..c13b5e898670 100644 --- a/src/plugins/query_enhancements/public/datasets/s3_type.ts +++ b/src/plugins/query_enhancements/public/datasets/s3_type.ts @@ -6,6 +6,7 @@ import { i18n } from '@osd/i18n'; import { trimEnd } from 'lodash'; import { HttpSetup, SavedObjectsClientContract } from 'opensearch-dashboards/public'; +import semver from 'semver'; import { DATA_STRUCTURE_META_TYPES, DEFAULT_DATA, @@ -197,17 +198,22 @@ const fetchDataSources = async (client: SavedObjectsClientContract): Promise ({ - id: savedObject.id, - title: savedObject.attributes.title, - type: 'DATA_SOURCE', - meta: { - query: { - id: savedObject.id, - }, - type: DATA_STRUCTURE_META_TYPES.CUSTOM, - } as DataStructureCustomMeta, - })); + const dataSources: DataStructure[] = resp.savedObjects + .filter((savedObject) => { + const coercedVersion = semver.coerce(savedObject.attributes.dataSourceVersion); + return coercedVersion ? semver.satisfies(coercedVersion, '>=1.0.0') : false; + }) + .map((savedObject) => ({ + id: savedObject.id, + title: savedObject.attributes.title, + type: 'DATA_SOURCE', + meta: { + query: { + id: savedObject.id, + }, + type: DATA_STRUCTURE_META_TYPES.CUSTOM, + } as DataStructureCustomMeta, + })); return dataSources; }; From 3e9e64d7b6bd31544485745233a9162d2f5a680c Mon Sep 17 00:00:00 2001 From: Daniel Rowe <51932404+d-rowe@users.noreply.github.com> Date: Fri, 22 Nov 2024 05:01:47 -0800 Subject: [PATCH 39/80] Fix query editor cursor alignment by removing monaco css overrides (#8912) * Fix query editor cursor alignment by removing monaco css overrides Monaco editor computes the cursor position internally and does not account for any css overrides that change the position of the line (padding, margin, etc). This change removes the css override while we implement these styles with the monaco api directly. Signed-off-by: Daniel Rowe * Changeset file for PR #8912 created/updated * fix linting 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> Signed-off-by: Federico Silva --- changelogs/fragments/8912.yml | 2 ++ .../query_editor/editors/default_editor/_default_editor.scss | 4 ---- 2 files changed, 2 insertions(+), 4 deletions(-) create mode 100644 changelogs/fragments/8912.yml diff --git a/changelogs/fragments/8912.yml b/changelogs/fragments/8912.yml new file mode 100644 index 000000000000..4f465fe54aaa --- /dev/null +++ b/changelogs/fragments/8912.yml @@ -0,0 +1,2 @@ +fix: +- Ensure query editor cursor is aligned with text ([#8912](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8912)) \ No newline at end of file diff --git a/src/plugins/data/public/ui/query_editor/editors/default_editor/_default_editor.scss b/src/plugins/data/public/ui/query_editor/editors/default_editor/_default_editor.scss index 2516c32ec27c..8a626dbe36e3 100644 --- a/src/plugins/data/public/ui/query_editor/editors/default_editor/_default_editor.scss +++ b/src/plugins/data/public/ui/query_editor/editors/default_editor/_default_editor.scss @@ -16,10 +16,6 @@ padding: 0 $euiSizeXS; } - .view-lines { - padding: 0 $euiSizeXS; - } - .monaco-scrollable-element { border-radius: 0 $euiSizeXS 0 0; } From fa16c5764f268bef4d91b73d0eefa600c1a1bc42 Mon Sep 17 00:00:00 2001 From: Kawika Avilla Date: Fri, 22 Nov 2024 14:45:13 -0800 Subject: [PATCH 40/80] [augmenter] do not support datasources with no version (#8915) Signed-off-by: Kawika Avilla Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> Signed-off-by: Federico Silva --- 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, 152 insertions(+), 19 deletions(-) create mode 100644 changelogs/fragments/8915.yml diff --git a/changelogs/fragments/8915.yml b/changelogs/fragments/8915.yml new file mode 100644 index 000000000000..46c124d3f25f --- /dev/null +++ b/changelogs/fragments/8915.yml @@ -0,0 +1,2 @@ +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 9760bfd75b2d..bd6e45a3967b 100644 --- a/src/plugins/vis_augmenter/public/plugin.ts +++ b/src/plugins/vis_augmenter/public/plugin.ts @@ -13,6 +13,7 @@ import { setUiActions, setEmbeddable, setQueryService, + setIndexPatterns, setVisualizations, setCore, } from './services'; @@ -62,6 +63,7 @@ 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 1d7f3e2111db..44a7ea8b424b 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 } from '../../../plugins/data/public'; +import { DataPublicPluginStart, IndexPatternsContract } from '../../../plugins/data/public'; import { VisualizationsStart } from '../../visualizations/public'; import { CoreStart } from '../../../core/public'; @@ -26,6 +26,10 @@ 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 f831deef3955..05f90522fe4a 100644 --- a/src/plugins/vis_augmenter/public/utils/utils.test.ts +++ b/src/plugins/vis_augmenter/public/utils/utils.test.ts @@ -21,11 +21,12 @@ 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 { setUISettings } from '../services'; +import { setIndexPatterns, setUISettings } from '../services'; import { STUB_INDEX_PATTERN_WITH_FIELDS, TYPES_REGISTRY, @@ -35,6 +36,7 @@ import { createPointInTimeEventsVisLayer, createVisLayer, } from '../mocks'; +import { dataPluginMock } from 'src/plugins/data/public/mocks'; describe('utils', () => { const uiSettingsMock = uiSettingsServiceMock.createStartContract(); @@ -60,7 +62,7 @@ describe('utils', () => { aggs: VALID_AGGS, }, } as unknown) as Vis; - expect(isEligibleForVisLayers(vis)).toEqual(false); + expect(await isEligibleForVisLayers(vis)).toEqual(false); }); it('vis is ineligible with no date_histogram', async () => { const invalidConfigStates = [ @@ -87,7 +89,7 @@ describe('utils', () => { invalidAggs, }, } as unknown) as Vis; - expect(isEligibleForVisLayers(vis)).toEqual(false); + expect(await isEligibleForVisLayers(vis)).toEqual(false); }); it('vis is ineligible with invalid aggs counts', async () => { const invalidConfigStates = [ @@ -111,7 +113,7 @@ describe('utils', () => { invalidAggs, }, } as unknown) as Vis; - expect(isEligibleForVisLayers(vis)).toEqual(false); + expect(await isEligibleForVisLayers(vis)).toEqual(false); }); it('vis is ineligible with no metric aggs', async () => { const invalidConfigStates = [ @@ -133,7 +135,7 @@ describe('utils', () => { invalidAggs, }, } as unknown) as Vis; - expect(isEligibleForVisLayers(vis)).toEqual(false); + expect(await isEligibleForVisLayers(vis)).toEqual(false); }); it('vis is ineligible with series param is not line type', async () => { const vis = ({ @@ -154,7 +156,7 @@ describe('utils', () => { aggs: VALID_AGGS, }, } as unknown) as Vis; - expect(isEligibleForVisLayers(vis)).toEqual(false); + expect(await isEligibleForVisLayers(vis)).toEqual(false); }); it('vis is ineligible with series param not all being line type', async () => { const vis = ({ @@ -178,7 +180,7 @@ describe('utils', () => { aggs: VALID_AGGS, }, } as unknown) as Vis; - expect(isEligibleForVisLayers(vis)).toEqual(false); + expect(await isEligibleForVisLayers(vis)).toEqual(false); }); it('vis is ineligible with invalid x-axis due to no segment aggregation', async () => { const badConfigStates = [ @@ -216,7 +218,7 @@ describe('utils', () => { badAggs, }, } as unknown) as Vis; - expect(isEligibleForVisLayers(invalidVis)).toEqual(false); + expect(await isEligibleForVisLayers(invalidVis)).toEqual(false); }); it('vis is ineligible with xaxis not on bottom', async () => { const invalidVis = ({ @@ -237,7 +239,7 @@ describe('utils', () => { aggs: VALID_AGGS, }, } as unknown) as Vis; - expect(isEligibleForVisLayers(invalidVis)).toEqual(false); + expect(await isEligibleForVisLayers(invalidVis)).toEqual(false); }); it('vis is ineligible with no seriesParams', async () => { const invalidVis = ({ @@ -253,16 +255,16 @@ describe('utils', () => { aggs: VALID_AGGS, }, } as unknown) as Vis; - expect(isEligibleForVisLayers(invalidVis)).toEqual(false); + expect(await 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(isEligibleForVisLayers(VALID_VIS)).toEqual(false); + expect(await isEligibleForVisLayers(VALID_VIS)).toEqual(false); }); it('vis is eligible with valid type', async () => { - expect(isEligibleForVisLayers(VALID_VIS)).toEqual(true); + expect(await isEligibleForVisLayers(VALID_VIS)).toEqual(true); }); }); @@ -660,4 +662,107 @@ 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 ce44964e6173..0ae3c9ec93aa 100644 --- a/src/plugins/vis_augmenter/public/utils/utils.ts +++ b/src/plugins/vis_augmenter/public/utils/utils.ts @@ -4,6 +4,7 @@ */ import { get, isEmpty } from 'lodash'; +import semver from 'semver'; import { Vis } from '../../../../plugins/visualizations/public'; import { formatExpression, @@ -20,10 +21,13 @@ import { VisLayerErrorTypes, } from '../'; import { PLUGIN_AUGMENTATION_ENABLE_SETTING } from '../../common/constants'; -import { getUISettings } from '../services'; +import { getUISettings, getIndexPatterns } from '../services'; import { IUiSettingsClient } from '../../../../core/public'; -export const isEligibleForVisLayers = (vis: Vis, uiSettingsClient?: IUiSettingsClient): boolean => { +export const isEligibleForVisLayers = async ( + vis: Vis, + uiSettingsClient?: IUiSettingsClient +): Promise => { // Only support a date histogram const dateHistograms = vis.data?.aggs?.byTypeName?.('date_histogram'); if (!Array.isArray(dateHistograms) || dateHistograms.length !== 1) return false; @@ -53,6 +57,9 @@ export const isEligibleForVisLayers = (vis: Vis, uiSettingsClient?: IUiSettingsC ) 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); @@ -163,7 +170,6 @@ 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[], @@ -187,3 +193,17 @@ 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 ac7f795c586e..f83f0e0b77d6 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 && - isEligibleForVisLayers(vis) && + (await 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 8650c6013801..e8d207017c00 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 || - !isEligibleForVisLayers(vis) + !(await 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 605c88067211..7bf996c148ea 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 && isEligibleForVisLayers(this.vis)) { + if (!isEmpty(augmentVisSavedObjs) && !aborted && (await 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 8a31bd827ae24a7c2a38b82ca02de0beae33bb5c Mon Sep 17 00:00:00 2001 From: Daniel Rowe <51932404+d-rowe@users.noreply.github.com> Date: Wed, 27 Nov 2024 14:43:27 -0800 Subject: [PATCH 41/80] 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> Signed-off-by: Federico Silva --- changelogs/fragments/8926.yml | 2 + cypress.config.js | 31 +++++ cypress.json | 21 --- cypress/support/e2e.js | 6 + package.json | 2 +- yarn.lock | 242 ++++++++++++++++++++++++++++++---- 6 files changed, 256 insertions(+), 48 deletions(-) create mode 100644 changelogs/fragments/8926.yml create mode 100644 cypress.config.js 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/cypress.config.js b/cypress.config.js new file mode 100644 index 000000000000..8ac393867e20 --- /dev/null +++ b/cypress.config.js @@ -0,0 +1,31 @@ +/* + * 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, + }, + e2e: { + baseUrl: 'http://localhost:5601', + specPattern: 'cypress/integration/**/*_spec.{js,jsx,ts,tsx}', + }, +}); 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 5bd2a4a5d09f..44315e13d5be 100644 --- a/package.json +++ b/package.json @@ -384,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 5b3dec208a45..4f21c30e1e52 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 ea152abf9ba62ef42de3387e5859b15204d88b6f Mon Sep 17 00:00:00 2001 From: Argus Li Date: Mon, 2 Dec 2024 16:19:58 -0800 Subject: [PATCH 42/80] Add utils to select a data source. Signed-off-by: Federico Silva --- 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: Tue, 3 Dec 2024 21:58:09 -0800 Subject: [PATCH 43/80] Add support for different languages when creating dataset. Filter Out is almost complete, just working on removing the filter. Signed-off-by: Federico Silva --- 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 f30e89de12c706a5c491f0406962ebb067b852d8 Mon Sep 17 00:00:00 2001 From: Miki Date: Mon, 25 Nov 2024 22:43:34 -0800 Subject: [PATCH 44/80] Mitigate the incorrect layout of Discover due to a race condition between loading column definition and data (#8928) Signed-off-by: Miki Signed-off-by: Federico Silva --- .../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 3cd9b9c3b3bceb66ed2ac6a5b701343f269fa010 Mon Sep 17 00:00:00 2001 From: Tianyu Gao Date: Wed, 27 Nov 2024 17:29:58 +0800 Subject: [PATCH 45/80] [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> Signed-off-by: Federico Silva --- 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 81387a2d8dcc6ca93c3e81f7d22590602aca9b99 Mon Sep 17 00:00:00 2001 From: Paul Sebastian Date: Wed, 27 Nov 2024 03:23:45 -0800 Subject: [PATCH 46/80] [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> Signed-off-by: Federico Silva --- 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 e9ff35cac09dae814ea8ee9d4a2bf083e523ab38 Mon Sep 17 00:00:00 2001 From: Miki Date: Wed, 27 Nov 2024 12:33:46 -0800 Subject: [PATCH 47/80] 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> Signed-off-by: Federico Silva --- 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 44315e13d5be..4bcdef195fcb 100644 --- a/package.json +++ b/package.json @@ -111,7 +111,6 @@ "**/jest-config": "npm:@amoo-miki/jest-config@27.5.1", "**/jest-jasmine2": "npm:@amoo-miki/jest-jasmine2@27.5.1", "**/joi/hoek": "npm:@amoo-miki/hoek@6.1.3", - "**/json11": "^2.0.0", "**/json-schema": "^0.4.0", "**/kind-of": ">=6.0.3", "**/load-bmfont/phin": "^3.7.1", @@ -166,7 +165,7 @@ "@hapi/vision": "^6.1.0", "@hapi/wreck": "^17.1.0", "@opensearch-dashboards-test/opensearch-dashboards-test-library": "https://github.com/opensearch-project/opensearch-dashboards-test-library/archive/refs/tags/1.0.6.tar.gz", - "@opensearch-project/opensearch": "^2.9.0", + "@opensearch-project/opensearch": "^2.13.0", "@opensearch/datemath": "5.0.3", "@osd/ace": "1.0.0", "@osd/analytics": "1.0.0", diff --git a/packages/osd-opensearch-archiver/package.json b/packages/osd-opensearch-archiver/package.json index d1e9174299fa..bc4e8b227b30 100644 --- a/packages/osd-opensearch-archiver/package.json +++ b/packages/osd-opensearch-archiver/package.json @@ -13,7 +13,7 @@ "dependencies": { "@osd/dev-utils": "1.0.0", "@osd/std": "1.0.0", - "@opensearch-project/opensearch": "^2.9.0" + "@opensearch-project/opensearch": "^2.13.0" }, "devDependencies": {} } diff --git a/packages/osd-opensearch/package.json b/packages/osd-opensearch/package.json index 4459c846c6c2..a70263e8af6d 100644 --- a/packages/osd-opensearch/package.json +++ b/packages/osd-opensearch/package.json @@ -12,7 +12,7 @@ "osd:watch": "../../scripts/use_node scripts/build --watch" }, "dependencies": { - "@opensearch-project/opensearch": "^2.9.0", + "@opensearch-project/opensearch": "^2.13.0", "@osd/dev-utils": "1.0.0", "abort-controller": "^3.0.0", "chalk": "^4.1.0", diff --git a/scripts/postinstall.js b/scripts/postinstall.js index 59be50284dca..7865473ee494 100644 --- a/scripts/postinstall.js +++ b/scripts/postinstall.js @@ -84,15 +84,6 @@ const run = async () => { }, ]) ); - //ToDo: Remove when opensearch-js is released to include https://github.com/opensearch-project/opensearch-js/pull/889 - promises.push( - patchFile('node_modules/@opensearch-project/opensearch/lib/Serializer.js', [ - { - from: 'val < Number.MAX_SAFE_INTEGER', - to: 'val < Number.MIN_SAFE_INTEGER', - }, - ]) - ); await Promise.all(promises); }; diff --git a/yarn.lock b/yarn.lock index 4f21c30e1e52..537af6f3662e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2594,15 +2594,15 @@ version "1.0.6" resolved "https://github.com/opensearch-project/opensearch-dashboards-test-library/archive/refs/tags/1.0.6.tar.gz#f2f489832a75191e243c6d2b42d49047265d9ce3" -"@opensearch-project/opensearch@^2.9.0": - version "2.9.0" - resolved "https://registry.yarnpkg.com/@opensearch-project/opensearch/-/opensearch-2.9.0.tgz#319b4d174540b6d000c31477a56618e5054c6fcb" - integrity sha512-BXPWSBME1rszZ8OvtBVQ9F6kLiZSENDSFPawbPa1fv0GouuQfWxkKSI9TcnfGLp869fgLTEIfeC5Qexd4RbAYw== +"@opensearch-project/opensearch@^2.13.0": + version "2.13.0" + resolved "https://registry.yarnpkg.com/@opensearch-project/opensearch/-/opensearch-2.13.0.tgz#e60c1a3a3dd059562f1d901aa8d3659035cb1781" + integrity sha512-Bu3jJ7pKzumbMMeefu7/npAWAvFu5W9SlbBow1ulhluqUpqc7QoXe0KidDrMy7Dy3BQrkI6llR3cWL4lQTZOFw== dependencies: aws4 "^1.11.0" debug "^4.3.1" hpagent "^1.2.0" - json11 "^1.0.4" + json11 "^2.0.0" ms "^2.1.3" secure-json-parse "^2.4.0" @@ -11500,7 +11500,7 @@ json-stringify-safe@5.0.1, json-stringify-safe@^5.0.1, json-stringify-safe@~5.0. resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= -json11@^1.0.4, json11@^2.0.0: +json11@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/json11/-/json11-2.0.0.tgz#06c4ad0a40b50c5de99a87f6d3028593137e5641" integrity sha512-VuKJKUSPEJape+daTm70Nx7vdcdorf4S6LCyN2z0jUVH4UrQ4ftXo2kC0bnHpCREmxHuHqCNVPA75BjI3CB6Ag== From 2c39431ad04ddbff2b4ef98d4975b6f8c6446626 Mon Sep 17 00:00:00 2001 From: Qxisylolo Date: Thu, 28 Nov 2024 17:17:28 +0800 Subject: [PATCH 48/80] [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> Signed-off-by: Federico Silva --- 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 d6ede1dfdd79d4fe8340658df91a27eec597f133 Mon Sep 17 00:00:00 2001 From: Hailong Cui Date: Thu, 28 Nov 2024 17:18:42 +0800 Subject: [PATCH 49/80] [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> Signed-off-by: Federico Silva --- 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 cabe8b8b6049e9d934d39da8e0adde892833d3f4 Mon Sep 17 00:00:00 2001 From: yuboluo Date: Mon, 2 Dec 2024 15:56:34 +0800 Subject: [PATCH 50/80] [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> Signed-off-by: Federico Silva --- 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 d303c878d7ed52d8cd638b178c5ac0b27ecf347e Mon Sep 17 00:00:00 2001 From: Sean Li Date: Mon, 2 Dec 2024 16:54:14 -0800 Subject: [PATCH 51/80] [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> Signed-off-by: Federico Silva --- 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 e41f26c60af351e702103bc8e0f1535e6342b81d Mon Sep 17 00:00:00 2001 From: yuboluo Date: Tue, 3 Dec 2024 11:24:56 +0800 Subject: [PATCH 52/80] [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> Signed-off-by: Federico Silva --- 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 0b35c1264c4e71644c7a1c82c25043754f34b201 Mon Sep 17 00:00:00 2001 From: Joshua Li Date: Mon, 2 Dec 2024 20:01:48 -0800 Subject: [PATCH 53/80] [Discover] fix PPL to not throw error if aggregation query fails (#8992) Signed-off-by: Joshua Li Signed-off-by: Federico Silva --- .../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 25bf53ee4bf638694875c1591b3313f1d99588ed Mon Sep 17 00:00:00 2001 From: Argus Li Date: Wed, 4 Dec 2024 12:29:36 -0800 Subject: [PATCH 54/80] Reformat to match OSD-functional-tests. Reformat for TS. Signed-off-by: Federico Silva --- 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 a231848e780e45fb5a659986edfeafb69e90b036 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 55/80] 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> Signed-off-by: Federico Silva --- changelogs/fragments/8993.yml | 2 ++ cypress.config.ts | 3 --- cypress/support/e2e.js | 6 ++++++ package.json | 1 + 4 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 changelogs/fragments/8993.yml create mode 100644 cypress/support/e2e.js 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 index 66802069f200..67e7b4f5039b 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -24,9 +24,6 @@ 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', 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 4bcdef195fcb..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", From 495a69ea45810d963e631f4c2bec398cdabf44c8 Mon Sep 17 00:00:00 2001 From: Joshua Li Date: Tue, 3 Dec 2024 13:22:27 -0800 Subject: [PATCH 56/80] [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 Signed-off-by: Federico Silva --- .../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 dc235e27714fe1ae9951b37ae54119e37d7f6f5f Mon Sep 17 00:00:00 2001 From: Tyler Ohlsen Date: Wed, 4 Dec 2024 09:48:20 -0800 Subject: [PATCH 57/80] Revert "[augmenter] do not support datasources with no version (#8915)" (#8925) This reverts commit 539675e688061e689b362801bcb05a3ef78431b2. Signed-off-by: Federico Silva --- 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 fea5a8fb1167495da05b025f2c7d9facdf30c19d Mon Sep 17 00:00:00 2001 From: Argus Li Date: Wed, 4 Dec 2024 23:36:11 -0800 Subject: [PATCH 58/80] Complete test suite filter actions in table field. Refactor to match OSD-functional-tests-layout. Signed-off-by: Federico Silva --- cypress.config.ts | 3 +- .../filter_for_value_spec.js | 9 +- cypress/support/e2e.js | 12 +- cypress/support/e2e.ts | 15 -- cypress/utils/{commands.ts => commands.js} | 38 ++-- .../dashboards/data_explorer/commands.js | 209 ++++++++++++++++++ .../dashboards/data_explorer/commands.ts | 171 -------------- .../dashboards/data_explorer/constants.js | 8 + .../{elements.ts => elements.js} | 1 + .../data/public/ui/filter_bar/filter_bar.tsx | 7 +- 10 files changed, 263 insertions(+), 210 deletions(-) delete mode 100644 cypress/support/e2e.ts rename cypress/utils/{commands.ts => commands.js} (62%) create mode 100644 cypress/utils/dashboards/data_explorer/commands.js delete mode 100644 cypress/utils/dashboards/data_explorer/commands.ts create mode 100644 cypress/utils/dashboards/data_explorer/constants.js rename cypress/utils/dashboards/data_explorer/{elements.ts => elements.js} (96%) diff --git a/cypress.config.ts b/cypress.config.ts index 67e7b4f5039b..52eddacb6e99 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -27,8 +27,9 @@ module.exports = defineConfig({ }, e2e: { baseUrl: 'http://localhost:5601', + supportFile: 'cypress/support/e2e.{js,jsx,ts,tsx}', specPattern: 'cypress/integration/**/*_spec.{js,jsx,ts,tsx}', - testIsolation: false, + testIsolation: true, setupNodeEvents, }, }); diff --git a/cypress/integration/core_opensearch_dashboards/filter_for_value_spec.js b/cypress/integration/core_opensearch_dashboards/filter_for_value_spec.js index 4f82d5f3d95e..4abbce49b11b 100644 --- a/cypress/integration/core_opensearch_dashboards/filter_for_value_spec.js +++ b/cypress/integration/core_opensearch_dashboards/filter_for_value_spec.js @@ -11,22 +11,25 @@ describe('filter for value spec', () => { beforeEach(() => { cy.localLogin(Cypress.env('username'), Cypress.env('password')); miscUtils.visitPage('app/data-explorer/discover'); - cy.clickNewSearchButton(); + cy.getNewSearchButton().click(); }); describe('filter actions in table field', () => { describe('index pattern dataset', () => { - // filter actions should not exist for DQL + // filter actions should exist for DQL it('DQL', () => { cy.selectIndexPatternDataset('DQL'); cy.setSearchRelativeDateRange('15', 'Years ago'); cy.checkDocTableFirstFieldFilterForAndOutButton(true); cy.checkDocTableFirstFieldFilterForButtonFiltersCorrectField(); + cy.checkDocTableFirstFieldFilterOutButtonFiltersCorrectField(); }); - // filter actions should not exist for PPL + // filter actions should exist for Lucene it('Lucene', () => { cy.selectIndexPatternDataset('Lucene'); cy.setSearchRelativeDateRange('15', 'Years ago'); cy.checkDocTableFirstFieldFilterForAndOutButton(true); + cy.checkDocTableFirstFieldFilterForButtonFiltersCorrectField(); + cy.checkDocTableFirstFieldFilterOutButtonFiltersCorrectField(); }); // filter actions should not exist for SQL it('SQL', () => { diff --git a/cypress/support/e2e.js b/cypress/support/e2e.js index fa35cf4214b4..b19e490d7080 100644 --- a/cypress/support/e2e.js +++ b/cypress/support/e2e.js @@ -3,4 +3,14 @@ * SPDX-License-Identifier: Apache-2.0 */ -import '../utils/commands'; +import '../utils/commands.js'; +import '../utils/dashboards/data_explorer/commands.js'; + +// Alternatively you can use CommonJS syntax: +// require('./commands') + +// eslint-disable-next-line no-unused-vars +Cypress.on('uncaught:exception', (_err) => { + // returning false here prevents Cypress from failing the test + return false; +}); diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.ts deleted file mode 100644 index ae89c76268a3..000000000000 --- a/cypress/support/e2e.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import '../utils/commands'; -import '../utils/dashboards/data_explorer/commands'; - -// Alternatively you can use CommonJS syntax: -// require('./commands') - -Cypress.on('uncaught:exception', (_err) => { - // returning false here prevents Cypress from failing the test - return false; -}); diff --git a/cypress/utils/commands.ts b/cypress/utils/commands.js similarity index 62% rename from cypress/utils/commands.ts rename to cypress/utils/commands.js index ea07f3ed4406..1ab606ef788b 100644 --- a/cypress/utils/commands.ts +++ b/cypress/utils/commands.js @@ -11,21 +11,35 @@ import { const miscUtils = new MiscUtils(cy); const loginPage = new LoginPage(cy); -// --- Typed commands -- - +/** + * Get DOM element by data-test-subj id. + */ Cypress.Commands.add('getElementByTestId', (testId, options = {}) => { return cy.get(`[data-test-subj="${testId}"]`, options); }); +/** + * Get multiple DOM elements by data-test-subj ids. + */ Cypress.Commands.add('getElementsByTestIds', (testIds, options = {}) => { const selectors = [testIds].flat(Infinity).map((testId) => `[data-test-subj="${testId}"]`); return cy.get(selectors.join(','), options); }); -Cypress.Commands.add('findElementByTestId', (testId, options = {}) => { - return cy.find(`[data-test-subj="${testId}"]`, options); -}); - +/** + * Find element from previous chained element by data-test-subj id. + */ +Cypress.Commands.add( + 'findElementByTestId', + { prevSubject: true }, + (subject, testId, options = {}) => { + return cy.wrap(subject).find(`[data-test-subj="${testId}"]`, options); + } +); + +/** + * Go to the local instance of OSD's home page and login. + */ Cypress.Commands.add('localLogin', (username, password) => { miscUtils.visitPage('/app/home'); loginPage.enterUserName(username); @@ -33,15 +47,3 @@ Cypress.Commands.add('localLogin', (username, password) => { loginPage.submit(); cy.url().should('contain', '/app/home'); }); - -Cypress.Commands.add('waitForLoader', () => { - const opts = { log: false }; - - Cypress.log({ - name: 'waitForPageLoad', - displayName: 'wait', - message: 'page load', - }); - cy.wait(Cypress.env('WAIT_FOR_LOADER_BUFFER_MS')); - cy.getElementByTestId('recentItemsSectionButton', opts); // Update to `homeLoader` once useExpandedHeader is enabled -}); diff --git a/cypress/utils/dashboards/data_explorer/commands.js b/cypress/utils/dashboards/data_explorer/commands.js new file mode 100644 index 000000000000..f8a46dcfb0a3 --- /dev/null +++ b/cypress/utils/dashboards/data_explorer/commands.js @@ -0,0 +1,209 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { DATA_EXPLORER_PAGE_ELEMENTS } from './elements.js'; +import { INDEX_CLUSTER_NAME, INDEX_NAME, INDEX_PATTERN_NAME } from './constants.js'; + +/** + * Get the New Search button. + */ +Cypress.Commands.add('getNewSearchButton', () => { + return cy + .getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.NEW_SEARCH_BUTTON, { timeout: 10000 }) + .should('be.visible'); +}); + +/** + * Open window to select Dataset + */ +Cypress.Commands.add('openDatasetExplorerWindow', () => { + cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_SELECTOR_BUTTON).click(); + cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.ALL_DATASETS_BUTTON).click(); +}); + +/** + * Select a Time Field in the Dataset Selector + * @param timeField Timefield for Language specific Time field. PPL allows "birthdate", "timestamp" and "I don't want to use the time filter" + */ +Cypress.Commands.add('selectDatasetTimeField', (timeField) => { + cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_SELECTOR_TIME_SELECTOR).select( + timeField + ); +}); + +/** + * Select a language in the Dataset Selector for Index + * @param datasetLanguage Index supports "OpenSearch SQL" and "PPL" + */ +Cypress.Commands.add('selectIndexDatasetLanguage', (datasetLanguage) => { + cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_SELECTOR_LANGUAGE_SELECTOR).select( + datasetLanguage + ); + switch (datasetLanguage) { + case 'PPL': + cy.selectDatasetTimeField("I don't want to use the time filter"); + break; + } + cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_SELECTOR_SELECT_DATA_BUTTON).click(); +}); + +/** + * Select an index dataset. + * @param datasetLanguage Index supports "DQL", "Lucene", "OpenSearch SQL" and "PPL" + */ +Cypress.Commands.add('selectIndexDataset', (datasetLanguage) => { + cy.openDatasetExplorerWindow(); + cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_EXPLORER_WINDOW) + .contains('Indexes') + .click(); + cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_EXPLORER_WINDOW) + .contains(INDEX_CLUSTER_NAME, { timeout: 10000 }) + .click(); + cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_EXPLORER_WINDOW) + .contains(INDEX_NAME, { timeout: 10000 }) + .click(); + cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_SELECTOR_NEXT_BUTTON).click(); + cy.selectIndexDatasetLanguage(datasetLanguage); +}); + +/** + * Select a language in the Dataset Selector for Index Pattern + * @param datasetLanguage Index supports "DQL", "Lucene", "OpenSearch SQL" and "PPL" + */ +Cypress.Commands.add('selectIndexPatternDatasetLanguage', (datasetLanguage) => { + cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_SELECTOR_LANGUAGE_SELECTOR).select( + datasetLanguage + ); + cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_SELECTOR_SELECT_DATA_BUTTON).click(); +}); + +/** + * Select an index pattern dataset. + * @param datasetLanguage Index supports "OpenSearch SQL" and "PPL" + */ +Cypress.Commands.add('selectIndexPatternDataset', (datasetLanguage) => { + cy.openDatasetExplorerWindow(); + cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_EXPLORER_WINDOW) + .contains('Index Patterns') + .click(); + cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_EXPLORER_WINDOW) + .contains(INDEX_PATTERN_NAME, { timeout: 10000 }) + .click(); + cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_SELECTOR_NEXT_BUTTON).click(); + cy.selectIndexPatternDatasetLanguage(datasetLanguage); +}); + +/** + * Set search Date range + * @param relativeNumber Relative integer string to set date range + * @param relativeUnit Unit for number. Accepted Units: seconds/Minutes/Hours/Days/Weeks/Months/Years ago/from now + * @example setSearchRelativeDateRange('15', 'years ago') + */ +Cypress.Commands.add('setSearchRelativeDateRange', (relativeNumber, relativeUnit) => { + cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.SEARCH_DATE_PICKER_BUTTON).click(); + cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.SEARCH_DATE_PICKER_RELATIVE_TAB).click(); + cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.SEARCH_DATE_RELATIVE_PICKER_INPUT) + .clear() + .type(relativeNumber); + cy.getElementByTestId( + DATA_EXPLORER_PAGE_ELEMENTS.SEARCH_DATE_RELATIVE_PICKER_UNIT_SELECTOR + ).select(relativeUnit); + cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.QUERY_SUBMIT_BUTTON).click(); +}); + +/** + * Get specific row of DocTable. + * @param rowNumber Integer starts from 0 for the first row + */ +Cypress.Commands.add('getDocTableRow', (rowNumber) => { + return cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DOC_TABLE).get('tbody tr').eq(rowNumber); +}); + +/** + * Get specific field of DocTable. + * @param columnNumber Integer starts from 0 for the first column + * @param rowNumber Integer starts from 0 for the first row + */ +Cypress.Commands.add('getDocTableField', (columnNumber, rowNumber) => { + return cy + .getDocTableRow(rowNumber) + .findElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DOC_TABLE_ROW_FIELD) + .eq(columnNumber); +}); + +/** + * Check the filter pill text matches expectedFilterText. + * @param expectedFilterText expected text in filter pill. + */ +Cypress.Commands.add('checkFilterPillText', (expectedFilterText) => { + cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.GLOBAL_QUERY_EDITOR_FILTER_VALUE, { + timeout: 10000, + }).should('have.text', expectedFilterText); +}); + +/** + * Check the query hit text matches expectedQueryHitText. + * @param expectedQueryHitText expected text for query hits + */ +Cypress.Commands.add('checkQueryHitText', (expectedQueryHitText) => { + cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DISCOVER_QUERY_HITS).should( + 'have.text', + expectedQueryHitText + ); +}); + +/** + * Check for the first Table Field's Filter For and Filter Out button. + * @param isExists Boolean determining if these button should exist + */ +Cypress.Commands.add('checkDocTableFirstFieldFilterForAndOutButton', (isExists) => { + const shouldText = isExists ? 'exist' : 'not.exist'; + cy.getDocTableField(0, 0).within(() => { + cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.TABLE_FIELD_FILTER_FOR_BUTTON).should( + shouldText + ); + cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.TABLE_FIELD_FILTER_OUT_BUTTON).should( + shouldText + ); + }); +}); + +/** + * Check the Doc Table first Field's Filter For button filters the correct value. + */ +Cypress.Commands.add('checkDocTableFirstFieldFilterForButtonFiltersCorrectField', () => { + cy.getDocTableField(0, 0).then(($field) => { + const filterFieldText = $field.find('span span').text(); + $field + .find(`[data-test-subj="${DATA_EXPLORER_PAGE_ELEMENTS.TABLE_FIELD_FILTER_FOR_BUTTON}"]`) + .click(); + cy.checkFilterPillText(filterFieldText); + cy.checkQueryHitText('1'); // checkQueryHitText must be in front of checking first line text to give time for DocTable to update. + cy.getDocTableField(0, 0).find('span span').should('have.text', filterFieldText); + }); + cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.GLOBAL_FILTER_BAR) + .find('[aria-label="Delete"]') + .click(); + cy.checkQueryHitText('10,000'); +}); + +/** + * Check the Doc Table first Field's Filter Out button filters the correct value. + */ +Cypress.Commands.add('checkDocTableFirstFieldFilterOutButtonFiltersCorrectField', () => { + cy.getDocTableField(0, 0).then(($field) => { + const filterFieldText = $field.find('span span').text(); + $field + .find(`[data-test-subj="${DATA_EXPLORER_PAGE_ELEMENTS.TABLE_FIELD_FILTER_OUT_BUTTON}"]`) + .click(); + cy.checkFilterPillText(filterFieldText); + cy.checkQueryHitText('9,999'); // checkQueryHitText must be in front of checking first line text to give time for DocTable to update. + cy.getDocTableField(0, 0).find('span span').should('not.have.text', filterFieldText); + }); + cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.GLOBAL_FILTER_BAR) + .find('[aria-label="Delete"]') + .click(); + cy.checkQueryHitText('10,000'); +}); diff --git a/cypress/utils/dashboards/data_explorer/commands.ts b/cypress/utils/dashboards/data_explorer/commands.ts deleted file mode 100644 index 19a9cf62b3ba..000000000000 --- a/cypress/utils/dashboards/data_explorer/commands.ts +++ /dev/null @@ -1,171 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { DATA_EXPLORER_PAGE_ELEMENTS } from './elements.js'; - -/** - * Click on the New Search button. - */ -Cypress.Commands.add('clickNewSearchButton', () => { - cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.NEW_SEARCH_BUTTON, { timeout: 10000 }) - .should('be.visible') - .click(); -}); - -/** - * Open window to select Dataset - */ -Cypress.Commands.add('openDatasetExplorerWindow', () => { - cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_SELECTOR_BUTTON).click(); - cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.ALL_DATASETS_BUTTON).click(); -}); - -/** - * Select a Time Field in the Dataset Selector - */ -Cypress.Commands.add('selectDatasetTimeField', (timeField) => { - cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_SELECTOR_TIME_SELECTOR).select( - timeField - ); -}); - -/** - * Select a language in the Dataset Selector for Index - */ -Cypress.Commands.add('selectIndexDatasetLanguage', (datasetLanguage) => { - cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_SELECTOR_LANGUAGE_SELECTOR).select( - datasetLanguage - ); - switch (datasetLanguage) { - case 'PPL': - this.selectDatasetTimeField("I don't want to use the time filter"); - break; - } - cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_SELECTOR_SELECT_DATA_BUTTON).click(); -}); - -/** - * Select a language in the Dataset Selector for Index Pattern - */ -Cypress.Commands.add('selectIndexPatternDatasetLanguage', (datasetLanguage) => { - cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_SELECTOR_LANGUAGE_SELECTOR).select( - datasetLanguage - ); - cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_SELECTOR_SELECT_DATA_BUTTON).click(); -}); - -/** - * Select an index dataset. - */ -Cypress.Commands.add('selectIndexDataset', (datasetLanguage) => { - this.openDatasetExplorerWindow(); - cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_EXPLORER_WINDOW) - .contains('Indexes') - .click(); - cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_EXPLORER_WINDOW) - .contains(Cypress.env('INDEX_CLUSTER_NAME'), { timeout: 10000 }) - .click(); - cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_EXPLORER_WINDOW) - .contains(Cypress.env('INDEX_NAME'), { timeout: 10000 }) - .click(); - cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_SELECTOR_NEXT_BUTTON).click(); - this.selectIndexDatasetLanguage(datasetLanguage); -}); - -/** - * Select an index pattern dataset. - */ -Cypress.Commands.add('selectIndexPatternDataset', (datasetLanguage) => { - this.openDatasetExplorerWindow(); - cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_EXPLORER_WINDOW) - .contains('Index Patterns') - .click(); - cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_EXPLORER_WINDOW) - .contains(Cypress.env('INDEX_PATTERN_NAME'), { timeout: 10000 }) - .click(); - cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_SELECTOR_NEXT_BUTTON).click(); - this.selectIndexPatternDatasetLanguage(datasetLanguage); -}); - -/** - * set search Date range - */ -Cypress.Commands.add('setSearchRelativeDateRange', (relativeNumber, relativeUnit) => { - cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.SEARCH_DATE_PICKER_BUTTON).click(); - cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.SEARCH_DATE_PICKER_RELATIVE_TAB).click(); - cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.SEARCH_DATE_RELATIVE_PICKER_INPUT) - .clear() - .type(relativeNumber); - cy.getElementByTestId( - DATA_EXPLORER_PAGE_ELEMENTS.SEARCH_DATE_RELATIVE_PICKER_UNIT_SELECTOR - ).select(relativeUnit); - cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.QUERY_SUBMIT_BUTTON).click(); -}); - -/** - * check for the first Table Field's Filter For and Filter Out button. - */ -Cypress.Commands.add('checkDocTableFirstFieldFilterForAndOutButton', (isExists) => { - const shouldText = isExists ? 'exist' : 'not.exist'; - cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DOC_TABLE) - .get('tbody tr') - .first() - .within(() => { - cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.TABLE_FIELD_FILTER_FOR_BUTTON).should( - shouldText - ); - cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.TABLE_FIELD_FILTER_OUT_BUTTON).should( - shouldText - ); - }); -}); - -/** - * Check the Doc Table first Field's Filter For button filters the correct value. - */ -Cypress.Commands.add('checkDocTableFirstFieldFilterForButtonFiltersCorrectField', () => { - cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DOC_TABLE) - .find('tbody tr') - .first() - .findElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DOC_TABLE_ROW_FIELD) - .then(($field) => { - const fieldText = $field.find('span span').text(); - $field - .findElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.TABLE_FIELD_FILTER_FOR_BUTTON) - .trigger(click); - cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.GLOBAL_QUERY_EDITOR_FILTER_VALUE, { - timeout: 10000, - }).should('have.text', fieldText); - cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DOC_TABLE) - .find('tbody tr') - .first() - .findElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DOC_TABLE_ROW_FIELD) - .find('span span') - .should('have.text', fieldText); - cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DISCOVER_QUERY_HITS).should( - 'have.text', - '1' - ); - }); -}); - -/** - * Check the Doc Table first Field's Filter Out button filters the correct value. - */ -Cypress.Commands.add('checkDocTableFirstFieldFilterOutButtonFiltersCorrectField', () => { - cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DOC_TABLE) - .find('tbody tr') - .first() - .findElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DOC_TABLE_ROW_FIELD) - .then(($field) => { - const fieldText = $field.find('span span').text(); - $field - .findElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.TABLE_FIELD_FILTER_OUT_BUTTON) - .trigger(click); - cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.GLOBAL_QUERY_EDITOR_FILTER_VALUE, { - timeout: 10000, - }).should('have.text', fieldText); - }); -}); diff --git a/cypress/utils/dashboards/data_explorer/constants.js b/cypress/utils/dashboards/data_explorer/constants.js new file mode 100644 index 000000000000..657e3201f680 --- /dev/null +++ b/cypress/utils/dashboards/data_explorer/constants.js @@ -0,0 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export const INDEX_CLUSTER_NAME = 'cypress-test-os'; +export const INDEX_NAME = 'vis-builder'; +export const INDEX_PATTERN_NAME = 'cypress-test-os::vis-builder*'; diff --git a/cypress/utils/dashboards/data_explorer/elements.ts b/cypress/utils/dashboards/data_explorer/elements.js similarity index 96% rename from cypress/utils/dashboards/data_explorer/elements.ts rename to cypress/utils/dashboards/data_explorer/elements.js index 5b28bbef59cb..0ac45ad63b0c 100644 --- a/cypress/utils/dashboards/data_explorer/elements.ts +++ b/cypress/utils/dashboards/data_explorer/elements.js @@ -23,4 +23,5 @@ export const DATA_EXPLORER_PAGE_ELEMENTS = { SEARCH_DATE_RELATIVE_PICKER_UNIT_SELECTOR: 'superDatePickerRelativeDateInputUnitSelector', QUERY_SUBMIT_BUTTON: 'querySubmitButton', GLOBAL_QUERY_EDITOR_FILTER_VALUE: 'globalFilterLabelValue', + GLOBAL_FILTER_BAR: 'globalFilterBar', }; diff --git a/src/plugins/data/public/ui/filter_bar/filter_bar.tsx b/src/plugins/data/public/ui/filter_bar/filter_bar.tsx index 822f962698e2..26fb97606001 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_bar.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_bar.tsx @@ -78,7 +78,12 @@ function FilterBarUI(props: Props) { function renderItems() { return props.filters.map((filter, i) => ( - + Date: Thu, 5 Dec 2024 00:33:39 -0800 Subject: [PATCH 59/80] Fix filter_label.test.tsx failing due to added data-test-subj Signed-off-by: Federico Silva --- .../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 bfa7e84cb142397ccb5f52d4daf5954b75231b52 Mon Sep 17 00:00:00 2001 From: yuboluo Date: Thu, 5 Dec 2024 17:06:58 +0800 Subject: [PATCH 60/80] [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> Signed-off-by: Federico Silva --- .../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 db6780fd41bfddca17b8fc10a5175b8a5df31858 Mon Sep 17 00:00:00 2001 From: Joshua Li Date: Thu, 5 Dec 2024 09:04:14 -0800 Subject: [PATCH 61/80] 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> Signed-off-by: Federico Silva --- 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 e9aec0dcd4a96e686615bf63bb99620aa81002b7 Mon Sep 17 00:00:00 2001 From: Argus Li Date: Thu, 5 Dec 2024 12:12:18 -0800 Subject: [PATCH 62/80] Address comments. Change testIsolation to be false, ignore uncaught errors to be more selective. Signed-off-by: Federico Silva --- 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 4e884c562d1e71c1bde041b7947af79e909f1db2 Mon Sep 17 00:00:00 2001 From: Joshua Li Date: Thu, 5 Dec 2024 10:32:08 -0800 Subject: [PATCH 63/80] [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> Signed-off-by: Federico Silva --- 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 f85a93b25293d91fcdbee8b815f82f70a9547b30 Mon Sep 17 00:00:00 2001 From: Argus Li Date: Thu, 5 Dec 2024 15:36:14 -0800 Subject: [PATCH 64/80] Reformat to use POM. Search bar objects that are non-page specific have been kept as commands. Signed-off-by: Federico Silva --- .../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 1ff3841768f1e8c89eec72d2504a8b14f8503ea6 Mon Sep 17 00:00:00 2001 From: Federico Silva Date: Fri, 6 Dec 2024 15:24:00 -0300 Subject: [PATCH 65/80] first sidebar test Signed-off-by: Federico Silva --- .../sidebar_test_spec.js | 62 +++++++++++++++++++ cypress/utils/commands.js | 18 ++++++ .../data_explorer/data_explorer_page.po.js | 43 +++++++++++++ .../dashboards/data_explorer/elements.js | 3 + .../ui/query_editor/language_selector.tsx | 1 + 5 files changed, 127 insertions(+) create mode 100644 cypress/integration/core_opensearch_dashboards/sidebar_test_spec.js diff --git a/cypress/integration/core_opensearch_dashboards/sidebar_test_spec.js b/cypress/integration/core_opensearch_dashboards/sidebar_test_spec.js new file mode 100644 index 000000000000..31d9782c8a26 --- /dev/null +++ b/cypress/integration/core_opensearch_dashboards/sidebar_test_spec.js @@ -0,0 +1,62 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { MiscUtils } from '@opensearch-dashboards-test/opensearch-dashboards-test-library'; +import { DataExplorerPage } from '../../utils/dashboards/data_explorer/data_explorer_page.po'; + +const miscUtils = new MiscUtils(cy); + +describe('sidebar spec', () => { + before(() => { + cy.localLogin(Cypress.env('username'), Cypress.env('password')); + miscUtils.visitPage('app/data-explorer/discover'); + }); + + it('add field: DQL', () => { + DataExplorerPage.setSearchRelativeDateRange('15', 'Years ago'); + + it('check default field name', () => { + cy.intercept('/internal/search/opensearch-with-long-numerals').as('data'); + DataExplorerPage.selectIndexPatternDataset('DQL'); + cy.wait('@data').then(() => { + DataExplorerPage.getDocTableHeader(1).should('have.text', '_source'); + }); + }); + + const testFields = ['_id', 'age', 'birthdate', 'salary']; + + it('add some fields and check default field is gone', () => { + testFields.forEach((field) => { + DataExplorerPage.getFieldBtnByName(field).click(); + }); + + DataExplorerPage.getDocTableHeader(1).should('not.have.text', '_source'); + }); + + it('check headers persistence between DQL and PPL', () => { + DataExplorerPage.checkTableHeadersByArray(testFields); + DataExplorerPage.setQueryEditorLanguage('PPL'); + DataExplorerPage.checkTableHeadersByArray(testFields); + }); + + it('remove two fields', () => { + const firstTestField = testFields[0]; + const secondTestField = testFields[1]; + DataExplorerPage.getFieldBtnByName(firstTestField).click(); + DataExplorerPage.getFieldBtnByName(secondTestField).click(); + DataExplorerPage.getDocTableHeader(1).should('not.have.text', firstTestField); + DataExplorerPage.getDocTableHeader(2).should('not.have.text', secondTestField); + }); + + it('remove remaining fields (all)', () => { + const thirdTestField = testFields[2]; + const fourthTestField = testFields[3]; + DataExplorerPage.getFieldBtnByName(thirdTestField).click(); + DataExplorerPage.getFieldBtnByName(fourthTestField).click(); + DataExplorerPage.getDocTableHeader(1).should('have.text', '_source'); + DataExplorerPage.getDocTableHeader(2).should('not.exist'); + }); + }); +}); diff --git a/cypress/utils/commands.js b/cypress/utils/commands.js index d30308576a80..0e5f6e92486f 100644 --- a/cypress/utils/commands.js +++ b/cypress/utils/commands.js @@ -13,11 +13,29 @@ const loginPage = new LoginPage(cy); /** * Get DOM element by data-test-subj id. + * @param testId data-test-subj value. + * @param options cy.get() options. Default: {} */ Cypress.Commands.add('getElementByTestId', (testId, options = {}) => { return cy.get(`[data-test-subj="${testId}"]`, options); }); +/** + * Get DOM element by partial data-test-subj id. + * @param testId data-test-subj value. + * @param options cy.get() options. Default: {} + * @comparisonType choose a partial data-test-subj comparison type. Accepted values: 'beginning', 'ending', 'substring'. + */ +Cypress.Commands.add('getElementByTestIdLike', (testId, comparisonType, options = {}) => { + const comparison = { + beginning: '^', + ending: '$', + substring: '*', + }; + const chosenType = comparison[comparisonType] || ''; + return cy.get(`[data-test-subj${chosenType}="${testId}"]`, options); +}); + /** * Get multiple DOM elements by data-test-subj ids. */ diff --git a/cypress/utils/dashboards/data_explorer/data_explorer_page.po.js b/cypress/utils/dashboards/data_explorer/data_explorer_page.po.js index f1d2f30605a2..2474604a5f06 100644 --- a/cypress/utils/dashboards/data_explorer/data_explorer_page.po.js +++ b/cypress/utils/dashboards/data_explorer/data_explorer_page.po.js @@ -56,6 +56,14 @@ export class DataExplorerPage { return cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DATASET_SELECTOR_NEXT_BUTTON); } + /** + * Get specific DocTable column header. + * @param index Integer starts from 0 for the first column header. + */ + static getDocTableHeader(index) { + return cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DOC_TABLE_HEADER_FIELD).eq(index); + } + /** * Get specific row of DocTable. * @param rowNumber Integer starts from 0 for the first row @@ -63,6 +71,7 @@ export class DataExplorerPage { static getDocTableRow(rowNumber) { return cy .getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DOC_TABLE) + .get('tbody tr') .eq(rowNumber); } @@ -75,9 +84,23 @@ export class DataExplorerPage { static getDocTableField(columnNumber, rowNumber) { return DataExplorerPage.getDocTableRow(rowNumber) .findElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DOC_TABLE_ROW_FIELD) + .eq(columnNumber); } + /** + * Set the query editor language + * @param language Accepted values: 'DQL', 'Lucene', 'OpenSearch SQL', 'PPL' + */ + static setQueryEditorLanguage(language) { + cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.QUERY_EDITOR_LANGUAGE_SELECTOR).click(); + + cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.QUERY_EDITOR_LANGUAGE_OPTIONS) + .find('button') + .contains(language) + .click(); + } + /** * Get filter pill value. */ @@ -115,6 +138,20 @@ export class DataExplorerPage { return cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.GLOBAL_FILTER_BAR); } + static getSidebarFieldByName() {} + + /** + * Get sidebar add field button. + * @param index Integer that starts at 0 for the first add button. + */ + static getFieldBtnByIndex(index) { + return cy.getElementByTestIdLike('fieldToggle-', 'beginning').eq(index); + } + + static getFieldBtnByName(name) { + return cy.getElementByTestId('fieldToggle-' + name); + } + /** * Open window to select Dataset */ @@ -262,4 +299,10 @@ export class DataExplorerPage { DataExplorerPage.getFilterBar().find('[aria-label="Delete"]').click(); DataExplorerPage.checkQueryHitsText('10,000'); } + + static checkTableHeadersByArray(arr) { + for (let i = 0; i < arr.length; i++) { + DataExplorerPage.getDocTableHeader(i + 1).should('have.text', arr[i]); + } + } } diff --git a/cypress/utils/dashboards/data_explorer/elements.js b/cypress/utils/dashboards/data_explorer/elements.js index 0ac45ad63b0c..8fb5c3338e69 100644 --- a/cypress/utils/dashboards/data_explorer/elements.js +++ b/cypress/utils/dashboards/data_explorer/elements.js @@ -14,6 +14,7 @@ export const DATA_EXPLORER_PAGE_ELEMENTS = { DATASET_SELECTOR_TIME_SELECTOR: 'advancedSelectorTimeFieldSelect', DATASET_SELECTOR_SELECT_DATA_BUTTON: 'advancedSelectorConfirmButton', DOC_TABLE: 'docTable', + DOC_TABLE_HEADER_FIELD: 'docTableHeaderField', DOC_TABLE_ROW_FIELD: 'docTableField', TABLE_FIELD_FILTER_FOR_BUTTON: 'filterForValue', TABLE_FIELD_FILTER_OUT_BUTTON: 'filterOutValue', @@ -21,6 +22,8 @@ export const DATA_EXPLORER_PAGE_ELEMENTS = { SEARCH_DATE_PICKER_RELATIVE_TAB: 'superDatePickerRelativeTab', SEARCH_DATE_RELATIVE_PICKER_INPUT: 'superDatePickerRelativeDateInputNumber', SEARCH_DATE_RELATIVE_PICKER_UNIT_SELECTOR: 'superDatePickerRelativeDateInputUnitSelector', + QUERY_EDITOR_LANGUAGE_SELECTOR: 'queryEditorLanguageSelector', + QUERY_EDITOR_LANGUAGE_OPTIONS: 'queryEditorLanguageOptions', QUERY_SUBMIT_BUTTON: 'querySubmitButton', GLOBAL_QUERY_EDITOR_FILTER_VALUE: 'globalFilterLabelValue', GLOBAL_FILTER_BAR: 'globalFilterBar', diff --git a/src/plugins/data/public/ui/query_editor/language_selector.tsx b/src/plugins/data/public/ui/query_editor/language_selector.tsx index 3047cefda31b..0177ede6f7fb 100644 --- a/src/plugins/data/public/ui/query_editor/language_selector.tsx +++ b/src/plugins/data/public/ui/query_editor/language_selector.tsx @@ -151,6 +151,7 @@ export const QueryLanguageSelector = (props: QueryLanguageSelectorProps) => { )} size="s" items={languageOptionsMenu} + data-test-subj="queryEditorLanguageOptions" /> ); From 9d5d6e97e0dbbe771fb6686d66158179740db0d2 Mon Sep 17 00:00:00 2001 From: Federico Silva Date: Fri, 6 Dec 2024 15:27:19 -0300 Subject: [PATCH 66/80] fix syntax error uncaught by eslint Signed-off-by: Federico Silva --- .../sidebar_test_spec.js | 60 ++++++++----------- 1 file changed, 25 insertions(+), 35 deletions(-) diff --git a/cypress/integration/core_opensearch_dashboards/sidebar_test_spec.js b/cypress/integration/core_opensearch_dashboards/sidebar_test_spec.js index 31d9782c8a26..bff9ed0669af 100644 --- a/cypress/integration/core_opensearch_dashboards/sidebar_test_spec.js +++ b/cypress/integration/core_opensearch_dashboards/sidebar_test_spec.js @@ -17,46 +17,36 @@ describe('sidebar spec', () => { it('add field: DQL', () => { DataExplorerPage.setSearchRelativeDateRange('15', 'Years ago'); - it('check default field name', () => { - cy.intercept('/internal/search/opensearch-with-long-numerals').as('data'); - DataExplorerPage.selectIndexPatternDataset('DQL'); - cy.wait('@data').then(() => { - DataExplorerPage.getDocTableHeader(1).should('have.text', '_source'); - }); + cy.intercept('/internal/search/opensearch-with-long-numerals').as('data'); + DataExplorerPage.selectIndexPatternDataset('DQL'); + cy.wait('@data').then(() => { + DataExplorerPage.getDocTableHeader(1).should('have.text', '_source'); }); const testFields = ['_id', 'age', 'birthdate', 'salary']; - it('add some fields and check default field is gone', () => { - testFields.forEach((field) => { - DataExplorerPage.getFieldBtnByName(field).click(); - }); - - DataExplorerPage.getDocTableHeader(1).should('not.have.text', '_source'); - }); - - it('check headers persistence between DQL and PPL', () => { - DataExplorerPage.checkTableHeadersByArray(testFields); - DataExplorerPage.setQueryEditorLanguage('PPL'); - DataExplorerPage.checkTableHeadersByArray(testFields); + testFields.forEach((field) => { + DataExplorerPage.getFieldBtnByName(field).click(); }); - it('remove two fields', () => { - const firstTestField = testFields[0]; - const secondTestField = testFields[1]; - DataExplorerPage.getFieldBtnByName(firstTestField).click(); - DataExplorerPage.getFieldBtnByName(secondTestField).click(); - DataExplorerPage.getDocTableHeader(1).should('not.have.text', firstTestField); - DataExplorerPage.getDocTableHeader(2).should('not.have.text', secondTestField); - }); - - it('remove remaining fields (all)', () => { - const thirdTestField = testFields[2]; - const fourthTestField = testFields[3]; - DataExplorerPage.getFieldBtnByName(thirdTestField).click(); - DataExplorerPage.getFieldBtnByName(fourthTestField).click(); - DataExplorerPage.getDocTableHeader(1).should('have.text', '_source'); - DataExplorerPage.getDocTableHeader(2).should('not.exist'); - }); + DataExplorerPage.getDocTableHeader(1).should('not.have.text', '_source'); + + DataExplorerPage.checkTableHeadersByArray(testFields); + DataExplorerPage.setQueryEditorLanguage('PPL'); + DataExplorerPage.checkTableHeadersByArray(testFields); + + const firstTestField = testFields[0]; + const secondTestField = testFields[1]; + DataExplorerPage.getFieldBtnByName(firstTestField).click(); + DataExplorerPage.getFieldBtnByName(secondTestField).click(); + DataExplorerPage.getDocTableHeader(1).should('not.have.text', firstTestField); + DataExplorerPage.getDocTableHeader(2).should('not.have.text', secondTestField); + + const thirdTestField = testFields[2]; + const fourthTestField = testFields[3]; + DataExplorerPage.getFieldBtnByName(thirdTestField).click(); + DataExplorerPage.getFieldBtnByName(fourthTestField).click(); + DataExplorerPage.getDocTableHeader(1).should('have.text', '_source'); + DataExplorerPage.getDocTableHeader(2).should('not.exist'); }); }); From a7f1901662183aba0a25a86d1ad0d194c2ec9a43 Mon Sep 17 00:00:00 2001 From: Federico Silva Date: Fri, 6 Dec 2024 18:19:59 -0300 Subject: [PATCH 67/80] query support Signed-off-by: Federico Silva --- .../sidebar_test_spec.js | 4 ++++ .../data_explorer/data_explorer_page.po.js | 24 +++++++++++++++++-- .../dashboards/data_explorer/elements.js | 1 + 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/cypress/integration/core_opensearch_dashboards/sidebar_test_spec.js b/cypress/integration/core_opensearch_dashboards/sidebar_test_spec.js index bff9ed0669af..001b122d6b34 100644 --- a/cypress/integration/core_opensearch_dashboards/sidebar_test_spec.js +++ b/cypress/integration/core_opensearch_dashboards/sidebar_test_spec.js @@ -48,5 +48,9 @@ describe('sidebar spec', () => { DataExplorerPage.getFieldBtnByName(fourthTestField).click(); DataExplorerPage.getDocTableHeader(1).should('have.text', '_source'); DataExplorerPage.getDocTableHeader(2).should('not.exist'); + + DataExplorerPage.clearQueryMultilineEditor(); + DataExplorerPage.getQueryMultilineEditor().type('source = vis-builder* | where age > 40'); + DataExplorerPage.getQuerySubmitBtn().click(); }); }); diff --git a/cypress/utils/dashboards/data_explorer/data_explorer_page.po.js b/cypress/utils/dashboards/data_explorer/data_explorer_page.po.js index 2474604a5f06..c6c6eaa60fc5 100644 --- a/cypress/utils/dashboards/data_explorer/data_explorer_page.po.js +++ b/cypress/utils/dashboards/data_explorer/data_explorer_page.po.js @@ -88,6 +88,28 @@ export class DataExplorerPage { .eq(columnNumber); } + /** + * Get query multiline editor element. + */ + static getQueryMultilineEditor() { + return cy + .getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.QUERY_EDITOR_MULTILINE) + .find('textarea'); + } + + static getQuerySubmitBtn() { + return cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.QUERY_SUBMIT_BUTTON); + } + + static clearQueryMultilineEditor() { + DataExplorerPage.getQueryMultilineEditor() + .invoke('val') + .then(function ($content) { + const contentLen = $content.length; + DataExplorerPage.getQueryMultilineEditor().type('{del}'.repeat(contentLen)); + }); + } + /** * Set the query editor language * @param language Accepted values: 'DQL', 'Lucene', 'OpenSearch SQL', 'PPL' @@ -138,8 +160,6 @@ export class DataExplorerPage { return cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.GLOBAL_FILTER_BAR); } - static getSidebarFieldByName() {} - /** * Get sidebar add field button. * @param index Integer that starts at 0 for the first add button. diff --git a/cypress/utils/dashboards/data_explorer/elements.js b/cypress/utils/dashboards/data_explorer/elements.js index 8fb5c3338e69..84ea6db46b82 100644 --- a/cypress/utils/dashboards/data_explorer/elements.js +++ b/cypress/utils/dashboards/data_explorer/elements.js @@ -25,6 +25,7 @@ export const DATA_EXPLORER_PAGE_ELEMENTS = { QUERY_EDITOR_LANGUAGE_SELECTOR: 'queryEditorLanguageSelector', QUERY_EDITOR_LANGUAGE_OPTIONS: 'queryEditorLanguageOptions', QUERY_SUBMIT_BUTTON: 'querySubmitButton', + QUERY_EDITOR_MULTILINE: 'osdQueryEditor__multiLine', GLOBAL_QUERY_EDITOR_FILTER_VALUE: 'globalFilterLabelValue', GLOBAL_FILTER_BAR: 'globalFilterBar', }; From baa41c287aaf9b599ca00e1a197128a598e6d083 Mon Sep 17 00:00:00 2001 From: Lin Wang Date: Fri, 6 Dec 2024 09:24:20 +0800 Subject: [PATCH 68/80] [Workspace]Fix error toasts in sample data page (#8842) * Set default index pattern when workspace disabled Signed-off-by: Lin Wang * Move saved objects first to avoid partial deleted Signed-off-by: Lin Wang * Skip ui setting update for non workspace admin Signed-off-by: Lin Wang * Add UT for sample_data_client Signed-off-by: Lin Wang * Changeset file for PR #8842 created/updated --------- Signed-off-by: Lin Wang Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> Signed-off-by: Federico Silva --- changelogs/fragments/8842.yml | 2 + .../opensearch_dashboards_services.ts | 2 + .../public/application/sample_data_client.js | 18 +- .../application/sample_data_client.test.js | 167 ++++++++++++++++++ src/plugins/home/public/plugin.ts | 1 + .../services/sample_data/routes/uninstall.ts | 48 ++--- 6 files changed, 215 insertions(+), 23 deletions(-) create mode 100644 changelogs/fragments/8842.yml create mode 100644 src/plugins/home/public/application/sample_data_client.test.js diff --git a/changelogs/fragments/8842.yml b/changelogs/fragments/8842.yml new file mode 100644 index 000000000000..b9973f347f9e --- /dev/null +++ b/changelogs/fragments/8842.yml @@ -0,0 +1,2 @@ +fix: +- [Workspace]Fix error toasts in sample data page ([#8842](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8842)) \ No newline at end of file diff --git a/src/plugins/home/public/application/opensearch_dashboards_services.ts b/src/plugins/home/public/application/opensearch_dashboards_services.ts index 1107e46ecf2e..eb4b085d86ae 100644 --- a/src/plugins/home/public/application/opensearch_dashboards_services.ts +++ b/src/plugins/home/public/application/opensearch_dashboards_services.ts @@ -37,6 +37,7 @@ import { SavedObjectsClientContract, IUiSettingsClient, ApplicationStart, + WorkspacesSetup, } from 'opensearch-dashboards/public'; import { UiStatsMetricType } from '@osd/analytics'; import { TelemetryPluginStart } from '../../../telemetry/public'; @@ -77,6 +78,7 @@ export interface HomeOpenSearchDashboardsServices { }; dataSource?: DataSourcePluginStart; sectionTypes: SectionTypeService; + workspaces?: WorkspacesSetup; } let services: HomeOpenSearchDashboardsServices | null = null; diff --git a/src/plugins/home/public/application/sample_data_client.js b/src/plugins/home/public/application/sample_data_client.js index 045736c428f6..b2adaf44cf81 100644 --- a/src/plugins/home/public/application/sample_data_client.js +++ b/src/plugins/home/public/application/sample_data_client.js @@ -41,11 +41,26 @@ export async function listSampleDataSets(dataSourceId) { return await getServices().http.get(sampleDataUrl, { query }); } +const canUpdateUISetting = () => { + const { + application: { capabilities }, + workspaces, + } = getServices(); + if ( + capabilities.workspaces && + capabilities.workspaces.enabled && + capabilities.workspaces.permissionEnabled + ) { + return !!workspaces?.currentWorkspace$.getValue()?.owner; + } + return true; +}; + export async function installSampleDataSet(id, sampleDataDefaultIndex, dataSourceId) { const query = buildQuery(dataSourceId); await getServices().http.post(`${sampleDataUrl}/${id}`, { query }); - if (getServices().uiSettings.isDefault('defaultIndex')) { + if (canUpdateUISetting() && getServices().uiSettings.isDefault('defaultIndex')) { getServices().uiSettings.set('defaultIndex', sampleDataDefaultIndex); } @@ -59,6 +74,7 @@ export async function uninstallSampleDataSet(id, sampleDataDefaultIndex, dataSou const uiSettings = getServices().uiSettings; if ( + canUpdateUISetting() && !uiSettings.isDefault('defaultIndex') && uiSettings.get('defaultIndex') === sampleDataDefaultIndex ) { diff --git a/src/plugins/home/public/application/sample_data_client.test.js b/src/plugins/home/public/application/sample_data_client.test.js new file mode 100644 index 000000000000..35f86efef729 --- /dev/null +++ b/src/plugins/home/public/application/sample_data_client.test.js @@ -0,0 +1,167 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { BehaviorSubject } from 'rxjs'; +import { setServices } from '../application/opensearch_dashboards_services'; +import { installSampleDataSet, uninstallSampleDataSet } from './sample_data_client'; + +const mockHttp = { + post: jest.fn(), + delete: jest.fn(), +}; + +const mockUiSettings = { + isDefault: jest.fn(), + set: jest.fn(), + get: jest.fn(), +}; + +const mockApplication = { + capabilities: { + workspaces: { + enabled: false, + permissionEnabled: false, + }, + }, +}; + +const mockIndexPatternService = { + clearCache: jest.fn(), +}; + +const mockWorkspace = { + currentWorkspace$: new BehaviorSubject(), +}; + +const mockServices = { + workspaces: mockWorkspace, + http: mockHttp, + uiSettings: mockUiSettings, + application: mockApplication, + indexPatternService: mockIndexPatternService, +}; + +setServices(mockServices); + +describe('installSampleDataSet', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockUiSettings.isDefault.mockReturnValue(true); + setServices(mockServices); + }); + + it('should install the sample data set and set the default index', async () => { + const id = 'sample-data-id'; + const sampleDataDefaultIndex = 'sample-data-index'; + const dataSourceId = 'data-source-id'; + + await installSampleDataSet(id, sampleDataDefaultIndex, dataSourceId); + + expect(mockHttp.post).toHaveBeenCalledWith(`/api/sample_data/${id}`, { + query: expect.anything(), + }); + expect(mockUiSettings.set).toHaveBeenCalledWith('defaultIndex', sampleDataDefaultIndex); + expect(mockIndexPatternService.clearCache).toHaveBeenCalled(); + }); + + it('should install the sample data set and not set the default index when workspace is enabled', async () => { + const id = 'sample-data-id'; + const sampleDataDefaultIndex = 'sample-data-index'; + const dataSourceId = 'data-source-id'; + + setServices({ + ...mockServices, + workspaces: { + currentWorkspace$: new BehaviorSubject(), + }, + application: { + capabilities: { + workspaces: { + enabled: true, + permissionEnabled: true, + }, + }, + }, + }); + + await installSampleDataSet(id, sampleDataDefaultIndex, dataSourceId); + + expect(mockHttp.post).toHaveBeenCalledWith(`/api/sample_data/${id}`, { + query: expect.anything(), + }); + expect(mockUiSettings.set).not.toHaveBeenCalled(); + expect(mockIndexPatternService.clearCache).toHaveBeenCalled(); + }); +}); + +describe('uninstallSampleDataSet', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockUiSettings.isDefault.mockReturnValue(false); + setServices(mockServices); + }); + + it('should uninstall the sample data set and clear the default index', async () => { + const id = 'sample-data-id'; + const sampleDataDefaultIndex = 'sample-data-index'; + const dataSourceId = 'data-source-id'; + + mockUiSettings.get.mockReturnValue(sampleDataDefaultIndex); + + await uninstallSampleDataSet(id, sampleDataDefaultIndex, dataSourceId); + + expect(mockHttp.delete).toHaveBeenCalledWith(`/api/sample_data/${id}`, { + query: expect.anything(), + }); + expect(mockUiSettings.set).toHaveBeenCalledWith('defaultIndex', null); + expect(mockIndexPatternService.clearCache).toHaveBeenCalled(); + }); + + it('should uninstall the sample data set and not clear the default index when workspace is enabled', async () => { + const id = 'sample-data-id'; + const sampleDataDefaultIndex = 'sample-data-index'; + const dataSourceId = 'data-source-id'; + + setServices({ + ...mockServices, + workspaces: { + currentWorkspace$: new BehaviorSubject(), + }, + application: { + capabilities: { + workspaces: { + enabled: true, + permissionEnabled: true, + }, + }, + }, + }); + + await uninstallSampleDataSet(id, sampleDataDefaultIndex, dataSourceId); + + expect(mockHttp.delete).toHaveBeenCalledWith(`/api/sample_data/${id}`, { + query: expect.anything(), + }); + expect(mockUiSettings.set).not.toHaveBeenCalled(); + expect(mockIndexPatternService.clearCache).toHaveBeenCalled(); + }); + + it('should uninstall the sample data set and not clear the default index when it is not the sample data index', async () => { + const id = 'sample-data-id'; + const sampleDataDefaultIndex = 'sample-data-index'; + const dataSourceId = 'data-source-id'; + + mockUiSettings.isDefault.mockReturnValue(false); + mockUiSettings.get.mockReturnValue('other-index'); + + await uninstallSampleDataSet(id, sampleDataDefaultIndex, dataSourceId); + + expect(mockHttp.delete).toHaveBeenCalledWith(`/api/sample_data/${id}`, { + query: expect.anything(), + }); + expect(mockUiSettings.set).not.toHaveBeenCalled(); + expect(mockIndexPatternService.clearCache).toHaveBeenCalled(); + }); +}); diff --git a/src/plugins/home/public/plugin.ts b/src/plugins/home/public/plugin.ts index 435c7d4d3b9f..6d9771c724ef 100644 --- a/src/plugins/home/public/plugin.ts +++ b/src/plugins/home/public/plugin.ts @@ -156,6 +156,7 @@ export class HomePublicPlugin injectedMetadata: coreStart.injectedMetadata, dataSource, sectionTypes: this.sectionTypeService, + workspaces: core.workspaces, ...homeOpenSearchDashboardsServices, }); }; diff --git a/src/plugins/home/server/services/sample_data/routes/uninstall.ts b/src/plugins/home/server/services/sample_data/routes/uninstall.ts index 3e4636c32486..da8dea3c2fe3 100644 --- a/src/plugins/home/server/services/sample_data/routes/uninstall.ts +++ b/src/plugins/home/server/services/sample_data/routes/uninstall.ts @@ -62,27 +62,10 @@ export function createUninstallRoute( return response.notFound(); } - const caller = dataSourceId - ? context.dataSource.opensearch.legacy.getClient(dataSourceId).callAPI - : context.core.opensearch.legacy.client.callAsCurrentUser; - - for (let i = 0; i < sampleDataset.dataIndices.length; i++) { - const dataIndexConfig = sampleDataset.dataIndices[i]; - const index = - dataIndexConfig.indexName ?? createIndexName(sampleDataset.id, dataIndexConfig.id); - - try { - await caller('indices.delete', { index }); - } catch (err) { - return response.customError({ - statusCode: err.status, - body: { - message: `Unable to delete sample data index "${index}", error: ${err.message}`, - }, - }); - } - } - + /** + * Delete saved objects before removing the data index to avoid partial deletion + * of sample data when a read-only workspace user attempts to remove sample data. + */ const savedObjectsList = getFinalSavedObjects({ dataset: sampleDataset, workspaceId, @@ -99,7 +82,7 @@ export function createUninstallRoute( // ignore 404s since users could have deleted some of the saved objects via the UI if (_.get(err, 'output.statusCode') !== 404) { return response.customError({ - statusCode: err.status, + statusCode: err.status || _.get(err, 'output.statusCode'), body: { message: `Unable to delete sample dataset saved objects, error: ${err.message}`, }, @@ -107,6 +90,27 @@ export function createUninstallRoute( } } + const caller = dataSourceId + ? context.dataSource.opensearch.legacy.getClient(dataSourceId).callAPI + : context.core.opensearch.legacy.client.callAsCurrentUser; + + for (let i = 0; i < sampleDataset.dataIndices.length; i++) { + const dataIndexConfig = sampleDataset.dataIndices[i]; + const index = + dataIndexConfig.indexName ?? createIndexName(sampleDataset.id, dataIndexConfig.id); + + try { + await caller('indices.delete', { index }); + } catch (err) { + return response.customError({ + statusCode: err.status, + body: { + message: `Unable to delete sample data index "${index}", error: ${err.message}`, + }, + }); + } + } + // track the usage operation in a non-blocking way usageTracker.addUninstall(request.params.id); From f160f236b31867664df4651062ecca785e28dfd1 Mon Sep 17 00:00:00 2001 From: Justin Kim Date: Fri, 6 Dec 2024 12:37:55 -0800 Subject: [PATCH 69/80] fix: update the osd-plugin-generator template to reference the correct button (#9014) * fix: update the osd-plugin-generator template to reference the correct button Signed-off-by: Justin Kim * remove the size prop Signed-off-by: Justin Kim * change it to EuiButton Signed-off-by: Justin Kim --------- Signed-off-by: Justin Kim Signed-off-by: Federico Silva --- .../osd-plugin-generator/template/public/components/app.tsx.ejs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/osd-plugin-generator/template/public/components/app.tsx.ejs b/packages/osd-plugin-generator/template/public/components/app.tsx.ejs index 2029a69dd8db..876b3f8c5e75 100644 --- a/packages/osd-plugin-generator/template/public/components/app.tsx.ejs +++ b/packages/osd-plugin-generator/template/public/components/app.tsx.ejs @@ -4,7 +4,7 @@ import { FormattedMessage, I18nProvider } from '@osd/i18n/react'; import { BrowserRouter as Router } from 'react-router-dom'; import { -EuiSmallButton, +EuiButton, EuiHorizontalRule, EuiPage, EuiPageBody, From 8109e977a2a85d0020aafc70d2008f0e773eed83 Mon Sep 17 00:00:00 2001 From: Joshua Li Date: Fri, 6 Dec 2024 13:25:54 -0800 Subject: [PATCH 70/80] [Discover] Support custom logic to insert time filter based on dataset type (#8932) * Pass time filter if language overrides hideDatePicker --------- Signed-off-by: Joshua Li Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> Signed-off-by: Federico Silva --- changelogs/fragments/8932.yml | 2 + .../dataset_service/dataset_service.mock.ts | 3 + .../query_string/dataset_service/types.ts | 9 ++- .../ui/dataset_selector/configurator.test.tsx | 72 +++++++++++++++++-- .../ui/dataset_selector/configurator.tsx | 40 +++++++---- .../query_editor_top_row.test.tsx | 62 ++++++++++++++-- .../ui/query_editor/query_editor_top_row.tsx | 55 +++++++++++--- .../query_enhancements/common/types.ts | 7 +- .../query_enhancements/common/utils.ts | 1 + .../public/search/ppl_search_interceptor.ts | 21 +++++- .../public/search/sql_search_interceptor.ts | 11 +++ .../query_enhancements/server/routes/index.ts | 1 + 12 files changed, 246 insertions(+), 38 deletions(-) create mode 100644 changelogs/fragments/8932.yml diff --git a/changelogs/fragments/8932.yml b/changelogs/fragments/8932.yml new file mode 100644 index 000000000000..a048de0a102a --- /dev/null +++ b/changelogs/fragments/8932.yml @@ -0,0 +1,2 @@ +feat: +- Support custom logic to insert time filter based on dataset type ([#8932](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8932)) \ No newline at end of file diff --git a/src/plugins/data/public/query/query_string/dataset_service/dataset_service.mock.ts b/src/plugins/data/public/query/query_string/dataset_service/dataset_service.mock.ts index df5521078feb..ba491cb51191 100644 --- a/src/plugins/data/public/query/query_string/dataset_service/dataset_service.mock.ts +++ b/src/plugins/data/public/query/query_string/dataset_service/dataset_service.mock.ts @@ -43,6 +43,9 @@ const createSetupDatasetServiceMock = (): jest.Mocked => fetchOptions: jest.fn(), getRecentDatasets: jest.fn(), addRecentDataset: jest.fn(), + clearCache: jest.fn(), + getLastCacheTime: jest.fn(), + removeFromRecentDatasets: jest.fn(), }; }; diff --git a/src/plugins/data/public/query/query_string/dataset_service/types.ts b/src/plugins/data/public/query/query_string/dataset_service/types.ts index 65c322acec6f..d97afec8abb6 100644 --- a/src/plugins/data/public/query/query_string/dataset_service/types.ts +++ b/src/plugins/data/public/query/query_string/dataset_service/types.ts @@ -43,6 +43,13 @@ export interface DatasetTypeConfig { id: string; /** Human-readable title for the dataset type */ title: string; + languageOverrides?: { + [language: string]: { + /** The override transfers the responsibility of handling the input from + * the language interceptor to the dataset type search strategy. */ + hideDatePicker?: boolean; + }; + }; /** Metadata for UI representation */ meta: { /** Icon to represent the dataset type */ @@ -51,7 +58,7 @@ export interface DatasetTypeConfig { tooltip?: string; /** Optional preference for search on page load else defaulted to true */ searchOnLoad?: boolean; - /** Optional supportsTimeFilter determines if a time filter is needed */ + /** Optional supportsTimeFilter determines if a time field is supported */ supportsTimeFilter?: boolean; /** Optional isFieldLoadAsync determines if field loads are async */ isFieldLoadAsync?: boolean; diff --git a/src/plugins/data/public/ui/dataset_selector/configurator.test.tsx b/src/plugins/data/public/ui/dataset_selector/configurator.test.tsx index 462c6298a0a3..38d4e4e12183 100644 --- a/src/plugins/data/public/ui/dataset_selector/configurator.test.tsx +++ b/src/plugins/data/public/ui/dataset_selector/configurator.test.tsx @@ -3,14 +3,14 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { render, screen, fireEvent, waitFor } from '@testing-library/react'; -import { Configurator } from './configurator'; import '@testing-library/jest-dom'; +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import React from 'react'; -import { setQueryService, setIndexPatterns } from '../../services'; import { IntlProvider } from 'react-intl'; -import { Query } from '../../../../data/public'; import { Dataset } from 'src/plugins/data/common'; +import { Query } from '../../../../data/public'; +import { setIndexPatterns, setQueryService } from '../../services'; +import { Configurator } from './configurator'; const getQueryMock = jest.fn().mockReturnValue({ query: '', @@ -358,4 +358,68 @@ describe('Configurator Component', () => { expect(submitButton).toBeEnabled(); }); }); + + it('should show the date picker if supportsTimeFilter is undefined', async () => { + const mockDataset = { + ...mockBaseDataset, + timeFieldName: undefined, + type: 'index', + }; + const { container } = render( + + + + ); + + expect( + container.querySelector(`[data-test-subj="advancedSelectorTimeFieldSelect"]`) + ).toBeTruthy(); + }); + + it('should hide the date picker if supportsTimeFilter is false', async () => { + const mockDataset = { + ...mockBaseDataset, + timeFieldName: undefined, + type: 'index', + }; + const datasetTypeConfig = mockServices + .getQueryService() + .queryString.getDatasetService() + .getType(); + mockServices + .getQueryService() + .queryString.getDatasetService() + .getType.mockReturnValue({ + ...datasetTypeConfig, + meta: { + supportsTimeFilter: false, + }, + }); + const { container } = render( + + + + ); + + expect( + container.querySelector(`[data-test-subj="advancedSelectorTimeFieldSelect"]`) + ).toBeFalsy(); + + mockServices + .getQueryService() + .queryString.getDatasetService() + .getType.mockReturnValue(datasetTypeConfig); + }); }); diff --git a/src/plugins/data/public/ui/dataset_selector/configurator.tsx b/src/plugins/data/public/ui/dataset_selector/configurator.tsx index 0dba9107934c..4906bec2ef84 100644 --- a/src/plugins/data/public/ui/dataset_selector/configurator.tsx +++ b/src/plugins/data/public/ui/dataset_selector/configurator.tsx @@ -69,6 +69,7 @@ export const Configurator = ({ const [selectedIndexedView, setSelectedIndexedView] = useState(); const [indexedViews, setIndexedViews] = useState([]); const [isLoadingIndexedViews, setIsLoadingIndexedViews] = useState(false); + const [timeFieldsLoading, setTimeFieldsLoading] = useState(false); useEffect(() => { let isMounted = true; @@ -91,23 +92,26 @@ export const Configurator = ({ const submitDisabled = useMemo(() => { return ( - timeFieldName === undefined && - !( - languageService.getLanguage(language)?.hideDatePicker || - dataset.type === DEFAULT_DATA.SET_TYPES.INDEX_PATTERN - ) && - timeFields && - timeFields.length > 0 + timeFieldsLoading || + (timeFieldName === undefined && + !(dataset.type === DEFAULT_DATA.SET_TYPES.INDEX_PATTERN) && + timeFields && + timeFields.length > 0) ); - }, [dataset, language, timeFieldName, timeFields, languageService]); + }, [dataset, timeFieldName, timeFields, timeFieldsLoading]); useEffect(() => { const fetchFields = async () => { - const datasetFields = await queryString - .getDatasetService() - .getType(baseDataset.type) - ?.fetchFields(baseDataset); + const datasetType = queryString.getDatasetService().getType(baseDataset.type); + if (!datasetType) { + setTimeFields([]); + return; + } + setTimeFieldsLoading(true); + const datasetFields = await datasetType + .fetchFields(baseDataset) + .finally(() => setTimeFieldsLoading(false)); const dateFields = datasetFields?.filter((field) => field.type === 'date'); setTimeFields(dateFields || []); }; @@ -152,6 +156,16 @@ export const Configurator = ({ }; }, [indexedViewsService, selectedIndexedView, dataset]); + const shouldRenderDatePickerField = useCallback(() => { + const datasetType = queryString.getDatasetService().getType(dataset.type); + + const supportsTimeField = datasetType?.meta?.supportsTimeFilter; + if (supportsTimeField !== undefined) { + return Boolean(supportsTimeField); + } + return true; + }, [dataset.type, queryString]); + return ( <> @@ -256,7 +270,7 @@ export const Configurator = ({ data-test-subj="advancedSelectorLanguageSelect" /> - {!languageService.getLanguage(language)?.hideDatePicker && + {shouldRenderDatePickerField() && (dataset.type === DEFAULT_DATA.SET_TYPES.INDEX_PATTERN ? ( ({ }); const dataPlugin = dataPluginMock.createStartContract(true); +const datasetService = datasetServiceMock.createStartContract(); function wrapQueryEditorTopRowInContext(testProps: any) { const defaultOptions = { @@ -111,6 +113,7 @@ describe('QueryEditorTopRow', () => { beforeEach(() => { jest.clearAllMocks(); (getQueryService as jest.Mock).mockReturnValue(dataPlugin.query); + dataPlugin.query.queryString.getDatasetService = jest.fn().mockReturnValue(datasetService); }); afterEach(() => { @@ -155,4 +158,49 @@ describe('QueryEditorTopRow', () => { await waitFor(() => expect(container.querySelector(QUERY_EDITOR)).toBeTruthy()); expect(container.querySelector(DATE_PICKER)).toBeFalsy(); }); + + it('Should not render date picker if dataset type does not support time field', async () => { + const query: Query = { + query: 'test query', + dataset: datasetService.getDefault(), + language: 'test-language', + }; + dataPlugin.query.queryString.getQuery = jest.fn().mockReturnValue(query); + datasetService.getType.mockReturnValue({ + meta: { supportsTimeFilter: false }, + } as DatasetTypeConfig); + + const { container } = render( + wrapQueryEditorTopRowInContext({ + query, + showQueryEditor: false, + showDatePicker: true, + }) + ); + await waitFor(() => expect(container.querySelector(QUERY_EDITOR)).toBeTruthy()); + expect(container.querySelector(DATE_PICKER)).toBeFalsy(); + }); + + it('Should render date picker if dataset overrides hideDatePicker to false', async () => { + const query: Query = { + query: 'test query', + dataset: datasetService.getDefault(), + language: 'test-language', + }; + dataPlugin.query.queryString.getQuery = jest.fn().mockReturnValue(query); + datasetService.getType.mockReturnValue(({ + meta: { supportsTimeFilter: true }, + languageOverrides: { 'test-language': { hideDatePicker: false } }, + } as unknown) as DatasetTypeConfig); + + const { container } = render( + wrapQueryEditorTopRowInContext({ + query, + showQueryEditor: false, + showDatePicker: true, + }) + ); + await waitFor(() => expect(container.querySelector(QUERY_EDITOR)).toBeTruthy()); + expect(container.querySelector(DATE_PICKER)).toBeTruthy(); + }); }); diff --git a/src/plugins/data/public/ui/query_editor/query_editor_top_row.tsx b/src/plugins/data/public/ui/query_editor/query_editor_top_row.tsx index ea15fbfeeaa1..ad22750207ed 100644 --- a/src/plugins/data/public/ui/query_editor/query_editor_top_row.tsx +++ b/src/plugins/data/public/ui/query_editor/query_editor_top_row.tsx @@ -224,18 +224,53 @@ export default function QueryEditorTopRow(props: QueryEditorTopRowProps) { ); } + /** + * Determines if the date picker should be rendered based on UI settings, dataset configuration, and language settings. + * + * @returns {boolean} Whether the date picker should be rendered + * + * UI Settings permutations (isDatePickerEnabled): + * - showDatePicker=true || showAutoRefreshOnly=true => true + * - showDatePicker=false && showAutoRefreshOnly=false => false + * - both undefined => true (default) + * If isDatePickerEnabled is false, returns false immediately + * + * Dataset Type permutations (datasetType?.meta?.supportsTimeFilter): + * - supportsTimeFilter=false => false + * + * Language permutations (when dataset.meta.supportsTimeFilter is undefined or true): + * - queryLanguage=undefined => true (shows date picker) + * - queryLanguage exists: + * - languageOverrides[queryLanguage].hideDatePicker=true => false + * - languageOverrides[queryLanguage].hideDatePicker=false => true + * - hideDatePicker=true => false + * - hideDatePicker=false => true + * - hideDatePicker=undefined => true + */ function shouldRenderDatePicker(): boolean { - return ( - Boolean((props.showDatePicker || props.showAutoRefreshOnly) ?? true) && - !( - queryLanguage && - data.query.queryString.getLanguageService().getLanguage(queryLanguage)?.hideDatePicker - ) && - (props.query?.dataset - ? data.query.queryString.getDatasetService().getType(props.query.dataset.type)?.meta - ?.supportsTimeFilter !== false - : true) + const { queryString } = data.query; + const datasetService = queryString.getDatasetService(); + const languageService = queryString.getLanguageService(); + const isDatePickerEnabled = Boolean( + (props.showDatePicker || props.showAutoRefreshOnly) ?? true ); + if (!isDatePickerEnabled) return false; + + // Get dataset type configuration + const datasetType = props.query?.dataset + ? datasetService.getType(props.query?.dataset.type) + : undefined; + // Check if dataset type explicitly configures the `supportsTimeFilter` option + if (datasetType?.meta?.supportsTimeFilter === false) return false; + + if ( + queryLanguage && + datasetType?.languageOverrides?.[queryLanguage]?.hideDatePicker !== undefined + ) { + return Boolean(!datasetType.languageOverrides[queryLanguage].hideDatePicker); + } + + return Boolean(!(queryLanguage && languageService.getLanguage(queryLanguage)?.hideDatePicker)); } function shouldRenderQueryEditor(): boolean { diff --git a/src/plugins/query_enhancements/common/types.ts b/src/plugins/query_enhancements/common/types.ts index 1bb977527d4a..2f73ca52d496 100644 --- a/src/plugins/query_enhancements/common/types.ts +++ b/src/plugins/query_enhancements/common/types.ts @@ -4,7 +4,7 @@ */ import { CoreSetup } from 'opensearch-dashboards/public'; -import { PollQueryResultsParams } from '../../data/common'; +import { PollQueryResultsParams, TimeRange } from '../../data/common'; export interface QueryAggConfig { [key: string]: { @@ -26,7 +26,10 @@ export interface EnhancedFetchContext { http: CoreSetup['http']; path: string; signal?: AbortSignal; - body?: { pollQueryResultsParams: PollQueryResultsParams }; + body?: { + pollQueryResultsParams?: PollQueryResultsParams; + timeRange?: TimeRange; + }; } export interface QueryStatusOptions { diff --git a/src/plugins/query_enhancements/common/utils.ts b/src/plugins/query_enhancements/common/utils.ts index 29e49b00eab0..634a56b84603 100644 --- a/src/plugins/query_enhancements/common/utils.ts +++ b/src/plugins/query_enhancements/common/utils.ts @@ -55,6 +55,7 @@ export const fetch = (context: EnhancedFetchContext, query: Query, aggConfig?: Q query: { ...query, format: 'jdbc' }, aggConfig, pollQueryResultsParams: context.body?.pollQueryResultsParams, + timeRange: context.body?.timeRange, }); return from( http.fetch({ diff --git a/src/plugins/query_enhancements/public/search/ppl_search_interceptor.ts b/src/plugins/query_enhancements/public/search/ppl_search_interceptor.ts index 57152dbe98ea..ecfe32ff8a75 100644 --- a/src/plugins/query_enhancements/public/search/ppl_search_interceptor.ts +++ b/src/plugins/query_enhancements/public/search/ppl_search_interceptor.ts @@ -50,6 +50,7 @@ export class PPLSearchInterceptor extends SearchInterceptor { signal, body: { pollQueryResultsParams: request.params?.pollQueryResultsParams, + timeRange: request.params?.body?.timeRange, }, }; @@ -68,15 +69,33 @@ export class PPLSearchInterceptor extends SearchInterceptor { .getDatasetService() .getType(datasetType); strategy = datasetTypeConfig?.getSearchOptions?.().strategy ?? strategy; + + if ( + dataset?.timeFieldName && + datasetTypeConfig?.languageOverrides?.PPL?.hideDatePicker === false + ) { + request.params = { + ...request.params, + body: { + ...request.params.body, + timeRange: this.queryService.timefilter.timefilter.getTime(), + }, + }; + } } return this.runSearch(request, options.abortSignal, strategy); } private buildQuery() { - const query: Query = this.queryService.queryString.getQuery(); + const { queryString } = this.queryService; + const query: Query = queryString.getQuery(); const dataset = query.dataset; if (!dataset || !dataset.timeFieldName) return query; + const datasetService = queryString.getDatasetService(); + if (datasetService.getType(dataset.type)?.languageOverrides?.PPL?.hideDatePicker === false) + return query; + const [baseQuery, ...afterPipeParts] = query.query.split('|'); const afterPipe = afterPipeParts.length > 0 ? ` | ${afterPipeParts.join('|').trim()}` : ''; const timeFilter = this.getTimeFilter(dataset.timeFieldName); diff --git a/src/plugins/query_enhancements/public/search/sql_search_interceptor.ts b/src/plugins/query_enhancements/public/search/sql_search_interceptor.ts index 9fe17fc79322..9f93dd067cb3 100644 --- a/src/plugins/query_enhancements/public/search/sql_search_interceptor.ts +++ b/src/plugins/query_enhancements/public/search/sql_search_interceptor.ts @@ -42,6 +42,7 @@ export class SQLSearchInterceptor extends SearchInterceptor { signal, body: { pollQueryResultsParams: request.params?.pollQueryResultsParams, + timeRange: request.params?.body?.timeRange, }, }; @@ -62,6 +63,16 @@ export class SQLSearchInterceptor extends SearchInterceptor { .getDatasetService() .getType(datasetType); strategy = datasetTypeConfig?.getSearchOptions?.().strategy ?? strategy; + + if (datasetTypeConfig?.languageOverrides?.SQL?.hideDatePicker === false) { + request.params = { + ...request.params, + body: { + ...request.params.body, + timeRange: this.queryService.timefilter.timefilter.getTime(), + }, + }; + } } return this.runSearch(request, options.abortSignal, strategy); diff --git a/src/plugins/query_enhancements/server/routes/index.ts b/src/plugins/query_enhancements/server/routes/index.ts index 84cf19bec50c..2cda4a9f0cbf 100644 --- a/src/plugins/query_enhancements/server/routes/index.ts +++ b/src/plugins/query_enhancements/server/routes/index.ts @@ -86,6 +86,7 @@ export function defineSearchStrategyRouteProvider(logger: Logger, router: IRoute sessionId: schema.maybe(schema.string()), }) ), + timeRange: schema.maybe(schema.object({}, { unknowns: 'allow' })), }), }, }, From aef853074be66efdf62d564994ac7c8d4d6b9d1e Mon Sep 17 00:00:00 2001 From: Federico Silva Date: Mon, 9 Dec 2024 15:34:16 -0300 Subject: [PATCH 71/80] finish index pattern and index tests Signed-off-by: Federico Silva --- .../sidebar_test_spec.js | 193 +++++++++++++++++- cypress/plugins/index.js | 22 ++ .../data_explorer/data_explorer_page.po.js | 30 ++- 3 files changed, 232 insertions(+), 13 deletions(-) create mode 100644 cypress/plugins/index.js diff --git a/cypress/integration/core_opensearch_dashboards/sidebar_test_spec.js b/cypress/integration/core_opensearch_dashboards/sidebar_test_spec.js index 001b122d6b34..7f728b278bd3 100644 --- a/cypress/integration/core_opensearch_dashboards/sidebar_test_spec.js +++ b/cypress/integration/core_opensearch_dashboards/sidebar_test_spec.js @@ -8,33 +8,38 @@ import { DataExplorerPage } from '../../utils/dashboards/data_explorer/data_expl const miscUtils = new MiscUtils(cy); -describe('sidebar spec', () => { - before(() => { +describe('sidebar: add fields', function () { + beforeEach(function () { cy.localLogin(Cypress.env('username'), Cypress.env('password')); miscUtils.visitPage('app/data-explorer/discover'); }); - it('add field: DQL', () => { + it('index pattern: DQL to PPL and SQL', function () { + DataExplorerPage.setQueryEditorLanguage('DQL'); DataExplorerPage.setSearchRelativeDateRange('15', 'Years ago'); cy.intercept('/internal/search/opensearch-with-long-numerals').as('data'); DataExplorerPage.selectIndexPatternDataset('DQL'); - cy.wait('@data').then(() => { + cy.wait('@data').then(function () { + // Check default second column DataExplorerPage.getDocTableHeader(1).should('have.text', '_source'); }); const testFields = ['_id', 'age', 'birthdate', 'salary']; + // Select some fields testFields.forEach((field) => { DataExplorerPage.getFieldBtnByName(field).click(); }); DataExplorerPage.getDocTableHeader(1).should('not.have.text', '_source'); + // Check table headers persistence between DQL and PPL DataExplorerPage.checkTableHeadersByArray(testFields); DataExplorerPage.setQueryEditorLanguage('PPL'); DataExplorerPage.checkTableHeadersByArray(testFields); + // Remove some fields const firstTestField = testFields[0]; const secondTestField = testFields[1]; DataExplorerPage.getFieldBtnByName(firstTestField).click(); @@ -42,6 +47,7 @@ describe('sidebar spec', () => { DataExplorerPage.getDocTableHeader(1).should('not.have.text', firstTestField); DataExplorerPage.getDocTableHeader(2).should('not.have.text', secondTestField); + // Remove all fields const thirdTestField = testFields[2]; const fourthTestField = testFields[3]; DataExplorerPage.getFieldBtnByName(thirdTestField).click(); @@ -49,8 +55,181 @@ describe('sidebar spec', () => { DataExplorerPage.getDocTableHeader(1).should('have.text', '_source'); DataExplorerPage.getDocTableHeader(2).should('not.exist'); - DataExplorerPage.clearQueryMultilineEditor(); - DataExplorerPage.getQueryMultilineEditor().type('source = vis-builder* | where age > 40'); - DataExplorerPage.getQuerySubmitBtn().click(); + // Select some fields + testFields.forEach((field) => { + DataExplorerPage.getFieldBtnByName(field).click(); + }); + // Check default column again + DataExplorerPage.getDocTableHeader(0).should('not.have.text', '_source'); + // Check the columns match the selected fields + DataExplorerPage.checkTableHeadersByArray(testFields); + + // Validate default hits + DataExplorerPage.checkQueryHitsText('10,000'); + + const expectedValues = ['50', '57', '52', '66', '46']; + + // Send PPL query + cy.intercept('/api/enhancements/search/ppl').as('pplQuery'); + DataExplorerPage.sendQueryOnMultilineEditor('source = vis-builder* | where age > 40'); + cy.wait('@pplQuery').then(function () { + // Check table headers persistence after PPL query + DataExplorerPage.checkTableHeadersByArray(testFields); + // Check filter was correctly applied + DataExplorerPage.checkQueryHitsText('6,588'); + + // Validate the first 5 rows on the _id column + DataExplorerPage.checkDocTableColumnByArr(expectedValues, 2); + }); + + // Send SQL query + DataExplorerPage.setQueryEditorLanguage('OpenSearch SQL'); + cy.intercept('/api/enhancements/search/sql').as('sqlQuery'); + DataExplorerPage.sendQueryOnMultilineEditor('SELECT * FROM vis-builder* WHERE age > 40', false); + cy.wait('@sqlQuery').then(function () { + // Check table headers persistence after SQL query + DataExplorerPage.checkTableHeadersByArray(testFields); + + // Validate the first 5 rows on the _id column + DataExplorerPage.checkDocTableColumnByArr(expectedValues, 2); + }); + }); + + it('index: SQL and PPL', function () { + cy.intercept('/api/enhancements/search/sql').as('sqlData'); + DataExplorerPage.selectIndexDataset('OpenSearch SQL'); + cy.wait('@sqlData').then(function () { + // Check default second column + DataExplorerPage.getDocTableHeader(0).should('have.text', '_source'); + }); + + const testFields = ['_id', 'age', 'birthdate', 'salary']; + + // Select some fields + testFields.forEach((field) => { + DataExplorerPage.getFieldBtnByName(field).click(); + }); + + DataExplorerPage.getDocTableHeader(0).should('not.have.text', '_source'); + + // Check table headers persistence between DQL and PPL + DataExplorerPage.checkTableHeadersByArray(testFields, 0); + DataExplorerPage.setQueryEditorLanguage('PPL'); + DataExplorerPage.checkTableHeadersByArray(testFields, 0); + + // Remove some fields + const firstTestField = testFields[0]; + const secondTestField = testFields[1]; + DataExplorerPage.getFieldBtnByName(firstTestField).click(); + DataExplorerPage.getFieldBtnByName(secondTestField).click(); + DataExplorerPage.getDocTableHeader(0).should('not.have.text', firstTestField); + DataExplorerPage.getDocTableHeader(1).should('not.have.text', secondTestField); + + // Remove all fields + const thirdTestField = testFields[2]; + const fourthTestField = testFields[3]; + DataExplorerPage.getFieldBtnByName(thirdTestField).click(); + DataExplorerPage.getFieldBtnByName(fourthTestField).click(); + DataExplorerPage.getDocTableHeader(0).should('have.text', '_source'); + DataExplorerPage.getDocTableHeader(1).should('not.exist'); + + // Select some fields + testFields.forEach((field) => { + DataExplorerPage.getFieldBtnByName(field).click(); + }); + DataExplorerPage.getDocTableHeader(0).should('not.have.text', '_source'); + DataExplorerPage.checkTableHeadersByArray(testFields, 0); + + const expectedValues = ['50', '57', '52', '66', '46']; + + // Send PPL query + cy.intercept('/api/enhancements/search/ppl').as('pplQuery'); + DataExplorerPage.sendQueryOnMultilineEditor('source = vis-builder* | where age > 40'); + cy.wait('@pplQuery').then(function () { + // Check table headers persistence after PPL query + DataExplorerPage.checkTableHeadersByArray(testFields, 0); + // Validate the first 5 rows on the _id column + DataExplorerPage.checkDocTableColumnByArr(expectedValues, 1); + }); + + // Send SQL query + DataExplorerPage.setQueryEditorLanguage('OpenSearch SQL'); + cy.intercept('/api/enhancements/search/sql').as('sqlQuery'); + DataExplorerPage.sendQueryOnMultilineEditor('SELECT * FROM vis-builder* WHERE age > 40', false); + cy.wait('@sqlQuery').then(function () { + // Check table headers persistence after SQL query + DataExplorerPage.checkTableHeadersByArray(testFields, 0); + // Validate the first 5 rows on the _id column + DataExplorerPage.checkDocTableColumnByArr(expectedValues, 1); + }); + }); + + it('s3: SQL and PPL', function () { + cy.intercept('/api/enhancements/search/sql').as('sqlData'); + DataExplorerPage.selectIndexDataset; + cy.wait('@sqlData').then(function () { + // Check default second column + DataExplorerPage.getDocTableHeader(0).should('have.text', '_source'); + }); + + const testFields = ['_id', 'age', 'birthdate', 'salary']; + + // Select some fields + testFields.forEach((field) => { + DataExplorerPage.getFieldBtnByName(field).click(); + }); + + DataExplorerPage.getDocTableHeader(0).should('not.have.text', '_source'); + + // Check table headers persistence between DQL and PPL + DataExplorerPage.checkTableHeadersByArray(testFields, 0); + DataExplorerPage.setQueryEditorLanguage('PPL'); + DataExplorerPage.checkTableHeadersByArray(testFields, 0); + + // Remove some fields + const firstTestField = testFields[0]; + const secondTestField = testFields[1]; + DataExplorerPage.getFieldBtnByName(firstTestField).click(); + DataExplorerPage.getFieldBtnByName(secondTestField).click(); + DataExplorerPage.getDocTableHeader(0).should('not.have.text', firstTestField); + DataExplorerPage.getDocTableHeader(1).should('not.have.text', secondTestField); + + // Remove all fields + const thirdTestField = testFields[2]; + const fourthTestField = testFields[3]; + DataExplorerPage.getFieldBtnByName(thirdTestField).click(); + DataExplorerPage.getFieldBtnByName(fourthTestField).click(); + DataExplorerPage.getDocTableHeader(0).should('have.text', '_source'); + DataExplorerPage.getDocTableHeader(1).should('not.exist'); + + // Select some fields + testFields.forEach((field) => { + DataExplorerPage.getFieldBtnByName(field).click(); + }); + DataExplorerPage.getDocTableHeader(0).should('not.have.text', '_source'); + DataExplorerPage.checkTableHeadersByArray(testFields, 0); + + const expectedValues = ['50', '57', '52', '66', '46']; + + // Send PPL query + cy.intercept('/api/enhancements/search/ppl').as('pplQuery'); + DataExplorerPage.sendQueryOnMultilineEditor('source = vis-builder* | where age > 40'); + cy.wait('@pplQuery').then(function () { + // Check table headers persistence after PPL query + DataExplorerPage.checkTableHeadersByArray(testFields, 0); + // Validate the first 5 rows on the _id column + DataExplorerPage.checkDocTableColumnByArr(expectedValues, 1); + }); + + // Send SQL query + DataExplorerPage.setQueryEditorLanguage('OpenSearch SQL'); + cy.intercept('/api/enhancements/search/sql').as('sqlQuery'); + DataExplorerPage.sendQueryOnMultilineEditor('SELECT * FROM vis-builder* WHERE age > 40', false); + cy.wait('@sqlQuery').then(function () { + // Check table headers persistence after SQL query + DataExplorerPage.checkTableHeadersByArray(testFields, 0); + // Validate the first 5 rows on the _id column + DataExplorerPage.checkDocTableColumnByArr(expectedValues, 1); + }); }); }); diff --git a/cypress/plugins/index.js b/cypress/plugins/index.js new file mode 100644 index 000000000000..59b2bab6e4e6 --- /dev/null +++ b/cypress/plugins/index.js @@ -0,0 +1,22 @@ +/// +// *********************************************************** +// This example plugins/index.js can be used to load plugins +// +// You can change the location of this file or turn off loading +// the plugins file with the 'pluginsFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/plugins-guide +// *********************************************************** + +// This function is called when a project is opened or re-opened (e.g. due to +// the project's config changing) + +/** + * @type {Cypress.PluginConfig} + */ +// eslint-disable-next-line no-unused-vars +module.exports = (on, config) => { + // `on` is used to hook into various events Cypress emits + // `config` is the resolved Cypress config +} diff --git a/cypress/utils/dashboards/data_explorer/data_explorer_page.po.js b/cypress/utils/dashboards/data_explorer/data_explorer_page.po.js index c6c6eaa60fc5..4e5d40beee5d 100644 --- a/cypress/utils/dashboards/data_explorer/data_explorer_page.po.js +++ b/cypress/utils/dashboards/data_explorer/data_explorer_page.po.js @@ -71,7 +71,6 @@ export class DataExplorerPage { static getDocTableRow(rowNumber) { return cy .getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DOC_TABLE) - .get('tbody tr') .eq(rowNumber); } @@ -84,7 +83,6 @@ export class DataExplorerPage { static getDocTableField(columnNumber, rowNumber) { return DataExplorerPage.getDocTableRow(rowNumber) .findElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.DOC_TABLE_ROW_FIELD) - .eq(columnNumber); } @@ -101,15 +99,35 @@ export class DataExplorerPage { return cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.QUERY_SUBMIT_BUTTON); } - static clearQueryMultilineEditor() { + /** + * + * @param expectedValues array of expected values. E.g. ['50', '57', '52'] + * @param columnNumber column index beginning at 0 + */ + static checkDocTableColumnByArr(expectedValues, columnNumber) { + let currentRow = 0; + expectedValues.forEach((value) => { + DataExplorerPage.getDocTableField(columnNumber, currentRow).should('have.text', value); + currentRow++; + }); + } + + static clearQueryMultilineEditor(del = true) { DataExplorerPage.getQueryMultilineEditor() .invoke('val') .then(function ($content) { const contentLen = $content.length; - DataExplorerPage.getQueryMultilineEditor().type('{del}'.repeat(contentLen)); + const deletionType = del ? '{del}' : '{backspace}'; + DataExplorerPage.getQueryMultilineEditor().type(deletionType.repeat(contentLen)); }); } + static sendQueryOnMultilineEditor(query, del = true) { + DataExplorerPage.clearQueryMultilineEditor(del); + DataExplorerPage.getQueryMultilineEditor().type(query); + DataExplorerPage.getQuerySubmitBtn().click(); + } + /** * Set the query editor language * @param language Accepted values: 'DQL', 'Lucene', 'OpenSearch SQL', 'PPL' @@ -320,9 +338,9 @@ export class DataExplorerPage { DataExplorerPage.checkQueryHitsText('10,000'); } - static checkTableHeadersByArray(arr) { + static checkTableHeadersByArray(arr, offset = 1) { for (let i = 0; i < arr.length; i++) { - DataExplorerPage.getDocTableHeader(i + 1).should('have.text', arr[i]); + DataExplorerPage.getDocTableHeader(i + offset).should('have.text', arr[i]); } } } From ea07e12f4bc570075acfb9d92598c3ba48200f72 Mon Sep 17 00:00:00 2001 From: Argus Li Date: Fri, 6 Dec 2024 08:56:52 -0800 Subject: [PATCH 72/80] Remove should be visible as each command should be a simple get. Signed-off-by: Federico Silva --- .../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 + ); }); From 36cb9b0b6f9e3371fb83db5d9323fd3dd8c6202d Mon Sep 17 00:00:00 2001 From: Federico Silva Date: Mon, 9 Dec 2024 16:55:46 -0300 Subject: [PATCH 73/80] complete first test suite Signed-off-by: Federico Silva --- .../sidebar_test_spec.js | 367 +++++++----------- .../data_explorer/data_explorer_page.po.js | 46 ++- 2 files changed, 184 insertions(+), 229 deletions(-) diff --git a/cypress/integration/core_opensearch_dashboards/sidebar_test_spec.js b/cypress/integration/core_opensearch_dashboards/sidebar_test_spec.js index 7f728b278bd3..5d7281e8f5b1 100644 --- a/cypress/integration/core_opensearch_dashboards/sidebar_test_spec.js +++ b/cypress/integration/core_opensearch_dashboards/sidebar_test_spec.js @@ -8,228 +8,161 @@ import { DataExplorerPage } from '../../utils/dashboards/data_explorer/data_expl const miscUtils = new MiscUtils(cy); -describe('sidebar: add fields', function () { +describe('sidebar spec', function () { beforeEach(function () { cy.localLogin(Cypress.env('username'), Cypress.env('password')); miscUtils.visitPage('app/data-explorer/discover'); }); - it('index pattern: DQL to PPL and SQL', function () { - DataExplorerPage.setQueryEditorLanguage('DQL'); - DataExplorerPage.setSearchRelativeDateRange('15', 'Years ago'); - - cy.intercept('/internal/search/opensearch-with-long-numerals').as('data'); - DataExplorerPage.selectIndexPatternDataset('DQL'); - cy.wait('@data').then(function () { - // Check default second column - DataExplorerPage.getDocTableHeader(1).should('have.text', '_source'); - }); - - const testFields = ['_id', 'age', 'birthdate', 'salary']; - - // Select some fields - testFields.forEach((field) => { - DataExplorerPage.getFieldBtnByName(field).click(); - }); - - DataExplorerPage.getDocTableHeader(1).should('not.have.text', '_source'); - - // Check table headers persistence between DQL and PPL - DataExplorerPage.checkTableHeadersByArray(testFields); - DataExplorerPage.setQueryEditorLanguage('PPL'); - DataExplorerPage.checkTableHeadersByArray(testFields); - - // Remove some fields - const firstTestField = testFields[0]; - const secondTestField = testFields[1]; - DataExplorerPage.getFieldBtnByName(firstTestField).click(); - DataExplorerPage.getFieldBtnByName(secondTestField).click(); - DataExplorerPage.getDocTableHeader(1).should('not.have.text', firstTestField); - DataExplorerPage.getDocTableHeader(2).should('not.have.text', secondTestField); - - // Remove all fields - const thirdTestField = testFields[2]; - const fourthTestField = testFields[3]; - DataExplorerPage.getFieldBtnByName(thirdTestField).click(); - DataExplorerPage.getFieldBtnByName(fourthTestField).click(); - DataExplorerPage.getDocTableHeader(1).should('have.text', '_source'); - DataExplorerPage.getDocTableHeader(2).should('not.exist'); - - // Select some fields - testFields.forEach((field) => { - DataExplorerPage.getFieldBtnByName(field).click(); - }); - // Check default column again - DataExplorerPage.getDocTableHeader(0).should('not.have.text', '_source'); - // Check the columns match the selected fields - DataExplorerPage.checkTableHeadersByArray(testFields); - - // Validate default hits - DataExplorerPage.checkQueryHitsText('10,000'); - - const expectedValues = ['50', '57', '52', '66', '46']; - - // Send PPL query - cy.intercept('/api/enhancements/search/ppl').as('pplQuery'); - DataExplorerPage.sendQueryOnMultilineEditor('source = vis-builder* | where age > 40'); - cy.wait('@pplQuery').then(function () { - // Check table headers persistence after PPL query - DataExplorerPage.checkTableHeadersByArray(testFields); - // Check filter was correctly applied - DataExplorerPage.checkQueryHitsText('6,588'); - - // Validate the first 5 rows on the _id column - DataExplorerPage.checkDocTableColumnByArr(expectedValues, 2); - }); - - // Send SQL query - DataExplorerPage.setQueryEditorLanguage('OpenSearch SQL'); - cy.intercept('/api/enhancements/search/sql').as('sqlQuery'); - DataExplorerPage.sendQueryOnMultilineEditor('SELECT * FROM vis-builder* WHERE age > 40', false); - cy.wait('@sqlQuery').then(function () { - // Check table headers persistence after SQL query - DataExplorerPage.checkTableHeadersByArray(testFields); - - // Validate the first 5 rows on the _id column - DataExplorerPage.checkDocTableColumnByArr(expectedValues, 2); - }); - }); - - it('index: SQL and PPL', function () { - cy.intercept('/api/enhancements/search/sql').as('sqlData'); - DataExplorerPage.selectIndexDataset('OpenSearch SQL'); - cy.wait('@sqlData').then(function () { - // Check default second column - DataExplorerPage.getDocTableHeader(0).should('have.text', '_source'); - }); - - const testFields = ['_id', 'age', 'birthdate', 'salary']; - - // Select some fields - testFields.forEach((field) => { - DataExplorerPage.getFieldBtnByName(field).click(); - }); - - DataExplorerPage.getDocTableHeader(0).should('not.have.text', '_source'); - - // Check table headers persistence between DQL and PPL - DataExplorerPage.checkTableHeadersByArray(testFields, 0); - DataExplorerPage.setQueryEditorLanguage('PPL'); - DataExplorerPage.checkTableHeadersByArray(testFields, 0); - - // Remove some fields - const firstTestField = testFields[0]; - const secondTestField = testFields[1]; - DataExplorerPage.getFieldBtnByName(firstTestField).click(); - DataExplorerPage.getFieldBtnByName(secondTestField).click(); - DataExplorerPage.getDocTableHeader(0).should('not.have.text', firstTestField); - DataExplorerPage.getDocTableHeader(1).should('not.have.text', secondTestField); - - // Remove all fields - const thirdTestField = testFields[2]; - const fourthTestField = testFields[3]; - DataExplorerPage.getFieldBtnByName(thirdTestField).click(); - DataExplorerPage.getFieldBtnByName(fourthTestField).click(); - DataExplorerPage.getDocTableHeader(0).should('have.text', '_source'); - DataExplorerPage.getDocTableHeader(1).should('not.exist'); - - // Select some fields - testFields.forEach((field) => { - DataExplorerPage.getFieldBtnByName(field).click(); - }); - DataExplorerPage.getDocTableHeader(0).should('not.have.text', '_source'); - DataExplorerPage.checkTableHeadersByArray(testFields, 0); - - const expectedValues = ['50', '57', '52', '66', '46']; - - // Send PPL query - cy.intercept('/api/enhancements/search/ppl').as('pplQuery'); - DataExplorerPage.sendQueryOnMultilineEditor('source = vis-builder* | where age > 40'); - cy.wait('@pplQuery').then(function () { - // Check table headers persistence after PPL query - DataExplorerPage.checkTableHeadersByArray(testFields, 0); - // Validate the first 5 rows on the _id column - DataExplorerPage.checkDocTableColumnByArr(expectedValues, 1); - }); - - // Send SQL query - DataExplorerPage.setQueryEditorLanguage('OpenSearch SQL'); - cy.intercept('/api/enhancements/search/sql').as('sqlQuery'); - DataExplorerPage.sendQueryOnMultilineEditor('SELECT * FROM vis-builder* WHERE age > 40', false); - cy.wait('@sqlQuery').then(function () { - // Check table headers persistence after SQL query - DataExplorerPage.checkTableHeadersByArray(testFields, 0); - // Validate the first 5 rows on the _id column - DataExplorerPage.checkDocTableColumnByArr(expectedValues, 1); - }); - }); - - it('s3: SQL and PPL', function () { - cy.intercept('/api/enhancements/search/sql').as('sqlData'); - DataExplorerPage.selectIndexDataset; - cy.wait('@sqlData').then(function () { - // Check default second column - DataExplorerPage.getDocTableHeader(0).should('have.text', '_source'); - }); - - const testFields = ['_id', 'age', 'birthdate', 'salary']; - - // Select some fields - testFields.forEach((field) => { - DataExplorerPage.getFieldBtnByName(field).click(); - }); - - DataExplorerPage.getDocTableHeader(0).should('not.have.text', '_source'); - - // Check table headers persistence between DQL and PPL - DataExplorerPage.checkTableHeadersByArray(testFields, 0); - DataExplorerPage.setQueryEditorLanguage('PPL'); - DataExplorerPage.checkTableHeadersByArray(testFields, 0); - - // Remove some fields - const firstTestField = testFields[0]; - const secondTestField = testFields[1]; - DataExplorerPage.getFieldBtnByName(firstTestField).click(); - DataExplorerPage.getFieldBtnByName(secondTestField).click(); - DataExplorerPage.getDocTableHeader(0).should('not.have.text', firstTestField); - DataExplorerPage.getDocTableHeader(1).should('not.have.text', secondTestField); - - // Remove all fields - const thirdTestField = testFields[2]; - const fourthTestField = testFields[3]; - DataExplorerPage.getFieldBtnByName(thirdTestField).click(); - DataExplorerPage.getFieldBtnByName(fourthTestField).click(); - DataExplorerPage.getDocTableHeader(0).should('have.text', '_source'); - DataExplorerPage.getDocTableHeader(1).should('not.exist'); - - // Select some fields - testFields.forEach((field) => { - DataExplorerPage.getFieldBtnByName(field).click(); - }); - DataExplorerPage.getDocTableHeader(0).should('not.have.text', '_source'); - DataExplorerPage.checkTableHeadersByArray(testFields, 0); - - const expectedValues = ['50', '57', '52', '66', '46']; - - // Send PPL query - cy.intercept('/api/enhancements/search/ppl').as('pplQuery'); - DataExplorerPage.sendQueryOnMultilineEditor('source = vis-builder* | where age > 40'); - cy.wait('@pplQuery').then(function () { - // Check table headers persistence after PPL query - DataExplorerPage.checkTableHeadersByArray(testFields, 0); - // Validate the first 5 rows on the _id column - DataExplorerPage.checkDocTableColumnByArr(expectedValues, 1); - }); - - // Send SQL query - DataExplorerPage.setQueryEditorLanguage('OpenSearch SQL'); - cy.intercept('/api/enhancements/search/sql').as('sqlQuery'); - DataExplorerPage.sendQueryOnMultilineEditor('SELECT * FROM vis-builder* WHERE age > 40', false); - cy.wait('@sqlQuery').then(function () { - // Check table headers persistence after SQL query - DataExplorerPage.checkTableHeadersByArray(testFields, 0); - // Validate the first 5 rows on the _id column - DataExplorerPage.checkDocTableColumnByArr(expectedValues, 1); + describe('results display and interaction in table', function () { + describe('filter by sidebar fields', function () { + const expectedValues = ['50', '57', '52', '66', '46']; + + it('index pattern: DQL to PPL and SQL', function () { + DataExplorerPage.setQueryEditorLanguage('DQL'); + DataExplorerPage.setSearchRelativeDateRange('15', 'Years ago'); + + cy.intercept('/internal/search/opensearch-with-long-numerals').as('data'); + DataExplorerPage.selectIndexPatternDataset('DQL'); + cy.wait('@data').then(function () { + // Check default second column + DataExplorerPage.getDocTableHeader(1).should('have.text', '_source'); + }); + + const testFields = ['_id', 'age', 'birthdate', 'salary']; + + // Select some fields + testFields.forEach((field) => { + DataExplorerPage.getFieldBtnByName(field).click(); + }); + + DataExplorerPage.getDocTableHeader(1).should('not.have.text', '_source'); + + // Check table headers persistence between DQL and PPL + DataExplorerPage.checkTableHeadersByArray(testFields); + DataExplorerPage.setQueryEditorLanguage('PPL'); + DataExplorerPage.checkTableHeadersByArray(testFields); + + // Remove some fields + const firstTestField = testFields[0]; + const secondTestField = testFields[1]; + DataExplorerPage.getFieldBtnByName(firstTestField).click(); + DataExplorerPage.getFieldBtnByName(secondTestField).click(); + DataExplorerPage.getDocTableHeader(1).should('not.have.text', firstTestField); + DataExplorerPage.getDocTableHeader(2).should('not.have.text', secondTestField); + + // Remove all fields + const thirdTestField = testFields[2]; + const fourthTestField = testFields[3]; + DataExplorerPage.getFieldBtnByName(thirdTestField).click(); + DataExplorerPage.getFieldBtnByName(fourthTestField).click(); + DataExplorerPage.getDocTableHeader(1).should('have.text', '_source'); + DataExplorerPage.getDocTableHeader(2).should('not.exist'); + + // Select some fields + testFields.forEach((field) => { + DataExplorerPage.getFieldBtnByName(field).click(); + }); + // Check default column again + DataExplorerPage.getDocTableHeader(0).should('not.have.text', '_source'); + // Check the columns match the selected fields + DataExplorerPage.checkTableHeadersByArray(testFields); + + // Validate default hits + DataExplorerPage.checkQueryHitsText('10,000'); + + // Send PPL query + cy.intercept('/api/enhancements/search/ppl').as('pplQuery'); + DataExplorerPage.sendQueryOnMultilineEditor('source = vis-builder* | where age > 40'); + cy.wait('@pplQuery').then(function () { + // Check table headers persistence after PPL query + DataExplorerPage.checkTableHeadersByArray(testFields); + // Check filter was correctly applied + DataExplorerPage.checkQueryHitsText('6,588'); + + // Validate the first 5 rows on the _id column + DataExplorerPage.checkDocTableColumnByArr(expectedValues, 2); + }); + + // Send SQL query + DataExplorerPage.setQueryEditorLanguage('OpenSearch SQL'); + cy.intercept('/api/enhancements/search/sql').as('sqlQuery'); + DataExplorerPage.sendQueryOnMultilineEditor('SELECT * FROM vis-builder* WHERE age > 40', false); + cy.wait('@sqlQuery').then(function () { + // Check table headers persistence after SQL query + DataExplorerPage.checkTableHeadersByArray(testFields); + + // Validate the first 5 rows on the _id column + DataExplorerPage.checkDocTableColumnByArr(expectedValues, 2); + }); + }); + + it.only('index: SQL and PPL', function () { + cy.intercept('/api/enhancements/search/sql').as('sqlData'); + DataExplorerPage.selectIndexDataset('OpenSearch SQL', "I don't want to use the time filter"); + cy.wait('@sqlData').then(function () { + // Check default first column + DataExplorerPage.getDocTableHeader(0).should('have.text', '_source'); + }); + + const testFields = ['_id', 'age', 'birthdate', 'salary']; + + // Select some fields + testFields.forEach((field) => { + DataExplorerPage.getFieldBtnByName(field).click(); + }); + + DataExplorerPage.getDocTableHeader(0).should('not.have.text', '_source'); + + // Check table headers persistence between DQL and PPL + DataExplorerPage.checkTableHeadersByArray(testFields, 0); + DataExplorerPage.setQueryEditorLanguage('PPL'); + DataExplorerPage.checkTableHeadersByArray(testFields, 0); + + // Remove some fields + const firstTestField = testFields[0]; + const secondTestField = testFields[1]; + DataExplorerPage.getFieldBtnByName(firstTestField).click(); + DataExplorerPage.getFieldBtnByName(secondTestField).click(); + DataExplorerPage.getDocTableHeader(0).should('not.have.text', firstTestField); + DataExplorerPage.getDocTableHeader(1).should('not.have.text', secondTestField); + + // Remove all fields + const thirdTestField = testFields[2]; + const fourthTestField = testFields[3]; + DataExplorerPage.getFieldBtnByName(thirdTestField).click(); + DataExplorerPage.getFieldBtnByName(fourthTestField).click(); + DataExplorerPage.getDocTableHeader(0).should('have.text', '_source'); + DataExplorerPage.getDocTableHeader(1).should('not.exist'); + + // Select some fields + testFields.forEach((field) => { + DataExplorerPage.getFieldBtnByName(field).click(); + }); + DataExplorerPage.getDocTableHeader(0).should('not.have.text', '_source'); + DataExplorerPage.checkTableHeadersByArray(testFields, 0); + + // Send PPL query + cy.intercept('/api/enhancements/search/ppl').as('pplQuery'); + DataExplorerPage.sendQueryOnMultilineEditor('source = vis-builder* | where age > 40'); + cy.wait('@pplQuery').then(function () { + // Check table headers persistence after PPL query + DataExplorerPage.checkTableHeadersByArray(testFields, 0); + // Validate the first 5 rows on the _id column + DataExplorerPage.checkDocTableColumnByArr(expectedValues, 1); + }); + + // Send SQL query + DataExplorerPage.setQueryEditorLanguage('OpenSearch SQL'); + cy.intercept('/api/enhancements/search/sql').as('sqlQuery'); + DataExplorerPage.sendQueryOnMultilineEditor('SELECT * FROM vis-builder* WHERE age > 40', false); + cy.wait('@sqlQuery').then(function () { + // Check table headers persistence after SQL query + DataExplorerPage.checkTableHeadersByArray(testFields, 0); + // Validate the first 5 rows on the _id column + DataExplorerPage.checkDocTableColumnByArr(expectedValues, 1); + }); + }); }); }); }); diff --git a/cypress/utils/dashboards/data_explorer/data_explorer_page.po.js b/cypress/utils/dashboards/data_explorer/data_explorer_page.po.js index 4e5d40beee5d..81c6d305a191 100644 --- a/cypress/utils/dashboards/data_explorer/data_explorer_page.po.js +++ b/cypress/utils/dashboards/data_explorer/data_explorer_page.po.js @@ -95,12 +95,15 @@ export class DataExplorerPage { .find('textarea'); } + /** + * Selects the query submit button over the query multiline editor. + */ static getQuerySubmitBtn() { return cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.QUERY_SUBMIT_BUTTON); } /** - * + * * @param expectedValues array of expected values. E.g. ['50', '57', '52'] * @param columnNumber column index beginning at 0 */ @@ -112,6 +115,12 @@ export class DataExplorerPage { }); } + /** + * Clears the query multiline editor content. + * Default cy.clear() will not work. + * @param del true/false. true: Deletes character to the right of the cursor; false: Deletes character to the left of the cursor + * @see https://docs.cypress.io/api/commands/type#Arguments + */ static clearQueryMultilineEditor(del = true) { DataExplorerPage.getQueryMultilineEditor() .invoke('val') @@ -122,6 +131,11 @@ export class DataExplorerPage { }); } + /** + * Sends a new query via the query multiline editor. + * @param del true/false. true: Deletes character to the right of the cursor; false: Deletes character to the left of the cursor + * @see https://docs.cypress.io/api/commands/type#Arguments + */ static sendQueryOnMultilineEditor(query, del = true) { DataExplorerPage.clearQueryMultilineEditor(del); DataExplorerPage.getQueryMultilineEditor().type(query); @@ -179,13 +193,16 @@ export class DataExplorerPage { } /** - * Get sidebar add field button. + * Get sidebar add field button by index. * @param index Integer that starts at 0 for the first add button. */ static getFieldBtnByIndex(index) { return cy.getElementByTestIdLike('fieldToggle-', 'beginning').eq(index); } + /** + * Get sidebar add field button by name. + */ static getFieldBtnByName(name) { return cy.getElementByTestId('fieldToggle-' + name); } @@ -210,12 +227,12 @@ export class DataExplorerPage { * Select a language in the Dataset Selector for Index * @param datasetLanguage Index supports "OpenSearch SQL" and "PPL" */ - static selectIndexDatasetLanguage(datasetLanguage) { + static selectIndexDatasetLanguage(datasetLanguage, timeField) { DataExplorerPage.getDatasetLanguageSelector().select(datasetLanguage); - switch (datasetLanguage) { - case 'PPL': - DataExplorerPage.selectDatasetTimeField("I don't want to use the time filter"); - break; + if (datasetLanguage === 'PPL') { + DataExplorerPage.selectDatasetTimeField("I don't want to use the time filter"); + } else { + DataExplorerPage.selectDatasetTimeField(timeField); } DataExplorerPage.getDatasetSelectDataButton().click(); } @@ -224,7 +241,7 @@ export class DataExplorerPage { * Select an index dataset. * @param datasetLanguage Index supports "OpenSearch SQL" and "PPL" */ - static selectIndexDataset(datasetLanguage) { + static selectIndexDataset(datasetLanguage, timeField) { DataExplorerPage.openDatasetExplorerWindow(); DataExplorerPage.getDatasetExplorerWindow().contains('Indexes').click(); DataExplorerPage.getDatasetExplorerWindow() @@ -232,7 +249,7 @@ export class DataExplorerPage { .click(); DataExplorerPage.getDatasetExplorerWindow().contains(INDEX_NAME, { timeout: 10000 }).click(); DataExplorerPage.getDatasetExplorerNextButton().click(); - DataExplorerPage.selectIndexDatasetLanguage(datasetLanguage); + DataExplorerPage.selectIndexDatasetLanguage(datasetLanguage, timeField); } /** @@ -338,9 +355,14 @@ export class DataExplorerPage { DataExplorerPage.checkQueryHitsText('10,000'); } - static checkTableHeadersByArray(arr, offset = 1) { - for (let i = 0; i < arr.length; i++) { - DataExplorerPage.getDocTableHeader(i + offset).should('have.text', arr[i]); + /** + * + * @param expectedHeaders array containing the expected header names + * @param offset used to adjust the index of the table headers being checked. Set to 1 by default, which means the method starts checking headers from an index that is 1 higher than the current loop index (i + offset). + */ + static checkTableHeadersByArray(expectedHeaders, offset = 1) { + for (let i = 0; i < expectedHeaders.length; i++) { + DataExplorerPage.getDocTableHeader(i + offset).should('have.text', expectedHeaders[i]); } } } From af95d245710ab4f017291339e18c988e964819fe Mon Sep 17 00:00:00 2001 From: Federico Silva Date: Mon, 9 Dec 2024 17:00:42 -0300 Subject: [PATCH 74/80] update whitespaces Signed-off-by: Federico Silva --- .../sidebar_test_spec.js | 63 +++++++++++-------- .../data_explorer/data_explorer_page.po.js | 4 +- 2 files changed, 38 insertions(+), 29 deletions(-) diff --git a/cypress/integration/core_opensearch_dashboards/sidebar_test_spec.js b/cypress/integration/core_opensearch_dashboards/sidebar_test_spec.js index 5d7281e8f5b1..192aaf70f8af 100644 --- a/cypress/integration/core_opensearch_dashboards/sidebar_test_spec.js +++ b/cypress/integration/core_opensearch_dashboards/sidebar_test_spec.js @@ -21,28 +21,28 @@ describe('sidebar spec', function () { it('index pattern: DQL to PPL and SQL', function () { DataExplorerPage.setQueryEditorLanguage('DQL'); DataExplorerPage.setSearchRelativeDateRange('15', 'Years ago'); - + cy.intercept('/internal/search/opensearch-with-long-numerals').as('data'); DataExplorerPage.selectIndexPatternDataset('DQL'); cy.wait('@data').then(function () { // Check default second column DataExplorerPage.getDocTableHeader(1).should('have.text', '_source'); }); - + const testFields = ['_id', 'age', 'birthdate', 'salary']; - + // Select some fields testFields.forEach((field) => { DataExplorerPage.getFieldBtnByName(field).click(); }); - + DataExplorerPage.getDocTableHeader(1).should('not.have.text', '_source'); - + // Check table headers persistence between DQL and PPL DataExplorerPage.checkTableHeadersByArray(testFields); DataExplorerPage.setQueryEditorLanguage('PPL'); DataExplorerPage.checkTableHeadersByArray(testFields); - + // Remove some fields const firstTestField = testFields[0]; const secondTestField = testFields[1]; @@ -50,7 +50,7 @@ describe('sidebar spec', function () { DataExplorerPage.getFieldBtnByName(secondTestField).click(); DataExplorerPage.getDocTableHeader(1).should('not.have.text', firstTestField); DataExplorerPage.getDocTableHeader(2).should('not.have.text', secondTestField); - + // Remove all fields const thirdTestField = testFields[2]; const fourthTestField = testFields[3]; @@ -58,7 +58,7 @@ describe('sidebar spec', function () { DataExplorerPage.getFieldBtnByName(fourthTestField).click(); DataExplorerPage.getDocTableHeader(1).should('have.text', '_source'); DataExplorerPage.getDocTableHeader(2).should('not.exist'); - + // Select some fields testFields.forEach((field) => { DataExplorerPage.getFieldBtnByName(field).click(); @@ -67,10 +67,10 @@ describe('sidebar spec', function () { DataExplorerPage.getDocTableHeader(0).should('not.have.text', '_source'); // Check the columns match the selected fields DataExplorerPage.checkTableHeadersByArray(testFields); - + // Validate default hits DataExplorerPage.checkQueryHitsText('10,000'); - + // Send PPL query cy.intercept('/api/enhancements/search/ppl').as('pplQuery'); DataExplorerPage.sendQueryOnMultilineEditor('source = vis-builder* | where age > 40'); @@ -79,46 +79,52 @@ describe('sidebar spec', function () { DataExplorerPage.checkTableHeadersByArray(testFields); // Check filter was correctly applied DataExplorerPage.checkQueryHitsText('6,588'); - + // Validate the first 5 rows on the _id column DataExplorerPage.checkDocTableColumnByArr(expectedValues, 2); }); - + // Send SQL query DataExplorerPage.setQueryEditorLanguage('OpenSearch SQL'); cy.intercept('/api/enhancements/search/sql').as('sqlQuery'); - DataExplorerPage.sendQueryOnMultilineEditor('SELECT * FROM vis-builder* WHERE age > 40', false); + DataExplorerPage.sendQueryOnMultilineEditor( + 'SELECT * FROM vis-builder* WHERE age > 40', + false + ); cy.wait('@sqlQuery').then(function () { // Check table headers persistence after SQL query DataExplorerPage.checkTableHeadersByArray(testFields); - + // Validate the first 5 rows on the _id column DataExplorerPage.checkDocTableColumnByArr(expectedValues, 2); }); }); - - it.only('index: SQL and PPL', function () { + + it('index: SQL and PPL', function () { cy.intercept('/api/enhancements/search/sql').as('sqlData'); - DataExplorerPage.selectIndexDataset('OpenSearch SQL', "I don't want to use the time filter"); + DataExplorerPage.selectIndexDataset( + 'OpenSearch SQL', + "I don't want to use the time filter" + ); cy.wait('@sqlData').then(function () { // Check default first column DataExplorerPage.getDocTableHeader(0).should('have.text', '_source'); }); - + const testFields = ['_id', 'age', 'birthdate', 'salary']; - + // Select some fields testFields.forEach((field) => { DataExplorerPage.getFieldBtnByName(field).click(); }); - + DataExplorerPage.getDocTableHeader(0).should('not.have.text', '_source'); - + // Check table headers persistence between DQL and PPL DataExplorerPage.checkTableHeadersByArray(testFields, 0); DataExplorerPage.setQueryEditorLanguage('PPL'); DataExplorerPage.checkTableHeadersByArray(testFields, 0); - + // Remove some fields const firstTestField = testFields[0]; const secondTestField = testFields[1]; @@ -126,7 +132,7 @@ describe('sidebar spec', function () { DataExplorerPage.getFieldBtnByName(secondTestField).click(); DataExplorerPage.getDocTableHeader(0).should('not.have.text', firstTestField); DataExplorerPage.getDocTableHeader(1).should('not.have.text', secondTestField); - + // Remove all fields const thirdTestField = testFields[2]; const fourthTestField = testFields[3]; @@ -134,14 +140,14 @@ describe('sidebar spec', function () { DataExplorerPage.getFieldBtnByName(fourthTestField).click(); DataExplorerPage.getDocTableHeader(0).should('have.text', '_source'); DataExplorerPage.getDocTableHeader(1).should('not.exist'); - + // Select some fields testFields.forEach((field) => { DataExplorerPage.getFieldBtnByName(field).click(); }); DataExplorerPage.getDocTableHeader(0).should('not.have.text', '_source'); DataExplorerPage.checkTableHeadersByArray(testFields, 0); - + // Send PPL query cy.intercept('/api/enhancements/search/ppl').as('pplQuery'); DataExplorerPage.sendQueryOnMultilineEditor('source = vis-builder* | where age > 40'); @@ -151,11 +157,14 @@ describe('sidebar spec', function () { // Validate the first 5 rows on the _id column DataExplorerPage.checkDocTableColumnByArr(expectedValues, 1); }); - + // Send SQL query DataExplorerPage.setQueryEditorLanguage('OpenSearch SQL'); cy.intercept('/api/enhancements/search/sql').as('sqlQuery'); - DataExplorerPage.sendQueryOnMultilineEditor('SELECT * FROM vis-builder* WHERE age > 40', false); + DataExplorerPage.sendQueryOnMultilineEditor( + 'SELECT * FROM vis-builder* WHERE age > 40', + false + ); cy.wait('@sqlQuery').then(function () { // Check table headers persistence after SQL query DataExplorerPage.checkTableHeadersByArray(testFields, 0); diff --git a/cypress/utils/dashboards/data_explorer/data_explorer_page.po.js b/cypress/utils/dashboards/data_explorer/data_explorer_page.po.js index 81c6d305a191..88d6ca8f4301 100644 --- a/cypress/utils/dashboards/data_explorer/data_explorer_page.po.js +++ b/cypress/utils/dashboards/data_explorer/data_explorer_page.po.js @@ -103,7 +103,7 @@ export class DataExplorerPage { } /** - * + * * @param expectedValues array of expected values. E.g. ['50', '57', '52'] * @param columnNumber column index beginning at 0 */ @@ -356,7 +356,7 @@ export class DataExplorerPage { } /** - * + * * @param expectedHeaders array containing the expected header names * @param offset used to adjust the index of the table headers being checked. Set to 1 by default, which means the method starts checking headers from an index that is 1 higher than the current loop index (i + offset). */ From a2595ec6db15ffb3d460d192a84f1409e16ce757 Mon Sep 17 00:00:00 2001 From: Federico Silva Date: Mon, 9 Dec 2024 17:08:23 -0300 Subject: [PATCH 75/80] fix filter for value broken index tests Signed-off-by: Federico Silva --- .../core_opensearch_dashboards/filter_for_value_spec.js | 7 +++++-- .../dashboards/data_explorer/data_explorer_page.po.js | 6 +----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/cypress/integration/core_opensearch_dashboards/filter_for_value_spec.js b/cypress/integration/core_opensearch_dashboards/filter_for_value_spec.js index 02016d17e455..43305f08bf60 100644 --- a/cypress/integration/core_opensearch_dashboards/filter_for_value_spec.js +++ b/cypress/integration/core_opensearch_dashboards/filter_for_value_spec.js @@ -47,12 +47,15 @@ describe('filter for value spec', () => { describe('index dataset', () => { // filter actions should not exist for SQL it('SQL', () => { - DataExplorerPage.selectIndexDataset('OpenSearch SQL'); + DataExplorerPage.selectIndexDataset( + 'OpenSearch SQL', + "I don't want to use the time filter" + ); DataExplorerPage.checkDocTableFirstFieldFilterForAndOutButton(false); }); // filter actions should not exist for PPL it('PPL', () => { - DataExplorerPage.selectIndexDataset('PPL'); + DataExplorerPage.selectIndexDataset('PPL', "I don't want to use the time filter"); DataExplorerPage.checkDocTableFirstFieldFilterForAndOutButton(false); }); }); diff --git a/cypress/utils/dashboards/data_explorer/data_explorer_page.po.js b/cypress/utils/dashboards/data_explorer/data_explorer_page.po.js index 88d6ca8f4301..57acb2c29887 100644 --- a/cypress/utils/dashboards/data_explorer/data_explorer_page.po.js +++ b/cypress/utils/dashboards/data_explorer/data_explorer_page.po.js @@ -229,11 +229,7 @@ export class DataExplorerPage { */ static selectIndexDatasetLanguage(datasetLanguage, timeField) { DataExplorerPage.getDatasetLanguageSelector().select(datasetLanguage); - if (datasetLanguage === 'PPL') { - DataExplorerPage.selectDatasetTimeField("I don't want to use the time filter"); - } else { - DataExplorerPage.selectDatasetTimeField(timeField); - } + DataExplorerPage.selectDatasetTimeField(timeField); DataExplorerPage.getDatasetSelectDataButton().click(); } From 1f778c67b88760864ff049d9bd71f6455dd78db1 Mon Sep 17 00:00:00 2001 From: Daniel Rowe <51932404+d-rowe@users.noreply.github.com> Date: Mon, 2 Dec 2024 15:34:08 -0800 Subject: [PATCH 76/80] 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: Federico Silva --- cypress.config.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/cypress.config.ts b/cypress.config.ts index d1363c2bf7ca..67e7b4f5039b 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -27,7 +27,6 @@ module.exports = defineConfig({ }, e2e: { baseUrl: 'http://localhost:5601', - supportFile: 'cypress/support/e2e.{js,jsx,ts,tsx}', specPattern: 'cypress/integration/**/*_spec.{js,jsx,ts,tsx}', testIsolation: false, setupNodeEvents, From b8b90b758605ff7de2019b94f9ab7eeb53a19a82 Mon Sep 17 00:00:00 2001 From: Federico Silva Date: Tue, 10 Dec 2024 16:28:35 -0300 Subject: [PATCH 77/80] finish second test suite Signed-off-by: Federico Silva --- .../sidebar_test_spec.js | 75 ++++++++++++------- .../data_explorer/data_explorer_page.po.js | 39 ++++++++++ .../dashboards/data_explorer/elements.js | 1 + 3 files changed, 87 insertions(+), 28 deletions(-) diff --git a/cypress/integration/core_opensearch_dashboards/sidebar_test_spec.js b/cypress/integration/core_opensearch_dashboards/sidebar_test_spec.js index 192aaf70f8af..fc421373c52c 100644 --- a/cypress/integration/core_opensearch_dashboards/sidebar_test_spec.js +++ b/cypress/integration/core_opensearch_dashboards/sidebar_test_spec.js @@ -14,22 +14,19 @@ describe('sidebar spec', function () { miscUtils.visitPage('app/data-explorer/discover'); }); - describe('results display and interaction in table', function () { - describe('filter by sidebar fields', function () { + describe('filter by sidebar fields', function () { + describe('add fields', function () { const expectedValues = ['50', '57', '52', '66', '46']; + const testFields = ['_id', 'age', 'birthdate', 'salary']; + const pplQuery = 'source = vis-builder* | where age > 40'; + const sqlQuery = 'SELECT * FROM vis-builder* WHERE age > 40'; it('index pattern: DQL to PPL and SQL', function () { - DataExplorerPage.setQueryEditorLanguage('DQL'); - DataExplorerPage.setSearchRelativeDateRange('15', 'Years ago'); - - cy.intercept('/internal/search/opensearch-with-long-numerals').as('data'); DataExplorerPage.selectIndexPatternDataset('DQL'); - cy.wait('@data').then(function () { - // Check default second column - DataExplorerPage.getDocTableHeader(1).should('have.text', '_source'); - }); - const testFields = ['_id', 'age', 'birthdate', 'salary']; + DataExplorerPage.setQueryEditorLanguage('DQL'); + DataExplorerPage.setSearchRelativeDateRange('15', 'Years ago'); + DataExplorerPage.getDocTableHeader(1).should('have.text', '_source'); // Select some fields testFields.forEach((field) => { @@ -73,7 +70,7 @@ describe('sidebar spec', function () { // Send PPL query cy.intercept('/api/enhancements/search/ppl').as('pplQuery'); - DataExplorerPage.sendQueryOnMultilineEditor('source = vis-builder* | where age > 40'); + DataExplorerPage.sendQueryOnMultilineEditor(pplQuery); cy.wait('@pplQuery').then(function () { // Check table headers persistence after PPL query DataExplorerPage.checkTableHeadersByArray(testFields); @@ -87,10 +84,7 @@ describe('sidebar spec', function () { // Send SQL query DataExplorerPage.setQueryEditorLanguage('OpenSearch SQL'); cy.intercept('/api/enhancements/search/sql').as('sqlQuery'); - DataExplorerPage.sendQueryOnMultilineEditor( - 'SELECT * FROM vis-builder* WHERE age > 40', - false - ); + DataExplorerPage.sendQueryOnMultilineEditor(sqlQuery, false); cy.wait('@sqlQuery').then(function () { // Check table headers persistence after SQL query DataExplorerPage.checkTableHeadersByArray(testFields); @@ -101,17 +95,11 @@ describe('sidebar spec', function () { }); it('index: SQL and PPL', function () { - cy.intercept('/api/enhancements/search/sql').as('sqlData'); DataExplorerPage.selectIndexDataset( 'OpenSearch SQL', "I don't want to use the time filter" ); - cy.wait('@sqlData').then(function () { - // Check default first column - DataExplorerPage.getDocTableHeader(0).should('have.text', '_source'); - }); - - const testFields = ['_id', 'age', 'birthdate', 'salary']; + DataExplorerPage.getDocTableHeader(0).should('have.text', '_source'); // Select some fields testFields.forEach((field) => { @@ -150,7 +138,7 @@ describe('sidebar spec', function () { // Send PPL query cy.intercept('/api/enhancements/search/ppl').as('pplQuery'); - DataExplorerPage.sendQueryOnMultilineEditor('source = vis-builder* | where age > 40'); + DataExplorerPage.sendQueryOnMultilineEditor(pplQuery); cy.wait('@pplQuery').then(function () { // Check table headers persistence after PPL query DataExplorerPage.checkTableHeadersByArray(testFields, 0); @@ -161,10 +149,7 @@ describe('sidebar spec', function () { // Send SQL query DataExplorerPage.setQueryEditorLanguage('OpenSearch SQL'); cy.intercept('/api/enhancements/search/sql').as('sqlQuery'); - DataExplorerPage.sendQueryOnMultilineEditor( - 'SELECT * FROM vis-builder* WHERE age > 40', - false - ); + DataExplorerPage.sendQueryOnMultilineEditor(sqlQuery, false); cy.wait('@sqlQuery').then(function () { // Check table headers persistence after SQL query DataExplorerPage.checkTableHeadersByArray(testFields, 0); @@ -173,5 +158,39 @@ describe('sidebar spec', function () { }); }); }); + + describe('filter fields', function () { + it('index pattern: DQL, PPL and SQL', function () { + DataExplorerPage.selectIndexPatternDataset('DQL'); + DataExplorerPage.setQueryEditorLanguage('DQL'); + DataExplorerPage.setSearchRelativeDateRange('15', 'Years ago'); + DataExplorerPage.checkSidebarFilterBarResults('equal', 'categories'); + DataExplorerPage.checkSidebarFilterBarResults('include', 'a'); + DataExplorerPage.checkSidebarFilterBarResults('include', 'ag'); + + DataExplorerPage.setQueryEditorLanguage('PPL'); + DataExplorerPage.checkSidebarFilterBarResults('equal', 'categories'); + DataExplorerPage.checkSidebarFilterBarResults('include', 'a'); + DataExplorerPage.checkSidebarFilterBarResults('include', 'ag'); + + DataExplorerPage.setQueryEditorLanguage('OpenSearch SQL'); + DataExplorerPage.checkSidebarFilterBarResults('equal', 'categories'); + DataExplorerPage.checkSidebarFilterBarResults('include', 'a'); + DataExplorerPage.checkSidebarFilterBarResults('include', 'ag'); + }); + + it('index: PPL and SQL', function () { + DataExplorerPage.selectIndexDataset('PPL', "I don't want to use the time filter"); + DataExplorerPage.setQueryEditorLanguage('PPL'); + DataExplorerPage.checkSidebarFilterBarResults('equal', 'categories'); + DataExplorerPage.checkSidebarFilterBarResults('include', 'a'); + DataExplorerPage.checkSidebarFilterBarResults('include', 'ag'); + + DataExplorerPage.setQueryEditorLanguage('OpenSearch SQL'); + DataExplorerPage.checkSidebarFilterBarResults('equal', 'categories'); + DataExplorerPage.checkSidebarFilterBarResults('include', 'a'); + DataExplorerPage.checkSidebarFilterBarResults('include', 'ag'); + }); + }); }); }); diff --git a/cypress/utils/dashboards/data_explorer/data_explorer_page.po.js b/cypress/utils/dashboards/data_explorer/data_explorer_page.po.js index 57acb2c29887..ee64479e8ca3 100644 --- a/cypress/utils/dashboards/data_explorer/data_explorer_page.po.js +++ b/cypress/utils/dashboards/data_explorer/data_explorer_page.po.js @@ -192,6 +192,20 @@ export class DataExplorerPage { return cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.GLOBAL_FILTER_BAR); } + /** + * Get sidebar filter bar. + */ + static getSidebarFilterBar() { + return cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.SIDEBAR_FILTER_BAR); + } + + /** + * Click on the "Clear input" button on the sidebar filter bar. + */ + static clearSidebarFilterBar() { + return cy.get('button[aria-label="Clear input"]').click(); + } + /** * Get sidebar add field button by index. * @param index Integer that starts at 0 for the first add button. @@ -207,6 +221,31 @@ export class DataExplorerPage { return cy.getElementByTestId('fieldToggle-' + name); } + /** + * Get all sidebar add field button. + */ + static getAllSidebarAddFields() { + return cy.get('[data-test-subj^="field-"]:not([data-test-subj$="showDetails"])'); + } + + /** + * Check the results of the sidebar filter bar search. + * @param search string to look up + * @param assertion the type of assertion that is going to be performed. Example: 'eq', 'include' + */ + static checkSidebarFilterBarResults(assertion, search) { + DataExplorerPage.getSidebarFilterBar().type(search); + DataExplorerPage.getAllSidebarAddFields().each(function ($field) { + cy.wrap($field) + .should('be.visible') + .invoke('text') + .then(function ($fieldTxt) { + cy.wrap($fieldTxt).should(assertion, search); + }); + }); + DataExplorerPage.clearSidebarFilterBar(); + } + /** * Open window to select Dataset */ diff --git a/cypress/utils/dashboards/data_explorer/elements.js b/cypress/utils/dashboards/data_explorer/elements.js index 84ea6db46b82..b484a6500fbb 100644 --- a/cypress/utils/dashboards/data_explorer/elements.js +++ b/cypress/utils/dashboards/data_explorer/elements.js @@ -28,4 +28,5 @@ export const DATA_EXPLORER_PAGE_ELEMENTS = { QUERY_EDITOR_MULTILINE: 'osdQueryEditor__multiLine', GLOBAL_QUERY_EDITOR_FILTER_VALUE: 'globalFilterLabelValue', GLOBAL_FILTER_BAR: 'globalFilterBar', + SIDEBAR_FILTER_BAR: 'fieldFilterSearchInput', }; From 85273e3be84b132dcd0f1d2a53bc66ebe997cdd1 Mon Sep 17 00:00:00 2001 From: Brandon Shien <44730413+bshien@users.noreply.github.com> Date: Tue, 10 Dec 2024 16:41:04 -0800 Subject: [PATCH 78/80] Added release notes for 1.3.20 (#9033) Signed-off-by: Brandon Shien --- ...opensearch-dashboards.release-notes-1.3.20.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 release-notes/opensearch-dashboards.release-notes-1.3.20.md diff --git a/release-notes/opensearch-dashboards.release-notes-1.3.20.md b/release-notes/opensearch-dashboards.release-notes-1.3.20.md new file mode 100644 index 000000000000..b03051525446 --- /dev/null +++ b/release-notes/opensearch-dashboards.release-notes-1.3.20.md @@ -0,0 +1,16 @@ +# Version 1.3.20 Release Notes + +### 🛡 Security + +- [CVE-2024-45590] Bump body-parser from 1.19.0 to 1.20.3 ([#9007](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/9007)) +- [CVE-2024-45296] Bump various version of path-to-regexp to required versions ([#9007](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/9007)) + +### 📈 Features/Enhancements + +### 🐛 Bug Fixes + +### 🚞 Infrastructure + +### 📝 Documentation + +### 🛠 Maintenance \ No newline at end of file From ba783498ae375f99a5f2ac97376e8c4e1181504c Mon Sep 17 00:00:00 2001 From: Federico Silva Date: Thu, 12 Dec 2024 11:30:10 -0300 Subject: [PATCH 79/80] finish testid-140 --- .../sidebar_test_spec.js | 303 +++++++++++------- .../data_explorer/data_explorer_page.po.js | 47 ++- .../dashboards/data_explorer/elements.js | 2 + .../public/components/sidebar/index.tsx | 1 + 4 files changed, 229 insertions(+), 124 deletions(-) diff --git a/cypress/integration/core_opensearch_dashboards/sidebar_test_spec.js b/cypress/integration/core_opensearch_dashboards/sidebar_test_spec.js index fc421373c52c..8455058b010c 100644 --- a/cypress/integration/core_opensearch_dashboards/sidebar_test_spec.js +++ b/cypress/integration/core_opensearch_dashboards/sidebar_test_spec.js @@ -14,182 +14,253 @@ describe('sidebar spec', function () { miscUtils.visitPage('app/data-explorer/discover'); }); - describe('filter by sidebar fields', function () { + describe('sidebar fields', function () { describe('add fields', function () { - const expectedValues = ['50', '57', '52', '66', '46']; - const testFields = ['_id', 'age', 'birthdate', 'salary']; - const pplQuery = 'source = vis-builder* | where age > 40'; - const sqlQuery = 'SELECT * FROM vis-builder* WHERE age > 40'; - - it('index pattern: DQL to PPL and SQL', function () { - DataExplorerPage.selectIndexPatternDataset('DQL'); - - DataExplorerPage.setQueryEditorLanguage('DQL'); - DataExplorerPage.setSearchRelativeDateRange('15', 'Years ago'); - DataExplorerPage.getDocTableHeader(1).should('have.text', '_source'); - + function addFields( + testFields, + expectedValues, + pplQuery, + sqlQuery, + indexPattern = true, + nested = false + ) { + const offset = indexPattern ? 1 : 0; // defines starting column + const dataColumnOffset = nested ? -1 : 0; + if (indexPattern) { + DataExplorerPage.selectIndexPatternDataset('DQL'); + DataExplorerPage.setQueryEditorLanguage('DQL'); + DataExplorerPage.setSearchRelativeDateRange('15', 'Years ago'); + } else { + if (nested) { + DataExplorerPage.selectIndexDataset( + 'OpenSearch SQL', + "I don't want to use the time filter", + 'cypress-test-os', + 'opensearch_dashboards_sample_data_ecommerce' + ); + } else { + DataExplorerPage.selectIndexDataset( + 'OpenSearch SQL', + "I don't want to use the time filter", + 'cypress-test-os', + 'vis-builder' + ); + } + } + // Check default column + DataExplorerPage.getDocTableHeader(0 + offset).should('have.text', '_source'); // Select some fields testFields.forEach((field) => { DataExplorerPage.getFieldBtnByName(field).click(); }); - - DataExplorerPage.getDocTableHeader(1).should('not.have.text', '_source'); - + // Check that the default column no longer exists + DataExplorerPage.getDocTableHeader(0 + offset).should('not.have.text', '_source'); // Check table headers persistence between DQL and PPL - DataExplorerPage.checkTableHeadersByArray(testFields); + DataExplorerPage.checkTableHeadersByArray(testFields, offset); DataExplorerPage.setQueryEditorLanguage('PPL'); - DataExplorerPage.checkTableHeadersByArray(testFields); - + DataExplorerPage.checkTableHeadersByArray(testFields, offset); // Remove some fields const firstTestField = testFields[0]; const secondTestField = testFields[1]; DataExplorerPage.getFieldBtnByName(firstTestField).click(); DataExplorerPage.getFieldBtnByName(secondTestField).click(); - DataExplorerPage.getDocTableHeader(1).should('not.have.text', firstTestField); - DataExplorerPage.getDocTableHeader(2).should('not.have.text', secondTestField); - + DataExplorerPage.getDocTableHeader(0 + offset).should('not.have.text', firstTestField); + DataExplorerPage.getDocTableHeader(1 + offset).should('not.have.text', secondTestField); // Remove all fields const thirdTestField = testFields[2]; const fourthTestField = testFields[3]; DataExplorerPage.getFieldBtnByName(thirdTestField).click(); DataExplorerPage.getFieldBtnByName(fourthTestField).click(); - DataExplorerPage.getDocTableHeader(1).should('have.text', '_source'); - DataExplorerPage.getDocTableHeader(2).should('not.exist'); - + DataExplorerPage.getDocTableHeader(0 + offset).should('have.text', '_source'); + DataExplorerPage.getDocTableHeader(1 + offset).should('not.exist'); // Select some fields testFields.forEach((field) => { DataExplorerPage.getFieldBtnByName(field).click(); }); // Check default column again - DataExplorerPage.getDocTableHeader(0).should('not.have.text', '_source'); + DataExplorerPage.getDocTableHeader(0 + offset).should('not.have.text', '_source'); // Check the columns match the selected fields - DataExplorerPage.checkTableHeadersByArray(testFields); - - // Validate default hits - DataExplorerPage.checkQueryHitsText('10,000'); - + DataExplorerPage.checkTableHeadersByArray(testFields, offset); + if (indexPattern) { + // Validate default hits + DataExplorerPage.checkQueryHitsText('10,000'); + } // Send PPL query cy.intercept('/api/enhancements/search/ppl').as('pplQuery'); DataExplorerPage.sendQueryOnMultilineEditor(pplQuery); cy.wait('@pplQuery').then(function () { // Check table headers persistence after PPL query - DataExplorerPage.checkTableHeadersByArray(testFields); - // Check filter was correctly applied - DataExplorerPage.checkQueryHitsText('6,588'); - + DataExplorerPage.checkTableHeadersByArray(testFields, offset); + if (indexPattern) { + // Check filter was correctly applied + DataExplorerPage.checkQueryHitsText('6,588'); + } // Validate the first 5 rows on the _id column - DataExplorerPage.checkDocTableColumnByArr(expectedValues, 2); + DataExplorerPage.checkDocTableColumnByArr(expectedValues, 1 + offset + dataColumnOffset); }); - // Send SQL query DataExplorerPage.setQueryEditorLanguage('OpenSearch SQL'); cy.intercept('/api/enhancements/search/sql').as('sqlQuery'); DataExplorerPage.sendQueryOnMultilineEditor(sqlQuery, false); cy.wait('@sqlQuery').then(function () { // Check table headers persistence after SQL query - DataExplorerPage.checkTableHeadersByArray(testFields); - + DataExplorerPage.checkTableHeadersByArray(testFields, offset); // Validate the first 5 rows on the _id column - DataExplorerPage.checkDocTableColumnByArr(expectedValues, 2); - }); - }); - - it('index: SQL and PPL', function () { - DataExplorerPage.selectIndexDataset( - 'OpenSearch SQL', - "I don't want to use the time filter" - ); - DataExplorerPage.getDocTableHeader(0).should('have.text', '_source'); - - // Select some fields - testFields.forEach((field) => { - DataExplorerPage.getFieldBtnByName(field).click(); + DataExplorerPage.checkDocTableColumnByArr(expectedValues, 1 + offset + dataColumnOffset); }); + } - DataExplorerPage.getDocTableHeader(0).should('not.have.text', '_source'); - - // Check table headers persistence between DQL and PPL - DataExplorerPage.checkTableHeadersByArray(testFields, 0); - DataExplorerPage.setQueryEditorLanguage('PPL'); - DataExplorerPage.checkTableHeadersByArray(testFields, 0); - - // Remove some fields - const firstTestField = testFields[0]; - const secondTestField = testFields[1]; - DataExplorerPage.getFieldBtnByName(firstTestField).click(); - DataExplorerPage.getFieldBtnByName(secondTestField).click(); - DataExplorerPage.getDocTableHeader(0).should('not.have.text', firstTestField); - DataExplorerPage.getDocTableHeader(1).should('not.have.text', secondTestField); - - // Remove all fields - const thirdTestField = testFields[2]; - const fourthTestField = testFields[3]; - DataExplorerPage.getFieldBtnByName(thirdTestField).click(); - DataExplorerPage.getFieldBtnByName(fourthTestField).click(); - DataExplorerPage.getDocTableHeader(0).should('have.text', '_source'); - DataExplorerPage.getDocTableHeader(1).should('not.exist'); + const pplQuery = 'source = vis-builder* | where age > 40'; + const sqlQuery = 'SELECT * FROM vis-builder* WHERE age > 40'; + const testFields = ['_id', 'age', 'birthdate', 'salary']; + const expectedIdValues = ['50', '57', '52', '66', '46']; + it('add field in index pattern: DQL to PPL and SQL', function () { + addFields(testFields, expectedIdValues, pplQuery, sqlQuery); + }); - // Select some fields - testFields.forEach((field) => { - DataExplorerPage.getFieldBtnByName(field).click(); - }); - DataExplorerPage.getDocTableHeader(0).should('not.have.text', '_source'); - DataExplorerPage.checkTableHeadersByArray(testFields, 0); + it('add field in index: SQL and PPL', function () { + addFields(testFields, expectedIdValues, pplQuery, sqlQuery, false); + }); - // Send PPL query - cy.intercept('/api/enhancements/search/ppl').as('pplQuery'); - DataExplorerPage.sendQueryOnMultilineEditor(pplQuery); - cy.wait('@pplQuery').then(function () { - // Check table headers persistence after PPL query - DataExplorerPage.checkTableHeadersByArray(testFields, 0); - // Validate the first 5 rows on the _id column - DataExplorerPage.checkDocTableColumnByArr(expectedValues, 1); - }); + const nestedTestFields = [ + 'geoip.region_name', + 'products.quantity', + 'event.dataset', + 'products.taxful_price', + ]; + const expectedRegionValues = [ + 'Cairo Governorate', + 'Dubai', + 'California', + ' - ', + 'Cairo Governorate', + ]; + it.skip('add nested field in index pattern: DQL to PPL and SQL', function () { + addFields(nestedTestFields, expectedRegionValues, pplQuery, sqlQuery, true, true); + }); - // Send SQL query - DataExplorerPage.setQueryEditorLanguage('OpenSearch SQL'); - cy.intercept('/api/enhancements/search/sql').as('sqlQuery'); - DataExplorerPage.sendQueryOnMultilineEditor(sqlQuery, false); - cy.wait('@sqlQuery').then(function () { - // Check table headers persistence after SQL query - DataExplorerPage.checkTableHeadersByArray(testFields, 0); - // Validate the first 5 rows on the _id column - DataExplorerPage.checkDocTableColumnByArr(expectedValues, 1); - }); + it('add field in index: SQL and PPL', function () { + addFields(nestedTestFields, expectedRegionValues, pplQuery, sqlQuery, false, true); }); }); describe('filter fields', function () { + function filterFields() { + DataExplorerPage.checkSidebarFilterBarResults('equal', 'categories'); + DataExplorerPage.checkSidebarFilterBarResults('include', 'a'); + DataExplorerPage.checkSidebarFilterBarResults('include', 'ag'); + DataExplorerPage.checkSidebarFilterBarNegativeResults('non-existent field'); + } + it('index pattern: DQL, PPL and SQL', function () { DataExplorerPage.selectIndexPatternDataset('DQL'); DataExplorerPage.setQueryEditorLanguage('DQL'); DataExplorerPage.setSearchRelativeDateRange('15', 'Years ago'); - DataExplorerPage.checkSidebarFilterBarResults('equal', 'categories'); - DataExplorerPage.checkSidebarFilterBarResults('include', 'a'); - DataExplorerPage.checkSidebarFilterBarResults('include', 'ag'); + filterFields(); DataExplorerPage.setQueryEditorLanguage('PPL'); - DataExplorerPage.checkSidebarFilterBarResults('equal', 'categories'); - DataExplorerPage.checkSidebarFilterBarResults('include', 'a'); - DataExplorerPage.checkSidebarFilterBarResults('include', 'ag'); + filterFields(); DataExplorerPage.setQueryEditorLanguage('OpenSearch SQL'); - DataExplorerPage.checkSidebarFilterBarResults('equal', 'categories'); - DataExplorerPage.checkSidebarFilterBarResults('include', 'a'); - DataExplorerPage.checkSidebarFilterBarResults('include', 'ag'); + filterFields(); }); it('index: PPL and SQL', function () { - DataExplorerPage.selectIndexDataset('PPL', "I don't want to use the time filter"); + DataExplorerPage.selectIndexDataset( + 'PPL', + "I don't want to use the time filter", + 'cypress-test-os', + 'vis-builder' + ); DataExplorerPage.setQueryEditorLanguage('PPL'); - DataExplorerPage.checkSidebarFilterBarResults('equal', 'categories'); - DataExplorerPage.checkSidebarFilterBarResults('include', 'a'); - DataExplorerPage.checkSidebarFilterBarResults('include', 'ag'); + filterFields(); DataExplorerPage.setQueryEditorLanguage('OpenSearch SQL'); - DataExplorerPage.checkSidebarFilterBarResults('equal', 'categories'); - DataExplorerPage.checkSidebarFilterBarResults('include', 'a'); - DataExplorerPage.checkSidebarFilterBarResults('include', 'ag'); + filterFields(); + }); + }); + + describe('side panel collapse/expand', function () { + function collapseAndExpand() { + DataExplorerPage.getSidebar().should('be.visible'); + DataExplorerPage.collapseSidebar(); + DataExplorerPage.getSidebar().should('not.be.visible'); + DataExplorerPage.expandSidebar(); + DataExplorerPage.getSidebar().should('be.visible'); + } + + function checkCollapseAndExpand(indexPattern = true) { + if (indexPattern) { + DataExplorerPage.setQueryEditorLanguage('DQL'); + collapseAndExpand(); + } + DataExplorerPage.setQueryEditorLanguage('PPL'); + collapseAndExpand(); + DataExplorerPage.setQueryEditorLanguage('OpenSearch SQL'); + collapseAndExpand(); + if (indexPattern) { + DataExplorerPage.setQueryEditorLanguage('DQL'); + DataExplorerPage.getSidebar().should('be.visible'); + } + } + + function checkCollapse(indexPattern = true) { + if (indexPattern) { + DataExplorerPage.setQueryEditorLanguage('DQL'); + DataExplorerPage.collapseSidebar(); + DataExplorerPage.getSidebar().should('not.be.visible'); + } + DataExplorerPage.setQueryEditorLanguage('PPL'); + if (!indexPattern) { + DataExplorerPage.collapseSidebar(); + } + DataExplorerPage.getSidebar().should('not.be.visible'); + DataExplorerPage.setQueryEditorLanguage('OpenSearch SQL'); + DataExplorerPage.getSidebar().should('not.be.visible'); + if (indexPattern) { + DataExplorerPage.setQueryEditorLanguage('DQL'); + DataExplorerPage.getSidebar().should('not.be.visible'); + } + } + + it('index pattern: collapse and expand for DQL, PPL and SQL', function () { + // this test case does three things: + // 1. checks the persistence of the sidebar state accross query languages + // 2. checks that the default state is expanded (first iteration of collapseAndExpand()) + // 3. collapses and expands the sidebar for every language + DataExplorerPage.selectIndexPatternDataset('DQL'); + checkCollapseAndExpand(); + }); + + it('index pattern: check collapsed state for DQL, PPL and SQL', function () { + // this test case checks that the sidebar remains collapsed accross query languages + DataExplorerPage.selectIndexPatternDataset('DQL'); + checkCollapse(); + }); + + it('index: collapse and expand for PPL and SQL', function () { + // this test case does three things: + // 1. checks the persistence of the sidebar state accross query languages + // 2. checks that the default state is expanded (first iteration of collapseAndExpand()) + // 3. collapses and expands the sidebar for every language + DataExplorerPage.selectIndexDataset( + 'PPL', + "I don't want to use the time filter", + 'cypress-test-os', + 'vis-builder' + ); + checkCollapseAndExpand(false); + }); + + it('index: check collapsed state for PPL and SQL', function () { + // this test case checks that the sidebar remains collapsed accross query languages + DataExplorerPage.selectIndexDataset( + 'PPL', + "I don't want to use the time filter", + 'cypress-test-os', + 'vis-builder' + ); + checkCollapse(false); }); }); }); diff --git a/cypress/utils/dashboards/data_explorer/data_explorer_page.po.js b/cypress/utils/dashboards/data_explorer/data_explorer_page.po.js index ee64479e8ca3..c206b709d62b 100644 --- a/cypress/utils/dashboards/data_explorer/data_explorer_page.po.js +++ b/cypress/utils/dashboards/data_explorer/data_explorer_page.po.js @@ -4,7 +4,7 @@ */ import { DATA_EXPLORER_PAGE_ELEMENTS } from './elements.js'; -import { INDEX_CLUSTER_NAME, INDEX_NAME, INDEX_PATTERN_NAME } from './constants.js'; +import { INDEX_PATTERN_NAME } from './constants.js'; export class DataExplorerPage { /** @@ -127,7 +127,9 @@ export class DataExplorerPage { .then(function ($content) { const contentLen = $content.length; const deletionType = del ? '{del}' : '{backspace}'; - DataExplorerPage.getQueryMultilineEditor().type(deletionType.repeat(contentLen)); + DataExplorerPage.getQueryMultilineEditor().type(deletionType.repeat(contentLen), { + force: true, + }); }); } @@ -138,7 +140,7 @@ export class DataExplorerPage { */ static sendQueryOnMultilineEditor(query, del = true) { DataExplorerPage.clearQueryMultilineEditor(del); - DataExplorerPage.getQueryMultilineEditor().type(query); + DataExplorerPage.getQueryMultilineEditor().type(query, { force: true }); DataExplorerPage.getQuerySubmitBtn().click(); } @@ -228,6 +230,27 @@ export class DataExplorerPage { return cy.get('[data-test-subj^="field-"]:not([data-test-subj$="showDetails"])'); } + static getSidebar() { + return cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.SIDEBAR_PANEL_OWNREFERENCE); + } + + static getResizeableBar() { + return cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.SIDEBAR_PANEL_RESIZEABLE_BAR); + } + + static getResizeableToggleButton() { + return cy.get('.euiResizableToggleButton'); + } + + static collapseSidebar() { + DataExplorerPage.getResizeableBar().trigger('mouseover').click(); + DataExplorerPage.getResizeableToggleButton().click(); + } + + static expandSidebar() { + DataExplorerPage.getResizeableToggleButton().click(); + } + /** * Check the results of the sidebar filter bar search. * @param search string to look up @@ -246,6 +269,16 @@ export class DataExplorerPage { DataExplorerPage.clearSidebarFilterBar(); } + /** + * Checks that the searched non-existent field does not appear on the DOM. + * @param search non-existent field + */ + static checkSidebarFilterBarNegativeResults(search) { + DataExplorerPage.getSidebarFilterBar().type(search); + DataExplorerPage.getAllSidebarAddFields().should('not.exist'); + DataExplorerPage.clearSidebarFilterBar(); + } + /** * Open window to select Dataset */ @@ -276,13 +309,11 @@ export class DataExplorerPage { * Select an index dataset. * @param datasetLanguage Index supports "OpenSearch SQL" and "PPL" */ - static selectIndexDataset(datasetLanguage, timeField) { + static selectIndexDataset(datasetLanguage, timeField, indexCluster, indexName) { 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.getDatasetExplorerWindow().contains(indexCluster, { timeout: 10000 }).click(); + DataExplorerPage.getDatasetExplorerWindow().contains(indexName, { timeout: 10000 }).click(); DataExplorerPage.getDatasetExplorerNextButton().click(); DataExplorerPage.selectIndexDatasetLanguage(datasetLanguage, timeField); } diff --git a/cypress/utils/dashboards/data_explorer/elements.js b/cypress/utils/dashboards/data_explorer/elements.js index b484a6500fbb..b87495995ba2 100644 --- a/cypress/utils/dashboards/data_explorer/elements.js +++ b/cypress/utils/dashboards/data_explorer/elements.js @@ -28,5 +28,7 @@ export const DATA_EXPLORER_PAGE_ELEMENTS = { QUERY_EDITOR_MULTILINE: 'osdQueryEditor__multiLine', GLOBAL_QUERY_EDITOR_FILTER_VALUE: 'globalFilterLabelValue', GLOBAL_FILTER_BAR: 'globalFilterBar', + SIDEBAR_PANEL_OWNREFERENCE: 'sidebarPanel', + SIDEBAR_PANEL_RESIZEABLE_BAR: 'euiResizableButton', SIDEBAR_FILTER_BAR: 'fieldFilterSearchInput', }; diff --git a/src/plugins/data_explorer/public/components/sidebar/index.tsx b/src/plugins/data_explorer/public/components/sidebar/index.tsx index 6a4ef0585e33..2aa747abca86 100644 --- a/src/plugins/data_explorer/public/components/sidebar/index.tsx +++ b/src/plugins/data_explorer/public/components/sidebar/index.tsx @@ -125,6 +125,7 @@ export const Sidebar: FC = ({ children, datasetSelectorRef }) => { className="eui-yScroll deSidebar_panel" hasBorder={true} borderRadius="l" + data-test-subj="sidebarPanel" > {isEnhancementEnabled &&
} From aa289a9abbf27a608b4623a9567a61d512a70d05 Mon Sep 17 00:00:00 2001 From: Federico Silva Date: Fri, 13 Dec 2024 13:59:52 -0300 Subject: [PATCH 80/80] update tests to cope with the helper message breaking them --- cypress.config.ts | 1 + .../sidebar_test_spec.js | 9 +++-- cypress/support/e2e.js | 1 + cypress/utils/commands.js | 18 +++++----- .../data_explorer/data_explorer_page.po.js | 36 +++++++++++-------- .../dashboards/data_explorer/elements.js | 1 + .../editors/default_editor/index.tsx | 2 +- 7 files changed, 39 insertions(+), 29 deletions(-) diff --git a/cypress.config.ts b/cypress.config.ts index 67e7b4f5039b..1a9d8cc98028 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -7,6 +7,7 @@ import { defineConfig } from 'cypress'; import webpackPreprocessor from '@cypress/webpack-preprocessor'; module.exports = defineConfig({ + retries: 2, defaultCommandTimeout: 60000, requestTimeout: 60000, responseTimeout: 60000, diff --git a/cypress/integration/core_opensearch_dashboards/sidebar_test_spec.js b/cypress/integration/core_opensearch_dashboards/sidebar_test_spec.js index 8455058b010c..b90d212e0ef7 100644 --- a/cypress/integration/core_opensearch_dashboards/sidebar_test_spec.js +++ b/cypress/integration/core_opensearch_dashboards/sidebar_test_spec.js @@ -101,7 +101,7 @@ describe('sidebar spec', function () { // Send SQL query DataExplorerPage.setQueryEditorLanguage('OpenSearch SQL'); cy.intercept('/api/enhancements/search/sql').as('sqlQuery'); - DataExplorerPage.sendQueryOnMultilineEditor(sqlQuery, false); + DataExplorerPage.sendQueryOnMultilineEditor(sqlQuery); cy.wait('@sqlQuery').then(function () { // Check table headers persistence after SQL query DataExplorerPage.checkTableHeadersByArray(testFields, offset); @@ -139,7 +139,7 @@ describe('sidebar spec', function () { addFields(nestedTestFields, expectedRegionValues, pplQuery, sqlQuery, true, true); }); - it('add field in index: SQL and PPL', function () { + it('add nested field in index: SQL and PPL', function () { addFields(nestedTestFields, expectedRegionValues, pplQuery, sqlQuery, false, true); }); }); @@ -157,10 +157,8 @@ describe('sidebar spec', function () { DataExplorerPage.setQueryEditorLanguage('DQL'); DataExplorerPage.setSearchRelativeDateRange('15', 'Years ago'); filterFields(); - DataExplorerPage.setQueryEditorLanguage('PPL'); filterFields(); - DataExplorerPage.setQueryEditorLanguage('OpenSearch SQL'); filterFields(); }); @@ -174,9 +172,10 @@ describe('sidebar spec', function () { ); DataExplorerPage.setQueryEditorLanguage('PPL'); filterFields(); - DataExplorerPage.setQueryEditorLanguage('OpenSearch SQL'); filterFields(); + DataExplorerPage.setQueryEditorLanguage('Lucene'); + filterFields(); }); }); diff --git a/cypress/support/e2e.js b/cypress/support/e2e.js index fa35cf4214b4..bc7c1b25b8eb 100644 --- a/cypress/support/e2e.js +++ b/cypress/support/e2e.js @@ -4,3 +4,4 @@ */ import '../utils/commands'; +import '../utils/dashboards/data_explorer/commands'; diff --git a/cypress/utils/commands.js b/cypress/utils/commands.js index 0e5f6e92486f..c67cd85e1020 100644 --- a/cypress/utils/commands.js +++ b/cypress/utils/commands.js @@ -59,13 +59,15 @@ Cypress.Commands.add( * Go to the local instance of OSD's home page and login if needed. */ Cypress.Commands.add('localLogin', (username, password) => { - miscUtils.visitPage('/app/home'); - cy.url().then(($url) => { - if ($url.includes('login')) { - loginPage.enterUserName(username); - loginPage.enterPassword(password); - loginPage.submit(); - } - cy.url().should('contain', '/app/home'); + cy.session('test_automation', function () { + miscUtils.visitPage('/app/home'); + cy.url().then(($url) => { + if ($url.includes('login')) { + loginPage.enterUserName(username); + loginPage.enterPassword(password); + loginPage.submit(); + } + cy.url().should('contain', '/app/home'); + }); }); }); diff --git a/cypress/utils/dashboards/data_explorer/data_explorer_page.po.js b/cypress/utils/dashboards/data_explorer/data_explorer_page.po.js index c206b709d62b..1ed080833f3f 100644 --- a/cypress/utils/dashboards/data_explorer/data_explorer_page.po.js +++ b/cypress/utils/dashboards/data_explorer/data_explorer_page.po.js @@ -86,13 +86,19 @@ export class DataExplorerPage { .eq(columnNumber); } + /** + * Get page header. + */ + static getPageHeader() { + return cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.HEADER_GLOBAL_NAV); + } + /** * Get query multiline editor element. */ static getQueryMultilineEditor() { - return cy - .getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.QUERY_EDITOR_MULTILINE) - .find('textarea'); + DataExplorerPage.getPageHeader().click(); + return cy.get('.view-line'); } /** @@ -121,15 +127,13 @@ export class DataExplorerPage { * @param del true/false. true: Deletes character to the right of the cursor; false: Deletes character to the left of the cursor * @see https://docs.cypress.io/api/commands/type#Arguments */ - static clearQueryMultilineEditor(del = true) { + static clearQueryMultilineEditor() { DataExplorerPage.getQueryMultilineEditor() - .invoke('val') + .invoke('text') .then(function ($content) { - const contentLen = $content.length; - const deletionType = del ? '{del}' : '{backspace}'; - DataExplorerPage.getQueryMultilineEditor().type(deletionType.repeat(contentLen), { - force: true, - }); + const contentLen = $content.length + 1; + DataExplorerPage.getQueryMultilineEditor().type('a'); + DataExplorerPage.getQueryMultilineEditor().type('{backspace}'.repeat(contentLen)); }); } @@ -138,9 +142,9 @@ export class DataExplorerPage { * @param del true/false. true: Deletes character to the right of the cursor; false: Deletes character to the left of the cursor * @see https://docs.cypress.io/api/commands/type#Arguments */ - static sendQueryOnMultilineEditor(query, del = true) { - DataExplorerPage.clearQueryMultilineEditor(del); - DataExplorerPage.getQueryMultilineEditor().type(query, { force: true }); + static sendQueryOnMultilineEditor(query) { + DataExplorerPage.clearQueryMultilineEditor(); + DataExplorerPage.getQueryMultilineEditor().type(query); DataExplorerPage.getQuerySubmitBtn().click(); } @@ -149,6 +153,8 @@ export class DataExplorerPage { * @param language Accepted values: 'DQL', 'Lucene', 'OpenSearch SQL', 'PPL' */ static setQueryEditorLanguage(language) { + DataExplorerPage.getPageHeader().click(); // remove helper message + cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.QUERY_EDITOR_LANGUAGE_SELECTOR).click(); cy.getElementByTestId(DATA_EXPLORER_PAGE_ELEMENTS.QUERY_EDITOR_LANGUAGE_OPTIONS) @@ -244,7 +250,7 @@ export class DataExplorerPage { static collapseSidebar() { DataExplorerPage.getResizeableBar().trigger('mouseover').click(); - DataExplorerPage.getResizeableToggleButton().click(); + DataExplorerPage.getResizeableToggleButton().click({ force: true }); } static expandSidebar() { @@ -257,7 +263,7 @@ export class DataExplorerPage { * @param assertion the type of assertion that is going to be performed. Example: 'eq', 'include' */ static checkSidebarFilterBarResults(assertion, search) { - DataExplorerPage.getSidebarFilterBar().type(search); + DataExplorerPage.getSidebarFilterBar().type(search, { force: true }); DataExplorerPage.getAllSidebarAddFields().each(function ($field) { cy.wrap($field) .should('be.visible') diff --git a/cypress/utils/dashboards/data_explorer/elements.js b/cypress/utils/dashboards/data_explorer/elements.js index b87495995ba2..3346fe2a585f 100644 --- a/cypress/utils/dashboards/data_explorer/elements.js +++ b/cypress/utils/dashboards/data_explorer/elements.js @@ -4,6 +4,7 @@ */ export const DATA_EXPLORER_PAGE_ELEMENTS = { + HEADER_GLOBAL_NAV: 'headerGlobalNav', NEW_SEARCH_BUTTON: 'discoverNewButton', DISCOVER_QUERY_HITS: 'discoverQueryHits', DATASET_SELECTOR_BUTTON: 'datasetSelectorButton', diff --git a/src/plugins/data/public/ui/query_editor/editors/default_editor/index.tsx b/src/plugins/data/public/ui/query_editor/editors/default_editor/index.tsx index 1eaf373f2c8e..f642548bafc3 100644 --- a/src/plugins/data/public/ui/query_editor/editors/default_editor/index.tsx +++ b/src/plugins/data/public/ui/query_editor/editors/default_editor/index.tsx @@ -32,7 +32,7 @@ export const DefaultInput: React.FC = ({ provideCompletionItems, }) => { return ( -
+