From b31206a83833cfb66867d0eb4f3a83f0ddb8ec0c Mon Sep 17 00:00:00 2001 From: yuboluo Date: Mon, 2 Dec 2024 15:56:34 +0800 Subject: [PATCH 01/16] [Workspace] Isolate objects based on workspace when calling get/bulkGet (#8888) * Isolate objects based on workspace when calling get/bulkGet Signed-off-by: yubonluo * Changeset file for PR #8888 created/updated * add integration tests Signed-off-by: yubonluo * optimize the code Signed-off-by: yubonluo * optimize the code Signed-off-by: yubonluo * optimize the code Signed-off-by: yubonluo * optimize the function name Signed-off-by: yubonluo * add data source validate Signed-off-by: yubonluo * optimize the code Signed-off-by: yubonluo --------- Signed-off-by: yubonluo Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> --- changelogs/fragments/8888.yml | 2 + .../workspace_id_consumer_wrapper.test.ts | 154 ++++++ .../workspace_id_consumer_wrapper.test.ts | 492 ++++++++++++++++++ .../workspace_id_consumer_wrapper.ts | 98 +++- ...space_saved_objects_client_wrapper.test.ts | 281 ---------- .../workspace_saved_objects_client_wrapper.ts | 58 --- 6 files changed, 744 insertions(+), 341 deletions(-) create mode 100644 changelogs/fragments/8888.yml diff --git a/changelogs/fragments/8888.yml b/changelogs/fragments/8888.yml new file mode 100644 index 000000000000..cf22e39bf062 --- /dev/null +++ b/changelogs/fragments/8888.yml @@ -0,0 +1,2 @@ +refactor: +- [Workspace] Isolate objects based on workspace when calling get/bulkGet ([#8888](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8888)) \ No newline at end of file diff --git a/src/plugins/workspace/server/saved_objects/integration_tests/workspace_id_consumer_wrapper.test.ts b/src/plugins/workspace/server/saved_objects/integration_tests/workspace_id_consumer_wrapper.test.ts index c8212d9cc6b1..c762d08cedff 100644 --- a/src/plugins/workspace/server/saved_objects/integration_tests/workspace_id_consumer_wrapper.test.ts +++ b/src/plugins/workspace/server/saved_objects/integration_tests/workspace_id_consumer_wrapper.test.ts @@ -36,6 +36,8 @@ describe('workspace_id_consumer integration test', () => { let createdBarWorkspace: WorkspaceAttributes = { id: '', }; + const deleteWorkspace = (workspaceId: string) => + osdTestServer.request.delete(root, `/api/workspaces/${workspaceId}`); beforeAll(async () => { const { startOpenSearch, startOpenSearchDashboards } = osdTestServer.createTestServers({ adjustTimeout: (t: number) => jest.setTimeout(t), @@ -75,6 +77,10 @@ describe('workspace_id_consumer integration test', () => { }).then((resp) => resp.body.result); }, 30000); afterAll(async () => { + await Promise.all([ + deleteWorkspace(createdFooWorkspace.id), + deleteWorkspace(createdBarWorkspace.id), + ]); await root.shutdown(); await opensearchServer.stop(); }); @@ -312,5 +318,153 @@ describe('workspace_id_consumer integration test', () => { expect(importWithWorkspacesResult.body.success).toEqual(true); expect(findResult.body.saved_objects[0].workspaces).toEqual([createdFooWorkspace.id]); }); + + it('get', async () => { + await clearFooAndBar(); + await osdTestServer.request.delete( + root, + `/api/saved_objects/${config.type}/${packageInfo.version}` + ); + const createResultFoo = await osdTestServer.request + .post(root, `/w/${createdFooWorkspace.id}/api/saved_objects/_bulk_create`) + .send([ + { + ...dashboard, + id: 'foo', + }, + ]) + .expect(200); + + const createResultBar = await osdTestServer.request + .post(root, `/w/${createdBarWorkspace.id}/api/saved_objects/_bulk_create`) + .send([ + { + ...dashboard, + id: 'bar', + }, + ]) + .expect(200); + + await osdTestServer.request + .post(root, `/api/saved_objects/${config.type}/${packageInfo.version}`) + .send({ + attributes: { + legacyConfig: 'foo', + }, + }) + .expect(200); + + const getResultWithRequestWorkspace = await osdTestServer.request + .get(root, `/w/${createdFooWorkspace.id}/api/saved_objects/${dashboard.type}/foo`) + .expect(200); + expect(getResultWithRequestWorkspace.body.id).toEqual('foo'); + expect(getResultWithRequestWorkspace.body.workspaces).toEqual([createdFooWorkspace.id]); + + const getResultWithoutRequestWorkspace = await osdTestServer.request + .get(root, `/api/saved_objects/${dashboard.type}/bar`) + .expect(200); + expect(getResultWithoutRequestWorkspace.body.id).toEqual('bar'); + + const getGlobalResultWithinWorkspace = await osdTestServer.request + .get( + root, + `/w/${createdFooWorkspace.id}/api/saved_objects/${config.type}/${packageInfo.version}` + ) + .expect(200); + expect(getGlobalResultWithinWorkspace.body.id).toEqual(packageInfo.version); + + await osdTestServer.request + .get(root, `/w/${createdFooWorkspace.id}/api/saved_objects/${dashboard.type}/bar`) + .expect(403); + + await Promise.all( + [...createResultFoo.body.saved_objects, ...createResultBar.body.saved_objects].map((item) => + deleteItem({ + type: item.type, + id: item.id, + }) + ) + ); + await osdTestServer.request.delete( + root, + `/api/saved_objects/${config.type}/${packageInfo.version}` + ); + }); + + it('bulk get', async () => { + await clearFooAndBar(); + const createResultFoo = await osdTestServer.request + .post(root, `/w/${createdFooWorkspace.id}/api/saved_objects/_bulk_create`) + .send([ + { + ...dashboard, + id: 'foo', + }, + ]) + .expect(200); + + const createResultBar = await osdTestServer.request + .post(root, `/w/${createdBarWorkspace.id}/api/saved_objects/_bulk_create`) + .send([ + { + ...dashboard, + id: 'bar', + }, + ]) + .expect(200); + + const payload = [ + { id: 'foo', type: 'dashboard' }, + { id: 'bar', type: 'dashboard' }, + ]; + const bulkGetResultWithWorkspace = await osdTestServer.request + .post(root, `/w/${createdFooWorkspace.id}/api/saved_objects/_bulk_get`) + .send(payload) + .expect(200); + + expect(bulkGetResultWithWorkspace.body.saved_objects.length).toEqual(2); + expect(bulkGetResultWithWorkspace.body.saved_objects[0].id).toEqual('foo'); + expect(bulkGetResultWithWorkspace.body.saved_objects[0].workspaces).toEqual([ + createdFooWorkspace.id, + ]); + expect(bulkGetResultWithWorkspace.body.saved_objects[0]?.error).toBeUndefined(); + expect(bulkGetResultWithWorkspace.body.saved_objects[1].id).toEqual('bar'); + expect(bulkGetResultWithWorkspace.body.saved_objects[1].workspaces).toEqual([ + createdBarWorkspace.id, + ]); + expect(bulkGetResultWithWorkspace.body.saved_objects[1]?.error).toMatchInlineSnapshot(` + Object { + "error": "Forbidden", + "message": "Saved object does not belong to the workspace", + "statusCode": 403, + } + `); + + const bulkGetResultWithoutWorkspace = await osdTestServer.request + .post(root, `/api/saved_objects/_bulk_get`) + .send(payload) + .expect(200); + + expect(bulkGetResultWithoutWorkspace.body.saved_objects.length).toEqual(2); + expect(bulkGetResultWithoutWorkspace.body.saved_objects[0].id).toEqual('foo'); + expect(bulkGetResultWithoutWorkspace.body.saved_objects[0].workspaces).toEqual([ + createdFooWorkspace.id, + ]); + expect(bulkGetResultWithoutWorkspace.body.saved_objects[0]?.error).toBeUndefined(); + expect(bulkGetResultWithoutWorkspace.body.saved_objects[1].id).toEqual('bar'); + expect(bulkGetResultWithoutWorkspace.body.saved_objects[1].workspaces).toEqual([ + createdBarWorkspace.id, + ]); + expect(bulkGetResultWithoutWorkspace.body.saved_objects[1]?.error).toBeUndefined(); + + await Promise.all( + [...createResultFoo.body.saved_objects, ...createResultBar.body.saved_objects].map((item) => + deleteItem({ + type: item.type, + id: item.id, + }) + ) + ); + }); }); }); diff --git a/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.test.ts b/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.test.ts index 570d701d7c63..ca19ffc927ad 100644 --- a/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.test.ts +++ b/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.test.ts @@ -8,6 +8,7 @@ import { SavedObject } from '../../../../core/public'; import { httpServerMock, savedObjectsClientMock, coreMock } from '../../../../core/server/mocks'; import { WorkspaceIdConsumerWrapper } from './workspace_id_consumer_wrapper'; import { workspaceClientMock } from '../workspace_client.mock'; +import { SavedObjectsErrorHelpers } from '../../../../core/server'; describe('WorkspaceIdConsumerWrapper', () => { const requestHandlerContext = coreMock.createRequestHandlerContext(); @@ -196,4 +197,495 @@ describe('WorkspaceIdConsumerWrapper', () => { }); }); }); + + describe('get', () => { + beforeEach(() => { + mockedClient.get.mockClear(); + }); + + it(`Should get object belonging to options.workspaces`, async () => { + const savedObject = { + type: 'dashboard', + id: 'dashboard_id', + attributes: {}, + references: [], + workspaces: ['foo'], + }; + mockedClient.get.mockResolvedValueOnce(savedObject); + const result = await wrapperClient.get(savedObject.type, savedObject.id, { + workspaces: savedObject.workspaces, + }); + expect(mockedClient.get).toBeCalledWith(savedObject.type, savedObject.id, { + workspaces: savedObject.workspaces, + }); + expect(result).toEqual(savedObject); + }); + + it(`Should get object belonging to the workspace in request`, async () => { + const savedObject = { + type: 'dashboard', + id: 'dashboard_id', + attributes: {}, + references: [], + workspaces: ['foo'], + }; + mockedClient.get.mockResolvedValueOnce(savedObject); + const result = await wrapperClient.get(savedObject.type, savedObject.id); + expect(mockedClient.get).toBeCalledWith(savedObject.type, savedObject.id, {}); + expect(result).toEqual(savedObject); + }); + + it(`Should get object if the object type is workspace`, async () => { + const savedObject = { + type: 'workspace', + id: 'workspace_id', + attributes: {}, + references: [], + }; + mockedClient.get.mockResolvedValueOnce(savedObject); + const result = await wrapperClient.get(savedObject.type, savedObject.id); + expect(mockedClient.get).toBeCalledWith(savedObject.type, savedObject.id, {}); + expect(result).toEqual(savedObject); + }); + + it(`Should get object if the object type is config`, async () => { + const savedObject = { + type: 'config', + id: 'config_id', + attributes: {}, + references: [], + }; + mockedClient.get.mockResolvedValueOnce(savedObject); + const result = await wrapperClient.get(savedObject.type, savedObject.id); + expect(mockedClient.get).toBeCalledWith(savedObject.type, savedObject.id, {}); + expect(result).toEqual(savedObject); + }); + + it(`Should get object when there is no workspace in options/request`, async () => { + const workspaceIdConsumerWrapper = new WorkspaceIdConsumerWrapper(mockedWorkspaceClient); + const mockRequest = httpServerMock.createOpenSearchDashboardsRequest(); + updateWorkspaceState(mockRequest, {}); + const mockedWrapperClient = workspaceIdConsumerWrapper.wrapperFactory({ + client: mockedClient, + typeRegistry: requestHandlerContext.savedObjects.typeRegistry, + request: mockRequest, + }); + const savedObject = { + type: 'dashboard', + id: 'dashboard_id', + attributes: {}, + references: [], + }; + mockedClient.get.mockResolvedValueOnce(savedObject); + const result = await mockedWrapperClient.get(savedObject.type, savedObject.id); + expect(mockedClient.get).toBeCalledWith(savedObject.type, savedObject.id, {}); + expect(result).toEqual(savedObject); + }); + + it(`Should throw error when the object is not belong to the workspace`, async () => { + const savedObject = { + type: 'dashboard', + id: 'dashboard_id', + attributes: {}, + references: [], + workspaces: ['bar'], + }; + mockedClient.get.mockResolvedValueOnce(savedObject); + expect(wrapperClient.get(savedObject.type, savedObject.id)).rejects.toMatchInlineSnapshot( + `[Error: Saved object does not belong to the workspace]` + ); + expect(mockedClient.get).toBeCalledWith(savedObject.type, savedObject.id, {}); + }); + + it(`Should throw error when the object does not exist`, async () => { + mockedClient.get.mockRejectedValueOnce(SavedObjectsErrorHelpers.createGenericNotFoundError()); + expect(wrapperClient.get('type', 'id')).rejects.toMatchInlineSnapshot(`[Error: Not Found]`); + expect(mockedClient.get).toHaveBeenCalledTimes(1); + }); + + it(`Should throw error when the options.workspaces has more than one workspace.`, async () => { + const savedObject = { + type: 'dashboard', + id: 'dashboard_id', + attributes: {}, + references: [], + workspaces: ['bar'], + }; + const options = { workspaces: ['foo', 'bar'] }; + expect( + wrapperClient.get(savedObject.type, savedObject.id, options) + ).rejects.toMatchInlineSnapshot(`[Error: Multiple workspace parameters: Bad Request]`); + expect(mockedClient.get).not.toBeCalled(); + }); + + it(`Should get data source when user is data source admin`, async () => { + const workspaceIdConsumerWrapper = new WorkspaceIdConsumerWrapper(mockedWorkspaceClient); + const mockRequest = httpServerMock.createOpenSearchDashboardsRequest(); + updateWorkspaceState(mockRequest, { isDataSourceAdmin: true, requestWorkspaceId: 'foo' }); + const mockedWrapperClient = workspaceIdConsumerWrapper.wrapperFactory({ + client: mockedClient, + typeRegistry: requestHandlerContext.savedObjects.typeRegistry, + request: mockRequest, + }); + const savedObject = { + type: 'data-source', + id: 'data-source_id', + attributes: {}, + references: [], + }; + mockedClient.get.mockResolvedValueOnce(savedObject); + const result = await mockedWrapperClient.get(savedObject.type, savedObject.id); + expect(mockedClient.get).toBeCalledWith(savedObject.type, savedObject.id, {}); + expect(result).toEqual(savedObject); + }); + + it(`Should throw error when the object is global data source`, async () => { + const savedObject = { + type: 'data-source', + id: 'data-source_id', + attributes: {}, + references: [], + }; + mockedClient.get.mockResolvedValueOnce(savedObject); + mockedClient.get.mockResolvedValueOnce(savedObject); + expect(wrapperClient.get(savedObject.type, savedObject.id)).rejects.toMatchInlineSnapshot( + `[Error: Saved object does not belong to the workspace]` + ); + expect(mockedClient.get).toBeCalledWith(savedObject.type, savedObject.id, {}); + }); + }); + + describe('bulkGet', () => { + const payload = [ + { id: 'dashboard_id', type: 'dashboard' }, + { id: 'dashboard_error_id', type: 'dashboard' }, + { id: 'visualization_id', type: 'visualization' }, + { id: 'global_data_source_id', type: 'data-source' }, + { id: 'data_source_id', type: 'data-source' }, + ]; + const savedObjects = [ + { + type: 'dashboard', + id: 'dashboard_id', + attributes: {}, + references: [], + workspaces: ['foo'], + }, + { + type: 'dashboard', + id: 'dashboard_error_id', + attributes: {}, + references: [], + error: { + statusCode: 404, + error: 'Not Found', + message: 'Saved object [dashboard/dashboard_error_id] not found', + }, + }, + { + type: 'visualization', + id: 'visualization_id', + attributes: {}, + references: [], + workspaces: ['bar'], + }, + { + type: 'config', + id: 'config_id', + attributes: {}, + references: [], + }, + { + type: 'workspace', + id: 'workspace_id', + attributes: {}, + references: [], + }, + { + type: 'data-source', + id: 'global_data_source_id', + attributes: {}, + references: [], + }, + { + type: 'data-source', + id: 'data_source_id', + attributes: {}, + references: [], + workspaces: ['foo'], + }, + ]; + const options = { workspaces: ['foo'] }; + beforeEach(() => { + mockedClient.bulkGet.mockClear(); + }); + + it(`Should bulkGet objects belonging to options.workspaces`, async () => { + mockedClient.bulkGet.mockResolvedValueOnce({ saved_objects: savedObjects }); + const result = await wrapperClient.bulkGet(payload, options); + expect(mockedClient.bulkGet).toBeCalledWith(payload, options); + expect(result).toMatchInlineSnapshot(` + Object { + "saved_objects": Array [ + Object { + "attributes": Object {}, + "id": "dashboard_id", + "references": Array [], + "type": "dashboard", + "workspaces": Array [ + "foo", + ], + }, + Object { + "attributes": Object {}, + "error": Object { + "error": "Not Found", + "message": "Saved object [dashboard/dashboard_error_id] not found", + "statusCode": 404, + }, + "id": "dashboard_error_id", + "references": Array [], + "type": "dashboard", + }, + Object { + "attributes": Object {}, + "error": Object { + "error": "Forbidden", + "message": "Saved object does not belong to the workspace", + "statusCode": 403, + }, + "id": "visualization_id", + "references": Array [], + "type": "visualization", + "workspaces": Array [ + "bar", + ], + }, + Object { + "attributes": Object {}, + "id": "config_id", + "references": Array [], + "type": "config", + }, + Object { + "attributes": Object {}, + "id": "workspace_id", + "references": Array [], + "type": "workspace", + }, + Object { + "attributes": Object {}, + "error": Object { + "error": "Forbidden", + "message": "Saved object does not belong to the workspace", + "statusCode": 403, + }, + "id": "global_data_source_id", + "references": Array [], + "type": "data-source", + }, + Object { + "attributes": Object {}, + "id": "data_source_id", + "references": Array [], + "type": "data-source", + "workspaces": Array [ + "foo", + ], + }, + ], + } + `); + }); + + it(`Should bulkGet objects belonging to the workspace in request`, async () => { + mockedClient.bulkGet.mockResolvedValueOnce({ saved_objects: savedObjects }); + const result = await wrapperClient.bulkGet(payload); + expect(mockedClient.bulkGet).toBeCalledWith(payload, {}); + expect(result).toMatchInlineSnapshot(` + Object { + "saved_objects": Array [ + Object { + "attributes": Object {}, + "id": "dashboard_id", + "references": Array [], + "type": "dashboard", + "workspaces": Array [ + "foo", + ], + }, + Object { + "attributes": Object {}, + "error": Object { + "error": "Not Found", + "message": "Saved object [dashboard/dashboard_error_id] not found", + "statusCode": 404, + }, + "id": "dashboard_error_id", + "references": Array [], + "type": "dashboard", + }, + Object { + "attributes": Object {}, + "error": Object { + "error": "Forbidden", + "message": "Saved object does not belong to the workspace", + "statusCode": 403, + }, + "id": "visualization_id", + "references": Array [], + "type": "visualization", + "workspaces": Array [ + "bar", + ], + }, + Object { + "attributes": Object {}, + "id": "config_id", + "references": Array [], + "type": "config", + }, + Object { + "attributes": Object {}, + "id": "workspace_id", + "references": Array [], + "type": "workspace", + }, + Object { + "attributes": Object {}, + "error": Object { + "error": "Forbidden", + "message": "Saved object does not belong to the workspace", + "statusCode": 403, + }, + "id": "global_data_source_id", + "references": Array [], + "type": "data-source", + }, + Object { + "attributes": Object {}, + "id": "data_source_id", + "references": Array [], + "type": "data-source", + "workspaces": Array [ + "foo", + ], + }, + ], + } + `); + }); + + it(`Should bulkGet objects when there is no workspace in options/request`, async () => { + const workspaceIdConsumerWrapper = new WorkspaceIdConsumerWrapper(mockedWorkspaceClient); + const mockRequest = httpServerMock.createOpenSearchDashboardsRequest(); + updateWorkspaceState(mockRequest, {}); + const mockedWrapperClient = workspaceIdConsumerWrapper.wrapperFactory({ + client: mockedClient, + typeRegistry: requestHandlerContext.savedObjects.typeRegistry, + request: mockRequest, + }); + mockedClient.bulkGet.mockResolvedValueOnce({ saved_objects: savedObjects }); + const result = await mockedWrapperClient.bulkGet(payload); + expect(mockedClient.bulkGet).toBeCalledWith(payload, {}); + expect(result).toEqual({ saved_objects: savedObjects }); + }); + + it(`Should throw error when the objects do not exist`, async () => { + mockedClient.bulkGet.mockRejectedValueOnce( + SavedObjectsErrorHelpers.createGenericNotFoundError() + ); + expect(wrapperClient.bulkGet(payload)).rejects.toMatchInlineSnapshot(`[Error: Not Found]`); + expect(mockedClient.bulkGet).toBeCalledWith(payload, {}); + }); + + it(`Should throw error when the options.workspaces has more than one workspace.`, async () => { + expect( + wrapperClient.bulkGet(payload, { workspaces: ['foo', 'var'] }) + ).rejects.toMatchInlineSnapshot(`[Error: Multiple workspace parameters: Bad Request]`); + expect(mockedClient.bulkGet).not.toBeCalled(); + }); + + it(`Should bulkGet data source when user is data source admin`, async () => { + const workspaceIdConsumerWrapper = new WorkspaceIdConsumerWrapper(mockedWorkspaceClient); + const mockRequest = httpServerMock.createOpenSearchDashboardsRequest(); + updateWorkspaceState(mockRequest, { isDataSourceAdmin: true, requestWorkspaceId: 'foo' }); + const mockedWrapperClient = workspaceIdConsumerWrapper.wrapperFactory({ + client: mockedClient, + typeRegistry: requestHandlerContext.savedObjects.typeRegistry, + request: mockRequest, + }); + + mockedClient.bulkGet.mockResolvedValueOnce({ saved_objects: savedObjects }); + const result = await mockedWrapperClient.bulkGet(payload); + expect(mockedClient.bulkGet).toBeCalledWith(payload, {}); + expect(result).toMatchInlineSnapshot(` + Object { + "saved_objects": Array [ + Object { + "attributes": Object {}, + "id": "dashboard_id", + "references": Array [], + "type": "dashboard", + "workspaces": Array [ + "foo", + ], + }, + Object { + "attributes": Object {}, + "error": Object { + "error": "Not Found", + "message": "Saved object [dashboard/dashboard_error_id] not found", + "statusCode": 404, + }, + "id": "dashboard_error_id", + "references": Array [], + "type": "dashboard", + }, + Object { + "attributes": Object {}, + "error": Object { + "error": "Forbidden", + "message": "Saved object does not belong to the workspace", + "statusCode": 403, + }, + "id": "visualization_id", + "references": Array [], + "type": "visualization", + "workspaces": Array [ + "bar", + ], + }, + Object { + "attributes": Object {}, + "id": "config_id", + "references": Array [], + "type": "config", + }, + Object { + "attributes": Object {}, + "id": "workspace_id", + "references": Array [], + "type": "workspace", + }, + Object { + "attributes": Object {}, + "id": "global_data_source_id", + "references": Array [], + "type": "data-source", + }, + Object { + "attributes": Object {}, + "id": "data_source_id", + "references": Array [], + "type": "data-source", + "workspaces": Array [ + "foo", + ], + }, + ], + } + `); + }); + }); }); diff --git a/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.ts b/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.ts index 90820c835d47..43393da03ef5 100644 --- a/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.ts +++ b/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.ts @@ -14,13 +14,26 @@ import { OpenSearchDashboardsRequest, SavedObjectsFindOptions, SavedObjectsErrorHelpers, + SavedObject, + SavedObjectsBulkGetObject, + SavedObjectsBulkResponse, } from '../../../../core/server'; import { IWorkspaceClientImpl } from '../types'; +import { validateIsWorkspaceDataSourceAndConnectionObjectType } from '../../common/utils'; const UI_SETTINGS_SAVED_OBJECTS_TYPE = 'config'; type WorkspaceOptions = Pick | undefined; +const generateSavedObjectsForbiddenError = () => + SavedObjectsErrorHelpers.decorateForbiddenError( + new Error( + i18n.translate('workspace.id_consumer.saved_objects.forbidden', { + defaultMessage: 'Saved object does not belong to the workspace', + }) + ) + ); + export class WorkspaceIdConsumerWrapper { private formatWorkspaceIdParams( request: OpenSearchDashboardsRequest, @@ -48,6 +61,36 @@ export class WorkspaceIdConsumerWrapper { return type === UI_SETTINGS_SAVED_OBJECTS_TYPE; } + private validateObjectInAWorkspace( + object: SavedObject, + workspace: string, + request: OpenSearchDashboardsRequest + ) { + // Keep the original object error + if (!!object?.error) { + return true; + } + // Data source is a workspace level object, validate if the request has access to the data source within the requested workspace. + if (validateIsWorkspaceDataSourceAndConnectionObjectType(object.type)) { + if (!!getWorkspaceState(request).isDataSourceAdmin) { + return true; + } + // Deny access if the object is a global data source (no workspaces assigned) + if (!object.workspaces || object.workspaces.length === 0) { + return false; + } + } + /* + * Allow access if the requested workspace matches one of the object's assigned workspaces + * This ensures that the user can only access data sources within their current workspace + */ + if (object.workspaces && object.workspaces.length > 0) { + return object.workspaces.includes(workspace); + } + // Allow access if the object is a global object (object.workspaces is null/[]) + return true; + } + public wrapperFactory: SavedObjectsClientWrapperFactory = (wrapperOptions) => { return { ...wrapperOptions.client, @@ -126,8 +169,59 @@ export class WorkspaceIdConsumerWrapper { } return wrapperOptions.client.find(finalOptions); }, - bulkGet: wrapperOptions.client.bulkGet, - get: wrapperOptions.client.get, + bulkGet: async ( + objects: SavedObjectsBulkGetObject[] = [], + options: SavedObjectsBaseOptions = {} + ): Promise> => { + const { workspaces } = this.formatWorkspaceIdParams(wrapperOptions.request, options); + if (!!workspaces && workspaces.length > 1) { + // Version 2.18 does not support the passing of multiple workspaces. + throw SavedObjectsErrorHelpers.createBadRequestError('Multiple workspace parameters'); + } + + const objectToBulkGet = await wrapperOptions.client.bulkGet(objects, options); + + if (workspaces?.length === 1) { + return { + ...objectToBulkGet, + saved_objects: objectToBulkGet.saved_objects.map((object) => { + return this.validateObjectInAWorkspace(object, workspaces[0], wrapperOptions.request) + ? object + : { + ...object, + error: { + ...generateSavedObjectsForbiddenError().output.payload, + }, + }; + }), + }; + } + + return objectToBulkGet; + }, + get: async ( + type: string, + id: string, + options: SavedObjectsBaseOptions = {} + ): Promise> => { + const { workspaces } = this.formatWorkspaceIdParams(wrapperOptions.request, options); + if (!!workspaces && workspaces.length > 1) { + // Version 2.18 does not support the passing of multiple workspaces. + throw SavedObjectsErrorHelpers.createBadRequestError('Multiple workspace parameters'); + } + + const objectToGet = await wrapperOptions.client.get(type, id, options); + + if ( + workspaces?.length === 1 && + !this.validateObjectInAWorkspace(objectToGet, workspaces[0], wrapperOptions.request) + ) { + throw generateSavedObjectsForbiddenError(); + } + + // Allow access if no specific workspace is requested. + return objectToGet; + }, update: wrapperOptions.client.update, bulkUpdate: wrapperOptions.client.bulkUpdate, addToNamespaces: wrapperOptions.client.addToNamespaces, diff --git a/src/plugins/workspace/server/saved_objects/workspace_saved_objects_client_wrapper.test.ts b/src/plugins/workspace/server/saved_objects/workspace_saved_objects_client_wrapper.test.ts index e9f5c5c2a409..55098d6e2b27 100644 --- a/src/plugins/workspace/server/saved_objects/workspace_saved_objects_client_wrapper.test.ts +++ b/src/plugins/workspace/server/saved_objects/workspace_saved_objects_client_wrapper.test.ts @@ -652,127 +652,6 @@ describe('WorkspaceSavedObjectsClientWrapper', () => { } `); }); - - it('should validate data source or data connection workspace field', async () => { - const { wrapper } = generateWorkspaceSavedObjectsClientWrapper(); - let errorCatched; - try { - await wrapper.get('data-source', 'workspace-1-data-source'); - } catch (e) { - errorCatched = e; - } - expect(errorCatched?.message).toEqual( - 'Invalid data source permission, please associate it to current workspace' - ); - - try { - await wrapper.get('data-connection', 'workspace-1-data-connection'); - } catch (e) { - errorCatched = e; - } - expect(errorCatched?.message).toEqual( - 'Invalid data source permission, please associate it to current workspace' - ); - - let result = await wrapper.get('data-source', 'workspace-2-data-source'); - expect(result).toEqual( - expect.objectContaining({ - attributes: { - title: 'Workspace 2 data source', - }, - id: 'workspace-2-data-source', - type: 'data-source', - workspaces: ['mock-request-workspace-id'], - }) - ); - result = await wrapper.get('data-connection', 'workspace-2-data-connection'); - expect(result).toEqual( - expect.objectContaining({ - attributes: { - title: 'Workspace 2 data connection', - }, - id: 'workspace-2-data-connection', - type: 'data-connection', - workspaces: ['mock-request-workspace-id'], - }) - ); - }); - - it('should not validate data source or data connection when not in workspace', async () => { - const { wrapper, requestMock } = generateWorkspaceSavedObjectsClientWrapper(); - updateWorkspaceState(requestMock, { requestWorkspaceId: undefined }); - let result = await wrapper.get('data-source', 'workspace-1-data-source'); - expect(result).toEqual({ - type: DATA_SOURCE_SAVED_OBJECT_TYPE, - id: 'workspace-1-data-source', - attributes: { title: 'Workspace 1 data source' }, - workspaces: ['workspace-1'], - references: [], - }); - result = await wrapper.get('data-connection', 'workspace-1-data-connection'); - expect(result).toEqual({ - type: DATA_CONNECTION_SAVED_OBJECT_TYPE, - id: 'workspace-1-data-connection', - attributes: { title: 'Workspace 1 data connection' }, - workspaces: ['workspace-1'], - references: [], - }); - }); - - it('should not validate data source when user is data source admin', async () => { - const { wrapper } = generateWorkspaceSavedObjectsClientWrapper(DATASOURCE_ADMIN); - const result = await wrapper.get('data-source', 'workspace-1-data-source'); - expect(result).toEqual({ - type: DATA_SOURCE_SAVED_OBJECT_TYPE, - id: 'workspace-1-data-source', - attributes: { title: 'Workspace 1 data source' }, - workspaces: ['workspace-1'], - references: [], - }); - }); - - it('should throw permission error when tried to access a global data source or data connection', async () => { - const { wrapper } = generateWorkspaceSavedObjectsClientWrapper(); - let errorCatched; - try { - await wrapper.get('data-source', 'global-data-source'); - } catch (e) { - errorCatched = e; - } - expect(errorCatched?.message).toEqual( - 'Invalid data source permission, please associate it to current workspace' - ); - try { - await wrapper.get('data-connection', 'global-data-connection'); - } catch (e) { - errorCatched = e; - } - expect(errorCatched?.message).toEqual( - 'Invalid data source permission, please associate it to current workspace' - ); - }); - - it('should throw permission error when tried to access a empty workspaces global data source or data connection', async () => { - const { wrapper, requestMock } = generateWorkspaceSavedObjectsClientWrapper(); - updateWorkspaceState(requestMock, { requestWorkspaceId: undefined }); - let errorCatched; - try { - await wrapper.get('data-source', 'global-data-source-empty-workspaces'); - } catch (e) { - errorCatched = e; - } - expect(errorCatched?.message).toEqual( - 'Invalid data source permission, please associate it to current workspace' - ); - try { - await wrapper.get('data-connection', 'global-data-connection-empty-workspaces'); - } catch (e) { - errorCatched = e; - } - expect(errorCatched?.message).toEqual( - 'Invalid data source permission, please associate it to current workspace' - ); - }); }); describe('bulk get', () => { it("should call permission validate with object's workspace and throw permission error", async () => { @@ -837,166 +716,6 @@ describe('WorkspaceSavedObjectsClientWrapper', () => { {} ); }); - it('should validate data source or data connection workspace field', async () => { - const { wrapper } = generateWorkspaceSavedObjectsClientWrapper(); - let errorCatched; - try { - await wrapper.bulkGet([ - { - type: 'data-source', - id: 'workspace-1-data-source', - }, - ]); - } catch (e) { - errorCatched = e; - } - expect(errorCatched?.message).toEqual( - 'Invalid data source permission, please associate it to current workspace' - ); - - try { - await wrapper.bulkGet([ - { - type: 'data-connection', - id: 'workspace-1-data-connection', - }, - ]); - } catch (e) { - errorCatched = e; - } - expect(errorCatched?.message).toEqual( - 'Invalid data source permission, please associate it to current workspace' - ); - - let result = await await wrapper.bulkGet([ - { - type: 'data-source', - id: 'workspace-2-data-source', - }, - ]); - expect(result).toEqual({ - saved_objects: [ - { - attributes: { - title: 'Workspace 2 data source', - }, - id: 'workspace-2-data-source', - type: 'data-source', - workspaces: ['mock-request-workspace-id'], - references: [], - }, - ], - }); - - result = await await wrapper.bulkGet([ - { - type: 'data-connection', - id: 'workspace-2-data-connection', - }, - ]); - expect(result).toEqual({ - saved_objects: [ - { - attributes: { - title: 'Workspace 2 data connection', - }, - id: 'workspace-2-data-connection', - type: 'data-connection', - workspaces: ['mock-request-workspace-id'], - references: [], - }, - ], - }); - }); - - it('should not validate data source or data connection when not in workspace', async () => { - const { wrapper, requestMock } = generateWorkspaceSavedObjectsClientWrapper(); - updateWorkspaceState(requestMock, { requestWorkspaceId: undefined }); - let result = await wrapper.bulkGet([ - { - type: 'data-source', - id: 'workspace-1-data-source', - }, - ]); - expect(result).toEqual({ - saved_objects: [ - { - attributes: { - title: 'Workspace 1 data source', - }, - id: 'workspace-1-data-source', - type: 'data-source', - workspaces: ['workspace-1'], - references: [], - }, - ], - }); - - result = await wrapper.bulkGet([ - { - type: 'data-connection', - id: 'workspace-1-data-connection', - }, - ]); - expect(result).toEqual({ - saved_objects: [ - { - attributes: { - title: 'Workspace 1 data connection', - }, - id: 'workspace-1-data-connection', - type: 'data-connection', - workspaces: ['workspace-1'], - references: [], - }, - ], - }); - }); - - it('should throw permission error when tried to bulk get global data source or data connection', async () => { - const { wrapper, requestMock } = generateWorkspaceSavedObjectsClientWrapper(); - updateWorkspaceState(requestMock, { requestWorkspaceId: undefined }); - let errorCatched; - try { - await wrapper.bulkGet([{ type: 'data-source', id: 'global-data-source' }]); - } catch (e) { - errorCatched = e; - } - expect(errorCatched?.message).toEqual( - 'Invalid data source permission, please associate it to current workspace' - ); - try { - await wrapper.bulkGet([{ type: 'data-connection', id: 'global-data-connection' }]); - } catch (e) { - errorCatched = e; - } - expect(errorCatched?.message).toEqual( - 'Invalid data source permission, please associate it to current workspace' - ); - }); - - it('should throw permission error when tried to bulk get a empty workspace global data source or data connection', async () => { - const { wrapper, requestMock } = generateWorkspaceSavedObjectsClientWrapper(); - updateWorkspaceState(requestMock, { requestWorkspaceId: undefined }); - let errorCatched; - try { - await wrapper.bulkGet([ - { type: 'data-source', id: 'global-data-source-empty-workspaces' }, - ]); - } catch (e) { - errorCatched = e; - } - expect(errorCatched?.message).toEqual( - 'Invalid data source permission, please associate it to current workspace' - ); - try { - await wrapper.bulkGet([ - { type: 'data-connection', id: 'global-data-connection-empty-workspaces' }, - ]); - } catch (e) { - errorCatched = e; - } - }); }); describe('find', () => { it('should call client.find with consistent params when ACLSearchParams and workspaceOperator not provided', async () => { diff --git a/src/plugins/workspace/server/saved_objects/workspace_saved_objects_client_wrapper.ts b/src/plugins/workspace/server/saved_objects/workspace_saved_objects_client_wrapper.ts index 162f7a488ad2..0adc27b39a43 100644 --- a/src/plugins/workspace/server/saved_objects/workspace_saved_objects_client_wrapper.ts +++ b/src/plugins/workspace/server/saved_objects/workspace_saved_objects_client_wrapper.ts @@ -61,15 +61,6 @@ const generateSavedObjectsPermissionError = () => ) ); -const generateDataSourcePermissionError = () => - SavedObjectsErrorHelpers.decorateForbiddenError( - new Error( - i18n.translate('workspace.saved_objects.data_source.invalidate', { - defaultMessage: 'Invalid data source permission, please associate it to current workspace', - }) - ) - ); - const generateOSDAdminPermissionError = () => SavedObjectsErrorHelpers.decorateForbiddenError( new Error( @@ -205,32 +196,6 @@ export class WorkspaceSavedObjectsClientWrapper { return hasPermission; } - // Data source is a workspace level object, validate if the request has access to the data source within the requested workspace. - private validateDataSourcePermissions = ( - object: SavedObject, - request: OpenSearchDashboardsRequest - ) => { - const requestWorkspaceId = getWorkspaceState(request).requestWorkspaceId; - // Deny access if the object is a global data source (no workspaces assigned) - if (!object.workspaces || object.workspaces.length === 0) { - return false; - } - /** - * Allow access if no specific workspace is requested. - * This typically occurs when retrieving data sources or performing operations - * that don't require a specific workspace, such as pages within the - * Data Administration navigation group that include a data source picker. - */ - if (!requestWorkspaceId) { - return true; - } - /* - * Allow access if the requested workspace matches one of the object's assigned workspaces - * This ensures that the user can only access data sources within their current workspace - */ - return object.workspaces.includes(requestWorkspaceId); - }; - private getWorkspaceTypeEnabledClient(request: OpenSearchDashboardsRequest) { return this.getScopedClient?.(request, { includedHiddenTypes: [WORKSPACE_TYPE], @@ -462,21 +427,6 @@ export class WorkspaceSavedObjectsClientWrapper { ): Promise> => { const objectToGet = await wrapperOptions.client.get(type, id, options); - if (validateIsWorkspaceDataSourceAndConnectionObjectType(objectToGet.type)) { - if (isDataSourceAdmin) { - ACLAuditor?.increment(ACLAuditorStateKey.VALIDATE_SUCCESS, 1); - return objectToGet; - } - const hasPermission = this.validateDataSourcePermissions( - objectToGet, - wrapperOptions.request - ); - if (!hasPermission) { - ACLAuditor?.increment(ACLAuditorStateKey.VALIDATE_FAILURE, 1); - throw generateDataSourcePermissionError(); - } - } - if ( !(await this.validateWorkspacesAndSavedObjectsPermissions( objectToGet, @@ -504,14 +454,6 @@ export class WorkspaceSavedObjectsClientWrapper { ); for (const object of objectToBulkGet.saved_objects) { - if (validateIsWorkspaceDataSourceAndConnectionObjectType(object.type)) { - const hasPermission = this.validateDataSourcePermissions(object, wrapperOptions.request); - if (!hasPermission) { - ACLAuditor?.increment(ACLAuditorStateKey.VALIDATE_FAILURE, 1); - throw generateDataSourcePermissionError(); - } - } - if ( !(await this.validateWorkspacesAndSavedObjectsPermissions( object, From 080b0db8142451af98b156d0faceed7a269bf558 Mon Sep 17 00:00:00 2001 From: Sean Li Date: Mon, 2 Dec 2024 16:54:14 -0800 Subject: [PATCH 02/16] [Discover] Fix Initialization if No Saved Query (#8930) * replace default query with current query Signed-off-by: Sean Li * Changeset file for PR #8930 created/updated * adding unit tests Signed-off-by: Sean Li --------- Signed-off-by: Sean Li Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> --- changelogs/fragments/8930.yml | 2 + .../view_components/utils/use_search.test.tsx | 59 ++++++++++++++++++- .../view_components/utils/use_search.ts | 5 +- 3 files changed, 62 insertions(+), 4 deletions(-) create mode 100644 changelogs/fragments/8930.yml diff --git a/changelogs/fragments/8930.yml b/changelogs/fragments/8930.yml new file mode 100644 index 000000000000..50551ecb2956 --- /dev/null +++ b/changelogs/fragments/8930.yml @@ -0,0 +1,2 @@ +fix: +- Update saved search initialization logic to use current query instead of default query ([#8930](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8930)) \ No newline at end of file diff --git a/src/plugins/discover/public/application/view_components/utils/use_search.test.tsx b/src/plugins/discover/public/application/view_components/utils/use_search.test.tsx index b76651899b61..f5021b90c1e7 100644 --- a/src/plugins/discover/public/application/view_components/utils/use_search.test.tsx +++ b/src/plugins/discover/public/application/view_components/utils/use_search.test.tsx @@ -18,12 +18,37 @@ jest.mock('./use_index_pattern', () => ({ useIndexPattern: jest.fn(), })); +const mockQuery = { + query: 'test query', + language: 'test language', +}; + +const mockDefaultQuery = { + query: 'default query', + language: 'default language', +}; + const mockSavedSearch = { id: 'test-saved-search', title: 'Test Saved Search', searchSource: { setField: jest.fn(), - getField: jest.fn(), + getField: jest.fn().mockReturnValue(mockQuery), + fetch: jest.fn(), + getSearchRequestBody: jest.fn().mockResolvedValue({}), + getOwnField: jest.fn(), + getDataFrame: jest.fn(() => ({ name: 'test-pattern' })), + }, + getFullPath: jest.fn(), + getOpenSearchType: jest.fn(), +}; + +const mockSavedSearchEmptyQuery = { + id: 'test-saved-search', + title: 'Test Saved Search', + searchSource: { + setField: jest.fn(), + getField: jest.fn().mockReturnValue(undefined), fetch: jest.fn(), getSearchRequestBody: jest.fn().mockResolvedValue({}), getOwnField: jest.fn(), @@ -215,4 +240,36 @@ describe('useSearch', () => { expect.objectContaining({ status: ResultStatus.LOADING, rows: [] }) ); }); + + it('should load saved search', async () => { + const services = createMockServices(); + services.data.query.queryString.setQuery = jest.fn(); + + const { waitForNextUpdate } = renderHook(() => useSearch(services), { + wrapper, + }); + + await act(async () => { + await waitForNextUpdate(); + }); + + expect(services.data.query.queryString.setQuery).toBeCalledWith(mockQuery); + }); + + it('if no saved search, use get query', async () => { + const services = createMockServices(); + services.getSavedSearchById = jest.fn().mockResolvedValue(mockSavedSearchEmptyQuery); + services.data.query.queryString.getQuery = jest.fn().mockReturnValue(mockDefaultQuery); + services.data.query.queryString.setQuery = jest.fn(); + + const { waitForNextUpdate } = renderHook(() => useSearch(services), { + wrapper, + }); + + await act(async () => { + await waitForNextUpdate(); + }); + + expect(services.data.query.queryString.setQuery).toBeCalledWith(mockDefaultQuery); + }); }); diff --git a/src/plugins/discover/public/application/view_components/utils/use_search.ts b/src/plugins/discover/public/application/view_components/utils/use_search.ts index 158a9cd46074..7923f0e717c2 100644 --- a/src/plugins/discover/public/application/view_components/utils/use_search.ts +++ b/src/plugins/discover/public/application/view_components/utils/use_search.ts @@ -392,8 +392,7 @@ export const useSearch = (services: DiscoverViewServices) => { const savedSearchInstance = await getSavedSearchById(savedSearchId); const query = - savedSearchInstance.searchSource.getField('query') || - data.query.queryString.getDefaultQuery(); + savedSearchInstance.searchSource.getField('query') || data.query.queryString.getQuery(); const isEnhancementsEnabled = await uiSettings.get('query:enhancements:enabled'); if (isEnhancementsEnabled && query.dataset) { @@ -432,7 +431,7 @@ export const useSearch = (services: DiscoverViewServices) => { } filterManager.setAppFilters(actualFilters); - data.query.queryString.setQuery(savedQuery ? data.query.queryString.getQuery() : query); + data.query.queryString.setQuery(query); setSavedSearch(savedSearchInstance); if (savedSearchInstance?.id) { From 340326ff3d3813a6ed308886a46b97638bee1564 Mon Sep 17 00:00:00 2001 From: yuboluo Date: Tue, 3 Dec 2024 11:24:56 +0800 Subject: [PATCH 03/16] [Workspace][Bug] Check if workspaces exists when creating saved objects (#8739) * Check if workspaces exists when creating saved objects Signed-off-by: yubonluo * Changeset file for PR #8739 created/updated * optimize the code Signed-off-by: yubonluo * fix test error Signed-off-by: yubonluo * optimize the code Signed-off-by: yubonluo * fix test errors Signed-off-by: yubonluo * add integration tests Signed-off-by: yubonluo --------- Signed-off-by: yubonluo Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> --- changelogs/fragments/8739.yml | 2 + .../workspace_id_consumer_wrapper.test.ts | 60 ++++++++- ...space_saved_objects_client_wrapper.test.ts | 35 ++---- .../workspace_id_consumer_wrapper.test.ts | 67 +++++++++- .../workspace_id_consumer_wrapper.ts | 115 ++++++++++-------- 5 files changed, 199 insertions(+), 80 deletions(-) create mode 100644 changelogs/fragments/8739.yml diff --git a/changelogs/fragments/8739.yml b/changelogs/fragments/8739.yml new file mode 100644 index 000000000000..563d6c0cacac --- /dev/null +++ b/changelogs/fragments/8739.yml @@ -0,0 +1,2 @@ +fix: +- [Workspace] [Bug] Check if workspaces exists when creating saved objects. ([#8739](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8739)) \ No newline at end of file diff --git a/src/plugins/workspace/server/saved_objects/integration_tests/workspace_id_consumer_wrapper.test.ts b/src/plugins/workspace/server/saved_objects/integration_tests/workspace_id_consumer_wrapper.test.ts index c762d08cedff..f597dd369272 100644 --- a/src/plugins/workspace/server/saved_objects/integration_tests/workspace_id_consumer_wrapper.test.ts +++ b/src/plugins/workspace/server/saved_objects/integration_tests/workspace_id_consumer_wrapper.test.ts @@ -150,10 +150,35 @@ describe('workspace_id_consumer integration test', () => { `/api/saved_objects/${config.type}/${packageInfo.version}` ); - // workspaces arrtibutes should not be append + // workspaces attributes should not be append expect(!getConfigResult.body.workspaces).toEqual(true); }); + it('should return error when create with a not existing workspace', async () => { + await clearFooAndBar(); + const createResultWithNonExistRequestWorkspace = await osdTestServer.request + .post(root, `/w/not_exist_workspace_id/api/saved_objects/${dashboard.type}`) + .send({ + attributes: dashboard.attributes, + }) + .expect(400); + + expect(createResultWithNonExistRequestWorkspace.body.message).toEqual( + 'Exist invalid workspaces' + ); + + const createResultWithNonExistOptionsWorkspace = await osdTestServer.request + .post(root, `/api/saved_objects/${dashboard.type}`) + .send({ + attributes: dashboard.attributes, + workspaces: ['not_exist_workspace_id'], + }) + .expect(400); + expect(createResultWithNonExistOptionsWorkspace.body.message).toEqual( + 'Exist invalid workspaces' + ); + }); + it('bulk create', async () => { await clearFooAndBar(); const createResultFoo = await osdTestServer.request @@ -184,6 +209,37 @@ describe('workspace_id_consumer integration test', () => { ); }); + it('should return error when bulk create with a not existing workspace', async () => { + await clearFooAndBar(); + const bulkCreateResultWithNonExistRequestWorkspace = await osdTestServer.request + .post(root, `/w/not_exist_workspace_id/api/saved_objects/_bulk_create`) + .send([ + { + ...dashboard, + id: 'foo', + }, + ]) + .expect(400); + + expect(bulkCreateResultWithNonExistRequestWorkspace.body.message).toEqual( + 'Exist invalid workspaces' + ); + + const bulkCreateResultWithNonExistOptionsWorkspace = await osdTestServer.request + .post(root, `/api/saved_objects/_bulk_create?workspaces=not_exist_workspace_id`) + .send([ + { + ...dashboard, + id: 'foo', + }, + ]) + .expect(400); + + expect(bulkCreateResultWithNonExistOptionsWorkspace.body.message).toEqual( + 'Exist invalid workspaces' + ); + }); + it('checkConflicts when importing ndjson', async () => { await clearFooAndBar(); const createResultFoo = await osdTestServer.request @@ -288,7 +344,7 @@ describe('workspace_id_consumer integration test', () => { .get(root, `/w/not_exist_workspace_id/api/saved_objects/_find?type=${dashboard.type}`) .expect(400); - expect(findResult.body.message).toEqual('Invalid workspaces'); + expect(findResult.body.message).toEqual('Exist invalid workspaces'); }); it('import within workspace', async () => { diff --git a/src/plugins/workspace/server/saved_objects/integration_tests/workspace_saved_objects_client_wrapper.test.ts b/src/plugins/workspace/server/saved_objects/integration_tests/workspace_saved_objects_client_wrapper.test.ts index 82c943545aca..e3eddb443990 100644 --- a/src/plugins/workspace/server/saved_objects/integration_tests/workspace_saved_objects_client_wrapper.test.ts +++ b/src/plugins/workspace/server/saved_objects/integration_tests/workspace_saved_objects_client_wrapper.test.ts @@ -250,7 +250,7 @@ describe('WorkspaceSavedObjectsClientWrapper', () => { perPage: 999, page: 1, }) - ).rejects.toMatchInlineSnapshot(`[Error: Invalid workspaces]`); + ).rejects.toMatchInlineSnapshot(`[Error: Exist invalid workspaces]`); }); it('should return consistent inner workspace data when user permitted', async () => { @@ -349,21 +349,16 @@ describe('WorkspaceSavedObjectsClientWrapper', () => { }); describe('create', () => { - it('should throw forbidden error when workspace not permitted and create called', async () => { - let error; - try { - await notPermittedSavedObjectedClient.create( + it('should throw bad request error when workspace is invalid and create called', async () => { + await expect( + notPermittedSavedObjectedClient.create( 'dashboard', {}, { workspaces: ['workspace-1'], } - ); - } catch (e) { - error = e; - } - - expect(SavedObjectsErrorHelpers.isForbiddenError(error)).toBe(true); + ) + ).rejects.toMatchInlineSnapshot(`[Error: Exist invalid workspaces]`); }); it('should able to create saved objects into permitted workspaces after create called', async () => { @@ -427,7 +422,7 @@ describe('WorkspaceSavedObjectsClientWrapper', () => { expect(createResult.error).toBeUndefined(); }); - it('should throw forbidden error when user create a workspce and is not OSD admin', async () => { + it('should throw forbidden error when user create a workspace and is not OSD admin', async () => { let error; try { await permittedSavedObjectedClient.create('workspace', {}, {}); @@ -468,17 +463,12 @@ describe('WorkspaceSavedObjectsClientWrapper', () => { }); describe('bulkCreate', () => { - it('should throw forbidden error when workspace not permitted and bulkCreate called', async () => { - let error; - try { - await notPermittedSavedObjectedClient.bulkCreate([{ type: 'dashboard', attributes: {} }], { + it('should throw bad request error when workspace is invalid and bulkCreate called', async () => { + await expect( + notPermittedSavedObjectedClient.bulkCreate([{ type: 'dashboard', attributes: {} }], { workspaces: ['workspace-1'], - }); - } catch (e) { - error = e; - } - - expect(SavedObjectsErrorHelpers.isForbiddenError(error)).toBe(true); + }) + ).rejects.toMatchInlineSnapshot(`[Error: Exist invalid workspaces]`); }); it('should able to create saved objects into permitted workspaces after bulkCreate called', async () => { @@ -506,7 +496,6 @@ describe('WorkspaceSavedObjectsClientWrapper', () => { ], { overwrite: true, - workspaces: ['workspace-1'], } ); } catch (e) { diff --git a/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.test.ts b/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.test.ts index ca19ffc927ad..fcef67870523 100644 --- a/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.test.ts +++ b/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.test.ts @@ -38,8 +38,15 @@ describe('WorkspaceIdConsumerWrapper', () => { describe('create', () => { beforeEach(() => { mockedClient.create.mockClear(); + mockedWorkspaceClient.get.mockClear(); + mockedWorkspaceClient.list.mockClear(); }); it(`Should add workspaces parameters when create`, async () => { + mockedWorkspaceClient.get.mockImplementationOnce((requestContext, id) => { + return { + success: true, + }; + }); await wrapperClient.create('dashboard', { name: 'foo', }); @@ -68,13 +75,54 @@ describe('WorkspaceIdConsumerWrapper', () => { expect(mockedClient.create.mock.calls[0][2]?.hasOwnProperty('workspaces')).toEqual(false); }); + + it(`Should throw error when passing in invalid workspaces`, async () => { + const workspaceIdConsumerWrapper = new WorkspaceIdConsumerWrapper(mockedWorkspaceClient); + const mockRequest = httpServerMock.createOpenSearchDashboardsRequest(); + updateWorkspaceState(mockRequest, {}); + const mockedWrapperClient = workspaceIdConsumerWrapper.wrapperFactory({ + client: mockedClient, + typeRegistry: requestHandlerContext.savedObjects.typeRegistry, + request: mockRequest, + }); + + mockedWorkspaceClient.list.mockResolvedValueOnce({ + success: true, + result: { + workspaces: [ + { + id: 'foo', + }, + ], + }, + }); + + expect( + mockedWrapperClient.create( + 'dashboard', + { + name: 'foo', + }, + { workspaces: ['zoo', 'noo'] } + ) + ).rejects.toMatchInlineSnapshot(`[Error: Exist invalid workspaces]`); + expect(mockedWorkspaceClient.get).toBeCalledTimes(0); + expect(mockedWorkspaceClient.list).toBeCalledTimes(1); + }); }); describe('bulkCreate', () => { beforeEach(() => { mockedClient.bulkCreate.mockClear(); + mockedWorkspaceClient.get.mockClear(); + mockedWorkspaceClient.list.mockClear(); }); it(`Should add workspaces parameters when bulk create`, async () => { + mockedWorkspaceClient.get.mockImplementationOnce((requestContext, id) => { + return { + success: true, + }; + }); await wrapperClient.bulkCreate([ getSavedObject({ id: 'foo', @@ -88,6 +136,23 @@ describe('WorkspaceIdConsumerWrapper', () => { } ); }); + + it(`Should throw error when passing in invalid workspaces`, async () => { + mockedWorkspaceClient.get.mockImplementationOnce((requestContext, id) => { + return { + success: false, + }; + }); + expect( + wrapperClient.bulkCreate([ + getSavedObject({ + id: 'foo', + }), + ]) + ).rejects.toMatchInlineSnapshot(`[Error: Exist invalid workspaces]`); + expect(mockedWorkspaceClient.get).toBeCalledTimes(1); + expect(mockedWorkspaceClient.list).toBeCalledTimes(0); + }); }); describe('checkConflict', () => { @@ -174,7 +239,7 @@ describe('WorkspaceIdConsumerWrapper', () => { type: ['dashboard', 'visualization'], workspaces: ['foo', 'not-exist'], }) - ).rejects.toMatchInlineSnapshot(`[Error: Invalid workspaces]`); + ).rejects.toMatchInlineSnapshot(`[Error: Exist invalid workspaces]`); expect(mockedWorkspaceClient.get).toBeCalledTimes(0); expect(mockedWorkspaceClient.list).toBeCalledTimes(1); }); diff --git a/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.ts b/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.ts index 43393da03ef5..f6efb690c5cd 100644 --- a/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.ts +++ b/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.ts @@ -14,6 +14,7 @@ import { OpenSearchDashboardsRequest, SavedObjectsFindOptions, SavedObjectsErrorHelpers, + SavedObjectsClientWrapperOptions, SavedObject, SavedObjectsBulkGetObject, SavedObjectsBulkResponse, @@ -61,6 +62,52 @@ export class WorkspaceIdConsumerWrapper { return type === UI_SETTINGS_SAVED_OBJECTS_TYPE; } + private async checkWorkspacesExist( + workspaces: SavedObject['workspaces'] | null, + wrapperOptions: SavedObjectsClientWrapperOptions + ) { + if (workspaces?.length) { + let invalidWorkspaces: string[] = []; + // If only has one workspace, we should use get to optimize performance + if (workspaces.length === 1) { + const workspaceGet = await this.workspaceClient.get( + { request: wrapperOptions.request }, + workspaces[0] + ); + if (!workspaceGet.success) { + invalidWorkspaces = [workspaces[0]]; + } + } else { + const workspaceList = await this.workspaceClient.list( + { + request: wrapperOptions.request, + }, + { + perPage: 9999, + } + ); + if (workspaceList.success) { + const workspaceIdsSet = new Set( + workspaceList.result.workspaces.map((workspace) => workspace.id) + ); + invalidWorkspaces = workspaces.filter( + (targetWorkspace) => !workspaceIdsSet.has(targetWorkspace) + ); + } + } + + if (invalidWorkspaces.length > 0) { + throw SavedObjectsErrorHelpers.decorateBadRequestError( + new Error( + i18n.translate('workspace.id_consumer.invalid', { + defaultMessage: 'Exist invalid workspaces', + }) + ) + ); + } + } + } + private validateObjectInAWorkspace( object: SavedObject, workspace: string, @@ -94,22 +141,21 @@ export class WorkspaceIdConsumerWrapper { public wrapperFactory: SavedObjectsClientWrapperFactory = (wrapperOptions) => { return { ...wrapperOptions.client, - create: (type: string, attributes: T, options: SavedObjectsCreateOptions = {}) => - wrapperOptions.client.create( - type, - attributes, - this.isConfigType(type) - ? options - : this.formatWorkspaceIdParams(wrapperOptions.request, options) - ), - bulkCreate: ( + create: async (type: string, attributes: T, options: SavedObjectsCreateOptions = {}) => { + const finalOptions = this.isConfigType(type) + ? options + : this.formatWorkspaceIdParams(wrapperOptions.request, options); + await this.checkWorkspacesExist(finalOptions?.workspaces, wrapperOptions); + return wrapperOptions.client.create(type, attributes, finalOptions); + }, + bulkCreate: async ( objects: Array>, options: SavedObjectsCreateOptions = {} - ) => - wrapperOptions.client.bulkCreate( - objects, - this.formatWorkspaceIdParams(wrapperOptions.request, options) - ), + ) => { + const finalOptions = this.formatWorkspaceIdParams(wrapperOptions.request, options); + await this.checkWorkspacesExist(finalOptions?.workspaces, wrapperOptions); + return wrapperOptions.client.bulkCreate(objects, finalOptions); + }, checkConflicts: ( objects: SavedObjectsCheckConflictsObject[] = [], options: SavedObjectsBaseOptions = {} @@ -127,46 +173,7 @@ export class WorkspaceIdConsumerWrapper { this.isConfigType(options.type as string) && options.sortField === 'buildNum' ? options : this.formatWorkspaceIdParams(wrapperOptions.request, options); - if (finalOptions.workspaces?.length) { - let isAllTargetWorkspaceExisting = false; - // If only has one workspace, we should use get to optimize performance - if (finalOptions.workspaces.length === 1) { - const workspaceGet = await this.workspaceClient.get( - { request: wrapperOptions.request }, - finalOptions.workspaces[0] - ); - if (workspaceGet.success) { - isAllTargetWorkspaceExisting = true; - } - } else { - const workspaceList = await this.workspaceClient.list( - { - request: wrapperOptions.request, - }, - { - perPage: 9999, - } - ); - if (workspaceList.success) { - const workspaceIdsSet = new Set( - workspaceList.result.workspaces.map((workspace) => workspace.id) - ); - isAllTargetWorkspaceExisting = finalOptions.workspaces.every((targetWorkspace) => - workspaceIdsSet.has(targetWorkspace) - ); - } - } - - if (!isAllTargetWorkspaceExisting) { - throw SavedObjectsErrorHelpers.decorateBadRequestError( - new Error( - i18n.translate('workspace.id_consumer.invalid', { - defaultMessage: 'Invalid workspaces', - }) - ) - ); - } - } + await this.checkWorkspacesExist(finalOptions?.workspaces, wrapperOptions); return wrapperOptions.client.find(finalOptions); }, bulkGet: async ( From d5e0087f825faf89094ae639d88e26c18b3708c0 Mon Sep 17 00:00:00 2001 From: Joshua Li Date: Mon, 2 Dec 2024 20:01:48 -0800 Subject: [PATCH 04/16] [Discover] fix PPL to not throw error if aggregation query fails (#8992) Signed-off-by: Joshua Li --- .../query_enhancements/common/utils.test.ts | 6 +- .../query_enhancements/common/utils.ts | 2 +- .../search/ppl_async_search_strategy.ts | 6 +- .../server/search/ppl_search_strategy.test.ts | 372 ++++++++++++++++++ .../server/search/ppl_search_strategy.ts | 6 +- .../search/sql_async_search_strategy.ts | 6 +- .../server/search/sql_search_strategy.ts | 4 +- 7 files changed, 387 insertions(+), 15 deletions(-) create mode 100644 src/plugins/query_enhancements/server/search/ppl_search_strategy.test.ts diff --git a/src/plugins/query_enhancements/common/utils.test.ts b/src/plugins/query_enhancements/common/utils.test.ts index 39bbdc258bea..787cebb0c082 100644 --- a/src/plugins/query_enhancements/common/utils.test.ts +++ b/src/plugins/query_enhancements/common/utils.test.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { handleFacetError } from './utils'; +import { throwFacetError } from './utils'; describe('handleFacetError', () => { const error = new Error('mock-error'); @@ -16,9 +16,9 @@ describe('handleFacetError', () => { data: error, }; - expect(() => handleFacetError(response)).toThrowError(); + expect(() => throwFacetError(response)).toThrowError(); try { - handleFacetError(response); + throwFacetError(response); } catch (err: any) { expect(err.message).toBe('test error message'); expect(err.name).toBe('400'); diff --git a/src/plugins/query_enhancements/common/utils.ts b/src/plugins/query_enhancements/common/utils.ts index 9b2bb9e3aacf..29e49b00eab0 100644 --- a/src/plugins/query_enhancements/common/utils.ts +++ b/src/plugins/query_enhancements/common/utils.ts @@ -42,7 +42,7 @@ export const removeKeyword = (queryString: string | undefined) => { return queryString?.replace(new RegExp('.keyword'), '') ?? ''; }; -export const handleFacetError = (response: any) => { +export const throwFacetError = (response: any) => { const error = new Error(response.data.body?.message ?? response.data.body ?? response.data); error.name = response.data.status ?? response.status ?? response.data.statusCode; (error as any).status = error.name; diff --git a/src/plugins/query_enhancements/server/search/ppl_async_search_strategy.ts b/src/plugins/query_enhancements/server/search/ppl_async_search_strategy.ts index 309c5fd522b6..2af66fb427c2 100644 --- a/src/plugins/query_enhancements/server/search/ppl_async_search_strategy.ts +++ b/src/plugins/query_enhancements/server/search/ppl_async_search_strategy.ts @@ -13,7 +13,7 @@ import { Query, } from '../../../data/common'; import { ISearchStrategy, SearchUsage } from '../../../data/server'; -import { buildQueryStatusConfig, getFields, handleFacetError, SEARCH_STRATEGY } from '../../common'; +import { buildQueryStatusConfig, getFields, throwFacetError, SEARCH_STRATEGY } from '../../common'; import { Facet } from '../utils'; export const pplAsyncSearchStrategyProvider = ( @@ -45,7 +45,7 @@ export const pplAsyncSearchStrategyProvider = ( request.body = { ...request.body, lang: SEARCH_STRATEGY.PPL }; const rawResponse: any = await pplAsyncFacet.describeQuery(context, request); - if (!rawResponse.success) handleFacetError(rawResponse); + if (!rawResponse.success) throwFacetError(rawResponse); const statusConfig = buildQueryStatusConfig(rawResponse); @@ -60,7 +60,7 @@ export const pplAsyncSearchStrategyProvider = ( request.params = { queryId: inProgressQueryId }; const queryStatusResponse = await pplAsyncJobsFacet.describeQuery(context, request); - if (!queryStatusResponse.success) handleFacetError(queryStatusResponse); + if (!queryStatusResponse.success) throwFacetError(queryStatusResponse); const queryStatus = queryStatusResponse.data?.status; logger.info(`pplAsyncSearchStrategy: JOB: ${inProgressQueryId} - STATUS: ${queryStatus}`); diff --git a/src/plugins/query_enhancements/server/search/ppl_search_strategy.test.ts b/src/plugins/query_enhancements/server/search/ppl_search_strategy.test.ts new file mode 100644 index 000000000000..ae8105180db8 --- /dev/null +++ b/src/plugins/query_enhancements/server/search/ppl_search_strategy.test.ts @@ -0,0 +1,372 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + ILegacyClusterClient, + Logger, + RequestHandlerContext, + SharedGlobalConfig, +} from 'opensearch-dashboards/server'; +import { Observable, of } from 'rxjs'; +import { DATA_FRAME_TYPES, IOpenSearchDashboardsSearchRequest } from '../../../data/common'; +import { SearchUsage } from '../../../data/server'; +import * as utils from '../../common/utils'; +import * as facet from '../utils/facet'; +import { pplSearchStrategyProvider } from './ppl_search_strategy'; + +jest.mock('../../common/utils', () => ({ + ...jest.requireActual('../../common/utils'), + getFields: jest.fn(), +})); + +describe('pplSearchStrategyProvider', () => { + let config$: Observable; + let logger: Logger; + let client: ILegacyClusterClient; + let usage: SearchUsage; + const emptyRequestHandlerContext = ({} as unknown) as RequestHandlerContext; + + beforeEach(() => { + config$ = of({} as SharedGlobalConfig); + logger = ({ + error: jest.fn(), + } as unknown) as Logger; + client = {} as ILegacyClusterClient; + usage = { + trackSuccess: jest.fn(), + trackError: jest.fn(), + } as SearchUsage; + }); + + it('should return an object with a search method', () => { + const strategy = pplSearchStrategyProvider(config$, logger, client, usage); + expect(strategy).toHaveProperty('search'); + expect(typeof strategy.search).toBe('function'); + }); + + it('should handle successful search response', async () => { + const mockResponse = { + success: true, + data: { + schema: [ + { name: 'field1', type: 'long' }, + { name: 'field2', type: 'text' }, + ], + datarows: [ + [1, 'value1'], + [2, 'value2'], + ], + }, + took: 100, + }; + const mockFacet = ({ + describeQuery: jest.fn().mockResolvedValue(mockResponse), + } as unknown) as facet.Facet; + jest.spyOn(facet, 'Facet').mockImplementation(() => mockFacet); + (utils.getFields as jest.Mock).mockReturnValue([ + { name: 'field1', type: 'long' }, + { name: 'field2', type: 'text' }, + ]); + + const strategy = pplSearchStrategyProvider(config$, logger, client, usage); + const result = await strategy.search( + emptyRequestHandlerContext, + ({ + body: { query: { query: 'source = table', dataset: { id: 'test-dataset' } } }, + } as unknown) as IOpenSearchDashboardsSearchRequest, + {} + ); + + expect(result).toEqual({ + type: DATA_FRAME_TYPES.DEFAULT, + body: { + name: 'test-dataset', + fields: [ + { name: 'field1', type: 'long', values: [] }, + { name: 'field2', type: 'text', values: [] }, + ], + schema: [ + { name: 'field1', type: 'long', values: [] }, + { name: 'field2', type: 'text', values: [] }, + ], + size: 2, + }, + took: 100, + }); + expect(usage.trackSuccess).toHaveBeenCalledWith(100); + }); + + it('should handle failed search response', async () => { + const mockResponse = { + success: false, + data: { cause: 'Query failed' }, + took: 50, + }; + const mockFacet = ({ + describeQuery: jest.fn().mockResolvedValue(mockResponse), + } as unknown) as facet.Facet; + jest.spyOn(facet, 'Facet').mockImplementation(() => mockFacet); + + const strategy = pplSearchStrategyProvider(config$, logger, client, usage); + await expect( + strategy.search( + emptyRequestHandlerContext, + ({ + body: { query: { query: 'source = table' } }, + } as unknown) as IOpenSearchDashboardsSearchRequest, + {} + ) + ).rejects.toThrow(); + }); + + it('should handle exceptions', async () => { + const mockError = new Error('Something went wrong'); + const mockFacet = ({ + describeQuery: jest.fn().mockRejectedValue(mockError), + } as unknown) as facet.Facet; + jest.spyOn(facet, 'Facet').mockImplementation(() => mockFacet); + + const strategy = pplSearchStrategyProvider(config$, logger, client, usage); + await expect( + strategy.search( + emptyRequestHandlerContext, + ({ + body: { query: { query: 'source = table' } }, + } as unknown) as IOpenSearchDashboardsSearchRequest, + {} + ) + ).rejects.toThrow(mockError); + expect(logger.error).toHaveBeenCalledWith(`pplSearchStrategy: ${mockError.message}`); + expect(usage.trackError).toHaveBeenCalled(); + }); + + it('should throw error when describeQuery success is false', async () => { + const mockError = new Error('Something went wrong'); + const mockFacet = ({ + describeQuery: jest.fn().mockResolvedValue({ success: false, data: mockError }), + } as unknown) as facet.Facet; + jest.spyOn(facet, 'Facet').mockImplementation(() => mockFacet); + + const strategy = pplSearchStrategyProvider(config$, logger, client, usage); + await expect( + strategy.search( + emptyRequestHandlerContext, + ({ + body: { query: { query: 'source = table' } }, + } as unknown) as IOpenSearchDashboardsSearchRequest, + {} + ) + ).rejects.toThrowError(); + expect(logger.error).toHaveBeenCalledWith(expect.stringContaining(mockError.message)); + expect(usage.trackError).toHaveBeenCalled(); + }); + + it('should handle empty search response', async () => { + const mockResponse = { + success: true, + data: { + schema: [ + { name: 'field1', type: 'long' }, + { name: 'field2', type: 'text' }, + ], + datarows: [], + }, + took: 10, + }; + const mockFacet = ({ + describeQuery: jest.fn().mockResolvedValue(mockResponse), + } as unknown) as facet.Facet; + jest.spyOn(facet, 'Facet').mockImplementation(() => mockFacet); + (utils.getFields as jest.Mock).mockReturnValue([ + { name: 'field1', type: 'long' }, + { name: 'field2', type: 'text' }, + ]); + + const strategy = pplSearchStrategyProvider(config$, logger, client, usage); + const result = await strategy.search( + emptyRequestHandlerContext, + ({ + body: { query: { query: 'source = empty_table', dataset: { id: 'empty-dataset' } } }, + } as unknown) as IOpenSearchDashboardsSearchRequest, + {} + ); + + expect(result).toEqual({ + type: DATA_FRAME_TYPES.DEFAULT, + body: { + name: 'empty-dataset', + fields: [ + { name: 'field1', type: 'long', values: [] }, + { name: 'field2', type: 'text', values: [] }, + ], + schema: [ + { name: 'field1', type: 'long', values: [] }, + { name: 'field2', type: 'text', values: [] }, + ], + size: 0, + }, + took: 10, + }); + expect(usage.trackSuccess).toHaveBeenCalledWith(10); + }); + + it('should handle aggConfig when response succeeds', async () => { + const mockResponse = { + success: true, + data: { + schema: [ + { name: 'field1', type: 'long' }, + { name: 'field2', type: 'text' }, + ], + datarows: [ + [1, 'value1'], + [2, 'value2'], + ], + }, + took: 10, + }; + const mockFacet = ({ + describeQuery: jest.fn().mockResolvedValue(mockResponse), + } as unknown) as facet.Facet; + jest.spyOn(facet, 'Facet').mockImplementation(() => mockFacet); + (utils.getFields as jest.Mock).mockReturnValue([ + { name: 'field1', type: 'long' }, + { name: 'field2', type: 'text' }, + ]); + + const strategy = pplSearchStrategyProvider(config$, logger, client, usage); + const result = await strategy.search( + emptyRequestHandlerContext, + ({ + body: { + query: { query: 'source = empty_table', dataset: { id: 'empty-dataset' } }, + aggConfig: { + date_histogram: { + field: 'timestamp', + fixed_interval: '12h', + time_zone: 'America/Los_Angeles', + min_doc_count: 1, + }, + qs: { + '2': 'source = empty_table | stats count() by span(timestamp, 12h)', + }, + }, + }, + } as unknown) as IOpenSearchDashboardsSearchRequest, + {} + ); + + expect(result).toEqual({ + type: DATA_FRAME_TYPES.DEFAULT, + body: { + name: 'empty-dataset', + fields: [ + { name: 'field1', type: 'long', values: [] }, + { name: 'field2', type: 'text', values: [] }, + ], + schema: [ + { name: 'field1', type: 'long', values: [] }, + { name: 'field2', type: 'text', values: [] }, + ], + aggs: { + '2': [ + { key: 'value1', value: 1 }, + { key: 'value2', value: 2 }, + ], + }, + meta: { + date_histogram: { + field: 'timestamp', + fixed_interval: '12h', + time_zone: 'America/Los_Angeles', + min_doc_count: 1, + }, + qs: { '2': 'source = empty_table | stats count() by span(timestamp, 12h)' }, + }, + size: 2, + }, + took: 10, + }); + expect(usage.trackSuccess).toHaveBeenCalledWith(10); + }); + + it('should handle aggConfig when aggregation fails', async () => { + const mockResponse = { + success: true, + data: { + schema: [ + { name: 'field1', type: 'long' }, + { name: 'field2', type: 'text' }, + ], + datarows: [ + [1, 'value1'], + [2, 'value2'], + ], + }, + took: 10, + }; + const mockError = new Error('Something went wrong'); + const mockFacet = ({ + describeQuery: jest + .fn() + .mockResolvedValueOnce(mockResponse) + .mockResolvedValue({ success: false, data: mockError }), + } as unknown) as facet.Facet; + jest.spyOn(facet, 'Facet').mockImplementation(() => mockFacet); + (utils.getFields as jest.Mock).mockReturnValue([ + { name: 'field1', type: 'long' }, + { name: 'field2', type: 'text' }, + ]); + + const strategy = pplSearchStrategyProvider(config$, logger, client, usage); + const result = await strategy.search( + emptyRequestHandlerContext, + ({ + body: { + query: { query: 'source = empty_table', dataset: { id: 'empty-dataset' } }, + aggConfig: { + date_histogram: { + field: 'timestamp', + fixed_interval: '12h', + time_zone: 'America/Los_Angeles', + min_doc_count: 1, + }, + qs: { + '2': 'source = empty_table | stats count() by span(timestamp, 12h)', + }, + }, + }, + } as unknown) as IOpenSearchDashboardsSearchRequest, + {} + ); + + expect(result).toEqual({ + type: DATA_FRAME_TYPES.DEFAULT, + body: { + name: 'empty-dataset', + fields: [ + { name: 'field1', type: 'long', values: [] }, + { name: 'field2', type: 'text', values: [] }, + ], + schema: [ + { name: 'field1', type: 'long', values: [] }, + { name: 'field2', type: 'text', values: [] }, + ], + meta: { + date_histogram: { + field: 'timestamp', + fixed_interval: '12h', + time_zone: 'America/Los_Angeles', + min_doc_count: 1, + }, + qs: { '2': 'source = empty_table | stats count() by span(timestamp, 12h)' }, + }, + size: 2, + }, + took: 10, + }); + expect(usage.trackSuccess).toHaveBeenCalledWith(10); + }); +}); diff --git a/src/plugins/query_enhancements/server/search/ppl_search_strategy.ts b/src/plugins/query_enhancements/server/search/ppl_search_strategy.ts index d71ae6810fad..d47d2ca41c4a 100644 --- a/src/plugins/query_enhancements/server/search/ppl_search_strategy.ts +++ b/src/plugins/query_enhancements/server/search/ppl_search_strategy.ts @@ -14,7 +14,7 @@ import { Query, createDataFrame, } from '../../../data/common'; -import { getFields, handleFacetError } from '../../common/utils'; +import { getFields, throwFacetError } from '../../common/utils'; import { Facet } from '../utils'; import { QueryAggConfig } from '../../common'; @@ -39,7 +39,7 @@ export const pplSearchStrategyProvider = ( const aggConfig: QueryAggConfig | undefined = request.body.aggConfig; const rawResponse: any = await pplFacet.describeQuery(context, request); - if (!rawResponse.success) handleFacetError(rawResponse); + if (!rawResponse.success) throwFacetError(rawResponse); const dataFrame = createDataFrame({ name: query.dataset?.id, @@ -56,7 +56,7 @@ export const pplSearchStrategyProvider = ( for (const [key, aggQueryString] of Object.entries(aggConfig.qs)) { request.body.query.query = aggQueryString; const rawAggs: any = await pplFacet.describeQuery(context, request); - if (!rawAggs.success) handleFacetError(rawResponse); + if (!rawAggs.success) continue; (dataFrame as IDataFrameWithAggs).aggs = {}; (dataFrame as IDataFrameWithAggs).aggs[key] = rawAggs.data.datarows?.map((hit: any) => { return { diff --git a/src/plugins/query_enhancements/server/search/sql_async_search_strategy.ts b/src/plugins/query_enhancements/server/search/sql_async_search_strategy.ts index bc25f69a70f6..76642b9dbac5 100644 --- a/src/plugins/query_enhancements/server/search/sql_async_search_strategy.ts +++ b/src/plugins/query_enhancements/server/search/sql_async_search_strategy.ts @@ -13,7 +13,7 @@ import { Query, } from '../../../data/common'; import { ISearchStrategy, SearchUsage } from '../../../data/server'; -import { buildQueryStatusConfig, getFields, handleFacetError, SEARCH_STRATEGY } from '../../common'; +import { buildQueryStatusConfig, getFields, throwFacetError, SEARCH_STRATEGY } from '../../common'; import { Facet } from '../utils'; export const sqlAsyncSearchStrategyProvider = ( @@ -45,7 +45,7 @@ export const sqlAsyncSearchStrategyProvider = ( request.body = { ...request.body, lang: SEARCH_STRATEGY.SQL }; const rawResponse: any = await sqlAsyncFacet.describeQuery(context, request); - if (!rawResponse.success) handleFacetError(rawResponse); + if (!rawResponse.success) throwFacetError(rawResponse); const statusConfig = buildQueryStatusConfig(rawResponse); @@ -60,7 +60,7 @@ export const sqlAsyncSearchStrategyProvider = ( request.params = { queryId: inProgressQueryId }; const queryStatusResponse = await sqlAsyncJobsFacet.describeQuery(context, request); - if (!queryStatusResponse.success) handleFacetError(queryStatusResponse); + if (!queryStatusResponse.success) throwFacetError(queryStatusResponse); const queryStatus = queryStatusResponse.data?.status; logger.info(`sqlAsyncSearchStrategy: JOB: ${inProgressQueryId} - STATUS: ${queryStatus}`); diff --git a/src/plugins/query_enhancements/server/search/sql_search_strategy.ts b/src/plugins/query_enhancements/server/search/sql_search_strategy.ts index 8fa945c8809e..09f2775d0fe2 100644 --- a/src/plugins/query_enhancements/server/search/sql_search_strategy.ts +++ b/src/plugins/query_enhancements/server/search/sql_search_strategy.ts @@ -13,7 +13,7 @@ import { Query, createDataFrame, } from '../../../data/common'; -import { getFields, handleFacetError } from '../../common/utils'; +import { getFields, throwFacetError } from '../../common/utils'; import { Facet } from '../utils'; export const sqlSearchStrategyProvider = ( @@ -36,7 +36,7 @@ export const sqlSearchStrategyProvider = ( const query: Query = request.body.query; const rawResponse: any = await sqlFacet.describeQuery(context, request); - if (!rawResponse.success) handleFacetError(rawResponse); + if (!rawResponse.success) throwFacetError(rawResponse); const dataFrame = createDataFrame({ name: query.dataset?.id, From e07680b8f3f40e5d805be5396a4d3742ca7f0763 Mon Sep 17 00:00:00 2001 From: Daniel Rowe <51932404+d-rowe@users.noreply.github.com> Date: Tue, 3 Dec 2024 12:31:10 -0800 Subject: [PATCH 05/16] Upgrade Cypress to v12 (#8995) * Update Cypress to v12 (#8926) * Update cypress to v12 Signed-off-by: Daniel Rowe * Add required e2e.js Signed-off-by: Daniel Rowe * Changeset file for PR #8926 created/updated * Update license header Signed-off-by: Daniel Rowe <51932404+d-rowe@users.noreply.github.com> * Update license in e2e.js Signed-off-by: Daniel Rowe <51932404+d-rowe@users.noreply.github.com> --------- Signed-off-by: Daniel Rowe Signed-off-by: Daniel Rowe <51932404+d-rowe@users.noreply.github.com> Co-authored-by: Daniel Rowe Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> * fix: support imports without extensions in cypress webpack build (#8993) * fix: support imports without extensions in cypress webpack build Signed-off-by: Daniel Rowe * Changeset file for PR #8993 created/updated * use typescript config Signed-off-by: Daniel Rowe <51932404+d-rowe@users.noreply.github.com> * fix lint Signed-off-by: Daniel Rowe <51932404+d-rowe@users.noreply.github.com> * disable new test isolation feature This isolation was causing regressions Signed-off-by: Daniel Rowe <51932404+d-rowe@users.noreply.github.com> --------- Signed-off-by: Daniel Rowe Signed-off-by: Daniel Rowe <51932404+d-rowe@users.noreply.github.com> Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> --------- Signed-off-by: Daniel Rowe Signed-off-by: Daniel Rowe <51932404+d-rowe@users.noreply.github.com> Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> --- changelogs/fragments/8926.yml | 2 + changelogs/fragments/8993.yml | 2 + cypress.config.ts | 65 +++++++++ cypress.json | 21 --- cypress/support/e2e.js | 6 + package.json | 3 +- yarn.lock | 242 ++++++++++++++++++++++++++++++---- 7 files changed, 293 insertions(+), 48 deletions(-) create mode 100644 changelogs/fragments/8926.yml create mode 100644 changelogs/fragments/8993.yml create mode 100644 cypress.config.ts delete mode 100644 cypress.json create mode 100644 cypress/support/e2e.js diff --git a/changelogs/fragments/8926.yml b/changelogs/fragments/8926.yml new file mode 100644 index 000000000000..b99f449c54ca --- /dev/null +++ b/changelogs/fragments/8926.yml @@ -0,0 +1,2 @@ +chore: +- Update cypress to v12 ([#8926](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8926)) \ No newline at end of file diff --git a/changelogs/fragments/8993.yml b/changelogs/fragments/8993.yml new file mode 100644 index 000000000000..dac519c8b746 --- /dev/null +++ b/changelogs/fragments/8993.yml @@ -0,0 +1,2 @@ +fix: +- Support imports without extensions in cypress webpack build ([#8993](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8993)) \ No newline at end of file diff --git a/cypress.config.ts b/cypress.config.ts new file mode 100644 index 000000000000..67e7b4f5039b --- /dev/null +++ b/cypress.config.ts @@ -0,0 +1,65 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { defineConfig } from 'cypress'; +import webpackPreprocessor from '@cypress/webpack-preprocessor'; + +module.exports = defineConfig({ + defaultCommandTimeout: 60000, + requestTimeout: 60000, + responseTimeout: 60000, + viewportWidth: 2000, + viewportHeight: 1320, + env: { + openSearchUrl: 'http://localhost:9200', + SECURITY_ENABLED: false, + AGGREGATION_VIEW: false, + username: 'admin', + password: 'myStrongPassword123!', + ENDPOINT_WITH_PROXY: false, + MANAGED_SERVICE_ENDPOINT: false, + VISBUILDER_ENABLED: true, + DATASOURCE_MANAGEMENT_ENABLED: false, + ML_COMMONS_DASHBOARDS_ENABLED: true, + WAIT_FOR_LOADER_BUFFER_MS: 0, + }, + e2e: { + baseUrl: 'http://localhost:5601', + specPattern: 'cypress/integration/**/*_spec.{js,jsx,ts,tsx}', + testIsolation: false, + setupNodeEvents, + }, +}); + +function setupNodeEvents( + on: Cypress.PluginEvents, + config: Cypress.PluginConfigOptions +): Cypress.PluginConfigOptions { + const { webpackOptions } = webpackPreprocessor.defaultOptions; + + /** + * By default, cypress' internal webpack preprocessor doesn't allow imports without file extensions. + * This makes our life a bit hard since if any file in our testing dependency graph has an import without + * the .js extension our cypress build will fail. + * + * This extra rule relaxes this a bit by allowing imports without file extension + * ex. import module from './module' + */ + webpackOptions!.module!.rules.unshift({ + test: /\.m?js/, + resolve: { + enforceExtension: false, + }, + }); + + on( + 'file:preprocessor', + webpackPreprocessor({ + webpackOptions, + }) + ); + + return config; +} diff --git a/cypress.json b/cypress.json deleted file mode 100644 index 46e8c7e8ea16..000000000000 --- a/cypress.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "defaultCommandTimeout": 60000, - "requestTimeout": 60000, - "responseTimeout": 60000, - "baseUrl": "http://localhost:5601", - "viewportWidth": 2000, - "viewportHeight": 1320, - "env": { - "openSearchUrl": "http://localhost:9200", - "SECURITY_ENABLED": false, - "AGGREGATION_VIEW": false, - "username": "admin", - "password": "myStrongPassword123!", - "ENDPOINT_WITH_PROXY": false, - "MANAGED_SERVICE_ENDPOINT": false, - "VISBUILDER_ENABLED": true, - "DATASOURCE_MANAGEMENT_ENABLED": false, - "ML_COMMONS_DASHBOARDS_ENABLED": true, - "WAIT_FOR_LOADER_BUFFER_MS": 0 - } -} diff --git a/cypress/support/e2e.js b/cypress/support/e2e.js new file mode 100644 index 000000000000..fa35cf4214b4 --- /dev/null +++ b/cypress/support/e2e.js @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import '../utils/commands'; diff --git a/package.json b/package.json index 9d83eec7c6cf..0a103b9fdab1 100644 --- a/package.json +++ b/package.json @@ -259,6 +259,7 @@ "@babel/plugin-transform-class-static-block": "^7.24.4", "@babel/register": "^7.22.9", "@babel/types": "^7.22.9", + "@cypress/webpack-preprocessor": "^5.17.1", "@elastic/apm-rum": "^5.6.1", "@elastic/charts": "31.1.0", "@elastic/ems-client": "7.10.0", @@ -383,7 +384,7 @@ "chromedriver": "^121.0.1", "classnames": "^2.3.1", "compare-versions": "3.5.1", - "cypress": "9.5.4", + "cypress": "12.17.4", "d3": "3.5.17", "d3-cloud": "1.2.5", "dedent": "^0.7.0", diff --git a/yarn.lock b/yarn.lock index b19d1350a13f..537af6f3662e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1294,7 +1294,7 @@ resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== -"@cypress/request@^2.88.10": +"@cypress/request@2.88.12": version "2.88.12" resolved "https://registry.yarnpkg.com/@cypress/request/-/request-2.88.12.tgz#ba4911431738494a85e93fb04498cb38bc55d590" integrity sha512-tOn+0mDZxASFM+cuAP9szGUGPI1HwWVSvdzm7V4cCsPdFTx6qMj29CwaQmRAMIEhORIUBFBsYROYJcveK4uOjA== @@ -2929,6 +2929,11 @@ resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== +"@tootallnate/quickjs-emscripten@^0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz#db4ecfd499a9765ab24002c3b696d02e6d32a12c" + integrity sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA== + "@tsd/typescript@~4.7.3": version "4.7.4" resolved "https://registry.yarnpkg.com/@tsd/typescript/-/typescript-4.7.4.tgz#f1e4e6c3099a174a0cb7aa51cf53f34f6494e528" @@ -3534,7 +3539,7 @@ dependencies: "@types/node" "*" -"@types/node@*", "@types/node@12.20.24", "@types/node@16.9.1", "@types/node@^14.14.31", "@types/node@~18.7.0": +"@types/node@*", "@types/node@12.20.24", "@types/node@16.9.1", "@types/node@^16.18.39", "@types/node@~18.7.0": version "18.7.23" resolved "https://registry.yarnpkg.com/@types/node/-/node-18.7.23.tgz#75c580983846181ebe5f4abc40fe9dfb2d65665f" integrity sha512-DWNcCHolDq0ZKGizjx2DZjR/PqsYwAcYUJmfMWqtVU2MBMG5Mo+xFZrhGId5r/O5HOuMPyQEcM6KUBp5lBZZBg== @@ -4367,6 +4372,13 @@ agent-base@6: dependencies: debug "4" +agent-base@^7.0.2, agent-base@^7.1.0, agent-base@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.1.tgz#bdbded7dfb096b751a2a087eeeb9664725b2e317" + integrity sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA== + dependencies: + debug "^4.3.4" + agentkeepalive@^3.4.1, agentkeepalive@^4.2.1, agentkeepalive@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.5.0.tgz#2673ad1389b3c418c5a20c5d7364f93ca04be923" @@ -4815,6 +4827,13 @@ ast-types-flow@^0.0.7: resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.7.tgz#f70b735c6bca1a5c9c22d982c3e39e7feba3bdad" integrity sha1-9wtzXGvKGlycItmCw+Oef+ujva0= +ast-types@^0.13.4: + version "0.13.4" + resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.13.4.tgz#ee0d77b343263965ecc3fb62da16e7222b2b6782" + integrity sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w== + dependencies: + tslib "^2.0.1" + astral-regex@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9" @@ -4927,7 +4946,7 @@ axe-core@^4.0.2, axe-core@^4.3.5: resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.4.1.tgz#7dbdc25989298f9ad006645cd396782443757413" integrity sha512-gd1kmb21kwNuWr6BQz8fv6GNECPBnUasepcoLbekws23NVBLODdsClRZ+bQ8+9Uomf3Sm3+Vwn0oYG9NvwnJCw== -axios@^1.6.1, axios@^1.6.5: +axios@^1.6.1: version "1.7.7" resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.7.tgz#2f554296f9892a72ac8d8e4c5b79c14a91d0a47f" integrity sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q== @@ -4936,6 +4955,15 @@ axios@^1.6.1, axios@^1.6.5: form-data "^4.0.0" proxy-from-env "^1.1.0" +axios@^1.7.4: + version "1.7.8" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.8.tgz#1997b1496b394c21953e68c14aaa51b7b5de3d6e" + integrity sha512-Uu0wb7KNqK2t5K+YQyVCLM76prD5sRFjKHbJYCP1J7JFGEQ6nN7HWn9+04LAeiJ3ji54lgS/gZCH1oxyrf1SPw== + dependencies: + follow-redirects "^1.15.6" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + axobject-query@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be" @@ -5123,6 +5151,11 @@ basic-auth@^2.0.1: dependencies: safe-buffer "5.1.2" +basic-ftp@^5.0.2: + version "5.0.5" + resolved "https://registry.yarnpkg.com/basic-ftp/-/basic-ftp-5.0.5.tgz#14a474f5fffecca1f4f406f1c26b18f800225ac0" + integrity sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg== + batch-processor@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/batch-processor/-/batch-processor-1.0.0.tgz#75c95c32b748e0850d10c2b168f6bdbe9891ace8" @@ -5710,16 +5743,16 @@ chrome-trace-event@^1.0.2: resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac" integrity sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg== -chromedriver@^121.0.1: - version "121.0.2" - resolved "https://registry.yarnpkg.com/chromedriver/-/chromedriver-121.0.2.tgz#208909a61e9d510913107ea6faf34bcdd72cdced" - integrity sha512-58MUSCEE3oB3G3Y/Jo3URJ2Oa1VLHcVBufyYt7vNfGrABSJm7ienQLF9IQ8LPDlPVgLUXt2OBfggK3p2/SlEBg== +chromedriver@^131.0.1: + version "131.0.1" + resolved "https://registry.yarnpkg.com/chromedriver/-/chromedriver-131.0.1.tgz#bfbf47f6c2ad7a65c154ff47d321bd8c33b52a77" + integrity sha512-LHRh+oaNU1WowJjAkWsviN8pTzQYJDbv/FvJyrQ7XhjKdIzVh/s3GV1iU7IjMTsxIQnBsTjx+9jWjzCWIXC7ug== dependencies: "@testim/chrome-version" "^1.1.4" - axios "^1.6.5" + axios "^1.7.4" compare-versions "^6.1.0" extract-zip "^2.0.1" - https-proxy-agent "^5.0.1" + proxy-agent "^6.4.0" proxy-from-env "^1.1.0" tcp-port-used "^1.0.2" @@ -6017,11 +6050,16 @@ commander@^4.0.1: resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== -commander@^5.0.0, commander@^5.1.0: +commander@^5.0.0: version "5.1.0" resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae" integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg== +commander@^6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" + integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== + comment-stripper@^0.0.4: version "0.0.4" resolved "https://registry.yarnpkg.com/comment-stripper/-/comment-stripper-0.0.4.tgz#e8d61366d362779ea225c764f05cca6c950f8a2c" @@ -6498,14 +6536,14 @@ cyclist@^1.0.1: resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9" integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk= -cypress@9.5.4: - version "9.5.4" - resolved "https://registry.yarnpkg.com/cypress/-/cypress-9.5.4.tgz#49d9272f62eba12f2314faf29c2a865610e87550" - integrity sha512-6AyJAD8phe7IMvOL4oBsI9puRNOWxZjl8z1lgixJMcgJ85JJmyKeP6uqNA0dI1z14lmJ7Qklf2MOgP/xdAqJ/Q== +cypress@12.17.4: + version "12.17.4" + resolved "https://registry.yarnpkg.com/cypress/-/cypress-12.17.4.tgz#b4dadf41673058493fa0d2362faa3da1f6ae2e6c" + integrity sha512-gAN8Pmns9MA5eCDFSDJXWKUpaL3IDd89N9TtIupjYnzLSmlpVr+ZR+vb4U/qaMp+lB6tBvAmt7504c3Z4RU5KQ== dependencies: - "@cypress/request" "^2.88.10" + "@cypress/request" "2.88.12" "@cypress/xvfb" "^1.2.4" - "@types/node" "^14.14.31" + "@types/node" "^16.18.39" "@types/sinonjs__fake-timers" "8.1.1" "@types/sizzle" "^2.3.2" arch "^2.2.0" @@ -6517,12 +6555,12 @@ cypress@9.5.4: check-more-types "^2.24.0" cli-cursor "^3.1.0" cli-table3 "~0.6.1" - commander "^5.1.0" + commander "^6.2.1" common-tags "^1.8.0" dayjs "^1.10.4" - debug "^4.3.2" + debug "^4.3.4" enquirer "^2.3.6" - eventemitter2 "^6.4.3" + eventemitter2 "6.4.7" execa "4.1.0" executable "^4.1.1" extract-zip "2.0.1" @@ -6535,12 +6573,13 @@ cypress@9.5.4: listr2 "^3.8.3" lodash "^4.17.21" log-symbols "^4.0.0" - minimist "^1.2.6" + minimist "^1.2.8" ospath "^1.2.2" pretty-bytes "^5.6.0" + process "^0.11.10" proxy-from-env "1.0.0" request-progress "^3.0.0" - semver "^7.3.2" + semver "^7.5.3" supports-color "^8.1.1" tmp "~0.2.1" untildify "^4.0.0" @@ -6769,6 +6808,11 @@ dashify@^0.1.0: resolved "https://registry.yarnpkg.com/dashify/-/dashify-0.1.0.tgz#107daf9cca5e326e30a8b39ffa5048b6684922ea" integrity sha1-EH2vnMpeMm4wqLOf+lBItmhJIuo= +data-uri-to-buffer@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz#8a58bb67384b261a38ef18bea1810cb01badd28b" + integrity sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw== + data-urls@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-2.0.0.tgz#156485a72963a970f5d5821aaf642bef2bf2db9b" @@ -6967,6 +7011,15 @@ defined@^1.0.0: resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693" integrity sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM= +degenerator@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/degenerator/-/degenerator-5.0.1.tgz#9403bf297c6dad9a1ece409b37db27954f91f2f5" + integrity sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ== + dependencies: + ast-types "^0.13.4" + escodegen "^2.1.0" + esprima "^4.0.1" + del-cli@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/del-cli/-/del-cli-3.0.1.tgz#2d27ff260204b5104cadeda86f78f180a4ebe89a" @@ -7808,6 +7861,17 @@ escodegen@^2.0.0: optionalDependencies: source-map "~0.6.1" +escodegen@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-2.1.0.tgz#ba93bbb7a43986d29d6041f99f5262da773e2e17" + integrity sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w== + dependencies: + esprima "^4.0.1" + estraverse "^5.2.0" + esutils "^2.0.2" + optionalDependencies: + source-map "~0.6.1" + eslint-config-prettier@^6.11.0: version "6.15.0" resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-6.15.0.tgz#7f93f6cb7d45a92f1537a70ecc06366e1ac6fed9" @@ -8197,10 +8261,10 @@ event-target-shim@^5.0.0: resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== -eventemitter2@^6.4.3: - version "6.4.9" - resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.4.9.tgz#41f2750781b4230ed58827bc119d293471ecb125" - integrity sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg== +eventemitter2@6.4.7: + version "6.4.7" + resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.4.7.tgz#a7f6c4d7abf28a14c1ef3442f21cb306a054271d" + integrity sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg== eventemitter2@~0.4.13: version "0.4.14" @@ -8822,6 +8886,15 @@ fs-constants@^1.0.0: resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== +fs-extra@^11.2.0: + version "11.2.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.2.0.tgz#e70e17dfad64232287d01929399e0ea7c86b0e5b" + integrity sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + fs-extra@^9.1.0: version "9.1.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" @@ -8977,6 +9050,16 @@ get-symbol-description@^1.0.0: call-bind "^1.0.2" get-intrinsic "^1.1.1" +get-uri@^6.0.1: + version "6.0.3" + resolved "https://registry.yarnpkg.com/get-uri/-/get-uri-6.0.3.tgz#0d26697bc13cf91092e519aa63aa60ee5b6f385a" + integrity sha512-BzUrJBS9EcUb4cFol8r4W3v1cPsSyajLSthNkz5BxbpDcHN5tIrM10E2eNvfnvBn3DaT3DUgx0OpsBKkaOpanw== + dependencies: + basic-ftp "^5.0.2" + data-uri-to-buffer "^6.0.2" + debug "^4.3.4" + fs-extra "^11.2.0" + get-value@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/get-value/-/get-value-3.0.1.tgz#5efd2a157f1d6a516d7524e124ac52d0a39ef5a8" @@ -9803,6 +9886,14 @@ http-proxy-agent@^4.0.1: agent-base "6" debug "4" +http-proxy-agent@^7.0.0, http-proxy-agent@^7.0.1: + version "7.0.2" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz#9a8b1f246866c028509486585f62b8f2c18c270e" + integrity sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig== + dependencies: + agent-base "^7.1.0" + debug "^4.3.4" + http-signature@~1.3.6: version "1.3.6" resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.3.6.tgz#cb6fbfdf86d1c974f343be94e87f7fc128662cf9" @@ -9825,7 +9916,7 @@ https-browserify@^1.0.0: resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM= -https-proxy-agent@5.0.1, https-proxy-agent@^5.0.0, https-proxy-agent@^5.0.1: +https-proxy-agent@5.0.1, https-proxy-agent@^5.0.0: version "5.0.1" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== @@ -9833,6 +9924,14 @@ https-proxy-agent@5.0.1, https-proxy-agent@^5.0.0, https-proxy-agent@^5.0.1: agent-base "6" debug "4" +https-proxy-agent@^7.0.3, https-proxy-agent@^7.0.5: + version "7.0.5" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz#9e8b5013873299e11fab6fd548405da2d6c602b2" + integrity sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw== + dependencies: + agent-base "^7.0.2" + debug "4" + human-signals@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" @@ -10128,6 +10227,14 @@ ip-address@^6.3.0: lodash.repeat "4.1.0" sprintf-js "1.1.2" +ip-address@^9.0.5: + version "9.0.5" + resolved "https://registry.yarnpkg.com/ip-address/-/ip-address-9.0.5.tgz#117a960819b08780c3bd1f14ef3c1cc1d3f3ea5a" + integrity sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g== + dependencies: + jsbn "1.1.0" + sprintf-js "^1.1.3" + ip-cidr@^2.1.0: version "2.1.5" resolved "https://registry.yarnpkg.com/ip-cidr/-/ip-cidr-2.1.5.tgz#67fd02ee001d6ac0f253a1d577e4170a8f7d480b" @@ -12022,6 +12129,11 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" +lru-cache@^7.14.1: + version "7.18.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.18.3.tgz#f793896e0fd0e954a59dfdd82f0773808df6aa89" + integrity sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA== + lru-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/lru-queue/-/lru-queue-0.1.0.tgz#2738bd9f0d3cf4f84490c5736c48699ac632cda3" @@ -12418,6 +12530,11 @@ minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== +minimist@^1.2.8: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + minipass-collect@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/minipass-collect/-/minipass-collect-1.0.2.tgz#22b813bf745dc6edba2576b940022ad6edc8c617" @@ -12724,6 +12841,11 @@ nested-error-stacks@^2.0.0, nested-error-stacks@^2.1.0: resolved "https://registry.yarnpkg.com/nested-error-stacks/-/nested-error-stacks-2.1.1.tgz#26c8a3cee6cc05fbcf1e333cd2fc3e003326c0b5" integrity sha512-9iN1ka/9zmX1ZvLV9ewJYEk9h7RyRRtqdK0woXcqohu8EWIerfPUjYJPg0ULy0UqP7cslmdGc8xKDJcojlKiaw== +netmask@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/netmask/-/netmask-2.0.2.tgz#8b01a07644065d536383835823bc52004ebac5e7" + integrity sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg== + newtype-ts@^0.2.4: version "0.2.4" resolved "https://registry.yarnpkg.com/newtype-ts/-/newtype-ts-0.2.4.tgz#a02a8f160a3d179f871848d687a93de73a964a41" @@ -13338,6 +13460,28 @@ p-try@^2.0.0: resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== +pac-proxy-agent@^7.0.1: + version "7.0.2" + resolved "https://registry.yarnpkg.com/pac-proxy-agent/-/pac-proxy-agent-7.0.2.tgz#0fb02496bd9fb8ae7eb11cfd98386daaac442f58" + integrity sha512-BFi3vZnO9X5Qt6NRz7ZOaPja3ic0PhlsmCRYLOpN11+mWBCR6XJDqW5RF3j8jm4WGGQZtBA+bTfxYzeKW73eHg== + dependencies: + "@tootallnate/quickjs-emscripten" "^0.23.0" + agent-base "^7.0.2" + debug "^4.3.4" + get-uri "^6.0.1" + http-proxy-agent "^7.0.0" + https-proxy-agent "^7.0.5" + pac-resolver "^7.0.1" + socks-proxy-agent "^8.0.4" + +pac-resolver@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/pac-resolver/-/pac-resolver-7.0.1.tgz#54675558ea368b64d210fd9c92a640b5f3b8abb6" + integrity sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg== + dependencies: + degenerator "^5.0.0" + netmask "^2.0.2" + package-hash@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/package-hash/-/package-hash-4.0.0.tgz#3537f654665ec3cc38827387fc904c163c54f506" @@ -13938,6 +14082,20 @@ property-information@^5.0.0, property-information@^5.3.0: dependencies: xtend "^4.0.0" +proxy-agent@^6.4.0: + version "6.4.0" + resolved "https://registry.yarnpkg.com/proxy-agent/-/proxy-agent-6.4.0.tgz#b4e2dd51dee2b377748aef8d45604c2d7608652d" + integrity sha512-u0piLU+nCOHMgGjRbimiXmA9kM/L9EHh3zL81xCdp7m+Y2pHIsnmbdDoEDoAz5geaonNR6q6+yOPQs6n4T6sBQ== + dependencies: + agent-base "^7.0.2" + debug "^4.3.4" + http-proxy-agent "^7.0.1" + https-proxy-agent "^7.0.3" + lru-cache "^7.14.1" + pac-proxy-agent "^7.0.1" + proxy-from-env "^1.1.0" + socks-proxy-agent "^8.0.2" + proxy-from-env@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.0.0.tgz#33c50398f70ea7eb96d21f7b817630a55791c7ee" @@ -15581,6 +15739,28 @@ slide@~1.1.3: resolved "https://registry.yarnpkg.com/slide/-/slide-1.1.6.tgz#56eb027d65b4d2dce6cb2e2d32c4d4afc9e1d707" integrity sha1-VusCfWW00tzmyy4tMsTUr8nh1wc= +smart-buffer@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae" + integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== + +socks-proxy-agent@^8.0.2, socks-proxy-agent@^8.0.4: + version "8.0.4" + resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-8.0.4.tgz#9071dca17af95f483300316f4b063578fa0db08c" + integrity sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSG+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRw== + dependencies: + agent-base "^7.1.1" + debug "^4.3.4" + socks "^2.8.3" + +socks@^2.8.3: + version "2.8.3" + resolved "https://registry.yarnpkg.com/socks/-/socks-2.8.3.tgz#1ebd0f09c52ba95a09750afe3f3f9f724a800cb5" + integrity sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw== + dependencies: + ip-address "^9.0.5" + smart-buffer "^4.2.0" + sonic-boom@^1.0.2: version "1.4.1" resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-1.4.1.tgz#d35d6a74076624f12e6f917ade7b9d75e918f53e" @@ -15793,6 +15973,11 @@ sprintf-js@1.1.2, sprintf-js@^1.1.1: resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.2.tgz#da1765262bf8c0f571749f2ad6c26300207ae673" integrity sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug== +sprintf-js@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.3.tgz#4914b903a2f8b685d17fdf78a70e917e872e444a" + integrity sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA== + sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" @@ -16899,6 +17084,11 @@ tslib@^2.0.0, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.2.0, tslib@^2.3.0, tslib@^2.3 resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== +tslib@^2.0.1: + version "2.8.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" + integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== + tslib@~2.4.0: version "2.4.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.1.tgz#0d0bfbaac2880b91e22df0768e55be9753a5b17e" From fe616e7755faf7e263c55792d5062c597663847b Mon Sep 17 00:00:00 2001 From: Joshua Li Date: Tue, 3 Dec 2024 13:22:27 -0800 Subject: [PATCH 06/16] [Query enhancements] use status 503 if search strategy throws 500 (#8876) * [Query enhancements] use status 503 if opensearch throws 500 Signed-off-by: Joshua Li * update unit tests Signed-off-by: Joshua Li --------- Signed-off-by: Joshua Li --- .../server/routes/index.test.ts | 21 +++++++++++++++++++ .../query_enhancements/server/routes/index.ts | 11 +++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 src/plugins/query_enhancements/server/routes/index.test.ts diff --git a/src/plugins/query_enhancements/server/routes/index.test.ts b/src/plugins/query_enhancements/server/routes/index.test.ts new file mode 100644 index 000000000000..9c7c7a56de2e --- /dev/null +++ b/src/plugins/query_enhancements/server/routes/index.test.ts @@ -0,0 +1,21 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { coerceStatusCode } from '.'; + +describe('coerceStatusCode', () => { + it('should return 503 when input is 500', () => { + expect(coerceStatusCode(500)).toBe(503); + }); + + it('should return the input status code when it is not 500', () => { + expect(coerceStatusCode(404)).toBe(404); + }); + + it('should return 503 when input is undefined or null', () => { + expect(coerceStatusCode((undefined as unknown) as number)).toBe(503); + expect(coerceStatusCode((null as unknown) as number)).toBe(503); + }); +}); diff --git a/src/plugins/query_enhancements/server/routes/index.ts b/src/plugins/query_enhancements/server/routes/index.ts index 79b93a279272..84cf19bec50c 100644 --- a/src/plugins/query_enhancements/server/routes/index.ts +++ b/src/plugins/query_enhancements/server/routes/index.ts @@ -16,6 +16,15 @@ import { API } from '../../common'; import { registerQueryAssistRoutes } from './query_assist'; import { registerDataSourceConnectionsRoutes } from './data_source_connection'; +/** + * Coerce status code to 503 for 500 errors from dependency services. Only use + * this function to handle errors throw by other services, and not from OSD. + */ +export const coerceStatusCode = (statusCode: number) => { + if (statusCode === 500) return 503; + return statusCode || 503; +}; + /** * @experimental * @@ -92,7 +101,7 @@ export function defineSearchStrategyRouteProvider(logger: Logger, router: IRoute error = err; } return res.custom({ - statusCode: error.status || err.status, + statusCode: coerceStatusCode(error.status || err.status), body: err.message, }); } From 98e9042114a27fecb810e94f10e5b7fe8813917f Mon Sep 17 00:00:00 2001 From: Tyler Ohlsen Date: Wed, 4 Dec 2024 09:48:20 -0800 Subject: [PATCH 07/16] Revert "[augmenter] do not support datasources with no version (#8915)" (#8925) This reverts commit 539675e688061e689b362801bcb05a3ef78431b2. --- changelogs/fragments/8915.yml | 2 - src/plugins/vis_augmenter/public/plugin.ts | 2 - src/plugins/vis_augmenter/public/services.ts | 6 +- .../vis_augmenter/public/utils/utils.test.ts | 129 ++---------------- .../vis_augmenter/public/utils/utils.ts | 26 +--- .../actions/view_events_option_action.tsx | 2 +- .../public/line_to_expression.ts | 2 +- .../public/embeddable/visualize_embeddable.ts | 2 +- 8 files changed, 19 insertions(+), 152 deletions(-) delete mode 100644 changelogs/fragments/8915.yml diff --git a/changelogs/fragments/8915.yml b/changelogs/fragments/8915.yml deleted file mode 100644 index 46c124d3f25f..000000000000 --- a/changelogs/fragments/8915.yml +++ /dev/null @@ -1,2 +0,0 @@ -fix: -- Do not support data sources with no version for vis augmenter ([#8915](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8915)) \ No newline at end of file diff --git a/src/plugins/vis_augmenter/public/plugin.ts b/src/plugins/vis_augmenter/public/plugin.ts index bd6e45a3967b..9760bfd75b2d 100644 --- a/src/plugins/vis_augmenter/public/plugin.ts +++ b/src/plugins/vis_augmenter/public/plugin.ts @@ -13,7 +13,6 @@ import { setUiActions, setEmbeddable, setQueryService, - setIndexPatterns, setVisualizations, setCore, } from './services'; @@ -63,7 +62,6 @@ export class VisAugmenterPlugin setUiActions(uiActions); setEmbeddable(embeddable); setQueryService(data.query); - setIndexPatterns(data.indexPatterns); setVisualizations(visualizations); setCore(core); setFlyoutState(VIEW_EVENTS_FLYOUT_STATE.CLOSED); diff --git a/src/plugins/vis_augmenter/public/services.ts b/src/plugins/vis_augmenter/public/services.ts index 44a7ea8b424b..1d7f3e2111db 100644 --- a/src/plugins/vis_augmenter/public/services.ts +++ b/src/plugins/vis_augmenter/public/services.ts @@ -8,7 +8,7 @@ import { IUiSettingsClient } from '../../../core/public'; import { SavedObjectLoaderAugmentVis } from './saved_augment_vis'; import { EmbeddableStart } from '../../embeddable/public'; import { UiActionsStart } from '../../ui_actions/public'; -import { DataPublicPluginStart, IndexPatternsContract } from '../../../plugins/data/public'; +import { DataPublicPluginStart } from '../../../plugins/data/public'; import { VisualizationsStart } from '../../visualizations/public'; import { CoreStart } from '../../../core/public'; @@ -26,10 +26,6 @@ export const [getQueryService, setQueryService] = createGetterSetter< DataPublicPluginStart['query'] >('Query'); -export const [getIndexPatterns, setIndexPatterns] = createGetterSetter( - 'IndexPatterns' -); - export const [getVisualizations, setVisualizations] = createGetterSetter( 'visualizations' ); diff --git a/src/plugins/vis_augmenter/public/utils/utils.test.ts b/src/plugins/vis_augmenter/public/utils/utils.test.ts index 05f90522fe4a..f831deef3955 100644 --- a/src/plugins/vis_augmenter/public/utils/utils.test.ts +++ b/src/plugins/vis_augmenter/public/utils/utils.test.ts @@ -21,12 +21,11 @@ import { PluginResource, VisLayerErrorTypes, SavedObjectLoaderAugmentVis, - isEligibleForDataSource, } from '../'; import { PLUGIN_AUGMENTATION_ENABLE_SETTING } from '../../common/constants'; import { AggConfigs } from '../../../data/common'; import { uiSettingsServiceMock } from '../../../../core/public/mocks'; -import { setIndexPatterns, setUISettings } from '../services'; +import { setUISettings } from '../services'; import { STUB_INDEX_PATTERN_WITH_FIELDS, TYPES_REGISTRY, @@ -36,7 +35,6 @@ import { createPointInTimeEventsVisLayer, createVisLayer, } from '../mocks'; -import { dataPluginMock } from 'src/plugins/data/public/mocks'; describe('utils', () => { const uiSettingsMock = uiSettingsServiceMock.createStartContract(); @@ -62,7 +60,7 @@ describe('utils', () => { aggs: VALID_AGGS, }, } as unknown) as Vis; - expect(await isEligibleForVisLayers(vis)).toEqual(false); + expect(isEligibleForVisLayers(vis)).toEqual(false); }); it('vis is ineligible with no date_histogram', async () => { const invalidConfigStates = [ @@ -89,7 +87,7 @@ describe('utils', () => { invalidAggs, }, } as unknown) as Vis; - expect(await isEligibleForVisLayers(vis)).toEqual(false); + expect(isEligibleForVisLayers(vis)).toEqual(false); }); it('vis is ineligible with invalid aggs counts', async () => { const invalidConfigStates = [ @@ -113,7 +111,7 @@ describe('utils', () => { invalidAggs, }, } as unknown) as Vis; - expect(await isEligibleForVisLayers(vis)).toEqual(false); + expect(isEligibleForVisLayers(vis)).toEqual(false); }); it('vis is ineligible with no metric aggs', async () => { const invalidConfigStates = [ @@ -135,7 +133,7 @@ describe('utils', () => { invalidAggs, }, } as unknown) as Vis; - expect(await isEligibleForVisLayers(vis)).toEqual(false); + expect(isEligibleForVisLayers(vis)).toEqual(false); }); it('vis is ineligible with series param is not line type', async () => { const vis = ({ @@ -156,7 +154,7 @@ describe('utils', () => { aggs: VALID_AGGS, }, } as unknown) as Vis; - expect(await isEligibleForVisLayers(vis)).toEqual(false); + expect(isEligibleForVisLayers(vis)).toEqual(false); }); it('vis is ineligible with series param not all being line type', async () => { const vis = ({ @@ -180,7 +178,7 @@ describe('utils', () => { aggs: VALID_AGGS, }, } as unknown) as Vis; - expect(await isEligibleForVisLayers(vis)).toEqual(false); + expect(isEligibleForVisLayers(vis)).toEqual(false); }); it('vis is ineligible with invalid x-axis due to no segment aggregation', async () => { const badConfigStates = [ @@ -218,7 +216,7 @@ describe('utils', () => { badAggs, }, } as unknown) as Vis; - expect(await isEligibleForVisLayers(invalidVis)).toEqual(false); + expect(isEligibleForVisLayers(invalidVis)).toEqual(false); }); it('vis is ineligible with xaxis not on bottom', async () => { const invalidVis = ({ @@ -239,7 +237,7 @@ describe('utils', () => { aggs: VALID_AGGS, }, } as unknown) as Vis; - expect(await isEligibleForVisLayers(invalidVis)).toEqual(false); + expect(isEligibleForVisLayers(invalidVis)).toEqual(false); }); it('vis is ineligible with no seriesParams', async () => { const invalidVis = ({ @@ -255,16 +253,16 @@ describe('utils', () => { aggs: VALID_AGGS, }, } as unknown) as Vis; - expect(await isEligibleForVisLayers(invalidVis)).toEqual(false); + expect(isEligibleForVisLayers(invalidVis)).toEqual(false); }); it('vis is ineligible with valid type and disabled setting', async () => { uiSettingsMock.get.mockImplementation((key: string) => { return key !== PLUGIN_AUGMENTATION_ENABLE_SETTING; }); - expect(await isEligibleForVisLayers(VALID_VIS)).toEqual(false); + expect(isEligibleForVisLayers(VALID_VIS)).toEqual(false); }); it('vis is eligible with valid type', async () => { - expect(await isEligibleForVisLayers(VALID_VIS)).toEqual(true); + expect(isEligibleForVisLayers(VALID_VIS)).toEqual(true); }); }); @@ -662,107 +660,4 @@ describe('utils', () => { expect(mockDeleteFn).toHaveBeenCalledTimes(1); }); }); - - describe('isEligibleForDataSource', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - it('returns true if the Vis indexPattern does not have a dataSourceRef', async () => { - const indexPatternsMock = dataPluginMock.createStartContract().indexPatterns; - indexPatternsMock.getDataSource = jest.fn().mockReturnValue(undefined); - setIndexPatterns(indexPatternsMock); - const vis = { - data: { - indexPattern: { - id: '123', - }, - }, - } as Vis; - expect(await isEligibleForDataSource(vis)).toEqual(true); - }); - it('returns true if the Vis indexPattern has a dataSourceRef with a compatible version', async () => { - const indexPatternsMock = dataPluginMock.createStartContract().indexPatterns; - indexPatternsMock.getDataSource = jest.fn().mockReturnValue({ - id: '456', - attributes: { - dataSourceVersion: '1.2.3', - }, - }); - setIndexPatterns(indexPatternsMock); - const vis = { - data: { - indexPattern: { - id: '123', - dataSourceRef: { - id: '456', - }, - }, - }, - } as Vis; - expect(await isEligibleForDataSource(vis)).toEqual(true); - }); - it('returns false if the Vis indexPattern has a dataSourceRef with an incompatible version', async () => { - const indexPatternsMock = dataPluginMock.createStartContract().indexPatterns; - indexPatternsMock.getDataSource = jest.fn().mockReturnValue({ - id: '456', - attributes: { - dataSourceVersion: '.0', - }, - }); - setIndexPatterns(indexPatternsMock); - const vis = { - data: { - indexPattern: { - id: '123', - dataSourceRef: { - id: '456', - }, - }, - }, - } as Vis; - expect(await isEligibleForDataSource(vis)).toEqual(false); - }); - it('returns false if the Vis indexPattern has a dataSourceRef with an undefined version', async () => { - const indexPatternsMock = dataPluginMock.createStartContract().indexPatterns; - indexPatternsMock.getDataSource = jest.fn().mockReturnValue({ - id: '456', - attributes: { - dataSourceVersion: undefined, - }, - }); - setIndexPatterns(indexPatternsMock); - const vis = { - data: { - indexPattern: { - id: '123', - dataSourceRef: { - id: '456', - }, - }, - }, - } as Vis; - expect(await isEligibleForDataSource(vis)).toEqual(false); - }); - it('returns false if the Vis indexPattern has a dataSourceRef with an empty string version', async () => { - const indexPatternsMock = dataPluginMock.createStartContract().indexPatterns; - indexPatternsMock.getDataSource = jest.fn().mockReturnValue({ - id: '456', - attributes: { - dataSourceVersion: '', - }, - }); - setIndexPatterns(indexPatternsMock); - const vis = { - data: { - indexPattern: { - id: '123', - dataSourceRef: { - id: '456', - }, - }, - }, - } as Vis; - expect(await isEligibleForDataSource(vis)).toEqual(false); - }); - }); }); diff --git a/src/plugins/vis_augmenter/public/utils/utils.ts b/src/plugins/vis_augmenter/public/utils/utils.ts index 0ae3c9ec93aa..ce44964e6173 100644 --- a/src/plugins/vis_augmenter/public/utils/utils.ts +++ b/src/plugins/vis_augmenter/public/utils/utils.ts @@ -4,7 +4,6 @@ */ import { get, isEmpty } from 'lodash'; -import semver from 'semver'; import { Vis } from '../../../../plugins/visualizations/public'; import { formatExpression, @@ -21,13 +20,10 @@ import { VisLayerErrorTypes, } from '../'; import { PLUGIN_AUGMENTATION_ENABLE_SETTING } from '../../common/constants'; -import { getUISettings, getIndexPatterns } from '../services'; +import { getUISettings } from '../services'; import { IUiSettingsClient } from '../../../../core/public'; -export const isEligibleForVisLayers = async ( - vis: Vis, - uiSettingsClient?: IUiSettingsClient -): Promise => { +export const isEligibleForVisLayers = (vis: Vis, uiSettingsClient?: IUiSettingsClient): boolean => { // Only support a date histogram const dateHistograms = vis.data?.aggs?.byTypeName?.('date_histogram'); if (!Array.isArray(dateHistograms) || dateHistograms.length !== 1) return false; @@ -57,9 +53,6 @@ export const isEligibleForVisLayers = async ( ) return false; - // Check if the vis datasource is eligible for the augmentation - if (!(await isEligibleForDataSource(vis))) return false; - // Checks if the augmentation setting is enabled const config = uiSettingsClient ?? getUISettings(); return config.get(PLUGIN_AUGMENTATION_ENABLE_SETTING); @@ -170,6 +163,7 @@ export const getAnyErrors = (visLayers: VisLayer[], visTitle: string): Error | u * @param visLayers the produced VisLayers containing details if the resource has been deleted * @param visualizationsLoader the visualizations saved object loader to handle deletion */ + export const cleanupStaleObjects = ( augmentVisSavedObjs: ISavedAugmentVis[], visLayers: VisLayer[], @@ -193,17 +187,3 @@ export const cleanupStaleObjects = ( loader?.delete(objIdsToDelete); } }; - -/** - * Returns true if the Vis is eligible to be used with the DataSource feature. - * @param vis - The Vis to check - * @returns true if the Vis is eligible for the DataSource feature, false otherwise - */ -export const isEligibleForDataSource = async (vis: Vis) => { - const dataSourceRef = vis.data.indexPattern?.dataSourceRef; - if (!dataSourceRef) return true; - const dataSource = await getIndexPatterns().getDataSource(dataSourceRef.id); - if (!dataSource || !dataSource.attributes) return false; - const version = semver.coerce(dataSource.attributes.dataSourceVersion); - return version ? semver.satisfies(version, '>=1.0.0') : false; -}; diff --git a/src/plugins/vis_augmenter/public/view_events_flyout/actions/view_events_option_action.tsx b/src/plugins/vis_augmenter/public/view_events_flyout/actions/view_events_option_action.tsx index f83f0e0b77d6..ac7f795c586e 100644 --- a/src/plugins/vis_augmenter/public/view_events_flyout/actions/view_events_option_action.tsx +++ b/src/plugins/vis_augmenter/public/view_events_flyout/actions/view_events_option_action.tsx @@ -46,7 +46,7 @@ export class ViewEventsOptionAction implements Action { const vis = (embeddable as VisualizeEmbeddable).vis; return ( vis !== undefined && - (await isEligibleForVisLayers(vis)) && + isEligibleForVisLayers(vis) && !isEmpty((embeddable as VisualizeEmbeddable).visLayers) ); } diff --git a/src/plugins/vis_type_vislib/public/line_to_expression.ts b/src/plugins/vis_type_vislib/public/line_to_expression.ts index e8d207017c00..8650c6013801 100644 --- a/src/plugins/vis_type_vislib/public/line_to_expression.ts +++ b/src/plugins/vis_type_vislib/public/line_to_expression.ts @@ -32,7 +32,7 @@ export const toExpressionAst = async (vis: Vis, params: any) => { if ( params.visLayers == null || Object.keys(params.visLayers).length === 0 || - !(await isEligibleForVisLayers(vis)) + !isEligibleForVisLayers(vis) ) { // Render using vislib instead of vega-lite const visConfig = { ...vis.params, dimensions }; diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts index 7bf996c148ea..605c88067211 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts @@ -541,7 +541,7 @@ export class VisualizeEmbeddable this.visAugmenterConfig?.visLayerResourceIds ); - if (!isEmpty(augmentVisSavedObjs) && !aborted && (await isEligibleForVisLayers(this.vis))) { + if (!isEmpty(augmentVisSavedObjs) && !aborted && isEligibleForVisLayers(this.vis)) { const visLayersPipeline = buildPipelineFromAugmentVisSavedObjs(augmentVisSavedObjs); // The initial input for the pipeline will just be an empty arr of VisLayers. As plugin // expression functions are ran, they will incrementally append their generated VisLayers to it. From 36bf5e83fe2b44b2221488d3c0149a58d5d4884a Mon Sep 17 00:00:00 2001 From: yuboluo Date: Thu, 5 Dec 2024 17:06:58 +0800 Subject: [PATCH 08/16] [Workspace] Clear the attribute of error objects (#9003) * clear the attribute of error objects Signed-off-by: yubonluo * Changeset file for PR #9003 created/updated * Changeset file for PR #9003 deleted --------- Signed-off-by: yubonluo Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> --- .../workspace_id_consumer_wrapper.test.ts | 4 +- .../workspace_id_consumer_wrapper.test.ts | 41 ++++++++++--------- .../workspace_id_consumer_wrapper.ts | 5 ++- 3 files changed, 27 insertions(+), 23 deletions(-) diff --git a/src/plugins/workspace/server/saved_objects/integration_tests/workspace_id_consumer_wrapper.test.ts b/src/plugins/workspace/server/saved_objects/integration_tests/workspace_id_consumer_wrapper.test.ts index f597dd369272..eca47fbb5b72 100644 --- a/src/plugins/workspace/server/saved_objects/integration_tests/workspace_id_consumer_wrapper.test.ts +++ b/src/plugins/workspace/server/saved_objects/integration_tests/workspace_id_consumer_wrapper.test.ts @@ -485,9 +485,7 @@ describe('workspace_id_consumer integration test', () => { ]); expect(bulkGetResultWithWorkspace.body.saved_objects[0]?.error).toBeUndefined(); expect(bulkGetResultWithWorkspace.body.saved_objects[1].id).toEqual('bar'); - expect(bulkGetResultWithWorkspace.body.saved_objects[1].workspaces).toEqual([ - createdBarWorkspace.id, - ]); + expect(bulkGetResultWithWorkspace.body.saved_objects[1].workspaces).toBeUndefined(); expect(bulkGetResultWithWorkspace.body.saved_objects[1]?.error).toMatchInlineSnapshot(` Object { "error": "Forbidden", diff --git a/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.test.ts b/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.test.ts index fcef67870523..5d9a4094336e 100644 --- a/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.test.ts +++ b/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.test.ts @@ -432,8 +432,8 @@ describe('WorkspaceIdConsumerWrapper', () => { { type: 'dashboard', id: 'dashboard_id', - attributes: {}, - references: [], + attributes: { description: 'description' }, + references: ['reference_id'], workspaces: ['foo'], }, { @@ -450,8 +450,8 @@ describe('WorkspaceIdConsumerWrapper', () => { { type: 'visualization', id: 'visualization_id', - attributes: {}, - references: [], + attributes: { description: 'description' }, + references: ['reference_id'], workspaces: ['bar'], }, { @@ -493,9 +493,13 @@ describe('WorkspaceIdConsumerWrapper', () => { Object { "saved_objects": Array [ Object { - "attributes": Object {}, + "attributes": Object { + "description": "description", + }, "id": "dashboard_id", - "references": Array [], + "references": Array [ + "reference_id", + ], "type": "dashboard", "workspaces": Array [ "foo", @@ -522,9 +526,6 @@ describe('WorkspaceIdConsumerWrapper', () => { "id": "visualization_id", "references": Array [], "type": "visualization", - "workspaces": Array [ - "bar", - ], }, Object { "attributes": Object {}, @@ -571,9 +572,13 @@ describe('WorkspaceIdConsumerWrapper', () => { Object { "saved_objects": Array [ Object { - "attributes": Object {}, + "attributes": Object { + "description": "description", + }, "id": "dashboard_id", - "references": Array [], + "references": Array [ + "reference_id", + ], "type": "dashboard", "workspaces": Array [ "foo", @@ -600,9 +605,6 @@ describe('WorkspaceIdConsumerWrapper', () => { "id": "visualization_id", "references": Array [], "type": "visualization", - "workspaces": Array [ - "bar", - ], }, Object { "attributes": Object {}, @@ -688,9 +690,13 @@ describe('WorkspaceIdConsumerWrapper', () => { Object { "saved_objects": Array [ Object { - "attributes": Object {}, + "attributes": Object { + "description": "description", + }, "id": "dashboard_id", - "references": Array [], + "references": Array [ + "reference_id", + ], "type": "dashboard", "workspaces": Array [ "foo", @@ -717,9 +723,6 @@ describe('WorkspaceIdConsumerWrapper', () => { "id": "visualization_id", "references": Array [], "type": "visualization", - "workspaces": Array [ - "bar", - ], }, Object { "attributes": Object {}, diff --git a/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.ts b/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.ts index f6efb690c5cd..b9edaecd2c9d 100644 --- a/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.ts +++ b/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.ts @@ -195,7 +195,10 @@ export class WorkspaceIdConsumerWrapper { return this.validateObjectInAWorkspace(object, workspaces[0], wrapperOptions.request) ? object : { - ...object, + id: object.id, + type: object.type, + attributes: {} as T, + references: [], error: { ...generateSavedObjectsForbiddenError().output.payload, }, From 41ac8ab6c9c5162b3ea419f22fa4173e6d6ad9e5 Mon Sep 17 00:00:00 2001 From: Joshua Li Date: Thu, 5 Dec 2024 09:04:14 -0800 Subject: [PATCH 09/16] bump `url` to 0.11.4 (#8611) * bump url to 0.11.4 Signed-off-by: Joshua Li * Changeset file for PR #8611 created/updated --------- Signed-off-by: Joshua Li Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> --- changelogs/fragments/8611.yml | 2 + package.json | 1 + yarn.lock | 108 +++++++++++++++++++++++++++------- 3 files changed, 90 insertions(+), 21 deletions(-) create mode 100644 changelogs/fragments/8611.yml diff --git a/changelogs/fragments/8611.yml b/changelogs/fragments/8611.yml new file mode 100644 index 000000000000..2f7ec1677a58 --- /dev/null +++ b/changelogs/fragments/8611.yml @@ -0,0 +1,2 @@ +fix: +- Bump url to 0.11.4 ([#8611](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8611)) \ No newline at end of file diff --git a/package.json b/package.json index 0a103b9fdab1..7c3bb252ecef 100644 --- a/package.json +++ b/package.json @@ -124,6 +124,7 @@ "**/trim": "^0.0.3", "**/typescript": "4.6.4", "**/unset-value": "^2.0.1", + "**/url": "^0.11.4", "**/watchpack-chokidar2/chokidar": "^3.5.3", "**/xml2js": "^0.5.0", "**/yaml": "^2.2.2" diff --git a/yarn.lock b/yarn.lock index 537af6f3662e..69ddeeeabd5b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5525,6 +5525,17 @@ call-bind@^1.0.0, call-bind@^1.0.2, call-bind@^1.0.4, call-bind@^1.0.5: get-intrinsic "^1.2.1" set-function-length "^1.1.1" +call-bind@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" + integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + set-function-length "^1.2.1" + callsites@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" @@ -6997,6 +7008,15 @@ define-data-property@^1.0.1, define-data-property@^1.1.1: gopd "^1.0.1" has-property-descriptors "^1.0.0" +define-data-property@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" + integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + gopd "^1.0.1" + define-properties@^1.1.3, define-properties@^1.1.4, define-properties@^1.2.0: version "1.2.1" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" @@ -7726,6 +7746,18 @@ es-array-method-boxes-properly@^1.0.0: resolved "https://registry.yarnpkg.com/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz#873f3e84418de4ee19c5be752990b2e44718d09e" integrity sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA== +es-define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" + integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== + dependencies: + get-intrinsic "^1.2.4" + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + es-get-iterator@^1.1.1: version "1.1.2" resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.2.tgz#9234c54aba713486d7ebde0220864af5e2b283f7" @@ -9010,6 +9042,17 @@ get-intrinsic@^1.0.1, get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@ has-symbols "^1.0.3" hasown "^2.0.0" +get-intrinsic@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" + integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + has-proto "^1.0.1" + has-symbols "^1.0.3" + hasown "^2.0.0" + get-nonce@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/get-nonce/-/get-nonce-1.0.1.tgz#fdf3f0278073820d2ce9426c18f07481b1e0cdf3" @@ -9562,6 +9605,13 @@ has-property-descriptors@^1.0.0: dependencies: get-intrinsic "^1.1.1" +has-property-descriptors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" + integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== + dependencies: + es-define-property "^1.0.0" + has-proto@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.1.tgz#1885c1305538958aff469fef37937c22795408e0" @@ -14158,20 +14208,15 @@ pumpify@^1.3.3, pumpify@^1.3.5: inherits "^2.0.3" pump "^2.0.0" -punycode@1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" - integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0= - punycode@2.x.x, punycode@^2.1.0, punycode@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== -punycode@^1.2.4: +punycode@^1.2.4, punycode@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" - integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= + integrity sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ== qs@^6.11.0: version "6.11.0" @@ -14180,6 +14225,13 @@ qs@^6.11.0: dependencies: side-channel "^1.0.4" +qs@^6.12.3: + version "6.13.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906" + integrity sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg== + dependencies: + side-channel "^1.0.6" + qs@~6.10.3: version "6.10.5" resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.5.tgz#974715920a80ff6a262264acd2c7e6c2a53282b4" @@ -15561,6 +15613,18 @@ set-function-length@^1.1.1: gopd "^1.0.1" has-property-descriptors "^1.0.0" +set-function-length@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" + integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" + set-function-name@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/set-function-name/-/set-function-name-2.0.1.tgz#12ce38b7954310b9f61faa12701620a0c882793a" @@ -15648,6 +15712,16 @@ side-channel@^1.0.3, side-channel@^1.0.4: get-intrinsic "^1.0.2" object-inspect "^1.9.0" +side-channel@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" + integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + object-inspect "^1.13.1" + signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7: version "3.0.7" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" @@ -17540,21 +17614,13 @@ url-parse@^1.5.10, url-parse@^1.5.3: querystringify "^2.1.1" requires-port "^1.0.0" -url@0.10.3: - version "0.10.3" - resolved "https://registry.yarnpkg.com/url/-/url-0.10.3.tgz#021e4d9c7705f21bbf37d03ceb58767402774c64" - integrity sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ== - dependencies: - punycode "1.3.2" - querystring "0.2.0" - -url@^0.11.0: - version "0.11.0" - resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" - integrity sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE= +url@0.10.3, url@^0.11.0, url@^0.11.4: + version "0.11.4" + resolved "https://registry.yarnpkg.com/url/-/url-0.11.4.tgz#adca77b3562d56b72746e76b330b7f27b6721f3c" + integrity sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg== dependencies: - punycode "1.3.2" - querystring "0.2.0" + punycode "^1.4.1" + qs "^6.12.3" use-callback-ref@^1.2.3, use-callback-ref@^1.2.5: version "1.2.5" From 3f659387cc6fa1a9294348869d6132264d788d68 Mon Sep 17 00:00:00 2001 From: Joshua Li Date: Thu, 5 Dec 2024 10:32:08 -0800 Subject: [PATCH 10/16] [Discover] use roundUp when converting timestamp for PPL (#8935) Signed-off-by: Joshua Li Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> --- changelogs/fragments/8935.yml | 2 ++ packages/opensearch-datemath/index.d.ts | 2 ++ .../data/common/data_frames/utils.test.ts | 27 +++++++++++++++++++ src/plugins/data/common/data_frames/utils.ts | 6 ++--- 4 files changed, 34 insertions(+), 3 deletions(-) create mode 100644 changelogs/fragments/8935.yml create mode 100644 src/plugins/data/common/data_frames/utils.test.ts diff --git a/changelogs/fragments/8935.yml b/changelogs/fragments/8935.yml new file mode 100644 index 000000000000..84922a039ffc --- /dev/null +++ b/changelogs/fragments/8935.yml @@ -0,0 +1,2 @@ +fix: +- Use roundUp when converting timestamp for PPL ([#8935](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8935)) \ No newline at end of file diff --git a/packages/opensearch-datemath/index.d.ts b/packages/opensearch-datemath/index.d.ts index 0706d7d0dccf..fde4b10013a7 100644 --- a/packages/opensearch-datemath/index.d.ts +++ b/packages/opensearch-datemath/index.d.ts @@ -47,6 +47,8 @@ declare const datemath: { /** * Parses a string into a moment object. The string can be something like "now - 15m". + * @param options.roundUp - If true, rounds the parsed date to the end of the + * unit. Only works for string with "/" like "now/d". * @param options.forceNow If this optional parameter is supplied, "now" will be treated as this * date, rather than the real "now". */ diff --git a/src/plugins/data/common/data_frames/utils.test.ts b/src/plugins/data/common/data_frames/utils.test.ts new file mode 100644 index 000000000000..5ba877c963c2 --- /dev/null +++ b/src/plugins/data/common/data_frames/utils.test.ts @@ -0,0 +1,27 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import datemath from '@opensearch/datemath'; +import { formatTimePickerDate } from '.'; + +describe('formatTimePickerDate', () => { + const mockDateFormat = 'YYYY-MM-DD HH:mm:ss'; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should handle date range with rounding', () => { + jest.spyOn(datemath, 'parse'); + + const result = formatTimePickerDate({ from: 'now/d', to: 'now/d' }, mockDateFormat); + + expect(result.fromDate).not.toEqual(result.toDate); + + expect(datemath.parse).toHaveBeenCalledTimes(2); + expect(datemath.parse).toHaveBeenCalledWith('now/d', { roundUp: undefined }); + expect(datemath.parse).toHaveBeenCalledWith('now/d', { roundUp: true }); + }); +}); diff --git a/src/plugins/data/common/data_frames/utils.ts b/src/plugins/data/common/data_frames/utils.ts index fdee757bfabb..7e280478630a 100644 --- a/src/plugins/data/common/data_frames/utils.ts +++ b/src/plugins/data/common/data_frames/utils.ts @@ -156,13 +156,13 @@ export const getTimeField = ( * the `dateFormat` parameter */ export const formatTimePickerDate = (dateRange: TimeRange, dateFormat: string) => { - const dateMathParse = (date: string) => { - const parsedDate = datemath.parse(date); + const dateMathParse = (date: string, roundUp?: boolean) => { + const parsedDate = datemath.parse(date, { roundUp }); return parsedDate ? parsedDate.utc().format(dateFormat) : ''; }; const fromDate = dateMathParse(dateRange.from); - const toDate = dateMathParse(dateRange.to); + const toDate = dateMathParse(dateRange.to, true); return { fromDate, toDate }; }; From af429b6f5db5a1f973924ff3cff6743322ea4fe3 Mon Sep 17 00:00:00 2001 From: Lin Wang Date: Fri, 6 Dec 2024 09:24:20 +0800 Subject: [PATCH 11/16] [Workspace]Fix error toasts in sample data page (#8842) * Set default index pattern when workspace disabled Signed-off-by: Lin Wang * Move saved objects first to avoid partial deleted Signed-off-by: Lin Wang * Skip ui setting update for non workspace admin Signed-off-by: Lin Wang * Add UT for sample_data_client Signed-off-by: Lin Wang * Changeset file for PR #8842 created/updated --------- Signed-off-by: Lin Wang Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> --- changelogs/fragments/8842.yml | 2 + .../opensearch_dashboards_services.ts | 2 + .../public/application/sample_data_client.js | 18 +- .../application/sample_data_client.test.js | 167 ++++++++++++++++++ src/plugins/home/public/plugin.ts | 1 + .../services/sample_data/routes/uninstall.ts | 48 ++--- 6 files changed, 215 insertions(+), 23 deletions(-) create mode 100644 changelogs/fragments/8842.yml create mode 100644 src/plugins/home/public/application/sample_data_client.test.js diff --git a/changelogs/fragments/8842.yml b/changelogs/fragments/8842.yml new file mode 100644 index 000000000000..b9973f347f9e --- /dev/null +++ b/changelogs/fragments/8842.yml @@ -0,0 +1,2 @@ +fix: +- [Workspace]Fix error toasts in sample data page ([#8842](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8842)) \ No newline at end of file diff --git a/src/plugins/home/public/application/opensearch_dashboards_services.ts b/src/plugins/home/public/application/opensearch_dashboards_services.ts index 1107e46ecf2e..eb4b085d86ae 100644 --- a/src/plugins/home/public/application/opensearch_dashboards_services.ts +++ b/src/plugins/home/public/application/opensearch_dashboards_services.ts @@ -37,6 +37,7 @@ import { SavedObjectsClientContract, IUiSettingsClient, ApplicationStart, + WorkspacesSetup, } from 'opensearch-dashboards/public'; import { UiStatsMetricType } from '@osd/analytics'; import { TelemetryPluginStart } from '../../../telemetry/public'; @@ -77,6 +78,7 @@ export interface HomeOpenSearchDashboardsServices { }; dataSource?: DataSourcePluginStart; sectionTypes: SectionTypeService; + workspaces?: WorkspacesSetup; } let services: HomeOpenSearchDashboardsServices | null = null; diff --git a/src/plugins/home/public/application/sample_data_client.js b/src/plugins/home/public/application/sample_data_client.js index 045736c428f6..b2adaf44cf81 100644 --- a/src/plugins/home/public/application/sample_data_client.js +++ b/src/plugins/home/public/application/sample_data_client.js @@ -41,11 +41,26 @@ export async function listSampleDataSets(dataSourceId) { return await getServices().http.get(sampleDataUrl, { query }); } +const canUpdateUISetting = () => { + const { + application: { capabilities }, + workspaces, + } = getServices(); + if ( + capabilities.workspaces && + capabilities.workspaces.enabled && + capabilities.workspaces.permissionEnabled + ) { + return !!workspaces?.currentWorkspace$.getValue()?.owner; + } + return true; +}; + export async function installSampleDataSet(id, sampleDataDefaultIndex, dataSourceId) { const query = buildQuery(dataSourceId); await getServices().http.post(`${sampleDataUrl}/${id}`, { query }); - if (getServices().uiSettings.isDefault('defaultIndex')) { + if (canUpdateUISetting() && getServices().uiSettings.isDefault('defaultIndex')) { getServices().uiSettings.set('defaultIndex', sampleDataDefaultIndex); } @@ -59,6 +74,7 @@ export async function uninstallSampleDataSet(id, sampleDataDefaultIndex, dataSou const uiSettings = getServices().uiSettings; if ( + canUpdateUISetting() && !uiSettings.isDefault('defaultIndex') && uiSettings.get('defaultIndex') === sampleDataDefaultIndex ) { diff --git a/src/plugins/home/public/application/sample_data_client.test.js b/src/plugins/home/public/application/sample_data_client.test.js new file mode 100644 index 000000000000..35f86efef729 --- /dev/null +++ b/src/plugins/home/public/application/sample_data_client.test.js @@ -0,0 +1,167 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { BehaviorSubject } from 'rxjs'; +import { setServices } from '../application/opensearch_dashboards_services'; +import { installSampleDataSet, uninstallSampleDataSet } from './sample_data_client'; + +const mockHttp = { + post: jest.fn(), + delete: jest.fn(), +}; + +const mockUiSettings = { + isDefault: jest.fn(), + set: jest.fn(), + get: jest.fn(), +}; + +const mockApplication = { + capabilities: { + workspaces: { + enabled: false, + permissionEnabled: false, + }, + }, +}; + +const mockIndexPatternService = { + clearCache: jest.fn(), +}; + +const mockWorkspace = { + currentWorkspace$: new BehaviorSubject(), +}; + +const mockServices = { + workspaces: mockWorkspace, + http: mockHttp, + uiSettings: mockUiSettings, + application: mockApplication, + indexPatternService: mockIndexPatternService, +}; + +setServices(mockServices); + +describe('installSampleDataSet', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockUiSettings.isDefault.mockReturnValue(true); + setServices(mockServices); + }); + + it('should install the sample data set and set the default index', async () => { + const id = 'sample-data-id'; + const sampleDataDefaultIndex = 'sample-data-index'; + const dataSourceId = 'data-source-id'; + + await installSampleDataSet(id, sampleDataDefaultIndex, dataSourceId); + + expect(mockHttp.post).toHaveBeenCalledWith(`/api/sample_data/${id}`, { + query: expect.anything(), + }); + expect(mockUiSettings.set).toHaveBeenCalledWith('defaultIndex', sampleDataDefaultIndex); + expect(mockIndexPatternService.clearCache).toHaveBeenCalled(); + }); + + it('should install the sample data set and not set the default index when workspace is enabled', async () => { + const id = 'sample-data-id'; + const sampleDataDefaultIndex = 'sample-data-index'; + const dataSourceId = 'data-source-id'; + + setServices({ + ...mockServices, + workspaces: { + currentWorkspace$: new BehaviorSubject(), + }, + application: { + capabilities: { + workspaces: { + enabled: true, + permissionEnabled: true, + }, + }, + }, + }); + + await installSampleDataSet(id, sampleDataDefaultIndex, dataSourceId); + + expect(mockHttp.post).toHaveBeenCalledWith(`/api/sample_data/${id}`, { + query: expect.anything(), + }); + expect(mockUiSettings.set).not.toHaveBeenCalled(); + expect(mockIndexPatternService.clearCache).toHaveBeenCalled(); + }); +}); + +describe('uninstallSampleDataSet', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockUiSettings.isDefault.mockReturnValue(false); + setServices(mockServices); + }); + + it('should uninstall the sample data set and clear the default index', async () => { + const id = 'sample-data-id'; + const sampleDataDefaultIndex = 'sample-data-index'; + const dataSourceId = 'data-source-id'; + + mockUiSettings.get.mockReturnValue(sampleDataDefaultIndex); + + await uninstallSampleDataSet(id, sampleDataDefaultIndex, dataSourceId); + + expect(mockHttp.delete).toHaveBeenCalledWith(`/api/sample_data/${id}`, { + query: expect.anything(), + }); + expect(mockUiSettings.set).toHaveBeenCalledWith('defaultIndex', null); + expect(mockIndexPatternService.clearCache).toHaveBeenCalled(); + }); + + it('should uninstall the sample data set and not clear the default index when workspace is enabled', async () => { + const id = 'sample-data-id'; + const sampleDataDefaultIndex = 'sample-data-index'; + const dataSourceId = 'data-source-id'; + + setServices({ + ...mockServices, + workspaces: { + currentWorkspace$: new BehaviorSubject(), + }, + application: { + capabilities: { + workspaces: { + enabled: true, + permissionEnabled: true, + }, + }, + }, + }); + + await uninstallSampleDataSet(id, sampleDataDefaultIndex, dataSourceId); + + expect(mockHttp.delete).toHaveBeenCalledWith(`/api/sample_data/${id}`, { + query: expect.anything(), + }); + expect(mockUiSettings.set).not.toHaveBeenCalled(); + expect(mockIndexPatternService.clearCache).toHaveBeenCalled(); + }); + + it('should uninstall the sample data set and not clear the default index when it is not the sample data index', async () => { + const id = 'sample-data-id'; + const sampleDataDefaultIndex = 'sample-data-index'; + const dataSourceId = 'data-source-id'; + + mockUiSettings.isDefault.mockReturnValue(false); + mockUiSettings.get.mockReturnValue('other-index'); + + await uninstallSampleDataSet(id, sampleDataDefaultIndex, dataSourceId); + + expect(mockHttp.delete).toHaveBeenCalledWith(`/api/sample_data/${id}`, { + query: expect.anything(), + }); + expect(mockUiSettings.set).not.toHaveBeenCalled(); + expect(mockIndexPatternService.clearCache).toHaveBeenCalled(); + }); +}); diff --git a/src/plugins/home/public/plugin.ts b/src/plugins/home/public/plugin.ts index 435c7d4d3b9f..6d9771c724ef 100644 --- a/src/plugins/home/public/plugin.ts +++ b/src/plugins/home/public/plugin.ts @@ -156,6 +156,7 @@ export class HomePublicPlugin injectedMetadata: coreStart.injectedMetadata, dataSource, sectionTypes: this.sectionTypeService, + workspaces: core.workspaces, ...homeOpenSearchDashboardsServices, }); }; diff --git a/src/plugins/home/server/services/sample_data/routes/uninstall.ts b/src/plugins/home/server/services/sample_data/routes/uninstall.ts index 3e4636c32486..da8dea3c2fe3 100644 --- a/src/plugins/home/server/services/sample_data/routes/uninstall.ts +++ b/src/plugins/home/server/services/sample_data/routes/uninstall.ts @@ -62,27 +62,10 @@ export function createUninstallRoute( return response.notFound(); } - const caller = dataSourceId - ? context.dataSource.opensearch.legacy.getClient(dataSourceId).callAPI - : context.core.opensearch.legacy.client.callAsCurrentUser; - - for (let i = 0; i < sampleDataset.dataIndices.length; i++) { - const dataIndexConfig = sampleDataset.dataIndices[i]; - const index = - dataIndexConfig.indexName ?? createIndexName(sampleDataset.id, dataIndexConfig.id); - - try { - await caller('indices.delete', { index }); - } catch (err) { - return response.customError({ - statusCode: err.status, - body: { - message: `Unable to delete sample data index "${index}", error: ${err.message}`, - }, - }); - } - } - + /** + * Delete saved objects before removing the data index to avoid partial deletion + * of sample data when a read-only workspace user attempts to remove sample data. + */ const savedObjectsList = getFinalSavedObjects({ dataset: sampleDataset, workspaceId, @@ -99,7 +82,7 @@ export function createUninstallRoute( // ignore 404s since users could have deleted some of the saved objects via the UI if (_.get(err, 'output.statusCode') !== 404) { return response.customError({ - statusCode: err.status, + statusCode: err.status || _.get(err, 'output.statusCode'), body: { message: `Unable to delete sample dataset saved objects, error: ${err.message}`, }, @@ -107,6 +90,27 @@ export function createUninstallRoute( } } + const caller = dataSourceId + ? context.dataSource.opensearch.legacy.getClient(dataSourceId).callAPI + : context.core.opensearch.legacy.client.callAsCurrentUser; + + for (let i = 0; i < sampleDataset.dataIndices.length; i++) { + const dataIndexConfig = sampleDataset.dataIndices[i]; + const index = + dataIndexConfig.indexName ?? createIndexName(sampleDataset.id, dataIndexConfig.id); + + try { + await caller('indices.delete', { index }); + } catch (err) { + return response.customError({ + statusCode: err.status, + body: { + message: `Unable to delete sample data index "${index}", error: ${err.message}`, + }, + }); + } + } + // track the usage operation in a non-blocking way usageTracker.addUninstall(request.params.id); From afb260501bdf3f7f9a151af6db8cfef4c0dd0219 Mon Sep 17 00:00:00 2001 From: Justin Kim Date: Fri, 6 Dec 2024 12:37:55 -0800 Subject: [PATCH 12/16] fix: update the osd-plugin-generator template to reference the correct button (#9014) * fix: update the osd-plugin-generator template to reference the correct button Signed-off-by: Justin Kim * remove the size prop Signed-off-by: Justin Kim * change it to EuiButton Signed-off-by: Justin Kim --------- Signed-off-by: Justin Kim --- .../osd-plugin-generator/template/public/components/app.tsx.ejs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/osd-plugin-generator/template/public/components/app.tsx.ejs b/packages/osd-plugin-generator/template/public/components/app.tsx.ejs index 2029a69dd8db..876b3f8c5e75 100644 --- a/packages/osd-plugin-generator/template/public/components/app.tsx.ejs +++ b/packages/osd-plugin-generator/template/public/components/app.tsx.ejs @@ -4,7 +4,7 @@ import { FormattedMessage, I18nProvider } from '@osd/i18n/react'; import { BrowserRouter as Router } from 'react-router-dom'; import { -EuiSmallButton, +EuiButton, EuiHorizontalRule, EuiPage, EuiPageBody, From 7df73ddeea9eb8f0c462cc8a099dc32f49d14692 Mon Sep 17 00:00:00 2001 From: Joshua Li Date: Fri, 6 Dec 2024 13:25:54 -0800 Subject: [PATCH 13/16] [Discover] Support custom logic to insert time filter based on dataset type (#8932) * Pass time filter if language overrides hideDatePicker --------- Signed-off-by: Joshua Li Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> --- changelogs/fragments/8932.yml | 2 + .../dataset_service/dataset_service.mock.ts | 3 + .../query_string/dataset_service/types.ts | 9 ++- .../ui/dataset_selector/configurator.test.tsx | 72 +++++++++++++++++-- .../ui/dataset_selector/configurator.tsx | 40 +++++++---- .../query_editor_top_row.test.tsx | 62 ++++++++++++++-- .../ui/query_editor/query_editor_top_row.tsx | 55 +++++++++++--- .../query_enhancements/common/types.ts | 7 +- .../query_enhancements/common/utils.ts | 1 + .../public/search/ppl_search_interceptor.ts | 21 +++++- .../public/search/sql_search_interceptor.ts | 11 +++ .../query_enhancements/server/routes/index.ts | 1 + 12 files changed, 246 insertions(+), 38 deletions(-) create mode 100644 changelogs/fragments/8932.yml diff --git a/changelogs/fragments/8932.yml b/changelogs/fragments/8932.yml new file mode 100644 index 000000000000..a048de0a102a --- /dev/null +++ b/changelogs/fragments/8932.yml @@ -0,0 +1,2 @@ +feat: +- Support custom logic to insert time filter based on dataset type ([#8932](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8932)) \ No newline at end of file diff --git a/src/plugins/data/public/query/query_string/dataset_service/dataset_service.mock.ts b/src/plugins/data/public/query/query_string/dataset_service/dataset_service.mock.ts index df5521078feb..ba491cb51191 100644 --- a/src/plugins/data/public/query/query_string/dataset_service/dataset_service.mock.ts +++ b/src/plugins/data/public/query/query_string/dataset_service/dataset_service.mock.ts @@ -43,6 +43,9 @@ const createSetupDatasetServiceMock = (): jest.Mocked => fetchOptions: jest.fn(), getRecentDatasets: jest.fn(), addRecentDataset: jest.fn(), + clearCache: jest.fn(), + getLastCacheTime: jest.fn(), + removeFromRecentDatasets: jest.fn(), }; }; diff --git a/src/plugins/data/public/query/query_string/dataset_service/types.ts b/src/plugins/data/public/query/query_string/dataset_service/types.ts index 65c322acec6f..d97afec8abb6 100644 --- a/src/plugins/data/public/query/query_string/dataset_service/types.ts +++ b/src/plugins/data/public/query/query_string/dataset_service/types.ts @@ -43,6 +43,13 @@ export interface DatasetTypeConfig { id: string; /** Human-readable title for the dataset type */ title: string; + languageOverrides?: { + [language: string]: { + /** The override transfers the responsibility of handling the input from + * the language interceptor to the dataset type search strategy. */ + hideDatePicker?: boolean; + }; + }; /** Metadata for UI representation */ meta: { /** Icon to represent the dataset type */ @@ -51,7 +58,7 @@ export interface DatasetTypeConfig { tooltip?: string; /** Optional preference for search on page load else defaulted to true */ searchOnLoad?: boolean; - /** Optional supportsTimeFilter determines if a time filter is needed */ + /** Optional supportsTimeFilter determines if a time field is supported */ supportsTimeFilter?: boolean; /** Optional isFieldLoadAsync determines if field loads are async */ isFieldLoadAsync?: boolean; diff --git a/src/plugins/data/public/ui/dataset_selector/configurator.test.tsx b/src/plugins/data/public/ui/dataset_selector/configurator.test.tsx index 462c6298a0a3..38d4e4e12183 100644 --- a/src/plugins/data/public/ui/dataset_selector/configurator.test.tsx +++ b/src/plugins/data/public/ui/dataset_selector/configurator.test.tsx @@ -3,14 +3,14 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { render, screen, fireEvent, waitFor } from '@testing-library/react'; -import { Configurator } from './configurator'; import '@testing-library/jest-dom'; +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import React from 'react'; -import { setQueryService, setIndexPatterns } from '../../services'; import { IntlProvider } from 'react-intl'; -import { Query } from '../../../../data/public'; import { Dataset } from 'src/plugins/data/common'; +import { Query } from '../../../../data/public'; +import { setIndexPatterns, setQueryService } from '../../services'; +import { Configurator } from './configurator'; const getQueryMock = jest.fn().mockReturnValue({ query: '', @@ -358,4 +358,68 @@ describe('Configurator Component', () => { expect(submitButton).toBeEnabled(); }); }); + + it('should show the date picker if supportsTimeFilter is undefined', async () => { + const mockDataset = { + ...mockBaseDataset, + timeFieldName: undefined, + type: 'index', + }; + const { container } = render( + + + + ); + + expect( + container.querySelector(`[data-test-subj="advancedSelectorTimeFieldSelect"]`) + ).toBeTruthy(); + }); + + it('should hide the date picker if supportsTimeFilter is false', async () => { + const mockDataset = { + ...mockBaseDataset, + timeFieldName: undefined, + type: 'index', + }; + const datasetTypeConfig = mockServices + .getQueryService() + .queryString.getDatasetService() + .getType(); + mockServices + .getQueryService() + .queryString.getDatasetService() + .getType.mockReturnValue({ + ...datasetTypeConfig, + meta: { + supportsTimeFilter: false, + }, + }); + const { container } = render( + + + + ); + + expect( + container.querySelector(`[data-test-subj="advancedSelectorTimeFieldSelect"]`) + ).toBeFalsy(); + + mockServices + .getQueryService() + .queryString.getDatasetService() + .getType.mockReturnValue(datasetTypeConfig); + }); }); diff --git a/src/plugins/data/public/ui/dataset_selector/configurator.tsx b/src/plugins/data/public/ui/dataset_selector/configurator.tsx index 0dba9107934c..4906bec2ef84 100644 --- a/src/plugins/data/public/ui/dataset_selector/configurator.tsx +++ b/src/plugins/data/public/ui/dataset_selector/configurator.tsx @@ -69,6 +69,7 @@ export const Configurator = ({ const [selectedIndexedView, setSelectedIndexedView] = useState(); const [indexedViews, setIndexedViews] = useState([]); const [isLoadingIndexedViews, setIsLoadingIndexedViews] = useState(false); + const [timeFieldsLoading, setTimeFieldsLoading] = useState(false); useEffect(() => { let isMounted = true; @@ -91,23 +92,26 @@ export const Configurator = ({ const submitDisabled = useMemo(() => { return ( - timeFieldName === undefined && - !( - languageService.getLanguage(language)?.hideDatePicker || - dataset.type === DEFAULT_DATA.SET_TYPES.INDEX_PATTERN - ) && - timeFields && - timeFields.length > 0 + timeFieldsLoading || + (timeFieldName === undefined && + !(dataset.type === DEFAULT_DATA.SET_TYPES.INDEX_PATTERN) && + timeFields && + timeFields.length > 0) ); - }, [dataset, language, timeFieldName, timeFields, languageService]); + }, [dataset, timeFieldName, timeFields, timeFieldsLoading]); useEffect(() => { const fetchFields = async () => { - const datasetFields = await queryString - .getDatasetService() - .getType(baseDataset.type) - ?.fetchFields(baseDataset); + const datasetType = queryString.getDatasetService().getType(baseDataset.type); + if (!datasetType) { + setTimeFields([]); + return; + } + setTimeFieldsLoading(true); + const datasetFields = await datasetType + .fetchFields(baseDataset) + .finally(() => setTimeFieldsLoading(false)); const dateFields = datasetFields?.filter((field) => field.type === 'date'); setTimeFields(dateFields || []); }; @@ -152,6 +156,16 @@ export const Configurator = ({ }; }, [indexedViewsService, selectedIndexedView, dataset]); + const shouldRenderDatePickerField = useCallback(() => { + const datasetType = queryString.getDatasetService().getType(dataset.type); + + const supportsTimeField = datasetType?.meta?.supportsTimeFilter; + if (supportsTimeField !== undefined) { + return Boolean(supportsTimeField); + } + return true; + }, [dataset.type, queryString]); + return ( <> @@ -256,7 +270,7 @@ export const Configurator = ({ data-test-subj="advancedSelectorLanguageSelect" /> - {!languageService.getLanguage(language)?.hideDatePicker && + {shouldRenderDatePickerField() && (dataset.type === DEFAULT_DATA.SET_TYPES.INDEX_PATTERN ? ( ({ }); const dataPlugin = dataPluginMock.createStartContract(true); +const datasetService = datasetServiceMock.createStartContract(); function wrapQueryEditorTopRowInContext(testProps: any) { const defaultOptions = { @@ -111,6 +113,7 @@ describe('QueryEditorTopRow', () => { beforeEach(() => { jest.clearAllMocks(); (getQueryService as jest.Mock).mockReturnValue(dataPlugin.query); + dataPlugin.query.queryString.getDatasetService = jest.fn().mockReturnValue(datasetService); }); afterEach(() => { @@ -155,4 +158,49 @@ describe('QueryEditorTopRow', () => { await waitFor(() => expect(container.querySelector(QUERY_EDITOR)).toBeTruthy()); expect(container.querySelector(DATE_PICKER)).toBeFalsy(); }); + + it('Should not render date picker if dataset type does not support time field', async () => { + const query: Query = { + query: 'test query', + dataset: datasetService.getDefault(), + language: 'test-language', + }; + dataPlugin.query.queryString.getQuery = jest.fn().mockReturnValue(query); + datasetService.getType.mockReturnValue({ + meta: { supportsTimeFilter: false }, + } as DatasetTypeConfig); + + const { container } = render( + wrapQueryEditorTopRowInContext({ + query, + showQueryEditor: false, + showDatePicker: true, + }) + ); + await waitFor(() => expect(container.querySelector(QUERY_EDITOR)).toBeTruthy()); + expect(container.querySelector(DATE_PICKER)).toBeFalsy(); + }); + + it('Should render date picker if dataset overrides hideDatePicker to false', async () => { + const query: Query = { + query: 'test query', + dataset: datasetService.getDefault(), + language: 'test-language', + }; + dataPlugin.query.queryString.getQuery = jest.fn().mockReturnValue(query); + datasetService.getType.mockReturnValue(({ + meta: { supportsTimeFilter: true }, + languageOverrides: { 'test-language': { hideDatePicker: false } }, + } as unknown) as DatasetTypeConfig); + + const { container } = render( + wrapQueryEditorTopRowInContext({ + query, + showQueryEditor: false, + showDatePicker: true, + }) + ); + await waitFor(() => expect(container.querySelector(QUERY_EDITOR)).toBeTruthy()); + expect(container.querySelector(DATE_PICKER)).toBeTruthy(); + }); }); diff --git a/src/plugins/data/public/ui/query_editor/query_editor_top_row.tsx b/src/plugins/data/public/ui/query_editor/query_editor_top_row.tsx index ea15fbfeeaa1..ad22750207ed 100644 --- a/src/plugins/data/public/ui/query_editor/query_editor_top_row.tsx +++ b/src/plugins/data/public/ui/query_editor/query_editor_top_row.tsx @@ -224,18 +224,53 @@ export default function QueryEditorTopRow(props: QueryEditorTopRowProps) { ); } + /** + * Determines if the date picker should be rendered based on UI settings, dataset configuration, and language settings. + * + * @returns {boolean} Whether the date picker should be rendered + * + * UI Settings permutations (isDatePickerEnabled): + * - showDatePicker=true || showAutoRefreshOnly=true => true + * - showDatePicker=false && showAutoRefreshOnly=false => false + * - both undefined => true (default) + * If isDatePickerEnabled is false, returns false immediately + * + * Dataset Type permutations (datasetType?.meta?.supportsTimeFilter): + * - supportsTimeFilter=false => false + * + * Language permutations (when dataset.meta.supportsTimeFilter is undefined or true): + * - queryLanguage=undefined => true (shows date picker) + * - queryLanguage exists: + * - languageOverrides[queryLanguage].hideDatePicker=true => false + * - languageOverrides[queryLanguage].hideDatePicker=false => true + * - hideDatePicker=true => false + * - hideDatePicker=false => true + * - hideDatePicker=undefined => true + */ function shouldRenderDatePicker(): boolean { - return ( - Boolean((props.showDatePicker || props.showAutoRefreshOnly) ?? true) && - !( - queryLanguage && - data.query.queryString.getLanguageService().getLanguage(queryLanguage)?.hideDatePicker - ) && - (props.query?.dataset - ? data.query.queryString.getDatasetService().getType(props.query.dataset.type)?.meta - ?.supportsTimeFilter !== false - : true) + const { queryString } = data.query; + const datasetService = queryString.getDatasetService(); + const languageService = queryString.getLanguageService(); + const isDatePickerEnabled = Boolean( + (props.showDatePicker || props.showAutoRefreshOnly) ?? true ); + if (!isDatePickerEnabled) return false; + + // Get dataset type configuration + const datasetType = props.query?.dataset + ? datasetService.getType(props.query?.dataset.type) + : undefined; + // Check if dataset type explicitly configures the `supportsTimeFilter` option + if (datasetType?.meta?.supportsTimeFilter === false) return false; + + if ( + queryLanguage && + datasetType?.languageOverrides?.[queryLanguage]?.hideDatePicker !== undefined + ) { + return Boolean(!datasetType.languageOverrides[queryLanguage].hideDatePicker); + } + + return Boolean(!(queryLanguage && languageService.getLanguage(queryLanguage)?.hideDatePicker)); } function shouldRenderQueryEditor(): boolean { diff --git a/src/plugins/query_enhancements/common/types.ts b/src/plugins/query_enhancements/common/types.ts index 1bb977527d4a..2f73ca52d496 100644 --- a/src/plugins/query_enhancements/common/types.ts +++ b/src/plugins/query_enhancements/common/types.ts @@ -4,7 +4,7 @@ */ import { CoreSetup } from 'opensearch-dashboards/public'; -import { PollQueryResultsParams } from '../../data/common'; +import { PollQueryResultsParams, TimeRange } from '../../data/common'; export interface QueryAggConfig { [key: string]: { @@ -26,7 +26,10 @@ export interface EnhancedFetchContext { http: CoreSetup['http']; path: string; signal?: AbortSignal; - body?: { pollQueryResultsParams: PollQueryResultsParams }; + body?: { + pollQueryResultsParams?: PollQueryResultsParams; + timeRange?: TimeRange; + }; } export interface QueryStatusOptions { diff --git a/src/plugins/query_enhancements/common/utils.ts b/src/plugins/query_enhancements/common/utils.ts index 29e49b00eab0..634a56b84603 100644 --- a/src/plugins/query_enhancements/common/utils.ts +++ b/src/plugins/query_enhancements/common/utils.ts @@ -55,6 +55,7 @@ export const fetch = (context: EnhancedFetchContext, query: Query, aggConfig?: Q query: { ...query, format: 'jdbc' }, aggConfig, pollQueryResultsParams: context.body?.pollQueryResultsParams, + timeRange: context.body?.timeRange, }); return from( http.fetch({ diff --git a/src/plugins/query_enhancements/public/search/ppl_search_interceptor.ts b/src/plugins/query_enhancements/public/search/ppl_search_interceptor.ts index 57152dbe98ea..ecfe32ff8a75 100644 --- a/src/plugins/query_enhancements/public/search/ppl_search_interceptor.ts +++ b/src/plugins/query_enhancements/public/search/ppl_search_interceptor.ts @@ -50,6 +50,7 @@ export class PPLSearchInterceptor extends SearchInterceptor { signal, body: { pollQueryResultsParams: request.params?.pollQueryResultsParams, + timeRange: request.params?.body?.timeRange, }, }; @@ -68,15 +69,33 @@ export class PPLSearchInterceptor extends SearchInterceptor { .getDatasetService() .getType(datasetType); strategy = datasetTypeConfig?.getSearchOptions?.().strategy ?? strategy; + + if ( + dataset?.timeFieldName && + datasetTypeConfig?.languageOverrides?.PPL?.hideDatePicker === false + ) { + request.params = { + ...request.params, + body: { + ...request.params.body, + timeRange: this.queryService.timefilter.timefilter.getTime(), + }, + }; + } } return this.runSearch(request, options.abortSignal, strategy); } private buildQuery() { - const query: Query = this.queryService.queryString.getQuery(); + const { queryString } = this.queryService; + const query: Query = queryString.getQuery(); const dataset = query.dataset; if (!dataset || !dataset.timeFieldName) return query; + const datasetService = queryString.getDatasetService(); + if (datasetService.getType(dataset.type)?.languageOverrides?.PPL?.hideDatePicker === false) + return query; + const [baseQuery, ...afterPipeParts] = query.query.split('|'); const afterPipe = afterPipeParts.length > 0 ? ` | ${afterPipeParts.join('|').trim()}` : ''; const timeFilter = this.getTimeFilter(dataset.timeFieldName); diff --git a/src/plugins/query_enhancements/public/search/sql_search_interceptor.ts b/src/plugins/query_enhancements/public/search/sql_search_interceptor.ts index 9fe17fc79322..9f93dd067cb3 100644 --- a/src/plugins/query_enhancements/public/search/sql_search_interceptor.ts +++ b/src/plugins/query_enhancements/public/search/sql_search_interceptor.ts @@ -42,6 +42,7 @@ export class SQLSearchInterceptor extends SearchInterceptor { signal, body: { pollQueryResultsParams: request.params?.pollQueryResultsParams, + timeRange: request.params?.body?.timeRange, }, }; @@ -62,6 +63,16 @@ export class SQLSearchInterceptor extends SearchInterceptor { .getDatasetService() .getType(datasetType); strategy = datasetTypeConfig?.getSearchOptions?.().strategy ?? strategy; + + if (datasetTypeConfig?.languageOverrides?.SQL?.hideDatePicker === false) { + request.params = { + ...request.params, + body: { + ...request.params.body, + timeRange: this.queryService.timefilter.timefilter.getTime(), + }, + }; + } } return this.runSearch(request, options.abortSignal, strategy); diff --git a/src/plugins/query_enhancements/server/routes/index.ts b/src/plugins/query_enhancements/server/routes/index.ts index 84cf19bec50c..2cda4a9f0cbf 100644 --- a/src/plugins/query_enhancements/server/routes/index.ts +++ b/src/plugins/query_enhancements/server/routes/index.ts @@ -86,6 +86,7 @@ export function defineSearchStrategyRouteProvider(logger: Logger, router: IRoute sessionId: schema.maybe(schema.string()), }) ), + timeRange: schema.maybe(schema.object({}, { unknowns: 'allow' })), }), }, }, From 6a0484ae23251ecff347bb126b1a5dc9391bbe30 Mon Sep 17 00:00:00 2001 From: Argus Li <43020525+ArgusLi@users.noreply.github.com> Date: Tue, 10 Dec 2024 11:32:22 -0800 Subject: [PATCH 14/16] [Discover Test] TestID-69: Filtering. Add data-test-subj tags. (#9027) * Update Cypress to v12 (#8926) * Update cypress to v12 Signed-off-by: Daniel Rowe * Add required e2e.js Signed-off-by: Daniel Rowe * Changeset file for PR #8926 created/updated * Update license header Signed-off-by: Daniel Rowe <51932404+d-rowe@users.noreply.github.com> * Update license in e2e.js Signed-off-by: Daniel Rowe <51932404+d-rowe@users.noreply.github.com> --------- Signed-off-by: Daniel Rowe Signed-off-by: Daniel Rowe <51932404+d-rowe@users.noreply.github.com> Co-authored-by: Daniel Rowe Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> * fix: support imports without extensions in cypress webpack build (#8993) * fix: support imports without extensions in cypress webpack build Signed-off-by: Daniel Rowe * Changeset file for PR #8993 created/updated * use typescript config Signed-off-by: Daniel Rowe <51932404+d-rowe@users.noreply.github.com> * fix lint Signed-off-by: Daniel Rowe <51932404+d-rowe@users.noreply.github.com> * disable new test isolation feature This isolation was causing regressions Signed-off-by: Daniel Rowe <51932404+d-rowe@users.noreply.github.com> --------- Signed-off-by: Daniel Rowe Signed-off-by: Daniel Rowe <51932404+d-rowe@users.noreply.github.com> Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> * Add data-test-subj tags. Signed-off-by: Argus Li --------- Signed-off-by: Daniel Rowe Signed-off-by: Daniel Rowe <51932404+d-rowe@users.noreply.github.com> Signed-off-by: Argus Li Co-authored-by: Daniel Rowe <51932404+d-rowe@users.noreply.github.com> Co-authored-by: Daniel Rowe Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> --- .../data/public/ui/dataset_selector/dataset_explorer.tsx | 1 + src/plugins/data/public/ui/filter_bar/filter_bar.tsx | 7 ++++++- .../ui/filter_bar/filter_editor/lib/filter_label.test.tsx | 4 ++++ .../ui/filter_bar/filter_editor/lib/filter_label.tsx | 6 +++++- 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/plugins/data/public/ui/dataset_selector/dataset_explorer.tsx b/src/plugins/data/public/ui/dataset_selector/dataset_explorer.tsx index 7861dd836cd1..ec8e118157b1 100644 --- a/src/plugins/data/public/ui/dataset_selector/dataset_explorer.tsx +++ b/src/plugins/data/public/ui/dataset_selector/dataset_explorer.tsx @@ -152,6 +152,7 @@ export const DatasetExplorer = ({
( - + { : Warning @@ -125,6 +126,7 @@ test('alias with error status', () => { : Error @@ -141,6 +143,7 @@ test('warning', () => { : Warning @@ -157,6 +160,7 @@ test('error', () => { : Error diff --git a/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_label.tsx b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_label.tsx index 529053ffd042..32f14b3eba34 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_label.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_label.tsx @@ -59,7 +59,11 @@ export default function FilterLabel({ filter, valueLabel, filterLabelStatus }: F ); const getValue = (text?: string) => { - return {text}; + return ( + + {text} + + ); }; if (filter.meta.alias !== null) { From 9f234422debc120bd9028ed8d202d7d3da7e565e Mon Sep 17 00:00:00 2001 From: Federico Silva Date: Tue, 10 Dec 2024 17:15:59 -0300 Subject: [PATCH 15/16] add queryEditorLanguageOptions data-test-subj on language_selector.tsx (#9029) Signed-off-by: Federico Silva Co-authored-by: Federico Silva --- src/plugins/data/public/ui/query_editor/language_selector.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/plugins/data/public/ui/query_editor/language_selector.tsx b/src/plugins/data/public/ui/query_editor/language_selector.tsx index 3047cefda31b..0177ede6f7fb 100644 --- a/src/plugins/data/public/ui/query_editor/language_selector.tsx +++ b/src/plugins/data/public/ui/query_editor/language_selector.tsx @@ -151,6 +151,7 @@ export const QueryLanguageSelector = (props: QueryLanguageSelectorProps) => { )} size="s" items={languageOptionsMenu} + data-test-subj="queryEditorLanguageOptions" /> ); From 85273e3be84b132dcd0f1d2a53bc66ebe997cdd1 Mon Sep 17 00:00:00 2001 From: Brandon Shien <44730413+bshien@users.noreply.github.com> Date: Tue, 10 Dec 2024 16:41:04 -0800 Subject: [PATCH 16/16] Added release notes for 1.3.20 (#9033) Signed-off-by: Brandon Shien --- ...opensearch-dashboards.release-notes-1.3.20.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 release-notes/opensearch-dashboards.release-notes-1.3.20.md diff --git a/release-notes/opensearch-dashboards.release-notes-1.3.20.md b/release-notes/opensearch-dashboards.release-notes-1.3.20.md new file mode 100644 index 000000000000..b03051525446 --- /dev/null +++ b/release-notes/opensearch-dashboards.release-notes-1.3.20.md @@ -0,0 +1,16 @@ +# Version 1.3.20 Release Notes + +### 🛡 Security + +- [CVE-2024-45590] Bump body-parser from 1.19.0 to 1.20.3 ([#9007](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/9007)) +- [CVE-2024-45296] Bump various version of path-to-regexp to required versions ([#9007](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/9007)) + +### 📈 Features/Enhancements + +### 🐛 Bug Fixes + +### 🚞 Infrastructure + +### 📝 Documentation + +### 🛠 Maintenance \ No newline at end of file