diff --git a/DEVELOPER_GUIDE.md b/DEVELOPER_GUIDE.md index b084b32bd1f1..422784751761 100644 --- a/DEVELOPER_GUIDE.md +++ b/DEVELOPER_GUIDE.md @@ -925,30 +925,6 @@ license. The following developer guide rules are specific for working with the React framework. -#### Prefer reactDirective over react-component - -When using `ngReact` to embed your react components inside Angular HTML, prefer the -`reactDirective` service over the `react-component` directive. -You can read more about these two ngReact methods [here](https://github.com/ngReact/ngReact#features). - -Using `react-component` means adding a bunch of components into angular, while `reactDirective` keeps them isolated, and is also a more succinct syntax. - -**Good:** - -```html - -``` - -**Bad:** - -```html - -``` - #### Name action functions and prop functions appropriately Name action functions in the form of a strong verb and passed properties in the form of on. E.g: diff --git a/packages/osd-i18n/GUIDELINE.md b/packages/osd-i18n/GUIDELINE.md index ae5b2b5ca298..ac2fb0047c9b 100644 --- a/packages/osd-i18n/GUIDELINE.md +++ b/packages/osd-i18n/GUIDELINE.md @@ -92,17 +92,6 @@ The long term plan is to rely on using `FormattedMessage` and `i18n.translate()` Currently, we support the following ReactJS `i18n` tools, but they will be removed in future releases: - Usage of `props.intl.formatmessage()` (where `intl` is passed to `props` by `injectI18n` HOC). -#### In AngularJS - -The long term plan is to rely on using `i18n.translate()` by statically importing `i18n` from the `@osd/i18n` package. **Avoid using the `i18n` filter and the `i18n` service injected in controllers, directives, services.** - -- Call JS function `i18n.translate()` from the `@osd/i18n` package. -- Use `i18nId` directive in template. - -Currently, we support the following AngluarJS `i18n` tools, but they will be removed in future releases: -- Usage of `i18n` service in controllers, directives, services by injecting it. -- Usage of `i18n` filter in template for attribute translation. Note: Use one-time binding ("{{:: ... }}") in filters wherever it's possible to prevent unnecessary expression re-evaluation. - #### In JavaScript - Use `i18n.translate()` in NodeJS or any other framework agnostic code, where `i18n` is the I18n engine from `@osd/i18n` package. diff --git a/packages/osd-i18n/README.md b/packages/osd-i18n/README.md index 16e740e925d5..a58a15ab4fd9 100644 --- a/packages/osd-i18n/README.md +++ b/packages/osd-i18n/README.md @@ -1,14 +1,13 @@ # I18n -OpenSearch Dashboards relies on several UI frameworks (ReactJS and AngularJS) and +OpenSearch Dashboards relies on UI frameworks (ReactJS) and requires localization in different environments (browser and NodeJS). Internationalization engine is framework agnostic and consumable in -all parts of OpenSearch Dashboards (ReactJS, AngularJS and NodeJS). In order to simplify +all parts of OpenSearch Dashboards (ReactJS and NodeJS). In order to simplify internationalization in UI frameworks, the additional abstractions are -built around the I18n engine: `react-intl` for React and custom -components for AngularJS. [React-intl](https://github.com/yahoo/react-intl) +built around the I18n engine: `react-intl` for React. [React-intl](https://github.com/yahoo/react-intl) is built around [intl-messageformat](https://github.com/yahoo/intl-messageformat), -so both React and AngularJS frameworks use the same engine and the same +so the React framework uses the same engine and the same message syntax. ## Localization files @@ -343,98 +342,6 @@ export const MyComponent = injectI18n( ); ``` -## AngularJS - -The long term plan is to rely on using `i18n.translate()` by statically importing `i18n` from the `@osd/i18n` package. **Avoid using the `i18n` filter and the `i18n` service injected in controllers, directives, services.** - -AngularJS wrapper has 4 entities: translation `provider`, `service`, `directive` -and `filter`. Both the directive and the filter use the translation `service` -with i18n engine under the hood. - -The translation `provider` is used for `service` configuration and -has the following methods: -- `addMessages(messages: Map, [locale: string])` - provides a way to register -translations with the library -- `setLocale(locale: string)` - tells the library which language to use by given -language key -- `getLocale()` - returns the current locale -- `setDefaultLocale(locale: string)` - tells the library which language to fallback -when missing translations -- `getDefaultLocale()` - returns the default locale -- `setFormats(formats: object)` - supplies a set of options to the underlying formatter -- `getFormats()` - returns current formats -- `getRegisteredLocales()` - returns array of locales having translations -- `init(messages: Map)` - initializes the engine - -The translation `service` provides only one method: -- `i18n(id: string, { values: object, defaultMessage: string, description: string })` – -translate message by id - -The translation `filter` is used for attributes translation and has -the following syntax: -``` -{{ ::'translationId' | i18n: { values: object, defaultMessage: string, description: string } }} -``` - -Where: -- `translationId` - translation id to be translated -- `values` - values to pass into translation -- `defaultMessage` - will be used unless translation was successful (the final - fallback in english, will be used for generating `en.json`) -- `description` - optional context comment that will be extracted by i18n tools -and added as a comment next to translation message at `defaultMessages.json` - -The translation `directive` has the following syntax: -```html - -``` - -Where: -- `i18n-id` - translation id to be translated -- `i18n-default-message` - will be used unless translation was successful -- `i18n-values` - values to pass into translation -- `i18n-description` - optional context comment that will be extracted by i18n tools -and added as a comment next to translation message at `defaultMessages.json` - -If HTML rendering in `i18n-values` is required then value key in `i18n-values` object -should have `html_` prefix. Otherwise the value will be inserted to the message without -HTML rendering.\ -Example: -```html -

