From 0762566ec143fe9a8c1d2b61d4d4c33d64f825d3 Mon Sep 17 00:00:00 2001 From: Miki Date: Mon, 17 Apr 2023 15:23:52 -0700 Subject: [PATCH 01/35] Omit adding the `osd-version` header when the Fetch request is to an external origin (#3643) * Making `fetch` requests using core/public/http/fetch, an `osd-version` header is forcefully added, even to external requests. This change examines the destination and only adds the header to relative URLs and those that are to OSD itself. * This change also adds `osd-xsrf` to calls that use `osd-version` incorrectly to satisfy XSRF protection Fixes #3277 Signed-off-by: Miki --- CHANGELOG.md | 2 ++ src/core/public/http/fetch.ts | 14 +++++++++++++- .../__snapshots__/ui_settings_api.test.ts.snap | 7 +++++++ .../integration_tests/lifecycle_handlers.test.ts | 5 ++++- src/core/server/http/lifecycle_handlers.test.ts | 9 +++++---- src/core/server/http/lifecycle_handlers.ts | 3 ++- src/plugins/bfetch/public/plugin.ts | 1 + .../public/angular/angular_config.tsx | 4 ++++ 8 files changed, 38 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ad516b6c049f..c29de254733b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -71,6 +71,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [Vis Builder] Add metric to metric, bucket to bucket aggregation persistence ([#3495](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3495)) - Use mirrors to download Node.js binaries to escape sporadic 404 errors ([#3619](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3619)) - [Multiple DataSource] Refactor dev tool console to use opensearch-js client to send requests ([#3544](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3544)) +- Add `osd-xsrf` header to all requests that incorrectly used `node-version` to satisfy XSRF protection ([#3643](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3643)) - [Data] Add geo shape filter field ([#3605](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3605)) - [VisBuilder] Add UI actions handler ([#3732](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3732)) - [Dashboard] Indicate that IE is no longer supported ([#3641](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3641)) @@ -117,6 +118,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [Region Maps] Add ui setting to configure custom vector map's size parameter([#3399](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3399)) - [Search Telemetry] Fixes search telemetry's observable object that won't be GC-ed([#3390](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3390)) - Clean up and rebuild `@osd/pm` ([#3570](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3570)) +- Omit adding the `osd-version` header when the Fetch request is to an external origin ([#3643](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3643)) - [Vega] Add Filter custom label for opensearchDashboardsAddFilter ([#3640](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3640)) - [Timeline] Fix y-axis label color in dark mode ([#3698](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3698)) - [VisBuilder] Fix multiple warnings thrown on page load ([#3732](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3732)) diff --git a/src/core/public/http/fetch.ts b/src/core/public/http/fetch.ts index cefaa353f7fa..694372c46d99 100644 --- a/src/core/public/http/fetch.ts +++ b/src/core/public/http/fetch.ts @@ -31,6 +31,7 @@ import { omitBy } from 'lodash'; import { format } from 'url'; import { BehaviorSubject } from 'rxjs'; +import { isRelativeUrl } from '@osd/std'; import { IBasePath, @@ -144,7 +145,6 @@ export class Fetch { headers: removedUndefined({ 'Content-Type': 'application/json', ...options.headers, - 'osd-version': this.params.opensearchDashboardsVersion, }), }; @@ -158,6 +158,18 @@ export class Fetch { fetchOptions.headers['osd-system-request'] = 'true'; } + /* `osd-version` is used on the server-side to make sure that an incoming request originated from a front-end + * of the same version; see core/server/http/lifecycle_handlers.ts + * `osd-xsrf` is to satisfy XSRF protection but is only meaningful to OpenSearch Dashboards. + * + * If the `url` equals `basePath`, starts with `basePath` + '/', or is relative, add `osd-version` and `osd-xsrf` headers. + */ + const basePath = this.params.basePath.get(); + if (isRelativeUrl(url) || url === basePath || url.startsWith(`${basePath}/`)) { + fetchOptions.headers['osd-version'] = this.params.opensearchDashboardsVersion; + fetchOptions.headers['osd-xsrf'] = 'osd-fetch'; + } + return new Request(url, fetchOptions as RequestInit); } diff --git a/src/core/public/ui_settings/__snapshots__/ui_settings_api.test.ts.snap b/src/core/public/ui_settings/__snapshots__/ui_settings_api.test.ts.snap index f80d88dd91ff..52d04f52ff8b 100644 --- a/src/core/public/ui_settings/__snapshots__/ui_settings_api.test.ts.snap +++ b/src/core/public/ui_settings/__snapshots__/ui_settings_api.test.ts.snap @@ -9,6 +9,7 @@ Array [ "accept": "application/json", "content-type": "application/json", "osd-version": "opensearchDashboardsVersion", + "osd-xsrf": "osd-fetch", }, "method": "POST", }, @@ -20,6 +21,7 @@ Array [ "accept": "application/json", "content-type": "application/json", "osd-version": "opensearchDashboardsVersion", + "osd-xsrf": "osd-fetch", }, "method": "POST", }, @@ -36,6 +38,7 @@ Array [ "accept": "application/json", "content-type": "application/json", "osd-version": "opensearchDashboardsVersion", + "osd-xsrf": "osd-fetch", }, "method": "POST", }, @@ -47,6 +50,7 @@ Array [ "accept": "application/json", "content-type": "application/json", "osd-version": "opensearchDashboardsVersion", + "osd-xsrf": "osd-fetch", }, "method": "POST", }, @@ -63,6 +67,7 @@ Array [ "accept": "application/json", "content-type": "application/json", "osd-version": "opensearchDashboardsVersion", + "osd-xsrf": "osd-fetch", }, "method": "POST", }, @@ -74,6 +79,7 @@ Array [ "accept": "application/json", "content-type": "application/json", "osd-version": "opensearchDashboardsVersion", + "osd-xsrf": "osd-fetch", }, "method": "POST", }, @@ -113,6 +119,7 @@ Array [ "accept": "application/json", "content-type": "application/json", "osd-version": "opensearchDashboardsVersion", + "osd-xsrf": "osd-fetch", }, "method": "POST", }, diff --git a/src/core/server/http/integration_tests/lifecycle_handlers.test.ts b/src/core/server/http/integration_tests/lifecycle_handlers.test.ts index fc13c1ae3fbb..0742e8c08d6d 100644 --- a/src/core/server/http/integration_tests/lifecycle_handlers.test.ts +++ b/src/core/server/http/integration_tests/lifecycle_handlers.test.ts @@ -231,20 +231,23 @@ describe('core lifecycle handlers', () => { .expect(200, 'ok'); }); + // ToDo: Remove next; `osd-version` incorrectly used for satisfying XSRF protection it('accepts requests with the version header', async () => { await getSupertest(method.toLowerCase(), testPath) .set(versionHeader, actualVersion) .expect(200, 'ok'); }); + // ToDo: Rename next; `osd-version` incorrectly used for satisfying XSRF protection it('rejects requests without either an xsrf or version header', async () => { await getSupertest(method.toLowerCase(), testPath).expect(400, { statusCode: 400, error: 'Bad Request', - message: 'Request must contain a osd-xsrf header.', + message: 'Request must contain the osd-xsrf header.', }); }); + // ToDo: Rename next; `osd-version` incorrectly used for satisfying XSRF protection it('accepts whitelisted requests without either an xsrf or version header', async () => { await getSupertest(method.toLowerCase(), whitelistedTestPath).expect(200, 'ok'); }); diff --git a/src/core/server/http/lifecycle_handlers.test.ts b/src/core/server/http/lifecycle_handlers.test.ts index 6a49bbfa14fa..cd96f0ea7760 100644 --- a/src/core/server/http/lifecycle_handlers.test.ts +++ b/src/core/server/http/lifecycle_handlers.test.ts @@ -102,6 +102,7 @@ describe('xsrf post-auth handler', () => { expect(result).toEqual('next'); }); + // ToDo: Remove; `osd-version` incorrectly used for satisfying XSRF protection it('accepts requests with version header', () => { const config = createConfig({ xsrf: { whitelist: [], disableProtection: false } }); const handler = createXsrfPostAuthHandler(config); @@ -129,7 +130,7 @@ describe('xsrf post-auth handler', () => { expect(responseFactory.badRequest).toHaveBeenCalledTimes(1); expect(responseFactory.badRequest.mock.calls[0][0]).toMatchInlineSnapshot(` Object { - "body": "Request must contain a osd-xsrf header.", + "body": "Request must contain the osd-xsrf header.", } `); expect(result).toEqual('badRequest'); @@ -199,7 +200,7 @@ describe('versionCheck post-auth handler', () => { responseFactory = httpServerMock.createLifecycleResponseFactory(); }); - it('forward the request to the next interceptor if header matches', () => { + it('forward the request to the next interceptor if osd-version header matches the actual version', () => { const handler = createVersionCheckPostAuthHandler('actual-version'); const request = forgeRequest({ headers: { 'osd-version': 'actual-version' } }); @@ -212,7 +213,7 @@ describe('versionCheck post-auth handler', () => { expect(result).toBe('next'); }); - it('returns a badRequest error if header does not match', () => { + it('returns a badRequest error if osd-version header exists but does not match the actual version', () => { const handler = createVersionCheckPostAuthHandler('actual-version'); const request = forgeRequest({ headers: { 'osd-version': 'another-version' } }); @@ -236,7 +237,7 @@ describe('versionCheck post-auth handler', () => { expect(result).toBe('badRequest'); }); - it('forward the request to the next interceptor if header is not present', () => { + it('forward the request to the next interceptor if osd-version header is not present', () => { const handler = createVersionCheckPostAuthHandler('actual-version'); const request = forgeRequest({ headers: {} }); diff --git a/src/core/server/http/lifecycle_handlers.ts b/src/core/server/http/lifecycle_handlers.ts index 636bb8af4522..f17b07942e6a 100644 --- a/src/core/server/http/lifecycle_handlers.ts +++ b/src/core/server/http/lifecycle_handlers.ts @@ -54,8 +54,9 @@ export const createXsrfPostAuthHandler = (config: HttpConfig): OnPostAuthHandler const hasVersionHeader = VERSION_HEADER in request.headers; const hasXsrfHeader = XSRF_HEADER in request.headers; + // ToDo: Remove !hasVersionHeader; `osd-version` incorrectly used for satisfying XSRF protection if (!isSafeMethod(request.route.method) && !hasVersionHeader && !hasXsrfHeader) { - return response.badRequest({ body: `Request must contain a ${XSRF_HEADER} header.` }); + return response.badRequest({ body: `Request must contain the ${XSRF_HEADER} header.` }); } return toolkit.next(); diff --git a/src/plugins/bfetch/public/plugin.ts b/src/plugins/bfetch/public/plugin.ts index 1a3a0bffc821..e115b35a44e1 100644 --- a/src/plugins/bfetch/public/plugin.ts +++ b/src/plugins/bfetch/public/plugin.ts @@ -95,6 +95,7 @@ export class BfetchPublicPlugin url: `${basePath}/${removeLeadingSlash(params.url)}`, headers: { 'Content-Type': 'application/json', + 'osd-xsrf': 'osd-bfetch', 'osd-version': version, ...(params.headers || {}), }, diff --git a/src/plugins/opensearch_dashboards_legacy/public/angular/angular_config.tsx b/src/plugins/opensearch_dashboards_legacy/public/angular/angular_config.tsx index ef01d29e4b14..fbe36a289d70 100644 --- a/src/plugins/opensearch_dashboards_legacy/public/angular/angular_config.tsx +++ b/src/plugins/opensearch_dashboards_legacy/public/angular/angular_config.tsx @@ -159,6 +159,8 @@ export const $setupXsrfRequestInterceptor = (version: string) => { // Configure jQuery prefilter $.ajaxPrefilter(({ osdXsrfToken = true }: any, originalOptions, jqXHR) => { if (osdXsrfToken) { + jqXHR.setRequestHeader('osd-xsrf', 'osd-legacy'); + // ToDo: Remove next; `osd-version` incorrectly used for satisfying XSRF protection jqXHR.setRequestHeader('osd-version', version); } }); @@ -170,6 +172,8 @@ export const $setupXsrfRequestInterceptor = (version: string) => { request(opts) { const { osdXsrfToken = true } = opts as any; if (osdXsrfToken) { + set(opts, ['headers', 'osd-xsrf'], 'osd-legacy'); + // ToDo: Remove next; `osd-version` incorrectly used for satisfying XSRF protection set(opts, ['headers', 'osd-version'], version); } return opts; From fc195ea580844dea47e21c4854ac7767f2513531 Mon Sep 17 00:00:00 2001 From: Anan Zhuang Date: Mon, 17 Apr 2023 15:54:52 -0700 Subject: [PATCH 02/35] [Table Vis] move format table, consolidate types and add unit tests (#3397) Currently, table data is formatted by a until function convertToFormattedData in TableVisComponent. In this PR, we moved the formatting data process to table_vis_response_handler.ts to combine with other data process logics. In this way, component render and data handling logics are completely isolated. This PR also solidate some types. Issue Resolved: https://github.com/opensearch-project/OpenSearch-Dashboards/issues/3395 https://github.com/opensearch-project/OpenSearch-Dashboards/issues/2856 Signed-off-by: Anan Zhuang --- CHANGELOG.md | 1 + .../public/components/table_vis_app.scss | 16 +- .../public/components/table_vis_app.test.tsx | 98 ++++++++ .../public/components/table_vis_app.tsx | 17 +- .../public/components/table_vis_cell.test.tsx | 65 +++++ .../public/components/table_vis_cell.tsx | 38 +++ .../components/table_vis_component.test.tsx | 234 ++++++++++++++++++ .../public/components/table_vis_component.tsx | 117 ++++----- .../table_vis_component_group.test.tsx | 67 +++++ .../components/table_vis_component_group.tsx | 7 +- .../components/table_vis_grid_columns.tsx | 21 +- .../vis_type_table/public/table_vis_fn.ts | 4 +- .../public/table_vis_renderer.test.tsx | 78 ++++++ .../public/table_vis_response_handler.test.ts | 157 ++++++++++++ .../public/table_vis_response_handler.ts | 60 ++--- src/plugins/vis_type_table/public/types.ts | 7 - .../add_percentage_col.test.ts.snap | 170 +++++++++++++ .../public/utils/add_percentage_col.test.ts | 76 ++++++ .../public/utils/add_percentage_col.ts | 76 ++++++ .../public/utils/convert_to_csv_data.test.ts | 76 ++++++ .../public/utils/convert_to_csv_data.ts | 2 +- .../utils/convert_to_formatted_data.test.ts | 136 ++++++++++ .../public/utils/convert_to_formatted_data.ts | 69 ++---- .../public/utils/get_table_ui_state.test.ts | 66 +++++ .../public/utils/get_table_ui_state.ts | 40 +++ .../vis_type_table/public/utils/index.ts | 2 + .../public/utils/use_pagination.test.ts | 101 ++++++++ .../public/utils/use_pagination.ts | 23 +- 28 files changed, 1627 insertions(+), 197 deletions(-) create mode 100644 src/plugins/vis_type_table/public/components/table_vis_app.test.tsx create mode 100644 src/plugins/vis_type_table/public/components/table_vis_cell.test.tsx create mode 100644 src/plugins/vis_type_table/public/components/table_vis_cell.tsx create mode 100644 src/plugins/vis_type_table/public/components/table_vis_component.test.tsx create mode 100644 src/plugins/vis_type_table/public/components/table_vis_component_group.test.tsx create mode 100644 src/plugins/vis_type_table/public/table_vis_renderer.test.tsx create mode 100644 src/plugins/vis_type_table/public/table_vis_response_handler.test.ts create mode 100644 src/plugins/vis_type_table/public/utils/__snapshots__/add_percentage_col.test.ts.snap create mode 100644 src/plugins/vis_type_table/public/utils/add_percentage_col.test.ts create mode 100644 src/plugins/vis_type_table/public/utils/add_percentage_col.ts create mode 100644 src/plugins/vis_type_table/public/utils/convert_to_csv_data.test.ts create mode 100644 src/plugins/vis_type_table/public/utils/convert_to_formatted_data.test.ts create mode 100644 src/plugins/vis_type_table/public/utils/get_table_ui_state.test.ts create mode 100644 src/plugins/vis_type_table/public/utils/get_table_ui_state.ts create mode 100644 src/plugins/vis_type_table/public/utils/use_pagination.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index c29de254733b..001f9248f2b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -79,6 +79,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [Multiple DataSource] Allow create and distinguish index pattern with same name but from different datasources ([#3571](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3571)) - [Multiple DataSource] Integrate multiple datasource with dev tool console ([#3754](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3754)) - Add satisfaction survey link to help menu ([#3676] (https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3676)) +- [Table Visualization] Move format table, consolidate types and add unit tests ([#3397](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3397)) ### 🐛 Bug Fixes diff --git a/src/plugins/vis_type_table/public/components/table_vis_app.scss b/src/plugins/vis_type_table/public/components/table_vis_app.scss index 876847667418..aafcd40e7382 100644 --- a/src/plugins/vis_type_table/public/components/table_vis_app.scss +++ b/src/plugins/vis_type_table/public/components/table_vis_app.scss @@ -1,21 +1,29 @@ +// Container for the Table Visualization component .visTable { display: flex; flex-direction: column; flex: 1 0 0; overflow: auto; + + @include euiScrollBar; } +// Group container for table visualization components .visTable__group { padding: $euiSizeS; margin-bottom: $euiSizeL; + display: flex; + flex-direction: column; + flex: 0 0 auto; +} - > h3 { - text-align: center; - } +// Style for table component title +.visTable__component__title { + text-align: center; } +// Modifier for visTables__group when displayed in columns .visTable__groupInColumns { - display: flex; flex-direction: row; align-items: flex-start; } diff --git a/src/plugins/vis_type_table/public/components/table_vis_app.test.tsx b/src/plugins/vis_type_table/public/components/table_vis_app.test.tsx new file mode 100644 index 000000000000..37cb753765f8 --- /dev/null +++ b/src/plugins/vis_type_table/public/components/table_vis_app.test.tsx @@ -0,0 +1,98 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { render } from '@testing-library/react'; +import { coreMock } from '../../../../core/public/mocks'; +import { IInterpreterRenderHandlers } from 'src/plugins/expressions'; +import { TableVisApp } from './table_vis_app'; +import { TableVisConfig } from '../types'; +import { TableVisData } from '../table_vis_response_handler'; + +jest.mock('./table_vis_component_group', () => ({ + TableVisComponentGroup: () => ( +
TableVisComponentGroup
+ ), +})); + +jest.mock('./table_vis_component', () => ({ + TableVisComponent: () =>
TableVisComponent
, +})); + +describe('TableVisApp', () => { + const serviceMock = coreMock.createStart(); + const handlersMock = ({ + done: jest.fn(), + uiState: { + get: jest.fn((key) => { + switch (key) { + case 'vis.sortColumn': + return {}; + case 'vis.columnsWidth': + return []; + default: + return undefined; + } + }), + set: jest.fn(), + }, + event: 'event', + } as unknown) as IInterpreterRenderHandlers; + const visConfigMock = ({} as unknown) as TableVisConfig; + + it('should render TableVisComponent if no split table', () => { + const visDataMock = { + table: { + columns: [], + rows: [], + formattedColumns: [], + }, + tableGroups: [], + } as TableVisData; + const { getByTestId } = render( + + ); + expect(getByTestId('TableVisComponent')).toBeInTheDocument(); + }); + + it('should render TableVisComponentGroup component if split direction is column', () => { + const visDataMock = { + tableGroups: [], + direction: 'column', + } as TableVisData; + const { container, getByTestId } = render( + + ); + expect(container.outerHTML.includes('visTable visTable__groupInColumns')).toBe(true); + expect(getByTestId('TableVisComponentGroup')).toBeInTheDocument(); + }); + + it('should render TableVisComponentGroup component if split direction is row', () => { + const visDataMock = { + tableGroups: [], + direction: 'row', + } as TableVisData; + const { container, getByTestId } = render( + + ); + expect(container.outerHTML.includes('visTable')).toBe(true); + expect(getByTestId('TableVisComponentGroup')).toBeInTheDocument(); + }); +}); diff --git a/src/plugins/vis_type_table/public/components/table_vis_app.tsx b/src/plugins/vis_type_table/public/components/table_vis_app.tsx index af10500a1a92..81f4d775f1e5 100644 --- a/src/plugins/vis_type_table/public/components/table_vis_app.tsx +++ b/src/plugins/vis_type_table/public/components/table_vis_app.tsx @@ -4,20 +4,22 @@ */ import './table_vis_app.scss'; -import React, { useEffect, useState } from 'react'; +import React, { useEffect } from 'react'; import classNames from 'classnames'; import { CoreStart } from 'opensearch-dashboards/public'; import { I18nProvider } from '@osd/i18n/react'; import { IInterpreterRenderHandlers } from 'src/plugins/expressions'; +import { PersistedState } from '../../../visualizations/public'; import { OpenSearchDashboardsContextProvider } from '../../../opensearch_dashboards_react/public'; -import { TableContext } from '../table_vis_response_handler'; -import { TableVisConfig, ColumnSort, ColumnWidth, TableUiState } from '../types'; +import { TableVisData } from '../table_vis_response_handler'; +import { TableVisConfig } from '../types'; import { TableVisComponent } from './table_vis_component'; import { TableVisComponentGroup } from './table_vis_component_group'; +import { getTableUIState, TableUiState } from '../utils'; interface TableVisAppProps { services: CoreStart; - visData: TableContext; + visData: TableVisData; visConfig: TableVisConfig; handlers: IInterpreterRenderHandlers; } @@ -38,12 +40,7 @@ export const TableVisApp = ({ visTable__groupInColumns: direction === 'column', }); - // TODO: remove duplicate sort and width state - // Issue: https://github.com/opensearch-project/OpenSearch-Dashboards/issues/2704#issuecomment-1299380818 - const [sort, setSort] = useState({ colIndex: null, direction: null }); - const [width, setWidth] = useState([]); - - const tableUiState: TableUiState = { sort, setSort, width, setWidth }; + const tableUiState: TableUiState = getTableUIState(handlers.uiState as PersistedState); return ( diff --git a/src/plugins/vis_type_table/public/components/table_vis_cell.test.tsx b/src/plugins/vis_type_table/public/components/table_vis_cell.test.tsx new file mode 100644 index 000000000000..9b1b1c02ac40 --- /dev/null +++ b/src/plugins/vis_type_table/public/components/table_vis_cell.test.tsx @@ -0,0 +1,65 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { render } from '@testing-library/react'; + +import { OpenSearchDashboardsDatatableRow } from 'src/plugins/expressions'; +import { FormattedColumn } from '../types'; +import { getTableVisCellValue } from './table_vis_cell'; +import { FieldFormat } from 'src/plugins/data/public'; + +class MockFieldFormat extends FieldFormat { + convert = jest.fn(); +} + +describe('getTableVisCellValue', () => { + const mockFormatter = new MockFieldFormat(); + + const columns: FormattedColumn[] = [ + { + id: 'testId', + title: 'Test Column', + formatter: mockFormatter, + filterable: true, + }, + ]; + + const sortedRows: OpenSearchDashboardsDatatableRow[] = [ + { + testId: 'Test Value 1', + }, + { + testId: 'Test Value 2', + }, + ]; + + const TableCell = ({ rowIndex, columnId }: { rowIndex: number; columnId: string }) => { + const getCellValue = getTableVisCellValue(sortedRows, columns); + return getCellValue({ rowIndex, columnId }); + }; + + beforeEach(() => { + mockFormatter.convert.mockClear(); + }); + + test('should render cell value with correct formatting', () => { + mockFormatter.convert.mockReturnValueOnce('Test Value 1'); + const { getByText } = render(); + expect(mockFormatter.convert).toHaveBeenCalledWith('Test Value 1', 'html'); + expect(getByText('Test Value 1')).toBeInTheDocument(); + expect(getByText('Test Value 1').closest('strong')).toBeInTheDocument(); + }); + + test('should return null when rowIndex is out of bounds', () => { + const { container } = render(); + expect(container.firstChild).toBeNull(); + }); + + test('should return null when no matching columnId is found', () => { + const { container } = render(); + expect(container.firstChild).toBeNull(); + }); +}); diff --git a/src/plugins/vis_type_table/public/components/table_vis_cell.tsx b/src/plugins/vis_type_table/public/components/table_vis_cell.tsx new file mode 100644 index 000000000000..30c0877df701 --- /dev/null +++ b/src/plugins/vis_type_table/public/components/table_vis_cell.tsx @@ -0,0 +1,38 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import dompurify from 'dompurify'; + +import { OpenSearchDashboardsDatatableRow } from 'src/plugins/expressions'; +import { FormattedColumn } from '../types'; + +export const getTableVisCellValue = ( + sortedRows: OpenSearchDashboardsDatatableRow[], + columns: FormattedColumn[] +) => ({ rowIndex, columnId }: { rowIndex: number; columnId: string }) => { + if (rowIndex < 0 || rowIndex >= sortedRows.length) { + return null; + } + const row = sortedRows[rowIndex]; + if (!row || !row.hasOwnProperty(columnId)) { + return null; + } + const rawContent = row[columnId]; + const colIndex = columns.findIndex((col) => col.id === columnId); + const htmlContent = columns[colIndex].formatter.convert(rawContent, 'html'); + const formattedContent = ( + /* + * Justification for dangerouslySetInnerHTML: + * This is one of the visualizations which makes use of the HTML field formatters. + * Since these formatters produce raw HTML, this visualization needs to be able to render them as-is, relying + * on the field formatter to only produce safe HTML. + * `htmlContent` is created by converting raw data via HTML field formatter, so we need to make sure this value never contains + * any unsafe HTML (e.g. by bypassing the field formatter). + */ +
// eslint-disable-line react/no-danger + ); + return formattedContent || null; +}; diff --git a/src/plugins/vis_type_table/public/components/table_vis_component.test.tsx b/src/plugins/vis_type_table/public/components/table_vis_component.test.tsx new file mode 100644 index 000000000000..6e2d0090aa36 --- /dev/null +++ b/src/plugins/vis_type_table/public/components/table_vis_component.test.tsx @@ -0,0 +1,234 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { shallow } from 'enzyme'; +import { TableVisConfig, ColumnSort } from '../types'; +import { TableVisComponent } from './table_vis_component'; +import { FormattedColumn } from '../types'; +import { FormattedTableContext } from '../table_vis_response_handler'; +import { getTableVisCellValue } from './table_vis_cell'; +import { getDataGridColumns } from './table_vis_grid_columns'; +import { EuiDataGridColumn } from '@elastic/eui'; + +jest.mock('./table_vis_cell', () => ({ + getTableVisCellValue: jest.fn(() => () => {}), +})); + +const mockGetDataGridColumns = jest.fn(() => []); +jest.mock('./table_vis_grid_columns', () => ({ + getDataGridColumns: jest.fn(() => mockGetDataGridColumns()), +})); + +const table = { + formattedColumns: [ + { + id: 'col-0-2', + title: 'name.keyword: Descending', + formatter: {}, + filterable: true, + }, + { + id: 'col-1-1', + title: 'Count', + formatter: {}, + filterable: false, + sumTotal: 5, + formattedTotal: 5, + total: 5, + }, + ] as FormattedColumn[], + rows: [ + { 'col-0-2': 'Alice', 'col-1-1': 3 }, + { 'col-0-2': 'Anthony', 'col-1-1': 1 }, + { 'col-0-2': 'Timmy', 'col-1-1': 1 }, + ], + columns: [ + { id: 'col-0-2', name: 'Name' }, + { id: 'col-1-1', name: 'Count' }, + ], +} as FormattedTableContext; + +const visConfig = { + buckets: [ + { + accessor: 0, + aggType: 'terms', + format: { + id: 'terms', + params: { + id: 'number', + missingBucketLabel: 'Missing', + otherBucketLabel: 'Other', + parsedUrl: { + basePath: '/arf', + origin: '', + pathname: '/arf/app/home', + }, + }, + }, + label: 'age: Descending', + params: {}, + }, + ], + metrics: [ + { + accessor: 1, + aggType: 'count', + format: { + id: 'number', + }, + label: 'Count', + params: {}, + }, + ], + perPage: 10, + percentageCol: '', + showMetricsAtAllLevels: false, + showPartialRows: false, + showTotal: false, + title: '', + totalFunc: 'sum', +} as TableVisConfig; + +const uiState = { + sort: {} as ColumnSort, + setSort: jest.fn(), + colWidth: [], + setWidth: jest.fn(), +}; + +describe('TableVisComponent', function () { + const props = { + title: '', + table, + visConfig, + event: jest.fn(), + uiState, + }; + + const dataGridColumnsValue = [ + { + id: 'col-0-2', + display: 'name.keyword: Descending', + displayAsText: 'name.keyword: Descending', + actions: { + showHide: false, + showMoveLeft: false, + showMoveRight: false, + showSortAsc: {}, + showSortDesc: {}, + }, + cellActions: expect.any(Function), + }, + { + id: 'col-1-1', + display: 'Count', + displayAsText: 'Count', + actions: { + showHide: false, + showMoveLeft: false, + showMoveRight: false, + showSortAsc: {}, + showSortDesc: {}, + }, + cellActions: undefined, + }, + ] as EuiDataGridColumn[]; + + it('should render data grid', () => { + const comp = shallow(); + expect(comp.find('EuiDataGrid')).toHaveLength(1); + }); + + it('should render title when provided', () => { + const compWithTitle = shallow(); + const titleElement = compWithTitle.find('EuiTitle'); + expect(titleElement).toHaveLength(1); + expect(titleElement.find('h3').text()).toEqual('Test Title'); + }); + + it('should not render title when not provided', () => { + const compWithoutTitle = shallow(); + const titleElement = compWithoutTitle.find('EuiTitle'); + expect(titleElement).toHaveLength(0); + }); + + it('should set sort if sort column', () => { + mockGetDataGridColumns.mockReturnValueOnce(dataGridColumnsValue); + const comp = shallow(); + const { onSort } = comp.find('EuiDataGrid').prop('sorting') as any; + onSort([]); + expect(props.uiState.setSort).toHaveBeenCalledWith([]); + onSort([{ id: 'col-0-2', direction: 'asc' }]); + expect(props.uiState.setSort).toHaveBeenCalledWith({ colIndex: 0, direction: 'asc' }); + onSort([ + { id: 'col-0-2', direction: 'asc' }, + { id: 'col-1-1', direction: 'desc' }, + ]); + expect(props.uiState.setSort).toHaveBeenCalledWith({ colIndex: 1, direction: 'desc' }); + }); + + it('should set width if adjust column width', () => { + const uiStateProps = { + ...props.uiState, + width: [ + { colIndex: 0, width: 12 }, + { colIndex: 1, width: 8 }, + ], + }; + const comp = shallow(); + const onColumnResize = comp.find('EuiDataGrid').prop('onColumnResize') as any; + onColumnResize({ columnId: 'col-0-2', width: 18 }); + expect(props.uiState.setWidth).toHaveBeenCalledWith({ colIndex: 0, width: 18 }); + const updatedComp = shallow(); + const onColumnResizeUpdate = updatedComp.find('EuiDataGrid').prop('onColumnResize') as any; + onColumnResizeUpdate({ columnId: 'col-0-2', width: 18 }); + expect(props.uiState.setWidth).toHaveBeenCalledWith({ colIndex: 0, width: 18 }); + }); + + it('should create sortedRows and pass to getTableVisCellValue', () => { + const uiStateProps = { + ...props.uiState, + sort: { colIndex: 1, direction: 'asc' } as ColumnSort, + }; + const sortedRows = [ + { 'col-0-2': 'Anthony', 'col-1-1': 1 }, + { 'col-0-2': 'Timmy', 'col-1-1': 1 }, + { 'col-0-2': 'Alice', 'col-1-1': 3 }, + ]; + mockGetDataGridColumns.mockReturnValueOnce(dataGridColumnsValue); + shallow(); + expect(getTableVisCellValue).toHaveBeenCalledWith(sortedRows, table.formattedColumns); + expect(getDataGridColumns).toHaveBeenCalledWith(table, props.event, props.uiState.colWidth); + }); + + it('should return formattedTotal from footerCellValue', () => { + let comp = shallow(); + let renderFooterCellValue = comp.find('EuiDataGrid').prop('renderFooterCellValue') as any; + expect(renderFooterCellValue).toEqual(undefined); + comp = shallow(); + renderFooterCellValue = comp.find('EuiDataGrid').prop('renderFooterCellValue'); + expect(renderFooterCellValue({ columnId: 'col-1-1' })).toEqual(5); + expect(renderFooterCellValue({ columnId: 'col-0-2' })).toEqual(null); + }); + + it('should apply pagination correctly', () => { + const comp = shallow(); + const paginationProps = comp.find('EuiDataGrid').prop('pagination'); + expect(paginationProps).toMatchObject({ + pageIndex: 0, + pageSize: 3, + onChangeItemsPerPage: expect.any(Function), + onChangePage: expect.any(Function), + }); + }); + + it('should not call renderFooterCellValue when showTotal is false', () => { + const comp = shallow(); + const renderFooterCellValue = comp.find('EuiDataGrid').prop('renderFooterCellValue'); + expect(renderFooterCellValue).toBeUndefined(); + }); +}); diff --git a/src/plugins/vis_type_table/public/components/table_vis_component.tsx b/src/plugins/vis_type_table/public/components/table_vis_component.tsx index 4576e3420e22..1b16ec170a84 100644 --- a/src/plugins/vis_type_table/public/components/table_vis_component.tsx +++ b/src/plugins/vis_type_table/public/components/table_vis_component.tsx @@ -5,20 +5,20 @@ import React, { useCallback, useMemo } from 'react'; import { orderBy } from 'lodash'; -import dompurify from 'dompurify'; import { EuiDataGridProps, EuiDataGrid, EuiDataGridSorting, EuiTitle } from '@elastic/eui'; import { IInterpreterRenderHandlers } from 'src/plugins/expressions'; -import { Table } from '../table_vis_response_handler'; -import { TableVisConfig, ColumnWidth, ColumnSort, TableUiState } from '../types'; +import { FormattedTableContext } from '../table_vis_response_handler'; +import { TableVisConfig, ColumnSort } from '../types'; import { getDataGridColumns } from './table_vis_grid_columns'; +import { getTableVisCellValue } from './table_vis_cell'; import { usePagination } from '../utils'; -import { convertToFormattedData } from '../utils/convert_to_formatted_data'; import { TableVisControl } from './table_vis_control'; +import { TableUiState } from '../utils'; interface TableVisComponentProps { title?: string; - table: Table; + table: FormattedTableContext; visConfig: TableVisConfig; event: IInterpreterRenderHandlers['event']; uiState: TableUiState; @@ -29,52 +29,44 @@ export const TableVisComponent = ({ table, visConfig, event, - uiState, + uiState: { sort, setSort, colWidth, setWidth }, }: TableVisComponentProps) => { - const { formattedRows: rows, formattedColumns: columns } = convertToFormattedData( - table, - visConfig - ); + const { rows, formattedColumns } = table; const pagination = usePagination(visConfig, rows.length); const sortedRows = useMemo(() => { - return uiState.sort.colIndex !== null && - columns[uiState.sort.colIndex].id && - uiState.sort.direction - ? orderBy(rows, columns[uiState.sort.colIndex].id, uiState.sort.direction) - : rows; - }, [columns, rows, uiState]); + const sortColumnId = + sort.colIndex !== null && sort.colIndex !== undefined + ? formattedColumns[sort.colIndex]?.id + : undefined; + + if (sortColumnId && sort.direction) { + return orderBy(rows, sortColumnId, sort.direction); + } else { + return rows; + } + }, [formattedColumns, rows, sort]); - const renderCellValue = useMemo(() => { - return (({ rowIndex, columnId }) => { - const rawContent = sortedRows[rowIndex][columnId]; - const colIndex = columns.findIndex((col) => col.id === columnId); - const htmlContent = columns[colIndex].formatter.convert(rawContent, 'html'); - const formattedContent = ( - /* - * Justification for dangerouslySetInnerHTML: - * This is one of the visualizations which makes use of the HTML field formatters. - * Since these formatters produce raw HTML, this visualization needs to be able to render them as-is, relying - * on the field formatter to only produce safe HTML. - * `htmlContent` is created by converting raw data via HTML field formatter, so we need to make sure this value never contains - * any unsafe HTML (e.g. by bypassing the field formatter). - */ -
// eslint-disable-line react/no-danger - ); - return sortedRows.hasOwnProperty(rowIndex) ? formattedContent || null : null; - }) as EuiDataGridProps['renderCellValue']; - }, [sortedRows, columns]); + const renderCellValue = useMemo(() => getTableVisCellValue(sortedRows, formattedColumns), [ + sortedRows, + formattedColumns, + ]); - const dataGridColumns = getDataGridColumns(sortedRows, columns, table, event, uiState.width); + const dataGridColumns = getDataGridColumns(table, event, colWidth); const sortedColumns = useMemo(() => { - return uiState.sort.colIndex !== null && - dataGridColumns[uiState.sort.colIndex].id && - uiState.sort.direction - ? [{ id: dataGridColumns[uiState.sort.colIndex].id, direction: uiState.sort.direction }] - : []; - }, [dataGridColumns, uiState]); + if ( + sort.colIndex !== null && + sort.colIndex !== undefined && + dataGridColumns[sort.colIndex].id && + sort.direction + ) { + return [{ id: dataGridColumns[sort.colIndex].id, direction: sort.direction }]; + } else { + return []; + } + }, [dataGridColumns, sort]); const onSort = useCallback( (sortingCols: EuiDataGridSorting['columns'] | []) => { @@ -85,47 +77,34 @@ export const TableVisComponent = ({ colIndex: dataGridColumns.findIndex((col) => col.id === nextSortValue?.id), direction: nextSortValue.direction, } - : { - colIndex: null, - direction: null, - }; - uiState.setSort(nextSort); + : []; + setSort(nextSort); return nextSort; }, - [dataGridColumns, uiState] + [dataGridColumns, setSort] ); const onColumnResize: EuiDataGridProps['onColumnResize'] = useCallback( - ({ columnId, width }) => { - const curWidth: ColumnWidth[] = uiState.width; - const nextWidth = [...curWidth]; - const nextColIndex = columns.findIndex((col) => col.id === columnId); - const curColIndex = curWidth.findIndex((col) => col.colIndex === nextColIndex); - const nextColWidth = { colIndex: nextColIndex, width }; - - // if updated column index is not found, then add it to nextWidth - // else reset it in nextWidth - if (curColIndex < 0) nextWidth.push(nextColWidth); - else nextWidth[curColIndex] = nextColWidth; - - // update uiState.width - uiState.setWidth(nextWidth); + ({ columnId, width }: { columnId: string; width: number }) => { + const colIndex = formattedColumns.findIndex((col) => col.id === columnId); + // update width in uiState + setWidth({ colIndex, width }); }, - [columns, uiState] + [formattedColumns, setWidth] ); const ariaLabel = title || visConfig.title || 'tableVis'; const footerCellValue = visConfig.showTotal ? ({ columnId }: { columnId: any }) => { - return columns.find((col) => col.id === columnId)?.formattedTotal || null; + return formattedColumns.find((col) => col.id === columnId)?.formattedTotal || null; } : undefined; return ( <> {title && ( - +

{title}

)} @@ -133,7 +112,7 @@ export const TableVisComponent = ({ aria-label={ariaLabel} columns={dataGridColumns} columnVisibility={{ - visibleColumns: columns.map(({ id }) => id), + visibleColumns: formattedColumns.map(({ id }) => id), setVisibleColumns: () => {}, }} rowCount={rows.length} @@ -153,7 +132,11 @@ export const TableVisComponent = ({ showFullScreenSelector: false, showStyleSelector: false, additionalControls: ( - + ), }} /> diff --git a/src/plugins/vis_type_table/public/components/table_vis_component_group.test.tsx b/src/plugins/vis_type_table/public/components/table_vis_component_group.test.tsx new file mode 100644 index 000000000000..570c8c0b853b --- /dev/null +++ b/src/plugins/vis_type_table/public/components/table_vis_component_group.test.tsx @@ -0,0 +1,67 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { render } from '@testing-library/react'; +import { TableVisComponentGroup } from './table_vis_component_group'; +import { TableVisConfig, ColumnSort } from '../types'; +import { Table, TableGroup } from '../table_vis_response_handler'; + +jest.mock('./table_vis_component', () => ({ + TableVisComponent: () =>
TableVisComponent
, +})); + +const table1 = { + table: { + columns: [], + rows: [], + formattedColumns: [], + } as Table, + title: '', +} as TableGroup; + +const table2 = { + table: { + columns: [], + rows: [], + formattedColumns: [], + } as Table, + title: '', +} as TableGroup; + +const tableUiStateMock = { + sort: { colIndex: undefined, direction: undefined } as ColumnSort, + setSort: jest.fn(), + width: [], + setWidth: jest.fn(), +}; + +describe('TableVisApp', () => { + it('should not render table or table group components if no table', () => { + const { container, queryAllByText } = render( + + ); + expect(queryAllByText('TableVisComponent')).toHaveLength(0); + expect(container.outerHTML.includes('visTable__group')).toBe(false); + }); + + it('should render table component 2 times', () => { + const { container, queryAllByText } = render( + + ); + expect(queryAllByText('TableVisComponent')).toHaveLength(2); + expect(container.outerHTML.includes('visTable__group')).toBe(true); + }); +}); diff --git a/src/plugins/vis_type_table/public/components/table_vis_component_group.tsx b/src/plugins/vis_type_table/public/components/table_vis_component_group.tsx index 633b9d2230bd..af8fd8048cbc 100644 --- a/src/plugins/vis_type_table/public/components/table_vis_component_group.tsx +++ b/src/plugins/vis_type_table/public/components/table_vis_component_group.tsx @@ -7,8 +7,9 @@ import React, { memo } from 'react'; import { IInterpreterRenderHandlers } from 'src/plugins/expressions'; import { TableGroup } from '../table_vis_response_handler'; -import { TableVisConfig, TableUiState } from '../types'; +import { TableVisConfig } from '../types'; import { TableVisComponent } from './table_vis_component'; +import { TableUiState } from '../utils'; interface TableVisGroupComponentProps { tableGroups: TableGroup[]; @@ -21,11 +22,11 @@ export const TableVisComponentGroup = memo( ({ tableGroups, visConfig, event, uiState }: TableVisGroupComponentProps) => { return ( <> - {tableGroups.map(({ tables, title }) => ( + {tableGroups.map(({ table, title }) => (
{ const filterBucket = (rowIndex: number, columnIndex: number, negate: boolean) => { - const foramttedColumnId = cols[columnIndex].id; - const rawColumnIndex = table.columns.findIndex((col) => col.id === foramttedColumnId); event({ name: 'filterBucket', data: { @@ -28,10 +23,10 @@ export const getDataGridColumns = ( { table: { columns: table.columns, - rows, + rows: table.rows, }, row: rowIndex, - column: rawColumnIndex, + column: columnIndex, }, ], negate, @@ -39,11 +34,11 @@ export const getDataGridColumns = ( }); }; - return cols.map((col, colIndex) => { + return table.formattedColumns.map((col, colIndex) => { const cellActions = col.filterable ? [ ({ rowIndex, columnId, Component, closePopover }: EuiDataGridColumnCellActionProps) => { - const filterValue = rows[rowIndex][columnId]; + const filterValue = table.rows[rowIndex][columnId]; const filterContent = col.formatter?.convert(filterValue); const filterForValueText = i18n.translate( @@ -79,7 +74,7 @@ export const getDataGridColumns = ( ); }, ({ rowIndex, columnId, Component, closePopover }: EuiDataGridColumnCellActionProps) => { - const filterValue = rows[rowIndex][columnId]; + const filterValue = table.rows[rowIndex][columnId]; const filterContent = col.formatter?.convert(filterValue); const filterOutValueText = i18n.translate( diff --git a/src/plugins/vis_type_table/public/table_vis_fn.ts b/src/plugins/vis_type_table/public/table_vis_fn.ts index 4f0fb2c0ba1f..556cfaf24e00 100644 --- a/src/plugins/vis_type_table/public/table_vis_fn.ts +++ b/src/plugins/vis_type_table/public/table_vis_fn.ts @@ -4,7 +4,7 @@ */ import { i18n } from '@osd/i18n'; -import { tableVisResponseHandler, TableContext } from './table_vis_response_handler'; +import { tableVisResponseHandler, TableVisData } from './table_vis_response_handler'; import { ExpressionFunctionDefinition, OpenSearchDashboardsDatatable, @@ -19,7 +19,7 @@ interface Arguments { } export interface TableVisRenderValue { - visData: TableContext; + visData: TableVisData; visType: 'table'; visConfig: TableVisConfig; } diff --git a/src/plugins/vis_type_table/public/table_vis_renderer.test.tsx b/src/plugins/vis_type_table/public/table_vis_renderer.test.tsx new file mode 100644 index 000000000000..ee18bcfaf734 --- /dev/null +++ b/src/plugins/vis_type_table/public/table_vis_renderer.test.tsx @@ -0,0 +1,78 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { unmountComponentAtNode } from 'react-dom'; +import { act } from '@testing-library/react'; + +import { CoreStart } from 'opensearch-dashboards/public'; +import { getTableVisRenderer } from './table_vis_renderer'; +import { TableVisData } from './table_vis_response_handler'; +import { TableVisConfig } from './types'; +import { TableVisRenderValue } from './table_vis_fn'; + +const mockVisData = { + tableGroups: [], + direction: 'row', +} as TableVisData; + +const mockVisConfig = { + title: 'My Table', + metrics: [] as any, + buckets: [] as any, +} as TableVisConfig; + +const mockHandlers = { + done: jest.fn(), + reload: jest.fn(), + update: jest.fn(), + event: jest.fn(), + onDestroy: jest.fn(), +}; + +const mockCoreStart = {} as CoreStart; + +describe('getTableVisRenderer', () => { + let container: any = null; + + beforeEach(() => { + container = document.createElement('div'); + document.body.appendChild(container); + }); + + afterEach(() => { + unmountComponentAtNode(container); + container.remove(); + container = null; + }); + + it('should render table visualization', async () => { + const renderer = getTableVisRenderer(mockCoreStart); + const mockTableVisRenderValue = { + visData: mockVisData, + visType: 'table', + visConfig: mockVisConfig, + } as TableVisRenderValue; + await act(async () => { + renderer.render(container, mockTableVisRenderValue, mockHandlers); + }); + expect(container.querySelector('.tableVis')).toBeTruthy(); + }); + + it('should destroy table on unmount', async () => { + const renderer = getTableVisRenderer(mockCoreStart); + const mockTableVisRenderValue = { + visData: mockVisData, + visType: 'table', + visConfig: mockVisConfig, + } as TableVisRenderValue; + await act(async () => { + renderer.render(container, mockTableVisRenderValue, mockHandlers); + }); + await act(async () => { + unmountComponentAtNode(container); + }); + expect(mockHandlers.onDestroy).toHaveBeenCalled(); + }); +}); diff --git a/src/plugins/vis_type_table/public/table_vis_response_handler.test.ts b/src/plugins/vis_type_table/public/table_vis_response_handler.test.ts new file mode 100644 index 000000000000..89627854b449 --- /dev/null +++ b/src/plugins/vis_type_table/public/table_vis_response_handler.test.ts @@ -0,0 +1,157 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { tableVisResponseHandler } from './table_vis_response_handler'; + +jest.mock('./services', () => { + const formatService = { + deserialize: jest.fn(() => ({ + convert: jest.fn((value) => value), + })), + }; + + return { + getFormatService: () => formatService, + }; +}); + +const createTableGroup = (title, rows) => ({ + title, + table: { + columns: [ + { id: 'col-0', meta: { type: 'string' }, name: 'Column 1' }, + { id: 'col-1', meta: { type: 'number' }, name: 'Column 2' }, + ], + formattedColumns: [ + { + id: 'col-0', + title: 'Column 1', + formatter: { convert: expect.any(Function) }, + filterable: true, + }, + { + id: 'col-1', + title: 'Column 2', + formatter: { convert: expect.any(Function) }, + filterable: false, + }, + ], + rows, + }, +}); + +describe('tableVisResponseHandler', () => { + const input = { + type: 'datatable', + columns: [ + { id: 'col-0', name: 'Column 1', meta: { type: 'string' } }, + { id: 'col-1', name: 'Column 2', meta: { type: 'number' } }, + ], + rows: [ + { 'col-0': 'Group 1', 'col-1': 100 }, + { 'col-0': 'Group 2', 'col-1': 200 }, + ], + }; + + const baseVisConfig = { + title: 'My Table', + buckets: [ + { + accessor: 0, + label: 'Column 1', + format: { + id: 'string', + params: {}, + }, + params: {}, + aggType: 'terms', + }, + ], + metrics: [ + { + accessor: 1, + label: 'Count', + format: { + id: 'number', + }, + params: {}, + aggType: 'count', + }, + ], + }; + + const splitConfig = { + accessor: 0, + label: 'Column 1', + format: { + id: 'string', + params: {}, + }, + params: {}, + aggType: 'terms', + }; + + it('should correctly format data with splitRow', () => { + const visConfig = { ...baseVisConfig, splitRow: [splitConfig] }; + + const expected = { + table: undefined, + tableGroups: [ + createTableGroup('Group 1: Column 1', [{ 'col-0': 'Group 1', 'col-1': 100 }]), + createTableGroup('Group 2: Column 1', [{ 'col-0': 'Group 2', 'col-1': 200 }]), + ], + direction: 'row', + }; + + const result = tableVisResponseHandler(input, visConfig); + expect(result).toEqual(expected); + }); + + it('should correctly format data with splitColumn', () => { + const visConfig = { ...baseVisConfig, splitColumn: [splitConfig] }; + + const expected = { + table: undefined, + tableGroups: [ + createTableGroup('Group 1: Column 1', [{ 'col-0': 'Group 1', 'col-1': 100 }]), + createTableGroup('Group 2: Column 1', [{ 'col-0': 'Group 2', 'col-1': 200 }]), + ], + direction: 'column', + }; + + const result = tableVisResponseHandler(input, visConfig); + expect(result).toEqual(expected); + }); + + it('should correctly format data with no split', () => { + const visConfig = baseVisConfig; + + const expected = { + table: { + columns: input.columns, + formattedColumns: [ + { + id: 'col-0', + title: 'Column 1', + formatter: { convert: expect.any(Function) }, + filterable: true, + }, + { + id: 'col-1', + title: 'Column 2', + formatter: { convert: expect.any(Function) }, + filterable: false, + }, + ], + rows: input.rows, + }, + tableGroups: [], + direction: undefined, + }; + + const result = tableVisResponseHandler(input, visConfig); + expect(result).toEqual(expected); + }); +}); diff --git a/src/plugins/vis_type_table/public/table_vis_response_handler.ts b/src/plugins/vis_type_table/public/table_vis_response_handler.ts index b1d41edfff8b..975038c4c11f 100644 --- a/src/plugins/vis_type_table/public/table_vis_response_handler.ts +++ b/src/plugins/vis_type_table/public/table_vis_response_handler.ts @@ -30,25 +30,25 @@ import { getFormatService } from './services'; import { OpenSearchDashboardsDatatable } from '../../expressions/public'; -import { TableVisConfig } from './types'; +import { FormattedColumn, TableVisConfig } from './types'; +import { convertToFormattedData } from './utils/convert_to_formatted_data'; export interface Table { columns: OpenSearchDashboardsDatatable['columns']; rows: OpenSearchDashboardsDatatable['rows']; } +export interface FormattedTableContext extends Table { + formattedColumns: FormattedColumn[]; +} + export interface TableGroup { - table: OpenSearchDashboardsDatatable; - tables: Table[]; + table: FormattedTableContext; title: string; - name: string; - key: any; - column: number; - row: number; } -export interface TableContext { - table?: Table; +export interface TableVisData { + table?: FormattedTableContext; tableGroups: TableGroup[]; direction?: 'row' | 'column'; } @@ -56,10 +56,10 @@ export interface TableContext { export function tableVisResponseHandler( input: OpenSearchDashboardsDatatable, config: TableVisConfig -): TableContext { - let table: Table | undefined; +): TableVisData { + let table: FormattedTableContext | undefined; const tableGroups: TableGroup[] = []; - let direction: TableContext['direction']; + let direction: TableVisData['direction']; const split = config.splitColumn || config.splitRow; @@ -78,30 +78,32 @@ export function tableVisResponseHandler( (splitMap as any)[splitValue] = splitIndex++; const tableGroup: TableGroup = { title: `${splitColumnFormatter.convert(splitValue)}: ${splitColumn.name}`, - name: splitColumn.name, - key: splitValue, - column: splitColumnIndex, - row: rowIndex, - table: input, - tables: [], + table: { + formattedColumns: [], + rows: [], + columns: input.columns, + }, }; - tableGroup.tables.push({ - columns: input.columns, - rows: [], - }); - tableGroups.push(tableGroup); } - const tableIndex = (splitMap as any)[splitValue]; - (tableGroups[tableIndex] as any).tables[0].rows.push(row); + const tableIndex = splitMap[splitValue]; + tableGroups[tableIndex].table.rows.push(row); + }); + + // format tables + tableGroups.forEach((tableGroup) => { + tableGroup.table = convertToFormattedData(tableGroup.table, config); }); } else { - table = { - columns: input.columns, - rows: input.rows, - }; + table = convertToFormattedData( + { + columns: input.columns, + rows: input.rows, + }, + config + ); } return { diff --git a/src/plugins/vis_type_table/public/types.ts b/src/plugins/vis_type_table/public/types.ts index 814a86f5ac69..a14767f96302 100644 --- a/src/plugins/vis_type_table/public/types.ts +++ b/src/plugins/vis_type_table/public/types.ts @@ -75,10 +75,3 @@ export interface ColumnSort { colIndex?: number; direction?: 'asc' | 'desc'; } - -export interface TableUiState { - sort: ColumnSort; - setSort: (sort: ColumnSort) => void; - width: ColumnWidth[]; - setWidth: (columnWidths: ColumnWidth[]) => void; -} diff --git a/src/plugins/vis_type_table/public/utils/__snapshots__/add_percentage_col.test.ts.snap b/src/plugins/vis_type_table/public/utils/__snapshots__/add_percentage_col.test.ts.snap new file mode 100644 index 000000000000..41f4f24ecffb --- /dev/null +++ b/src/plugins/vis_type_table/public/utils/__snapshots__/add_percentage_col.test.ts.snap @@ -0,0 +1,170 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`addPercentageCol should add new percentage column 1`] = ` +Object { + "cols": Array [ + Object { + "filterable": true, + "formatter": Object {}, + "id": "col-0-2", + "title": "name.keyword: Descending", + }, + Object { + "filterable": false, + "formatter": Object {}, + "id": "col-1-1", + "sumTotal": 5, + "title": "Count", + }, + Object { + "filterable": false, + "formatter": Object {}, + "id": "col-1-1-percents", + "title": "count percentages", + }, + ], + "rows": Array [ + Object { + "col-0-2": "Alice", + "col-1-1": 3, + "col-1-1-percents": 0.6, + }, + Object { + "col-0-2": "Anthony", + "col-1-1": 1, + "col-1-1-percents": 0.2, + }, + Object { + "col-0-2": "Timmy", + "col-1-1": 1, + "col-1-1-percents": 0.2, + }, + ], +} +`; + +exports[`addPercentageCol should handle empty input data 1`] = ` +Object { + "cols": Array [], + "rows": Array [], +} +`; + +exports[`addPercentageCol should handle input data with null values 1`] = ` +Object { + "cols": Array [ + Object { + "filterable": true, + "formatter": Object {}, + "id": "col-0-2", + "title": "name.keyword: Descending", + }, + Object { + "filterable": false, + "formatter": Object {}, + "id": "col-1-1", + "sumTotal": 5, + "title": "Count", + }, + Object { + "filterable": false, + "formatter": Object {}, + "id": "col-1-1-percents", + "title": "count percentages", + }, + ], + "rows": Array [ + Object { + "col-0-2": "Alice", + "col-1-1": null, + "col-1-1-percents": 0, + }, + Object { + "col-0-2": "Anthony", + "col-1-1": null, + "col-1-1-percents": 0, + }, + Object { + "col-0-2": "Timmy", + "col-1-1": null, + "col-1-1-percents": 0, + }, + ], +} +`; + +exports[`addPercentageCol should handle input data with one row 1`] = ` +Object { + "cols": Array [ + Object { + "filterable": true, + "formatter": Object {}, + "id": "col-0-2", + "title": "name.keyword: Descending", + }, + Object { + "filterable": false, + "formatter": Object {}, + "id": "col-1-1", + "sumTotal": 5, + "title": "Count", + }, + Object { + "filterable": false, + "formatter": Object {}, + "id": "col-1-1-percents", + "title": "count percentages", + }, + ], + "rows": Array [ + Object { + "col-0-2": "Alice", + "col-1-1": 3, + "col-1-1-percents": 0.6, + }, + ], +} +`; + +exports[`addPercentageCol should handle sumTotal being 0 1`] = ` +Object { + "cols": Array [ + Object { + "filterable": true, + "formatter": Object {}, + "id": "col-0-2", + "title": "name.keyword: Descending", + }, + Object { + "filterable": false, + "formatter": Object {}, + "id": "col-1-1", + "sumTotal": 0, + "title": "Count", + }, + Object { + "filterable": false, + "formatter": Object {}, + "id": "col-1-1-percents", + "title": "count percentages", + }, + ], + "rows": Array [ + Object { + "col-0-2": "Alice", + "col-1-1": 3, + "col-1-1-percents": 0, + }, + Object { + "col-0-2": "Anthony", + "col-1-1": 1, + "col-1-1-percents": 0, + }, + Object { + "col-0-2": "Timmy", + "col-1-1": 1, + "col-1-1-percents": 0, + }, + ], +} +`; diff --git a/src/plugins/vis_type_table/public/utils/add_percentage_col.test.ts b/src/plugins/vis_type_table/public/utils/add_percentage_col.test.ts new file mode 100644 index 000000000000..5f27cb5e49ea --- /dev/null +++ b/src/plugins/vis_type_table/public/utils/add_percentage_col.test.ts @@ -0,0 +1,76 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { addPercentageCol } from './add_percentage_col'; +import { FormattedColumn } from '../types'; +import { Table } from '../table_vis_response_handler'; + +const mockDeserialize = jest.fn(() => ({})); +jest.mock('../services', () => ({ + getFormatService: jest.fn(() => ({ + deserialize: mockDeserialize, + })), +})); + +let formattedColumns: FormattedColumn[]; +const rows = [ + { 'col-0-2': 'Alice', 'col-1-1': 3 }, + { 'col-0-2': 'Anthony', 'col-1-1': 1 }, + { 'col-0-2': 'Timmy', 'col-1-1': 1 }, +] as Table['rows']; + +beforeEach(() => { + formattedColumns = [ + { + id: 'col-0-2', + title: 'name.keyword: Descending', + formatter: {}, + filterable: true, + }, + { + id: 'col-1-1', + title: 'Count', + formatter: {}, + filterable: false, + sumTotal: 5, + }, + ] as FormattedColumn[]; +}); + +describe('addPercentageCol', () => { + it('should add new percentage column', () => { + const result = addPercentageCol(formattedColumns, 'count', rows, 1); + expect(result).toMatchSnapshot(); + }); + + it('should handle sumTotal being 0', () => { + formattedColumns[1].sumTotal = 0; + const result = addPercentageCol(formattedColumns, 'count', rows, 1); + expect(result).toMatchSnapshot(); + }); + + it('should handle empty input data', () => { + const emptyFormattedColumns: FormattedColumn[] = []; + const emptyRows: Table['rows'] = []; + const result = addPercentageCol(emptyFormattedColumns, 'count', emptyRows, 1); + expect(result).toMatchSnapshot(); + }); + + it('should handle input data with one row', () => { + const oneRow = [{ 'col-0-2': 'Alice', 'col-1-1': 3 }] as Table['rows']; + const result = addPercentageCol(formattedColumns, 'count', oneRow, 1); + expect(result).toMatchSnapshot(); + }); + + it('should handle input data with null values', () => { + const nullValueRows = [ + { 'col-0-2': 'Alice', 'col-1-1': null }, + { 'col-0-2': 'Anthony', 'col-1-1': null }, + { 'col-0-2': 'Timmy', 'col-1-1': null }, + ] as Table['rows']; + const result = addPercentageCol(formattedColumns, 'count', nullValueRows, 1); + expect(result).toMatchSnapshot(); + }); +}); diff --git a/src/plugins/vis_type_table/public/utils/add_percentage_col.ts b/src/plugins/vis_type_table/public/utils/add_percentage_col.ts new file mode 100644 index 000000000000..8c300f29d06a --- /dev/null +++ b/src/plugins/vis_type_table/public/utils/add_percentage_col.ts @@ -0,0 +1,76 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@osd/i18n'; +import { Table } from '../table_vis_response_handler'; +import { getFormatService } from '../services'; +import { FormattedColumn } from '../types'; + +function insert(arr: FormattedColumn[], index: number, col: FormattedColumn) { + const newArray = [...arr]; + newArray.splice(index + 1, 0, col); + return newArray; +} +/** + * @param columns - the formatted columns that will be displayed + * @param title - the title of the column to add to + * @param rows - the row data for the columns + * @param insertAtIndex - the index to insert the percentage column at + * @returns cols and rows for the table to render now included percentage column(s) + */ +export function addPercentageCol( + columns: FormattedColumn[], + title: string, + rows: Table['rows'], + insertAtIndex: number +) { + if (columns.length === 0) { + return { cols: columns, rows }; + } + const { id, sumTotal } = columns[insertAtIndex]; + const newId = `${id}-percents`; + const formatter = getFormatService().deserialize({ id: 'percent' }); + const i18nTitle = i18n.translate('visTypeTable.params.percentageTableColumnName', { + defaultMessage: '{title} percentages', + values: { title }, + }); + const newCols = insert(columns, insertAtIndex, { + title: i18nTitle, + id: newId, + formatter, + filterable: false, + }); + const newRows = rows.map((row) => ({ + [newId]: sumTotal === 0 ? 0 : (row[id] as number) / (sumTotal as number), + ...row, + })); + + return { cols: newCols, rows: newRows }; +} diff --git a/src/plugins/vis_type_table/public/utils/convert_to_csv_data.test.ts b/src/plugins/vis_type_table/public/utils/convert_to_csv_data.test.ts new file mode 100644 index 000000000000..591dbe5454ce --- /dev/null +++ b/src/plugins/vis_type_table/public/utils/convert_to_csv_data.test.ts @@ -0,0 +1,76 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { IUiSettingsClient } from 'opensearch-dashboards/public'; +import { FormattedColumn } from '../types'; +import { toCsv } from './convert_to_csv_data'; +import { IFieldFormat } from 'src/plugins/data/common'; + +const mockConvert = jest.fn((x) => x); +const defaultFormatter = { convert: (x) => mockConvert(x) } as IFieldFormat; + +function implementConvert(nRow: number) { + for (let i = 0; i < nRow; i++) { + mockConvert.mockImplementationOnce((x) => x); + mockConvert.mockImplementationOnce((x) => x); + mockConvert.mockImplementationOnce((x) => { + return parseFloat(x) * 100 + '%'; + }); + } +} + +const columns = [ + { + id: 'col-0-2', + title: 'name.keyword: Descending', + formatter: defaultFormatter, + filterable: true, + }, + { + id: 'col-1-1', + title: 'Count', + formatter: defaultFormatter, + filterable: false, + sumTotal: 5, + formattedTotal: 5, + total: 5, + }, + { + id: 'col-1-1-percents', + title: 'Count percentages', + formatter: defaultFormatter, + filterable: false, + }, +] as FormattedColumn[]; + +const rows = [ + { 'col-1-1-percents': 0.6, 'col-0-2': 'Alice', 'col-1-1': 3 }, + { 'col-1-1-percents': 0.2, 'col-0-2': 'Anthony', 'col-1-1': 1 }, + { 'col-1-1-percents': 0.2, 'col-0-2': 'Timmy', 'col-1-1': 1 }, +]; + +const uiSettings = { + get: (key: string) => { + if (key === 'csv:separator') return ','; + else if (key === 'csv:quoteValues') return true; + }, +} as IUiSettingsClient; + +describe('toCsv', () => { + it('should create csv rows if not formatted', () => { + const result = toCsv(false, { rows, columns, uiSettings }); + expect(result).toEqual( + '"name.keyword: Descending",Count,"Count percentages"\r\nAlice,3,"0.6"\r\nAnthony,1,"0.2"\r\nTimmy,1,"0.2"\r\n' + ); + }); + + it('should create csv rows if formatted', () => { + implementConvert(3); + const result = toCsv(true, { rows, columns, uiSettings }); + expect(result).toEqual( + '"name.keyword: Descending",Count,"Count percentages"\r\nAlice,3,"60%"\r\nAnthony,1,"20%"\r\nTimmy,1,"20%"\r\n' + ); + }); +}); diff --git a/src/plugins/vis_type_table/public/utils/convert_to_csv_data.ts b/src/plugins/vis_type_table/public/utils/convert_to_csv_data.ts index 2c37df1aa3d5..3d4781736689 100644 --- a/src/plugins/vis_type_table/public/utils/convert_to_csv_data.ts +++ b/src/plugins/vis_type_table/public/utils/convert_to_csv_data.ts @@ -46,7 +46,7 @@ interface CSVDataProps { uiSettings: CoreStart['uiSettings']; } -const toCsv = function (formatted: boolean, { rows, columns, uiSettings }: CSVDataProps) { +export const toCsv = function (formatted: boolean, { rows, columns, uiSettings }: CSVDataProps) { const separator = uiSettings.get(CSV_SEPARATOR_SETTING); const quoteValues = uiSettings.get(CSV_QUOTE_VALUES_SETTING); diff --git a/src/plugins/vis_type_table/public/utils/convert_to_formatted_data.test.ts b/src/plugins/vis_type_table/public/utils/convert_to_formatted_data.test.ts new file mode 100644 index 000000000000..34085b70a278 --- /dev/null +++ b/src/plugins/vis_type_table/public/utils/convert_to_formatted_data.test.ts @@ -0,0 +1,136 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { convertToFormattedData } from './convert_to_formatted_data'; +import { TableVisConfig } from '../types'; +import { Table } from '../table_vis_response_handler'; +import { AggTypes } from '../types'; + +const mockDeserialize = jest.fn(() => ({})); +jest.mock('../services', () => ({ + getFormatService: jest.fn(() => ({ + deserialize: mockDeserialize, + })), +})); + +const table = { + type: 'opensearch_dashboards_datatable', + columns: [ + { id: 'col-0-2', name: 'name.keyword: Descending', meta: { type: 'terms' } }, + { id: 'col-1-1', name: 'Count', meta: { type: 'count' } }, + ], + rows: [ + { 'col-0-2': 'Alice', 'col-1-1': 3 }, + { 'col-0-2': 'Anthony', 'col-1-1': 1 }, + { 'col-0-2': 'Timmy', 'col-1-1': 1 }, + ], +} as Table; + +let visConfig = {} as TableVisConfig; + +function implementDeserialize() { + mockDeserialize.mockImplementationOnce(() => ({})); + mockDeserialize.mockImplementationOnce(() => ({ + allowsNumericalAggregations: true, + convert: jest.fn((x: number) => x), + })); +} + +describe('convertToFormattedData', () => { + beforeEach(() => { + visConfig = { + buckets: [ + { + accessor: 0, + aggType: 'terms', + format: { + id: 'terms', + params: { + id: 'string', + missingBucketLabel: 'Missing', + otherBucketLabel: 'Other', + parsedUrl: { + basePath: '/arf', + origin: '', + pathname: '/arf/app/home', + }, + }, + }, + label: 'name.keyword: Descending', + params: {}, + }, + ], + metrics: [ + { + accessor: 1, + aggType: 'count', + format: { + id: 'number', + }, + label: 'Count', + params: {}, + }, + ], + perPage: 10, + percentageCol: '', + showMetricsAtAllLevels: false, + showPartialRows: false, + showTotal: false, + title: '', + totalFunc: 'sum', + } as TableVisConfig; + }); + + it('should create formatted data', () => { + const result = convertToFormattedData(table, visConfig); + expect(result.rows).toEqual(table.rows); + expect(result.formattedColumns).toEqual([ + { + id: 'col-0-2', + title: 'name.keyword: Descending', + formatter: {}, + filterable: true, + }, + { id: 'col-1-1', title: 'Count', formatter: {}, filterable: false }, + ]); + }); + + describe.each([ + [AggTypes.SUM, 5], + [AggTypes.AVG, 1.6666666666666667], + [AggTypes.MIN, 1], + [AggTypes.MAX, 3], + [AggTypes.COUNT, 3], + ])('with totalFunc as %s', (totalFunc, expectedTotal) => { + beforeEach(() => { + implementDeserialize(); + visConfig.showTotal = true; + visConfig.totalFunc = totalFunc; + }); + + it(`should add ${totalFunc} total`, () => { + const result = convertToFormattedData(table, visConfig); + const expectedFormattedColumns = [ + { + id: 'col-0-2', + title: 'name.keyword: Descending', + formatter: {}, + filterable: true, + ...(totalFunc === AggTypes.COUNT ? { sumTotal: 0, formattedTotal: 3, total: 3 } : {}), + }, + { + id: 'col-1-1', + title: 'Count', + formatter: { allowsNumericalAggregations: true, convert: expect.any(Function) }, + filterable: false, + sumTotal: 5, + formattedTotal: expectedTotal, + total: expectedTotal, + }, + ]; + expect(result.formattedColumns).toEqual(expectedFormattedColumns); + }); + }); +}); diff --git a/src/plugins/vis_type_table/public/utils/convert_to_formatted_data.ts b/src/plugins/vis_type_table/public/utils/convert_to_formatted_data.ts index 2ab67e3b0a67..afb2906af8a1 100644 --- a/src/plugins/vis_type_table/public/utils/convert_to_formatted_data.ts +++ b/src/plugins/vis_type_table/public/utils/convert_to_formatted_data.ts @@ -28,56 +28,20 @@ * under the License. */ -import { i18n } from '@osd/i18n'; import { chain } from 'lodash'; -import { OpenSearchDashboardsDatatableRow } from 'src/plugins/expressions'; +import { + OpenSearchDashboardsDatatableRow, + OpenSearchDashboardsDatatableColumn, +} from 'src/plugins/expressions'; import { Table } from '../table_vis_response_handler'; import { AggTypes, TableVisConfig } from '../types'; import { getFormatService } from '../services'; import { FormattedColumn } from '../types'; - -function insert(arr: FormattedColumn[], index: number, col: FormattedColumn) { - const newArray = [...arr]; - newArray.splice(index + 1, 0, col); - return newArray; -} - -/** - * @param columns - the formatted columns that will be displayed - * @param title - the title of the column to add to - * @param rows - the row data for the columns - * @param insertAtIndex - the index to insert the percentage column at - * @returns cols and rows for the table to render now included percentage column(s) - */ -function addPercentageCol( - columns: FormattedColumn[], - title: string, - rows: Table['rows'], - insertAtIndex: number -) { - const { id, sumTotal } = columns[insertAtIndex]; - const newId = `${id}-percents`; - const formatter = getFormatService().deserialize({ id: 'percent' }); - const i18nTitle = i18n.translate('visTypeTable.params.percentageTableColumnName', { - defaultMessage: '{title} percentages', - values: { title }, - }); - const newCols = insert(columns, insertAtIndex, { - title: i18nTitle, - id: newId, - formatter, - filterable: false, - }); - const newRows = rows.map((row) => ({ - [newId]: (row[id] as number) / (sumTotal as number), - ...row, - })); - - return { cols: newCols, rows: newRows }; -} +import { addPercentageCol } from './add_percentage_col'; export interface FormattedDataProps { - formattedRows: OpenSearchDashboardsDatatableRow[]; + rows: OpenSearchDashboardsDatatableRow[]; + columns: OpenSearchDashboardsDatatableColumn[]; formattedColumns: FormattedColumn[]; } @@ -107,12 +71,15 @@ export const convertToFormattedData = ( const allowsNumericalAggregations = formatter?.allowsNumericalAggregations; if (allowsNumericalAggregations || isDate || visConfig.totalFunc === AggTypes.COUNT) { - const sum = table.rows.reduce((prev, curr) => { - // some metrics return undefined for some of the values - // derivative is an example of this as it returns undefined in the first row - if (curr[col.id] === undefined) return prev; - return prev + (curr[col.id] as number); - }, 0); + // only calculate the sumTotal for numerical columns + const sum = isBucket + ? 0 + : table.rows.reduce((prev, curr) => { + // some metrics return undefined for some of the values + // derivative is an example of this as it returns undefined in the first row + if (curr[col.id] === undefined) return prev; + return prev + (curr[col.id] as number); + }, 0); formattedColumn.sumTotal = sum; switch (visConfig.totalFunc) { @@ -164,7 +131,7 @@ export const convertToFormattedData = ( ); // column to show percentage was removed - if (insertAtIndex < 0) return { formattedRows, formattedColumns }; + if (insertAtIndex < 0) return { rows: table.rows, columns: table.columns, formattedColumns }; const { cols, rows } = addPercentageCol( formattedColumns, @@ -175,5 +142,5 @@ export const convertToFormattedData = ( formattedRows = rows; formattedColumns = cols; } - return { formattedRows, formattedColumns }; + return { rows: formattedRows, columns: table.columns, formattedColumns }; }; diff --git a/src/plugins/vis_type_table/public/utils/get_table_ui_state.test.ts b/src/plugins/vis_type_table/public/utils/get_table_ui_state.test.ts new file mode 100644 index 000000000000..64488d9275eb --- /dev/null +++ b/src/plugins/vis_type_table/public/utils/get_table_ui_state.test.ts @@ -0,0 +1,66 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { PersistedState } from '../../../visualizations/public'; +import { TableUiState, getTableUIState } from './get_table_ui_state'; +import { ColumnWidth, ColumnSort } from '../types'; + +describe('getTableUIState', () => { + let uiState: PersistedState; + let tableUiState: TableUiState; + + beforeEach(() => { + uiState = ({ + get: jest.fn(), + set: jest.fn(), + emit: jest.fn(), + } as unknown) as PersistedState; + tableUiState = getTableUIState(uiState); + }); + + it('should get initial sort and width values from uiState', () => { + const initialSort: ColumnSort = { colIndex: 1, direction: 'asc' }; + const initialWidth: ColumnWidth[] = [{ colIndex: 0, width: 100 }]; + + (uiState.get as jest.Mock).mockImplementation((key: string) => { + if (key === 'vis.sortColumn') return initialSort; + if (key === 'vis.columnsWidth') return initialWidth; + }); + + const newTableUiState = getTableUIState(uiState); + expect(newTableUiState.sort).toEqual(initialSort); + expect(newTableUiState.colWidth).toEqual(initialWidth); + }); + + it('should set and emit sort values', () => { + const newSort: ColumnSort = { colIndex: 2, direction: 'desc' }; + tableUiState.setSort(newSort); + + expect(uiState.set).toHaveBeenCalledWith('vis.sortColumn', newSort); + expect(uiState.emit).toHaveBeenCalledWith('reload'); + }); + + it('should set and emit width values for a new column', () => { + const newWidth: ColumnWidth = { colIndex: 1, width: 150 }; + tableUiState.setWidth(newWidth); + + expect(uiState.set).toHaveBeenCalledWith('vis.columnsWidth', [newWidth]); + expect(uiState.emit).toHaveBeenCalledWith('reload'); + }); + + it('should update and emit width values for an existing column', () => { + const initialWidth: ColumnWidth[] = [{ colIndex: 0, width: 100 }]; + (uiState.get as jest.Mock).mockReturnValue(initialWidth); + + const updatedTableUiState = getTableUIState(uiState); + + const updatedWidth: ColumnWidth = { colIndex: 0, width: 150 }; + updatedTableUiState.setWidth(updatedWidth); + + const expectedWidths = [{ colIndex: 0, width: 150 }]; + expect(uiState.set).toHaveBeenCalledWith('vis.columnsWidth', expectedWidths); + expect(uiState.emit).toHaveBeenCalledWith('reload'); + }); +}); diff --git a/src/plugins/vis_type_table/public/utils/get_table_ui_state.ts b/src/plugins/vis_type_table/public/utils/get_table_ui_state.ts new file mode 100644 index 000000000000..58fc6b472a40 --- /dev/null +++ b/src/plugins/vis_type_table/public/utils/get_table_ui_state.ts @@ -0,0 +1,40 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { PersistedState } from '../../../visualizations/public'; +import { ColumnSort, ColumnWidth } from '../types'; + +export interface TableUiState { + sort: ColumnSort; + setSort: (sort: ColumnSort) => void; + colWidth: ColumnWidth[]; + setWidth: (columnWidths: ColumnWidth) => void; +} + +export function getTableUIState(uiState: PersistedState): TableUiState { + const sort: ColumnSort = uiState.get('vis.sortColumn') || {}; + const colWidth: ColumnWidth[] = uiState.get('vis.columnsWidth') || []; + + const setSort = (newSort: ColumnSort) => { + uiState.set('vis.sortColumn', newSort); + uiState.emit('reload'); + }; + + const setWidth = (columnWidth: ColumnWidth) => { + const nextState = [...colWidth]; + const curColIndex = colWidth.findIndex((col) => col.colIndex === columnWidth.colIndex); + + if (curColIndex < 0) { + nextState.push(columnWidth); + } else { + nextState[curColIndex] = columnWidth; + } + + uiState.set('vis.columnsWidth', nextState); + uiState.emit('reload'); + }; + + return { sort, setSort, colWidth, setWidth }; +} diff --git a/src/plugins/vis_type_table/public/utils/index.ts b/src/plugins/vis_type_table/public/utils/index.ts index 1fd0e3f1e0fd..3277d92efc71 100644 --- a/src/plugins/vis_type_table/public/utils/index.ts +++ b/src/plugins/vis_type_table/public/utils/index.ts @@ -6,3 +6,5 @@ export * from './convert_to_csv_data'; export * from './convert_to_formatted_data'; export * from './use_pagination'; +export * from './add_percentage_col'; +export * from './get_table_ui_state'; diff --git a/src/plugins/vis_type_table/public/utils/use_pagination.test.ts b/src/plugins/vis_type_table/public/utils/use_pagination.test.ts new file mode 100644 index 000000000000..d8e7a02a0799 --- /dev/null +++ b/src/plugins/vis_type_table/public/utils/use_pagination.test.ts @@ -0,0 +1,101 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { act, renderHook } from '@testing-library/react-hooks'; +import { AggTypes, TableVisParams } from '../types'; +import { usePagination } from './use_pagination'; + +describe('usePagination', () => { + const visParams = { + perPage: 10, + showPartialRows: false, + showMetricsAtAllLevels: false, + showTotal: false, + totalFunc: AggTypes.SUM, + percentageCol: '', + } as TableVisParams; + + it('should not set pagination if perPage is empty string', () => { + const params = { + ...visParams, + perPage: '', + }; + const { result } = renderHook(() => usePagination(params, 20)); + expect(result.current).toEqual(undefined); + }); + + it('should init pagination', () => { + const { result } = renderHook(() => usePagination(visParams, 20)); + expect(result.current).toEqual({ + pageIndex: 0, + pageSize: 10, + onChangeItemsPerPage: expect.any(Function), + onChangePage: expect.any(Function), + }); + }); + + it('should init pagination with pageSize as the minimum of perPage and nRow', () => { + const { result } = renderHook(() => usePagination(visParams, 8)); + expect(result.current).toEqual({ + pageIndex: 0, + pageSize: 8, + onChangeItemsPerPage: expect.any(Function), + onChangePage: expect.any(Function), + }); + }); + + it('should set pageSize to the lesser of perPage and nRow when nRow is less than perPage', () => { + const { result } = renderHook(() => usePagination(visParams, 5)); + expect(result.current).toEqual({ + pageIndex: 0, + pageSize: 5, + onChangeItemsPerPage: expect.any(Function), + onChangePage: expect.any(Function), + }); + }); + + it('should set page index via onChangePage', () => { + const { result } = renderHook(() => usePagination(visParams, 50)); + act(() => { + // set page index to 3 + result.current?.onChangePage(3); + }); + expect(result.current?.pageIndex).toEqual(3); + }); + + it('should set to max page index via onChangePage if exceed maxiPageIndex', () => { + const { result, rerender } = renderHook((props) => usePagination(props.visParams, props.nRow), { + initialProps: { + visParams, + nRow: 55, + }, + }); + + act(() => { + // set page index to the last page + result.current?.onChangePage(5); + }); + + rerender({ visParams, nRow: 15 }); + // when the number of rows decreases, page index should + // be set to maxiPageIndex + expect(result.current).toEqual({ + pageIndex: 1, + pageSize: 10, + onChangeItemsPerPage: expect.any(Function), + onChangePage: expect.any(Function), + }); + }); + + it('should pagination via onChangeItemsPerPage', () => { + const { result } = renderHook(() => usePagination(visParams, 20)); + act(() => { + // set page size to 5 + result.current?.onChangeItemsPerPage(5); + }); + + expect(result.current?.pageSize).toEqual(5); + }); +}); diff --git a/src/plugins/vis_type_table/public/utils/use_pagination.ts b/src/plugins/vis_type_table/public/utils/use_pagination.ts index e3993f1c0868..97ecfd6b85e6 100644 --- a/src/plugins/vis_type_table/public/utils/use_pagination.ts +++ b/src/plugins/vis_type_table/public/utils/use_pagination.ts @@ -4,12 +4,12 @@ */ import { useCallback, useEffect, useMemo, useState } from 'react'; -import { TableVisConfig } from '../types'; +import { TableVisParams } from '../types'; -export const usePagination = (visConfig: TableVisConfig, nRow: number) => { +export const usePagination = (visParams: TableVisParams, nRow: number) => { const [pagination, setPagination] = useState({ pageIndex: 0, - pageSize: Math.min(visConfig.perPage || 10, nRow), + pageSize: Math.min(visParams.perPage || 0, nRow), }); const onChangeItemsPerPage = useCallback( (pageSize) => setPagination((p) => ({ ...p, pageSize, pageIndex: 0 })), @@ -20,20 +20,23 @@ export const usePagination = (visConfig: TableVisConfig, nRow: number) => { ]); useEffect(() => { - const perPage = Math.min(visConfig.perPage || 10, nRow); + const perPage = Math.min(visParams.perPage || 0, nRow); const maxiPageIndex = Math.ceil(nRow / perPage) - 1; setPagination((p) => ({ pageIndex: p.pageIndex > maxiPageIndex ? maxiPageIndex : p.pageIndex, pageSize: perPage, })); - }, [nRow, visConfig.perPage]); + }, [nRow, visParams.perPage]); return useMemo( - () => ({ - ...pagination, - onChangeItemsPerPage, - onChangePage, - }), + () => + pagination.pageSize + ? { + ...pagination, + onChangeItemsPerPage, + onChangePage, + } + : undefined, [pagination, onChangeItemsPerPage, onChangePage] ); }; From 14bde2bba9b338183f117a8e852cbd4ce02c0694 Mon Sep 17 00:00:00 2001 From: Ashwin P Chandran Date: Mon, 17 Apr 2023 16:00:28 -0700 Subject: [PATCH 03/35] Adds toast ID to toast api (#3752) * Currently sending the same toast multiple times results in multiple toasts being rendered on the screen. This change allows the toast api to additionally accept an id parameter that * Update changelog * Update src/core/public/notifications/toasts/toasts_api.tsx Issue Resolved: https://github.com/opensearch-project/OpenSearch-Dashboards/issues/2643 Signed-off-by: Ashwin P Chandran Co-authored-by: Josh Romero --------- Signed-off-by: Ashwin P Chandran Co-authored-by: Sean Neumann <1413295+seanneumann@users.noreply.github.com> Co-authored-by: Josh Romero --- CHANGELOG.md | 1 + .../notifications/toasts/toasts_api.test.ts | 23 +++++++++++++++++++ .../notifications/toasts/toasts_api.tsx | 14 ++++++++--- .../application/components/workspace.tsx | 6 ++++- .../utils/state_management/store.ts | 4 ++-- src/plugins/vis_builder/public/plugin.ts | 3 ++- 6 files changed, 44 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 001f9248f2b7..92adeabb33a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -73,6 +73,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [Multiple DataSource] Refactor dev tool console to use opensearch-js client to send requests ([#3544](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3544)) - Add `osd-xsrf` header to all requests that incorrectly used `node-version` to satisfy XSRF protection ([#3643](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3643)) - [Data] Add geo shape filter field ([#3605](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3605)) +- [Notifications] Adds id to toast api for deduplication ([#3752](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3752)) - [VisBuilder] Add UI actions handler ([#3732](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3732)) - [Dashboard] Indicate that IE is no longer supported ([#3641](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3641)) - [UI] Add support for comma delimiters in the global filter bar ([#3686](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3686)) diff --git a/src/core/public/notifications/toasts/toasts_api.test.ts b/src/core/public/notifications/toasts/toasts_api.test.ts index ef4f469194d7..dacfa98b623a 100644 --- a/src/core/public/notifications/toasts/toasts_api.test.ts +++ b/src/core/public/notifications/toasts/toasts_api.test.ts @@ -105,6 +105,24 @@ describe('#get$()', () => { toasts.remove('bar'); expect(onToasts).not.toHaveBeenCalled(); }); + + it('does not emit a new toast list when a toast with the same id is passed to add()', () => { + const toasts = new ToastsApi(toastDeps()); + const onToasts = jest.fn(); + + toasts.get$().subscribe(onToasts); + toasts.add({ + id: 'foo', + title: 'foo', + }); + onToasts.mockClear(); + + toasts.add({ + id: 'foo', + title: 'bar', + }); + expect(onToasts).not.toHaveBeenCalled(); + }); }); describe('#add()', () => { @@ -135,6 +153,11 @@ describe('#add()', () => { const toasts = new ToastsApi(toastDeps()); expect(toasts.add('foo')).toHaveProperty('title', 'foo'); }); + + it('accepts an id and does not auto increment', async () => { + const toasts = new ToastsApi(toastDeps()); + expect(toasts.add({ id: 'foo', title: 'not foo' })).toHaveProperty('id', 'foo'); + }); }); describe('#remove()', () => { diff --git a/src/core/public/notifications/toasts/toasts_api.tsx b/src/core/public/notifications/toasts/toasts_api.tsx index 008c4fb3c507..76d0bf9cf9b2 100644 --- a/src/core/public/notifications/toasts/toasts_api.tsx +++ b/src/core/public/notifications/toasts/toasts_api.tsx @@ -42,12 +42,10 @@ import { I18nStart } from '../../i18n'; /** * Allowed fields for {@link ToastInput}. * - * @remarks - * `id` cannot be specified. - * * @public */ export type ToastInputFields = Pick> & { + id?: string; title?: string | MountPoint; text?: string | MountPoint; }; @@ -143,6 +141,16 @@ export class ToastsApi implements IToasts { * @returns a {@link Toast} */ public add(toastOrTitle: ToastInput) { + if (typeof toastOrTitle !== 'string') { + const toastObject = toastOrTitle; + const list = this.toasts$.getValue(); + const existingToast = list.find((toast) => toast.id === toastObject.id); + + if (existingToast) { + return existingToast; + } + } + const toast: Toast = { id: String(this.idCounter++), toastLifeTimeMs: this.uiSettings.get('notifications:lifetime:info'), diff --git a/src/plugins/vis_builder/public/application/components/workspace.tsx b/src/plugins/vis_builder/public/application/components/workspace.tsx index 214a735f6ba8..baccd4faf342 100644 --- a/src/plugins/vis_builder/public/application/components/workspace.tsx +++ b/src/plugins/vis_builder/public/application/components/workspace.tsx @@ -56,7 +56,11 @@ export const WorkspaceUI = () => { const err = schemaValidation.errorMsg || aggValidation.errorMsg; - if (err) toasts.addWarning(err); + if (err) + toasts.addWarning({ + id: 'vb_expression_validation', + title: err, + }); return; } diff --git a/src/plugins/vis_builder/public/application/utils/state_management/store.ts b/src/plugins/vis_builder/public/application/utils/state_management/store.ts index f1b1c0eeae2a..588c221a50fd 100644 --- a/src/plugins/vis_builder/public/application/utils/state_management/store.ts +++ b/src/plugins/vis_builder/public/application/utils/state_management/store.ts @@ -47,9 +47,9 @@ export const getPreloadedStore = async (services: VisBuilderServices) => { }; // the store subscriber will automatically detect changes and call handleChange function - store.subscribe(handleChange); + const unsubscribe = store.subscribe(handleChange); - return store; + return { store, unsubscribe }; }; // Infer the `RootState` and `AppDispatch` types from the store itself diff --git a/src/plugins/vis_builder/public/plugin.ts b/src/plugins/vis_builder/public/plugin.ts index 0c1d569f6bed..5744341ab3cf 100644 --- a/src/plugins/vis_builder/public/plugin.ts +++ b/src/plugins/vis_builder/public/plugin.ts @@ -163,7 +163,7 @@ export class VisBuilderPlugin }; // Instantiate the store - const store = await getPreloadedStore(services); + const { store, unsubscribe: unsubscribeStore } = await getPreloadedStore(services); const unmount = renderApp(params, services, store); // Render the application @@ -171,6 +171,7 @@ export class VisBuilderPlugin unlistenParentHistory(); unmount(); appUnMounted(); + unsubscribeStore(); }; }, }); From 8b0587e7aa683ebe9a25c4a454ddb117c68d7335 Mon Sep 17 00:00:00 2001 From: sayuree <52072402+sayuree@users.noreply.github.com> Date: Tue, 18 Apr 2023 05:11:43 +0600 Subject: [PATCH 04/35] Add tooltip to help icon (#3626) * Add tooltip to help icon Signed-off-by: sabina.zaripova Add tooltip to menu icon Signed-off-by: sabina.zaripova --- CHANGELOG.md | 1 + .../header/__snapshots__/header.test.tsx.snap | 46 +++++++++++++++++++ .../header_help_menu.test.tsx.snap | 40 ++++++++++++++++ src/core/public/chrome/ui/header/header.tsx | 8 +++- .../chrome/ui/header/header_help_menu.tsx | 8 ++++ 5 files changed, 102 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 92adeabb33a7..f2c52847d8b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -245,6 +245,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [TSVB] Fixes undefined serial diff aggregation documentation link ([#3503](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3503)) - [Console] Fix dev tool console autocomplete not loading issue ([#3775](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3775)) - [Console] Fix dev tool console run command with query parameter error ([#3813](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3813)) +- Add clarifying tooltips to header navigation ([#3573](https://github.com/opensearch-project/OpenSearch-Dashboards/issues/3573)) ### 🚞 Infrastructure diff --git a/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap index c7e883d9b00b..6486e207ed37 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap @@ -2798,6 +2798,7 @@ exports[`Header handles visibility and lock changes 1`] = ` > @@ -2842,11 +2843,13 @@ exports[`Header handles visibility and lock changes 1`] = ` > @@ -5416,6 +5419,11 @@ exports[`Header handles visibility and lock changes 1`] = ` size="m" type="questionInCircle" /> + } closePopover={[Function]} @@ -5467,6 +5475,10 @@ exports[`Header handles visibility and lock changes 1`] = ` + @@ -5514,6 +5526,17 @@ exports[`Header handles visibility and lock changes 1`] = ` size="m" /> + + + @@ -8117,6 +8140,7 @@ exports[`Header renders condensed header 1`] = ` > @@ -8161,11 +8185,13 @@ exports[`Header renders condensed header 1`] = ` > @@ -10652,6 +10678,11 @@ exports[`Header renders condensed header 1`] = ` size="m" type="questionInCircle" /> + } closePopover={[Function]} @@ -10703,6 +10734,10 @@ exports[`Header renders condensed header 1`] = ` + @@ -10750,6 +10785,17 @@ exports[`Header renders condensed header 1`] = ` size="m" /> + + + diff --git a/src/core/public/chrome/ui/header/__snapshots__/header_help_menu.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/header_help_menu.test.tsx.snap index 835c2d8d4a4e..5406fbf6abaa 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/header_help_menu.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/header_help_menu.test.tsx.snap @@ -1626,6 +1626,11 @@ exports[`Header help menu hides survey link 1`] = ` size="m" type="questionInCircle" /> + } closePopover={[Function]} @@ -1677,6 +1682,10 @@ exports[`Header help menu hides survey link 1`] = ` + @@ -1724,6 +1733,17 @@ exports[`Header help menu hides survey link 1`] = ` size="m" /> + + + @@ -3782,6 +3802,11 @@ exports[`Header help menu renders survey link 1`] = ` size="m" type="questionInCircle" /> + } closePopover={[Function]} @@ -3833,6 +3858,10 @@ exports[`Header help menu renders survey link 1`] = ` + @@ -3880,6 +3909,17 @@ exports[`Header help menu renders survey link 1`] = ` size="m" /> + + + diff --git a/src/core/public/chrome/ui/header/header.tsx b/src/core/public/chrome/ui/header/header.tsx index 917d1ebd4c46..a78371f4f264 100644 --- a/src/core/public/chrome/ui/header/header.tsx +++ b/src/core/public/chrome/ui/header/header.tsx @@ -173,7 +173,13 @@ export function Header({ aria-controls={navId} ref={toggleCollapsibleNavRef} > - + diff --git a/src/core/public/chrome/ui/header/header_help_menu.tsx b/src/core/public/chrome/ui/header/header_help_menu.tsx index 6f536184edc1..f0be769937f5 100644 --- a/src/core/public/chrome/ui/header/header_help_menu.tsx +++ b/src/core/public/chrome/ui/header/header_help_menu.tsx @@ -331,6 +331,14 @@ class HeaderHelpMenuUI extends Component { onClick={this.onMenuButtonClick} > + ); From d110fc69f4e431c6b3f4adda7e3578acd6a64080 Mon Sep 17 00:00:00 2001 From: Kawika Avilla Date: Mon, 17 Apr 2023 17:08:36 -0700 Subject: [PATCH 05/35] [Dashboards Listing] Fix edit button (#3862) Fixes the edit button by not adding the base path to the URL when the route continues from the dashboards path. Also re-add `addBasePath` to the interface defined by RenderDeps as before it was removed since we navigated away from using it but was missed in reverting back to the original implementation. Issue Resolved: https://github.com/opensearch-project/OpenSearch-Dashboards/issues/3861 Signed-off-by: Kawika Avilla --- src/plugins/dashboard/public/application/application.ts | 1 + src/plugins/dashboard/public/application/legacy_app.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/plugins/dashboard/public/application/application.ts b/src/plugins/dashboard/public/application/application.ts index e527565b3c4e..f29b1bd9180a 100644 --- a/src/plugins/dashboard/public/application/application.ts +++ b/src/plugins/dashboard/public/application/application.ts @@ -81,6 +81,7 @@ export interface RenderDeps { }; uiSettings: IUiSettingsClient; chrome: ChromeStart; + addBasePath: (path: string) => string; savedQueryService: DataPublicPluginStart['query']['savedQueries']; embeddable: EmbeddableStart; localStorage: Storage; diff --git a/src/plugins/dashboard/public/application/legacy_app.js b/src/plugins/dashboard/public/application/legacy_app.js index b68958143275..027f65591bdb 100644 --- a/src/plugins/dashboard/public/application/legacy_app.js +++ b/src/plugins/dashboard/public/application/legacy_app.js @@ -165,7 +165,7 @@ export function initDashboardApp(app, deps) { }; $scope.editItem = ({ editUrl }) => { - history.push(deps.addBasePath(editUrl)); + history.push(editUrl); }; $scope.viewItem = ({ viewUrl }) => { history.push(deps.addBasePath(viewUrl)); From ee32d201d48595efefc922384a8f1f7aee5609fa Mon Sep 17 00:00:00 2001 From: Ashwin P Chandran Date: Mon, 17 Apr 2023 17:48:40 -0700 Subject: [PATCH 06/35] [VisBuilder] Adds UIState to vis, adds index patterns to embeddable, bug fixes (#3751) * adds uiActions to visBuilder * prevents multiple errors on load * fixes visbuilder type errors * fixes save * adds ui state to vis builder * fixes tests * Adds support in embeddables for multiple indices * Moves ui state to separate slice * Adds changelog Signed-off-by: Ashwin P Chandran --------- Signed-off-by: Ashwin P Chandran Co-authored-by: Josh Romero --- CHANGELOG.md | 2 + .../vis_builder_saved_object_attributes.ts | 1 + .../application/components/right_nav.tsx | 2 +- .../application/components/workspace.tsx | 25 ++++++-- .../public/application/utils/schema.json | 63 ++++++++++++------- .../utils/state_management/metadata_slice.ts | 2 +- .../utils/state_management/preload.ts | 3 + .../redux_persistence.test.tsx | 2 + .../state_management/redux_persistence.ts | 7 +-- .../utils/state_management/store.ts | 4 ++ .../utils/state_management/ui_state_slice.ts | 42 +++++++++++++ .../utils/use/use_saved_vis_builder_vis.ts | 10 ++- .../embeddable/vis_builder_embeddable.tsx | 45 ++++++++++--- .../vis_builder_embeddable_factory.tsx | 28 +++++---- src/plugins/vis_builder/public/plugin.ts | 9 ++- .../public/saved_visualizations/_saved_vis.ts | 4 +- .../saved_visualizations/transforms.test.ts | 6 +- .../public/saved_visualizations/transforms.ts | 7 ++- src/plugins/vis_builder/public/types.ts | 1 + .../server/saved_objects/vis_builder_app.ts | 4 ++ 20 files changed, 207 insertions(+), 60 deletions(-) create mode 100644 src/plugins/vis_builder/public/application/utils/state_management/ui_state_slice.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index f2c52847d8b8..2f49d62513d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -80,6 +80,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [Multiple DataSource] Allow create and distinguish index pattern with same name but from different datasources ([#3571](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3571)) - [Multiple DataSource] Integrate multiple datasource with dev tool console ([#3754](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3754)) - Add satisfaction survey link to help menu ([#3676] (https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3676)) +- [Vis Builder] Add persistence to visualizations inner state ([#3751](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3751)) - [Table Visualization] Move format table, consolidate types and add unit tests ([#3397](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3397)) ### 🐛 Bug Fixes @@ -126,6 +127,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [VisBuilder] Fix multiple warnings thrown on page load ([#3732](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3732)) - [VisBuilder] Fix Firefox legend selection issue ([#3732](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3732)) - [VisBuilder] Fix type errors ([#3732](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3732)) +- [VisBuilder] Fix indexpattern selection in filter bar ([#3751](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3751)) - [Table Visualization] Fix table rendering empty unused space ([#3797](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3797)) - [Table Visualization] Fix data table not adjusting height on the initial load ([#3816](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3816)) - Cleanup unused url ([#3847](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3847)) diff --git a/src/plugins/vis_builder/common/vis_builder_saved_object_attributes.ts b/src/plugins/vis_builder/common/vis_builder_saved_object_attributes.ts index 243e455d7157..3e157f9bf6df 100644 --- a/src/plugins/vis_builder/common/vis_builder_saved_object_attributes.ts +++ b/src/plugins/vis_builder/common/vis_builder_saved_object_attributes.ts @@ -13,6 +13,7 @@ export interface VisBuilderSavedObjectAttributes extends SavedObjectAttributes { visualizationState?: string; updated_at?: string; styleState?: string; + uiState?: string; version: number; searchSourceFields?: { index?: string; diff --git a/src/plugins/vis_builder/public/application/components/right_nav.tsx b/src/plugins/vis_builder/public/application/components/right_nav.tsx index 5d3f298c6bf3..fde4f3110d1c 100644 --- a/src/plugins/vis_builder/public/application/components/right_nav.tsx +++ b/src/plugins/vis_builder/public/application/components/right_nav.tsx @@ -122,6 +122,6 @@ const OptionItem = ({ icon, title }: { icon: IconType; title: string }) => ( ); -// The app uses EuiResizableContainer that triggers a rerender for ever mouseover action. +// The app uses EuiResizableContainer that triggers a rerender for every mouseover action. // To prevent this child component from unnecessarily rerendering in that instance, it needs to be memoized export const RightNav = React.memo(RightNavUI); diff --git a/src/plugins/vis_builder/public/application/components/workspace.tsx b/src/plugins/vis_builder/public/application/components/workspace.tsx index baccd4faf342..31880e93bb7f 100644 --- a/src/plugins/vis_builder/public/application/components/workspace.tsx +++ b/src/plugins/vis_builder/public/application/components/workspace.tsx @@ -10,7 +10,7 @@ import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react import { IExpressionLoaderParams } from '../../../../expressions/public'; import { VisBuilderServices } from '../../types'; import { validateSchemaState, validateAggregations } from '../utils/validations'; -import { useTypedSelector } from '../utils/state_management'; +import { useTypedDispatch, useTypedSelector, setUIStateState } from '../utils/state_management'; import { useAggs, useVisualizationType } from '../utils/use'; import { PersistedState } from '../../../../visualizations/public'; @@ -39,8 +39,25 @@ export const WorkspaceUI = () => { timeRange: data.query.timefilter.timefilter.getTime(), }); const rootState = useTypedSelector((state) => state); - // Visualizations require the uiState to persist even when the expression changes - const uiState = useMemo(() => new PersistedState(), []); + const dispatch = useTypedDispatch(); + // Visualizations require the uiState object to persist even when the expression changes + // eslint-disable-next-line react-hooks/exhaustive-deps + const uiState = useMemo(() => new PersistedState(rootState.ui), []); + + useEffect(() => { + if (rootState.metadata.editor.state === 'loaded') { + uiState.setSilent(rootState.ui); + } + // To update uiState once saved object data is loaded + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [rootState.metadata.editor.state, uiState]); + + useEffect(() => { + uiState.on('change', (args) => { + // Store changes to UI state + dispatch(setUIStateState(uiState.toJSON())); + }); + }, [dispatch, uiState]); useEffect(() => { async function loadExpression() { @@ -137,6 +154,6 @@ export const WorkspaceUI = () => { ); }; -// The app uses EuiResizableContainer that triggers a rerender for ever mouseover action. +// The app uses EuiResizableContainer that triggers a rerender for every mouseover action. // To prevent this child component from unnecessarily rerendering in that instance, it needs to be memoized export const Workspace = React.memo(WorkspaceUI); diff --git a/src/plugins/vis_builder/public/application/utils/schema.json b/src/plugins/vis_builder/public/application/utils/schema.json index 9effed97b2be..7cf8bbc2534f 100644 --- a/src/plugins/vis_builder/public/application/utils/schema.json +++ b/src/plugins/vis_builder/public/application/utils/schema.json @@ -1,28 +1,47 @@ { - "type": "object", - "properties": { - "styleState": { - "type": "object" - }, - "visualizationState": { - "type": "object", - "properties": { - "activeVisualization": { - "type": "object", - "properties": { - "name": { "type": "string" }, - "aggConfigParams": { "type": "array" } + "type": "object", + "properties": { + "styleState": { + "type": "object" + }, + "visualizationState": { + "type": "object", + "properties": { + "activeVisualization": { + "type": "object", + "properties": { + "name": { + "type": "string" }, - "required": ["name", "aggConfigParams"], - "additionalProperties": false + "aggConfigParams": { + "type": "array" + } }, - "indexPattern": { "type": "string" }, - "searchField": { "type": "string" } + "required": [ + "name", + "aggConfigParams" + ], + "additionalProperties": false }, - "required": ["searchField"], - "additionalProperties": false - } + "indexPattern": { + "type": "string" + }, + "searchField": { + "type": "string" + } + }, + "required": [ + "searchField" + ], + "additionalProperties": false }, - "required": ["styleState", "visualizationState"], - "additionalProperties": false + "uiState": { + "type": "object" + } + }, + "required": [ + "styleState", + "visualizationState" + ], + "additionalProperties": false } \ No newline at end of file diff --git a/src/plugins/vis_builder/public/application/utils/state_management/metadata_slice.ts b/src/plugins/vis_builder/public/application/utils/state_management/metadata_slice.ts index 05ceb324aaa1..880c15f3e44a 100644 --- a/src/plugins/vis_builder/public/application/utils/state_management/metadata_slice.ts +++ b/src/plugins/vis_builder/public/application/utils/state_management/metadata_slice.ts @@ -11,7 +11,7 @@ import { VisBuilderServices } from '../../../types'; * Clean state: when viz finished loading and ready to be edited * Dirty state: when there are changes applied to the viz after it finished loading */ -type EditorState = 'loading' | 'clean' | 'dirty'; +type EditorState = 'loading' | 'loaded' | 'clean' | 'dirty'; export interface MetadataState { editor: { diff --git a/src/plugins/vis_builder/public/application/utils/state_management/preload.ts b/src/plugins/vis_builder/public/application/utils/state_management/preload.ts index 43aa2e7b8ede..f7a0f6bd7ad3 100644 --- a/src/plugins/vis_builder/public/application/utils/state_management/preload.ts +++ b/src/plugins/vis_builder/public/application/utils/state_management/preload.ts @@ -8,6 +8,7 @@ import { VisBuilderServices } from '../../..'; import { getPreloadedState as getPreloadedStyleState } from './style_slice'; import { getPreloadedState as getPreloadedVisualizationState } from './visualization_slice'; import { getPreloadedState as getPreloadedMetadataState } from './metadata_slice'; +import { getPreloadedState as getPreloadedUIState } from './ui_state_slice'; import { RootState } from './store'; export const getPreloadedState = async ( @@ -16,10 +17,12 @@ export const getPreloadedState = async ( const styleState = await getPreloadedStyleState(services); const visualizationState = await getPreloadedVisualizationState(services); const metadataState = await getPreloadedMetadataState(services); + const uiState = await getPreloadedUIState(services); return { style: styleState, visualization: visualizationState, metadata: metadataState, + ui: uiState, }; }; diff --git a/src/plugins/vis_builder/public/application/utils/state_management/redux_persistence.test.tsx b/src/plugins/vis_builder/public/application/utils/state_management/redux_persistence.test.tsx index 91f760bbf231..a46d5c027656 100644 --- a/src/plugins/vis_builder/public/application/utils/state_management/redux_persistence.test.tsx +++ b/src/plugins/vis_builder/public/application/utils/state_management/redux_persistence.test.tsx @@ -18,6 +18,7 @@ describe('test redux state persistence', () => { style: 'style', visualization: 'visualization', metadata: 'metadata', + ui: 'ui', }; }); @@ -33,6 +34,7 @@ describe('test redux state persistence', () => { editor: { errors: {}, state: 'loading' }, originatingApp: undefined, }, + ui: {}, }; const returnStates = await loadReduxState(mockServices); diff --git a/src/plugins/vis_builder/public/application/utils/state_management/redux_persistence.ts b/src/plugins/vis_builder/public/application/utils/state_management/redux_persistence.ts index a531986a9ac9..3ebfa47268ec 100644 --- a/src/plugins/vis_builder/public/application/utils/state_management/redux_persistence.ts +++ b/src/plugins/vis_builder/public/application/utils/state_management/redux_persistence.ts @@ -12,22 +12,21 @@ export const loadReduxState = async (services: VisBuilderServices) => { const serializedState = services.osdUrlStateStorage.get('_a'); if (serializedState !== null) return serializedState; } catch (err) { - /* eslint-disable no-console */ + // eslint-disable-next-line no-console console.error(err); - /* eslint-enable no-console */ } return await getPreloadedState(services); }; export const persistReduxState = ( - { style, visualization, metadata }, + { style, visualization, metadata, ui }: RootState, services: VisBuilderServices ) => { try { services.osdUrlStateStorage.set( '_a', - { style, visualization, metadata }, + { style, visualization, metadata, ui }, { replace: true, } diff --git a/src/plugins/vis_builder/public/application/utils/state_management/store.ts b/src/plugins/vis_builder/public/application/utils/state_management/store.ts index 588c221a50fd..8fe5c23fd657 100644 --- a/src/plugins/vis_builder/public/application/utils/state_management/store.ts +++ b/src/plugins/vis_builder/public/application/utils/state_management/store.ts @@ -8,12 +8,14 @@ import { isEqual } from 'lodash'; import { reducer as styleReducer } from './style_slice'; import { reducer as visualizationReducer } from './visualization_slice'; import { reducer as metadataReducer } from './metadata_slice'; +import { reducer as uiStateReducer } from './ui_state_slice'; import { VisBuilderServices } from '../../..'; import { loadReduxState, persistReduxState } from './redux_persistence'; import { handlerEditorState } from './handlers/editor_state'; import { handlerParentAggs } from './handlers/parent_aggs'; const rootReducer = combineReducers({ + ui: uiStateReducer, style: styleReducer, visualization: visualizationReducer, metadata: metadataReducer, @@ -60,3 +62,5 @@ export type AppDispatch = Store['dispatch']; export { setState as setStyleState, StyleState } from './style_slice'; export { setState as setVisualizationState, VisualizationState } from './visualization_slice'; +export { MetadataState } from './metadata_slice'; +export { setState as setUIStateState, UIStateState } from './ui_state_slice'; diff --git a/src/plugins/vis_builder/public/application/utils/state_management/ui_state_slice.ts b/src/plugins/vis_builder/public/application/utils/state_management/ui_state_slice.ts new file mode 100644 index 000000000000..826fe9d9873d --- /dev/null +++ b/src/plugins/vis_builder/public/application/utils/state_management/ui_state_slice.ts @@ -0,0 +1,42 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { VisBuilderServices } from '../../../types'; + +export type UIStateState = T; + +const initialState = {} as UIStateState; + +export const getPreloadedState = async ({ + types, + data, +}: VisBuilderServices): Promise => { + return initialState; +}; + +export const uiStateSlice = createSlice({ + name: 'ui', + initialState, + reducers: { + setState(state: T, action: PayloadAction>) { + return action.payload; + }, + updateState(state: T, action: PayloadAction>>) { + state = { + ...state, + ...action.payload, + }; + }, + }, +}); + +// Exposing the state functions as generics +export const setState = uiStateSlice.actions.setState as (payload: T) => PayloadAction; +export const updateState = uiStateSlice.actions.updateState as ( + payload: Partial +) => PayloadAction>; + +export const { reducer } = uiStateSlice; diff --git a/src/plugins/vis_builder/public/application/utils/use/use_saved_vis_builder_vis.ts b/src/plugins/vis_builder/public/application/utils/use/use_saved_vis_builder_vis.ts index 604c90a25ccd..44ffbaf75953 100644 --- a/src/plugins/vis_builder/public/application/utils/use/use_saved_vis_builder_vis.ts +++ b/src/plugins/vis_builder/public/application/utils/use/use_saved_vis_builder_vis.ts @@ -14,7 +14,12 @@ import { import { EDIT_PATH, PLUGIN_ID } from '../../../../common'; import { VisBuilderServices } from '../../../types'; import { getCreateBreadcrumbs, getEditBreadcrumbs } from '../breadcrumbs'; -import { useTypedDispatch, setStyleState, setVisualizationState } from '../state_management'; +import { + useTypedDispatch, + setStyleState, + setVisualizationState, + setUIStateState, +} from '../state_management'; import { useOpenSearchDashboards } from '../../../../../opensearch_dashboards_react/public'; import { setEditorState } from '../state_management/metadata_slice'; import { getStateFromSavedObject } from '../../../saved_visualizations/transforms'; @@ -46,6 +51,7 @@ export const useSavedVisBuilderVis = (visualizationIdFromUrl: string | undefined const loadSavedVisBuilderVis = async () => { try { + dispatch(setEditorState({ state: 'loading' })); const savedVisBuilderVis = await getSavedVisBuilderVis( savedVisBuilderLoader, visualizationIdFromUrl @@ -56,8 +62,10 @@ export const useSavedVisBuilderVis = (visualizationIdFromUrl: string | undefined chrome.setBreadcrumbs(getEditBreadcrumbs(title, navigateToApp)); chrome.docTitle.change(title); + dispatch(setUIStateState(state.ui)); dispatch(setStyleState(state.style)); dispatch(setVisualizationState(state.visualization)); + dispatch(setEditorState({ state: 'loaded' })); } else { chrome.setBreadcrumbs(getCreateBreadcrumbs(navigateToApp)); } diff --git a/src/plugins/vis_builder/public/embeddable/vis_builder_embeddable.tsx b/src/plugins/vis_builder/public/embeddable/vis_builder_embeddable.tsx index 7142c9c3c0cf..a931877ffe6d 100644 --- a/src/plugins/vis_builder/public/embeddable/vis_builder_embeddable.tsx +++ b/src/plugins/vis_builder/public/embeddable/vis_builder_embeddable.tsx @@ -22,6 +22,7 @@ import { } from '../../../expressions/public'; import { Filter, + IIndexPattern, opensearchFilters, Query, TimefilterContract, @@ -37,20 +38,23 @@ import { import { PersistedState } from '../../../visualizations/public'; import { VisBuilderSavedVis } from '../saved_visualizations/transforms'; import { handleVisEvent } from '../application/utils/handle_vis_event'; +import { VisBuilderEmbeddableFactoryDeps } from './vis_builder_embeddable_factory'; // Apparently this needs to match the saved object type for the clone and replace panel actions to work export const VISBUILDER_EMBEDDABLE = VISBUILDER_SAVED_OBJECT; export interface VisBuilderEmbeddableConfiguration { savedVis: VisBuilderSavedVis; - // TODO: add indexPatterns as part of configuration - // indexPatterns?: IIndexPattern[]; + indexPatterns?: IIndexPattern[]; editPath: string; editUrl: string; editable: boolean; + deps: VisBuilderEmbeddableFactoryDeps; } -export type VisBuilderInput = SavedObjectEmbeddableInput; +export interface VisBuilderInput extends SavedObjectEmbeddableInput { + uiState?: any; +} export interface VisBuilderOutput extends EmbeddableOutput { /** @@ -58,11 +62,12 @@ export interface VisBuilderOutput extends EmbeddableOutput { * `input.savedObjectId`. If the id is invalid, this may be undefined. */ savedVis?: VisBuilderSavedVis; + indexPatterns?: IIndexPattern[]; } type ExpressionLoader = InstanceType; -export class VisBuilderEmbeddable extends Embeddable { +export class VisBuilderEmbeddable extends Embeddable { public readonly type = VISBUILDER_EMBEDDABLE; private handler?: ExpressionLoader; private timeRange?: TimeRange; @@ -75,11 +80,19 @@ export class VisBuilderEmbeddable extends Embeddable s.unsubscribe()); + this.uiState.off('change', this.uiStateChangeHandler); + this.uiState.off('reload', this.reload); if (this.node) { ReactDOM.unmountComponentAtNode(this.node); } @@ -249,6 +268,7 @@ export class VisBuilderEmbeddable extends Embeddable { + this.updateInput({ + uiState: this.uiState.toJSON(), + }); + }; + + private transferInputToUiState = () => { + if (JSON.stringify(this.input.uiState) !== this.uiState.toString()) + this.uiState.set(this.input.uiState); + }; + // TODO: we may eventually need to add support for visualizations that use triggers like filter or brush, but current VisBuilder vis types don't support triggers // public supportedTriggers(): TriggerId[] { // return this.visType.getSupportedTriggers?.() ?? []; diff --git a/src/plugins/vis_builder/public/embeddable/vis_builder_embeddable_factory.tsx b/src/plugins/vis_builder/public/embeddable/vis_builder_embeddable_factory.tsx index f80ad18ee363..90048ba91322 100644 --- a/src/plugins/vis_builder/public/embeddable/vis_builder_embeddable_factory.tsx +++ b/src/plugins/vis_builder/public/embeddable/vis_builder_embeddable_factory.tsx @@ -5,7 +5,6 @@ import { i18n } from '@osd/i18n'; import { - EmbeddableFactory, EmbeddableFactoryDefinition, EmbeddableOutput, ErrorEmbeddable, @@ -35,16 +34,14 @@ import { getTimeFilter, getUISettings, } from '../plugin_services'; +import { StartServicesGetter } from '../../../opensearch_dashboards_utils/public'; +import { VisBuilderPluginStartDependencies } from '../types'; -// TODO: use or remove? -export type VisBuilderEmbeddableFactory = EmbeddableFactory< - SavedObjectEmbeddableInput, - VisBuilderOutput | EmbeddableOutput, - VisBuilderEmbeddable | DisabledEmbeddable, - VisBuilderSavedObjectAttributes ->; +export interface VisBuilderEmbeddableFactoryDeps { + start: StartServicesGetter; +} -export class VisBuilderEmbeddableFactoryDefinition +export class VisBuilderEmbeddableFactory implements EmbeddableFactoryDefinition< SavedObjectEmbeddableInput, @@ -62,7 +59,7 @@ export class VisBuilderEmbeddableFactoryDefinition }; // TODO: Would it be better to explicitly declare start service dependencies? - constructor() {} + constructor(private readonly deps: VisBuilderEmbeddableFactoryDeps) {} public canCreateNew() { // Because VisBuilder creation starts with the visualization modal, no need to have a separate entry for VisBuilder until it's separate @@ -90,13 +87,22 @@ export class VisBuilderEmbeddableFactoryDefinition return new DisabledEmbeddable(PLUGIN_NAME, input); } + const savedVis = getStateFromSavedObject(savedObject); + const indexPatternService = this.deps.start().plugins.data.indexPatterns; + const indexPattern = await indexPatternService.get( + savedVis.state.visualization.indexPattern || '' + ); + const indexPatterns = indexPattern ? [indexPattern] : []; + return new VisBuilderEmbeddable( getTimeFilter(), { - savedVis: getStateFromSavedObject(savedObject), + savedVis, editUrl, editPath, editable: true, + deps: this.deps, + indexPatterns, }, { ...input, diff --git a/src/plugins/vis_builder/public/plugin.ts b/src/plugins/vis_builder/public/plugin.ts index 5744341ab3cf..1445de923010 100644 --- a/src/plugins/vis_builder/public/plugin.ts +++ b/src/plugins/vis_builder/public/plugin.ts @@ -23,7 +23,7 @@ import { VisBuilderSetup, VisBuilderStart, } from './types'; -import { VisBuilderEmbeddableFactoryDefinition, VISBUILDER_EMBEDDABLE } from './embeddable'; +import { VisBuilderEmbeddableFactory, VISBUILDER_EMBEDDABLE } from './embeddable'; import visBuilderIconSecondaryFill from './assets/vis_builder_icon_secondary_fill.svg'; import visBuilderIcon from './assets/vis_builder_icon.svg'; import { @@ -54,6 +54,7 @@ import { ConfigSchema } from '../config'; import { createOsdUrlStateStorage, createOsdUrlTracker, + createStartServicesGetter, withNotifyOnErrors, } from '../../opensearch_dashboards_utils/public'; import { opensearchFilters } from '../../data/public'; @@ -177,10 +178,8 @@ export class VisBuilderPlugin }); // Register embeddable - // TODO: investigate simplification via getter a la visualizations: - // const start = createStartServicesGetter(core.getStartServices)); - // const embeddableFactory = new VisBuilderEmbeddableFactoryDefinition({ start }); - const embeddableFactory = new VisBuilderEmbeddableFactoryDefinition(); + const start = createStartServicesGetter(core.getStartServices); + const embeddableFactory = new VisBuilderEmbeddableFactory({ start }); embeddable.registerEmbeddableFactory(VISBUILDER_EMBEDDABLE, embeddableFactory); // Register the plugin as an alias to create visualization diff --git a/src/plugins/vis_builder/public/saved_visualizations/_saved_vis.ts b/src/plugins/vis_builder/public/saved_visualizations/_saved_vis.ts index 53ccc7a9d7d0..021af777df17 100644 --- a/src/plugins/vis_builder/public/saved_visualizations/_saved_vis.ts +++ b/src/plugins/vis_builder/public/saved_visualizations/_saved_vis.ts @@ -22,6 +22,7 @@ export function createSavedVisBuilderVisClass(services: SavedObjectOpenSearchDas description: 'text', visualizationState: 'text', styleState: 'text', + uiState: 'text', version: 'integer', }; @@ -44,7 +45,8 @@ export function createSavedVisBuilderVisClass(services: SavedObjectOpenSearchDas description: '', visualizationState: '{}', styleState: '{}', - version: 2, + uiState: '{}', + version: 3, }, }); this.showInRecentlyAccessed = true; diff --git a/src/plugins/vis_builder/public/saved_visualizations/transforms.test.ts b/src/plugins/vis_builder/public/saved_visualizations/transforms.test.ts index efbcfd23f799..68c24dfe4af1 100644 --- a/src/plugins/vis_builder/public/saved_visualizations/transforms.test.ts +++ b/src/plugins/vis_builder/public/saved_visualizations/transforms.test.ts @@ -25,7 +25,7 @@ describe('transforms', () => { savedObject = {} as VisBuilderSavedObject; rootState = { metadata: { editor: { state: 'loading', errors: {} } }, - style: '', + style: {}, visualization: { searchField: '', indexPattern: TEST_INDEX_PATTERN_ID, @@ -34,6 +34,7 @@ describe('transforms', () => { aggConfigParams: [], }, }, + ui: {}, }; indexPattern = getStubIndexPattern( TEST_INDEX_PATTERN_ID, @@ -49,6 +50,7 @@ describe('transforms', () => { expect(savedObject.visualizationState).not.toContain(TEST_INDEX_PATTERN_ID); expect(savedObject.styleState).toEqual(JSON.stringify(rootState.style)); + expect(savedObject.uiState).toEqual(JSON.stringify(rootState.ui)); expect(savedObject.searchSourceFields?.index?.id).toEqual(TEST_INDEX_PATTERN_ID); }); @@ -69,6 +71,7 @@ describe('transforms', () => { visualizationState: JSON.stringify({ searchField: '', }), + uiState: '{}', searchSourceFields: { index: 'test-index', }, @@ -80,6 +83,7 @@ describe('transforms', () => { expect(state).toMatchInlineSnapshot(` Object { "style": Object {}, + "ui": Object {}, "visualization": Object { "indexPattern": "test-index", "searchField": "", diff --git a/src/plugins/vis_builder/public/saved_visualizations/transforms.ts b/src/plugins/vis_builder/public/saved_visualizations/transforms.ts index 0a7a6e529a6b..9f8dd705e3e4 100644 --- a/src/plugins/vis_builder/public/saved_visualizations/transforms.ts +++ b/src/plugins/vis_builder/public/saved_visualizations/transforms.ts @@ -27,6 +27,7 @@ export const saveStateToSavedObject = ( ); obj.styleState = JSON.stringify(state.style); obj.searchSourceFields = { index: indexPattern }; + obj.uiState = JSON.stringify(state.ui); return obj; }; @@ -40,7 +41,8 @@ export const getStateFromSavedObject = ( obj: VisBuilderSavedObjectAttributes ): VisBuilderSavedVis => { const { id, title, description } = obj; - const styleState = JSON.parse(obj.styleState || ''); + const styleState = JSON.parse(obj.styleState || '{}'); + const uiState = JSON.parse(obj.uiState || '{}'); const vizStateWithoutIndex = JSON.parse(obj.visualizationState || ''); const visualizationState: VisualizationState = { searchField: '', @@ -48,7 +50,7 @@ export const getStateFromSavedObject = ( indexPattern: obj.searchSourceFields?.index, }; - const validateResult = validateVisBuilderState({ styleState, visualizationState }); + const validateResult = validateVisBuilderState({ styleState, visualizationState, uiState }); if (!validateResult.valid) { throw new InvalidJSONProperty( @@ -75,6 +77,7 @@ export const getStateFromSavedObject = ( state: { visualization: visualizationState, style: styleState, + ui: uiState, }, }; }; diff --git a/src/plugins/vis_builder/public/types.ts b/src/plugins/vis_builder/public/types.ts index 5221a1c513ec..1ba8843e016a 100644 --- a/src/plugins/vis_builder/public/types.ts +++ b/src/plugins/vis_builder/public/types.ts @@ -62,6 +62,7 @@ export interface ISavedVis { description?: string; visualizationState?: string; styleState?: string; + uiState?: string; version?: number; } diff --git a/src/plugins/vis_builder/server/saved_objects/vis_builder_app.ts b/src/plugins/vis_builder/server/saved_objects/vis_builder_app.ts index 15d785b3b451..029557010bee 100644 --- a/src/plugins/vis_builder/server/saved_objects/vis_builder_app.ts +++ b/src/plugins/vis_builder/server/saved_objects/vis_builder_app.ts @@ -46,6 +46,10 @@ export const visBuilderSavedObjectType: SavedObjectsType = { type: 'text', index: false, }, + uiState: { + type: 'text', + index: false, + }, version: { type: 'integer' }, // Need to add a kibanaSavedObjectMeta attribute here to follow the current saved object flow // When we save a saved object, the saved object plugin will extract the search source into two parts From 9d270234378dc06c51939b8d115beb6082c46e3b Mon Sep 17 00:00:00 2001 From: Peter Fitzgibbons Date: Tue, 18 Apr 2023 03:06:59 -0700 Subject: [PATCH 07/35] [FIX] Dashboard list integrated delete (#3796) * [FIx] Dashboard-List Integrated Delete Signed-off-by: Peter Fitzgibbons * Add toast notification on deletion error Signed-off-by: Josh Romero * add toast notification dependency Signed-off-by: Josh Romero --------- Signed-off-by: Peter Fitzgibbons Signed-off-by: Josh Romero Co-authored-by: Peter Fitzgibbons Co-authored-by: Josh Romero --- .../dashboard/public/application/application.ts | 2 ++ .../dashboard/public/application/legacy_app.js | 14 ++++++++++++-- src/plugins/dashboard/public/plugin.tsx | 2 +- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/plugins/dashboard/public/application/application.ts b/src/plugins/dashboard/public/application/application.ts index f29b1bd9180a..35899cddf69d 100644 --- a/src/plugins/dashboard/public/application/application.ts +++ b/src/plugins/dashboard/public/application/application.ts @@ -37,6 +37,7 @@ import 'angular-sanitize'; import { i18nDirective, i18nFilter, I18nProvider } from '@osd/i18n/angular'; import { ChromeStart, + ToastsStart, IUiSettingsClient, CoreStart, SavedObjectsClientContract, @@ -93,6 +94,7 @@ export interface RenderDeps { setHeaderActionMenu: AppMountParameters['setHeaderActionMenu']; savedObjects: SavedObjectsStart; restorePreviousUrl: () => void; + toastNotifications: ToastsStart; } let angularModuleInstance: IModule | null = null; diff --git a/src/plugins/dashboard/public/application/legacy_app.js b/src/plugins/dashboard/public/application/legacy_app.js index 027f65591bdb..3ed961e54049 100644 --- a/src/plugins/dashboard/public/application/legacy_app.js +++ b/src/plugins/dashboard/public/application/legacy_app.js @@ -114,7 +114,6 @@ export function initDashboardApp(app, deps) { deps.core.chrome.docTitle.change( i18n.translate('dashboard.dashboardPageTitle', { defaultMessage: 'Dashboards' }) ); - const service = deps.savedDashboards; const dashboardConfig = deps.dashboardConfig; // syncs `_g` portion of url with query services @@ -171,7 +170,18 @@ export function initDashboardApp(app, deps) { history.push(deps.addBasePath(viewUrl)); }; $scope.delete = (dashboards) => { - return service.delete(dashboards.map((d) => d.id)); + const ids = dashboards.map((d) => ({ id: d.id, appId: d.appId })); + return Promise.all( + ids.map(({ id, appId }) => { + return deps.savedObjectsClient.delete(appId, id); + }) + ).catch((error) => { + deps.toastNotifications.addError(error, { + title: i18n.translate('dashboard.dashboardListingDeleteErrorTitle', { + defaultMessage: 'Error deleting dashboard', + }), + }); + }); }; $scope.hideWriteControls = dashboardConfig.getHideWriteControls(); $scope.initialFilter = parse(history.location.search).filter || EMPTY_FILTER; diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index 56388cfc64e5..87f934925899 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -344,7 +344,7 @@ export class DashboardPlugin registerDashboardProvider({ savedObjectsType: 'dashboard', savedObjectsName: 'Dashboard', - appId: 'dashboards', + appId: 'dashboard', viewUrlPathFn: (obj) => `#/view/${obj.id}`, editUrlPathFn: (obj) => `/view/${obj.id}?_a=(viewMode:edit)`, createUrl: core.http.basePath.prepend('/app/dashboards#/create'), From 712a42fee9f8606b5e3b4fceb3164f948a87dce0 Mon Sep 17 00:00:00 2001 From: Ashwin P Chandran Date: Wed, 19 Apr 2023 00:40:41 -0700 Subject: [PATCH 08/35] Bump OUI to 1.1.1 (#3884) * chore(OUI): bump OUI to 1.1.0 Signed-off-by: Josh Romero * bootstrap and update yarn.lock Signed-off-by: Josh Romero * add changelog Signed-off-by: Josh Romero * update snapshot Signed-off-by: Josh Romero * updates to OUI to 1.1.1 Signed-off-by: Ashwin P Chandran * Update CHANGELOG.md Signed-off-by: Ashwin P Chandran * test cache fixes Signed-off-by: Ashwin P Chandran * test cache fixes Signed-off-by: Ashwin P Chandran * reenable ci tests Signed-off-by: Ashwin P Chandran --------- Signed-off-by: Josh Romero Signed-off-by: Ashwin P Chandran Co-authored-by: Josh Romero --- .github/workflows/build_and_test_workflow.yml | 11 ----------- CHANGELOG.md | 1 + package.json | 2 +- packages/osd-ui-framework/package.json | 2 +- packages/osd-ui-shared-deps/package.json | 2 +- .../__snapshots__/update_password_modal.test.tsx.snap | 4 ++-- .../plugins/osd_tp_run_pipeline/package.json | 2 +- .../plugins/osd_sample_panel_action/package.json | 2 +- .../plugins/osd_tp_custom_visualizations/package.json | 2 +- yarn.lock | 8 ++++---- 10 files changed, 13 insertions(+), 23 deletions(-) diff --git a/.github/workflows/build_and_test_workflow.yml b/.github/workflows/build_and_test_workflow.yml index 5daef79cc8b9..fd4cdbbefa78 100644 --- a/.github/workflows/build_and_test_workflow.yml +++ b/.github/workflows/build_and_test_workflow.yml @@ -343,17 +343,6 @@ jobs: npm i -g yarn@1.22.10 yarn config set network-timeout 1000000 -g - - name: Configure Yarn Cache - run: echo "YARN_CACHE_LOCATION=$(yarn cache dir)" >> $GITHUB_ENV - - - name: Initialize Yarn Cache - uses: actions/cache@v3 - with: - path: ${{ env.YARN_CACHE_LOCATION }} - key: yarn-${{ hashFiles('**/yarn.lock') }} - restore-keys: | - yarn- - - name: Get package version run: | echo "VERSION=$(yarn --silent pkg-version)" >> $GITHUB_ENV diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f49d62513d6..fb7c3fd87322 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -181,6 +181,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - Remove the unused `renovate.json5` file ([3489](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3489)) - Allow selecting the Node.js binary using `NODE_HOME` and `OSD_NODE_HOME` ([3508](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3508)) - Bump `styled-components` from 5.3.5 to 5.3.9 ([#3678](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3678)) +- Bump `oui` from `1.0.0` to `1.1.1` ([#3884](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3884)) ### 🪛 Refactoring diff --git a/package.json b/package.json index 998a5335fb40..b9eb244f7756 100644 --- a/package.json +++ b/package.json @@ -119,7 +119,7 @@ "dependencies": { "@aws-crypto/client-node": "^3.1.1", "@elastic/datemath": "5.0.3", - "@elastic/eui": "npm:@opensearch-project/oui@1.0.0", + "@elastic/eui": "npm:@opensearch-project/oui@1.1.1", "@elastic/good": "^9.0.1-kibana3", "@elastic/numeral": "^2.5.0", "@elastic/request-crypto": "2.0.0", diff --git a/packages/osd-ui-framework/package.json b/packages/osd-ui-framework/package.json index a89cc82a9d42..3ebc09f4a8ad 100644 --- a/packages/osd-ui-framework/package.json +++ b/packages/osd-ui-framework/package.json @@ -23,7 +23,7 @@ "enzyme-adapter-react-16": "^1.9.1" }, "devDependencies": { - "@elastic/eui": "npm:@opensearch-project/oui@1.0.0", + "@elastic/eui": "npm:@opensearch-project/oui@1.1.1", "@osd/babel-preset": "1.0.0", "@osd/optimizer": "1.0.0", "grunt": "^1.5.2", diff --git a/packages/osd-ui-shared-deps/package.json b/packages/osd-ui-shared-deps/package.json index c4d8103ecc00..61f7008fbc0f 100644 --- a/packages/osd-ui-shared-deps/package.json +++ b/packages/osd-ui-shared-deps/package.json @@ -10,7 +10,7 @@ }, "dependencies": { "@elastic/charts": "31.1.0", - "@elastic/eui": "npm:@opensearch-project/oui@1.0.0", + "@elastic/eui": "npm:@opensearch-project/oui@1.1.1", "@elastic/numeral": "^2.5.0", "@osd/i18n": "1.0.0", "@osd/monaco": "1.0.0", diff --git a/src/plugins/data_source_management/public/components/edit_data_source/components/update_password_modal/__snapshots__/update_password_modal.test.tsx.snap b/src/plugins/data_source_management/public/components/edit_data_source/components/update_password_modal/__snapshots__/update_password_modal.test.tsx.snap index 7c5de9b6ba27..21ec04bd300e 100644 --- a/src/plugins/data_source_management/public/components/edit_data_source/components/update_password_modal/__snapshots__/update_password_modal.test.tsx.snap +++ b/src/plugins/data_source_management/public/components/edit_data_source/components/update_password_modal/__snapshots__/update_password_modal.test.tsx.snap @@ -569,7 +569,7 @@ exports[`Datasource Management: Update Stored Password Modal should render norma onChange={[Function]} onFocus={[Function]} placeholder="Updated password" - spellCheck={false} + spellCheck="false" type="password" value="" /> @@ -733,7 +733,7 @@ exports[`Datasource Management: Update Stored Password Modal should render norma onChange={[Function]} onFocus={[Function]} placeholder="Confirm Updated password" - spellCheck={false} + spellCheck="false" type="password" value="" /> diff --git a/test/interpreter_functional/plugins/osd_tp_run_pipeline/package.json b/test/interpreter_functional/plugins/osd_tp_run_pipeline/package.json index a0ff828390c6..5f308268566c 100644 --- a/test/interpreter_functional/plugins/osd_tp_run_pipeline/package.json +++ b/test/interpreter_functional/plugins/osd_tp_run_pipeline/package.json @@ -12,7 +12,7 @@ "build": "../../../../scripts/use_node ../../../../scripts/remove.js './target' && tsc" }, "devDependencies": { - "@elastic/eui": "npm:@opensearch-project/oui@1.0.0", + "@elastic/eui": "npm:@opensearch-project/oui@1.1.1", "@osd/plugin-helpers": "1.0.0", "react": "^16.14.0", "react-dom": "^16.12.0", diff --git a/test/plugin_functional/plugins/osd_sample_panel_action/package.json b/test/plugin_functional/plugins/osd_sample_panel_action/package.json index 5c5237717ac3..35f7e84ae0e2 100644 --- a/test/plugin_functional/plugins/osd_sample_panel_action/package.json +++ b/test/plugin_functional/plugins/osd_sample_panel_action/package.json @@ -12,7 +12,7 @@ "build": "../../../../scripts/use_node ../../../../scripts/remove.js './target' && tsc" }, "devDependencies": { - "@elastic/eui": "npm:@opensearch-project/oui@1.0.0", + "@elastic/eui": "npm:@opensearch-project/oui@1.1.1", "react": "^16.14.0", "typescript": "4.0.2" } diff --git a/test/plugin_functional/plugins/osd_tp_custom_visualizations/package.json b/test/plugin_functional/plugins/osd_tp_custom_visualizations/package.json index 23be4ebf14d2..13f7b1695a0d 100644 --- a/test/plugin_functional/plugins/osd_tp_custom_visualizations/package.json +++ b/test/plugin_functional/plugins/osd_tp_custom_visualizations/package.json @@ -12,7 +12,7 @@ "build": "../../../../scripts/use_node ../../../../scripts/remove.js './target' && tsc" }, "devDependencies": { - "@elastic/eui": "npm:@opensearch-project/oui@1.0.0", + "@elastic/eui": "npm:@opensearch-project/oui@1.1.1", "@osd/plugin-helpers": "1.0.0", "react": "^16.14.0", "typescript": "4.0.2" diff --git a/yarn.lock b/yarn.lock index 455b3475298f..33ca05a2f2c1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1273,10 +1273,10 @@ resolved "https://registry.yarnpkg.com/@elastic/eslint-plugin-eui/-/eslint-plugin-eui-0.0.2.tgz#56b9ef03984a05cc213772ae3713ea8ef47b0314" integrity sha512-IoxURM5zraoQ7C8f+mJb9HYSENiZGgRVcG4tLQxE61yHNNRDXtGDWTZh8N1KIHcsqN1CEPETjuzBXkJYF/fDiQ== -"@elastic/eui@npm:@opensearch-project/oui@1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@opensearch-project/oui/-/oui-1.0.0.tgz#bf5e115c8d0f230415b07cc6acfb149ab081c5de" - integrity sha512-J709UQc7+il4y3aiqpHzeLOJAQhN6xEGLLHq4sUL3WHTsP37acONINXCpRNMa3FxZ+ChOd2ABmY+Ajs+fIgmug== +"@elastic/eui@npm:@opensearch-project/oui@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@opensearch-project/oui/-/oui-1.1.1.tgz#4a9318c2954659cdd8d83263ff2dc22a77cbd779" + integrity sha512-RBXbsZh6mjJKJqB/hSI2loenyM2rvdq9id29P/ZYlZGKKy0/tSreIOGcegSYMtNFmG029D20xVkhRmdn7cxK1A== dependencies: "@types/chroma-js" "^2.0.0" "@types/lodash" "^4.14.160" From e9dc8902c1bef80690a3d7a1ef3bbeaa861f5b3a Mon Sep 17 00:00:00 2001 From: Josh Romero Date: Wed, 19 Apr 2023 11:54:57 -0700 Subject: [PATCH 09/35] Add 2.7.0 release notes (#3889) Signed-off-by: Josh Romero --- ...ensearch-dashboards.release-notes-2.7.0.md | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 release-notes/opensearch-dashboards.release-notes-2.7.0.md diff --git a/release-notes/opensearch-dashboards.release-notes-2.7.0.md b/release-notes/opensearch-dashboards.release-notes-2.7.0.md new file mode 100644 index 000000000000..6f4c4fb73df2 --- /dev/null +++ b/release-notes/opensearch-dashboards.release-notes-2.7.0.md @@ -0,0 +1,88 @@ +## Version 2.7.0 Release Notes + +### Deprecations + +### 🛡 Security + +- [CVE-2023-26486] Bump vega from `5.22.1` to `5.23.0` ([#3533](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3533)) +- [CVE-2023-26487] Bump vega from `5.22.1` to `5.23.0` ([#3533](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3533)) +- [CVE-2023-0842] Bump xml2js from `0.4.23` to `0.5.0` ([#3842](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3842)) + +### 📈 Features/Enhancements + +- Add satisfaction survey link to help menu ([#3676](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3676)) +- Add `osd-xsrf` header to all requests that incorrectly used `node-version` to satisfy XSRF protection ([#3643](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3643)) +- [Dashboard] Add Dashboards-list integrations for Plugins ([#3090](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3090) ) +- [Data] Add geo shape filter field ([#3605](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3605)) +- [Doc Links] Add downgrade logic for branch in DocLinkService ([#3483](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3483)) +- [Monaco editor] Add json worker support ([#3424](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3424)) +- [Multiple DataSource] Allow create and distinguish index pattern with same name but from different datasources ([#3604](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3604)) +- [Multiple DataSource] Integrate multiple datasource with dev tool console ([#3754](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3754)) +- [Notifications] Add id to toast api for deduplication ([#3752](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3752)) +- [UI] Add support for comma delimiters in the global filter bar ([#3686](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3686)) +- [UI] Indicate that IE is no longer supported ([#3641](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3641)) +- [Vega] Add Filter custom label for opensearchDashboardsAddFilter ([#3640](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3640)) +- [VisBuilder] Add metric to metric, bucket to bucket aggregation persistence ([#3495](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3495)) +- [VisBuilder] Add UI actions handler ([#3732](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3732)) +- [VisBuilder] Add persistence to visualizations inner state ([#3751](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3751)) + +### 🐛 Bug Fixes + +- Clean up and rebuild `@osd/pm` ([#3570](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3570)) +- Omit adding the `osd-version` header when the Fetch request is to an external origin ([#3643](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3643)) +- [Console] Fix/update documentation links in Dev Tools console ([#3724](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3724)) +- [Console] Fix dev tool console autocomplete not loading issue ([#3775](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3775)) +- [Console] Fix dev tool console run command with query parameter error ([#3813](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3813)) +- [Table Visualization] Fix table rendering empty unused space ([#3797](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3797)) +- [Table Visualization] Fix data table not adjusting height on the initial load ([#3816](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3816)) +- [Timeline] Fix y-axis label color in dark mode ([#3698](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3698)) +- [TSVB] Fix undefined serial diff aggregation documentation link ([#3503](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3503)) +- [UI] Add clarifying tooltips to header navigation icons ([#3573](https://github.com/opensearch-project/OpenSearch-Dashboards/issues/3573)) +- [VisBuilder] Fix multiple warnings thrown on page load ([#3732](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3732)) +- [VisBuilder] Fix Firefox legend selection issue ([#3732](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3732)) +- [VisBuilder] Fix type errors ([#3732](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3732)) +- [VisBuilder] Fix indexpattern selection in filter bar ([#3751](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3751)) + +### 🚞 Infrastructure + +- Use mirrors to download Node.js binaries to escape sporadic 404 errors ([#3619](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3619)) +- [CI] Update NOTICE file, add validation to GitHub CI ([#3051](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3051)) +- [CI] Reduce redundancy by using matrix strategy on Windows and Linux workflows ([#3514](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3514)) +- [Darwin] Add support for Darwin for running OpenSearch snapshots with `yarn opensearch snapshot` ([#3537](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3537)) + +### 📝 Documentation + +- Correct copyright date range of NOTICE file and notice generator ([#3308](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3308)) +- Simplify the in-code instructions for upgrading `re2` ([#3328](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3328)) +- [Doc] Improve DEVELOPER_GUIDE to make first time setup quicker and easier ([#3421](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3421)) +- [Doc] Update DEVELOPER_GUIDE with added manual bootstrap timeout solution and max virtual memory error solution with docker ([#3764](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3764)) +- [Doc] Add second command to install yarn step in DEVELOPER_GUIDE ([#3633](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3633)) +- [Doc] Add docker dev set up instruction ([#3444](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3444)) +- [Doc] Add docker files and instructions for debugging Selenium functional tests ([#3747](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3747)) +- [Doc] Update SECURITY with instructions for nested dependencies and backporting ([#3497](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3497)) +- [TSVB] Fix typo in TSVB README ([#3518](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3518)) +- [UI Actions] Improve UI actions explorer ([#3614](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3614)) + +### 🛠 Maintenance + +- Relax the Node.js requirement to `^14.20.1` ([#3463](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3463)) +- Bump the version of Node.js installed by `nvm` to `14.21.3` ([#3463](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3463)) +- Allow selecting the Node.js binary using `NODE_HOME` and `OSD_NODE_HOME` ([#3508](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3508)) +- Remove the unused `renovate.json5` file ([#3489](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3489)) +- Bump `styled-components` from `5.3.5` to `5.3.9` ([#3678](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3678)) +- [Timeline] Update default expressions from `.es(*)` to `.opensearch(*)`. ([#2720](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2720)) + +### 🪛 Refactoring + +- Remove automatic addition of `osd-version` header to requests outside of OpenSearch Dashboards ([#3643](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3643)) +- [Console] Replace jQuery usage in console plugin with native methods ([#3733](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3733)) +- [Doc Links] Clean up docs_link_service organization so that strings are in the right categories. ([#3685](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3685)) +- [I18n] Fix Listr type errors and error handlers ([#3629](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3629)) +- [Multiple DataSource] Refactor dev tool console to use opensearch-js client to send requests ([#3544](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3544)) +- [Multiple DataSource] Present the authentication type choices in a drop-down ([#3693](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3693)) +- [Table Visualization] Move format table, consolidate types and add unit tests ([#3397](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3397)) + +### 🔩 Tests + +- Update caniuse to `1.0.30001460` to fix failed integration tests ([#3538](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3538)) +- [Tests] Fix unit tests for `get_keystore` ([#3854](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3854)) From 86d42bc1cd68120fd389d7c39b705fcde4d09770 Mon Sep 17 00:00:00 2001 From: Josh Romero Date: Thu, 20 Apr 2023 11:06:50 -0700 Subject: [PATCH 10/35] Chore (release notes): polish 2.7.0 notes (#3896) Add welcome/thank you section for first-time contributors Fix link that pointed to issue instead of PR Add entry for OUI bump Signed-off-by: Josh Romero --- release-notes/opensearch-dashboards.release-notes-2.7.0.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/release-notes/opensearch-dashboards.release-notes-2.7.0.md b/release-notes/opensearch-dashboards.release-notes-2.7.0.md index 6f4c4fb73df2..3ad55fe4d807 100644 --- a/release-notes/opensearch-dashboards.release-notes-2.7.0.md +++ b/release-notes/opensearch-dashboards.release-notes-2.7.0.md @@ -37,7 +37,7 @@ - [Table Visualization] Fix data table not adjusting height on the initial load ([#3816](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3816)) - [Timeline] Fix y-axis label color in dark mode ([#3698](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3698)) - [TSVB] Fix undefined serial diff aggregation documentation link ([#3503](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3503)) -- [UI] Add clarifying tooltips to header navigation icons ([#3573](https://github.com/opensearch-project/OpenSearch-Dashboards/issues/3573)) +- [UI] Add clarifying tooltips to header navigation icons ([#3626](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3626)) - [VisBuilder] Fix multiple warnings thrown on page load ([#3732](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3732)) - [VisBuilder] Fix Firefox legend selection issue ([#3732](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3732)) - [VisBuilder] Fix type errors ([#3732](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3732)) @@ -70,6 +70,7 @@ - Allow selecting the Node.js binary using `NODE_HOME` and `OSD_NODE_HOME` ([#3508](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3508)) - Remove the unused `renovate.json5` file ([#3489](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3489)) - Bump `styled-components` from `5.3.5` to `5.3.9` ([#3678](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3678)) +- Bump `oui` from `1.0.0` to `1.1.1` ([3884](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3884)) - [Timeline] Update default expressions from `.es(*)` to `.opensearch(*)`. ([#2720](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2720)) ### 🪛 Refactoring @@ -86,3 +87,7 @@ - Update caniuse to `1.0.30001460` to fix failed integration tests ([#3538](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3538)) - [Tests] Fix unit tests for `get_keystore` ([#3854](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3854)) + +## 🎉 Welcome + +Thank you to all the first-time contributors who made this release possible: @pjfitzgibbons, @djindjic, @jlabatut, @sayuree, @Nicksqain, @kappassov, @aswath86, @frost017, @curq, @Aigerim-ai, @andreymyssak, @Hailong-am! From e74ab2d4f5f5bcee4aebb5d04899aaa48b565513 Mon Sep 17 00:00:00 2001 From: "opensearch-trigger-bot[bot]" <98922864+opensearch-trigger-bot[bot]@users.noreply.github.com> Date: Sat, 22 Apr 2023 04:35:06 -0700 Subject: [PATCH 11/35] Fix header icon (#3910) (#3915) * fixes header change * Update src/core/public/chrome/ui/header/header_help_menu.tsx * fixes snapshots --------- (cherry picked from commit 3cca08851cb1527705c60bbb4202d50174f034af) Signed-off-by: Ashwin P Chandran Signed-off-by: github-actions[bot] Co-authored-by: github-actions[bot] Co-authored-by: Josh Romero --- .../header/__snapshots__/header.test.tsx.snap | 40 ++----------------- .../header_help_menu.test.tsx.snap | 40 ++----------------- .../chrome/ui/header/header_help_menu.tsx | 3 +- 3 files changed, 9 insertions(+), 74 deletions(-) diff --git a/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap index 6486e207ed37..4c2beb329a98 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap @@ -5415,14 +5415,10 @@ exports[`Header handles visibility and lock changes 1`] = ` aria-label="Help menu" onClick={[Function]} > - } @@ -5474,9 +5470,6 @@ exports[`Header handles visibility and lock changes 1`] = ` > - @@ -5519,21 +5512,12 @@ exports[`Header handles visibility and lock changes 1`] = ` > - - - @@ -10674,14 +10658,10 @@ exports[`Header renders condensed header 1`] = ` aria-label="Help menu" onClick={[Function]} > - } @@ -10733,9 +10713,6 @@ exports[`Header renders condensed header 1`] = ` > - @@ -10778,21 +10755,12 @@ exports[`Header renders condensed header 1`] = ` > - - - diff --git a/src/core/public/chrome/ui/header/__snapshots__/header_help_menu.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/header_help_menu.test.tsx.snap index 5406fbf6abaa..036e2b4ee0ce 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/header_help_menu.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/header_help_menu.test.tsx.snap @@ -1622,14 +1622,10 @@ exports[`Header help menu hides survey link 1`] = ` aria-label="Help menu" onClick={[Function]} > - } @@ -1681,9 +1677,6 @@ exports[`Header help menu hides survey link 1`] = ` > - @@ -1726,21 +1719,12 @@ exports[`Header help menu hides survey link 1`] = ` > - - - @@ -3798,14 +3782,10 @@ exports[`Header help menu renders survey link 1`] = ` aria-label="Help menu" onClick={[Function]} > - } @@ -3857,9 +3837,6 @@ exports[`Header help menu renders survey link 1`] = ` > - @@ -3902,21 +3879,12 @@ exports[`Header help menu renders survey link 1`] = ` > - - - diff --git a/src/core/public/chrome/ui/header/header_help_menu.tsx b/src/core/public/chrome/ui/header/header_help_menu.tsx index f0be769937f5..0eba4c0c2673 100644 --- a/src/core/public/chrome/ui/header/header_help_menu.tsx +++ b/src/core/public/chrome/ui/header/header_help_menu.tsx @@ -330,9 +330,8 @@ class HeaderHelpMenuUI extends Component { })} onClick={this.onMenuButtonClick} > - Date: Mon, 24 Apr 2023 14:35:01 -0700 Subject: [PATCH 12/35] Add server side private IP blocking for data source endpoints validation (#3912) Signed-off-by: Kristen Tian --- .lycheeexclude | 3 + CHANGELOG.md | 1 + config/opensearch_dashboards.yml | 27 ++++++++- src/plugins/data_source/config.ts | 1 + src/plugins/data_source/server/plugin.ts | 3 +- ...ata_source_saved_objects_client_wrapper.ts | 22 ++++--- .../server/util/endpoint_validator.test.js | 34 +++++++++++ .../server/util/endpoint_validator.ts | 59 +++++++++++++++++++ 8 files changed, 136 insertions(+), 14 deletions(-) create mode 100644 src/plugins/data_source/server/util/endpoint_validator.test.js create mode 100644 src/plugins/data_source/server/util/endpoint_validator.ts diff --git a/.lycheeexclude b/.lycheeexclude index 07317835aabf..252db4e82787 100644 --- a/.lycheeexclude +++ b/.lycheeexclude @@ -88,6 +88,7 @@ https://opensearch.org/redirect http://www.opensearch.org/painlessDocs https://www.hostedgraphite.com/ https://connectionurl.com +http://169.254.169.254/latest/meta-data/ # External urls https://www.zeek.org/ @@ -117,3 +118,5 @@ http://www.creedthoughts.gov https://media-for-the-masses.theacademyofperformingartsandscience.org/ https://yarnpkg.com/latest.msi https://forum.opensearch.org/ +https://facebook.github.io/jest/ +https://facebook.github.io/jest/docs/cli.html diff --git a/CHANGELOG.md b/CHANGELOG.md index fb7c3fd87322..c3402ef49b71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [CVE-2023-25653] Bump node-jose to 2.2.0 ([#3445](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3445)) - [CVE-2023-26486][cve-2023-26487] Bump vega from 5.22.1 to 5.23.0 ([#3533](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3533)) - [CVE-2023-0842] Bump xml2js from 0.4.23 to 0.5.0 ([#3842](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3842)) +- [Multi DataSource] Add private IP blocking validation on server side([#3912](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3912)) ### 📈 Features/Enhancements diff --git a/config/opensearch_dashboards.yml b/config/opensearch_dashboards.yml index 38377296bd20..1d751769f701 100644 --- a/config/opensearch_dashboards.yml +++ b/config/opensearch_dashboards.yml @@ -238,5 +238,30 @@ #data_source.encryption.wrappingKeyNamespace: 'changeme' #data_source.encryption.wrappingKey: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] +#data_source.endpointDeniedIPs: [ +# '127.0.0.0/8', +# '::1/128', +# '169.254.0.0/16', +# 'fe80::/10', +# '10.0.0.0/8', +# '172.16.0.0/12', +# '192.168.0.0/16', +# 'fc00::/7', +# '0.0.0.0/8', +# '100.64.0.0/10', +# '192.0.0.0/24', +# '192.0.2.0/24', +# '198.18.0.0/15', +# '192.88.99.0/24', +# '198.51.100.0/24', +# '203.0.113.0/24', +# '224.0.0.0/4', +# '240.0.0.0/4', +# '255.255.255.255/32', +# '::/128', +# '2001:db8::/32', +# 'ff00::/8', +# ] + # Set the value of this setting to false to hide the help menu link to the OpenSearch Dashboards user survey -# opensearchDashboards.survey.url: "https://survey.opensearch.org" \ No newline at end of file +# opensearchDashboards.survey.url: "https://survey.opensearch.org" diff --git a/src/plugins/data_source/config.ts b/src/plugins/data_source/config.ts index 1fc4e00c3e23..09ce35978921 100644 --- a/src/plugins/data_source/config.ts +++ b/src/plugins/data_source/config.ts @@ -37,6 +37,7 @@ export const configSchema = schema.object({ enabled: schema.boolean({ defaultValue: false }), appender: fileAppenderSchema, }), + endpointDeniedIPs: schema.maybe(schema.arrayOf(schema.string())), }); export type DataSourcePluginConfigType = TypeOf; diff --git a/src/plugins/data_source/server/plugin.ts b/src/plugins/data_source/server/plugin.ts index e038a0f7685e..0f3c47be4b4c 100644 --- a/src/plugins/data_source/server/plugin.ts +++ b/src/plugins/data_source/server/plugin.ts @@ -58,7 +58,8 @@ export class DataSourcePlugin implements Plugin(attributes: T) { this.validateAttributes(attributes); @@ -254,8 +250,10 @@ export class DataSourceSavedObjectsClientWrapper { ); } - if (!this.isValidUrl(endpoint)) { - throw SavedObjectsErrorHelpers.createBadRequestError('"endpoint" attribute is not valid'); + if (!isValidURL(endpoint, this.endpointBlockedIps)) { + throw SavedObjectsErrorHelpers.createBadRequestError( + '"endpoint" attribute is not valid or allowed' + ); } if (!auth) { diff --git a/src/plugins/data_source/server/util/endpoint_validator.test.js b/src/plugins/data_source/server/util/endpoint_validator.test.js new file mode 100644 index 000000000000..618bf52d4d95 --- /dev/null +++ b/src/plugins/data_source/server/util/endpoint_validator.test.js @@ -0,0 +1,34 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as validator from './endpoint_validator'; + +describe('endpoint_validator', function () { + it('Url1 that should be blocked should return false', function () { + expect(validator.isValidURL('http://127.0.0.1', ['127.0.0.0/8'])).toEqual(false); + }); + + it('Url2 that is invalid should return false', function () { + expect(validator.isValidURL('www.test.com', [])).toEqual(false); + }); + + it('Url3 that is invalid should return false', function () { + expect(validator.isValidURL('ftp://www.test.com', [])).toEqual(false); + }); + + it('Url4 that should be blocked should return false', function () { + expect( + validator.isValidURL('http://169.254.169.254/latest/meta-data/', ['169.254.0.0/16']) + ).toEqual(false); + }); + + it('Url5 that should not be blocked should return true', function () { + expect(validator.isValidURL('https://www.opensearch.org', ['127.0.0.0/8'])).toEqual(true); + }); + + it('Url6 that should not be blocked should return true when null IPs', function () { + expect(validator.isValidURL('https://www.opensearch.org')).toEqual(true); + }); +}); diff --git a/src/plugins/data_source/server/util/endpoint_validator.ts b/src/plugins/data_source/server/util/endpoint_validator.ts new file mode 100644 index 000000000000..1c032037d2f5 --- /dev/null +++ b/src/plugins/data_source/server/util/endpoint_validator.ts @@ -0,0 +1,59 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import dns from 'dns-sync'; +import IPCIDR from 'ip-cidr'; + +export function isValidURL(endpoint: string, deniedIPs?: string[]) { + // Check the format of URL, URL has be in the format as + // scheme://server/path/resource otherwise an TypeError + // would be thrown. + let url; + try { + url = new URL(endpoint); + } catch (err) { + return false; + } + + if (!(Boolean(url) && (url.protocol === 'http:' || url.protocol === 'https:'))) { + return false; + } + + const ip = getIpAddress(url); + if (!ip) { + return false; + } + + // IP CIDR check if a specific IP address fall in the + // range of an IP address block + for (const deniedIP of deniedIPs ?? []) { + const cidr = new IPCIDR(deniedIP); + if (cidr.contains(ip)) { + return false; + } + } + return true; +} + +/** + * Resolve hostname to IP address + * @param {object} urlObject + * @returns {string} configuredIP + * or null if it cannot be resolve + * According to RFC, all IPv6 IP address needs to be in [] + * such as [::1]. + * So if we detect a IPv6 address, we remove brackets. + */ +function getIpAddress(urlObject: URL) { + const hostname = urlObject.hostname; + const configuredIP = dns.resolve(hostname); + if (configuredIP) { + return configuredIP; + } + if (hostname.startsWith('[') && hostname.endsWith(']')) { + return hostname.substr(1).slice(0, -1); + } + return null; +} From a947240ea2bc28341c4d950e6c2badfc7ca365f9 Mon Sep 17 00:00:00 2001 From: Josh Romero Date: Mon, 24 Apr 2023 15:46:54 -0700 Subject: [PATCH 13/35] Docs (Jest): Update jest documentation links (#3931) Signed-off-by: Josh Romero --- .../README.md | 6 +++--- scripts/jest.js | 2 +- scripts/jest_integration.js | 2 +- src/dev/jest/junit_reporter.js | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/opensearch-eslint-config-opensearch-dashboards/README.md b/packages/opensearch-eslint-config-opensearch-dashboards/README.md index 4fb3f13c8f29..a2f60c437e57 100644 --- a/packages/opensearch-eslint-config-opensearch-dashboards/README.md +++ b/packages/opensearch-eslint-config-opensearch-dashboards/README.md @@ -4,7 +4,7 @@ The eslint config used by the opensearch dashboards team ## Usage -To use this eslint config, just install the peer dependencies and reference it +To use this eslint config, just install the peer dependencies and reference it in your `.eslintrc`: ```javascript @@ -17,8 +17,8 @@ in your `.eslintrc`: ## Optional jest config -If the project uses the [jest test runner](https://facebook.github.io/jest/), -the `@elastic/eslint-config-kibana/jest` config can be extended as well to use +If the project uses the [jest test runner](https://jestjs.io), +the `@elastic/eslint-config-kibana/jest` config can be extended as well to use `eslint-plugin-jest` and add settings specific to it: ```javascript diff --git a/scripts/jest.js b/scripts/jest.js index 53c687e51a84..80f269c72498 100755 --- a/scripts/jest.js +++ b/scripts/jest.js @@ -38,7 +38,7 @@ // // node scripts/jest --coverage // -// See all cli options in https://facebook.github.io/jest/docs/cli.html +// See all cli options in https://jestjs.io/docs/cli var resolve = require('path').resolve; process.argv.push('--config', resolve(__dirname, '../src/dev/jest/config.js')); diff --git a/scripts/jest_integration.js b/scripts/jest_integration.js index f5bc937c4d79..265919f20708 100755 --- a/scripts/jest_integration.js +++ b/scripts/jest_integration.js @@ -38,7 +38,7 @@ // // node scripts/jest_integration --coverage // -// See all cli options in https://facebook.github.io/jest/docs/cli.html +// See all cli options in https://jestjs.io/docs/cli var resolve = require('path').resolve; process.argv.push('--config', resolve(__dirname, '../src/dev/jest/config.integration.js')); diff --git a/src/dev/jest/junit_reporter.js b/src/dev/jest/junit_reporter.js index 65f244042a12..1add8e722a5a 100644 --- a/src/dev/jest/junit_reporter.js +++ b/src/dev/jest/junit_reporter.js @@ -53,7 +53,7 @@ export default class JestJUnitReporter { /** * Called by jest when all tests complete * @param {Object} contexts - * @param {JestResults} results see https://facebook.github.io/jest/docs/en/configuration.html#testresultsprocessor-string + * @param {JestResults} results see https://jestjs.io/docs/configuration/#testresultsprocessor-string * @return {undefined} */ onRunComplete(contexts, results) { From b97a4b5e8ea87db33d3946b9530dc00cf9e7540a Mon Sep 17 00:00:00 2001 From: Josh Romero Date: Mon, 24 Apr 2023 15:48:12 -0700 Subject: [PATCH 14/35] Revert "[CCI] Replace jquery usage in console plugin with native methods (#3733)" (#3929) This reverts commit ffe455615eef10d1719884fd131f6953976583ff. --- CHANGELOG.md | 1 - .../__tests__/input.test.js | 5 +-- .../__tests__/output_tokenization.test.js | 5 +-- .../legacy_core_editor/legacy_core_editor.ts | 31 +++++++++++-------- .../__tests__/integration.test.js | 4 +-- .../__tests__/sense_editor.test.js | 5 +-- .../sense_editor/sense_editor.test.mocks.ts | 8 +++++ .../ace_token_provider/token_provider.test.ts | 6 ++-- src/plugins/console/public/lib/osd/osd.js | 20 ++++++------ 9 files changed, 52 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c3402ef49b71..92f117e3a594 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -192,7 +192,6 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [Console] Replace jQuery.ajax with core.http when calling OSD APIs in console ([#3080](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3080)) - [I18n] Fix Listr type errors and error handlers ([#3629](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3629)) - [Multiple DataSource] Present the authentication type choices in a drop-down ([#3693](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3693)) -- [Console] Replace jQuery usage in console plugin with native methods ([#3733](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3733)) ### 🔩 Tests diff --git a/src/plugins/console/public/application/models/legacy_core_editor/__tests__/input.test.js b/src/plugins/console/public/application/models/legacy_core_editor/__tests__/input.test.js index 653e34aa0073..7accc948e6cd 100644 --- a/src/plugins/console/public/application/models/legacy_core_editor/__tests__/input.test.js +++ b/src/plugins/console/public/application/models/legacy_core_editor/__tests__/input.test.js @@ -31,6 +31,7 @@ import '../legacy_core_editor.test.mocks'; import RowParser from '../../../../lib/row_parser'; import { createTokenIterator } from '../../../factories'; +import $ from 'jquery'; import { create } from '../create'; describe('Input', () => { @@ -45,10 +46,10 @@ describe('Input', () => { coreEditor = create(document.querySelector('#ConAppEditor')); - coreEditor.getContainer().style.display = ''; + $(coreEditor.getContainer()).show(); }); afterEach(() => { - coreEditor.getContainer().style.display = 'none'; + $(coreEditor.getContainer()).hide(); }); describe('.getLineCount', () => { diff --git a/src/plugins/console/public/application/models/legacy_core_editor/__tests__/output_tokenization.test.js b/src/plugins/console/public/application/models/legacy_core_editor/__tests__/output_tokenization.test.js index d143d72e15c2..4973011a2aaa 100644 --- a/src/plugins/console/public/application/models/legacy_core_editor/__tests__/output_tokenization.test.js +++ b/src/plugins/console/public/application/models/legacy_core_editor/__tests__/output_tokenization.test.js @@ -29,6 +29,7 @@ */ import '../legacy_core_editor.test.mocks'; +import $ from 'jquery'; import RowParser from '../../../../lib/row_parser'; import ace from 'brace'; import { createReadOnlyAceEditor } from '../create_readonly'; @@ -38,11 +39,11 @@ const tokenIterator = ace.acequire('ace/token_iterator'); describe('Output Tokenization', () => { beforeEach(() => { output = createReadOnlyAceEditor(document.querySelector('#ConAppOutput')); - output.container.style.display = ''; + $(output.container).show(); }); afterEach(() => { - output.container.style.display = 'none'; + $(output.container).hide(); }); function tokensAsList() { diff --git a/src/plugins/console/public/application/models/legacy_core_editor/legacy_core_editor.ts b/src/plugins/console/public/application/models/legacy_core_editor/legacy_core_editor.ts index 5fe93ca4e094..55ee5fe2a343 100644 --- a/src/plugins/console/public/application/models/legacy_core_editor/legacy_core_editor.ts +++ b/src/plugins/console/public/application/models/legacy_core_editor/legacy_core_editor.ts @@ -30,6 +30,7 @@ import ace from 'brace'; import { Editor as IAceEditor, IEditSession as IAceEditSession } from 'brace'; +import $ from 'jquery'; import { CoreEditor, Position, @@ -53,11 +54,11 @@ const rangeToAceRange = ({ start, end }: Range) => export class LegacyCoreEditor implements CoreEditor { private _aceOnPaste: any; - actions: any; + $actions: any; resize: () => void; constructor(private readonly editor: IAceEditor, actions: HTMLElement) { - this.actions = actions; + this.$actions = $(actions); this.editor.setShowPrintMargin(false); const session = this.editor.getSession(); @@ -273,16 +274,20 @@ export class LegacyCoreEditor implements CoreEditor { private setActionsBar = (value?: any, topOrBottom: 'top' | 'bottom' = 'top') => { if (value === null) { - this.actions.style.visibility = 'hidden'; + this.$actions.css('visibility', 'hidden'); } else { if (topOrBottom === 'top') { - this.actions.style.bottom = 'auto'; - this.actions.style.top = value; - this.actions.style.visibility = 'visible'; + this.$actions.css({ + bottom: 'auto', + top: value, + visibility: 'visible', + }); } else { - this.actions.style.top = 'auto'; - this.actions.style.bottom = value; - this.actions.style.visibility = 'visible'; + this.$actions.css({ + top: 'auto', + bottom: value, + visibility: 'visible', + }); } } }; @@ -313,14 +318,14 @@ export class LegacyCoreEditor implements CoreEditor { } legacyUpdateUI(range: any) { - if (!this.actions) { + if (!this.$actions) { return; } if (range) { // elements are positioned relative to the editor's container // pageY is relative to page, so subtract the offset // from pageY to get the new top value - const offsetFromPage = this.editor.container.offsetTop; + const offsetFromPage = $(this.editor.container).offset()!.top; const startLine = range.start.lineNumber; const startColumn = range.start.column; const firstLine = this.getLineValue(startLine); @@ -340,11 +345,11 @@ export class LegacyCoreEditor implements CoreEditor { let offset = 0; if (isWrapping) { // Try get the line height of the text area in pixels. - const textArea = this.editor.container.querySelector('textArea'); + const textArea = $(this.editor.container.querySelector('textArea')!); const hasRoomOnNextLine = this.getLineValue(startLine).length < maxLineLength; if (textArea && hasRoomOnNextLine) { // Line height + the number of wraps we have on a line. - offset += this.getLineValue(startLine).length * textArea.getBoundingClientRect().height; + offset += this.getLineValue(startLine).length * textArea.height()!; } else { if (startLine > 1) { this.setActionsBar(getScreenCoords(startLine - 1)); diff --git a/src/plugins/console/public/application/models/sense_editor/__tests__/integration.test.js b/src/plugins/console/public/application/models/sense_editor/__tests__/integration.test.js index 88f9acc27e7f..cf6df4d31b06 100644 --- a/src/plugins/console/public/application/models/sense_editor/__tests__/integration.test.js +++ b/src/plugins/console/public/application/models/sense_editor/__tests__/integration.test.js @@ -44,11 +44,11 @@ describe('Integration', () => { '
'; senseEditor = create(document.querySelector('#ConAppEditor')); - senseEditor.getCoreEditor().getContainer().style.display = ''; + $(senseEditor.getCoreEditor().getContainer()).show(); senseEditor.autocomplete._test.removeChangeListener(); }); afterEach(() => { - senseEditor.getCoreEditor().getContainer().style.display = 'none'; + $(senseEditor.getCoreEditor().getContainer()).hide(); senseEditor.autocomplete._test.addChangeListener(); }); diff --git a/src/plugins/console/public/application/models/sense_editor/__tests__/sense_editor.test.js b/src/plugins/console/public/application/models/sense_editor/__tests__/sense_editor.test.js index de67ae1a8908..18d798c28c94 100644 --- a/src/plugins/console/public/application/models/sense_editor/__tests__/sense_editor.test.js +++ b/src/plugins/console/public/application/models/sense_editor/__tests__/sense_editor.test.js @@ -30,6 +30,7 @@ import '../sense_editor.test.mocks'; +import $ from 'jquery'; import _ from 'lodash'; import { create } from '../create'; @@ -50,11 +51,11 @@ describe('Editor', () => {
`; input = create(document.querySelector('#ConAppEditor')); - input.getCoreEditor().getContainer().style.display = ''; + $(input.getCoreEditor().getContainer()).show(); input.autocomplete._test.removeChangeListener(); }); afterEach(function () { - input.getCoreEditor().getContainer().style.display = 'none'; + $(input.getCoreEditor().getContainer()).hide(); input.autocomplete._test.addChangeListener(); }); diff --git a/src/plugins/console/public/application/models/sense_editor/sense_editor.test.mocks.ts b/src/plugins/console/public/application/models/sense_editor/sense_editor.test.mocks.ts index 92e86d60104c..6474fcb0ec9d 100644 --- a/src/plugins/console/public/application/models/sense_editor/sense_editor.test.mocks.ts +++ b/src/plugins/console/public/application/models/sense_editor/sense_editor.test.mocks.ts @@ -31,3 +31,11 @@ /* eslint no-undef: 0 */ import '../legacy_core_editor/legacy_core_editor.test.mocks'; + +import jQuery from 'jquery'; +jest.spyOn(jQuery, 'ajax').mockImplementation( + () => + new Promise(() => { + // never resolve + }) as any +); diff --git a/src/plugins/console/public/lib/ace_token_provider/token_provider.test.ts b/src/plugins/console/public/lib/ace_token_provider/token_provider.test.ts index a933fdeb8010..55c18381cb9c 100644 --- a/src/plugins/console/public/lib/ace_token_provider/token_provider.test.ts +++ b/src/plugins/console/public/lib/ace_token_provider/token_provider.test.ts @@ -30,6 +30,8 @@ import '../../application/models/sense_editor/sense_editor.test.mocks'; +import $ from 'jquery'; + // TODO: // We import from application models as a convenient way to bootstrap loading up of an editor using // this lib. We also need to import application specific mocks which is not ideal. @@ -57,14 +59,14 @@ describe('Ace (legacy) token provider', () => { senseEditor = create(document.querySelector('#ConAppEditor')!); - senseEditor.getCoreEditor().getContainer().style.display = ''; + $(senseEditor.getCoreEditor().getContainer())!.show(); (senseEditor as any).autocomplete._test.removeChangeListener(); tokenProvider = senseEditor.getCoreEditor().getTokenProvider(); }); afterEach(async () => { - senseEditor.getCoreEditor().getContainer().style.display = 'none'; + $(senseEditor.getCoreEditor().getContainer())!.hide(); (senseEditor as any).autocomplete._test.addChangeListener(); await senseEditor.update('', true); }); diff --git a/src/plugins/console/public/lib/osd/osd.js b/src/plugins/console/public/lib/osd/osd.js index cf8126271072..529fba754a93 100644 --- a/src/plugins/console/public/lib/osd/osd.js +++ b/src/plugins/console/public/lib/osd/osd.js @@ -38,6 +38,7 @@ import { UsernameAutocompleteComponent, } from '../autocomplete/components'; +import $ from 'jquery'; import _ from 'lodash'; import Api from './api'; @@ -173,19 +174,20 @@ function loadApisFromJson( // like this, it looks like a minor security issue. export function setActiveApi(api) { if (!api) { - fetch('../api/console/api_server', { - method: 'GET', + $.ajax({ + url: '../api/console/api_server', + dataType: 'json', // disable automatic guessing headers: { 'osd-xsrf': 'opensearch-dashboards', }, - }) - .then(function (response) { - response.json(); - }) - .then(function (data) { + }).then( + function (data) { setActiveApi(loadApisFromJson(data)); - }) - .catch((error) => console.log(`failed to load API '${api}': ${error}`)); + }, + function (jqXHR) { + console.log("failed to load API '" + api + "': " + jqXHR.responseText); + } + ); return; } From 0e34c3ccbc7de911862694c98f910dd223d5c27a Mon Sep 17 00:00:00 2001 From: Kawika Avilla Date: Mon, 24 Apr 2023 19:51:54 -0700 Subject: [PATCH 15/35] [BUG][Dashboard listing] push to history if dashboard otherwise nav (#3922) History push will just to the current route. However, dashboardsProvider was implemented with the expectation that it was a different app. So when a plugin registered it was attempting to navigate to `app/dashboard#/app/{url}` Add tests and extra data test subject. Signed-off-by: Kawika Avilla --- .../public/application/legacy_app.js | 16 +++-- .../listing/create_button.test.tsx | 2 +- .../application/listing/create_button.tsx | 2 +- test/plugin_functional/config.ts | 1 + .../opensearch_dashboards.json | 9 +++ .../package.json | 17 +++++ .../public/index.ts | 22 ++++++ .../public/plugin.tsx | 53 ++++++++++++++ .../tsconfig.json | 17 +++++ .../dashboard_listing_plugin.ts | 71 +++++++++++++++++++ .../dashboard_listing_plugin/index.ts | 35 +++++++++ 11 files changed, 239 insertions(+), 6 deletions(-) create mode 100644 test/plugin_functional/plugins/dashboard_listing_test_plugin/opensearch_dashboards.json create mode 100644 test/plugin_functional/plugins/dashboard_listing_test_plugin/package.json create mode 100644 test/plugin_functional/plugins/dashboard_listing_test_plugin/public/index.ts create mode 100644 test/plugin_functional/plugins/dashboard_listing_test_plugin/public/plugin.tsx create mode 100644 test/plugin_functional/plugins/dashboard_listing_test_plugin/tsconfig.json create mode 100644 test/plugin_functional/test_suites/dashboard_listing_plugin/dashboard_listing_plugin.ts create mode 100644 test/plugin_functional/test_suites/dashboard_listing_plugin/index.ts diff --git a/src/plugins/dashboard/public/application/legacy_app.js b/src/plugins/dashboard/public/application/legacy_app.js index 3ed961e54049..baacc69f7802 100644 --- a/src/plugins/dashboard/public/application/legacy_app.js +++ b/src/plugins/dashboard/public/application/legacy_app.js @@ -163,11 +163,19 @@ export function initDashboardApp(app, deps) { }; }; - $scope.editItem = ({ editUrl }) => { - history.push(editUrl); + $scope.editItem = ({ appId, editUrl }) => { + if (appId === 'dashboard') { + history.push(editUrl); + } else { + deps.core.application.navigateToUrl(editUrl); + } }; - $scope.viewItem = ({ viewUrl }) => { - history.push(deps.addBasePath(viewUrl)); + $scope.viewItem = ({ appId, viewUrl }) => { + if (appId === 'dashboard') { + history.push(viewUrl); + } else { + deps.core.application.navigateToUrl(viewUrl); + } }; $scope.delete = (dashboards) => { const ids = dashboards.map((d) => ({ id: d.id, appId: d.appId })); diff --git a/src/plugins/dashboard/public/application/listing/create_button.test.tsx b/src/plugins/dashboard/public/application/listing/create_button.test.tsx index 5d2a200f55df..9521df8590e6 100644 --- a/src/plugins/dashboard/public/application/listing/create_button.test.tsx +++ b/src/plugins/dashboard/public/application/listing/create_button.test.tsx @@ -57,7 +57,7 @@ describe('create button with props', () => { expect(createButtons.length).toBe(0); const createDropdown = findTestSubject(component, 'createMenuDropdown'); createDropdown.simulate('click'); - const contextMenus = findTestSubject(component, 'contextMenuItem'); + const contextMenus = findTestSubject(component, 'contextMenuItem-test'); expect(contextMenus.length).toBe(2); expect(contextMenus.at(0).prop('href')).toBe('test1'); }); diff --git a/src/plugins/dashboard/public/application/listing/create_button.tsx b/src/plugins/dashboard/public/application/listing/create_button.tsx index 04e6df883779..4959603fa271 100644 --- a/src/plugins/dashboard/public/application/listing/create_button.tsx +++ b/src/plugins/dashboard/public/application/listing/create_button.tsx @@ -38,7 +38,7 @@ const CreateButton = (props: CreateButtonProps) => { {provider.createLinkText} diff --git a/test/plugin_functional/config.ts b/test/plugin_functional/config.ts index e733a4e36368..ce027815a57f 100644 --- a/test/plugin_functional/config.ts +++ b/test/plugin_functional/config.ts @@ -52,6 +52,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { require.resolve('./test_suites/doc_views_links'), require.resolve('./test_suites/application_links'), require.resolve('./test_suites/data_plugin'), + require.resolve('./test_suites/dashboard_listing_plugin'), ], services: { ...functionalConfig.get('services'), diff --git a/test/plugin_functional/plugins/dashboard_listing_test_plugin/opensearch_dashboards.json b/test/plugin_functional/plugins/dashboard_listing_test_plugin/opensearch_dashboards.json new file mode 100644 index 000000000000..454d9ea58471 --- /dev/null +++ b/test/plugin_functional/plugins/dashboard_listing_test_plugin/opensearch_dashboards.json @@ -0,0 +1,9 @@ +{ + "id": "dashboard_listing_test_plugin", + "version": "0.0.1", + "opensearchDashboardsVersion": "opensearchDashboards", + "configPath": ["dashboard_listing_test_plugin"], + "server": false, + "ui": true, + "requiredPlugins": ["dashboard"] +} diff --git a/test/plugin_functional/plugins/dashboard_listing_test_plugin/package.json b/test/plugin_functional/plugins/dashboard_listing_test_plugin/package.json new file mode 100644 index 000000000000..0b593604a2ad --- /dev/null +++ b/test/plugin_functional/plugins/dashboard_listing_test_plugin/package.json @@ -0,0 +1,17 @@ +{ + "name": "dashboard_listing_test_plugin", + "version": "1.0.0", + "main": "target/test/plugin_functional/plugins/dashboard_listing_test_plugin", + "opensearchDashboards": { + "version": "opensearchDashboards", + "templateVersion": "1.0.0" + }, + "license": "Apache-2.0", + "scripts": { + "osd": "../../../../scripts/use_node ../../../../scripts/osd.js", + "build": "../../../../scripts/use_node ../../../../scripts/remove.js './target' && tsc" + }, + "devDependencies": { + "typescript": "4.0.2" + } +} diff --git a/test/plugin_functional/plugins/dashboard_listing_test_plugin/public/index.ts b/test/plugin_functional/plugins/dashboard_listing_test_plugin/public/index.ts new file mode 100644 index 000000000000..80ddbf8a3382 --- /dev/null +++ b/test/plugin_functional/plugins/dashboard_listing_test_plugin/public/index.ts @@ -0,0 +1,22 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +import { PluginInitializer } from 'opensearch-dashboards/public'; +import { + DashboardListingTestPlugin, + DashboardListingTestPluginSetup, + DashboardListingTestPluginStart, +} from './plugin'; + +export const plugin: PluginInitializer< + DashboardListingTestPluginSetup, + DashboardListingTestPluginStart +> = () => new DashboardListingTestPlugin(); diff --git a/test/plugin_functional/plugins/dashboard_listing_test_plugin/public/plugin.tsx b/test/plugin_functional/plugins/dashboard_listing_test_plugin/public/plugin.tsx new file mode 100644 index 000000000000..76a407f7c0d2 --- /dev/null +++ b/test/plugin_functional/plugins/dashboard_listing_test_plugin/public/plugin.tsx @@ -0,0 +1,53 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +import * as React from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; +import { Router, Switch, Route, Link } from 'react-router-dom'; +import { CoreSetup, Plugin } from 'opensearch-dashboards/public'; + +export class DashboardListingTestPlugin + implements Plugin { + public setup(core: CoreSetup, setupDeps: SetupDependencies) { + const ID = 'dashboard_listing_test_plugin'; + const BASE_URL = core.http.basePath.prepend(`/app/${ID}#`); + setupDeps.dashboard.registerDashboardProvider({ + appId: ID, + savedObjectsType: 'dashboardTest', + savedObjectsName: 'Dashboard Test', + editUrlPathFn: (obj: SavedObject) => `${BASE_URL}/${obj.id}/edit`, + viewUrlPathFn: (obj: SavedObject) => `${BASE_URL}/${obj.id}`, + createLinkText: 'Test Dashboard', + createSortText: 'Test Dashboard', + createUrl: `${BASE_URL}/create`, + }); + + core.application.register({ + id: ID, + title: 'Dashboard Listing Test Plugin', + appRoute: `app/${ID}`, + async mount(context, { element }) { + render( +

Dashboard Listing Test Header

, + element + ); + + return () => unmountComponentAtNode(element); + }, + }); + } + + public start() {} + public stop() {} +} + +export type DashboardListingTestPluginSetup = ReturnType; +export type DashboardListingTestPluginStart = ReturnType; diff --git a/test/plugin_functional/plugins/dashboard_listing_test_plugin/tsconfig.json b/test/plugin_functional/plugins/dashboard_listing_test_plugin/tsconfig.json new file mode 100644 index 000000000000..f77a5eaffc30 --- /dev/null +++ b/test/plugin_functional/plugins/dashboard_listing_test_plugin/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./target", + "skipLibCheck": true + }, + "include": [ + "index.ts", + "public/**/*.ts", + "public/**/*.tsx", + "../../../../typings/**/*", + ], + "exclude": [], + "references": [ + { "path": "../../../../src/core/tsconfig.json" } + ] +} diff --git a/test/plugin_functional/test_suites/dashboard_listing_plugin/dashboard_listing_plugin.ts b/test/plugin_functional/test_suites/dashboard_listing_plugin/dashboard_listing_plugin.ts new file mode 100644 index 000000000000..354cfac4fa87 --- /dev/null +++ b/test/plugin_functional/test_suites/dashboard_listing_plugin/dashboard_listing_plugin.ts @@ -0,0 +1,71 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +import url from 'url'; +import expect from '@osd/expect'; + +const getPathWithHash = (absoluteUrl: string) => { + const parsed = url.parse(absoluteUrl); + return `${parsed.path}${parsed.hash ?? ''}`; +}; + +export default function ({ getService, getPageObjects }) { + const testSubjects = getService('testSubjects'); + const PageObjects = getPageObjects(['common', 'dashboard', 'header']); + const browser = getService('browser'); + const listingTable = getService('listingTable'); + const find = getService('find'); + + describe('dashboard listing plugin', function describeIndexTests() { + const dashboardName = 'Dashboard Test'; + + before(async () => { + await PageObjects.dashboard.initTests({ + opensearchDashboardsIndex: '../functional/fixtures/opensearch_archiver/dashboard/legacy', + }); + await PageObjects.dashboard.clickCreateDashboardPrompt(); + await PageObjects.dashboard.saveDashboard('default'); + await PageObjects.dashboard.gotoDashboardLandingPage(); + }); + + it('should be able to navigate to create a dashboard', async () => { + await testSubjects.click('createMenuDropdown'); + await testSubjects.click('contextMenuItem-dashboard'); + await PageObjects.dashboard.saveDashboard(dashboardName); + + await PageObjects.dashboard.gotoDashboardLandingPage(); + await listingTable.searchAndExpectItemsCount('dashboard', dashboardName, 1); + }); + + it('should be able to navigate to view dashboard', async () => { + await listingTable.clickItemLink('dashboard', dashboardName); + await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); + await PageObjects.dashboard.getIsInViewMode(); + await PageObjects.dashboard.gotoDashboardLandingPage(); + }); + + it('should be able to navigate to edit dashboard', async () => { + await listingTable.searchForItemWithName(dashboardName); + const editBttn = await find.allByCssSelector('.euiToolTipAnchor'); + await editBttn[0].click(); + await PageObjects.dashboard.clickCancelOutOfEditMode(); + await PageObjects.dashboard.gotoDashboardLandingPage(); + }); + + it('should be able to navigate to create a test dashboard', async () => { + await testSubjects.click('createMenuDropdown'); + await testSubjects.click('contextMenuItem-dashboard_listing_test_plugin'); + expect(getPathWithHash(await browser.getCurrentUrl())).to.eql( + '/app/dashboard_listing_test_plugin#/create' + ); + }); + }); +} diff --git a/test/plugin_functional/test_suites/dashboard_listing_plugin/index.ts b/test/plugin_functional/test_suites/dashboard_listing_plugin/index.ts new file mode 100644 index 000000000000..a84790824f64 --- /dev/null +++ b/test/plugin_functional/test_suites/dashboard_listing_plugin/index.ts @@ -0,0 +1,35 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +export default function ({ getService, loadTestFile }) { + const browser = getService('browser'); + const opensearchArchiver = getService('opensearchArchiver'); + + async function loadLogstash() { + await browser.setWindowSize(1200, 900); + await opensearchArchiver.loadIfNeeded( + '../functional/fixtures/opensearch_archiver/logstash_functional' + ); + } + + async function unloadLogstash() { + await opensearchArchiver.unload( + '../functional/fixtures/opensearch_archiver/logstash_functional' + ); + } + + describe('dashboard listing plugin', () => { + before(loadLogstash); + after(unloadLogstash); + + loadTestFile(require.resolve('./dashboard_listing_plugin')); + }); +} From 66aa1228504dc32b5d9aee1e2a93f48a52c3a1f5 Mon Sep 17 00:00:00 2001 From: Josh Romero Date: Mon, 24 Apr 2023 20:04:59 -0700 Subject: [PATCH 16/35] remove jquery console release note for https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3929 revert (#3930) Signed-off-by: Josh Romero Co-authored-by: Ashwin P Chandran --- release-notes/opensearch-dashboards.release-notes-2.7.0.md | 1 - 1 file changed, 1 deletion(-) diff --git a/release-notes/opensearch-dashboards.release-notes-2.7.0.md b/release-notes/opensearch-dashboards.release-notes-2.7.0.md index 3ad55fe4d807..60eda3832436 100644 --- a/release-notes/opensearch-dashboards.release-notes-2.7.0.md +++ b/release-notes/opensearch-dashboards.release-notes-2.7.0.md @@ -76,7 +76,6 @@ ### 🪛 Refactoring - Remove automatic addition of `osd-version` header to requests outside of OpenSearch Dashboards ([#3643](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3643)) -- [Console] Replace jQuery usage in console plugin with native methods ([#3733](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3733)) - [Doc Links] Clean up docs_link_service organization so that strings are in the right categories. ([#3685](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3685)) - [I18n] Fix Listr type errors and error handlers ([#3629](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3629)) - [Multiple DataSource] Refactor dev tool console to use opensearch-js client to send requests ([#3544](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3544)) From 755f16bb33a06436b24ebe790e21c9f320afdefa Mon Sep 17 00:00:00 2001 From: Andrey Myssak <40265277+andreymyssak@users.noreply.github.com> Date: Wed, 26 Apr 2023 05:07:43 +0600 Subject: [PATCH 17/35] [CCI] Update js-yaml to v4.0.5 (#3770) * Update js-yaml to 4.0.5 (#3659) * Update CHANGELOG.md (#3659) Co-authored-by: Sergey Myssak Signed-off-by: Andrey Myssak --------- Signed-off-by: Andrey Myssak Signed-off-by: Josh Romero Co-authored-by: Sergey Myssak Co-authored-by: Josh Romero --- CHANGELOG.md | 1 + package.json | 4 ++-- packages/osd-apm-config-loader/package.json | 2 +- .../osd-apm-config-loader/src/utils/read_config.ts | 4 ++-- packages/osd-config/package.json | 2 +- packages/osd-config/src/raw/read_config.ts | 4 ++-- packages/osd-optimizer/package.json | 2 +- packages/osd-optimizer/src/limits.ts | 4 ++-- .../integration_tests/reload_logging_config.test.ts | 4 ++-- src/dev/stylelint/lint_files.js | 4 ++-- .../collectors/usage/telemetry_usage_collector.ts | 4 ++-- tasks/function_test_groups.js | 4 ++-- yarn.lock | 12 ++++++------ 13 files changed, 26 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 92f117e3a594..094455952569 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -182,6 +182,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - Remove the unused `renovate.json5` file ([3489](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3489)) - Allow selecting the Node.js binary using `NODE_HOME` and `OSD_NODE_HOME` ([3508](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3508)) - Bump `styled-components` from 5.3.5 to 5.3.9 ([#3678](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3678)) +- Bump `js-yaml` from 3.14.0 to 4.1.0 ([#3770](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3770)) - Bump `oui` from `1.0.0` to `1.1.1` ([#3884](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3884)) ### 🪛 Refactoring diff --git a/package.json b/package.json index b9eb244f7756..4bd9cd10f497 100644 --- a/package.json +++ b/package.json @@ -184,7 +184,7 @@ "inline-style": "^2.0.0", "ip-cidr": "^2.1.0", "joi": "^13.5.2", - "js-yaml": "^3.14.0", + "js-yaml": "^4.1.0", "json-stable-stringify": "^1.0.1", "json-stringify-safe": "5.0.1", "lodash": "^4.17.21", @@ -288,7 +288,7 @@ "@types/jest": "^27.4.0", "@types/joi": "^13.4.2", "@types/jquery": "^3.3.31", - "@types/js-yaml": "^3.11.1", + "@types/js-yaml": "^4.0.5", "@types/json-stable-stringify": "^1.0.32", "@types/json5": "^0.0.30", "@types/license-checker": "15.0.0", diff --git a/packages/osd-apm-config-loader/package.json b/packages/osd-apm-config-loader/package.json index c3c0249bd73e..fc06ff025e8a 100644 --- a/packages/osd-apm-config-loader/package.json +++ b/packages/osd-apm-config-loader/package.json @@ -13,7 +13,7 @@ "dependencies": { "@elastic/safer-lodash-set": "0.0.0", "@osd/utils": "1.0.0", - "js-yaml": "^3.14.0", + "js-yaml": "^4.1.0", "lodash": "^4.17.21" }, "devDependencies": { diff --git a/packages/osd-apm-config-loader/src/utils/read_config.ts b/packages/osd-apm-config-loader/src/utils/read_config.ts index 806a1ad2e92a..7076060cb04d 100644 --- a/packages/osd-apm-config-loader/src/utils/read_config.ts +++ b/packages/osd-apm-config-loader/src/utils/read_config.ts @@ -29,13 +29,13 @@ */ import { readFileSync } from 'fs'; -import { safeLoad } from 'js-yaml'; +import { load } from 'js-yaml'; import { set } from '@elastic/safer-lodash-set'; import { isPlainObject } from 'lodash'; import { ensureDeepObject } from './ensure_deep_object'; -const readYaml = (path: string) => safeLoad(readFileSync(path, 'utf8')); +const readYaml = (path: string) => load(readFileSync(path, 'utf8')); function replaceEnvVarRefs(val: string) { return val.replace(/\$\{(\w+)\}/g, (match, envVarName) => { diff --git a/packages/osd-config/package.json b/packages/osd-config/package.json index 7d2f68abc8d8..f6d363c9c386 100644 --- a/packages/osd-config/package.json +++ b/packages/osd-config/package.json @@ -14,7 +14,7 @@ "@osd/config-schema": "1.0.0", "@osd/logging": "1.0.0", "@osd/std": "1.0.0", - "js-yaml": "^3.14.0", + "js-yaml": "^4.1.0", "load-json-file": "^6.2.0", "lodash": "^4.17.21", "moment": "^2.24.0", diff --git a/packages/osd-config/src/raw/read_config.ts b/packages/osd-config/src/raw/read_config.ts index 0c4a7c2330de..ad7820381eee 100644 --- a/packages/osd-config/src/raw/read_config.ts +++ b/packages/osd-config/src/raw/read_config.ts @@ -29,13 +29,13 @@ */ import { readFileSync } from 'fs'; -import { safeLoad } from 'js-yaml'; +import { load } from 'js-yaml'; import { set } from '@elastic/safer-lodash-set'; import { isPlainObject } from 'lodash'; import { ensureDeepObject } from './ensure_deep_object'; -const readYaml = (path: string) => safeLoad(readFileSync(path, 'utf8')); +const readYaml = (path: string) => load(readFileSync(path, 'utf8')); function replaceEnvVarRefs(val: string) { return val.replace(/\$\{(\w+)\}/g, (match, envVarName) => { diff --git a/packages/osd-optimizer/package.json b/packages/osd-optimizer/package.json index 2aec5681ba91..5e82a7004fff 100644 --- a/packages/osd-optimizer/package.json +++ b/packages/osd-optimizer/package.json @@ -27,7 +27,7 @@ "execa": "^4.0.2", "fibers": "^5.0.3", "jest-diff": "^27.5.1", - "js-yaml": "^3.14.0", + "js-yaml": "^4.1.0", "json-stable-stringify": "^1.0.1", "lmdb-store": "^1.6.11", "normalize-path": "^3.0.0", diff --git a/packages/osd-optimizer/src/limits.ts b/packages/osd-optimizer/src/limits.ts index 86b186930275..d81137c3a9a4 100644 --- a/packages/osd-optimizer/src/limits.ts +++ b/packages/osd-optimizer/src/limits.ts @@ -51,7 +51,7 @@ export function readLimits(): Limits { } } - return yaml ? (Yaml.safeLoad(yaml) as any) : {}; + return yaml ? (Yaml.load(yaml) as any) : {}; } export function validateLimitsForAllBundles(log: ToolingLog, config: OptimizerConfig) { @@ -109,6 +109,6 @@ export function updateBundleLimits(log: ToolingLog, config: OptimizerConfig) { pageLoadAssetSize, }; - Fs.writeFileSync(LIMITS_PATH, Yaml.safeDump(newLimits)); + Fs.writeFileSync(LIMITS_PATH, Yaml.dump(newLimits)); log.success(`wrote updated limits to ${LIMITS_PATH}`); } diff --git a/src/cli/serve/integration_tests/reload_logging_config.test.ts b/src/cli/serve/integration_tests/reload_logging_config.test.ts index fb3c63ffc71d..5950cb1fdfb0 100644 --- a/src/cli/serve/integration_tests/reload_logging_config.test.ts +++ b/src/cli/serve/integration_tests/reload_logging_config.test.ts @@ -36,7 +36,7 @@ import Del from 'del'; import * as Rx from 'rxjs'; import { map, filter, take } from 'rxjs/operators'; -import { safeDump } from 'js-yaml'; +import { dump } from 'js-yaml'; import { getConfigFromFiles } from '@osd/config'; const legacyConfig = follow('__fixtures__/reload_logging_config/opensearch_dashboards.test.yml'); @@ -89,7 +89,7 @@ function createConfigManager(configPath: string) { return { modify(fn: (input: Record) => Record) { const oldContent = getConfigFromFiles([configPath]); - const yaml = safeDump(fn(oldContent)); + const yaml = dump(fn(oldContent)); Fs.writeFileSync(configPath, yaml); }, }; diff --git a/src/dev/stylelint/lint_files.js b/src/dev/stylelint/lint_files.js index ead1fde5bb23..2d94f98b396c 100644 --- a/src/dev/stylelint/lint_files.js +++ b/src/dev/stylelint/lint_files.js @@ -30,13 +30,13 @@ import stylelint from 'stylelint'; import path from 'path'; -import { safeLoad } from 'js-yaml'; +import { load } from 'js-yaml'; import fs from 'fs'; import { createFailError } from '@osd/dev-utils'; // load the include globs from .stylelintrc.yml and convert them to regular expressions for filtering files const stylelintPath = path.resolve(__dirname, '..', '..', '..', '.stylelintrc.yml'); -const styleLintConfig = safeLoad(fs.readFileSync(stylelintPath)); +const styleLintConfig = load(fs.readFileSync(stylelintPath)); /** * Lints a list of files with eslint. eslint reports are written to the log diff --git a/src/plugins/telemetry/server/collectors/usage/telemetry_usage_collector.ts b/src/plugins/telemetry/server/collectors/usage/telemetry_usage_collector.ts index 7347975dbc4a..e15a1921b734 100644 --- a/src/plugins/telemetry/server/collectors/usage/telemetry_usage_collector.ts +++ b/src/plugins/telemetry/server/collectors/usage/telemetry_usage_collector.ts @@ -29,7 +29,7 @@ */ import { accessSync, constants, readFileSync, statSync } from 'fs'; -import { safeLoad } from 'js-yaml'; +import { load } from 'js-yaml'; import { dirname, join } from 'path'; import { Observable } from 'rxjs'; @@ -78,7 +78,7 @@ export async function readTelemetryFile( try { if (isFileReadable(configPath)) { const yaml = readFileSync(configPath); - const data = safeLoad(yaml.toString()); + const data = load(yaml.toString()); // don't bother returning empty objects if (Object.keys(data).length) { diff --git a/tasks/function_test_groups.js b/tasks/function_test_groups.js index 91f017a437c3..d6df1573a661 100644 --- a/tasks/function_test_groups.js +++ b/tasks/function_test_groups.js @@ -33,10 +33,10 @@ import { resolve } from 'path'; import execa from 'execa'; import grunt from 'grunt'; -import { safeLoad } from 'js-yaml'; +import { load } from 'js-yaml'; const JOBS_YAML = readFileSync(resolve(__dirname, '../.ci/jobs.yml'), 'utf8'); -const TEST_TAGS = safeLoad(JOBS_YAML) +const TEST_TAGS = load(JOBS_YAML) .JOB.filter((id) => id.startsWith('opensearch-dashboards-ciGroup')) .map((id) => id.replace(/^opensearch-dashboards-/, '')); diff --git a/yarn.lock b/yarn.lock index 33ca05a2f2c1..7817a668b032 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3126,10 +3126,10 @@ resolved "https://registry.yarnpkg.com/@types/js-cookie/-/js-cookie-2.2.5.tgz#38dfaacae8623b37cc0b0d27398e574e3fc28b1e" integrity sha512-cpmwBRcHJmmZx0OGU7aPVwGWGbs4iKwVYchk9iuMtxNCA2zorwdaTz4GkLgs2WGxiRZRFKnV1k6tRUHX7tBMxg== -"@types/js-yaml@^3.11.1": - version "3.12.7" - resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-3.12.7.tgz#330c5d97a3500e9c903210d6e49f02964af04a0e" - integrity sha512-S6+8JAYTE1qdsc9HMVsfY7+SgSuUU/Tp6TYTmITW0PZxiyIMvol3Gy//y69Wkhs0ti4py5qgR3uZH6uz/DNzJQ== +"@types/js-yaml@^4.0.5": + version "4.0.5" + resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-4.0.5.tgz#738dd390a6ecc5442f35e7f03fa1431353f7e138" + integrity sha512-FhpRzf927MNQdRZP0J5DLIdTXhjLYzeUTmLAu69mnVksLH9CJY3IuSeEgbKUki7GQZm0WqDkGzyxju2EZGD2wA== "@types/json-schema@*", "@types/json-schema@^7.0.3", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": version "7.0.11" @@ -11234,14 +11234,14 @@ js-yaml-js-types@1.0.0: dependencies: esprima "^4.0.1" -js-yaml@4.1.0: +js-yaml@4.1.0, js-yaml@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== dependencies: argparse "^2.0.1" -js-yaml@^3.13.1, js-yaml@^3.14.0, js-yaml@~3.14.0: +js-yaml@^3.13.1, js-yaml@~3.14.0: version "3.14.1" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== From ac2ee3a3ee5c0d56243c7a2d731b8743bbd77ab8 Mon Sep 17 00:00:00 2001 From: Melissa Vagi Date: Wed, 26 Apr 2023 16:26:36 -0600 Subject: [PATCH 18/35] Update README.md (#3788) * Update README.md Signed-off-by: Melissa Vagi * Update README.md Co-authored-by: Miki --------- Signed-off-by: Melissa Vagi Co-authored-by: Miki Co-authored-by: Miki --- README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d1b8fc9ed9dd..e073be79e208 100644 --- a/README.md +++ b/README.md @@ -8,9 +8,13 @@ ## Welcome -OpenSearch Dashboards is an open source search and analytics visualization. We aim to be the best community-driven platform and provide all the contributors a great open source experience. +OpenSearch Dashboards is an open-source data visualization tool designed to work with OpenSearch. OpenSearch Dashboards gives you data visualization tools to improve and automate business intelligence and support data-driven decision-making and strategic planning. -Feel free to take a look at what the community has been up to, and then head over to the [Project Board](https://github.com/opensearch-project/OpenSearch-Dashboards/projects) to track release targets, or jump in and [start opening issues](https://github.com/opensearch-project/OpenSearch-Dashboards/issues/new/choose), [set up your development environment](DEVELOPER_GUIDE.md#getting-started), or [start contributing](CONTRIBUTING.md). +We aim to be an exceptional community-driven platform and to foster open participation and collective contribution with all contributors. Stay up to date on what's happening with the OpenSearch Project by tracking GitHub [issues](https://github.com/opensearch-project/OpenSearch-Dashboards/issues) and [pull requests](https://github.com/opensearch-project/OpenSearch-Dashboards/pulls). + +You can [contribute to this project](https://github.com/opensearch-project/OpenSearch-Dashboards/issues/CONTRIBUTING.md) by [opening issues](https://github.com/opensearch-project/OpenSearch-Dashboards/issues/new/choose) to give feedback, share ideas, identify bugs, and contribute code. + +Set up your [OpenSearch Dashboards development environment](ttps://github.com/opensearch-project/OpenSearch-Dashboards/blob/main/DEVELOPER_GUIDE.md#getting-started-guide) today! The project team looks forward to your contributions. ## Code Summary From a8ace28a97f5b178e5e767db03dc8d38046ce14e Mon Sep 17 00:00:00 2001 From: Manasvini B Suryanarayana Date: Thu, 27 Apr 2023 13:20:11 -0700 Subject: [PATCH 19/35] Bump yaml to 2.2.2 (#3947) Signed-off-by: Manasvini B Suryanarayana Co-authored-by: Sean Neumann <1413295+seanneumann@users.noreply.github.com> --- CHANGELOG.md | 3 ++- package.json | 3 ++- yarn.lock | 13 ++++--------- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 094455952569..6f969dec1268 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - Eliminate dependency on `got` versions older than 11.8.5 ([#2801](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2801)) - [Multi DataSource] Add explicit no spellcheck on password fields ([#2818](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2818)) - [CVE-2022-25912] Bumps simple-git from 3.4.0 to 3.15.0 ([#3036](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3036)) -- [CVE-2022-35256] Bumps node version from 14.20.0 to 14.20.1 [#3166](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3166)) +- [CVE-2022-35256] Bumps node version from 14.20.0 to 14.20.1 ([#3166](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3166)) - [CVE-2022-46175] Bumps json5 version from 1.0.1 and 2.2.1 to 1.0.2 and 2.2.3 ([#3201](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3201)) - [CVE-2022-25860] Bumps simple-git from 3.15.1 to 3.16.0 ([#3345](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3345)) - [Security] Bumps hapi/statehood to 7.0.4 ([#3411](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3411)) @@ -23,6 +23,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [CVE-2023-26486][cve-2023-26487] Bump vega from 5.22.1 to 5.23.0 ([#3533](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3533)) - [CVE-2023-0842] Bump xml2js from 0.4.23 to 0.5.0 ([#3842](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3842)) - [Multi DataSource] Add private IP blocking validation on server side([#3912](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3912)) +- [CVE-2023-2251] Bump yaml to 2.2.2 ([#3947](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3947)) ### 📈 Features/Enhancements diff --git a/package.json b/package.json index 4bd9cd10f497..04856baeb20c 100644 --- a/package.json +++ b/package.json @@ -97,7 +97,8 @@ "**/unset-value": "^2.0.1", "**/jest-config": "npm:@amoo-miki/jest-config@27.5.1", "**/jest-jasmine2": "npm:@amoo-miki/jest-jasmine2@27.5.1", - "**/xml2js": "^0.5.0" + "**/xml2js": "^0.5.0", + "**/yaml": "^2.2.2" }, "workspaces": { "packages": [ diff --git a/yarn.lock b/yarn.lock index 7817a668b032..64c5873a2e3f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18478,15 +18478,10 @@ yallist@^4.0.0: resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== -yaml@^1.10.0: - version "1.10.2" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" - integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== - -yaml@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.1.1.tgz#1e06fb4ca46e60d9da07e4f786ea370ed3c3cfec" - integrity sha512-o96x3OPo8GjWeSLF+wOAbrPfhFOGY0W00GNaxCDv+9hkcDJEnev1yh8S7pgHF0ik6zc8sQLuL8hjHjJULZp8bw== +yaml@^1.10.0, yaml@^2.0.0, yaml@^2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.2.2.tgz#ec551ef37326e6d42872dad1970300f8eb83a073" + integrity sha512-CBKFWExMn46Foo4cldiChEzn7S7SRV+wqiluAb6xmueD/fGyRHIhX8m14vVGgeFWjN540nKCNVj6P21eQjgTuA== yargs-parser@20.2.4: version "20.2.4" From ca0bb8f63f5db103c4ea2ca21fd41dc66310d957 Mon Sep 17 00:00:00 2001 From: Miki Date: Tue, 2 May 2023 11:40:56 -0700 Subject: [PATCH 20/35] Bump `joi` to v14 to avoid the possibility of prototype poisoning in a nested dependency (#3952) Signed-off-by: Miki --- CHANGELOG.md | 3 ++- package.json | 2 +- packages/osd-config-schema/package.json | 2 +- packages/osd-test/package.json | 2 +- yarn.lock | 15 +++++---------- 5 files changed, 10 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f969dec1268..f2924cdc1c11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,7 +22,8 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [CVE-2023-25653] Bump node-jose to 2.2.0 ([#3445](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3445)) - [CVE-2023-26486][cve-2023-26487] Bump vega from 5.22.1 to 5.23.0 ([#3533](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3533)) - [CVE-2023-0842] Bump xml2js from 0.4.23 to 0.5.0 ([#3842](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3842)) -- [Multi DataSource] Add private IP blocking validation on server side([#3912](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3912)) +- [Multi DataSource] Add private IP blocking validation on server side ([#3912](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3912)) +- Bump `joi` to v14 to avoid the possibility of prototype poisoning in a nested dependency ([#3952](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3952)) - [CVE-2023-2251] Bump yaml to 2.2.2 ([#3947](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3947)) ### 📈 Features/Enhancements diff --git a/package.json b/package.json index 04856baeb20c..2b894a95897a 100644 --- a/package.json +++ b/package.json @@ -184,7 +184,7 @@ "https-proxy-agent": "^5.0.0", "inline-style": "^2.0.0", "ip-cidr": "^2.1.0", - "joi": "^13.5.2", + "joi": "^14.3.1", "js-yaml": "^4.1.0", "json-stable-stringify": "^1.0.1", "json-stringify-safe": "5.0.1", diff --git a/packages/osd-config-schema/package.json b/packages/osd-config-schema/package.json index 52471e29527c..c88afe609e1f 100644 --- a/packages/osd-config-schema/package.json +++ b/packages/osd-config-schema/package.json @@ -16,7 +16,7 @@ }, "peerDependencies": { "lodash": "^4.17.21", - "joi": "^13.5.2", + "joi": "^14.3.1", "moment": "^2.24.0", "type-detect": "^4.0.8" } diff --git a/packages/osd-test/package.json b/packages/osd-test/package.json index 69fa50828fc0..c1ee4f1687cd 100644 --- a/packages/osd-test/package.json +++ b/packages/osd-test/package.json @@ -31,7 +31,7 @@ "exit-hook": "^2.2.0", "getopts": "^2.2.5", "glob": "^7.1.7", - "joi": "^13.5.2", + "joi": "^14.3.1", "lodash": "^4.17.21", "parse-link-header": "^2.0.0", "rxjs": "^6.5.5", diff --git a/yarn.lock b/yarn.lock index 64c5873a2e3f..c424c6592eb5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9589,11 +9589,6 @@ hmac-drbg@^1.0.1: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.1" -hoek@5.x.x: - version "5.0.4" - resolved "https://registry.yarnpkg.com/hoek/-/hoek-5.0.4.tgz#0f7fa270a1cafeb364a4b2ddfaa33f864e4157da" - integrity sha512-Alr4ZQgoMlnere5FZJsIyfIjORBqZll5POhDsF4q64dPuJR6rNxXdDxtHSQq8OXRurhmx+PWYEE8bXRROY8h0w== - hoek@6.x.x: version "6.1.3" resolved "https://registry.yarnpkg.com/hoek/-/hoek-6.1.3.tgz#73b7d33952e01fe27a38b0457294b79dd8da242c" @@ -11187,12 +11182,12 @@ jmespath@0.16.0: resolved "https://registry.yarnpkg.com/jmespath/-/jmespath-0.16.0.tgz#b15b0a85dfd4d930d43e69ed605943c802785076" integrity sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw== -joi@^13.5.2: - version "13.7.0" - resolved "https://registry.yarnpkg.com/joi/-/joi-13.7.0.tgz#cfd85ebfe67e8a1900432400b4d03bbd93fb879f" - integrity sha512-xuY5VkHfeOYK3Hdi91ulocfuFopwgbSORmIwzcwHKESQhC7w1kD5jaVSPnqDxS2I8t3RZ9omCKAxNwXN5zG1/Q== +joi@^14.3.1: + version "14.3.1" + resolved "https://registry.yarnpkg.com/joi/-/joi-14.3.1.tgz#164a262ec0b855466e0c35eea2a885ae8b6c703c" + integrity sha512-LQDdM+pkOrpAn4Lp+neNIFV3axv1Vna3j38bisbQhETPMANYRbFJFUyOZcOClYvM/hppMhGWuKSFEK9vjrB+bQ== dependencies: - hoek "5.x.x" + hoek "6.x.x" isemail "3.x.x" topo "3.x.x" From f5a978dfb510ef777b13eec8589b3134c693b2b7 Mon Sep 17 00:00:00 2001 From: Josh Romero Date: Wed, 3 May 2023 11:16:38 -0700 Subject: [PATCH 21/35] [Doc] Add communication guide (#3837) * docs(COMMUNICATION): Add communication guide with info on slack, forum, and developer office hours link from README, CONTRIBUTING, DEVELOPER_GUIDE Signed-off-by: Josh Romero --------- Signed-off-by: Josh Romero --- CHANGELOG.md | 1 + COMMUNICATIONS.md | 83 ++++++++++++++++++++++++++++++++++++++++++++++ CONTRIBUTING.md | 6 ++++ DEVELOPER_GUIDE.md | 4 +++ README.md | 2 +- 5 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 COMMUNICATIONS.md diff --git a/CHANGELOG.md b/CHANGELOG.md index f2924cdc1c11..37813fee68c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -167,6 +167,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [Doc] Update SECURITY.md with instructions for nested dependencies and backporting ([#3497](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3497)) - [Doc] [Console] Fix/update documentation links in Dev Tools console ([#3724](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3724)) - [Doc] Update DEVELOPER_GUIDE.md with added manual bootstrap timeout solution and max virtual memory error solution with docker ([#3764](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3764)) +- [Doc] Add COMMUNICATIONS.md with info about Slack, forum, office hours ([#3837](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3837)) - [Doc] Add docker files and instructions for debugging Selenium functional tests ([#3747](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3747)) ### 🛠 Maintenance diff --git a/COMMUNICATIONS.md b/COMMUNICATIONS.md new file mode 100644 index 000000000000..c1dbfc114ce7 --- /dev/null +++ b/COMMUNICATIONS.md @@ -0,0 +1,83 @@ +# OpenSearch Dashboards Communication + +- [Overview](#overview) +- [Slack](#slack) +- [Forum](#forum) +- [Developer Office Hours](#developer-office-hours) + - [What it is](#what-it-is) + - [When](#when) + - [How to sign up](#how-to-sign-up) + - [FAQ](#faq) + +## Overview + +The purpose of this document is to provide information regarding the communication channels for OpenSearch Dashboards. All communication is subject to the [OpenSearch Code of Conduct](CODE_OF_CONDUCT.md). Please see [CONTRIBUTING](CONTRIBUTING.md) if you're interested in contributing to the project. + +## Slack + +The OpenSearch project has a public workspace on [Slack](https://opensearch.slack.com). See the [Getting Started guide]() for steps to register and setup the workspace. + +Once registered, check out these channels for discussion of OpenSearch Dashboards topics: + +- [#dashboards](https://opensearch.slack.com/archives/C01QENNTGUD) +- [#dashboards-ux](https://opensearch.slack.com/archives/C05389T9LJC) + +## Forum + +Slack conversations are not searchable outside the workspace. For this reason we encourage using the [OpenSearch Dashboards category](https://forum.opensearch.org/c/opensearch-dashboards/57) of the forum for technical support discussions or summarizing findings for the rest of the community. + +## Developer Office Hours + +### What it is + +A recurring 1-hour virtual meeting for community developers to chat with [OpenSearch Dashboards project maintainers](MAINTAINERS.md). Priority will be given to topics that are signed-up in advance, but ad-hoc discussions are welcome in any remaining time. + +While we'll always prioritize asynchronous communication, sometimes a community call is the most effective and efficient venue to share information and knowledge. Some reasons to sign up: + +1. Review a proposal or technical design for a new feature in OpenSearch Dashboards or an OpenSearch Dashboards plugin +2. Learn more about how to build and extend OpenSearch Dashboards - which APIs, plugins, resources, and services are available to speed development +3. Discuss OpenSearch Dashboard roadmap and technical initiatives + +Signing up isn't required to attend - all OpenSearch Dashboards contributors or interested developers are welcome as participants. + +Bring your ideas and projects early, while you still have time and flexibility to make significant changes. + +### When + +Every other Thursday, 10AM-11AM PT. + +### How to sign up + +There will be a forum post for each iteration of the meeting, with pre-defined slots. To sign-up, simply reply in the forum thread with the following template: + +* Topic: [a brief description of what you'd like to discuss] +* Requested by: [provide GitHub aliases of attendees] +* GitHub issues or PRs: [before signing up, make sure to create an issue, whether in the OpenSearch Dashboards repository or your own plugin repository] +* Time required [choose 15, 30, 45, or 60 minutes] +* Requested maintainer: [optional; provide GitHub alias of any particular maintainer you’d like to attend] + +### FAQ + +#### Will the meetings be recorded? + +Yes, we plan to record each office hours session and post to our YouTube channel so the information can be more easily shared and referenced. + +#### Will all maintainers attend? + +Generally no, but there will always be at least one maintainer. We'll review the sign-ups ahead of time to make sure the right subject-matter experts will attend, depending on the topics. + +#### What happens if there are no sign-ups for a particular session? + +The session will still occur, and the maintainers will present a brief knowledge-sharing session or demo. We'll also hold ad-hoc discussions, but the session may end early. + +#### Is it first come first serve or do we get to decide which topics we discuss in a session? + +For sign-ups, it’s first-come first served, until we decide we need another method. + +#### Will there also be meeting notes? or is the recording the only available transcript? + +No. But we’ll also post the chat transcript and any slides shared (see https://forum.opensearch.org/t/opensearch-community-meeting-2023-0131/11892/5 as example) + +#### How can I cancel or reschedule? + +Just leave another forum reply, as early as possible so other folks have the opportunity to sign-up for the same spot. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 045b17019b7d..4a605d04c052 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,7 @@ - [Contributing to OpenSearch](#contributing-to-opensearch-dashboards) - [First Things First](#first-things-first) - [Ways to Contribute](#ways-to-contribute) + - [Join the Discussion](#join-the-discussion) - [Bug Reports](#bug-reports) - [Feature Requests](#feature-requests) - [Documentation Changes](#documentation-changes) @@ -19,6 +20,11 @@ OpenSearch is a community project that is built and maintained by people just li **Only submit your own work** (or work you have sufficient rights to submit) - Please make sure that any code or documentation you submit is your work or you have the rights to submit. We respect the intellectual property rights of others, and as part of contributing, we'll ask you to sign your contribution with a "Developer Certificate of Origin" (DCO) that states you have the rights to submit this work and you understand we'll use your contribution. There's more information about this topic in the [DCO section](#developer-certificate-of-origin). ## Ways to Contribute + +### Join the Discussion + +See the [communication guide](COMMUNICATION.md)for information on how to join our slack workspace, forum, or developer office hours. + ### Bug Reports A bug is when software behaves in a way that you didn't expect and the developer didn't intend. To help us understand what's going on, we first want to make sure you're working from the latest version. Please make sure you're testing against the [latest version](https://github.com/opensearch-project/OpenSearch-Dashboards). diff --git a/DEVELOPER_GUIDE.md b/DEVELOPER_GUIDE.md index d3c8b269c4c0..3df2106e9678 100644 --- a/DEVELOPER_GUIDE.md +++ b/DEVELOPER_GUIDE.md @@ -209,6 +209,10 @@ $ yarn start --run-examples - [Project testing guidelines](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/main/TESTING.md) - [Plugin conventions](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/main/src/core/CONVENTIONS.md#technical-conventions) +#### Join the discussion + +See the [communication guide](COMMUNICATION.md)for information on how to join our slack workspace, forum, or developer office hours. + ## Alternative development installations Although the [getting started guide](#getting-started-guide) covers the recommended development environment setup, there are several alternatives worth being aware of. diff --git a/README.md b/README.md index e073be79e208..5c6c764f87f6 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ Set up your [OpenSearch Dashboards development environment](ttps://github.com/op * [Project Website](https://opensearch.org/) * [Downloads](https://opensearch.org/downloads.html) * [Documentation](https://opensearch.org/docs/) -* Need help? Try [Forums](https://discuss.opendistrocommunity.dev/) +* Need help? See the [communication guide](COMMUNICATION.md) for various options * [Project Principles](https://opensearch.org/#principles) * [Developer Guide](DEVELOPER_GUIDE.md) * [Contributing to OpenSearch](CONTRIBUTING.md) From 5ea0cbe25223e24badfe27f79813ee313c9af0b0 Mon Sep 17 00:00:00 2001 From: Anan Zhuang Date: Fri, 5 May 2023 10:01:35 -0700 Subject: [PATCH 22/35] Temporarily hardcode chromedriver to 112.0.0 to enable all ftr tests (#3976) The latest version of chromedriver is 112.0.1 which does not support node 14. This PR hardcodes chromedriver to 112.0.0 temporarily. Pls revert it once we bump to node 18. Issue Resolved https://github.com/opensearch-project/OpenSearch-Dashboards/issues/3975 Signed-off-by: ananzh --- CHANGELOG.md | 1 + scripts/upgrade_chromedriver.js | 27 +++++++++++++++++++++------ 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 37813fee68c8..9bd5689a8e75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -211,6 +211,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - Prevent primitive linting limitations from being applied to unit tests found under `src/setup_node_env` ([#3403](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3403)) - [Tests] Fix unit tests for `get_keystore` ([#3854](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3854)) - [Tests] Use `scripts/use_node` instead of `node` in functional test plugins ([#3783](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3783)) +- Temporarily hardcode the largest support `chromedriver` version to `112.0.0` to enable all ftr tests ([#3976](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3976)) ## [2.x] diff --git a/scripts/upgrade_chromedriver.js b/scripts/upgrade_chromedriver.js index 3aa896fd1fa9..224486bb9867 100644 --- a/scripts/upgrade_chromedriver.js +++ b/scripts/upgrade_chromedriver.js @@ -71,16 +71,31 @@ versionCheckCommands.some((cmd) => { const majorVersion = versionCheckOutput?.match?.(/(?:^|\s)(9\d|\d{3})\./)?.[1]; if (majorVersion) { + let targetVersion = `^${majorVersion}`; + + // TODO: Temporary fix to install chromedriver 112.0.0 if major version is 112. + // Exit if major version is greater than 112. + // Revert this once node is bumped to 16+. + // https://github.com/opensearch-project/OpenSearch-Dashboards/issues/3975 + if (parseInt(majorVersion) === 112) { + targetVersion = '112.0.0'; + } else if (parseInt(majorVersion) > 112) { + console.error( + `::error::Chrome version (${majorVersion}) is not supported by this script. The largest chrome version we support is 112.` + ); + process.exit(1); + } + if (process.argv.includes('--install')) { - console.log(`Installing chromedriver@^${majorVersion}`); + console.log(`Installing chromedriver@${targetVersion}`); - spawnSync(`yarn add --dev chromedriver@^${majorVersion}`, { + spawnSync(`yarn add --dev chromedriver@${targetVersion}`, { stdio: 'inherit', cwd: process.cwd(), shell: true, }); } else { - console.log(`Upgrading to chromedriver@^${majorVersion}`); + console.log(`Upgrading to chromedriver@${targetVersion}`); let upgraded = false; const writeStream = createWriteStream('package.json.upgrading-chromedriver', { flags: 'w' }); @@ -92,7 +107,7 @@ if (majorVersion) { if (line.includes('"chromedriver": "')) { line = line.replace( /"chromedriver":\s*"[~^]?\d[\d.]*\d"/, - `"chromedriver": "^${majorVersion}"` + `"chromedriver": "${targetVersion}"` ); upgraded = true; } @@ -107,11 +122,11 @@ if (majorVersion) { renameSync('package.json', 'package.json.bak'); renameSync('package.json.upgrading-chromedriver', 'package.json'); - console.log(`Backed up package.json and updated chromedriver to ${majorVersion}`); + console.log(`Backed up package.json and updated chromedriver to ${targetVersion}`); } else { unlinkSync('package.json.upgrading-chromedriver'); console.error( - `Failed to update chromedriver to ${majorVersion}. Try adding the \`--install\` switch.` + `Failed to update chromedriver to ${targetVersion}. Try adding the \`--install\` switch.` ); } }); From 6e352ff5c0d029abbb7e3f7c3e69be897a95114b Mon Sep 17 00:00:00 2001 From: "Qingyang(Abby) Hu" Date: Fri, 5 May 2023 10:46:13 -0700 Subject: [PATCH 23/35] Fix wording and duplicate code in embeddable example plugin (#3911) * Fix wording and duplicate code in embeddable example plugin Signed-off-by: abbyhu2000 * Fix some wording in the embeddable readme Signed-off-by: abbyhu2000 --------- Signed-off-by: abbyhu2000 --- .../searchable_list_container_component.tsx | 1 - .../public/list_container_example.tsx | 9 +++++++++ src/plugins/embeddable/docs/input_and_output_state.md | 5 +---- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/examples/embeddable_examples/public/searchable_list_container/searchable_list_container_component.tsx b/examples/embeddable_examples/public/searchable_list_container/searchable_list_container_component.tsx index c24d258fd7d8..7e742aba7cc7 100644 --- a/examples/embeddable_examples/public/searchable_list_container/searchable_list_container_component.tsx +++ b/examples/embeddable_examples/public/searchable_list_container/searchable_list_container_component.tsx @@ -82,7 +82,6 @@ export class SearchableListContainerComponentInner extends Component (checked[id] = false)); this.state = { checked, hasMatch, diff --git a/examples/embeddable_explorer/public/list_container_example.tsx b/examples/embeddable_explorer/public/list_container_example.tsx index 797b87513039..f466d120e218 100644 --- a/examples/embeddable_explorer/public/list_container_example.tsx +++ b/examples/embeddable_explorer/public/list_container_example.tsx @@ -164,6 +164,15 @@ export function ListContainerExample({ The first HelloWorldEmbeddable does not emit the hasMatch output variable, so the container chooses to hide it.

+

+ Type some strings in the search bar, and press Check matching button. If the search + string matches with any strings from the title or the description of the children + embeddables, the child embeddable's check box will be checked. Noted that the + search filter is case sensitive. However, even if the search string matches with the + strings in the HelloWorldEmbeddable, its check box will not be checked because of the + reason explained above. If we click Delete checked, all the selected child embeddables + will be deleted from the container. +

Check out the "Dynamically adding children" section, to see how to add diff --git a/src/plugins/embeddable/docs/input_and_output_state.md b/src/plugins/embeddable/docs/input_and_output_state.md index 2fd8799099b6..a0cd4cc5c0ef 100644 --- a/src/plugins/embeddable/docs/input_and_output_state.md +++ b/src/plugins/embeddable/docs/input_and_output_state.md @@ -274,9 +274,6 @@ There are no real life examples showcasing this, it may not even be really neede the thinking being that it would support any type of rendering of child embeddables - whether in a "snap to grid" style like dashboard, or in a free form layout like canvas. -The only real implementation of a container in production code at the time this is written is Dashboard however, with no plans to migrate -Canvas over to use it (this was the original impetus for an abstraction). The container code is quite complicated with child management, -so it makes creating a new container very easy, as you can see in the developer examples of containers. But, it's possible this layer was - an over abstraction without a real prod use case (I can say that because I wrote it, I'm only insulting myself!) :). +The only real implementation of a container in production code at the time is written in the Dashboard plugin, however, with no plans to migrate over to Canvas (this was the original impetus for an abstraction). The container code is quite complicated with child management, so it makes creating a new container very easy, as you can see in the developer examples of containers. But, it's possible this layer was an over abstraction without a real prod use case (I can say that because I wrote it, I'm only insulting myself!) :). Be sure to read [Common mistakes with embeddable containers and inherited input state](./containers_and_inherited_state.md) next! \ No newline at end of file From 0188d05234f3fc510202b27ae1a122a1d1606686 Mon Sep 17 00:00:00 2001 From: Kawika Avilla Date: Thu, 11 May 2023 15:43:10 -0700 Subject: [PATCH 24/35] [CI] setup Chrome and utilize binary path (#3997) Within the CI, the virtual runner that we are utilizing has Chrome installed already. The version of Chrome is installed periodically. The most recent version of Chrome requires updates to dependencies that drop support for Node 14. This downloads chrome in the CI and then checks the chromedriver from the environment variable `TEST_BROWSER_BINARY_PATH`. Signed-off-by: Kawika Avilla --- .github/workflows/build_and_test_workflow.yml | 19 +++++++++++++++++ CHANGELOG.md | 1 + scripts/upgrade_chromedriver.js | 21 +++++++++++++++++-- 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build_and_test_workflow.yml b/.github/workflows/build_and_test_workflow.yml index fd4cdbbefa78..7568b0653210 100644 --- a/.github/workflows/build_and_test_workflow.yml +++ b/.github/workflows/build_and_test_workflow.yml @@ -25,6 +25,8 @@ env: TEST_OPENSEARCH_TRANSPORT_PORT: 9403 TEST_OPENSEARCH_PORT: 9400 OSD_SNAPSHOT_SKIP_VERIFY_CHECKSUM: true + # Version 112.0.5615.0 + CHROME_VERSION: 1109208 jobs: build-lint-test: @@ -122,6 +124,7 @@ jobs: functional-tests: name: Run functional tests on ${{ matrix.name }} (ciGroup${{ matrix.group }}) strategy: + fail-fast: false matrix: os: [ubuntu-latest, windows-latest] group: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13] @@ -134,6 +137,22 @@ jobs: steps: - run: echo Running functional tests for ciGroup${{ matrix.group }} + - name: Setup Chrome + id: setup-chrome + uses: browser-actions/setup-chrome@v1 + with: + chrome-version: ${{ env.CHROME_VERSION }} + + - name: Set Chrome Path + if: matrix.os != 'windows-latest' + run: | + echo "TEST_BROWSER_BINARY_PATH=${{ steps.setup-chrome.outputs.chrome-path }}" >> $GITHUB_ENV + + - name: Set Chrome Path (Windows) + if: matrix.os == 'windows-latest' + run: | + echo "TEST_BROWSER_BINARY_PATH=${{ steps.setup-chrome.outputs.chrome-path }}" >> $env:GITHUB_ENV + - name: Configure git's autocrlf (Windows only) if: matrix.os == 'windows-latest' run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index 9bd5689a8e75..ef17eb77691e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -148,6 +148,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - Upgrade yarn version to be compatible with @openearch-project/opensearch ([#3443](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3443)) - [CI] Reduce redundancy by using matrix strategy on Windows and Linux workflows ([#3514](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3514)) - Add an achievement badger to the PR ([#3721](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3721)) +- Install chrome driver for functional tests from path set by environment variable `TEST_BROWSER_BINARY_PATH`([#3997](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3997)) ### 📝 Documentation diff --git a/scripts/upgrade_chromedriver.js b/scripts/upgrade_chromedriver.js index 224486bb9867..ee992706a1fc 100644 --- a/scripts/upgrade_chromedriver.js +++ b/scripts/upgrade_chromedriver.js @@ -25,17 +25,34 @@ const versionCheckCommands = []; switch (process.platform) { case 'win32': versionCheckCommands.push( - 'powershell "(Get-Item \\"$Env:Programfiles/Google/Chrome/Application/chrome.exe\\").VersionInfo.FileVersion"' + ...[ + ...(process.env.TEST_BROWSER_BINARY_PATH + ? [ + `powershell "(Get-Item \\"${process.env.TEST_BROWSER_BINARY_PATH}\\").VersionInfo.FileVersion"`, + ] + : []), + 'powershell "(Get-Item \\"$Env:Programfiles/Google/Chrome/Application/chrome.exe\\").VersionInfo.FileVersion"', + ] ); break; case 'darwin': versionCheckCommands.push( - '/Applications/Google\\ Chrome.app/Contents/MacOS/Google\\ Chrome --version' + ...[ + ...(process.env.TEST_BROWSER_BINARY_PATH + ? [`${process.env.TEST_BROWSER_BINARY_PATH} --version`] + : []), + '/Applications/Google\\ Chrome.app/Contents/MacOS/Google\\ Chrome --version', + ] ); break; default: + versionCheckCommands.push( + ...(process.env.TEST_BROWSER_BINARY_PATH + ? [`${process.env.TEST_BROWSER_BINARY_PATH} --version`] + : []) + ); versionCheckCommands.push( ...[ '/usr/bin', From 8121c9df29db8e04d2b188ec29408c0e0ce02a6d Mon Sep 17 00:00:00 2001 From: Kawika Avilla Date: Mon, 15 May 2023 11:21:18 -0700 Subject: [PATCH 25/35] [Dashboards listing] fix listing limit (#4021) Initial page size was passed to the search function instead of the listing limit causing the max amount received to be significantly less than the previously implementation. Saved objects per page is `20` by default and the listing limit per page is `1000` by default. Issue: https://github.com/opensearch-project/OpenSearch-Dashboards/issues/4017 Signed-off-by: Kawika Avilla --- CHANGELOG.md | 1 + src/plugins/dashboard/public/application/legacy_app.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ef17eb77691e..bf7df824e532 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -255,6 +255,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [Console] Fix dev tool console autocomplete not loading issue ([#3775](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3775)) - [Console] Fix dev tool console run command with query parameter error ([#3813](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3813)) - Add clarifying tooltips to header navigation ([#3573](https://github.com/opensearch-project/OpenSearch-Dashboards/issues/3573)) +- [Dashboards Listing] Fix listing limit to utilize `savedObjects:listingLimit` instead of `savedObjects:perPage` ([#4021](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4021)) ### 🚞 Infrastructure diff --git a/src/plugins/dashboard/public/application/legacy_app.js b/src/plugins/dashboard/public/application/legacy_app.js index baacc69f7802..0c10653d7f41 100644 --- a/src/plugins/dashboard/public/application/legacy_app.js +++ b/src/plugins/dashboard/public/application/legacy_app.js @@ -150,7 +150,7 @@ export function initDashboardApp(app, deps) { type: $scope.dashboardListTypes, search: search ? `${search}*` : undefined, fields: ['title', 'type', 'description', 'updated_at'], - perPage: $scope.initialPageSize, + perPage: $scope.listingLimit, page: 1, searchFields: ['title^3', 'type', 'description'], defaultSearchOperator: 'AND', From 0e25f2e6e0e906e2474e513a6784be9ac63b26da Mon Sep 17 00:00:00 2001 From: Alexei Karikov Date: Tue, 16 May 2023 05:45:57 +0600 Subject: [PATCH 26/35] [CCI] Fix EUI/OUI type errors (#3798) * Update find_test_subject imports for tests Signed-off-by: Alexei Karikov * Update to available imports for findTestSubject Signed-off-by: Alexei Karikov * Fix available import for Query and custom icon Signed-off-by: Alexei Karikov * Add changelog entry Signed-off-by: Alexei Karikov * Add ts-ignore Signed-off-by: Alexei Karikov --------- Signed-off-by: Alexei Karikov Co-authored-by: Qingyang(Abby) Hu --- CHANGELOG.md | 1 + .../public/management_app/components/field/field.test.tsx | 2 +- .../public/management_app/components/form/form.test.tsx | 2 +- .../public/management_app/components/search/search.test.tsx | 2 +- .../public/management_app/components/search/search.tsx | 2 +- .../public/application/dashboard_empty_screen.test.tsx | 2 +- .../public/application/tests/dashboard_container.test.tsx | 2 +- .../shard_failure_open_modal_button.test.tsx | 2 +- src/plugins/dev_tools/public/plugin.ts | 2 +- .../angular/context/components/action_bar/action_bar.test.tsx | 2 +- .../doc_table/components/pager/tool_bar_pager_buttons.test.tsx | 2 +- .../doc_table/components/table_header/table_header.test.tsx | 2 +- .../context_error_message/context_error_message.test.tsx | 2 +- .../discover/public/application/components/doc/doc.test.tsx | 2 +- .../application/components/doc_viewer/doc_viewer.test.tsx | 2 +- .../application/components/hits_counter/hits_counter.test.tsx | 2 +- .../components/loading_spinner/loading_spinner.test.tsx | 2 +- .../components/sidebar/discover_field_search.test.tsx | 2 +- .../application/components/sidebar/discover_sidebar.test.tsx | 2 +- .../discover/public/application/components/table/table.test.tsx | 2 +- .../components/timechart_header/timechart_header.test.tsx | 2 +- .../embeddable/public/lib/embeddables/embeddable_root.test.tsx | 2 +- .../embeddable/public/lib/panel/embeddable_panel.test.tsx | 2 +- .../panel_actions/add_panel/add_panel_flyout.test.tsx | 2 +- .../public/components/editor/controls_tab.test.tsx | 2 +- .../public/components/editor/list_control_editor.test.tsx | 2 +- .../public/components/editor/range_control_editor.test.tsx | 2 +- .../public/components/vis/input_control_vis.test.tsx | 2 +- 28 files changed, 28 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf7df824e532..f49fa05b2c6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -197,6 +197,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [Console] Replace jQuery.ajax with core.http when calling OSD APIs in console ([#3080](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3080)) - [I18n] Fix Listr type errors and error handlers ([#3629](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3629)) - [Multiple DataSource] Present the authentication type choices in a drop-down ([#3693](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3693)) +- Fix EUI/OUI type errors ([#3798](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3798)) ### 🔩 Tests diff --git a/src/plugins/advanced_settings/public/management_app/components/field/field.test.tsx b/src/plugins/advanced_settings/public/management_app/components/field/field.test.tsx index c5a8a89157fe..4f17ab378d3c 100644 --- a/src/plugins/advanced_settings/public/management_app/components/field/field.test.tsx +++ b/src/plugins/advanced_settings/public/management_app/components/field/field.test.tsx @@ -36,7 +36,7 @@ import { FieldSetting } from '../../types'; import { UiSettingsType, StringValidation } from '../../../../../../core/public'; import { notificationServiceMock, docLinksServiceMock } from '../../../../../../core/public/mocks'; -import { findTestSubject } from '@elastic/eui/lib/test'; +import { findTestSubject } from 'test_utils/helpers'; import { Field, getEditableValue } from './field'; jest.mock('brace/theme/textmate', () => 'brace/theme/textmate'); diff --git a/src/plugins/advanced_settings/public/management_app/components/form/form.test.tsx b/src/plugins/advanced_settings/public/management_app/components/form/form.test.tsx index 650d8b259f0c..7f22894fb8a6 100644 --- a/src/plugins/advanced_settings/public/management_app/components/form/form.test.tsx +++ b/src/plugins/advanced_settings/public/management_app/components/form/form.test.tsx @@ -32,7 +32,7 @@ import React from 'react'; import { shallowWithI18nProvider, mountWithI18nProvider } from 'test_utils/enzyme_helpers'; import { UiSettingsType } from '../../../../../../core/public'; -import { findTestSubject } from '@elastic/eui/lib/test'; +import { findTestSubject } from 'test_utils/helpers'; import { notificationServiceMock } from '../../../../../../core/public/mocks'; import { SettingsChanges } from '../../types'; diff --git a/src/plugins/advanced_settings/public/management_app/components/search/search.test.tsx b/src/plugins/advanced_settings/public/management_app/components/search/search.test.tsx index 34b69888b3be..f362af408e0c 100644 --- a/src/plugins/advanced_settings/public/management_app/components/search/search.test.tsx +++ b/src/plugins/advanced_settings/public/management_app/components/search/search.test.tsx @@ -31,7 +31,7 @@ import React from 'react'; import { shallowWithI18nProvider, mountWithI18nProvider } from 'test_utils/enzyme_helpers'; -import { findTestSubject } from '@elastic/eui/lib/test'; +import { findTestSubject } from 'test_utils/helpers'; import { Query } from '@elastic/eui'; import { Search } from './search'; diff --git a/src/plugins/advanced_settings/public/management_app/components/search/search.tsx b/src/plugins/advanced_settings/public/management_app/components/search/search.tsx index a957fa793f8f..0b1de3bbfb24 100644 --- a/src/plugins/advanced_settings/public/management_app/components/search/search.tsx +++ b/src/plugins/advanced_settings/public/management_app/components/search/search.tsx @@ -31,7 +31,6 @@ import React, { Fragment, PureComponent } from 'react'; import { i18n } from '@osd/i18n'; import { EuiSearchBar, EuiFormErrorText, Query } from '@elastic/eui'; - import { getCategoryName } from '../../lib'; interface SearchProps { @@ -111,6 +110,7 @@ export class Search extends PureComponent { return ( + {/* @ts-ignore The Query types that typescript complains about here are identical and is a false flag. Once OUI migration is complete, this ignore can be removed */} {queryParseError} diff --git a/src/plugins/dashboard/public/application/dashboard_empty_screen.test.tsx b/src/plugins/dashboard/public/application/dashboard_empty_screen.test.tsx index fd4291b29455..ac22b069135c 100644 --- a/src/plugins/dashboard/public/application/dashboard_empty_screen.test.tsx +++ b/src/plugins/dashboard/public/application/dashboard_empty_screen.test.tsx @@ -31,7 +31,7 @@ import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { DashboardEmptyScreen, DashboardEmptyScreenProps } from './dashboard_empty_screen'; -import { findTestSubject } from '@elastic/eui/lib/test'; +import { findTestSubject } from 'test_utils/helpers'; import { coreMock } from '../../../../core/public/mocks'; describe('DashboardEmptyScreen', () => { diff --git a/src/plugins/dashboard/public/application/tests/dashboard_container.test.tsx b/src/plugins/dashboard/public/application/tests/dashboard_container.test.tsx index d3198f6e1e31..2746997947c9 100644 --- a/src/plugins/dashboard/public/application/tests/dashboard_container.test.tsx +++ b/src/plugins/dashboard/public/application/tests/dashboard_container.test.tsx @@ -28,7 +28,7 @@ * under the License. */ -import { findTestSubject } from '@elastic/eui/lib/test'; +import { findTestSubject } from 'test_utils/helpers'; import React from 'react'; import { mount } from 'enzyme'; import { nextTick } from 'test_utils/enzyme_helpers'; diff --git a/src/plugins/data/public/ui/shard_failure_modal/shard_failure_open_modal_button.test.tsx b/src/plugins/data/public/ui/shard_failure_modal/shard_failure_open_modal_button.test.tsx index 1ff83cf474c2..9a6b71ae6459 100644 --- a/src/plugins/data/public/ui/shard_failure_modal/shard_failure_open_modal_button.test.tsx +++ b/src/plugins/data/public/ui/shard_failure_modal/shard_failure_open_modal_button.test.tsx @@ -34,7 +34,7 @@ import { mountWithIntl } from 'test_utils/enzyme_helpers'; import ShardFailureOpenModalButton from './shard_failure_open_modal_button'; import { shardFailureRequest } from './__mocks__/shard_failure_request'; import { shardFailureResponse } from './__mocks__/shard_failure_response'; -import { findTestSubject } from '@elastic/eui/lib/test'; +import { findTestSubject } from 'test_utils/helpers'; describe('ShardFailureOpenModalButton', () => { it('triggers the openModal function when "Show details" button is clicked', () => { diff --git a/src/plugins/dev_tools/public/plugin.ts b/src/plugins/dev_tools/public/plugin.ts index 0c0b3b07f5c1..02663da57686 100644 --- a/src/plugins/dev_tools/public/plugin.ts +++ b/src/plugins/dev_tools/public/plugin.ts @@ -78,7 +78,7 @@ export class DevToolsPlugin implements Plugin { defaultMessage: 'Dev Tools', }), updater$: this.appStateUpdater, - euiIconType: '/plugins/home/assets/logos/opensearch_mark_default.svg', + icon: '/plugins/home/public/assets/logos/opensearch_mark_default.svg', order: 9010, category: DEFAULT_APP_CATEGORIES.management, mount: async (params: AppMountParameters) => { diff --git a/src/plugins/discover/public/application/angular/context/components/action_bar/action_bar.test.tsx b/src/plugins/discover/public/application/angular/context/components/action_bar/action_bar.test.tsx index db5abf3dbed4..2f7cc40b7d9a 100644 --- a/src/plugins/discover/public/application/angular/context/components/action_bar/action_bar.test.tsx +++ b/src/plugins/discover/public/application/angular/context/components/action_bar/action_bar.test.tsx @@ -31,7 +31,7 @@ import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { ActionBar, ActionBarProps } from './action_bar'; -import { findTestSubject } from '@elastic/eui/lib/test'; +import { findTestSubject } from 'test_utils/helpers'; import { MAX_CONTEXT_SIZE, MIN_CONTEXT_SIZE } from '../../query_parameters/constants'; describe('Test Discover Context ActionBar for successor | predecessor records', () => { diff --git a/src/plugins/discover/public/application/angular/doc_table/components/pager/tool_bar_pager_buttons.test.tsx b/src/plugins/discover/public/application/angular/doc_table/components/pager/tool_bar_pager_buttons.test.tsx index 204d0b161c68..2ac06b2b6ebf 100644 --- a/src/plugins/discover/public/application/angular/doc_table/components/pager/tool_bar_pager_buttons.test.tsx +++ b/src/plugins/discover/public/application/angular/doc_table/components/pager/tool_bar_pager_buttons.test.tsx @@ -31,7 +31,7 @@ import React from 'react'; import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; import { ToolBarPagerButtons } from './tool_bar_pager_buttons'; -import { findTestSubject } from '@elastic/eui/lib/test'; +import { findTestSubject } from 'test_utils/helpers'; test('it renders ToolBarPagerButtons', () => { const props = { diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_header/table_header.test.tsx b/src/plugins/discover/public/application/angular/doc_table/components/table_header/table_header.test.tsx index 2e486976bbdf..0e8efc18efd5 100644 --- a/src/plugins/discover/public/application/angular/doc_table/components/table_header/table_header.test.tsx +++ b/src/plugins/discover/public/application/angular/doc_table/components/table_header/table_header.test.tsx @@ -31,7 +31,7 @@ import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { TableHeader } from './table_header'; -import { findTestSubject } from '@elastic/eui/lib/test'; +import { findTestSubject } from 'test_utils/helpers'; import { SortOrder } from './helpers'; import { IndexPattern, IFieldType } from '../../../../../opensearch_dashboards_services'; diff --git a/src/plugins/discover/public/application/components/context_error_message/context_error_message.test.tsx b/src/plugins/discover/public/application/components/context_error_message/context_error_message.test.tsx index 30b57eaf0dc5..a1ef06b81cf2 100644 --- a/src/plugins/discover/public/application/components/context_error_message/context_error_message.test.tsx +++ b/src/plugins/discover/public/application/components/context_error_message/context_error_message.test.tsx @@ -34,7 +34,7 @@ import { ReactWrapper } from 'enzyme'; import { ContextErrorMessage } from './context_error_message'; // @ts-ignore import { FAILURE_REASONS, LOADING_STATUS } from '../../angular/context/query'; -import { findTestSubject } from '@elastic/eui/lib/test'; +import { findTestSubject } from 'test_utils/helpers'; describe('loading spinner', function () { let component: ReactWrapper; diff --git a/src/plugins/discover/public/application/components/doc/doc.test.tsx b/src/plugins/discover/public/application/components/doc/doc.test.tsx index 7385f0d360a1..4a3fb740492a 100644 --- a/src/plugins/discover/public/application/components/doc/doc.test.tsx +++ b/src/plugins/discover/public/application/components/doc/doc.test.tsx @@ -33,7 +33,7 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { ReactWrapper } from 'enzyme'; -import { findTestSubject } from '@elastic/eui/lib/test'; +import { findTestSubject } from 'test_utils/helpers'; import { Doc, DocProps } from './doc'; const mockSearchApi = jest.fn(); diff --git a/src/plugins/discover/public/application/components/doc_viewer/doc_viewer.test.tsx b/src/plugins/discover/public/application/components/doc_viewer/doc_viewer.test.tsx index 843ad5788253..ccab0be41ed2 100644 --- a/src/plugins/discover/public/application/components/doc_viewer/doc_viewer.test.tsx +++ b/src/plugins/discover/public/application/components/doc_viewer/doc_viewer.test.tsx @@ -31,7 +31,7 @@ import React from 'react'; import { mount, shallow } from 'enzyme'; import { DocViewer } from './doc_viewer'; -import { findTestSubject } from '@elastic/eui/lib/test'; +import { findTestSubject } from 'test_utils/helpers'; import { getDocViewsRegistry } from '../../../opensearch_dashboards_services'; import { DocViewRenderProps } from '../../doc_views/doc_views_types'; diff --git a/src/plugins/discover/public/application/components/hits_counter/hits_counter.test.tsx b/src/plugins/discover/public/application/components/hits_counter/hits_counter.test.tsx index 05435ccae99e..998ababbc47f 100644 --- a/src/plugins/discover/public/application/components/hits_counter/hits_counter.test.tsx +++ b/src/plugins/discover/public/application/components/hits_counter/hits_counter.test.tsx @@ -32,7 +32,7 @@ import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { ReactWrapper } from 'enzyme'; import { HitsCounter, HitsCounterProps } from './hits_counter'; -import { findTestSubject } from '@elastic/eui/lib/test'; +import { findTestSubject } from 'test_utils/helpers'; describe('hits counter', function () { let props: HitsCounterProps; diff --git a/src/plugins/discover/public/application/components/loading_spinner/loading_spinner.test.tsx b/src/plugins/discover/public/application/components/loading_spinner/loading_spinner.test.tsx index 5bc6aa29136b..fbc98e2550e0 100644 --- a/src/plugins/discover/public/application/components/loading_spinner/loading_spinner.test.tsx +++ b/src/plugins/discover/public/application/components/loading_spinner/loading_spinner.test.tsx @@ -32,7 +32,7 @@ import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { ReactWrapper } from 'enzyme'; import { LoadingSpinner } from './loading_spinner'; -import { findTestSubject } from '@elastic/eui/lib/test'; +import { findTestSubject } from 'test_utils/helpers'; describe('loading spinner', function () { let component: ReactWrapper; diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field_search.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field_search.test.tsx index 9170adccc7e7..f78505e11f1e 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field_search.test.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_field_search.test.tsx @@ -31,7 +31,7 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { findTestSubject } from '@elastic/eui/lib/test'; +import { findTestSubject } from 'test_utils/helpers'; import { DiscoverFieldSearch, Props } from './discover_field_search'; import { EuiButtonGroupProps, EuiPopover } from '@elastic/eui'; import { ReactWrapper } from 'enzyme'; diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx index dbc8c8962466..fa692ca22b5b 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx @@ -30,7 +30,7 @@ import _ from 'lodash'; import { ReactWrapper } from 'enzyme'; -import { findTestSubject } from '@elastic/eui/lib/test'; +import { findTestSubject } from 'test_utils/helpers'; // @ts-ignore import realHits from 'fixtures/real_hits.js'; // @ts-ignore diff --git a/src/plugins/discover/public/application/components/table/table.test.tsx b/src/plugins/discover/public/application/components/table/table.test.tsx index bb10fc137bcb..220ac57feae2 100644 --- a/src/plugins/discover/public/application/components/table/table.test.tsx +++ b/src/plugins/discover/public/application/components/table/table.test.tsx @@ -30,7 +30,7 @@ import React from 'react'; import { mount } from 'enzyme'; -import { findTestSubject } from '@elastic/eui/lib/test'; +import { findTestSubject } from 'test_utils/helpers'; import { DocViewTable } from './table'; import { indexPatterns, IndexPattern } from '../../../../../data/public'; diff --git a/src/plugins/discover/public/application/components/timechart_header/timechart_header.test.tsx b/src/plugins/discover/public/application/components/timechart_header/timechart_header.test.tsx index 4a88e469a086..9011c38a6acb 100644 --- a/src/plugins/discover/public/application/components/timechart_header/timechart_header.test.tsx +++ b/src/plugins/discover/public/application/components/timechart_header/timechart_header.test.tsx @@ -33,7 +33,7 @@ import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { ReactWrapper } from 'enzyme'; import { TimechartHeader, TimechartHeaderProps } from './timechart_header'; import { EuiIconTip } from '@elastic/eui'; -import { findTestSubject } from '@elastic/eui/lib/test'; +import { findTestSubject } from 'test_utils/helpers'; describe('timechart header', function () { let props: TimechartHeaderProps; diff --git a/src/plugins/embeddable/public/lib/embeddables/embeddable_root.test.tsx b/src/plugins/embeddable/public/lib/embeddables/embeddable_root.test.tsx index badeda5d4aef..486e99b1c281 100644 --- a/src/plugins/embeddable/public/lib/embeddables/embeddable_root.test.tsx +++ b/src/plugins/embeddable/public/lib/embeddables/embeddable_root.test.tsx @@ -32,7 +32,7 @@ import React from 'react'; import { HelloWorldEmbeddable } from '../../../../../../examples/embeddable_examples/public'; import { EmbeddableRoot } from './embeddable_root'; import { mount } from 'enzyme'; -import { findTestSubject } from '@elastic/eui/lib/test'; +import { findTestSubject } from 'test_utils/helpers'; test('EmbeddableRoot renders an embeddable', async () => { const embeddable = new HelloWorldEmbeddable({ id: 'hello' }); diff --git a/src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx b/src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx index 072bcd93d32f..4db6b30c9b57 100644 --- a/src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx +++ b/src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx @@ -32,7 +32,7 @@ import React from 'react'; import { mount } from 'enzyme'; import { nextTick } from 'test_utils/enzyme_helpers'; -import { findTestSubject } from '@elastic/eui/lib/test'; +import { findTestSubject } from 'test_utils/helpers'; import { I18nProvider } from '@osd/i18n/react'; import { CONTEXT_MENU_TRIGGER } from '../triggers'; import { Action, UiActionsStart, ActionType } from '../../../../ui_actions/public'; diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.test.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.test.tsx index 232c2ca82d15..ac7d6e7aa082 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.test.tsx +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.test.tsx @@ -41,7 +41,7 @@ import { ContainerInput } from '../../../../containers'; import { mountWithIntl as mount } from 'test_utils/enzyme_helpers'; import { ReactWrapper } from 'enzyme'; import { coreMock } from '../../../../../../../../core/public/mocks'; -import { findTestSubject } from '@elastic/eui/lib/test'; +import { findTestSubject } from 'test_utils/helpers'; import { embeddablePluginMock } from '../../../../../mocks'; function DummySavedObjectFinder(props: { children: React.ReactNode }) { diff --git a/src/plugins/input_control_vis/public/components/editor/controls_tab.test.tsx b/src/plugins/input_control_vis/public/components/editor/controls_tab.test.tsx index e2355799d72d..06ed4a66606c 100644 --- a/src/plugins/input_control_vis/public/components/editor/controls_tab.test.tsx +++ b/src/plugins/input_control_vis/public/components/editor/controls_tab.test.tsx @@ -30,7 +30,7 @@ import React from 'react'; import { shallowWithIntl, mountWithIntl } from 'test_utils/enzyme_helpers'; -import { findTestSubject } from '@elastic/eui/lib/test'; +import { findTestSubject } from 'test_utils/helpers'; import { getDepsMock, getIndexPatternMock } from '../../test_utils'; import { ControlsTab, ControlsTabUiProps } from './controls_tab'; import { Vis } from '../../../../visualizations/public'; diff --git a/src/plugins/input_control_vis/public/components/editor/list_control_editor.test.tsx b/src/plugins/input_control_vis/public/components/editor/list_control_editor.test.tsx index d8a36ac9b6c9..6837c14fafd2 100644 --- a/src/plugins/input_control_vis/public/components/editor/list_control_editor.test.tsx +++ b/src/plugins/input_control_vis/public/components/editor/list_control_editor.test.tsx @@ -32,7 +32,7 @@ import React from 'react'; import sinon from 'sinon'; import { shallow } from 'enzyme'; -import { findTestSubject } from '@elastic/eui/lib/test'; +import { findTestSubject } from 'test_utils/helpers'; import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; import { getIndexPatternMock } from '../../test_utils/get_index_pattern_mock'; diff --git a/src/plugins/input_control_vis/public/components/editor/range_control_editor.test.tsx b/src/plugins/input_control_vis/public/components/editor/range_control_editor.test.tsx index a8dcc0761f2c..76b1e15a9df7 100644 --- a/src/plugins/input_control_vis/public/components/editor/range_control_editor.test.tsx +++ b/src/plugins/input_control_vis/public/components/editor/range_control_editor.test.tsx @@ -33,7 +33,7 @@ import { shallow } from 'enzyme'; import { SinonSpy, spy, assert } from 'sinon'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { findTestSubject } from '@elastic/eui/lib/test'; +import { findTestSubject } from 'test_utils/helpers'; import { RangeControlEditor } from './range_control_editor'; import { ControlParams } from '../../editor_utils'; diff --git a/src/plugins/input_control_vis/public/components/vis/input_control_vis.test.tsx b/src/plugins/input_control_vis/public/components/vis/input_control_vis.test.tsx index 1e931c084a55..ea1ef17d79f4 100644 --- a/src/plugins/input_control_vis/public/components/vis/input_control_vis.test.tsx +++ b/src/plugins/input_control_vis/public/components/vis/input_control_vis.test.tsx @@ -32,7 +32,7 @@ import React from 'react'; import sinon from 'sinon'; import { shallow } from 'enzyme'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { findTestSubject } from '@elastic/eui/lib/test'; +import { findTestSubject } from 'test_utils/helpers'; import { InputControlVis } from './input_control_vis'; import { ListControl } from '../../control/list_control_factory'; From 87e795123a36958e7c2596a12f58d775bba2beff Mon Sep 17 00:00:00 2001 From: Sergey Myssak Date: Tue, 16 May 2023 06:18:59 +0600 Subject: [PATCH 27/35] Fix bottom bar visibility using create portal (#3336) (#3978) Signed-off-by: Sergey Myssak Co-authored-by: Andrey Myssak --- CHANGELOG.md | 1 + src/core/public/rendering/_base.scss | 1 - .../public/rendering/app_containers.test.tsx | 3 + src/core/public/rendering/app_containers.tsx | 6 +- .../management_app/_advanced_settings.scss | 4 - .../components/form/form.test.tsx | 9 ++ .../management_app/components/form/form.tsx | 121 +++++++++--------- 7 files changed, 79 insertions(+), 66 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f49fa05b2c6f..0e7670ad7a16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -134,6 +134,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [Table Visualization] Fix table rendering empty unused space ([#3797](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3797)) - [Table Visualization] Fix data table not adjusting height on the initial load ([#3816](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3816)) - Cleanup unused url ([#3847](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3847)) +- [BUG] Docked navigation impacts visibility of bottom bar component ([#3978](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3978)) ### 🚞 Infrastructure diff --git a/src/core/public/rendering/_base.scss b/src/core/public/rendering/_base.scss index e59008f08259..1333e48f6ca5 100644 --- a/src/core/public/rendering/_base.scss +++ b/src/core/public/rendering/_base.scss @@ -5,7 +5,6 @@ */ // SASSTODO: Naming here is too embedded and high up that changing them could cause major breaks #opensearch-dashboards-body { - overflow-x: hidden; min-height: 100%; } diff --git a/src/core/public/rendering/app_containers.test.tsx b/src/core/public/rendering/app_containers.test.tsx index fd43c79514c1..157253f5f757 100644 --- a/src/core/public/rendering/app_containers.test.tsx +++ b/src/core/public/rendering/app_containers.test.tsx @@ -43,6 +43,7 @@ describe('AppWrapper', () => { expect(component.getDOMNode()).toMatchInlineSnapshot(`

app-content
@@ -53,6 +54,7 @@ describe('AppWrapper', () => { expect(component.getDOMNode()).toMatchInlineSnapshot(`
app-content
@@ -63,6 +65,7 @@ describe('AppWrapper', () => { expect(component.getDOMNode()).toMatchInlineSnapshot(`
app-content
diff --git a/src/core/public/rendering/app_containers.tsx b/src/core/public/rendering/app_containers.tsx index dab85769bd0b..8ccf795f8dcf 100644 --- a/src/core/public/rendering/app_containers.tsx +++ b/src/core/public/rendering/app_containers.tsx @@ -37,7 +37,11 @@ export const AppWrapper: React.FunctionComponent<{ chromeVisible$: Observable; }> = ({ chromeVisible$, children }) => { const visible = useObservable(chromeVisible$); - return
{children}
; + return ( +
+ {children} +
+ ); }; export const AppContainer: React.FunctionComponent<{ diff --git a/src/plugins/advanced_settings/public/management_app/_advanced_settings.scss b/src/plugins/advanced_settings/public/management_app/_advanced_settings.scss index 82704fc93062..a33082c3ef64 100644 --- a/src/plugins/advanced_settings/public/management_app/_advanced_settings.scss +++ b/src/plugins/advanced_settings/public/management_app/_advanced_settings.scss @@ -76,7 +76,3 @@ .mgtAdvancedSettingsForm__button { width: 100%; } - -.osdBody--mgtAdvancedSettingsHasBottomBar .mgtPage__body { - padding-bottom: $euiSizeXL * 2; -} diff --git a/src/plugins/advanced_settings/public/management_app/components/form/form.test.tsx b/src/plugins/advanced_settings/public/management_app/components/form/form.test.tsx index 7f22894fb8a6..a0edaa5ab602 100644 --- a/src/plugins/advanced_settings/public/management_app/components/form/form.test.tsx +++ b/src/plugins/advanced_settings/public/management_app/components/form/form.test.tsx @@ -29,6 +29,7 @@ */ import React from 'react'; +import ReactDOM from 'react-dom'; import { shallowWithI18nProvider, mountWithI18nProvider } from 'test_utils/enzyme_helpers'; import { UiSettingsType } from '../../../../../../core/public'; @@ -38,6 +39,11 @@ import { notificationServiceMock } from '../../../../../../core/public/mocks'; import { SettingsChanges } from '../../types'; import { Form } from './form'; +jest.mock('react-dom', () => ({ + ...jest.requireActual('react-dom'), + createPortal: jest.fn((element) => element), +})); + jest.mock('../field', () => ({ Field: () => { return 'field'; @@ -45,6 +51,8 @@ jest.mock('../field', () => ({ })); beforeAll(() => { + ReactDOM.createPortal = jest.fn((children: any) => children); + const localStorage: Record = { 'core.chrome.isLocked': true, }; @@ -60,6 +68,7 @@ beforeAll(() => { }); afterAll(() => { + (ReactDOM.createPortal as jest.Mock).mockClear(); delete (window as any).localStorage; }); diff --git a/src/plugins/advanced_settings/public/management_app/components/form/form.tsx b/src/plugins/advanced_settings/public/management_app/components/form/form.tsx index 92b7a792d2d2..a74199771d2a 100644 --- a/src/plugins/advanced_settings/public/management_app/components/form/form.tsx +++ b/src/plugins/advanced_settings/public/management_app/components/form/form.tsx @@ -47,8 +47,9 @@ import { import { FormattedMessage } from '@osd/i18n/react'; import { isEmpty } from 'lodash'; import { i18n } from '@osd/i18n'; +import { DocLinksStart, ToastsStart } from 'opensearch-dashboards/public'; +import { createPortal } from 'react-dom'; import { toMountPoint } from '../../../../../opensearch_dashboards_react/public'; -import { DocLinksStart, ToastsStart } from '../../../../../../core/public'; import { getCategoryName } from '../../lib'; import { Field, getEditableValue } from '../field'; @@ -336,63 +337,69 @@ export class Form extends PureComponent { }; renderBottomBar = () => { - const areChangesInvalid = this.areChangesInvalid(); - return ( - - - -

{this.renderCountOfUnsaved()}

-
- - - - {i18n.translate('advancedSettings.form.cancelButtonLabel', { - defaultMessage: 'Cancel changes', - })} - - - - - + + +

{this.renderCountOfUnsaved()}

+
+ + + - {i18n.translate('advancedSettings.form.saveButtonLabel', { - defaultMessage: 'Save changes', + {i18n.translate('advancedSettings.form.cancelButtonLabel', { + defaultMessage: 'Cancel changes', })} -
-
-
-
-
- ); + + + + + + {i18n.translate('advancedSettings.form.saveButtonLabel', { + defaultMessage: 'Save changes', + })} + + + + + + ); + + return createPortal(bottomBar, document.getElementById('app-wrapper')!); + } catch (e) { + return null; + } }; render() { @@ -401,12 +408,6 @@ export class Form extends PureComponent { const currentCategories: Category[] = []; const hasUnsavedChanges = !isEmpty(unsavedChanges); - if (hasUnsavedChanges) { - document.body.classList.add('osdBody--mgtAdvancedSettingsHasBottomBar'); - } else { - document.body.classList.remove('osdBody--mgtAdvancedSettingsHasBottomBar'); - } - categories.forEach((category) => { if (visibleSettings[category] && visibleSettings[category].length) { currentCategories.push(category); From 69b185461d64ce9172cd145d607b731b35079060 Mon Sep 17 00:00:00 2001 From: Ashwin P Chandran Date: Tue, 16 May 2023 09:29:50 -0700 Subject: [PATCH 28/35] Adds threshold to code coverage changes for project (#4040) * Fixes code coverage workflow failures for the project test due to inderect flakey changes Signed-off-by: Ashwin P Chandran * Adds changelog Signed-off-by: Ashwin P Chandran --------- Signed-off-by: Ashwin P Chandran --- .github/.codecov.yml | 8 ++++++-- CHANGELOG.md | 1 + 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/.codecov.yml b/.github/.codecov.yml index 1ce321b650d3..ba7dd34e7473 100644 --- a/.github/.codecov.yml +++ b/.github/.codecov.yml @@ -1,7 +1,11 @@ # https://docs.codecov.com/docs/codecov-yaml +codecov: + require_ci_to_pass: yes + coverage: status: project: default: - # https://docs.codecov.com/docs/commit-status#target - target: auto # coverage must be equal or above the previous commit + target: auto + threshold: 2% # the leniency in hitting the target + diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e7670ad7a16..3ae939521a3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -150,6 +150,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [CI] Reduce redundancy by using matrix strategy on Windows and Linux workflows ([#3514](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3514)) - Add an achievement badger to the PR ([#3721](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3721)) - Install chrome driver for functional tests from path set by environment variable `TEST_BROWSER_BINARY_PATH`([#3997](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3997)) +- Adds threshold to code coverage config to prevent workflow failures ([#4040](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4040)) ### 📝 Documentation From 873b7f3c7c946972c7529c21159881b45b2b4fc3 Mon Sep 17 00:00:00 2001 From: Ashwin P Chandran Date: Tue, 16 May 2023 09:38:46 -0700 Subject: [PATCH 29/35] Updates PR template for screenshots and test instructions (#4042) Signed-off-by: Ashwin P Chandran --- .github/pull_request_template.md | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 9bd61e660b6c..2a9e814b5fe3 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -4,8 +4,20 @@ ### Issues Resolved - - + + + +## Screenshot + + + +## Testing the changes + + ### Check List From 2c33d574672d26c898d4c5d6759ace168111c9a7 Mon Sep 17 00:00:00 2001 From: Anan Zhuang Date: Tue, 16 May 2023 14:55:07 -0700 Subject: [PATCH 30/35] Replace re2 with RegExp in timeline and add unit tests (#3908) Remove re2 usage and replace it with JavaScript built-in RegExp object. Also add more unit tests to make sure that using RegExp has same expressions as using re2 library. Issue Resolve https://github.com/opensearch-project/OpenSearch-Dashboards/issues/3901 Signed-off-by: Anan Zhuang --- CHANGELOG.md | 1 + package.json | 1 - .../tasks/patch_native_modules_task.test.ts | 116 +++++--- .../build/tasks/patch_native_modules_task.ts | 69 ++--- .../server/series_functions/label.js | 6 +- .../server/series_functions/label.test.js | 20 ++ yarn.lock | 258 ++---------------- 7 files changed, 138 insertions(+), 333 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ae939521a3c..17c86ec89bb8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -247,6 +247,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [Multi DataSource] UX enhancement on Data source management stack ([#2521](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2521)) - [Multi DataSource] UX enhancement on Update stored password modal for Data source management stack ([#2532](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2532)) - [Monaco editor] Add json worker support ([#3424](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3424)) +- Replace re2 with RegExp in timeline and add unit tests ([#3908](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3908)) ### 🐛 Bug Fixes diff --git a/package.json b/package.json index 2b894a95897a..676bd6cb653c 100644 --- a/package.json +++ b/package.json @@ -200,7 +200,6 @@ "pegjs": "0.10.0", "proxy-from-env": "1.0.0", "query-string": "^6.13.2", - "re2": "1.17.4", "react": "^16.14.0", "react-dom": "^16.12.0", "react-input-range": "^1.3.0", diff --git a/src/dev/build/tasks/patch_native_modules_task.test.ts b/src/dev/build/tasks/patch_native_modules_task.test.ts index f3a3daa432c3..71553d39cd9c 100644 --- a/src/dev/build/tasks/patch_native_modules_task.test.ts +++ b/src/dev/build/tasks/patch_native_modules_task.test.ts @@ -9,8 +9,8 @@ import { createAnyInstanceSerializer, createAbsolutePathSerializer, } from '@osd/dev-utils'; -import { Build, Config } from '../lib'; -import { PatchNativeModules } from './patch_native_modules_task'; +import { Build, Config, read, download, untar, gunzip } from '../lib'; +import { createPatchNativeModulesTask } from './patch_native_modules_task'; const log = new ToolingLog(); const testWriter = new ToolingLogCollectingWriter(); @@ -19,16 +19,16 @@ expect.addSnapshotSerializer(createAnyInstanceSerializer(Config)); expect.addSnapshotSerializer(createAnyInstanceSerializer(ToolingLog)); expect.addSnapshotSerializer(createAbsolutePathSerializer()); -jest.mock('../lib/download'); -jest.mock('../lib/fs', () => ({ - ...jest.requireActual('../lib/fs'), - untar: jest.fn(), - gunzip: jest.fn(), -})); - -const { untar } = jest.requireMock('../lib/fs'); -const { gunzip } = jest.requireMock('../lib/fs'); -const { download } = jest.requireMock('../lib/download'); +jest.mock('../lib', () => { + const originalModule = jest.requireActual('../lib'); + return { + ...originalModule, + download: jest.fn(), + gunzip: jest.fn(), + untar: jest.fn(), + read: jest.fn(), + }; +}); async function setup() { const config = await Config.create({ @@ -38,14 +38,15 @@ async function setup() { linux: false, linuxArm: false, darwin: false, + windows: false, }, }); const build = new Build(config); - download.mockImplementation(() => {}); - untar.mockImplementation(() => {}); - gunzip.mockImplementation(() => {}); + (read as jest.MockedFunction).mockImplementation(async () => { + return JSON.stringify({ version: mockPackage.version }); + }); return { config, build }; } @@ -55,38 +56,77 @@ beforeEach(() => { jest.clearAllMocks(); }); -it('patch native modules task downloads the correct platform package', async () => { - const { config, build } = await setup(); - config.targetPlatforms.linuxArm = true; - await PatchNativeModules.run(config, log, build); - expect(download.mock.calls.length).toBe(1); - expect(download.mock.calls).toMatchInlineSnapshot(` +const mockPackage = { + name: 'mock-native-module', + version: '1.0.0', + destinationPath: 'path/to/destination', + extractMethod: 'untar', + archives: { + 'linux-arm64': { + url: 'https://example.com/mock-native-module/linux-arm64.tar.gz', + sha256: 'mock-sha256', + }, + 'linux-x64': { + url: 'https://example.com/mock-native-module/linux-x64.gz', + sha256: 'mock-sha256', + }, + }, +}; + +describe('patch native modules task', () => { + it('patch native modules task downloads the correct platform package', async () => { + const { config, build } = await setup(); + config.targetPlatforms.linuxArm = true; + const PatchNativeModulesWithMock = createPatchNativeModulesTask([mockPackage]); + await PatchNativeModulesWithMock.run(config, log, build); + expect((download as jest.MockedFunction).mock.calls.length).toBe(1); + expect((download as jest.MockedFunction).mock.calls).toMatchInlineSnapshot(` Array [ Array [ Object { - "destination": /.native_modules/re2/linux-arm64-83.tar.gz, + "destination": /.native_modules/mock-native-module/linux-arm64.tar.gz, "log": , "retries": 3, - "sha256": "d86ced75b794fbf518b90908847b3c09a50f3ff5a2815aa30f53080f926a2873", - "url": "https://d1v1sj258etie.cloudfront.net/node-re2/releases/download/1.17.4/linux-arm64-83.tar.gz", + "sha256": "mock-sha256", + "url": "https://example.com/mock-native-module/linux-arm64.tar.gz", }, ], ] `); -}); + }); -it('for .tar.gz artifact, patch native modules task unzip it via untar', async () => { - const { config, build } = await setup(); - config.targetPlatforms.linuxArm = true; - await PatchNativeModules.run(config, log, build); - expect(untar.mock.calls.length).toBe(1); - expect(gunzip.mock.calls.length).toBe(0); -}); + it('for .tar.gz artifact, patch native modules task unzip it via untar', async () => { + const { config, build } = await setup(); + config.targetPlatforms.linuxArm = true; + const PatchNativeModulesWithMock = createPatchNativeModulesTask([mockPackage]); + await PatchNativeModulesWithMock.run(config, log, build); + expect(untar).toHaveBeenCalled(); + expect(gunzip).not.toHaveBeenCalled(); + }); -it('for .gz artifact, patch native modules task unzip it via gunzip', async () => { - const { config, build } = await setup(); - config.targetPlatforms.linux = true; - await PatchNativeModules.run(config, log, build); - expect(untar.mock.calls.length).toBe(0); - expect(gunzip.mock.calls.length).toBe(1); + it('for .gz artifact, patch native modules task unzip it via gunzip', async () => { + const mockPackageGZ = { + ...mockPackage, + extractMethod: 'gunzip', + }; + const { config, build } = await setup(); + config.targetPlatforms.linux = true; + const PatchNativeModulesWithMock = createPatchNativeModulesTask([mockPackageGZ]); + await PatchNativeModulesWithMock.run(config, log, build); + expect(gunzip).toHaveBeenCalled(); + expect(untar).not.toHaveBeenCalled(); + }); + + it('throws error for unsupported extract methods', async () => { + const mockPackageUnsupported = { + ...mockPackage, + extractMethod: 'unsupported', + }; + const { config, build } = await setup(); + config.targetPlatforms.linux = true; + const PatchNativeModulesWithMock = createPatchNativeModulesTask([mockPackageUnsupported]); + await expect(PatchNativeModulesWithMock.run(config, log, build)).rejects.toThrow( + 'Extract method of unsupported is not supported' + ); + }); }); diff --git a/src/dev/build/tasks/patch_native_modules_task.ts b/src/dev/build/tasks/patch_native_modules_task.ts index 3bd9fa63c358..b8c8d8a5b9fb 100644 --- a/src/dev/build/tasks/patch_native_modules_task.ts +++ b/src/dev/build/tasks/patch_native_modules_task.ts @@ -52,45 +52,7 @@ interface Package { >; } -/* Process for updating URLs and checksums after bumping the version of `re2` or NodeJS: - * 1. Match the `version` with the version in the yarn.lock file. - * 2. Match the module version, the digits at the end of the filename, with the output of - * `node -p process.versions.modules`. - * 3. Confirm that the URLs exist for each platform-architecture combo on - * https://github.com/uhop/node-re2/releases/tag/[VERSION]; reach out to maintainers for ARM - * releases of `re2` as they currently don't have an official ARM release. - * 4. Generate new checksums for each artifact by downloading each one and calling - * `shasum -a 256` or `sha256sum` on the downloaded file. - */ -const packages: Package[] = [ - { - name: 're2', - version: '1.17.4', - destinationPath: 'node_modules/re2/build/Release/re2.node', - extractMethod: 'gunzip', - archives: { - 'darwin-x64': { - url: 'https://github.com/uhop/node-re2/releases/download/1.17.4/darwin-x64-83.gz', - sha256: '9112ed93c1544ecc6397f7ff20bd2b28f3b04c7fbb54024e10f9a376a132a87d', - }, - 'linux-x64': { - url: 'https://github.com/uhop/node-re2/releases/download/1.17.4/linux-x64-83.gz', - sha256: '86e03540783a18c41f81df0aec320b1f64aca6cbd3a87fc1b7a9b4109c5f5986', - }, - 'linux-arm64': { - url: - 'https://d1v1sj258etie.cloudfront.net/node-re2/releases/download/1.17.4/linux-arm64-83.tar.gz', - sha256: 'd86ced75b794fbf518b90908847b3c09a50f3ff5a2815aa30f53080f926a2873', - overriddenExtractMethod: 'untar', - overriddenDestinationPath: 'node_modules/re2/build/Release', - }, - 'win32-x64': { - url: 'https://github.com/uhop/node-re2/releases/download/1.17.4/win32-x64-83.gz', - sha256: '2f842d9757288afd4bd5dec0e7b370a4c3e89ac98050598b17abb9e8e00e3294', - }, - }, - }, -]; +export const packages: Package[] = []; async function getInstalledVersion(config: Config, packageName: string) { const packageJSONPath = config.resolveFromRepo( @@ -145,15 +107,20 @@ async function patchModule( } } -export const PatchNativeModules: Task = { - description: 'Patching platform-specific native modules', - async run(config, log, build) { - for (const pkg of packages) { - await Promise.all( - config.getTargetPlatforms().map(async (platform) => { - await patchModule(config, log, build, platform, pkg); - }) - ); - } - }, -}; +export function createPatchNativeModulesTask(customPackages?: Package[]): Task { + return { + description: 'Patching platform-specific native modules', + async run(config, log, build) { + const targetPackages = customPackages || packages; + for (const pkg of targetPackages) { + await Promise.all( + config.getTargetPlatforms().map(async (platform) => { + await patchModule(config, log, build, platform, pkg); + }) + ); + } + }, + }; +} + +export const PatchNativeModules = createPatchNativeModulesTask(); diff --git a/src/plugins/vis_type_timeline/server/series_functions/label.js b/src/plugins/vis_type_timeline/server/series_functions/label.js index c935d537081f..4649ee6cf53f 100644 --- a/src/plugins/vis_type_timeline/server/series_functions/label.js +++ b/src/plugins/vis_type_timeline/server/series_functions/label.js @@ -62,10 +62,8 @@ export default new Chainable('label', { const config = args.byName; return alter(args, function (eachSeries) { if (config.regex) { - // not using a standard `import` so that if there's an issue with the re2 native module - // that it doesn't prevent OpenSearch Dashboards from starting up and we only have an issue using Timeline labels - const RE2 = require('re2'); - eachSeries.label = eachSeries.label.replace(new RE2(config.regex), config.label); + const regex = new RegExp(config.regex); + eachSeries.label = eachSeries.label.replace(regex, config.label); } else { eachSeries.label = config.label; } diff --git a/src/plugins/vis_type_timeline/server/series_functions/label.test.js b/src/plugins/vis_type_timeline/server/series_functions/label.test.js index c6dc832914a3..69268b385b07 100644 --- a/src/plugins/vis_type_timeline/server/series_functions/label.test.js +++ b/src/plugins/vis_type_timeline/server/series_functions/label.test.js @@ -51,4 +51,24 @@ describe('label.js', () => { expect(r.output.list[0].label).to.equal('beerative'); }); }); + + it('can use a regex to capture groups to modify series label', () => { + return invoke(fn, [seriesList, 'beer$2', '(N)(egative)']).then((r) => { + expect(r.output.list[0].label).to.equal('beeregative'); + }); + }); + + it('can handle different regex patterns', () => { + const seriesListCopy1 = JSON.parse(JSON.stringify(seriesList)); + const seriesListCopy2 = JSON.parse(JSON.stringify(seriesList)); + + return Promise.all([ + invoke(fn, [seriesListCopy1, 'beer$1 - $2', '(N)(egative)']).then((r) => { + expect(r.output.list[0].label).to.equal('beerN - egative'); + }), + invoke(fn, [seriesListCopy2, 'beer$1_$2', '(N)(eg.*)']).then((r) => { + expect(r.output.list[0].label).to.equal('beerN_egative'); + }), + ]); + }); }); diff --git a/yarn.lock b/yarn.lock index c424c6592eb5..3f8d40708ac1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4141,7 +4141,7 @@ agent-base@4: dependencies: es6-promisify "^5.0.0" -agent-base@6, agent-base@^6.0.2: +agent-base@6: version "6.0.2" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== @@ -4155,7 +4155,7 @@ agentkeepalive@^3.4.1: dependencies: humanize-ms "^1.2.1" -agentkeepalive@^4.1.3, agentkeepalive@^4.2.1: +agentkeepalive@^4.2.1: version "4.2.1" resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.2.1.tgz#a7975cbb9f83b367f06c90cc51ff28fe7d499717" integrity sha512-Zn4cw2NEqd+9fiSVWMscnjyQ1a8Yfoc5oBajLeo5w+YBHgDUcEBY2hS4YpTz6iN5f/2zQiktcuM6tS8x1p9dpA== @@ -4369,11 +4369,6 @@ append-transform@^2.0.0: dependencies: default-require-extensions "^3.0.0" -"aproba@^1.0.3 || ^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc" - integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ== - aproba@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" @@ -4413,14 +4408,6 @@ archy@^1.0.0: resolved "https://registry.yarnpkg.com/archy/-/archy-1.0.0.tgz#f9c8c13757cc1dd7bc379ac77b2c62a5c2868c40" integrity sha1-+cjBN1fMHde8N5rHeyxipcKGjEA= -are-we-there-yet@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz#679df222b278c64f2cdba1175cdc00b0d96164bd" - integrity sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg== - dependencies: - delegates "^1.0.0" - readable-stream "^3.6.0" - argparse@^1.0.7, argparse@~1.0.9: version "1.0.10" resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" @@ -5284,7 +5271,7 @@ cacache@^13.0.1: ssri "^7.0.0" unique-filename "^1.1.1" -cacache@^15.0.5, cacache@^15.2.0: +cacache@^15.0.5: version "15.3.0" resolved "https://registry.yarnpkg.com/cacache/-/cacache-15.3.0.tgz#dc85380fb2f556fe3dda4c719bfa0ec875a7f1eb" integrity sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ== @@ -5842,11 +5829,6 @@ color-string@^1.4.0: color-name "^1.0.0" simple-swizzle "^0.2.2" -color-support@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" - integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== - color@1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/color/-/color-1.0.3.tgz#e48e832d85f14ef694fb468811c2d5cfe729b55d" @@ -5973,11 +5955,6 @@ console-browserify@^1.1.0: resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336" integrity sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA== -console-control-strings@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" - integrity sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ== - console-log-level@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/console-log-level/-/console-log-level-1.4.1.tgz#9c5a6bb9ef1ef65b05aba83028b0ff894cdf630a" @@ -6655,7 +6632,7 @@ debug@3.X, debug@^3.2.6, debug@^3.2.7: dependencies: ms "^2.1.1" -debug@4, debug@4.3.4, debug@^4, debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4: +debug@4, debug@4.3.4, debug@^4, debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -6886,11 +6863,6 @@ delayed-stream@~1.0.0: resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= -delegates@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" - integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= - delete-empty@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/delete-empty/-/delete-empty-2.0.0.tgz#dcf7c4f93a98445119acd57b137d13e7af78fa39" @@ -7337,13 +7309,6 @@ emoticon@^3.2.0: resolved "https://registry.yarnpkg.com/emoticon/-/emoticon-3.2.0.tgz#c008ca7d7620fac742fe1bf4af8ff8fed154ae7f" integrity sha512-SNujglcLTTg+lDAcApPNgEdudaqQFiAbJCqzjNxJkvN9vAwCGi0uu8IUVvx+f16h+V44KCY6Y2yboroc9pilHg== -encoding@^0.1.12: - version "0.1.13" - resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.13.tgz#56574afdd791f54a8e9b2785c0582a2d26210fa9" - integrity sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A== - dependencies: - iconv-lite "^0.6.2" - end-of-stream@^1.0.0, end-of-stream@^1.1.0, end-of-stream@^1.4.1, end-of-stream@^1.4.4: version "1.4.4" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" @@ -7389,11 +7354,6 @@ entities@~2.1.0: resolved "https://registry.yarnpkg.com/entities/-/entities-2.1.0.tgz#992d3129cf7df6870b96c57858c249a120f8b8b5" integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w== -env-paths@^2.2.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2" - integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A== - envinfo@^7.7.3: version "7.8.1" resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.8.1.tgz#06377e3e5f4d379fea7ac592d5ad8927e0c4d475" @@ -7472,11 +7432,6 @@ enzyme@^3.11.0: rst-selector-parser "^2.2.3" string.prototype.trim "^1.2.1" -err-code@^2.0.2: - version "2.0.3" - resolved "https://registry.yarnpkg.com/err-code/-/err-code-2.0.3.tgz#23c2f3b756ffdfc608d30e27c9a941024807e7f9" - integrity sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA== - errno@^0.1.1, errno@^0.1.3, errno@~0.1.7: version "0.1.8" resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.8.tgz#8bb3e9c7d463be4976ff888f76b4809ebc2e811f" @@ -8795,20 +8750,6 @@ functions-have-names@^1.2.2: resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.2.tgz#98d93991c39da9361f8e50b337c4f6e41f120e21" integrity sha512-bLgc3asbWdwPbx2mNk2S49kmJCuQeu0nfmaOgbs8WIyzzkw3r4htszdIi9Q9EMezDPTYuJx2wvjZ/EwgAthpnA== -gauge@^4.0.3: - version "4.0.4" - resolved "https://registry.yarnpkg.com/gauge/-/gauge-4.0.4.tgz#52ff0652f2bbf607a989793d53b751bef2328dce" - integrity sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg== - dependencies: - aproba "^1.0.3 || ^2.0.0" - color-support "^1.1.3" - console-control-strings "^1.1.0" - has-unicode "^2.0.1" - signal-exit "^3.0.7" - string-width "^4.2.3" - strip-ansi "^6.0.1" - wide-align "^1.1.5" - geckodriver@^3.0.2: version "3.2.0" resolved "https://registry.yarnpkg.com/geckodriver/-/geckodriver-3.2.0.tgz#6b0a85e2aafbce209bca30e2d53af857707b1034" @@ -9389,11 +9330,6 @@ has-tostringtag@^1.0.0: dependencies: has-symbols "^1.0.2" -has-unicode@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" - integrity sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ== - has-value@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177" @@ -9707,7 +9643,7 @@ http-aws-es@6.0.0: resolved "https://registry.yarnpkg.com/http-aws-es/-/http-aws-es-6.0.0.tgz#1528978d2bee718b8732dcdced0856efa747aeff" integrity sha512-g+qp7J110/m4aHrR3iit4akAlnW0UljZ6oTq/rCcbsI8KP9x+95vqUtx49M2XQ2JMpwJio3B6gDYx+E8WDxqiA== -http-cache-semantics@^4.0.0, http-cache-semantics@^4.1.0: +http-cache-semantics@^4.0.0: version "4.1.1" resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a" integrity sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ== @@ -9786,7 +9722,7 @@ iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@^0.4.4, iconv-lite@~0.4.13: dependencies: safer-buffer ">= 2.1.2 < 3" -iconv-lite@0.6, iconv-lite@^0.6.2: +iconv-lite@0.6: version "0.6.3" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== @@ -9974,11 +9910,6 @@ inquirer@^7.0.0, inquirer@^7.3.3: strip-ansi "^6.0.0" through "^2.3.6" -install-artifact-from-github@^1.3.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/install-artifact-from-github/-/install-artifact-from-github-1.3.1.tgz#eefaad9af35d632e5d912ad1569c1de38c3c2462" - integrity sha512-3l3Bymg2eKDsN5wQuMfgGEj2x6l5MCAv0zPL6rxHESufFVlEAKW/6oY9F1aGgvY/EgWm5+eWGRjINveL4X7Hgg== - internal-slot@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.3.tgz#7347e307deeea2faac2ac6205d4bc7d34967f59c" @@ -10070,11 +10001,6 @@ ip-regex@^4.1.0: resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-4.3.0.tgz#687275ab0f57fa76978ff8f4dddc8a23d5990db5" integrity sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q== -ip@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ip/-/ip-2.0.0.tgz#4cf4ab182fee2314c75ede1276f8c80b479936da" - integrity sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ== - irregular-plurals@^3.2.0: version "3.3.0" resolved "https://registry.yarnpkg.com/irregular-plurals/-/irregular-plurals-3.3.0.tgz#67d0715d4361a60d9fd9ee80af3881c631a31ee2" @@ -10315,11 +10241,6 @@ is-interactive@^1.0.0: resolved "https://registry.yarnpkg.com/is-interactive/-/is-interactive-1.0.0.tgz#cea6e6ae5c870a7b0a0004070b7b587e0252912e" integrity sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w== -is-lambda@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-lambda/-/is-lambda-1.0.1.tgz#3d9877899e6a53efc0160504cde15f82e6f061d5" - integrity sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ== - is-map@^2.0.1, is-map@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.2.tgz#00922db8c9bf73e81b7a335827bc2a43f2b91127" @@ -11988,28 +11909,6 @@ make-dir@^3.0.0, make-dir@^3.0.2, make-dir@^3.1.0: dependencies: semver "^6.0.0" -make-fetch-happen@^9.1.0: - version "9.1.0" - resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz#53085a09e7971433e6765f7971bf63f4e05cb968" - integrity sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg== - dependencies: - agentkeepalive "^4.1.3" - cacache "^15.2.0" - http-cache-semantics "^4.1.0" - http-proxy-agent "^4.0.1" - https-proxy-agent "^5.0.0" - is-lambda "^1.0.1" - lru-cache "^6.0.0" - minipass "^3.1.3" - minipass-collect "^1.0.2" - minipass-fetch "^1.3.2" - minipass-flush "^1.0.5" - minipass-pipeline "^1.2.4" - negotiator "^0.6.2" - promise-retry "^2.0.1" - socks-proxy-agent "^6.0.0" - ssri "^8.0.0" - make-iterator@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/make-iterator/-/make-iterator-1.0.1.tgz#29b33f312aa8f547c4a5e490f56afcec99133ad6" @@ -12385,17 +12284,6 @@ minipass-collect@^1.0.2: dependencies: minipass "^3.0.0" -minipass-fetch@^1.3.2: - version "1.4.1" - resolved "https://registry.yarnpkg.com/minipass-fetch/-/minipass-fetch-1.4.1.tgz#d75e0091daac1b0ffd7e9d41629faff7d0c1f1b6" - integrity sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw== - dependencies: - minipass "^3.1.0" - minipass-sized "^1.0.3" - minizlib "^2.0.0" - optionalDependencies: - encoding "^0.1.12" - minipass-flush@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/minipass-flush/-/minipass-flush-1.0.5.tgz#82e7135d7e89a50ffe64610a787953c4c4cbb373" @@ -12403,20 +12291,13 @@ minipass-flush@^1.0.5: dependencies: minipass "^3.0.0" -minipass-pipeline@^1.2.2, minipass-pipeline@^1.2.4: +minipass-pipeline@^1.2.2: version "1.2.4" resolved "https://registry.yarnpkg.com/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz#68472f79711c084657c067c5c6ad93cddea8214c" integrity sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A== dependencies: minipass "^3.0.0" -minipass-sized@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/minipass-sized/-/minipass-sized-1.0.3.tgz#70ee5a7c5052070afacfbc22977ea79def353b70" - integrity sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g== - dependencies: - minipass "^3.0.0" - minipass@^3.0.0, minipass@^3.1.1: version "3.1.6" resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.6.tgz#3b8150aa688a711a1521af5e8779c1d3bb4f45ee" @@ -12424,13 +12305,6 @@ minipass@^3.0.0, minipass@^3.1.1: dependencies: yallist "^4.0.0" -minipass@^3.1.0, minipass@^3.1.3: - version "3.3.6" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.3.6.tgz#7bba384db3a1520d18c9c0e5251c3444e95dd94a" - integrity sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw== - dependencies: - yallist "^4.0.0" - minipass@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/minipass/-/minipass-4.0.0.tgz#7cebb0f9fa7d56f0c5b17853cbe28838a8dbbd3b" @@ -12438,7 +12312,7 @@ minipass@^4.0.0: dependencies: yallist "^4.0.0" -minizlib@^2.0.0, minizlib@^2.1.1: +minizlib@^2.1.1: version "2.1.2" resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== @@ -12656,11 +12530,6 @@ nan@^2.12.1, nan@^2.14.2: resolved "https://registry.yarnpkg.com/nan/-/nan-2.15.0.tgz#3f34a473ff18e15c1b5626b62903b5ad6e665fee" integrity sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ== -nan@^2.15.0: - version "2.17.0" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.17.0.tgz#c0150a2368a182f033e9aa5195ec76ea41a199cb" - integrity sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ== - nano-css@^5.2.1: version "5.3.4" resolved "https://registry.yarnpkg.com/nano-css/-/nano-css-5.3.4.tgz#40af6a83a76f84204f346e8ccaa9169cdae9167b" @@ -12731,11 +12600,6 @@ needle@^2.5.2: iconv-lite "^0.4.4" sax "^1.2.4" -negotiator@^0.6.2: - version "0.6.3" - resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" - integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== - neo-async@^2.5.0, neo-async@^2.6.0, neo-async@^2.6.1, neo-async@^2.6.2: version "2.6.2" resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" @@ -12819,22 +12683,6 @@ node-gyp-build@^4.2.3: resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.3.0.tgz#9f256b03e5826150be39c764bf51e993946d71a3" integrity sha512-iWjXZvmboq0ja1pUGULQBexmxq8CV4xBhX7VDOTbL7ZR4FOowwY/VOtRxBN/yKxmdGoIp4j5ysNT4u3S2pDQ3Q== -node-gyp@^8.4.1: - version "8.4.1" - resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-8.4.1.tgz#3d49308fc31f768180957d6b5746845fbd429937" - integrity sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w== - dependencies: - env-paths "^2.2.0" - glob "^7.1.4" - graceful-fs "^4.2.6" - make-fetch-happen "^9.1.0" - nopt "^5.0.0" - npmlog "^6.0.0" - rimraf "^3.0.2" - semver "^7.3.5" - tar "^6.1.2" - which "^2.0.2" - node-int64@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" @@ -12908,13 +12756,6 @@ nopt@^2.2.0: dependencies: abbrev "1" -nopt@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88" - integrity sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ== - dependencies: - abbrev "1" - nopt@~3.0.6: version "3.0.6" resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9" @@ -12996,16 +12837,6 @@ npm-run-path@^4.0.0, npm-run-path@^4.0.1: dependencies: path-key "^3.0.0" -npmlog@^6.0.0: - version "6.0.2" - resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-6.0.2.tgz#c8166017a42f2dea92d6453168dd865186a70830" - integrity sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg== - dependencies: - are-we-there-yet "^3.0.0" - console-control-strings "^1.1.0" - gauge "^4.0.3" - set-blocking "^2.0.0" - nth-check@^2.0.1, nth-check@~1.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.0.1.tgz#2efe162f5c3da06a28959fbd3db75dbeea9f0fc2" @@ -13969,14 +13800,6 @@ promise-polyfill@^8.1.3: resolved "https://registry.yarnpkg.com/promise-polyfill/-/promise-polyfill-8.2.3.tgz#2edc7e4b81aff781c88a0d577e5fe9da822107c6" integrity sha512-Og0+jCRQetV84U8wVjMNccfGCnMQ9mGs9Hv78QFe+pSDD3gWTpz0y+1QCuxy5d/vBFuZ3iwP2eycAkvqIMPmWg== -promise-retry@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/promise-retry/-/promise-retry-2.0.1.tgz#ff747a13620ab57ba688f5fc67855410c370da22" - integrity sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g== - dependencies: - err-code "^2.0.2" - retry "^0.12.0" - prompts@^2.0.1: version "2.4.2" resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" @@ -14219,15 +14042,6 @@ re-reselect@^3.4.0: resolved "https://registry.yarnpkg.com/re-reselect/-/re-reselect-3.4.0.tgz#0f2303f3c84394f57f0cd31fea08a1ca4840a7cd" integrity sha512-JsecfN+JlckncVXTWFWjn0Vk6uInl8GSf4eEd9tTk5qXHlgqkPdILpnYpgZcISXNYAzvfvsCZviaDk8AxyS5sg== -re2@1.17.4: - version "1.17.4" - resolved "https://registry.yarnpkg.com/re2/-/re2-1.17.4.tgz#7bf29290bdde963014e77bd2c2e799a6d788386e" - integrity sha512-xyZ4h5PqE8I9tAxTh3G0UttcK5ufrcUxReFjGzfX61vtanNbS1XZHjnwRSyPcLgChI4KLxVgOT/ioZXnUAdoTA== - dependencies: - install-artifact-from-github "^1.3.0" - nan "^2.15.0" - node-gyp "^8.4.1" - react-ace@^7.0.5: version "7.0.5" resolved "https://registry.yarnpkg.com/react-ace/-/react-ace-7.0.5.tgz#798299fd52ddf3a3dcc92afc5865538463544f01" @@ -15149,11 +14963,6 @@ ret@~0.1.10: resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== -retry@^0.12.0: - version "0.12.0" - resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" - integrity sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow== - reusify@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" @@ -15599,11 +15408,6 @@ 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== - snapdragon-node@^2.0.1: version "2.1.1" resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" @@ -15634,23 +15438,6 @@ snapdragon@^0.8.1: source-map-resolve "^0.5.0" use "^3.1.0" -socks-proxy-agent@^6.0.0: - version "6.2.1" - resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz#2687a31f9d7185e38d530bef1944fe1f1496d6ce" - integrity sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ== - dependencies: - agent-base "^6.0.2" - debug "^4.3.3" - socks "^2.6.2" - -socks@^2.6.2: - version "2.7.1" - resolved "https://registry.yarnpkg.com/socks/-/socks-2.7.1.tgz#d8e651247178fde79c0663043e07240196857d55" - integrity sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ== - dependencies: - ip "^2.0.0" - 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" @@ -15895,7 +15682,7 @@ ssri@^7.0.0: figgy-pudding "^3.5.1" minipass "^3.1.1" -ssri@^8.0.0, ssri@^8.0.1: +ssri@^8.0.1: version "8.0.1" resolved "https://registry.yarnpkg.com/ssri/-/ssri-8.0.1.tgz#638e4e439e2ffbd2cd289776d5ca457c4f51a2af" integrity sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ== @@ -16032,15 +15819,6 @@ string-width@^1.0.1: is-fullwidth-code-point "^1.0.0" strip-ansi "^3.0.0" -"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - string-width@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" @@ -16058,6 +15836,15 @@ string-width@^3.0.0: is-fullwidth-code-point "^2.0.0" strip-ansi "^5.1.0" +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + string.prototype.matchall@^4.0.6: version "4.0.7" resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.7.tgz#8e6ecb0d8a1fb1fda470d81acecb2dba057a481d" @@ -16546,7 +16333,7 @@ tar@6.1.11: mkdirp "^1.0.3" yallist "^4.0.0" -tar@^6.0.2, tar@^6.1.11, tar@^6.1.2: +tar@^6.0.2, tar@^6.1.11: version "6.1.13" resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.13.tgz#46e22529000f612180601a6fe0680e7da508847b" integrity sha512-jdIBIN6LTIe2jqzay/2vtYLlBHa3JF42ot3h1dW8Q0PaAG4v8rm0cvpVePtau5C6OKXGGcgO9q2AMNSWxiLqKw== @@ -18265,13 +18052,6 @@ which@^2.0.1, which@^2.0.2, which@~2.0.2: dependencies: isexe "^2.0.0" -wide-align@^1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.5.tgz#df1d4c206854369ecf3c9a4898f1b23fbd9d15d3" - integrity sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg== - dependencies: - string-width "^1.0.2 || 2 || 3 || 4" - wildcard@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.0.tgz#a77d20e5200c6faaac979e4b3aadc7b3dd7f8fec" From b04e6579650b15665458855b5fd322d70552262b Mon Sep 17 00:00:00 2001 From: Sirazh Gabdullin Date: Wed, 17 May 2023 23:46:38 +0600 Subject: [PATCH 31/35] [Console] [CCI] Remove unused ul element and its custom styling. (#3993) * remove unused ul element Signed-off-by: Sirazh Gabdullin * Update CHANGELOG.md Signed-off-by: Sirazh Gabdullin --------- Signed-off-by: Sirazh Gabdullin --- CHANGELOG.md | 1 + .../editor/legacy/console_editor/editor.tsx | 1 - src/plugins/console/public/styles/_app.scss | 12 ------------ 3 files changed, 1 insertion(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 17c86ec89bb8..80c247b9404a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -199,6 +199,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [Console] Replace jQuery.ajax with core.http when calling OSD APIs in console ([#3080](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3080)) - [I18n] Fix Listr type errors and error handlers ([#3629](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3629)) - [Multiple DataSource] Present the authentication type choices in a drop-down ([#3693](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3693)) +- [Console] Remove unused ul element and its custom styling ([#3993](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3993)) - Fix EUI/OUI type errors ([#3798](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3798)) ### 🔩 Tests diff --git a/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx b/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx index 876e94d4cbcb..1c47cc41e920 100644 --- a/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx +++ b/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx @@ -231,7 +231,6 @@ function EditorUI({ initialTextValue, dataSourceId }: EditorProps) { return (
-
    Date: Thu, 18 May 2023 11:37:36 -0700 Subject: [PATCH 32/35] Add 1.3.10 release note (#4060) (#4063) * Add release note for 1.3.10 * Address comments and add one CVE PR --------- (cherry picked from commit 43715871f3a9fc9b2a0fcac765805a33b4b305d5) Signed-off-by: abbyhu2000 Signed-off-by: github-actions[bot] Co-authored-by: github-actions[bot] --- ...nsearch-dashboards.release-notes-1.3.10.md | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 release-notes/opensearch-dashboards.release-notes-1.3.10.md diff --git a/release-notes/opensearch-dashboards.release-notes-1.3.10.md b/release-notes/opensearch-dashboards.release-notes-1.3.10.md new file mode 100644 index 000000000000..be08d46aac93 --- /dev/null +++ b/release-notes/opensearch-dashboards.release-notes-1.3.10.md @@ -0,0 +1,33 @@ +# Version 1.3.10 Release Notes + +### 🛡 Security + +- [CVE-2020-15366][1.x] Bump ajv from 4.11.8 to 6.12.6 ([#4035](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4035)) +- [CVE-2022-48285][1.x] Bump jszip from 3.7.1 to 3.10.1 ([#4011](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4011)) +- [CVE-2021-35065][1.x] Bump glob-parent from 6.0.0 to 6.0.2 ([#4005](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4005)) +- [CVE-2022-25851][1.x] Bump jpeg-js from 0.4.1 to 0.4.4 ([#3860](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3860)) +- [CVE-2022-25858][1.x] Bump terser from 4.8.0 to 4.8.1 ([#3786](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3786)) +- [CVE-2021-23490][1.x] Bump parse-link-header from 1.0.1 to 2.0.0 ([#3820](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3820)) +- [CVE-2021-3765][1.x] Bump validator from 8.2.0 to 13.9.0 ([#3753](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3753)) +- [CVE-2022-25758][1.x] Bump scss-tokenizer from 0.3.0 to 0.4.3 ([#3789](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3789)) +- [CVE-2021-3803][1.x] Bump nth-check from 1.0.2 to 2.0.1 ([#3745](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3745)) +- Bump highlight.js from 9.18.5 to 10.7.3 to solve security concerns ([#4062](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4062)) + +### 📈 Features/Enhancements + +- Add tooltip to help icon ([#3872](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3872)) + +### 🐛 Bug Fixes + +- [TSVB] Fix the link to "serial differencing aggregation" documentation ([#3503](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3503)) + +### 📝 Documentation + +- Update jest documentation links ([#3939](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3939)) + +### 🛠 Maintenance + +- Add threshold to code coverage changes for project ([#4050](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4050)) +- Temporarily hardcode chromedriver to 112.0.0 to enable all ftr tests ([#4039]()) +- Update MAINTAINERS.md and CODEOWNERS ([#3938](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3938)) +- Add opensearch-dashboards-docker-dev to .gitignore ([#3781](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3781)) From e73779084f34b28e2cf69f28a8563e44e09f168d Mon Sep 17 00:00:00 2001 From: Zhongnan Su Date: Fri, 19 May 2023 11:49:17 -0700 Subject: [PATCH 33/35] [Multiple Datasource] Support Amazon OpenSearch Serverless (#3957) * [Multiple Datasource]Support Amazon OpenSearch Serverless in SigV4 * remove experimental text in yml * Refactor create data source form for authentication Signed-off-by: Su --- CHANGELOG.md | 2 +- config/opensearch_dashboards.yml | 3 +- package.json | 4 +- packages/osd-opensearch/package.json | 2 +- .../data_source/common/data_sources/types.ts | 6 + .../data_source/opensearch_dashboards.json | 3 +- .../server/client/configure_client.test.ts | 24 ++ .../server/client/configure_client.ts | 3 +- .../server/client/configure_client_utils.ts | 3 +- .../legacy/configure_legacy_client.test.ts | 45 +- .../server/legacy/configure_legacy_client.ts | 8 +- .../data_source_connection_validator.ts | 12 +- .../server/routes/test_connection.ts | 14 +- ...ata_source_saved_objects_client_wrapper.ts | 11 +- .../create_data_source_form.test.tsx | 2 + .../create_form/create_data_source_form.tsx | 386 +++++++++++------- .../create_data_source_wizard.test.tsx | 13 + .../create_data_source_wizard.tsx | 1 + .../edit_form/edit_data_source_form.tsx | 64 ++- .../update_aws_credential_modal.tsx | 14 + .../validation/datasource_form_validation.ts | 7 + .../data_source_management/public/types.ts | 17 + yarn.lock | 26 +- 23 files changed, 468 insertions(+), 202 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 80c247b9404a..fa5a9f577a40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -85,7 +85,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - Add satisfaction survey link to help menu ([#3676] (https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3676)) - [Vis Builder] Add persistence to visualizations inner state ([#3751](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3751)) - [Table Visualization] Move format table, consolidate types and add unit tests ([#3397](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3397)) - +- [Multiple Datasource] Support Amazon OpenSearch Serverless ([#3957](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3957)) ### 🐛 Bug Fixes - [Vis Builder] Fixes auto bounds for timeseries bar chart visualization ([2401](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2401)) diff --git a/config/opensearch_dashboards.yml b/config/opensearch_dashboards.yml index 1d751769f701..d7e0d390b0fc 100644 --- a/config/opensearch_dashboards.yml +++ b/config/opensearch_dashboards.yml @@ -229,8 +229,7 @@ # functionality in Visualization. # vis_builder.enabled: false -# Set the value of this setting to true to enable the experimental multiple data source -# support feature. Use with caution. +# Set the value of this setting to true to enable multiple data source feature. #data_source.enabled: false # Set the value of these settings to customize crypto materials to encryption saved credentials # in data sources. diff --git a/package.json b/package.json index 676bd6cb653c..06442016f42a 100644 --- a/package.json +++ b/package.json @@ -136,7 +136,7 @@ "@hapi/podium": "^4.1.3", "@hapi/vision": "^6.1.0", "@hapi/wreck": "^17.1.0", - "@opensearch-project/opensearch": "^2.1.0", + "@opensearch-project/opensearch": "^2.2.0", "@osd/ace": "1.0.0", "@osd/analytics": "1.0.0", "@osd/apm-config-loader": "1.0.0", @@ -169,7 +169,7 @@ "dns-sync": "^0.2.1", "elastic-apm-node": "^3.7.0", "elasticsearch": "^16.7.0", - "http-aws-es": "6.0.0", + "http-aws-es": "npm:@zhongnansu/http-aws-es@6.0.1", "execa": "^4.0.2", "expiry-js": "0.1.7", "fast-deep-equal": "^3.1.1", diff --git a/packages/osd-opensearch/package.json b/packages/osd-opensearch/package.json index 02fcddb36e6f..44404a9ae5a3 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.1.0", + "@opensearch-project/opensearch": "^2.2.0", "@osd/dev-utils": "1.0.0", "abort-controller": "^3.0.0", "chalk": "^4.1.0", diff --git a/src/plugins/data_source/common/data_sources/types.ts b/src/plugins/data_source/common/data_sources/types.ts index 366e5a0f3f55..8763c5306c15 100644 --- a/src/plugins/data_source/common/data_sources/types.ts +++ b/src/plugins/data_source/common/data_sources/types.ts @@ -25,6 +25,7 @@ export interface SigV4Content extends SavedObjectAttributes { accessKey: string; secretKey: string; region: string; + service?: SigV4ServiceName; } export interface UsernamePasswordTypedContent extends SavedObjectAttributes { @@ -37,3 +38,8 @@ export enum AuthType { UsernamePasswordType = 'username_password', SigV4 = 'sigv4', } + +export enum SigV4ServiceName { + OpenSearch = 'es', + OpenSearchServerless = 'aoss', +} diff --git a/src/plugins/data_source/opensearch_dashboards.json b/src/plugins/data_source/opensearch_dashboards.json index 71183a411c79..871858403cf3 100644 --- a/src/plugins/data_source/opensearch_dashboards.json +++ b/src/plugins/data_source/opensearch_dashboards.json @@ -5,5 +5,6 @@ "server": true, "ui": true, "requiredPlugins": [], - "optionalPlugins": [] + "optionalPlugins": [], + "extraPublicDirs": ["common/data_sources"] } diff --git a/src/plugins/data_source/server/client/configure_client.test.ts b/src/plugins/data_source/server/client/configure_client.test.ts index 1499ccd411c2..aa367f0a6f89 100644 --- a/src/plugins/data_source/server/client/configure_client.test.ts +++ b/src/plugins/data_source/server/client/configure_client.test.ts @@ -167,6 +167,30 @@ describe('configureClient', () => { expect(decodeAndDecryptSpy).toHaveBeenCalledTimes(2); }); + test('configure client with auth.type == sigv4, service == aoss, should successfully call new Client()', async () => { + savedObjectsMock.get.mockReset().mockResolvedValueOnce({ + id: DATA_SOURCE_ID, + type: DATA_SOURCE_SAVED_OBJECT_TYPE, + attributes: { + ...dataSourceAttr, + auth: { + type: AuthType.SigV4, + credentials: { ...sigV4AuthContent, service: 'aoss' }, + }, + }, + references: [], + }); + + jest.spyOn(cryptographyMock, 'decodeAndDecrypt').mockResolvedValue({ + decryptedText: 'accessKey', + encryptionContext: { endpoint: 'http://localhost' }, + }); + + await configureClient(dataSourceClientParams, clientPoolSetup, config, logger); + + expect(ClientMock).toHaveBeenCalledTimes(1); + }); + test('configure test client for non-exist datasource should not call saved object api, nor decode any credential', async () => { const decodeAndDecryptSpy = jest.spyOn(cryptographyMock, 'decodeAndDecrypt').mockResolvedValue({ decryptedText: 'password', diff --git a/src/plugins/data_source/server/client/configure_client.ts b/src/plugins/data_source/server/client/configure_client.ts index 8b43ffa80b23..77a0e067bbc0 100644 --- a/src/plugins/data_source/server/client/configure_client.ts +++ b/src/plugins/data_source/server/client/configure_client.ts @@ -160,7 +160,7 @@ const getBasicAuthClient = ( }; const getAWSClient = (credential: SigV4Content, clientOptions: ClientOptions): Client => { - const { accessKey, secretKey, region } = credential; + const { accessKey, secretKey, region, service } = credential; const credentialProvider = (): Promise => { return new Promise((resolve) => { @@ -172,6 +172,7 @@ const getAWSClient = (credential: SigV4Content, clientOptions: ClientOptions): C ...AwsSigv4Signer({ region, getCredentials: credentialProvider, + service, }), ...clientOptions, }); diff --git a/src/plugins/data_source/server/client/configure_client_utils.ts b/src/plugins/data_source/server/client/configure_client_utils.ts index 3ef8acc97b58..2ca6c6f2a83f 100644 --- a/src/plugins/data_source/server/client/configure_client_utils.ts +++ b/src/plugins/data_source/server/client/configure_client_utils.ts @@ -91,7 +91,7 @@ export const getAWSCredential = async ( cryptography: CryptographyServiceSetup ): Promise => { const { endpoint } = dataSource; - const { accessKey, secretKey, region } = dataSource.auth.credentials! as SigV4Content; + const { accessKey, secretKey, region, service } = dataSource.auth.credentials! as SigV4Content; const { decryptedText: accessKeyText, @@ -122,6 +122,7 @@ export const getAWSCredential = async ( region, accessKey: accessKeyText, secretKey: secretKeyText, + service, }; return credential; diff --git a/src/plugins/data_source/server/legacy/configure_legacy_client.test.ts b/src/plugins/data_source/server/legacy/configure_legacy_client.test.ts index c047da70b285..59c110d06dc5 100644 --- a/src/plugins/data_source/server/legacy/configure_legacy_client.test.ts +++ b/src/plugins/data_source/server/legacy/configure_legacy_client.test.ts @@ -6,7 +6,7 @@ import { SavedObjectsClientContract } from '../../../../core/server'; import { loggingSystemMock, savedObjectsClientMock } from '../../../../core/server/mocks'; import { DATA_SOURCE_SAVED_OBJECT_TYPE } from '../../common'; -import { AuthType, DataSourceAttributes } from '../../common/data_sources'; +import { AuthType, DataSourceAttributes, SigV4Content } from '../../common/data_sources'; import { DataSourcePluginConfigType } from '../../config'; import { cryptographyServiceSetupMock } from '../cryptography_service.mocks'; import { CryptographyServiceSetup } from '../cryptography_service'; @@ -27,6 +27,7 @@ describe('configureLegacyClient', () => { let clientPoolSetup: OpenSearchClientPoolSetup; let configOptions: ConfigOptions; let dataSourceAttr: DataSourceAttributes; + let sigV4AuthContent: SigV4Content; let mockOpenSearchClientInstance: { close: jest.Mock; @@ -71,6 +72,12 @@ describe('configureLegacyClient', () => { }, } as DataSourceAttributes; + sigV4AuthContent = { + region: 'us-east-1', + accessKey: 'accessKey', + secretKey: 'secretKey', + }; + clientPoolSetup = { getClientFromPool: jest.fn(), addClientToPool: jest.fn(), @@ -157,6 +164,42 @@ describe('configureLegacyClient', () => { expect(mockResult).toBeDefined(); }); + test('configure client with auth.type == sigv4 and service param, should call new Client() with service param', async () => { + savedObjectsMock.get.mockReset().mockResolvedValueOnce({ + id: DATA_SOURCE_ID, + type: DATA_SOURCE_SAVED_OBJECT_TYPE, + attributes: { + ...dataSourceAttr, + auth: { + type: AuthType.SigV4, + credentials: { ...sigV4AuthContent, service: 'aoss' }, + }, + }, + references: [], + }); + + parseClientOptionsMock.mockReturnValue(configOptions); + + jest.spyOn(cryptographyMock, 'decodeAndDecrypt').mockResolvedValue({ + decryptedText: 'accessKey', + encryptionContext: { endpoint: 'http://localhost' }, + }); + + await configureLegacyClient( + dataSourceClientParams, + callApiParams, + clientPoolSetup, + config, + logger + ); + + expect(parseClientOptionsMock).toHaveBeenCalled(); + expect(ClientMock).toHaveBeenCalledTimes(1); + expect(ClientMock).toHaveBeenCalledWith(expect.objectContaining({ service: 'aoss' })); + + expect(savedObjectsMock.get).toHaveBeenCalledTimes(1); + }); + test('configure client with auth.type == username_password and password contaminated', async () => { const decodeAndDecryptSpy = jest .spyOn(cryptographyMock, 'decodeAndDecrypt') diff --git a/src/plugins/data_source/server/legacy/configure_legacy_client.ts b/src/plugins/data_source/server/legacy/configure_legacy_client.ts index 3a9b65634a28..0d074cf77d4a 100644 --- a/src/plugins/data_source/server/legacy/configure_legacy_client.ts +++ b/src/plugins/data_source/server/legacy/configure_legacy_client.ts @@ -5,10 +5,9 @@ import { Client } from '@opensearch-project/opensearch'; import { Client as LegacyClient, ConfigOptions } from 'elasticsearch'; -import { Credentials } from 'aws-sdk'; +import { Credentials, Config } from 'aws-sdk'; import { get } from 'lodash'; import HttpAmazonESConnector from 'http-aws-es'; -import { Config } from 'aws-sdk'; import { Headers, LegacyAPICaller, @@ -27,7 +26,7 @@ import { CryptographyServiceSetup } from '../cryptography_service'; import { DataSourceClientParams, LegacyClientCallAPIParams } from '../types'; import { OpenSearchClientPoolSetup } from '../client'; import { parseClientOptions } from './client_config'; -import { createDataSourceError, DataSourceError } from '../lib/error'; +import { createDataSourceError } from '../lib/error'; import { getRootClient, getAWSCredential, @@ -195,13 +194,14 @@ const getBasicAuthClient = async ( }; const getAWSClient = (credential: SigV4Content, clientOptions: ConfigOptions): LegacyClient => { - const { accessKey, secretKey, region } = credential; + const { accessKey, secretKey, region, service } = credential; const client = new LegacyClient({ connectionClass: HttpAmazonESConnector, awsConfig: new Config({ region, credentials: new Credentials({ accessKeyId: accessKey, secretAccessKey: secretKey }), }), + service, ...clientOptions, }); return client; diff --git a/src/plugins/data_source/server/routes/data_source_connection_validator.ts b/src/plugins/data_source/server/routes/data_source_connection_validator.ts index ecec07dafcc4..f3233859e854 100644 --- a/src/plugins/data_source/server/routes/data_source_connection_validator.ts +++ b/src/plugins/data_source/server/routes/data_source_connection_validator.ts @@ -5,13 +5,19 @@ import { OpenSearchClient } from 'opensearch-dashboards/server'; import { createDataSourceError } from '../lib/error'; - +import { SigV4ServiceName } from '../../common/data_sources'; export class DataSourceConnectionValidator { - constructor(private readonly callDataCluster: OpenSearchClient) {} + constructor( + private readonly callDataCluster: OpenSearchClient, + private readonly dataSourceAttr: any + ) {} async validate() { try { - return await this.callDataCluster.info(); + // Amazon OpenSearch Serverless does not support .info() API + if (this.dataSourceAttr.auth?.credentials?.service === SigV4ServiceName.OpenSearchServerless) + return await this.callDataCluster.cat.indices(); + return await this.callDataCluster.info(); } catch (e) { throw createDataSourceError(e); } diff --git a/src/plugins/data_source/server/routes/test_connection.ts b/src/plugins/data_source/server/routes/test_connection.ts index f36702171b45..cba42517e535 100644 --- a/src/plugins/data_source/server/routes/test_connection.ts +++ b/src/plugins/data_source/server/routes/test_connection.ts @@ -5,7 +5,7 @@ import { schema } from '@osd/config-schema'; import { IRouter, OpenSearchClient } from 'opensearch-dashboards/server'; -import { AuthType, DataSourceAttributes } from '../../common/data_sources'; +import { AuthType, DataSourceAttributes, SigV4ServiceName } from '../../common/data_sources'; import { DataSourceConnectionValidator } from './data_source_connection_validator'; import { DataSourceServiceSetup } from '../data_source_service'; import { CryptographyServiceSetup } from '../cryptography_service'; @@ -40,6 +40,10 @@ export const registerTestConnectionRoute = ( region: schema.string(), accessKey: schema.string(), secretKey: schema.string(), + service: schema.oneOf([ + schema.literal(SigV4ServiceName.OpenSearch), + schema.literal(SigV4ServiceName.OpenSearchServerless), + ]), }), ]) ), @@ -61,9 +65,13 @@ export const registerTestConnectionRoute = ( testClientDataSourceAttr: dataSourceAttr as DataSourceAttributes, } ); - const dsValidator = new DataSourceConnectionValidator(dataSourceClient); - await dsValidator.validate(); + const dataSourceValidator = new DataSourceConnectionValidator( + dataSourceClient, + dataSourceAttr + ); + + await dataSourceValidator.validate(); return response.ok({ body: { diff --git a/src/plugins/data_source/server/saved_objects/data_source_saved_objects_client_wrapper.ts b/src/plugins/data_source/server/saved_objects/data_source_saved_objects_client_wrapper.ts index 6abcaa6c7909..12d60b8da51e 100644 --- a/src/plugins/data_source/server/saved_objects/data_source_saved_objects_client_wrapper.ts +++ b/src/plugins/data_source/server/saved_objects/data_source_saved_objects_client_wrapper.ts @@ -301,7 +301,7 @@ export class DataSourceSavedObjectsClientWrapper { ); } - const { accessKey, secretKey, region } = credentials as SigV4Content; + const { accessKey, secretKey, region, service } = credentials as SigV4Content; if (!accessKey) { throw SavedObjectsErrorHelpers.createBadRequestError( @@ -320,6 +320,12 @@ export class DataSourceSavedObjectsClientWrapper { '"auth.credentials.region" attribute is required' ); } + + if (!service) { + throw SavedObjectsErrorHelpers.createBadRequestError( + '"auth.credentials.service" attribute is required' + ); + } break; default: throw SavedObjectsErrorHelpers.createBadRequestError(`Invalid auth type: '${type}'`); @@ -457,7 +463,7 @@ export class DataSourceSavedObjectsClientWrapper { private async encryptSigV4Credential(auth: T, encryptionContext: EncryptionContext) { const { - credentials: { accessKey, secretKey, region }, + credentials: { accessKey, secretKey, region, service }, } = auth; return { @@ -466,6 +472,7 @@ export class DataSourceSavedObjectsClientWrapper { region, accessKey: await this.cryptography.encryptAndEncode(accessKey, encryptionContext), secretKey: await this.cryptography.encryptAndEncode(secretKey, encryptionContext), + service, }, }; } diff --git a/src/plugins/data_source_management/public/components/create_data_source_wizard/components/create_form/create_data_source_form.test.tsx b/src/plugins/data_source_management/public/components/create_data_source_wizard/components/create_form/create_data_source_form.test.tsx index efb3453b6f35..ad1d02c87db4 100644 --- a/src/plugins/data_source_management/public/components/create_data_source_wizard/components/create_form/create_data_source_form.test.tsx +++ b/src/plugins/data_source_management/public/components/create_data_source_wizard/components/create_form/create_data_source_form.test.tsx @@ -27,6 +27,7 @@ describe('Datasource Management: Create Datasource form', () => { let component: ReactWrapper, React.Component<{}, {}, any>>; const mockSubmitHandler = jest.fn(); const mockTestConnectionHandler = jest.fn(); + const mockCancelHandler = jest.fn(); const getFields = (comp: ReactWrapper, React.Component<{}, {}, any>>) => { return { @@ -65,6 +66,7 @@ describe('Datasource Management: Create Datasource form', () => { ), diff --git a/src/plugins/data_source_management/public/components/create_data_source_wizard/components/create_form/create_data_source_form.tsx b/src/plugins/data_source_management/public/components/create_data_source_wizard/components/create_form/create_data_source_form.tsx index 1c08da5d6371..8b36e13e7deb 100644 --- a/src/plugins/data_source_management/public/components/create_data_source_wizard/components/create_form/create_data_source_form.tsx +++ b/src/plugins/data_source_management/public/components/create_data_source_wizard/components/create_form/create_data_source_form.tsx @@ -5,7 +5,9 @@ import React from 'react'; import { + EuiBottomBar, EuiButton, + EuiButtonEmpty, EuiFieldPassword, EuiFieldText, EuiFlexGroup, @@ -19,13 +21,14 @@ import { } from '@elastic/eui'; import { i18n } from '@osd/i18n'; import { FormattedMessage } from '@osd/i18n/react'; +import { SigV4Content, SigV4ServiceName } from '../../../../../../data_source/common/data_sources'; import { AuthType, credentialSourceOptions, DataSourceAttributes, DataSourceManagementContextValue, UsernamePasswordTypedContent, - SigV4Content, + sigV4ServiceOptions, } from '../../../../types'; import { Header } from '../header'; import { context as contextType } from '../../../../../../opensearch_dashboards_react/public'; @@ -41,6 +44,7 @@ export interface CreateDataSourceProps { existingDatasourceNamesList: string[]; handleSubmit: (formValues: DataSourceAttributes) => void; handleTestConnection: (formValues: DataSourceAttributes) => void; + handleCancel: () => void; } export interface CreateDataSourceState { /* Validation */ @@ -121,7 +125,31 @@ export class CreateDataSourceForm extends React.Component< }; onChangeAuthType = (e: React.ChangeEvent) => { - this.setState({ auth: { ...this.state.auth, type: e.target.value as AuthType } }); + const authType = e.target.value as AuthType; + this.setState({ + auth: { + ...this.state.auth, + type: authType, + credentials: { + ...this.state.auth.credentials, + service: + (this.state.auth.credentials.service as SigV4ServiceName) || + SigV4ServiceName.OpenSearch, + }, + }, + }); + }; + + onChangeSigV4ServiceName = (e: React.ChangeEvent) => { + this.setState({ + auth: { + ...this.state.auth, + credentials: { + ...this.state.auth.credentials, + service: e.target.value as SigV4ServiceName, + }, + }, + }); }; onChangeUsername = (e: { target: { value: any } }) => { @@ -267,6 +295,7 @@ export class CreateDataSourceForm extends React.Component< region: this.state.auth.credentials.region, accessKey: this.state.auth.credentials.accessKey, secretKey: this.state.auth.credentials.secretKey, + service: this.state.auth.credentials.service || SigV4ServiceName.OpenSearch, } as SigV4Content; } @@ -391,6 +420,19 @@ export class CreateDataSourceForm extends React.Component< data-test-subj="createDataSourceFormRegionField" /> + + this.onChangeSigV4ServiceName(e)} + name="ServiceName" + data-test-subj="createDataSourceFormAuthTypeSelect" + /> + { return ( - - {this.renderHeader()} - - - {/* Endpoint section */} - {this.renderSectionHeader( - 'dataSourceManagement.connectToDataSource.connectionDetails', - 'Connection Details' - )} - - - {/* Title */} - - + + {this.renderHeader()} + + + {/* Endpoint section */} + {this.renderSectionHeader( + 'dataSourceManagement.connectToDataSource.connectionDetails', + 'Connection Details' + )} + + + {/* Title */} + - + error={this.state.formErrorsByField.title} + > + + - {/* Description */} - - - - - {/* Endpoint URL */} - - + + + + {/* Endpoint URL */} + - + error={this.state.formErrorsByField.endpoint} + > + + - {/* Authentication Section: */} + {/* Authentication Section: */} - + - {this.renderSectionHeader( - 'dataSourceManagement.connectToDataSource.authenticationHeader', - 'Authentication Method' - )} - + {this.renderSectionHeader( + 'dataSourceManagement.connectToDataSource.authenticationHeader', + 'Authentication Method' + )} + - - - - + + - - - - - {/* Credential source */} - - - this.onChangeAuthType(e)} - name="Credential" - data-test-subj="createDataSourceFormAuthTypeSelect" - /> - - - {/* Create New credentials */} - {this.state.auth.type === AuthType.UsernamePasswordType - ? this.renderCreateNewCredentialsForm(this.state.auth.type) - : null} - - {this.state.auth.type === AuthType.SigV4 - ? this.renderCreateNewCredentialsForm(this.state.auth.type) - : null} - - - - - - {/* Test Connection button*/} - - - - - {/* Create Data Source button*/} - - + - - - - - - + + + + + {/* Credential source */} + + + this.onChangeAuthType(e)} + name="Credential" + data-test-subj="createDataSourceFormAuthTypeSelect" + /> + + + {/* Create New credentials */} + {this.state.auth.type === AuthType.UsernamePasswordType + ? this.renderCreateNewCredentialsForm(this.state.auth.type) + : null} + + {this.state.auth.type === AuthType.SigV4 + ? this.renderCreateNewCredentialsForm(this.state.auth.type) + : null} + + + + + + {/* Test Connection button*/} + + + + + + + + + + + {this.renderBottomBar()} + + ); + }; + + renderBottomBar = () => { + return ( + + + + + + + + + + + + + + + + ); }; diff --git a/src/plugins/data_source_management/public/components/create_data_source_wizard/create_data_source_wizard.test.tsx b/src/plugins/data_source_management/public/components/create_data_source_wizard/create_data_source_wizard.test.tsx index 162af4c891f7..adfbe8808637 100644 --- a/src/plugins/data_source_management/public/components/create_data_source_wizard/create_data_source_wizard.test.tsx +++ b/src/plugins/data_source_management/public/components/create_data_source_wizard/create_data_source_wizard.test.tsx @@ -46,6 +46,7 @@ describe('Datasource Management: Create Datasource Wizard', () => { }); component.update(); }); + test('should create datasource successfully', async () => { spyOn(utils, 'createSingleDataSource').and.returnValue({}); @@ -58,6 +59,7 @@ describe('Datasource Management: Create Datasource Wizard', () => { expect(utils.createSingleDataSource).toHaveBeenCalled(); expect(history.push).toBeCalledWith(''); }); + test('should fail to create datasource', async () => { spyOn(utils, 'createSingleDataSource').and.throwError('error'); await act(async () => { @@ -93,7 +95,17 @@ describe('Datasource Management: Create Datasource Wizard', () => { component.update(); expect(utils.testConnection).toHaveBeenCalled(); }); + + test('should go back to listing page if clicked on cancel button', async () => { + await act(async () => { + // @ts-ignore + await component.find(formIdentifier).first().prop('handleCancel')(); + }); + + expect(history.push).toBeCalledWith(''); + }); }); + describe('case2: should fail to load resources', () => { beforeEach(async () => { spyOn(utils, 'getDataSources').and.throwError(''); @@ -116,6 +128,7 @@ describe('Datasource Management: Create Datasource Wizard', () => { }); component.update(); }); + test('should not render component and go back to listing page', () => { expect(history.push).toBeCalledWith(''); }); diff --git a/src/plugins/data_source_management/public/components/create_data_source_wizard/create_data_source_wizard.tsx b/src/plugins/data_source_management/public/components/create_data_source_wizard/create_data_source_wizard.tsx index 83477b7a2426..05489ca6258a 100644 --- a/src/plugins/data_source_management/public/components/create_data_source_wizard/create_data_source_wizard.tsx +++ b/src/plugins/data_source_management/public/components/create_data_source_wizard/create_data_source_wizard.tsx @@ -116,6 +116,7 @@ export const CreateDataSourceWizard: React.FunctionComponent props.history.push('')} existingDatasourceNamesList={existingDatasourceNamesList} /> {isLoading ? : null} diff --git a/src/plugins/data_source_management/public/components/edit_data_source/components/edit_form/edit_data_source_form.tsx b/src/plugins/data_source_management/public/components/edit_data_source/components/edit_form/edit_data_source_form.tsx index 8e3128c7e987..2ea63e252295 100644 --- a/src/plugins/data_source_management/public/components/edit_data_source/components/edit_form/edit_data_source_form.tsx +++ b/src/plugins/data_source_management/public/components/edit_data_source/components/edit_form/edit_data_source_form.tsx @@ -23,13 +23,14 @@ import { } from '@elastic/eui'; import { i18n } from '@osd/i18n'; import { FormattedMessage } from '@osd/i18n/react'; +import { SigV4Content, SigV4ServiceName } from '../../../../../../data_source/common/data_sources'; import { Header } from '../header'; import { AuthType, credentialSourceOptions, DataSourceAttributes, DataSourceManagementContextValue, - SigV4Content, + sigV4ServiceOptions, ToastMessageItem, UsernamePasswordTypedContent, } from '../../../../types'; @@ -46,9 +47,9 @@ import { UpdateAwsCredentialModal } from '../update_aws_credential_modal'; export interface EditDataSourceProps { existingDataSource: DataSourceAttributes; existingDatasourceNamesList: string[]; - handleSubmit: (formValues: DataSourceAttributes) => void; - handleTestConnection: (formValues: DataSourceAttributes) => void; - onDeleteDataSource?: () => void; + handleSubmit: (formValues: DataSourceAttributes) => Promise; + handleTestConnection: (formValues: DataSourceAttributes) => Promise; + onDeleteDataSource?: () => Promise; displayToastMessage: (info: ToastMessageItem) => void; } export interface EditDataSourceState { @@ -123,7 +124,10 @@ export class EditDataSourceForm extends React.Component { const isValid = !!this.state.auth.credentials.username?.trim().length; this.setState({ @@ -221,6 +226,18 @@ export class EditDataSourceForm extends React.Component) => { + this.setState({ + auth: { + ...this.state.auth, + credentials: { + ...this.state.auth.credentials, + service: e.target.value as SigV4ServiceName, + } as SigV4Content, + }, + }); + }; + onChangeRegion = (e: { target: { value: any } }) => { this.setState({ auth: { @@ -253,7 +270,7 @@ export class EditDataSourceForm extends React.Component { - const isValid = !!this.state.auth.credentials.accessKey; + const isValid = !!this.state.auth.credentials?.accessKey; this.setState({ formErrorsByField: { ...this.state.formErrorsByField, @@ -275,7 +292,7 @@ export class EditDataSourceForm extends React.Component { - const isValid = !!this.state.auth.credentials.secretKey; + const isValid = !!this.state.auth.credentials?.secretKey; this.setState({ formErrorsByField: { ...this.state.formErrorsByField, @@ -338,9 +355,9 @@ export class EditDataSourceForm extends React.Component { + onClickDeleteDataSource = async () => { if (this.props.onDeleteDataSource) { - this.props.onDeleteDataSource(); + await this.props.onDeleteDataSource(); } }; @@ -359,6 +376,7 @@ export class EditDataSourceForm extends React.Component @@ -788,6 +807,19 @@ export class EditDataSourceForm extends React.Component + + this.onChangeSigV4ServiceName(e)} + name="ServiceName" + data-test-subj="createDataSourceFormAuthTypeSelect" + /> + void; closeUpdateAwsCredentialModal: () => void; } export const UpdateAwsCredentialModal = ({ region, + service, handleUpdateAwsCredential, closeUpdateAwsCredentialModal, }: UpdateAwsCredentialModalProps) => { @@ -87,6 +91,16 @@ export const UpdateAwsCredentialModal = ({ + {/* Service Name */} + + + {sigV4ServiceOptions.find((option) => option.value === service)?.text} + + {/* Region */} Date: Tue, 23 May 2023 01:10:06 -0700 Subject: [PATCH 34/35] Remove Sass from `tile_map` plugin (#4110) * Remove Sass from tile_map plugin Signed-off-by: Matt Provost * Update changelog Signed-off-by: Matt Provost --------- Signed-off-by: Matt Provost --- CHANGELOG.md | 1 + src/plugins/tile_map/public/_tile_map.scss | 15 --------------- src/plugins/tile_map/public/index.scss | 8 -------- 3 files changed, 1 insertion(+), 23 deletions(-) delete mode 100644 src/plugins/tile_map/public/_tile_map.scss delete mode 100644 src/plugins/tile_map/public/index.scss diff --git a/CHANGELOG.md b/CHANGELOG.md index fa5a9f577a40..ba97e49b658e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -201,6 +201,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [Multiple DataSource] Present the authentication type choices in a drop-down ([#3693](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3693)) - [Console] Remove unused ul element and its custom styling ([#3993](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3993)) - Fix EUI/OUI type errors ([#3798](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3798)) +- Remove unused Sass in `tile_map` plugin ([#4110](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4110)) ### 🔩 Tests diff --git a/src/plugins/tile_map/public/_tile_map.scss b/src/plugins/tile_map/public/_tile_map.scss deleted file mode 100644 index 5e4b20f79fed..000000000000 --- a/src/plugins/tile_map/public/_tile_map.scss +++ /dev/null @@ -1,15 +0,0 @@ -// SASSTODO: Does this selector exist today? -.tilemap { - margin-bottom: 6px; - border: $euiBorderThin; - position: relative; -} - -/** -* 1. Visualizations have some padding by default but tilemaps look nice flush against the edge to maximize viewing -* space. -*/ -// SASSTODO: Does this selector exist today? -.tile_map { - padding: 0; /* 1. */ -} diff --git a/src/plugins/tile_map/public/index.scss b/src/plugins/tile_map/public/index.scss deleted file mode 100644 index aa2117cb0c01..000000000000 --- a/src/plugins/tile_map/public/index.scss +++ /dev/null @@ -1,8 +0,0 @@ -// Prefix all styles with "tlm" to avoid conflicts. -// Examples -// tlmChart -// tlmChart__legend -// tlmChart__legend--small -// tlmChart__legend-isLoading - -@import "tile_map"; From 61ea841e4eab621d86aed9274c95b1bcc2f66c55 Mon Sep 17 00:00:00 2001 From: Bandini <63824432+bandinib-amzn@users.noreply.github.com> Date: Tue, 23 May 2023 09:59:25 -0700 Subject: [PATCH 35/35] Design for New Saved Object Service Interface for Custom Repository (#3954) * Adds design document for new saved object service interface for custom repository Signed-off-by: Bandini Bhopi --- CHANGELOG.md | 1 + .../current_saved_object_service_workflow.png | Bin 0 -> 26471 bytes ...proposed_saved_object_service_workflow.png | Bin 0 -> 53121 bytes ...current_saved_object_service_workflow.puml | 19 ++ ...roposed_saved_object_service_workflow.puml | 38 ++++ .../saved_object_repository_factory_design.md | 171 ++++++++++++++++++ 6 files changed, 229 insertions(+) create mode 100644 docs/saved_objects/img/current_saved_object_service_workflow.png create mode 100644 docs/saved_objects/img/proposed_saved_object_service_workflow.png create mode 100644 docs/saved_objects/resources/current_saved_object_service_workflow.puml create mode 100644 docs/saved_objects/resources/proposed_saved_object_service_workflow.puml create mode 100644 docs/saved_objects/saved_object_repository_factory_design.md diff --git a/CHANGELOG.md b/CHANGELOG.md index ba97e49b658e..cfa5aebd2ab5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -172,6 +172,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [Doc] Update DEVELOPER_GUIDE.md with added manual bootstrap timeout solution and max virtual memory error solution with docker ([#3764](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3764)) - [Doc] Add COMMUNICATIONS.md with info about Slack, forum, office hours ([#3837](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3837)) - [Doc] Add docker files and instructions for debugging Selenium functional tests ([#3747](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3747)) +- [Saved Object Service] Adds design doc for new Saved Object Service Interface for Custom Repository [#3954](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3954) ### 🛠 Maintenance diff --git a/docs/saved_objects/img/current_saved_object_service_workflow.png b/docs/saved_objects/img/current_saved_object_service_workflow.png new file mode 100644 index 0000000000000000000000000000000000000000..b763101b698793e87822e61d5fca0b2915d4e2f9 GIT binary patch literal 26471 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≦{#&}0Zam}ca?$w|v=~w{ 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 literal 0 HcmV?d00001 diff --git a/docs/saved_objects/resources/current_saved_object_service_workflow.puml b/docs/saved_objects/resources/current_saved_object_service_workflow.puml new file mode 100644 index 000000000000..bc5bbf82c621 --- /dev/null +++ b/docs/saved_objects/resources/current_saved_object_service_workflow.puml @@ -0,0 +1,19 @@ +@startuml +title: Current Saved Object Service Flow +actor User +participant "Saved Object Client" as Client +participant "Saved Object Repository" as Repo +participant "Opensearch" as OS + +User -> Client: Create Saved Object +Client -> Repo: Create Saved Object +Repo -> OS: Index Saved Object +OS --> Repo: Saved Object Saved +Client -> User: Saved Object Created +User -> Client: Get Saved Object +Client -> Repo: Get Saved Object +Repo -> OS: Get Saved Object +OS --> Repo: Return Saved Object +Repo -> Client: Return Saved Object +Client -> User: Saved Object Data +@enduml \ No newline at end of file diff --git a/docs/saved_objects/resources/proposed_saved_object_service_workflow.puml b/docs/saved_objects/resources/proposed_saved_object_service_workflow.puml new file mode 100644 index 000000000000..27a5e1cd49ed --- /dev/null +++ b/docs/saved_objects/resources/proposed_saved_object_service_workflow.puml @@ -0,0 +1,38 @@ +@startuml + +title: Proposed Saved Object Service Flow + +actor User + +participant "OpenSearch-Dashboards" as OSD + +box "Saved Object Service" #LightBlue +participant "Saved Object Client" as Client +participant "Repository Factory Provider" as Factory +end box + +box "Dashboards Storage Plugin" #LightYellow +participant "Repository\n(e.g. PostgresRepository,\nDynamoDBRepository)" as Repo +participant "Metadata Storage\n(e.g. Postgres, \nDynamoDB etc)" as Meta +end box + +autonumber +group OSD Bootstrap +Repo -> Factory: Register custom repository +Factory -> Client: Returns repository +Client -> OSD: Returns Saved Object Client +end group +User -> Client: Create Saved Object +Client -> Repo: Create Saved Object +Repo -> Meta: Create/Update Record +Meta --> Repo: Saved Object Saved +Client -> User: Saved Object Created +User -> Client: Get Saved Object +Client -> Repo: Get Saved Object +Repo -> Meta: Fetch Saved Object from storage +Meta --> Repo: Return Saved Object +Repo -> Client: Return Saved Object +Client -> User: Saved Object Data + +skinparam BoxPadding 15 +@enduml \ No newline at end of file diff --git a/docs/saved_objects/saved_object_repository_factory_design.md b/docs/saved_objects/saved_object_repository_factory_design.md new file mode 100644 index 000000000000..d3fedd3575dc --- /dev/null +++ b/docs/saved_objects/saved_object_repository_factory_design.md @@ -0,0 +1,171 @@ +# Proposed Saved Object Service Interface for Custom Repository + +## Introduction + +The new saved object service interface for custom repository is a project that aims to improve scalability of the existing saved object service by introducing a new interface. The goal of this project is to provide a more efficient and flexible interface that will make it easier for developers to configure metadata of Dashboards in any different storage than OpenSearch, such as mysql, postgres, DDB, serverless (S3+ Athena). + +Currently, Dashboards stores its metadata configuration inside OpenSearch index (called .kibana). This approach is by design of Dashboards and biased towards product decision by upstream which works seamlessly and out of the box for customers but it introduces challenges while operating at scale and providing high availability for Dashboards. While choosing OpenSearch as a storage for Dashboards metadata, availability of Dashboards depends on OpenSearch cluster’s availability and other cluster parameters such as cluster health, state, versions which could make Dashboards unavailable. + +To mitigate above problem and unblock future extensibility of Dashboards, we are building Dashboards Meta storage adaptor to decouple Dashboards metadata storage from OpenSearch. This project will focus on introducing new interface in Saved Object Service using which developer can build their custom repository and save Dashboards metadata in storage of their choice. + +The stakeholders of this new interface include the developers of the Dashboards and community contributors who wants to use other metadata store. + +## Architecture Overview + +The Saved Object Service is a critical component of Dashboards that provides a way to store and manage application data. It is built using a modular architecture that provides a high degree of flexibility and extensibility. The new interface will be designed to replace [ISavedObjectRepository](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/main/src/core/server/saved_objects/service/lib/repository.ts#L134) implementation so that developers can build plugins that leverage the power of existing saved object service and use their own database to store and retrieve metadata of OpenSearch Dashboards. + +### Current Architecture + +The repository interface named [ISavedObjectRepository](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/main/src/core/server/saved_objects/service/lib/repository.ts#L134) in OpenSearch-Dashboards is a module that provides an interface for managing saved objects. The [SavedObjectRepository](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/main/src/core/server/saved_objects/service/lib/repository.ts#L139) is the implementation of [ISavedObjectRepository](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/main/src/core/server/saved_objects/service/lib/repository.ts#L134), which uses OpenSearch index as it’s data store. It is responsible for storing, retrieving, and deleting saved objects for Dashboards, such as visualizations, dashboards, and searches. + +The Saved Object Repository is built on top of the OpenSearch client and provides a simplified interface for working with OpenSearch. It uses the Saved Object Serializer to convert saved objects between their internal and external representations. The repository is then being consumed by Saved object client to create scoped saved object client. + +![img](./img/current_saved_object_service_workflow.png) + +### Proposed Architecture + +- **Approach 1 (Preferred)**: The proposed architecture will add one more layer of abstraction in Saved Object Service. `The Repository Factory Provider` in OpenSearch Dashboards will be responsible for creating and managing instances of the Repository (e.g. SavedObjectRepository, PostgresRepository, DynamoDBRepository etc.), which is used to interact with the metadata storage that stores the saved objects. Currently we have an repository interface named [ISavedObjectRepository](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/main/src/core/server/saved_objects/service/lib/repository.ts#L134), and the [SavedObjectRepository](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/main/src/core/server/saved_objects/service/lib/repository.ts#L139) is the implementation, which use an OpenSearch index as its data store. This approach would make the implementation of [ISavedObjectRepository](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/main/src/core/server/saved_objects/service/lib/repository.ts#L134) replaceable by plugin. + + ![img](./img/proposed_saved_object_service_workflow.png) + + * Pros: + * Only change needed in Dashboard is to introduce one more abstraction layer in Saved Object Service. + * Adds opportunity for community developers to contribute for other meta store. + + * Cons + * Code reusability is low. +
    + +**POC**: +1) Core Dashboards Change: https://github.com/bandinib-amzn/OpenSearch-Dashboards/commit/b9cfc14 +2) Postgres Repository Plugin: https://github.com/bandinib-amzn/metadata_plugin/commit/dac35f0 + +`SavedObjectsServiceSetup` provides interface to create custom Saved Object Repository. +``` +/** +* Set the default {@link SavedObjectRepositoryFactoryProvider | factory provider} for creating Saved Objects repository. +* Only one repository can be set, subsequent calls to this method will fail. +*/ +registerRepositoryFactoryProvider: ( +respositoryFactoryProvider: SavedObjectRepositoryFactoryProvider +) => void; +``` + +Here are the main steps involved in using the Saved Objects Repository Factory in Dashboards: +1. Define the dependencies: The Saved Object Repository Factory Provider requires the function which creates instance of [ISavedObjectRepository](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/main/src/core/server/saved_objects/service/lib/repository.ts#L134). + ``` + export const repositoryFactoryProvider: SavedObjectRepositoryFactoryProvider = ( + options: SavedObjectsRepositoryOptions + ) => { + . + . + . + return new PostgresRepository({ + typeRegistry, + serializer, + migrator, + allowedTypes, + }); + } + ``` +2. Register the provider: Register the repository factory provider with right dependencies. + ``` + core.savedObjects.registerRepositoryFactoryProvider(repositoryFactoryProvider); + ``` +3. Implement the Saved Object Operations for chosen storage type: Implement the CRUD and other operations for contracts defined in [ISavedObjectRepository](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/main/src/core/server/saved_objects/service/lib/repository.ts#L134) + ``` + async create( + type: string, + attributes: T, + options: SavedObjectsCreateOptions = {} + ): Promise> { + ... + } + + async get( + type: string, + id: string, + options: SavedObjectsBaseOptions = {} + ): Promise> { + ... + } + + async update( + type: string, + id: string, + attributes: Partial, + options: SavedObjectsUpdateOptions = {} + ): Promise> { + ... + } + + async deleteFromNamespaces( + type: string, + id: string, + namespaces: string[], + options: SavedObjectsDeleteFromNamespacesOptions = {} + ): Promise { + ... + } + . + . + . + ``` + +- **Approach 2**: Build external plugin and using saved object client wrapper or client factory provider injection mechanism we can build custom object for Postgres or other DB. + + * Pros: + * No changes in core Dashboards. That means we can keep Dashboards as it is with very minimal changes. + + + * Cons + * Code reusability is low. + * Some components of Saved object service such as Serializer, Type registry, interface to create internal and scoped repository are only available during Saved Object Service Start. As per the current architecture, first Saved Object Service Setup → Plugin Setup → Saved Object Service Start → Plugin Start. Some core plugin (e.g. opensearch_dashboards_usage_collection) calls find operation before plugin start and it fails because some components are still not available before plugin start. +
    + + **POC**: https://github.com/bandinib-amzn/metadata_plugin/compare/f040daf...89213eb + + +- **Approach 3**: In this approach, we just extend the `SavedObjectsRepository` class and override CRUD and other saved object operation in core Dashboards. + + * Pros: + * As we are extending the repository in core saved object service itself, we can reuse the validation and utility functions for other database options. + + + * Cons + * Changes in core Dashboards : We will be making considerable changes in critical component of Dashboards. + * With this approach, user will have to use the data storage option that we choose. +
    + + **POC**: https://github.com/bandinib-amzn/OpenSearch-Dashboards/compare/main...22d7f30 + +## Implementation Details + + +| Repository | Component | Change | +| ----------- | ----------- | ----------- | +| OpenSearch-Dashboards | Saved Object Service | Add Saved object repository factory provider | +| OpenSearch-Dashboards | Config | Configuration for metadata storage | +| MetaStorage-Plugin [Name TBD] | Plugin / Extension | We will build new plugin for Postgres. This is use case for new interface in Saved Object Repository. | + +### Configuration for metadata storage: +``` +metaStorage.enabled: true +metaStorage.config: { + type: 'xxxx', + hostName: 'xxxx', + userName: 'xxxx', + password: 'xxxx', + port: xxxx, +} +``` + +## Testing and Quality Assurance + +### Testing Approach + +The following testing approach will be used to ensure the quality of the system: + +1. **Unit testing**: Metadata store plugin will be thoroughly unit tested to ensure it meets its requirements and performs as expected. Also we will add new test cases in OpenSearch-Dashboards to test new repository factory provider. +2. **Integration testing**: Components will be integrated and tested together to ensure they work together seamlessly and without conflicts. +