-``` - -Angular `I18n` module is placed into `autoload` module, so it will be -loaded automatically. After that we can use i18n directive in Angular templates: -```html - -``` - -In order to translate attributes in AngularJS we should use `i18nFilter`: -```html - -``` - ## I18n tools In order to simplify localization process, some additional tools were implemented: diff --git a/packages/osd-i18n/angular/package.json b/packages/osd-i18n/angular/package.json deleted file mode 100644 index 1979e988fa7c..000000000000 --- a/packages/osd-i18n/angular/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "browser": "../target/web/angular", - "main": "../target/node/angular", - "types": "../target/types/angular/index.d.ts" -} diff --git a/src/dev/i18n/README.md b/src/dev/i18n/README.md index 4a9e3f45eff0..7cfb938d38ec 100644 --- a/src/dev/i18n/README.md +++ b/src/dev/i18n/README.md @@ -4,7 +4,7 @@ ### Description -The tool is used to extract default messages from all `*.{js, ts, jsx, tsx, html }` files in provided plugins directories to a JSON file. +The tool is used to extract default messages from all `*.{js, ts, jsx, tsx }` files in provided plugins directories to a JSON file. It uses Babel to parse code and build an AST for each file or a single JS expression if whole file parsing is impossible. The tool is able to validate, extract and match IDs, default messages and descriptions only if they are defined statically and together, otherwise it will fail with detailed explanation. That means one can't define ID in one place and default message in another, or use function call to dynamically create default message etc. @@ -18,33 +18,6 @@ The `defaultMessage` must contain ICU references to all keys in the `values` and The `description` is optional, `values` is optional too unless `defaultMessage` references to it. -* **Angular (.html)** - - * **Filter** - - ``` - {{ ::'pluginNamespace.messageId' | i18n: { - defaultMessage: 'Default message string literal, {key}', - values: { key: 'value' }, - description: 'Message context or description' - } }} - ``` - - * Don't break `| i18n: {` with line breaks, and don't skip whitespaces around `i18n:`. - * `::` operator is optional. Omit it if you need data binding for the `values`. - - * **Directive** - - ```html -

- ``` - - * `html_` prefixes will be removed from `i18n-values` keys before validation. * **React (.jsx, .tsx)** diff --git a/src/dev/i18n/__fixtures__/extract_default_translations/test_plugin_1/test_file_4.html b/src/dev/i18n/__fixtures__/extract_default_translations/test_plugin_1/test_file_4.html deleted file mode 100644 index f725fa288405..000000000000 --- a/src/dev/i18n/__fixtures__/extract_default_translations/test_plugin_1/test_file_4.html +++ /dev/null @@ -1,8 +0,0 @@ - -
-
-
-
diff --git a/src/dev/i18n/__fixtures__/extract_default_translations/test_plugin_2/test_file.html b/src/dev/i18n/__fixtures__/extract_default_translations/test_plugin_2/test_file.html deleted file mode 100644 index c12843602b13..000000000000 --- a/src/dev/i18n/__fixtures__/extract_default_translations/test_plugin_2/test_file.html +++ /dev/null @@ -1 +0,0 @@ -

{{ ::'plugin_2.message-id' | i18n: { defaultMessage: 'Message text' } }}

diff --git a/src/dev/i18n/__fixtures__/extract_default_translations/test_plugin_2/test_file.jsx b/src/dev/i18n/__fixtures__/extract_default_translations/test_plugin_2/test_file.jsx new file mode 100644 index 000000000000..b3f0c8d2b9c1 --- /dev/null +++ b/src/dev/i18n/__fixtures__/extract_default_translations/test_plugin_2/test_file.jsx @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +i18n('plugin_2.message-id', { defaultMessage: 'Message text' }); diff --git a/src/dev/i18n/__fixtures__/extract_default_translations/test_plugin_4/test_file_4.jsx b/src/dev/i18n/__fixtures__/extract_default_translations/test_plugin_4/test_file_4.jsx new file mode 100644 index 000000000000..5ce7b916bcd4 --- /dev/null +++ b/src/dev/i18n/__fixtures__/extract_default_translations/test_plugin_4/test_file_4.jsx @@ -0,0 +1,18 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +/* eslint-disable */ +class Component extends PureComponent { + render() { + return ( +
+ +
+ ); + } +} \ No newline at end of file diff --git a/src/dev/i18n/__snapshots__/extract_default_translations.test.js.snap b/src/dev/i18n/__snapshots__/extract_default_translations.test.js.snap index 68ed0433f1ae..cf91d6252d05 100644 --- a/src/dev/i18n/__snapshots__/extract_default_translations.test.js.snap +++ b/src/dev/i18n/__snapshots__/extract_default_translations.test.js.snap @@ -33,6 +33,54 @@ Array [ ] `; +exports[`dev/i18n/extract_default_translations extracts messages from path to map 2`] = ` +Array [ + Array [ + "plugin_2.message-id", + Object { + "description": undefined, + "message": "Message text", + }, + ], +] +`; + +exports[`dev/i18n/extract_default_translations extracts messages from path to map 3`] = ` +Array [ + Array [ + "plugin_3.duplicate_id", + Object { + "description": undefined, + "message": "Message 1", + }, + ], +] +`; + +exports[`dev/i18n/extract_default_translations extracts messages from path to map 4`] = ` +Array [ + Array [ + "plugin_3.duplicate_id", + Object { + "description": undefined, + "message": "Message 1", + }, + ], +] +`; + +exports[`dev/i18n/extract_default_translations extracts messages from path to map 5`] = ` +Array [ + Array [ + "plugin_4.id_1", + Object { + "description": undefined, + "message": "Message 4", + }, + ], +] +`; + exports[`dev/i18n/extract_default_translations throws on id collision 1`] = ` Array [ " I18N ERROR  Error in src/dev/i18n/__fixtures__/extract_default_translations/test_plugin_3/test_file.jsx diff --git a/src/dev/i18n/extract_default_translations.test.js b/src/dev/i18n/extract_default_translations.test.js index ea4754f3645e..c995ec562735 100644 --- a/src/dev/i18n/extract_default_translations.test.js +++ b/src/dev/i18n/extract_default_translations.test.js @@ -42,6 +42,7 @@ const pluginsPaths = [ path.join(fixturesPath, 'test_plugin_2'), path.join(fixturesPath, 'test_plugin_3'), path.join(fixturesPath, 'test_plugin_3_additional_path'), + path.join(fixturesPath, 'test_plugin_4'), ]; const config = { @@ -52,17 +53,19 @@ const config = { 'src/dev/i18n/__fixtures__/extract_default_translations/test_plugin_3', 'src/dev/i18n/__fixtures__/extract_default_translations/test_plugin_3_additional_path', ], + plugin_4: ['src/dev/i18n/__fixtures__/extract_default_translations/test_plugin_4'], }, exclude: [], }; describe('dev/i18n/extract_default_translations', () => { test('extracts messages from path to map', async () => { - const [pluginPath] = pluginsPaths; - const resultMap = new Map(); + for (const pluginPath of pluginsPaths) { + const resultMap = new Map(); - await extractMessagesFromPathToMap(pluginPath, resultMap, config, new ErrorReporter()); - expect([...resultMap].sort()).toMatchSnapshot(); + await extractMessagesFromPathToMap(pluginPath, resultMap, config, new ErrorReporter()); + expect([...resultMap].sort()).toMatchSnapshot(); + } }); test('throws on id collision', async () => { @@ -88,11 +91,11 @@ describe('dev/i18n/extract_default_translations', () => { const id = 'plugin_3.message-id'; const filePath1 = path.resolve( __dirname, - '__fixtures__/extract_default_translations/test_plugin_3/test_file.html' + '__fixtures__/extract_default_translations/test_plugin_3/test_file.jsx' ); const filePath2 = path.resolve( __dirname, - '__fixtures__/extract_default_translations/test_plugin_3_additional_path/test_file.html' + '__fixtures__/extract_default_translations/test_plugin_3_additional_path/test_file.jsx' ); expect(() => validateMessageNamespace(id, filePath1, config.paths)).not.toThrow(); expect(() => validateMessageNamespace(id, filePath2, config.paths)).not.toThrow(); @@ -103,7 +106,7 @@ describe('dev/i18n/extract_default_translations', () => { const id = 'wrong_plugin_namespace.message-id'; const filePath = path.resolve( __dirname, - '__fixtures__/extract_default_translations/test_plugin_2/test_file.html' + '__fixtures__/extract_default_translations/test_plugin_2/test_file.jsx' ); expect(() => validateMessageNamespace(id, filePath, config.paths, { report })).not.toThrow(); diff --git a/src/plugins/discover/public/application/components/data_grid/data_grid_table.tsx b/src/plugins/discover/public/application/components/data_grid/data_grid_table.tsx index 701b3e296c0f..298643e2a2b7 100644 --- a/src/plugins/discover/public/application/components/data_grid/data_grid_table.tsx +++ b/src/plugins/discover/public/application/components/data_grid/data_grid_table.tsx @@ -30,6 +30,8 @@ export interface DataGridTableProps { sort: SortOrder[]; displayTimeColumn: boolean; services: DiscoverServices; + title?: string; + description?: string; isToolbarVisible?: boolean; isContextView?: boolean; isLoading?: boolean; @@ -46,6 +48,8 @@ export const DataGridTable = ({ sort, rows, displayTimeColumn, + title = '', + description = '', isToolbarVisible = true, isContextView = false, isLoading = false, @@ -168,7 +172,12 @@ export const DataGridTable = ({ indexPattern, }} > -
+
{table} diff --git a/src/plugins/discover/public/application/view_components/canvas/discover_table.tsx b/src/plugins/discover/public/application/view_components/canvas/discover_table.tsx index e292303d9f8d..3cdf48a30dfc 100644 --- a/src/plugins/discover/public/application/view_components/canvas/discover_table.tsx +++ b/src/plugins/discover/public/application/view_components/canvas/discover_table.tsx @@ -17,7 +17,7 @@ import { useDispatch, useSelector, } from '../../utils/state_management'; -import { ResultStatus, SearchData } from '../utils/use_search'; +import { ResultStatus, SearchData, useSearch } from '../utils/use_search'; import { IndexPatternField, opensearchFilters } from '../../../../../data/public'; import { DocViewFilterFn } from '../../doc_views/doc_views_types'; import { SortOrder } from '../../../saved_searches/types'; @@ -71,6 +71,7 @@ export const DiscoverTable = ({ history }: Props) => { ); const { rows } = fetchState || {}; + const { savedSearch } = useSearch(services); useEffect(() => { const subscription = data$.subscribe((next) => { @@ -107,6 +108,8 @@ export const DiscoverTable = ({ history }: Props) => { rows={rows} displayTimeColumn={displayTimeColumn} services={services} + title={savedSearch?.id ? savedSearch.title : ''} + description={savedSearch?.id ? savedSearch.description : ''} /> ); }; 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 2560fc6d3567..aa14f6e69dd8 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 @@ -67,6 +67,7 @@ export type RefetchSubject = Subject; * }, [data$]); */ export const useSearch = (services: DiscoverServices) => { + const initalSearchComplete = useRef(false); const [savedSearch, setSavedSearch] = useState(undefined); const { savedSearch: savedSearchId, sort, interval } = useSelector((state) => state.discover); const indexPattern = useIndexPattern(services); @@ -205,6 +206,8 @@ export const useSearch = (services: DiscoverServices) => { }); data.search.showError(error as Error); + } finally { + initalSearchComplete.current = true; } }, [ indexPattern, @@ -240,18 +243,29 @@ export const useSearch = (services: DiscoverServices) => { })(); }); - // kick off initial fetch - refetch$.next(); + // kick off initial refetch on page load + if (shouldSearchOnPageLoad() || initalSearchComplete.current === true) { + refetch$.next(); + } return () => { subscription.unsubscribe(); }; - }, [data$, data.query.queryString, filterManager, refetch$, timefilter, fetch, core.fatalErrors]); + }, [ + data$, + data.query.queryString, + filterManager, + refetch$, + timefilter, + fetch, + core.fatalErrors, + shouldSearchOnPageLoad, + ]); // Get savedSearch if it exists useEffect(() => { (async () => { - const savedSearchInstance = await getSavedSearchById(savedSearchId || ''); + const savedSearchInstance = await getSavedSearchById(savedSearchId); setSavedSearch(savedSearchInstance); // sync initial app filters from savedObject to filterManager diff --git a/src/plugins/discover/public/build_services.ts b/src/plugins/discover/public/build_services.ts index ebe4e80a70c5..785e72536417 100644 --- a/src/plugins/discover/public/build_services.ts +++ b/src/plugins/discover/public/build_services.ts @@ -78,7 +78,7 @@ export interface DiscoverServices { urlForwarding: UrlForwardingStart; timefilter: TimefilterContract; toastNotifications: ToastsStart; - getSavedSearchById: (id: string) => Promise; + getSavedSearchById: (id?: string) => Promise; getSavedSearchUrlById: (id: string) => Promise; uiSettings: IUiSettingsClient; visualizations: VisualizationsStart; @@ -107,7 +107,7 @@ export function buildServices( docLinks: core.docLinks, theme: plugins.charts.theme, filterManager: plugins.data.query.filterManager, - getSavedSearchById: async (id: string) => savedObjectService.get(id), + getSavedSearchById: async (id?: string) => savedObjectService.get(id), getSavedSearchUrlById: async (id: string) => savedObjectService.urlFor(id), history: getHistory, indexPatterns: plugins.data.indexPatterns, diff --git a/src/plugins/discover/public/embeddable/search_embeddable.tsx b/src/plugins/discover/public/embeddable/search_embeddable.tsx index 76b6b9f449c4..6a0fd097aeef 100644 --- a/src/plugins/discover/public/embeddable/search_embeddable.tsx +++ b/src/plugins/discover/public/embeddable/search_embeddable.tsx @@ -87,6 +87,7 @@ export interface SearchProps { isLoading?: boolean; displayTimeColumn?: boolean; services: DiscoverServices; + title?: string; } interface SearchEmbeddableConfig { @@ -226,6 +227,7 @@ export class SearchEmbeddable inspectorAdapters: this.inspectorAdaptors, rows: [], description: this.savedSearch.description, + title: this.savedSearch.title, services: this.services, indexPattern, isLoading: false, diff --git a/src/plugins/discover/public/embeddable/search_embeddable_component.tsx b/src/plugins/discover/public/embeddable/search_embeddable_component.tsx index f019d75db116..c8ae54a16429 100644 --- a/src/plugins/discover/public/embeddable/search_embeddable_component.tsx +++ b/src/plugins/discover/public/embeddable/search_embeddable_component.tsx @@ -38,6 +38,8 @@ export function SearchEmbeddableComponent({ searchProps }: SearchEmbeddableProps displayTimeColumn: searchProps.displayTimeColumn, services: searchProps.services, totalHitCount: searchProps.totalHitCount, + title: searchProps.title, + description: searchProps.description, } as DiscoverEmbeddableProps; return ( diff --git a/test/functional/apps/dashboard/dashboard_filter_bar.js b/test/functional/apps/dashboard/dashboard_filter_bar.js index 0d19c84a5089..9a0ce6a9042a 100644 --- a/test/functional/apps/dashboard/dashboard_filter_bar.js +++ b/test/functional/apps/dashboard/dashboard_filter_bar.js @@ -193,7 +193,12 @@ export default function ({ getService, getPageObjects }) { it('are added when a cell magnifying glass is clicked', async function () { await dashboardAddPanel.addSavedSearch('Rendering-Test:-saved-search'); await PageObjects.dashboard.waitForRenderComplete(); - await testSubjects.click('docTableCellFilter'); + + // Expand a doc row + await testSubjects.click('docTableExpandToggleColumn-0'); + + // Add a field filter + await testSubjects.click('tableDocViewRow-@message > addInclusiveFilterButton'); const filterCount = await filterBar.getFilterCount(); expect(filterCount).to.equal(1); diff --git a/test/functional/apps/dashboard/dashboard_filtering.js b/test/functional/apps/dashboard/dashboard_filtering.js index b3ff62c8b9da..e934169513f6 100644 --- a/test/functional/apps/dashboard/dashboard_filtering.js +++ b/test/functional/apps/dashboard/dashboard_filtering.js @@ -123,7 +123,7 @@ export default function ({ getService, getPageObjects }) { }); it('saved search is filtered', async () => { - await dashboardExpect.savedSearchRowCount(0); + await testSubjects.missingOrFail('euiDataGrid'); }); it('vega is filtered', async () => { @@ -171,7 +171,7 @@ export default function ({ getService, getPageObjects }) { }); it('saved search is filtered', async () => { - await dashboardExpect.savedSearchRowCount(0); + await testSubjects.missingOrFail('euiDataGrid'); }); it('vega is filtered', async () => { diff --git a/test/functional/apps/dashboard/dashboard_query_bar.js b/test/functional/apps/dashboard/dashboard_query_bar.js index f1c2893268a9..91885a282b15 100644 --- a/test/functional/apps/dashboard/dashboard_query_bar.js +++ b/test/functional/apps/dashboard/dashboard_query_bar.js @@ -49,7 +49,8 @@ export default function ({ getService, getPageObjects }) { await PageObjects.dashboard.loadSavedDashboard('dashboard with filter'); }); - it('causes panels to reload when refresh is clicked', async () => { + // https://github.com/opensearch-project/OpenSearch-Dashboards/issues/5116 + it.skip('causes panels to reload when refresh is clicked', async () => { await opensearchArchiver.unload('dashboard/current/data'); await queryBar.clickQuerySubmitButton(); diff --git a/test/functional/apps/home/_navigation.ts b/test/functional/apps/home/_navigation.ts index 03230f1270ed..733888f59711 100644 --- a/test/functional/apps/home/_navigation.ts +++ b/test/functional/apps/home/_navigation.ts @@ -33,14 +33,18 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const browser = getService('browser'); - const PageObjects = getPageObjects(['common', 'header', 'home', 'timePicker']); + const PageObjects = getPageObjects(['common', 'header', 'home', 'timePicker', 'discover']); const appsMenu = getService('appsMenu'); const opensearchArchiver = getService('opensearchArchiver'); + const opensearchDashboardsServer = getService('opensearchDashboardsServer'); describe('OpenSearch Dashboards browser back navigation should work', function describeIndexTests() { before(async () => { await opensearchArchiver.loadIfNeeded('discover'); await opensearchArchiver.loadIfNeeded('logstash_functional'); + await opensearchDashboardsServer.uiSettings.replace({ + defaultIndex: 'logstash-*', + }); }); it('detect navigate back issues', async () => { @@ -51,8 +55,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const homeUrl = await browser.getCurrentUrl(); // Navigate to discover app - await appsMenu.clickLink('Discover'); + await PageObjects.common.navigateToApp('discover'); const discoverUrl = await browser.getCurrentUrl(); + await PageObjects.timePicker.setDefaultAbsoluteRange(); const modifiedTimeDiscoverUrl = await browser.getCurrentUrl(); diff --git a/test/functional/apps/home/_sample_data.ts b/test/functional/apps/home/_sample_data.ts index 9f78c5eec4e6..4c1cf4f69ce1 100644 --- a/test/functional/apps/home/_sample_data.ts +++ b/test/functional/apps/home/_sample_data.ts @@ -125,8 +125,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await pieChart.expectPieSliceCount(4); log.debug('Checking area, bar and heatmap charts rendered'); await dashboardExpect.seriesElementCount(15); + // The saved search of data explorer now renders 100 lines max log.debug('Checking saved searches rendered'); - await dashboardExpect.savedSearchRowCount(50); + await dashboardExpect.savedSearchRowCount(100); log.debug('Checking input controls rendered'); await dashboardExpect.inputControlItemCount(3); log.debug('Checking tag cloud rendered'); diff --git a/test/functional/apps/management/_index_pattern_results_sort.js b/test/functional/apps/management/_index_pattern_results_sort.js index 1823c678b2bb..f5c1b7114d9c 100644 --- a/test/functional/apps/management/_index_pattern_results_sort.js +++ b/test/functional/apps/management/_index_pattern_results_sort.js @@ -39,6 +39,12 @@ export default function ({ getService, getPageObjects }) { before(async function () { // delete .kibana index and then wait for OpenSearch Dashboards to re-create it await opensearchDashboardsServer.uiSettings.replace({}); + await PageObjects.settings.navigateTo(); + await PageObjects.settings.createIndexPattern(); + }); + + after(async function () { + return await PageObjects.settings.removeIndexPattern(); }); const columns = [ @@ -64,14 +70,6 @@ export default function ({ getService, getPageObjects }) { columns.forEach(function (col) { describe('sort by heading - ' + col.heading, function indexPatternCreation() { - before(async function () { - await PageObjects.settings.createIndexPattern(); - }); - - after(async function () { - return await PageObjects.settings.removeIndexPattern(); - }); - it('should sort ascending', async function () { await PageObjects.settings.sortBy(col.heading); const rowText = await col.selector(); @@ -85,17 +83,9 @@ export default function ({ getService, getPageObjects }) { }); }); }); + describe('field list pagination', function () { const EXPECTED_FIELD_COUNT = 86; - - before(async function () { - await PageObjects.settings.createIndexPattern(); - }); - - after(async function () { - return await PageObjects.settings.removeIndexPattern(); - }); - it('makelogs data should have expected number of fields', async function () { await retry.try(async function () { const TabCount = await PageObjects.settings.getFieldsTabCount(); diff --git a/test/functional/apps/management/_opensearch_dashboards_settings.js b/test/functional/apps/management/_opensearch_dashboards_settings.js index 98cda687e23b..0e310953e8a2 100644 --- a/test/functional/apps/management/_opensearch_dashboards_settings.js +++ b/test/functional/apps/management/_opensearch_dashboards_settings.js @@ -39,8 +39,8 @@ export default function ({ getService, getPageObjects }) { before(async function () { // delete .kibana index and then wait for OpenSearch Dashboards to re-create it await opensearchDashboardsServer.uiSettings.replace({}); - await PageObjects.settings.createIndexPattern('logstash-*'); await PageObjects.settings.navigateTo(); + await PageObjects.settings.createIndexPattern('logstash-*'); }); after(async function afterAll() { diff --git a/test/functional/apps/management/_scripted_fields.js b/test/functional/apps/management/_scripted_fields.js index 7d9ae2be1166..7acceff07fce 100644 --- a/test/functional/apps/management/_scripted_fields.js +++ b/test/functional/apps/management/_scripted_fields.js @@ -161,6 +161,7 @@ export default function ({ getService, getPageObjects }) { const fromTime = 'Sep 17, 2015 @ 06:31:44.000'; const toTime = 'Sep 18, 2015 @ 18:31:44.000'; await PageObjects.common.navigateToApp('discover'); + await PageObjects.discover.selectIndexPattern('logstash-*'); await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); await PageObjects.discover.clickFieldListItem(scriptedPainlessFieldName); @@ -169,31 +170,31 @@ export default function ({ getService, getPageObjects }) { }); await PageObjects.header.waitUntilLoadingHasFinished(); - await retry.try(async function () { - const rowData = await PageObjects.discover.getDocTableIndex(1); - expect(rowData).to.be('Sep 18, 2015 @ 18:20:57.916\n18'); - }); + const rowData = await PageObjects.discover.getDataGridTableValues(); + expect(rowData[0][0]).to.be('Sep 18, 2015 @ 18:20:57.916'); + expect(rowData[0][1]).to.be('18'); }); //add a test to sort numeric scripted field it('should sort scripted field value in Discover', async function () { - await testSubjects.click(`docTableHeaderFieldSort_${scriptedPainlessFieldName}`); - // after the first click on the scripted field, it becomes secondary sort after time. - // click on the timestamp twice to make it be the secondary sort key. - await testSubjects.click('docTableHeaderFieldSort_@timestamp'); - await testSubjects.click('docTableHeaderFieldSort_@timestamp'); + await testSubjects.click(`dataGridHeaderCell-${scriptedPainlessFieldName}`); + await PageObjects.discover.clickTableHeaderListItem(scriptedPainlessFieldName, 'Sort A-Z'); await PageObjects.header.waitUntilLoadingHasFinished(); - await retry.try(async function () { - const rowData = await PageObjects.discover.getDocTableIndex(1); - expect(rowData).to.be('Sep 17, 2015 @ 10:53:14.181\n-1'); - }); - - await testSubjects.click(`docTableHeaderFieldSort_${scriptedPainlessFieldName}`); + await testSubjects.click('dataGridHeaderCell-@timestamp'); + await PageObjects.discover.clickTableHeaderListItem('@timestamp', 'Sort A-Z'); await PageObjects.header.waitUntilLoadingHasFinished(); - await retry.try(async function () { - const rowData = await PageObjects.discover.getDocTableIndex(1); - expect(rowData).to.be('Sep 17, 2015 @ 06:32:29.479\n20'); - }); + const sortedDataByTimeField = await PageObjects.discover.getDataGridTableValues(); + expect(sortedDataByTimeField[0][0]).contain('Sep 17, 2015 @ 10:53:14.181'); + expect(sortedDataByTimeField[0][1]).contain('-1'); + + // click the column sorting button to remove painless field sort + // should sort only by time field + await testSubjects.click('dataGridColumnSortingButton'); + await PageObjects.discover.removeSort(`${scriptedPainlessFieldName}`); + await PageObjects.header.waitUntilLoadingHasFinished(); + const sortedDataByPainlessField = await PageObjects.discover.getDataGridTableValues(); + expect(sortedDataByPainlessField[0][0]).contain('Sep 17, 2015 @ 06:32:29.479'); + expect(sortedDataByPainlessField[0][1]).contain('20'); }); it('should filter by scripted field value in Discover', async function () { @@ -278,6 +279,7 @@ export default function ({ getService, getPageObjects }) { const fromTime = 'Sep 17, 2015 @ 06:31:44.000'; const toTime = 'Sep 18, 2015 @ 18:31:44.000'; await PageObjects.common.navigateToApp('discover'); + await PageObjects.discover.selectIndexPattern('logstash-*'); await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); await PageObjects.discover.clickFieldListItem(scriptedPainlessFieldName2); @@ -286,31 +288,31 @@ export default function ({ getService, getPageObjects }) { }); await PageObjects.header.waitUntilLoadingHasFinished(); - await retry.try(async function () { - const rowData = await PageObjects.discover.getDocTableIndex(1); - expect(rowData).to.be('Sep 18, 2015 @ 18:20:57.916\ngood'); - }); + const rowData = await PageObjects.discover.getDataGridTableValues(); + expect(rowData[0][0]).to.be('Sep 18, 2015 @ 18:20:57.916'); + expect(rowData[0][1]).to.be('good'); }); //add a test to sort string scripted field it('should sort scripted field value in Discover', async function () { - await testSubjects.click(`docTableHeaderFieldSort_${scriptedPainlessFieldName2}`); - // after the first click on the scripted field, it becomes secondary sort after time. - // click on the timestamp twice to make it be the secondary sort key. - await testSubjects.click('docTableHeaderFieldSort_@timestamp'); - await testSubjects.click('docTableHeaderFieldSort_@timestamp'); + await testSubjects.click(`dataGridHeaderCell-${scriptedPainlessFieldName2}`); + await PageObjects.discover.clickTableHeaderListItem(scriptedPainlessFieldName2, 'Sort A-Z'); await PageObjects.header.waitUntilLoadingHasFinished(); - await retry.try(async function () { - const rowData = await PageObjects.discover.getDocTableIndex(1); - expect(rowData).to.be('Sep 17, 2015 @ 09:48:40.594\nbad'); - }); - - await testSubjects.click(`docTableHeaderFieldSort_${scriptedPainlessFieldName2}`); + await testSubjects.click('dataGridHeaderCell-@timestamp'); + await PageObjects.discover.clickTableHeaderListItem('@timestamp', 'Sort A-Z'); await PageObjects.header.waitUntilLoadingHasFinished(); - await retry.try(async function () { - const rowData = await PageObjects.discover.getDocTableIndex(1); - expect(rowData).to.be('Sep 17, 2015 @ 06:32:29.479\ngood'); - }); + const sortedDataByTimeField = await PageObjects.discover.getDataGridTableValues(); + expect(sortedDataByTimeField[0][0]).contain('Sep 17, 2015 @ 09:48:40.594'); + expect(sortedDataByTimeField[0][1]).contain('bad'); + + // click the column sorting button to remove painless field sort + // should sort only by time field + await testSubjects.click('dataGridColumnSortingButton'); + await PageObjects.discover.removeSort(`${scriptedPainlessFieldName2}`); + await PageObjects.header.waitUntilLoadingHasFinished(); + const sortedDataByPainlessField = await PageObjects.discover.getDataGridTableValues(); + expect(sortedDataByPainlessField[0][0]).contain('Sep 17, 2015 @ 06:32:29.479'); + expect(sortedDataByPainlessField[0][1]).contain('good'); }); it('should filter by scripted field value in Discover', async function () { @@ -373,6 +375,7 @@ export default function ({ getService, getPageObjects }) { const fromTime = 'Sep 17, 2015 @ 06:31:44.000'; const toTime = 'Sep 18, 2015 @ 18:31:44.000'; await PageObjects.common.navigateToApp('discover'); + await PageObjects.discover.selectIndexPattern('logstash-*'); await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); await PageObjects.discover.clickFieldListItem(scriptedPainlessFieldName2); @@ -381,10 +384,33 @@ export default function ({ getService, getPageObjects }) { }); await PageObjects.header.waitUntilLoadingHasFinished(); - await retry.try(async function () { - const rowData = await PageObjects.discover.getDocTableIndex(1); - expect(rowData).to.be('Sep 18, 2015 @ 18:20:57.916\ntrue'); - }); + const rowData = await PageObjects.discover.getDataGridTableValues(); + expect(rowData[0][0]).to.be('Sep 18, 2015 @ 18:20:57.916'); + expect(rowData[0][1]).to.be('true'); + }); + + // existing bug: https://github.com/opensearch-project/OpenSearch-Dashboards/issues/5126 hence the issue is skipped + // TODO: replace updateExpectedResultHere with actual data value once bug is fixed + it.skip('should sort scripted field value in Discover', async function () { + await testSubjects.click(`dataGridHeaderCell-${scriptedPainlessFieldName2}`); + await PageObjects.discover.clickTableHeaderListItem( + scriptedPainlessFieldName2, + 'Sort True-False' + ); + await PageObjects.header.waitUntilLoadingHasFinished(); + await testSubjects.click('dataGridHeaderCell-@timestamp'); + await PageObjects.discover.clickTableHeaderListItem('@timestamp', 'Sort A-Z'); + await PageObjects.header.waitUntilLoadingHasFinished(); + const sortedDataByTimeField = await PageObjects.discover.getDataGridTableValues(); + expect(sortedDataByTimeField[0][0]).contain('updateExpectedResultHere'); + expect(sortedDataByTimeField[0][1]).contain('true'); + + await testSubjects.click('dataGridColumnSortingButton'); + await PageObjects.discover.removeSort(`${scriptedPainlessFieldName2}`); + await PageObjects.header.waitUntilLoadingHasFinished(); + const sortedDataByPainlessField = await PageObjects.discover.getDataGridTableValues(); + expect(sortedDataByPainlessField[0][0]).contain('updateExpectedResultHere'); + expect(sortedDataByPainlessField[0][1]).contain('false'); }); it('should filter by scripted field value in Discover', async function () { @@ -399,28 +425,6 @@ export default function ({ getService, getPageObjects }) { await filterBar.removeAllFilters(); }); - //add a test to sort boolean - //existing bug: https://github.com/elastic/kibana/issues/75519 hence the issue is skipped. - it.skip('should sort scripted field value in Discover', async function () { - await testSubjects.click(`docTableHeaderFieldSort_${scriptedPainlessFieldName2}`); - // after the first click on the scripted field, it becomes secondary sort after time. - // click on the timestamp twice to make it be the secondary sort key. - await testSubjects.click('docTableHeaderFieldSort_@timestamp'); - await testSubjects.click('docTableHeaderFieldSort_@timestamp'); - await PageObjects.header.waitUntilLoadingHasFinished(); - await retry.try(async function () { - const rowData = await PageObjects.discover.getDocTableIndex(1); - expect(rowData).to.be('updateExpectedResultHere\ntrue'); - }); - - await testSubjects.click(`docTableHeaderFieldSort_${scriptedPainlessFieldName2}`); - await PageObjects.header.waitUntilLoadingHasFinished(); - await retry.try(async function () { - const rowData = await PageObjects.discover.getDocTableIndex(1); - expect(rowData).to.be('updateExpectedResultHere\nfalse'); - }); - }); - it('should visualize scripted field in vertical bar chart', async function () { await PageObjects.discover.clickFieldListItemVisualize(scriptedPainlessFieldName2); await PageObjects.header.waitUntilLoadingHasFinished(); @@ -469,6 +473,7 @@ export default function ({ getService, getPageObjects }) { const fromTime = 'Sep 17, 2015 @ 19:22:00.000'; const toTime = 'Sep 18, 2015 @ 07:00:00.000'; await PageObjects.common.navigateToApp('discover'); + await PageObjects.discover.selectIndexPattern('logstash-*'); await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); await PageObjects.discover.clickFieldListItem(scriptedPainlessFieldName2); @@ -477,32 +482,30 @@ export default function ({ getService, getPageObjects }) { }); await PageObjects.header.waitUntilLoadingHasFinished(); - await retry.try(async function () { - const rowData = await PageObjects.discover.getDocTableIndex(1); - expect(rowData).to.be('Sep 18, 2015 @ 06:52:55.953\n2015-09-18 07:00'); - }); + const rowData = await PageObjects.discover.getDataGridTableValues(); + expect(rowData[0][0]).to.be('Sep 18, 2015 @ 06:52:55.953'); + expect(rowData[0][1]).to.be('2015-09-18 07:00'); }); - //add a test to sort date scripted field - //https://github.com/elastic/kibana/issues/75711 + // existing bug: https://github.com/opensearch-project/OpenSearch-Dashboards/issues/5127 hence the issue is skipped + // TODO: replace updateExpectedResultHere with actual data value once bug is fixed it.skip('should sort scripted field value in Discover', async function () { - await testSubjects.click(`docTableHeaderFieldSort_${scriptedPainlessFieldName2}`); - // after the first click on the scripted field, it becomes secondary sort after time. - // click on the timestamp twice to make it be the secondary sort key. - await testSubjects.click('docTableHeaderFieldSort_@timestamp'); - await testSubjects.click('docTableHeaderFieldSort_@timestamp'); + await testSubjects.click(`dataGridHeaderCell-${scriptedPainlessFieldName2}`); + await PageObjects.discover.clickTableHeaderListItem(scriptedPainlessFieldName2, 'Sort A-Z'); await PageObjects.header.waitUntilLoadingHasFinished(); - await retry.try(async function () { - const rowData = await PageObjects.discover.getDocTableIndex(1); - expect(rowData).to.be('updateExpectedResultHere\n2015-09-18 07:00'); - }); + await testSubjects.click('dataGridHeaderCell-@timestamp'); + await PageObjects.discover.clickTableHeaderListItem('@timestamp', 'Sort A-Z'); + await PageObjects.header.waitUntilLoadingHasFinished(); + const sortedDataByTimeField = await PageObjects.discover.getDataGridTableValues(); + expect(sortedDataByTimeField[0][0]).contain('updateExpectedResultHere'); + expect(sortedDataByTimeField[0][1]).contain('2015-09-18 07:00'); - await testSubjects.click(`docTableHeaderFieldSort_${scriptedPainlessFieldName2}`); + await testSubjects.click('dataGridColumnSortingButton'); + await PageObjects.discover.removeSort(`${scriptedPainlessFieldName2}`); await PageObjects.header.waitUntilLoadingHasFinished(); - await retry.try(async function () { - const rowData = await PageObjects.discover.getDocTableIndex(1); - expect(rowData).to.be('updateExpectedResultHere\n2015-09-18 07:00'); - }); + const sortedDataByPainlessField = await PageObjects.discover.getDataGridTableValues(); + expect(sortedDataByPainlessField[0][0]).contain('updateExpectedResultHere'); + expect(sortedDataByPainlessField[0][1]).contain('2015-09-18 07:00'); }); it('should filter by scripted field value in Discover', async function () { diff --git a/test/functional/apps/management/_scripted_fields_preview.js b/test/functional/apps/management/_scripted_fields_preview.js index adeefa118345..304f757d006a 100644 --- a/test/functional/apps/management/_scripted_fields_preview.js +++ b/test/functional/apps/management/_scripted_fields_preview.js @@ -38,9 +38,8 @@ export default function ({ getService, getPageObjects }) { describe('scripted fields preview', () => { before(async function () { await browser.setWindowSize(1200, 800); - await PageObjects.settings.createIndexPattern(); - await PageObjects.settings.navigateTo(); + await PageObjects.settings.createIndexPattern(); await PageObjects.settings.clickOpenSearchDashboardsIndexPatterns(); await PageObjects.settings.clickIndexPatternLogstash(); await PageObjects.settings.clickScriptedFieldsTab(); diff --git a/test/functional/page_objects/discover_page.ts b/test/functional/page_objects/discover_page.ts index f09be976e38e..f81c287a478f 100644 --- a/test/functional/page_objects/discover_page.ts +++ b/test/functional/page_objects/discover_page.ts @@ -206,6 +206,7 @@ export function DiscoverPageProvider({ getService, getPageObjects }: FtrProvider } public async getDocHeader() { + const table = this.dataGrid; const docHeader = await find.byCssSelector('thead > tr:nth-child(1)'); return await docHeader.getVisibleText(); } @@ -439,6 +440,82 @@ export function DiscoverPageProvider({ getService, getPageObjects }: FtrProvider public async clearSavedQuery() { await testSubjects.click('saved-query-management-clear-button'); } + + /** + * Retrieves data grid table values. + * + * This function fetches the values present in a data grid table. + * + * @returns {Promise} A promise resolving to the table values. + */ + public async getDataGridTableValues(): Promise { + return await dataGridTable.getDataGridTableValues(); + } + + /** + * Removes sorting from a specified column in a data grid. + * + * This function removes sorting of column if applied in the sort field. + * + * @param {string} columnName - The name of the column from which sorting should be removed. + * @returns {Promise} + */ + public async removeSort(columnName: string): Promise { + const parentDiv = await testSubjects.find( + `euiDataGridColumnSorting-sortColumn-${columnName}` + ); + + // Within this parent div, locate the button with the specified aria-label using CSS and click it + const cssSelector = `.euiDataGridColumnSorting__button[aria-label^="Remove from data grid sort: ${columnName}"]`; + const buttonToRemoveSort = await parentDiv.findByCssSelector(cssSelector); + await buttonToRemoveSort.click(); + } + + /** + * Clicks on a list item within the table column header based on column name and title. + * + * This function searches for a list item via title associated with a given column name. + * Once the item is found, its associated button is clicked. + * + * @param {string} columnName - The name of the column. + * @param {string} title - The title of the list item to be clicked. + * @returns {Promise} + * @throws Will throw an error if a clickable list item with the specified title is not found. + */ + public async clickTableHeaderListItem(columnName: string, title: string): Promise { + // locate the ul using the columnName + const ulElement = await testSubjects.find(`dataGridHeaderCellActionGroup-${columnName}`); + const $ = await ulElement.parseDomContent(); + + // loop through each
  • within the ul + const liElements = $('li').toArray(); + let index = 0; + + for (const liElement of liElements) { + const li = $(liElement); + + // Check if the li contains the isClickable class substring + if (li.is('li[class*="euiListGroupItem-isClickable"]')) { + const span = li.find(`span[title="${title}"]`); + + // If the span with the given title is found + if (span.length > 0) { + // find and click the button + const seleniumLiElement = await ulElement.findByCssSelector( + `li:nth-child(${index + 1}) button` + ); + // Click on the located WebElement + await seleniumLiElement.click(); + return; + } + } + index++; + } + + throw new Error( + `Could not find a clickable list item for column "${columnName}" with list item "${title}".` + ); + } } return new DiscoverPage(); diff --git a/test/functional/services/dashboard/expectations.ts b/test/functional/services/dashboard/expectations.ts index 4bdf355eb049..641c56b586fd 100644 --- a/test/functional/services/dashboard/expectations.ts +++ b/test/functional/services/dashboard/expectations.ts @@ -36,6 +36,7 @@ export function DashboardExpectProvider({ getService, getPageObjects }: FtrProvi const log = getService('log'); const retry = getService('retry'); const testSubjects = getService('testSubjects'); + const dataGrid = getService('dataGrid'); const find = getService('find'); const filterBar = getService('filterBar'); const PageObjects = getPageObjects(['dashboard', 'visualize']); @@ -233,11 +234,9 @@ export function DashboardExpectProvider({ getService, getPageObjects }: FtrProvi async savedSearchRowCount(expectedCount: number) { log.debug(`DashboardExpect.savedSearchRowCount(${expectedCount})`); await retry.try(async () => { - const savedSearchRows = await testSubjects.findAll( - 'docTableExpandToggleColumn', - findTimeout - ); - expect(savedSearchRows.length).to.be(expectedCount); + // Need to change it here to find out how many rows there are + const timeStamps = await dataGrid.getDataGridTableColumn('date'); + expect(timeStamps.length).to.be(expectedCount); }); } diff --git a/test/functional/services/data_grid.ts b/test/functional/services/data_grid.ts index a80c377e1a7d..b23eabb4ef01 100644 --- a/test/functional/services/data_grid.ts +++ b/test/functional/services/data_grid.ts @@ -42,6 +42,7 @@ export function DataGridProvider({ getService }: FtrProviderContext) { class DataGrid { // This test no longer works in the new data explorer data grid table // since each data grid table cell is now rendered differently + // https://github.com/opensearch-project/OpenSearch-Dashboards/issues/5108 async getDataGridTableData(): Promise { const table = await find.byCssSelector('.euiDataGrid'); const $ = await table.parseDomContent(); @@ -70,6 +71,34 @@ export function DataGridProvider({ getService }: FtrProviderContext) { }; } + /** + * Retrieves the values from a data grid table. + * + * The function fetches values present in a data grid table and organizes them into rows and columns. + * Each row is an array of strings, and the entire table is an array of such rows. + * + * @returns {Promise} A promise resolving to a 2D array of table values. + */ + async getDataGridTableValues(): Promise { + const table = await testSubjects.find('docTable'); + const $ = await table.parseDomContent(); + const cellsArr = $.findTestSubjects('dataGridRowCell').toArray(); + const rows: string[][] = []; + let rowIdx = -1; + + for (const cell of cellsArr) { + const cCell = $(cell); + const isFirstColumn = cCell.attr('class').includes('euiDataGridRowCell--firstColumn'); + if (isFirstColumn) { + rowIdx++; + rows[rowIdx] = []; + } else { + rows[rowIdx].push(this.getTextFromCell(cCell)); + } + } + return Promise.resolve(rows); + } + /** * Retrieves the header fields of the data grid. * @@ -99,6 +128,15 @@ export function DataGridProvider({ getService }: FtrProviderContext) { await find.clickByButtonText('Remove column'); } + /** + * Retrieves values from a specific column in a data grid table. + * + * This function targets a column based on a CSS class selector and retrieves its cell values. + * It makes use of the Cheerio library to parse and navigate the DOM. + * + * @param {string} selector - The CSS class suffix used to identify cells of the desired column. + * @returns {Promise} A promise resolving to an array of cell values from the specified column. + */ async getDataGridTableColumn(selector: string): Promise { const table = await find.byCssSelector('.euiDataGrid'); const $ = await table.parseDomContent(); @@ -110,12 +148,26 @@ export function DataGridProvider({ getService }: FtrProviderContext) { const cCell = $(cell); if (cCell.hasClass(`euiDataGridRowCell--${selector}`)) { // The column structure is very nested to get the actual text - columnValues.push(cCell.children().children().children().children().text()); + columnValues.push(this.getTextFromCell(cCell)); } }); return columnValues; } + + /** + * Extracts the text from a cell in the data grid. + * + * Given a cell represented by a Cheerio object, this function navigates its nested structure + * to extract the contained text. + * + * @param {any} cCell - The Cheerio representation of the cell from which text needs to be extracted. + * @returns {string} The extracted text from the cell. + */ + getTextFromCell(cCell: any): string { + // navigate the nested structure and get the text + return cCell.children().children().children().children().text(); + } } return new DataGrid();