diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 307cecba44651..ac6d8a03d2d2b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -610,6 +610,7 @@ x-pack/packages/ml/data_view_utils @elastic/ml-ui x-pack/packages/ml/date_picker @elastic/ml-ui x-pack/packages/ml/date_utils @elastic/ml-ui x-pack/packages/ml/error_utils @elastic/ml-ui +x-pack/packages/ml/field_stats_flyout @elastic/ml-ui x-pack/packages/ml/in_memory_table @elastic/ml-ui x-pack/packages/ml/is_defined @elastic/ml-ui x-pack/packages/ml/is_populated_object @elastic/ml-ui @@ -617,6 +618,7 @@ x-pack/packages/ml/kibana_theme @elastic/ml-ui x-pack/packages/ml/local_storage @elastic/ml-ui x-pack/packages/ml/nested_property @elastic/ml-ui x-pack/packages/ml/number_utils @elastic/ml-ui +x-pack/packages/ml/parse_interval @elastic/ml-ui x-pack/plugins/ml @elastic/ml-ui x-pack/packages/ml/query_utils @elastic/ml-ui x-pack/packages/ml/random_sampler_utils @elastic/ml-ui @@ -628,6 +630,7 @@ x-pack/packages/ml/time_buckets @elastic/ml-ui x-pack/packages/ml/trained_models_utils @elastic/ml-ui x-pack/packages/ml/ui_actions @elastic/ml-ui x-pack/packages/ml/url_state @elastic/ml-ui +x-pack/packages/ml/validators @elastic/ml-ui packages/kbn-mock-idp-plugin @elastic/kibana-security packages/kbn-mock-idp-utils @elastic/kibana-security packages/kbn-monaco @elastic/appex-sharedux diff --git a/docs/setup/upgrade/resolving-migration-failures.asciidoc b/docs/setup/upgrade/resolving-migration-failures.asciidoc index 596674237eb7a..81ddac9a94900 100644 --- a/docs/setup/upgrade/resolving-migration-failures.asciidoc +++ b/docs/setup/upgrade/resolving-migration-failures.asciidoc @@ -120,10 +120,7 @@ If you fail to remedy this, your upgrade to 8.0+ will fail with a message like: [source,sh] -------------------------------------------- -Unable to complete saved object migrations for the [.kibana] index: Migration failed because some documents were found which use unknown saved object types: -- "firstDocId" (type "someType") -- "secondtDocId" (type "someType") -- "thirdDocId" (type "someOtherType") +Unable to complete saved object migrations for the [.kibana] index: Migration failed because some documents were found which use unknown saved object types: someType,someOtherType To proceed with the migration you can configure Kibana to discard unknown saved objects for this migration. -------------------------------------------- diff --git a/docs/user/ml/index.asciidoc b/docs/user/ml/index.asciidoc index 2e9e4bf0e007f..c13c1127124e6 100644 --- a/docs/user/ml/index.asciidoc +++ b/docs/user/ml/index.asciidoc @@ -19,10 +19,22 @@ if your data is stored in {es} and contains a time field, you can use the [role="screenshot"] image::user/ml/images/ml-data-visualizer-sample.png[{data-viz} for sample flight data] -You can also upload a CSV, NDJSON, or log file. The *{data-viz}* -identifies the file format and field mappings. You can then optionally import -that data into an {es} index. To change the default file size limit, see -<>. +You can upload different file formats for analysis with the *{data-viz}*. + +File formats supported up to 500 MB: +* CSV +* TSV +* NDJSON +* Log files + +File formats supported up to 60 MB: +* PDF +* Microsoft Office files (Word, Excel, PowerPoint) +* Plain Text (TXT) +* Rich Text (RTF) +* Open Document Format (ODF) + +The *{data-viz}* identifies the file format and field mappings, and you can import the data into an {es} index. To change the default file size limit, see <> in advanced settings. If {stack-security-features} are enabled, users must have the necessary privileges to use {ml-features}. Refer to diff --git a/oas_docs/output/kibana.serverless.staging.yaml b/oas_docs/output/kibana.serverless.staging.yaml index 15790040e6a46..c1079be34d264 100644 --- a/oas_docs/output/kibana.serverless.staging.yaml +++ b/oas_docs/output/kibana.serverless.staging.yaml @@ -8169,7 +8169,7 @@ paths: - Security Solution Endpoint Management API /api/entity_store/engines: get: - operationId: ListEntityStoreEngines + operationId: ListEntityEngines responses: '200': content: @@ -8185,14 +8185,14 @@ paths: #/components/schemas/Security_Solution_Entity_Analytics_API_EngineDescriptor type: array description: Successful response - summary: List the Entity Store engines + summary: List the Entity Engines tags: - Security Solution Entity Analytics API /api/entity_store/engines/{entityType}: delete: - operationId: DeleteEntityStore + operationId: DeleteEntityEngine parameters: - - description: The entity type of the store (either 'user' or 'host'). + - description: The entity type of the engine (either 'user' or 'host'). in: path name: entityType required: true @@ -8215,13 +8215,13 @@ paths: deleted: type: boolean description: Successful response - summary: Delete the Entity Store engine + summary: Delete the Entity Engine tags: - Security Solution Entity Analytics API get: - operationId: GetEntityStoreEngine + operationId: GetEntityEngine parameters: - - description: The entity type of the store (either 'user' or 'host'). + - description: The entity type of the engine (either 'user' or 'host'). in: path name: entityType required: true @@ -8236,14 +8236,14 @@ paths: $ref: >- #/components/schemas/Security_Solution_Entity_Analytics_API_EngineDescriptor description: Successful response - summary: Get the Entity Store engine + summary: Get an Entity Engine tags: - Security Solution Entity Analytics API /api/entity_store/engines/{entityType}/init: post: - operationId: InitEntityStore + operationId: InitEntityEngine parameters: - - description: The entity type of the store (either 'user' or 'host'). + - description: The entity type of the engine (either 'user' or 'host'). in: path name: entityType required: true @@ -8271,14 +8271,14 @@ paths: $ref: >- #/components/schemas/Security_Solution_Entity_Analytics_API_EngineDescriptor description: Successful response - summary: Initialize the Entity Store + summary: Initialize an Entity Engine tags: - Security Solution Entity Analytics API /api/entity_store/engines/{entityType}/start: post: - operationId: StartEntityStore + operationId: StartEntityEngine parameters: - - description: The entity type of the store (either 'user' or 'host'). + - description: The entity type of the engine (either 'user' or 'host'). in: path name: entityType required: true @@ -8295,14 +8295,14 @@ paths: started: type: boolean description: Successful response - summary: Start the Entity Store engine + summary: Start an Entity Engine tags: - Security Solution Entity Analytics API /api/entity_store/engines/{entityType}/stats: post: - operationId: GetEntityStoreStats + operationId: GetEntityEngineStats parameters: - - description: The entity type of the store (either 'user' or 'host'). + - description: The entity type of the engine (either 'user' or 'host'). in: path name: entityType required: true @@ -8334,14 +8334,14 @@ paths: $ref: >- #/components/schemas/Security_Solution_Entity_Analytics_API_EntityType description: Successful response - summary: Get the Entity Store engine stats + summary: Get Entity Engine stats tags: - Security Solution Entity Analytics API /api/entity_store/engines/{entityType}/stop: post: - operationId: StopEntityStore + operationId: StopEntityEngine parameters: - - description: The entity type of the store (either 'user' or 'host'). + - description: The entity type of the engine (either 'user' or 'host'). in: path name: entityType required: true @@ -8358,7 +8358,7 @@ paths: stopped: type: boolean description: Successful response - summary: Stop the Entity Store engine + summary: Stop an Entity Engine tags: - Security Solution Entity Analytics API /api/entity_store/entities/list: @@ -15090,7 +15090,8 @@ paths: type: string required: - noteId - - type: object + - nullable: true + type: object properties: noteIds: items: @@ -15121,19 +15122,18 @@ paths: parameters: - in: query name: documentIds - required: true schema: $ref: '#/components/schemas/Security_Solution_Timeline_API_DocumentIds' - in: query name: page schema: nullable: true - type: number + type: string - in: query name: perPage schema: nullable: true - type: number + type: string - in: query name: search schema: @@ -15156,6 +15156,13 @@ paths: type: string responses: '200': + content: + application/json; Elastic-Api-Version=2023-10-31: + schema: + oneOf: + - $ref: >- + #/components/schemas/Security_Solution_Timeline_API_GetNotesResult + - type: object description: Indicates the requested notes were returned. summary: Get notes tags: @@ -15205,19 +15212,8 @@ paths: type: object properties: persistNote: - type: object - properties: - code: - type: number - message: - type: string - note: - $ref: >- - #/components/schemas/Security_Solution_Timeline_API_Note - required: - - code - - message - - note + $ref: >- + #/components/schemas/Security_Solution_Timeline_API_ResponseNote required: - persistNote required: @@ -15589,15 +15585,8 @@ paths: type: object properties: persistPinnedEventOnTimeline: - allOf: - - $ref: >- - #/components/schemas/Security_Solution_Timeline_API_PinnedEvent - - type: object - properties: - code: - type: number - message: - type: string + $ref: >- + #/components/schemas/Security_Solution_Timeline_API_PersistPinnedEventResponse required: - persistPinnedEventOnTimeline required: @@ -32400,8 +32389,28 @@ components: nullable: true type: string timelineId: + type: string + updated: + nullable: true + type: number + updatedBy: + nullable: true + type: string + required: + - timelineId + Security_Solution_Timeline_API_BarePinnedEvent: + type: object + properties: + created: + nullable: true + type: number + createdBy: nullable: true type: string + eventId: + type: string + timelineId: + type: string updated: nullable: true type: number @@ -32409,6 +32418,7 @@ components: nullable: true type: string required: + - eventId - timelineId Security_Solution_Timeline_API_ColumnHeaderResult: type: object @@ -32584,6 +32594,18 @@ components: type: string script: type: string + Security_Solution_Timeline_API_GetNotesResult: + type: object + properties: + notes: + items: + $ref: '#/components/schemas/Security_Solution_Timeline_API_Note' + type: array + totalCount: + type: number + required: + - totalCount + - notes Security_Solution_Timeline_API_ImportTimelineResult: type: object properties: @@ -32644,34 +32666,38 @@ components: type: string version: type: string + required: + - noteId + - version + Security_Solution_Timeline_API_PersistPinnedEventResponse: + oneOf: + - allOf: + - $ref: '#/components/schemas/Security_Solution_Timeline_API_PinnedEvent' + - $ref: >- + #/components/schemas/Security_Solution_Timeline_API_PinnedEventBaseResponseBody + - nullable: true + type: object Security_Solution_Timeline_API_PinnedEvent: + allOf: + - $ref: '#/components/schemas/Security_Solution_Timeline_API_BarePinnedEvent' + - type: object + properties: + pinnedEventId: + type: string + version: + type: string + required: + - pinnedEventId + - version + Security_Solution_Timeline_API_PinnedEventBaseResponseBody: type: object properties: - created: - nullable: true - type: number - createdBy: - nullable: true - type: string - eventId: - type: string - pinnedEventId: - type: string - timelineId: - type: string - updated: - nullable: true + code: type: number - updatedBy: - nullable: true - type: string - version: + message: type: string required: - - eventId - - pinnedEventId - - timelineId - - version + - code Security_Solution_Timeline_API_QueryMatchResult: type: object properties: @@ -32716,6 +32742,19 @@ components: type: object readable: type: boolean + Security_Solution_Timeline_API_ResponseNote: + type: object + properties: + code: + type: number + message: + type: string + note: + $ref: '#/components/schemas/Security_Solution_Timeline_API_Note' + required: + - code + - message + - note Security_Solution_Timeline_API_RowRendererId: enum: - alert diff --git a/oas_docs/output/kibana.staging.yaml b/oas_docs/output/kibana.staging.yaml index c2b530c0af263..c92cf81b7f14a 100644 --- a/oas_docs/output/kibana.staging.yaml +++ b/oas_docs/output/kibana.staging.yaml @@ -11616,7 +11616,7 @@ paths: - Security Solution Endpoint Management API /api/entity_store/engines: get: - operationId: ListEntityStoreEngines + operationId: ListEntityEngines responses: '200': content: @@ -11632,14 +11632,14 @@ paths: #/components/schemas/Security_Solution_Entity_Analytics_API_EngineDescriptor type: array description: Successful response - summary: List the Entity Store engines + summary: List the Entity Engines tags: - Security Solution Entity Analytics API /api/entity_store/engines/{entityType}: delete: - operationId: DeleteEntityStore + operationId: DeleteEntityEngine parameters: - - description: The entity type of the store (either 'user' or 'host'). + - description: The entity type of the engine (either 'user' or 'host'). in: path name: entityType required: true @@ -11662,13 +11662,13 @@ paths: deleted: type: boolean description: Successful response - summary: Delete the Entity Store engine + summary: Delete the Entity Engine tags: - Security Solution Entity Analytics API get: - operationId: GetEntityStoreEngine + operationId: GetEntityEngine parameters: - - description: The entity type of the store (either 'user' or 'host'). + - description: The entity type of the engine (either 'user' or 'host'). in: path name: entityType required: true @@ -11683,14 +11683,14 @@ paths: $ref: >- #/components/schemas/Security_Solution_Entity_Analytics_API_EngineDescriptor description: Successful response - summary: Get the Entity Store engine + summary: Get an Entity Engine tags: - Security Solution Entity Analytics API /api/entity_store/engines/{entityType}/init: post: - operationId: InitEntityStore + operationId: InitEntityEngine parameters: - - description: The entity type of the store (either 'user' or 'host'). + - description: The entity type of the engine (either 'user' or 'host'). in: path name: entityType required: true @@ -11718,14 +11718,14 @@ paths: $ref: >- #/components/schemas/Security_Solution_Entity_Analytics_API_EngineDescriptor description: Successful response - summary: Initialize the Entity Store + summary: Initialize an Entity Engine tags: - Security Solution Entity Analytics API /api/entity_store/engines/{entityType}/start: post: - operationId: StartEntityStore + operationId: StartEntityEngine parameters: - - description: The entity type of the store (either 'user' or 'host'). + - description: The entity type of the engine (either 'user' or 'host'). in: path name: entityType required: true @@ -11742,14 +11742,14 @@ paths: started: type: boolean description: Successful response - summary: Start the Entity Store engine + summary: Start an Entity Engine tags: - Security Solution Entity Analytics API /api/entity_store/engines/{entityType}/stats: post: - operationId: GetEntityStoreStats + operationId: GetEntityEngineStats parameters: - - description: The entity type of the store (either 'user' or 'host'). + - description: The entity type of the engine (either 'user' or 'host'). in: path name: entityType required: true @@ -11781,14 +11781,14 @@ paths: $ref: >- #/components/schemas/Security_Solution_Entity_Analytics_API_EntityType description: Successful response - summary: Get the Entity Store engine stats + summary: Get Entity Engine stats tags: - Security Solution Entity Analytics API /api/entity_store/engines/{entityType}/stop: post: - operationId: StopEntityStore + operationId: StopEntityEngine parameters: - - description: The entity type of the store (either 'user' or 'host'). + - description: The entity type of the engine (either 'user' or 'host'). in: path name: entityType required: true @@ -11805,7 +11805,7 @@ paths: stopped: type: boolean description: Successful response - summary: Stop the Entity Store engine + summary: Stop an Entity Engine tags: - Security Solution Entity Analytics API /api/entity_store/entities/list: @@ -18539,7 +18539,8 @@ paths: type: string required: - noteId - - type: object + - nullable: true + type: object properties: noteIds: items: @@ -18570,19 +18571,18 @@ paths: parameters: - in: query name: documentIds - required: true schema: $ref: '#/components/schemas/Security_Solution_Timeline_API_DocumentIds' - in: query name: page schema: nullable: true - type: number + type: string - in: query name: perPage schema: nullable: true - type: number + type: string - in: query name: search schema: @@ -18605,6 +18605,13 @@ paths: type: string responses: '200': + content: + application/json; Elastic-Api-Version=2023-10-31: + schema: + oneOf: + - $ref: >- + #/components/schemas/Security_Solution_Timeline_API_GetNotesResult + - type: object description: Indicates the requested notes were returned. summary: Get notes tags: @@ -18654,19 +18661,8 @@ paths: type: object properties: persistNote: - type: object - properties: - code: - type: number - message: - type: string - note: - $ref: >- - #/components/schemas/Security_Solution_Timeline_API_Note - required: - - code - - message - - note + $ref: >- + #/components/schemas/Security_Solution_Timeline_API_ResponseNote required: - persistNote required: @@ -19038,15 +19034,8 @@ paths: type: object properties: persistPinnedEventOnTimeline: - allOf: - - $ref: >- - #/components/schemas/Security_Solution_Timeline_API_PinnedEvent - - type: object - properties: - code: - type: number - message: - type: string + $ref: >- + #/components/schemas/Security_Solution_Timeline_API_PersistPinnedEventResponse required: - persistPinnedEventOnTimeline required: @@ -40429,8 +40418,28 @@ components: nullable: true type: string timelineId: + type: string + updated: + nullable: true + type: number + updatedBy: + nullable: true + type: string + required: + - timelineId + Security_Solution_Timeline_API_BarePinnedEvent: + type: object + properties: + created: + nullable: true + type: number + createdBy: nullable: true type: string + eventId: + type: string + timelineId: + type: string updated: nullable: true type: number @@ -40438,6 +40447,7 @@ components: nullable: true type: string required: + - eventId - timelineId Security_Solution_Timeline_API_ColumnHeaderResult: type: object @@ -40613,6 +40623,18 @@ components: type: string script: type: string + Security_Solution_Timeline_API_GetNotesResult: + type: object + properties: + notes: + items: + $ref: '#/components/schemas/Security_Solution_Timeline_API_Note' + type: array + totalCount: + type: number + required: + - totalCount + - notes Security_Solution_Timeline_API_ImportTimelineResult: type: object properties: @@ -40673,34 +40695,38 @@ components: type: string version: type: string + required: + - noteId + - version + Security_Solution_Timeline_API_PersistPinnedEventResponse: + oneOf: + - allOf: + - $ref: '#/components/schemas/Security_Solution_Timeline_API_PinnedEvent' + - $ref: >- + #/components/schemas/Security_Solution_Timeline_API_PinnedEventBaseResponseBody + - nullable: true + type: object Security_Solution_Timeline_API_PinnedEvent: + allOf: + - $ref: '#/components/schemas/Security_Solution_Timeline_API_BarePinnedEvent' + - type: object + properties: + pinnedEventId: + type: string + version: + type: string + required: + - pinnedEventId + - version + Security_Solution_Timeline_API_PinnedEventBaseResponseBody: type: object properties: - created: - nullable: true - type: number - createdBy: - nullable: true - type: string - eventId: - type: string - pinnedEventId: - type: string - timelineId: - type: string - updated: - nullable: true + code: type: number - updatedBy: - nullable: true - type: string - version: + message: type: string required: - - eventId - - pinnedEventId - - timelineId - - version + - code Security_Solution_Timeline_API_QueryMatchResult: type: object properties: @@ -40745,6 +40771,19 @@ components: type: object readable: type: boolean + Security_Solution_Timeline_API_ResponseNote: + type: object + properties: + code: + type: number + message: + type: string + note: + $ref: '#/components/schemas/Security_Solution_Timeline_API_Note' + required: + - code + - message + - note Security_Solution_Timeline_API_RowRendererId: enum: - alert diff --git a/package.json b/package.json index 1aec2e615a1be..7fdcd237d0d33 100644 --- a/package.json +++ b/package.json @@ -645,6 +645,7 @@ "@kbn/ml-date-picker": "link:x-pack/packages/ml/date_picker", "@kbn/ml-date-utils": "link:x-pack/packages/ml/date_utils", "@kbn/ml-error-utils": "link:x-pack/packages/ml/error_utils", + "@kbn/ml-field-stats-flyout": "link:x-pack/packages/ml/field_stats_flyout", "@kbn/ml-in-memory-table": "link:x-pack/packages/ml/in_memory_table", "@kbn/ml-is-defined": "link:x-pack/packages/ml/is_defined", "@kbn/ml-is-populated-object": "link:x-pack/packages/ml/is_populated_object", @@ -652,6 +653,7 @@ "@kbn/ml-local-storage": "link:x-pack/packages/ml/local_storage", "@kbn/ml-nested-property": "link:x-pack/packages/ml/nested_property", "@kbn/ml-number-utils": "link:x-pack/packages/ml/number_utils", + "@kbn/ml-parse-interval": "link:x-pack/packages/ml/parse_interval", "@kbn/ml-plugin": "link:x-pack/plugins/ml", "@kbn/ml-query-utils": "link:x-pack/packages/ml/query_utils", "@kbn/ml-random-sampler-utils": "link:x-pack/packages/ml/random_sampler_utils", @@ -663,6 +665,7 @@ "@kbn/ml-trained-models-utils": "link:x-pack/packages/ml/trained_models_utils", "@kbn/ml-ui-actions": "link:x-pack/packages/ml/ui_actions", "@kbn/ml-url-state": "link:x-pack/packages/ml/url_state", + "@kbn/ml-validators": "link:x-pack/packages/ml/validators", "@kbn/monaco": "link:packages/kbn-monaco", "@kbn/monitoring-collection-plugin": "link:x-pack/plugins/monitoring_collection", "@kbn/monitoring-plugin": "link:x-pack/plugins/monitoring", @@ -1760,7 +1763,7 @@ "mochawesome-merge": "^4.3.0", "mock-fs": "^5.1.2", "ms-chromium-edge-driver": "^0.5.1", - "msw": "^2.4.5", + "msw": "^2.4.7", "multistream": "^4.1.0", "mutation-observer": "^1.0.3", "native-hdr-histogram": "^1.0.0", diff --git a/packages/core/chrome/core-chrome-browser-internal/src/chrome_service.tsx b/packages/core/chrome/core-chrome-browser-internal/src/chrome_service.tsx index dd39fa0287f27..3444b3edd8078 100644 --- a/packages/core/chrome/core-chrome-browser-internal/src/chrome_service.tsx +++ b/packages/core/chrome/core-chrome-browser-internal/src/chrome_service.tsx @@ -568,6 +568,8 @@ export class ChromeService { sideNav: { getIsCollapsed$: () => this.isSideNavCollapsed$.asObservable(), setIsCollapsed: setIsSideNavCollapsed, + getPanelSelectedNode$: projectNavigation.getPanelSelectedNode$.bind(projectNavigation), + setPanelSelectedNode: projectNavigation.setPanelSelectedNode.bind(projectNavigation), }, getActiveSolutionNavId$: () => projectNavigation.getActiveSolutionNavId$(), project: { diff --git a/packages/core/chrome/core-chrome-browser-internal/src/project_navigation/project_navigation_service.test.ts b/packages/core/chrome/core-chrome-browser-internal/src/project_navigation/project_navigation_service.test.ts index 4d9d625970b7d..d1be94aad246a 100644 --- a/packages/core/chrome/core-chrome-browser-internal/src/project_navigation/project_navigation_service.test.ts +++ b/packages/core/chrome/core-chrome-browser-internal/src/project_navigation/project_navigation_service.test.ts @@ -1003,4 +1003,69 @@ describe('solution navigations', () => { expect(activeSolution).toEqual(solution1); } }); + + it('should set and return the nav panel selected node', async () => { + const { projectNavigation } = setup({ navLinkIds: ['link1', 'link2', 'link3'] }); + + { + const selectedNode = await firstValueFrom(projectNavigation.getPanelSelectedNode$()); + expect(selectedNode).toBeNull(); + } + + { + const node: ChromeProjectNavigationNode = { + id: 'node1', + title: 'Node 1', + path: 'node1', + }; + projectNavigation.setPanelSelectedNode(node); + + const selectedNode = await firstValueFrom(projectNavigation.getPanelSelectedNode$()); + + expect(selectedNode).toBe(node); + } + + { + const fooSolution: SolutionNavigationDefinition = { + id: 'fooSolution', + title: 'Foo solution', + icon: 'logoSolution', + homePage: 'discover', + navigationTree$: of({ + body: [ + { + type: 'navGroup', + id: 'group1', + children: [ + { link: 'link1' }, + { + id: 'group2', + children: [ + { + link: 'link2', // We'll target this node using its id + }, + ], + }, + { link: 'link3' }, + ], + }, + ], + }), + }; + + projectNavigation.changeActiveSolutionNavigation('foo'); + projectNavigation.updateSolutionNavigations({ foo: fooSolution }); + + projectNavigation.setPanelSelectedNode('link2'); // Set the selected node using its id + + const selectedNode = await firstValueFrom(projectNavigation.getPanelSelectedNode$()); + + expect(selectedNode).toMatchObject({ + id: 'link2', + href: '/app/link2', + path: 'group1.group2.link2', + title: 'LINK2', + }); + } + }); }); diff --git a/packages/core/chrome/core-chrome-browser-internal/src/project_navigation/project_navigation_service.ts b/packages/core/chrome/core-chrome-browser-internal/src/project_navigation/project_navigation_service.ts index 1a8c05f13774f..6f77705069eaf 100644 --- a/packages/core/chrome/core-chrome-browser-internal/src/project_navigation/project_navigation_service.ts +++ b/packages/core/chrome/core-chrome-browser-internal/src/project_navigation/project_navigation_service.ts @@ -74,6 +74,10 @@ export class ProjectNavigationService { // The navigation tree for the Side nav UI that still contains layout information (body, footer, etc.) private navigationTreeUi$ = new BehaviorSubject(null); private activeNodes$ = new BehaviorSubject([]); + // Keep a reference to the nav node selected when the navigation panel is opened + private readonly panelSelectedNode$ = new BehaviorSubject( + null + ); private projectBreadcrumbs$ = new BehaviorSubject<{ breadcrumbs: ChromeProjectBreadcrumb[]; @@ -187,6 +191,8 @@ export class ProjectNavigationService { getActiveSolutionNavDefinition$: this.getActiveSolutionNavDefinition$.bind(this), /** In stateful Kibana, get the id of the active solution navigation */ getActiveSolutionNavId$: () => this.activeSolutionNavDefinitionId$.asObservable(), + getPanelSelectedNode$: () => this.panelSelectedNode$.asObservable(), + setPanelSelectedNode: this.setPanelSelectedNode.bind(this), }; } @@ -415,6 +421,34 @@ export class ProjectNavigationService { } } + private setPanelSelectedNode = (_node: string | ChromeProjectNavigationNode | null) => { + const node = typeof _node === 'string' ? this.findNodeById(_node) : _node; + this.panelSelectedNode$.next(node); + }; + + private findNodeById(id: string): ChromeProjectNavigationNode | null { + const allNodes = this.navigationTree$.getValue(); + if (!allNodes) return null; + + const find = (nodes: ChromeProjectNavigationNode[]): ChromeProjectNavigationNode | null => { + // Recursively search for the node with the given id + for (const node of nodes) { + if (node.id === id) { + return node; + } + if (node.children) { + const found = find(node.children); + if (found) { + return found; + } + } + } + return null; + }; + + return find(allNodes); + } + private get http() { if (!this._http) { throw new Error('Http service not provided.'); diff --git a/packages/core/chrome/core-chrome-browser-mocks/src/chrome_service.mock.ts b/packages/core/chrome/core-chrome-browser-mocks/src/chrome_service.mock.ts index 1380214e4311e..ff39293738ff0 100644 --- a/packages/core/chrome/core-chrome-browser-mocks/src/chrome_service.mock.ts +++ b/packages/core/chrome/core-chrome-browser-mocks/src/chrome_service.mock.ts @@ -54,6 +54,8 @@ const createStartContractMock = () => { sideNav: { getIsCollapsed$: jest.fn(), setIsCollapsed: jest.fn(), + getPanelSelectedNode$: jest.fn(), + setPanelSelectedNode: jest.fn(), }, getBreadcrumbsAppendExtension$: jest.fn(), setBreadcrumbsAppendExtension: jest.fn(), diff --git a/packages/core/chrome/core-chrome-browser/index.ts b/packages/core/chrome/core-chrome-browser/index.ts index 6be7f1077e6aa..4400c5e7d2b3f 100644 --- a/packages/core/chrome/core-chrome-browser/index.ts +++ b/packages/core/chrome/core-chrome-browser/index.ts @@ -51,6 +51,7 @@ export type { GroupDefinition, ItemDefinition, PresetDefinition, + PanelSelectedNode, RecentlyAccessedDefinition, NavigationGroupPreset, RootNavigationItemDefinition, diff --git a/packages/core/chrome/core-chrome-browser/src/contracts.ts b/packages/core/chrome/core-chrome-browser/src/contracts.ts index 1e9ea66bc0920..9fe54c971bbc7 100644 --- a/packages/core/chrome/core-chrome-browser/src/contracts.ts +++ b/packages/core/chrome/core-chrome-browser/src/contracts.ts @@ -16,6 +16,7 @@ import type { ChromeHelpExtension } from './help_extension'; import type { ChromeBreadcrumb, ChromeBreadcrumbsAppendExtension } from './breadcrumb'; import type { ChromeBadge, ChromeStyle, ChromeUserBanner } from './types'; import type { ChromeGlobalHelpExtensionMenuLink } from './help_extension'; +import type { PanelSelectedNode } from './project_navigation'; /** * ChromeStart allows plugins to customize the global chrome header UI and @@ -184,6 +185,20 @@ export interface ChromeStart { * @param isCollapsed The collapsed state of the side nav. */ setIsCollapsed(isCollapsed: boolean): void; + + /** + * Get an observable of the selected nav node that opens the side nav panel. + */ + getPanelSelectedNode$: () => Observable; + + /** + * Set the selected nav node that opens the side nav panel. + * + * @param node The selected nav node that opens the side nav panel. If a string is provided, + * it will be used as the **id** of the selected nav node. If `null` is provided, the side nav panel + * will be closed. + */ + setPanelSelectedNode(node: string | PanelSelectedNode | null): void; }; /** diff --git a/packages/core/chrome/core-chrome-browser/src/index.ts b/packages/core/chrome/core-chrome-browser/src/index.ts index ad041e8a3e297..7247bfe69710a 100644 --- a/packages/core/chrome/core-chrome-browser/src/index.ts +++ b/packages/core/chrome/core-chrome-browser/src/index.ts @@ -31,6 +31,7 @@ export type { ChromeBadge, ChromeUserBanner, ChromeStyle } from './types'; export type { ChromeProjectNavigationNode, + PanelSelectedNode, AppDeepLinkId, AppId, CloudLinkId, diff --git a/packages/core/chrome/core-chrome-browser/src/project_navigation.ts b/packages/core/chrome/core-chrome-browser/src/project_navigation.ts index 91f84d6c32145..417deea8e003e 100644 --- a/packages/core/chrome/core-chrome-browser/src/project_navigation.ts +++ b/packages/core/chrome/core-chrome-browser/src/project_navigation.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import type { ComponentType, MouseEventHandler } from 'react'; +import type { ComponentType, MouseEventHandler, ReactNode } from 'react'; import type { Location } from 'history'; import type { EuiSideNavItemType, EuiThemeSizes, IconType } from '@elastic/eui'; import type { Observable } from 'rxjs'; @@ -247,6 +247,13 @@ export interface ChromeProjectNavigationNode extends NodeDefinitionBase { isElasticInternalLink?: boolean; } +export type PanelSelectedNode = Pick< + ChromeProjectNavigationNode, + 'id' | 'children' | 'path' | 'sideNavStatus' | 'deepLink' +> & { + title: string | ReactNode; +}; + /** @public */ export interface SideNavCompProps { activeNodes: ChromeProjectNavigationNode[][]; diff --git a/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/model/extract_errors.test.ts b/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/model/extract_errors.test.ts index 5eb3947388d4f..01ade23e05db1 100644 --- a/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/model/extract_errors.test.ts +++ b/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/model/extract_errors.test.ts @@ -29,10 +29,7 @@ describe('extractUnknownDocFailureReason', () => { }, ]) ).toMatchInlineSnapshot(` - "Migration failed because some documents were found which use unknown saved object types: - - \\"unknownType:12\\" (type: \\"unknownType\\") - - \\"anotherUnknownType:42\\" (type: \\"anotherUnknownType\\") - + "Migration failed because some documents were found which use unknown saved object types: unknownType,anotherUnknownType To proceed with the migration you can configure Kibana to discard unknown saved objects for this migration. Please refer to some-url.co for more information." `); diff --git a/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/model/extract_errors.ts b/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/model/extract_errors.ts index edfdc98244985..0ed10e9e8f38a 100644 --- a/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/model/extract_errors.ts +++ b/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/model/extract_errors.ts @@ -52,10 +52,12 @@ export function extractUnknownDocFailureReason( resolveMigrationFailuresUrl: string, unknownDocs: DocumentIdAndType[] ): string { + const typesSet = new Set(unknownDocs.map(({ type }) => type)); return ( - `Migration failed because some documents were found which use unknown saved object types:\n` + - unknownDocs.map((doc) => `- "${doc.id}" (type: "${doc.type}")\n`).join('') + - `\nTo proceed with the migration you can configure Kibana to discard unknown saved objects for this migration.\n` + + `Migration failed because some documents were found which use unknown saved object types: ${Array.from( + typesSet.values() + )}\n` + + `To proceed with the migration you can configure Kibana to discard unknown saved objects for this migration.\n` + `Please refer to ${resolveMigrationFailuresUrl} for more information.` ); } diff --git a/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/model/model.ts b/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/model/model.ts index e619daa73cdae..14b171ac097da 100644 --- a/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/model/model.ts +++ b/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/model/model.ts @@ -559,13 +559,21 @@ export const model = (currentState: State, resW: ResponseType): deleteByQueryTaskId: res.right.taskId, }; } else { + const reason = extractUnknownDocFailureReason( + stateP.migrationDocLinks.resolveMigrationFailures, + res.left.unknownDocs + ); return { ...stateP, controlState: 'FATAL', - reason: extractUnknownDocFailureReason( - stateP.migrationDocLinks.resolveMigrationFailures, - res.left.unknownDocs - ), + reason, + logs: [ + ...logs, + { + level: 'error', + message: reason, + }, + ], }; } } else if (stateP.controlState === 'CLEANUP_UNKNOWN_AND_EXCLUDED_WAIT_FOR_TASK') { @@ -700,13 +708,22 @@ export const model = (currentState: State, resW: ResponseType): if (isTypeof(res.right, 'unknown_docs_found')) { if (!stateP.discardUnknownObjects) { + const reason = extractUnknownDocFailureReason( + stateP.migrationDocLinks.resolveMigrationFailures, + res.right.unknownDocs + ); + return { ...stateP, controlState: 'FATAL', - reason: extractUnknownDocFailureReason( - stateP.migrationDocLinks.resolveMigrationFailures, - res.right.unknownDocs - ), + reason, + logs: [ + ...logs, + { + level: 'error', + message: reason, + }, + ], }; } @@ -879,6 +896,13 @@ export const model = (currentState: State, resW: ResponseType): corruptDocumentIds: [], transformErrors: [], progress: createInitialProgress(), + logs: [ + ...logs, + { + level: 'info', + message: `REINDEX_SOURCE_TO_TEMP_OPEN_PIT PitId:${res.right.pitId}`, + }, + ], }; } else { throwBadResponse(stateP, res); diff --git a/packages/kbn-esql-editor/src/esql_editor.tsx b/packages/kbn-esql-editor/src/esql_editor.tsx index b4d169f278a0e..244abba93a5f9 100644 --- a/packages/kbn-esql-editor/src/esql_editor.tsx +++ b/packages/kbn-esql-editor/src/esql_editor.tsx @@ -719,6 +719,10 @@ export const ESQLEditor = memo(function ESQLEditor({ }); editor.onDidChangeModelContent(showSuggestionsIfEmptyQuery); + + // Auto-focus the editor and move the cursor to the end. + editor.focus(); + editor.setPosition({ column: Infinity, lineNumber: Infinity }); }} /> diff --git a/packages/shared-ux/chrome/navigation/src/services.tsx b/packages/shared-ux/chrome/navigation/src/services.tsx index 1b0102533e53e..26a8b61478ecb 100644 --- a/packages/shared-ux/chrome/navigation/src/services.tsx +++ b/packages/shared-ux/chrome/navigation/src/services.tsx @@ -37,6 +37,7 @@ export const NavigationKibanaProvider: FC ({ @@ -47,6 +48,8 @@ export const NavigationKibanaProvider: FC; isSideNavCollapsed: boolean; eventTracker: EventTracker; + selectedPanelNode?: PanelSelectedNode | null; + setSelectedPanelNode?: (node: PanelSelectedNode | null) => void; } /** @@ -55,6 +58,8 @@ export interface NavigationKibanaDependencies { }; sideNav: { getIsCollapsed$: () => Observable; + getPanelSelectedNode$: () => Observable; + setPanelSelectedNode(node: string | PanelSelectedNode | null): void; }; }; http: { diff --git a/packages/shared-ux/chrome/navigation/src/ui/components/panel/context.tsx b/packages/shared-ux/chrome/navigation/src/ui/components/panel/context.tsx index a5fee7c226fbd..917862b08b2b4 100644 --- a/packages/shared-ux/chrome/navigation/src/ui/components/panel/context.tsx +++ b/packages/shared-ux/chrome/navigation/src/ui/components/panel/context.tsx @@ -15,19 +15,20 @@ import React, { useMemo, useState, ReactNode, + useEffect, } from 'react'; -import type { ChromeProjectNavigationNode } from '@kbn/core-chrome-browser'; +import type { ChromeProjectNavigationNode, PanelSelectedNode } from '@kbn/core-chrome-browser'; import { DefaultContent } from './default_content'; -import { ContentProvider, PanelNavNode } from './types'; +import { ContentProvider } from './types'; export interface PanelContext { isOpen: boolean; toggle: () => void; - open: (navNode: PanelNavNode) => void; + open: (navNode: PanelSelectedNode) => void; close: () => void; /** The selected node is the node in the main panel that opens the Panel */ - selectedNode: PanelNavNode | null; + selectedNode: PanelSelectedNode | null; /** Handler to retrieve the component to render in the panel */ getContent: () => React.ReactNode; } @@ -37,29 +38,50 @@ const Context = React.createContext(null); interface Props { contentProvider?: ContentProvider; activeNodes: ChromeProjectNavigationNode[][]; + selectedNode?: PanelSelectedNode | null; + setSelectedNode?: (node: PanelSelectedNode | null) => void; } export const PanelProvider: FC> = ({ children, contentProvider, activeNodes, + selectedNode: selectedNodeProp = null, + setSelectedNode, }) => { const [isOpen, setIsOpen] = useState(false); - const [selectedNode, setActiveNode] = useState(null); + const [selectedNode, setActiveNode] = useState(selectedNodeProp); const toggle = useCallback(() => { setIsOpen((prev) => !prev); }, []); - const open = useCallback((navNode: PanelNavNode) => { - setActiveNode(navNode); - setIsOpen(true); - }, []); + const open = useCallback( + (navNode: PanelSelectedNode) => { + setActiveNode(navNode); + setIsOpen(true); + setSelectedNode?.(navNode); + }, + [setSelectedNode] + ); const close = useCallback(() => { setActiveNode(null); setIsOpen(false); - }, []); + setSelectedNode?.(null); + }, [setSelectedNode]); + + useEffect(() => { + if (selectedNodeProp === undefined) return; + + setActiveNode(selectedNodeProp); + + if (selectedNodeProp) { + setIsOpen(true); + } else { + setIsOpen(false); + } + }, [selectedNodeProp]); const getContent = useCallback(() => { if (!selectedNode) { diff --git a/packages/shared-ux/chrome/navigation/src/ui/components/panel/default_content.tsx b/packages/shared-ux/chrome/navigation/src/ui/components/panel/default_content.tsx index 7f18dd8358a8d..9892cc3a6f44a 100644 --- a/packages/shared-ux/chrome/navigation/src/ui/components/panel/default_content.tsx +++ b/packages/shared-ux/chrome/navigation/src/ui/components/panel/default_content.tsx @@ -8,12 +8,11 @@ */ import { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui'; -import { ChromeProjectNavigationNode } from '@kbn/core-chrome-browser'; +import type { ChromeProjectNavigationNode, PanelSelectedNode } from '@kbn/core-chrome-browser'; import React, { Fragment, type FC } from 'react'; import { PanelGroup } from './panel_group'; import { PanelNavItem } from './panel_nav_item'; -import type { PanelNavNode } from './types'; function isGroupNode({ children }: Pick) { return children !== undefined; @@ -33,7 +32,7 @@ function isItemNode({ children }: Pick) * @param node The current active node * @returns The children serialized */ -function serializeChildren(node: PanelNavNode): ChromeProjectNavigationNode[] | undefined { +function serializeChildren(node: PanelSelectedNode): ChromeProjectNavigationNode[] | undefined { if (!node.children) return undefined; const allChildrenAreItems = node.children.every((_node) => { @@ -69,7 +68,7 @@ function serializeChildren(node: PanelNavNode): ChromeProjectNavigationNode[] | interface Props { /** The selected node is the node in the main panel that opens the Panel */ - selectedNode: PanelNavNode; + selectedNode: PanelSelectedNode; } export const DefaultContent: FC = ({ selectedNode }) => { diff --git a/packages/shared-ux/chrome/navigation/src/ui/components/panel/navigation_panel.tsx b/packages/shared-ux/chrome/navigation/src/ui/components/panel/navigation_panel.tsx index 8e63e1179c955..5077cefc44625 100644 --- a/packages/shared-ux/chrome/navigation/src/ui/components/panel/navigation_panel.tsx +++ b/packages/shared-ux/chrome/navigation/src/ui/components/panel/navigation_panel.tsx @@ -18,11 +18,11 @@ import { import React, { useCallback, type FC } from 'react'; import classNames from 'classnames'; +import type { PanelSelectedNode } from '@kbn/core-chrome-browser'; import { usePanel } from './context'; import { getNavPanelStyles, getPanelWrapperStyles } from './styles'; -import { PanelNavNode } from './types'; -const getTestSubj = (selectedNode: PanelNavNode | null): string | undefined => { +const getTestSubj = (selectedNode: PanelSelectedNode | null): string | undefined => { if (!selectedNode) return; const deeplinkId = selectedNode.deepLink?.id; diff --git a/packages/shared-ux/chrome/navigation/src/ui/components/panel/types.ts b/packages/shared-ux/chrome/navigation/src/ui/components/panel/types.ts index 33af8c9e91ceb..8263e1f81c553 100644 --- a/packages/shared-ux/chrome/navigation/src/ui/components/panel/types.ts +++ b/packages/shared-ux/chrome/navigation/src/ui/components/panel/types.ts @@ -8,13 +8,13 @@ */ import type { ReactNode, ComponentType } from 'react'; -import type { ChromeProjectNavigationNode } from '@kbn/core-chrome-browser'; +import type { ChromeProjectNavigationNode, PanelSelectedNode } from '@kbn/core-chrome-browser'; export interface PanelComponentProps { /** Handler to close the panel */ closePanel: () => void; /** The node in the main panel that opens the secondary panel */ - selectedNode: PanelNavNode; + selectedNode: PanelSelectedNode; /** Jagged array of active nodes that match the current URL location */ activeNodes: ChromeProjectNavigationNode[][]; } @@ -25,10 +25,3 @@ export interface PanelContent { } export type ContentProvider = (nodeId: string) => PanelContent | void; - -export type PanelNavNode = Pick< - ChromeProjectNavigationNode, - 'id' | 'children' | 'path' | 'sideNavStatus' | 'deepLink' -> & { - title: string | ReactNode; -}; diff --git a/packages/shared-ux/chrome/navigation/src/ui/navigation.tsx b/packages/shared-ux/chrome/navigation/src/ui/navigation.tsx index b762ede8959da..3dacd01f8465f 100644 --- a/packages/shared-ux/chrome/navigation/src/ui/navigation.tsx +++ b/packages/shared-ux/chrome/navigation/src/ui/navigation.tsx @@ -47,7 +47,7 @@ export interface Props { } const NavigationComp: FC = ({ navigationTree$, dataTestSubj, panelContentProvider }) => { - const { activeNodes$ } = useNavigationService(); + const { activeNodes$, selectedPanelNode, setSelectedPanelNode } = useNavigationService(); const activeNodes = useObservable(activeNodes$, []); const navigationTree = useObservable(navigationTree$, { body: [] }); @@ -79,7 +79,12 @@ const NavigationComp: FC = ({ navigationTree$, dataTestSubj, panelContent ); return ( - + {/* Main navigation content */} diff --git a/src/core/server/integration_tests/saved_objects/migrations/archives/7.13.0_concurrent_5k_foo.zip b/src/core/server/integration_tests/saved_objects/migrations/archives/7.13.0_concurrent_5k_foo.zip index 46cc61cbe7b5f..d079624d95988 100644 Binary files a/src/core/server/integration_tests/saved_objects/migrations/archives/7.13.0_concurrent_5k_foo.zip and b/src/core/server/integration_tests/saved_objects/migrations/archives/7.13.0_concurrent_5k_foo.zip differ diff --git a/src/core/server/integration_tests/saved_objects/migrations/archives/7.13.0_with_unknown_so.zip b/src/core/server/integration_tests/saved_objects/migrations/archives/7.13.0_with_unknown_so.zip deleted file mode 100644 index 3312515024b91..0000000000000 Binary files a/src/core/server/integration_tests/saved_objects/migrations/archives/7.13.0_with_unknown_so.zip and /dev/null differ diff --git a/src/core/server/integration_tests/saved_objects/migrations/archives/7_13_corrupt_and_transform_failures_docs.zip b/src/core/server/integration_tests/saved_objects/migrations/archives/7_13_corrupt_and_transform_failures_docs.zip deleted file mode 100644 index b808bdc4f59ce..0000000000000 Binary files a/src/core/server/integration_tests/saved_objects/migrations/archives/7_13_corrupt_and_transform_failures_docs.zip and /dev/null differ diff --git a/src/core/server/integration_tests/saved_objects/migrations/archives/8.0.0_document_migration_failure.zip b/src/core/server/integration_tests/saved_objects/migrations/archives/8.0.0_document_migration_failure.zip deleted file mode 100644 index 9dc4de75c5d98..0000000000000 Binary files a/src/core/server/integration_tests/saved_objects/migrations/archives/8.0.0_document_migration_failure.zip and /dev/null differ diff --git a/src/core/server/integration_tests/saved_objects/migrations/archives/8.0.0_migrated_with_corrupt_outdated_docs.zip b/src/core/server/integration_tests/saved_objects/migrations/archives/8.0.0_migrated_with_corrupt_outdated_docs.zip deleted file mode 100644 index 726df7782cda3..0000000000000 Binary files a/src/core/server/integration_tests/saved_objects/migrations/archives/8.0.0_migrated_with_corrupt_outdated_docs.zip and /dev/null differ diff --git a/src/core/server/integration_tests/saved_objects/migrations/archives/8.0.0_migrated_with_outdated_docs.zip b/src/core/server/integration_tests/saved_objects/migrations/archives/8.0.0_migrated_with_outdated_docs.zip deleted file mode 100644 index 1fa9f061e15ff..0000000000000 Binary files a/src/core/server/integration_tests/saved_objects/migrations/archives/8.0.0_migrated_with_outdated_docs.zip and /dev/null differ diff --git a/src/core/server/integration_tests/saved_objects/migrations/group1/7_13_0_transform_failures.test.ts b/src/core/server/integration_tests/saved_objects/migrations/group1/7_13_0_transform_failures.test.ts deleted file mode 100644 index c186b4c0613ef..0000000000000 --- a/src/core/server/integration_tests/saved_objects/migrations/group1/7_13_0_transform_failures.test.ts +++ /dev/null @@ -1,279 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import Path from 'path'; -import Fs from 'fs'; -import Util from 'util'; -import { Env } from '@kbn/config'; -import { REPO_ROOT } from '@kbn/repo-info'; -import { getEnvOptions } from '@kbn/config-mocks'; -import type { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; -import { Root } from '@kbn/core-root-server-internal'; -import { SearchTotalHits } from '@elastic/elasticsearch/lib/api/types'; -import { getMigrationDocLink } from '../test_utils'; -import { - createRootWithCorePlugins, - TestElasticsearchUtils, - createTestServers as createkbnServerTestServers, -} from '@kbn/core-test-helpers-kbn-server'; -import { - MAIN_SAVED_OBJECT_INDEX, - TASK_MANAGER_SAVED_OBJECT_INDEX, - ANALYTICS_SAVED_OBJECT_INDEX, -} from '@kbn/core-saved-objects-server'; - -const migrationDocLink = getMigrationDocLink().resolveMigrationFailures; -const logFilePath = Path.join(__dirname, '7_13_corrupt_transform_failures.log'); - -const asyncUnlink = Util.promisify(Fs.unlink); - -async function removeLogFile() { - // ignore errors if it doesn't exist - await asyncUnlink(logFilePath).catch(() => void 0); -} - -// Failing 9.0 version update: https://github.com/elastic/kibana/issues/192624 -describe.skip('migration v2', () => { - let esServer: TestElasticsearchUtils; - let root: Root; - - beforeAll(async () => { - await removeLogFile(); - }); - - afterEach(async () => { - if (root) { - await root.shutdown(); - } - if (esServer) { - await esServer.stop(); - } - }); - - describe('when `migrations.discardCorruptObjects` does not match current kibana version', () => { - it('fails to migrate when corrupt objects and transform errors are encountered', async () => { - const { startES } = createTestServers(); - root = createRoot(); - esServer = await startES(); - await rootPrebootAndSetup(root); - - try { - await root.start(); - } catch (err) { - const errorMessage = err.message; - const errorLines = errorMessage.split('\n'); - const errorMessageWithoutStack = errorLines - .filter((line: string) => !line.includes(' at ')) - .join('\n'); - - expect(errorMessageWithoutStack).toMatchInlineSnapshot(` - "Unable to complete saved object migrations for the [.kibana] index: Migrations failed. Reason: 7 corrupt saved object documents were found: P2SQfHkBs3dBRGh--No5, QGSZfHkBs3dBRGh-ANoD, QWSZfHkBs3dBRGh-hNob, QmSZfHkBs3dBRGh-w9qH, one, two, Q2SZfHkBs3dBRGh-9dp2 - 7 transformation errors were encountered: - - space:default: Error: Migration function for version 7.14.0 threw an error - Caused by: - TypeError: Cannot set properties of undefined (setting 'bar') - - space:first: Error: Migration function for version 7.14.0 threw an error - Caused by: - TypeError: Cannot set properties of undefined (setting 'bar') - - space:forth: Error: Migration function for version 7.14.0 threw an error - Caused by: - TypeError: Cannot set properties of undefined (setting 'bar') - - space:second: Error: Migration function for version 7.14.0 threw an error - Caused by: - TypeError: Cannot set properties of undefined (setting 'bar') - - space:fifth: Error: Migration function for version 7.14.0 threw an error - Caused by: - TypeError: Cannot set properties of undefined (setting 'bar') - - space:third: Error: Migration function for version 7.14.0 threw an error - Caused by: - TypeError: Cannot set properties of undefined (setting 'bar') - - space:sixth: Error: Migration function for version 7.14.0 threw an error - Caused by: - TypeError: Cannot set properties of undefined (setting 'bar') - - To allow migrations to proceed, please delete or fix these documents. - Note that you can configure Kibana to automatically discard corrupt documents and transform errors for this migration. - Please refer to ${migrationDocLink} for more information." - `); - return; - } - // Fail test if above expression doesn't throw anything. - expect('to throw').toBe('but did not'); - }); - }); - - describe('when `migrations.discardCorruptObjects` matches current kibana version', () => { - it('proceeds with the migration, ignoring corrupt objects and transform errors', async () => { - const { startES } = createTestServers(); - const currentVersion = Env.createDefault(REPO_ROOT, getEnvOptions()).packageInfo.version; - root = createRoot(currentVersion); - esServer = await startES(); - await rootPrebootAndSetup(root); - - await expect(root.start()).resolves.not.toThrowError(); - // TODO check that the destination indices contain data, but NOT the conflicting objects - - const esClient: ElasticsearchClient = esServer.es.getClient(); - const docs = await esClient.search({ - index: [ - MAIN_SAVED_OBJECT_INDEX, - TASK_MANAGER_SAVED_OBJECT_INDEX, - ANALYTICS_SAVED_OBJECT_INDEX, - ], - _source: false, - fields: ['_id'], - size: 50, - }); - - // 34 saved objects (11 tasks + 23 misc) + 14 corrupt (discarded) = 48 total in the old indices - expect((docs.hits.total as SearchTotalHits).value).toEqual(34); - expect(docs.hits.hits.map(({ _id }) => _id).sort()).toEqual([ - 'config:7.13.0', - 'index-pattern:logs-*', - 'index-pattern:metrics-*', - 'task:Actions-actions_telemetry', - 'task:Actions-cleanup_failed_action_executions', - 'task:Alerting-alerting_health_check', - 'task:Alerting-alerting_telemetry', - 'task:Alerts-alerts_invalidate_api_keys', - 'task:Lens-lens_telemetry', - 'task:apm-telemetry-task', - 'task:data_enhanced_search_sessions_monitor', - 'task:endpoint:user-artifact-packager:1.0.0', - 'task:security:endpoint-diagnostics:1.0.0', - 'task:session_cleanup', - 'ui-metric:console:DELETE_delete', - 'ui-metric:console:GET_get', - 'ui-metric:console:GET_search', - 'ui-metric:console:POST_delete_by_query', - 'ui-metric:console:POST_index', - 'ui-metric:console:PUT_indices.put_mapping', - 'usage-counters:uiCounter:21052021:click:global_search_bar:user_navigated_to_application', - 'usage-counters:uiCounter:21052021:click:global_search_bar:user_navigated_to_application_unknown', - 'usage-counters:uiCounter:21052021:count:console:DELETE_delete', - 'usage-counters:uiCounter:21052021:count:console:GET_cat.aliases', - 'usage-counters:uiCounter:21052021:count:console:GET_cat.indices', - 'usage-counters:uiCounter:21052021:count:console:GET_get', - 'usage-counters:uiCounter:21052021:count:console:GET_search', - 'usage-counters:uiCounter:21052021:count:console:POST_delete_by_query', - 'usage-counters:uiCounter:21052021:count:console:POST_index', - 'usage-counters:uiCounter:21052021:count:console:PUT_indices.put_mapping', - 'usage-counters:uiCounter:21052021:count:global_search_bar:search_focus', - 'usage-counters:uiCounter:21052021:count:global_search_bar:search_request', - 'usage-counters:uiCounter:21052021:count:global_search_bar:shortcut_used', - 'usage-counters:uiCounter:21052021:loaded:console:opened_app', - ]); - }); - }); -}); - -function createTestServers() { - return createkbnServerTestServers({ - adjustTimeout: (t: number) => jest.setTimeout(t), - settings: { - es: { - license: 'basic', - // example of original 'foo' SO with corrupt id: - // _id: one - // { - // foo: { - // name: 'one', - // }, - // type: 'foo', - // references: [], - // migrationVersion: { - // foo: '7.13.0', - // }, - // "coreMigrationVersion": "7.13.0", - // "updated_at": "2021-05-16T18:16:45.450Z" - // }, - - // SO that will fail transformation: - // { - // type: 'space', - // space: {}, - // }, - // - // - dataArchive: Path.join( - __dirname, - '..', - 'archives', - '7_13_corrupt_and_transform_failures_docs.zip' - ), - }, - }, - }); -} - -function createRoot(discardCorruptObjects?: string) { - return createRootWithCorePlugins( - { - migrations: { - skip: false, - batchSize: 5, - discardCorruptObjects, - }, - logging: { - appenders: { - file: { - type: 'file', - fileName: logFilePath, - layout: { - type: 'json', - }, - }, - }, - loggers: [ - { - name: 'root', - level: 'info', - appenders: ['file'], - }, - ], - }, - }, - { - oss: true, - } - ); -} - -async function rootPrebootAndSetup(root: Root) { - await root.preboot(); - const coreSetup = await root.setup(); - - coreSetup.savedObjects.registerType({ - name: 'foo', - hidden: false, - mappings: { - properties: {}, - }, - namespaceType: 'agnostic', - migrations: { - '7.14.0': (doc) => doc, - }, - }); - - // registering the `space` type with a throwing migration fn to avoid the migration failing for unknown types - coreSetup.savedObjects.registerType({ - name: 'space', - hidden: false, - mappings: { - properties: {}, - }, - namespaceType: 'single', - migrations: { - '7.14.0': (doc) => { - doc.attributes.foo.bar = 12; - return doc; - }, - }, - }); -} diff --git a/src/core/server/integration_tests/saved_objects/migrations/group1/7_13_0_unknown_types.test.ts b/src/core/server/integration_tests/saved_objects/migrations/group1/7_13_0_unknown_types.test.ts deleted file mode 100644 index 25698f9b63f68..0000000000000 --- a/src/core/server/integration_tests/saved_objects/migrations/group1/7_13_0_unknown_types.test.ts +++ /dev/null @@ -1,166 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import Path from 'path'; -import fs from 'fs/promises'; -import type { IndicesIndexSettings } from '@elastic/elasticsearch/lib/api/types'; -import { Env } from '@kbn/config'; -import { REPO_ROOT } from '@kbn/repo-info'; -import { getEnvOptions } from '@kbn/config-mocks'; -import type { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; -import { Root } from '@kbn/core-root-server-internal'; -import { - createRootWithCorePlugins, - createTestServers, - type TestElasticsearchUtils, -} from '@kbn/core-test-helpers-kbn-server'; - -const logFilePath = Path.join(__dirname, '7_13_unknown_types.log'); - -async function removeLogFile() { - // ignore errors if it doesn't exist - await fs.unlink(logFilePath).catch(() => void 0); -} - -// Failing 9.0 version update: https://github.com/elastic/kibana/issues/192624 -describe.skip('migration v2', () => { - let esServer: TestElasticsearchUtils; - let root: Root; - let startES: () => Promise; - - beforeAll(async () => { - await removeLogFile(); - }); - - beforeEach(() => { - ({ startES } = createTestServers({ - adjustTimeout: (t: number) => jest.setTimeout(t), - settings: { - es: { - license: 'basic', - // dataset contains 2 type of unknown docs - // `foo` documents - // `space` documents (to mimic a migration with disabled plugins) - dataArchive: Path.join(__dirname, '..', 'archives', '7.13.0_with_unknown_so.zip'), - }, - }, - })); - }); - - afterEach(async () => { - if (root) { - await root.shutdown(); - } - if (esServer) { - await esServer.stop(); - } - }); - - describe('when `discardUnknownObjects` does not match current kibana version', () => { - it('fails the migration if unknown types are found in the source index', async () => { - // Start kibana with foo and space types disabled - root = createRoot('7.13.0'); - esServer = await startES(); - await root.preboot(); - await root.setup(); - - try { - await root.start(); - expect('should have thrown').toEqual('but it did not'); - } catch (err) { - const errorMessage = err.message; - - expect( - errorMessage.startsWith( - 'Unable to complete saved object migrations for the [.kibana] index: Migration failed because some documents ' + - 'were found which use unknown saved object types:' - ) - ).toBeTruthy(); - - const unknownDocs = [ - { type: 'space', id: 'space:default' }, - { type: 'space', id: 'space:first' }, - { type: 'space', id: 'space:second' }, - { type: 'space', id: 'space:third' }, - { type: 'space', id: 'space:forth' }, - { type: 'space', id: 'space:fifth' }, - { type: 'space', id: 'space:sixth' }, - { type: 'foo', id: 'P2SQfHkBs3dBRGh--No5' }, - { type: 'foo', id: 'QGSZfHkBs3dBRGh-ANoD' }, - { type: 'foo', id: 'QWSZfHkBs3dBRGh-hNob' }, - ]; - - unknownDocs.forEach(({ id, type }) => { - expect(errorMessage).toEqual(expect.stringContaining(`- "${id}" (type: "${type}")`)); - }); - - const client = esServer.es.getClient(); - const { body: response } = await client.indices.getSettings( - { index: '.kibana_7.13.0_001' }, - { meta: true } - ); - const settings = response['.kibana_7.13.0_001'].settings as IndicesIndexSettings; - expect(settings.index).not.toBeUndefined(); - expect(settings.index!.blocks?.write).not.toEqual('true'); - } - }); - }); - - describe('when `discardUnknownObjects` matches current kibana version', () => { - const currentVersion = Env.createDefault(REPO_ROOT, getEnvOptions()).packageInfo.version; - - it('discards the documents with unknown types and finishes the migration successfully', async () => { - // Start kibana with foo and space types disabled - root = createRoot(currentVersion); - esServer = await startES(); - await root.preboot(); - await root.setup(); - - // the migration process should finish successfully - await expect(root.start()).resolves.not.toThrowError(); - - const esClient: ElasticsearchClient = esServer.es.getClient(); - const body = await esClient.count({ q: 'type:foo|space' }); - expect(body.count).toEqual(0); - }); - }); -}); - -function createRoot(discardUnknownObjects?: string) { - return createRootWithCorePlugins( - { - migrations: { - skip: false, - batchSize: 5, - discardUnknownObjects, - }, - logging: { - appenders: { - file: { - type: 'file', - fileName: logFilePath, - layout: { - type: 'json', - }, - }, - }, - loggers: [ - { - name: 'root', - level: 'info', - appenders: ['file'], - }, - ], - }, - }, - { - oss: true, - } - ); -} diff --git a/src/core/server/integration_tests/saved_objects/migrations/group1/__snapshots__/v2_migration.test.ts.snap b/src/core/server/integration_tests/saved_objects/migrations/group1/__snapshots__/v2_migration.test.ts.snap new file mode 100644 index 0000000000000..1afb9e03cb6a2 --- /dev/null +++ b/src/core/server/integration_tests/saved_objects/migrations/group1/__snapshots__/v2_migration.test.ts.snap @@ -0,0 +1,504 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`v2 migration to a newer stack version with transform errors collects corrupt saved object documents across batches 1`] = ` +"Error: Cannot convert 'complex' objects with values that are multiple of 100 41e28595-9710-4583-aeb3-41c5899aa2ce +Error: Cannot convert 'complex' objects with values that are multiple of 100 a1f15bce-d9a2-4a4d-a280-c05f2eda99f1 +Error: Cannot convert 'complex' objects with values that are multiple of 100 06d3374e-193b-43d1-9c1a-0c8829cb0c51 +Error: Cannot convert 'complex' objects with values that are multiple of 100 b26f9630-7de4-45ec-a27c-c041b97fbe57 +Error: Cannot convert 'complex' objects with values that are multiple of 100 21c18cf5-825a-4847-a2e2-5c670033a560 +Error: Cannot convert 'complex' objects with values that are multiple of 100 cd75cc1c-e915-4573-b353-f3f2ee01cf47 +Error: Cannot convert 'complex' objects with values that are multiple of 100 8bee00b6-36dd-46e7-b055-f366cc37df48 +Error: Cannot convert 'complex' objects with values that are multiple of 100 2e675eed-4f4e-4151-99fd-83a32b5f8e79 +Error: Cannot convert 'complex' objects with values that are multiple of 100 c3de3e3c-b38c-4264-a6f6-e59eff793980 +Error: Cannot convert 'complex' objects with values that are multiple of 100 da9b73a8-b9f9-4073-a9bb-c853243c2dd8 +Error: Cannot convert 'complex' objects with values that are multiple of 100 6d3de4cc-2c2c-4b28-b8dc-4d2f607ae4f2 +Error: Cannot convert 'complex' objects with values that are multiple of 100 31fe1208-4702-495f-95c9-d3f948576791 +Error: Cannot convert 'complex' objects with values that are multiple of 100 e1e75224-dfb4-465e-9688-093ef4a663e6 +Error: Cannot convert 'complex' objects with values that are multiple of 100 d30d55f5-5513-436b-b0c1-2c2fc9dffcc3 +Error: Cannot convert 'complex' objects with values that are multiple of 100 8a5cd239-c02c-4370-abb5-f084d7f2d418 +Error: Cannot convert 'complex' objects with values that are multiple of 100 5eeded25-9d6c-45bd-9d7d-33657c67e73d +Error: Cannot convert 'complex' objects with values that are multiple of 100 90849a74-a3cf-4587-8ba8-d0c9480015c6 +Error: Cannot convert 'complex' objects with values that are multiple of 100 94f659fb-73d9-49a7-8a82-50e67c259ddb +Error: Cannot convert 'complex' objects with values that are multiple of 100 6548dc97-0f55-462b-a4d2-dc0bceb7fbdd +Error: Cannot convert 'complex' objects with values that are multiple of 100 e6b6316d-c839-4b75-9abb-18da0d0d3f31 +Error: Cannot convert 'complex' objects with values that are multiple of 100 1dadcae8-a0f7-49eb-bbf0-81fcc81061d7 +Error: Cannot convert 'complex' objects with values that are multiple of 100 1d1391ac-7564-4a97-8b65-f3c50bf56615 +Error: Cannot convert 'complex' objects with values that are multiple of 100 cbedd055-0619-46f4-ae2c-68fff60eef45 +Error: Cannot convert 'complex' objects with values that are multiple of 100 9907c207-e47f-4a1e-91a8-173915eefd8b +Error: Cannot convert 'complex' objects with values that are multiple of 100 cd7fdcdc-3d77-4b43-9bb9-41b715ba41a4 +Error: Cannot convert 'complex' objects with values that are multiple of 100 c144775d-b0d6-42c1-ad0d-7a4350687c85 +Error: Cannot convert 'complex' objects with values that are multiple of 100 306a4982-da6b-463f-b157-9dc33579cd28 +Error: Cannot convert 'complex' objects with values that are multiple of 100 df6c1595-68ef-41f6-b649-30952d586195 +Error: Cannot convert 'complex' objects with values that are multiple of 100 5d3e825d-3db7-4a04-be91-b1b92e00386a +Error: Cannot convert 'complex' objects with values that are multiple of 100 d257437f-529b-482d-a981-2f26bd79388f +Error: Cannot convert 'complex' objects with values that are multiple of 100 dac6c6c6-d329-4dd5-acd1-b2585917d736 +Error: Cannot convert 'complex' objects with values that are multiple of 100 e415176e-b3cd-41d1-9ba5-06c702a13cb3 +Error: Cannot convert 'complex' objects with values that are multiple of 100 64163b20-da0b-482b-90d2-347e73e8c068 +Error: Cannot convert 'complex' objects with values that are multiple of 100 f195b8b9-c1a4-4724-b861-2a533a58e6b4 +Error: Cannot convert 'complex' objects with values that are multiple of 100 d53b617d-94b7-4701-99a5-a194c6b34e0c +Error: Cannot convert 'complex' objects with values that are multiple of 100 e350a554-2086-4d36-a7ea-3898befc1792 +Error: Cannot convert 'complex' objects with values that are multiple of 100 2de91b3b-f69f-4e64-8d79-b432aedd46ec +Error: Cannot convert 'complex' objects with values that are multiple of 100 9cfc58ad-94fa-4b6c-b776-3b93942fdabc +Error: Cannot convert 'complex' objects with values that are multiple of 100 efcbc446-c869-4837-861c-20c787a50872 +Error: Cannot convert 'complex' objects with values that are multiple of 100 70e0b310-e8b6-4fcf-8a1d-fb9e28b0d1ed +Error: Cannot convert 'complex' objects with values that are multiple of 100 df92c6c1-7405-4fe2-849b-e1aec71f44d0 +Error: Cannot convert 'complex' objects with values that are multiple of 100 9b3839aa-5cad-4b30-ae0e-18340b566535 +Error: Cannot convert 'complex' objects with values that are multiple of 100 7cbad964-9f31-4a5c-87d7-734bdf08b42a +Error: Cannot convert 'complex' objects with values that are multiple of 100 5ffc4d92-df45-4151-b6e9-71e18dc67070 +Error: Cannot convert 'complex' objects with values that are multiple of 100 fb3d87d6-ba7b-41d2-ac79-e1e6f402a19c +Error: Cannot convert 'complex' objects with values that are multiple of 100 2e14fca1-ad13-410c-a6dd-969a3793aa78 +Error: Cannot convert 'complex' objects with values that are multiple of 100 691efa7f-9177-4971-996a-c6f6c1218046 +Error: Cannot convert 'complex' objects with values that are multiple of 100 34c47c79-369e-456c-b1af-286e247929cd +Error: Cannot convert 'complex' objects with values that are multiple of 100 add9a30d-8050-4370-bd44-237b5cce5d93 +Error: Cannot convert 'complex' objects with values that are multiple of 100 f0423b5a-56a3-40a3-97ec-f30ad0187e63 +Error: Cannot convert 'complex' objects with values that are multiple of 100 8b25b058-5931-4973-aa86-8dbbd91e4bd1 +Error: Cannot convert 'complex' objects with values that are multiple of 100 097d4e9f-afa6-4db0-a35b-183051116ed5 +Error: Cannot convert 'complex' objects with values that are multiple of 100 d7b3edcb-ae7d-4df8-ae38-e627aaeb46be +Error: Cannot convert 'complex' objects with values that are multiple of 100 344a9dee-5de8-4e50-af66-374c6b7675a7 +Error: Cannot convert 'complex' objects with values that are multiple of 100 b2c6c79e-6d9f-42a0-b4ab-91fb321b021f +Error: Cannot convert 'complex' objects with values that are multiple of 100 09452338-c116-4d86-b59a-be071b0cd689 +Error: Cannot convert 'complex' objects with values that are multiple of 100 f0ee199a-e921-484f-b551-f4aec202ea27 +Error: Cannot convert 'complex' objects with values that are multiple of 100 3d12c404-4dfc-40d9-9795-44f6292ce5ae +Error: Cannot convert 'complex' objects with values that are multiple of 100 31ca239d-a3cf-43c0-8ef3-a1d37c7e311f +Error: Cannot convert 'complex' objects with values that are multiple of 100 6f45a809-e866-4b7d-812e-21e9180133fe +Error: Cannot convert 'complex' objects with values that are multiple of 100 9ff098a5-0738-4f8c-b595-0dd615e5ce2b +Error: Cannot convert 'complex' objects with values that are multiple of 100 611156d0-d452-4582-8ef1-4b9f68ce40de +Error: Cannot convert 'complex' objects with values that are multiple of 100 6e55ba50-6717-4250-8c5e-0d34e608cf55 +Error: Cannot convert 'complex' objects with values that are multiple of 100 f204be38-103a-40cc-b599-9f9a6dc2b676 +Error: Cannot convert 'complex' objects with values that are multiple of 100 8e54c8e7-80f1-4a27-b96b-f36b5279b154 +Error: Cannot convert 'complex' objects with values that are multiple of 100 d33b9b61-4bb2-4935-a71f-7194918f86ab +Error: Cannot convert 'complex' objects with values that are multiple of 100 46042cbf-2da6-4ff3-8dec-b92f1e53910f +Error: Cannot convert 'complex' objects with values that are multiple of 100 004c3a5d-caa0-474e-a631-a9bee6fc432a +Error: Cannot convert 'complex' objects with values that are multiple of 100 a744e441-7653-4486-b39d-bef59af32c46 +Error: Cannot convert 'complex' objects with values that are multiple of 100 d9e2ebda-2370-4660-aa84-311a9a56ccc3 +Error: Cannot convert 'complex' objects with values that are multiple of 100 4e57d594-45ba-4b8e-a2fa-814a0fc2db0b +Error: Cannot convert 'complex' objects with values that are multiple of 100 228fe220-b046-48c3-b0dd-bfe3cc15b3af +Error: Cannot convert 'complex' objects with values that are multiple of 100 66afe76d-ab04-4381-aaf6-5a07b7043300 +Error: Cannot convert 'complex' objects with values that are multiple of 100 f1f48caa-5e33-49b3-8c8e-f9e47cc46553 +Error: Cannot convert 'complex' objects with values that are multiple of 100 6d1de2f4-6995-43e3-86e9-34da77dfb160 +Error: Cannot convert 'complex' objects with values that are multiple of 100 56911ce1-c027-4ef3-81fa-c8e99ee3c6bd +Error: Cannot convert 'complex' objects with values that are multiple of 100 a230e01b-356f-48c8-9aa9-3360c15bce9b +Error: Cannot convert 'complex' objects with values that are multiple of 100 2f0e1193-0f35-48f7-8b28-4bf142984a4e +Error: Cannot convert 'complex' objects with values that are multiple of 100 1168bfdc-0b3d-43df-8fd4-b2f111509710 +Error: Cannot convert 'complex' objects with values that are multiple of 100 752d393a-61e6-4d3d-b3cb-b25c976f5dc9 +Error: Cannot convert 'complex' objects with values that are multiple of 100 2afef4d0-5fb2-4dbc-8edc-cec127ba7566 +Error: Cannot convert 'complex' objects with values that are multiple of 100 bea60590-a57c-495b-be43-d29720cd1995 +Error: Cannot convert 'complex' objects with values that are multiple of 100 511c29d5-c8c6-4cfe-b9c5-f7e3272cfddd +Error: Cannot convert 'complex' objects with values that are multiple of 100 c4e2e677-0165-49d1-9ee9-bb102a4446e8 +Error: Cannot convert 'complex' objects with values that are multiple of 100 eb0e6312-35df-4f4b-8ede-9f4ff94f323d +Error: Cannot convert 'complex' objects with values that are multiple of 100 a86f12f2-a3cd-48e9-b5d8-37a67994149b +Error: Cannot convert 'complex' objects with values that are multiple of 100 26c96126-4450-4096-afe7-126b0a6da7ee +Error: Cannot convert 'complex' objects with values that are multiple of 100 d2f4a4e9-34bd-4ac8-bccf-96ccd7cfec38 +Error: Cannot convert 'complex' objects with values that are multiple of 100 864a2090-6d3a-459d-9c53-fbf30433e5d2 +Error: Cannot convert 'complex' objects with values that are multiple of 100 e15dcebb-733f-44d1-97dd-90a176770d4b +Error: Cannot convert 'complex' objects with values that are multiple of 100 cf3dc9c8-a377-41fa-84db-71c7a1669655 +Error: Cannot convert 'complex' objects with values that are multiple of 100 9764f40e-ade5-4623-9b77-f2e6286458c3 +Error: Cannot convert 'complex' objects with values that are multiple of 100 57bec873-0fa6-40f0-bb6d-73479aa12f12 +Error: Cannot convert 'complex' objects with values that are multiple of 100 b49cf0b4-072a-4e01-8099-e95783073d5c +Error: Cannot convert 'complex' objects with values that are multiple of 100 39067bbd-ed2b-4788-be3b-4df0041cc009 +Error: Cannot convert 'complex' objects with values that are multiple of 100 0916d31e-5b51-4acd-9735-75bdad4aaec8 +Error: Cannot convert 'complex' objects with values that are multiple of 100 13660507-d9e4-4dd8-a464-2142a27b4793 +Error: Cannot convert 'complex' objects with values that are multiple of 100 d06b9296-5bcf-41df-a0f3-b556f289f06a +Error: Cannot convert 'complex' objects with values that are multiple of 100 e7cf5995-4bb0-4d3a-8165-66be5d9854fc +Error: Cannot convert 'complex' objects with values that are multiple of 100 b8d7dc9f-0bb2-43e7-872e-a0098080470b +Error: Cannot convert 'complex' objects with values that are multiple of 100 2adf568c-1055-4013-bf2c-faa7e433700f +Error: Cannot convert 'complex' objects with values that are multiple of 100 764d554e-413e-4cd3-aeb5-366bf11ca769 +Error: Cannot convert 'complex' objects with values that are multiple of 100 b4677231-92c0-4f0e-8dad-3e2a6d09fb44 +Error: Cannot convert 'complex' objects with values that are multiple of 100 a2f0f8ea-0ccd-4bf3-bde6-8572018f571a +Error: Cannot convert 'complex' objects with values that are multiple of 100 44e15623-8b56-4907-96dd-64fbc78c9262 +Error: Cannot convert 'complex' objects with values that are multiple of 100 1f14b977-c54b-4eef-a48f-573055169eb1 +Error: Cannot convert 'complex' objects with values that are multiple of 100 5fa91020-c284-4a04-8aa3-b198dde8cf14 +Error: Cannot convert 'complex' objects with values that are multiple of 100 f12ddc96-95ff-45a2-8872-847828ce41d0 +Error: Cannot convert 'complex' objects with values that are multiple of 100 983d0fd8-90c7-4ade-ac03-4532b9ccf4d3 +Error: Cannot convert 'complex' objects with values that are multiple of 100 8d44f665-d001-4e45-8019-809c32b4dfd7 +Error: Cannot convert 'complex' objects with values that are multiple of 100 00c94e57-87fb-4004-812a-2f30e6465b28 +Error: Cannot convert 'complex' objects with values that are multiple of 100 59bc74cf-26df-4d25-bc7e-a720eb946667 +Error: Cannot convert 'complex' objects with values that are multiple of 100 efe7b66d-feae-4938-aced-1f0818c91e2c +Error: Cannot convert 'complex' objects with values that are multiple of 100 10e68507-5e58-4a43-a16d-3830394341aa +Error: Cannot convert 'complex' objects with values that are multiple of 100 dbe48539-bb49-41b5-93b9-3d05158fa20b +Error: Cannot convert 'complex' objects with values that are multiple of 100 ca0e995a-4189-42ab-a082-f342705470b5 +Error: Cannot convert 'complex' objects with values that are multiple of 100 966811b4-c0ee-4244-98cc-66b2f31855cd +Error: Cannot convert 'complex' objects with values that are multiple of 100 fa6918f8-b132-4720-8828-ded1777cb7d3 +Error: Cannot convert 'complex' objects with values that are multiple of 100 7ca309ac-fc0a-4f5c-90fe-b57bee2e1cc2 +Error: Cannot convert 'complex' objects with values that are multiple of 100 d3d9abce-39b2-42a9-a0b4-bb1b0d7179cb +Error: Cannot convert 'complex' objects with values that are multiple of 100 2a540aa3-f0e1-4d20-a623-c05c595e7cd3 +Error: Cannot convert 'complex' objects with values that are multiple of 100 986eba02-ca59-4f0d-92ff-62fc277aa87c +Error: Cannot convert 'complex' objects with values that are multiple of 100 11142ec5-ff29-4577-8936-56f69c2f4dbd +Error: Cannot convert 'complex' objects with values that are multiple of 100 15cffaae-7c1b-40b5-92fc-9e5e0b42174a +Error: Cannot convert 'complex' objects with values that are multiple of 100 8d2c15ec-5caa-44df-be9a-cd8c3a0039fd +Error: Cannot convert 'complex' objects with values that are multiple of 100 82025688-7cc1-4a1b-ba26-fba279c0a559 +Error: Cannot convert 'complex' objects with values that are multiple of 100 548d80cd-6a87-48aa-8fcb-05af29dde400 +Error: Cannot convert 'complex' objects with values that are multiple of 100 3ebe2353-d7e7-44e5-b5c9-92cbc5ccb239 +Error: Cannot convert 'complex' objects with values that are multiple of 100 189e1010-10e9-4a25-9eef-14760fe80cfa +Error: Cannot convert 'complex' objects with values that are multiple of 100 9b88d281-e9d7-494a-85f6-fa8c0a3f236b +Error: Cannot convert 'complex' objects with values that are multiple of 100 19e7a4de-012f-4d69-be2f-bd09f9c26350 +Error: Cannot convert 'complex' objects with values that are multiple of 100 734c74d5-e70a-4776-87e1-51bdf5577f3c +Error: Cannot convert 'complex' objects with values that are multiple of 100 164ca6f1-2cbe-4440-8652-8515df19075f +Error: Cannot convert 'complex' objects with values that are multiple of 100 cc288178-bfc9-428c-902f-5fbefb4f1958 +Error: Cannot convert 'complex' objects with values that are multiple of 100 ae2b9311-4084-438d-824b-f83ad45b35eb +Error: Cannot convert 'complex' objects with values that are multiple of 100 8e8bfb88-f4c1-4929-8b45-4b9a8b912ae8 +Error: Cannot convert 'complex' objects with values that are multiple of 100 8254f416-603b-4e71-8a10-3df84be64e7b +Error: Cannot convert 'complex' objects with values that are multiple of 100 15facbbc-6934-4c7d-8278-68a177bc1d40 +Error: Cannot convert 'complex' objects with values that are multiple of 100 fadf9362-aea6-47e1-94da-39da2f59dc6d +Error: Cannot convert 'complex' objects with values that are multiple of 100 a193a83c-fd64-4197-aae8-91f5fc23844e +Error: Cannot convert 'complex' objects with values that are multiple of 100 6ebf1dbe-de76-4539-8af8-7b12e47f5ce2 +Error: Cannot convert 'complex' objects with values that are multiple of 100 27f5606b-234a-4dc0-9e40-75cd74fe32c6 +Error: Cannot convert 'complex' objects with values that are multiple of 100 1b06e644-bec5-4723-b040-490ea6110fb7 +Error: Cannot convert 'complex' objects with values that are multiple of 100 c81ec835-94a9-4b84-9c55-98bfd27fc3bc +Error: Cannot convert 'complex' objects with values that are multiple of 100 a5e1812e-dbdb-4400-80b6-e7b70e058887 +Error: Cannot convert 'complex' objects with values that are multiple of 100 99a76670-22fd-4365-bfa8-15ee48761260 +Error: Cannot convert 'complex' objects with values that are multiple of 100 7eeb5f71-ac2f-4fd8-99cd-8b987bcbf70c +Error: Cannot convert 'complex' objects with values that are multiple of 100 aefdb49d-511c-4fa2-aab5-56a89428f260 +Error: Cannot convert 'complex' objects with values that are multiple of 100 cb0e6562-5e52-4ab2-adec-97be213f3dae +Error: Cannot convert 'complex' objects with values that are multiple of 100 c292e57b-59d5-47b2-a52f-753fddb7f444 +Error: Cannot convert 'complex' objects with values that are multiple of 100 658fc86d-0406-433c-a29c-01de07173c9f +Error: Cannot convert 'complex' objects with values that are multiple of 100 4df986b0-01a7-4ca0-84b7-e872168da048 +Error: Cannot convert 'complex' objects with values that are multiple of 100 9097e835-d1ae-4c88-9e29-63ed0160b127 +Error: Cannot convert 'complex' objects with values that are multiple of 100 44a33056-b110-4114-9073-a50498e2564b +Error: Cannot convert 'complex' objects with values that are multiple of 100 b001f6bd-5435-4da0-9632-c269768c243c +Error: Cannot convert 'complex' objects with values that are multiple of 100 7bf727c5-58d7-4860-a496-e05c76d5fca8 +Error: Cannot convert 'complex' objects with values that are multiple of 100 b7fbaa4a-b30b-4fe0-b2de-2baddaff4dd5 +Error: Cannot convert 'complex' objects with values that are multiple of 100 1233fb9c-bc84-4997-b980-724163cd9c99 +Error: Cannot convert 'complex' objects with values that are multiple of 100 0cc4ea6c-9882-4c38-99a3-f0e61136cf97 +Error: Cannot convert 'complex' objects with values that are multiple of 100 87623762-5557-4d50-874b-e783224d2d68 +Error: Cannot convert 'complex' objects with values that are multiple of 100 5c92c79e-6c4a-4482-8f88-7e5663c194f2 +Error: Cannot convert 'complex' objects with values that are multiple of 100 a1191ea3-3e5f-46de-b764-6a81182dab19 +Error: Cannot convert 'complex' objects with values that are multiple of 100 b9159be4-5cb2-4f25-a089-85fcd9b03b33 +Error: Cannot convert 'complex' objects with values that are multiple of 100 eb14b9ab-004d-4e6e-b254-02e8af9a351e +Error: Cannot convert 'complex' objects with values that are multiple of 100 bff8783a-a2d6-4424-8119-a16cf72bb52a +Error: Cannot convert 'complex' objects with values that are multiple of 100 28509c5a-462b-483f-883a-be26b4f462f4 +Error: Cannot convert 'complex' objects with values that are multiple of 100 32cca2bf-0c3a-4d40-baea-44833532aeb9 +Error: Cannot convert 'complex' objects with values that are multiple of 100 ebb778ab-992a-4438-919d-4e8d627c2058 +Error: Cannot convert 'complex' objects with values that are multiple of 100 81bc0a2a-ea4d-4944-b2c9-12e4b3bbee61 +Error: Cannot convert 'complex' objects with values that are multiple of 100 7a257c3e-90af-483c-9ed3-737bb77560eb +Error: Cannot convert 'complex' objects with values that are multiple of 100 f2ae2465-949d-4cc9-bdda-89ff964adc0f +Error: Cannot convert 'complex' objects with values that are multiple of 100 f9394e2f-6652-4161-9bd7-fbcfc218b942 +Error: Cannot convert 'complex' objects with values that are multiple of 100 61b99930-5dc3-4e7f-9a61-6e22fcfa5507 +Error: Cannot convert 'complex' objects with values that are multiple of 100 a54b457e-0164-4773-84d9-2bc49bf335a2 +Error: Cannot convert 'complex' objects with values that are multiple of 100 b01dcfa3-b6fc-42e3-8682-a5a3349d22d5 +Error: Cannot convert 'complex' objects with values that are multiple of 100 ce4b8596-3ae9-4f2a-9a9e-ceb96fbf0171 +Error: Cannot convert 'complex' objects with values that are multiple of 100 fea29815-35c9-494f-9912-4718d4c65d93 +Error: Cannot convert 'complex' objects with values that are multiple of 100 c4e210ed-5d5e-4329-867b-565de7ec14c2 +Error: Cannot convert 'complex' objects with values that are multiple of 100 e49e182c-44c9-442b-92ef-fc83433f7e50 +Error: Cannot convert 'complex' objects with values that are multiple of 100 7faac5b2-c36b-4359-bb24-367b77815592 +Error: Cannot convert 'complex' objects with values that are multiple of 100 27fe1231-fed2-41a1-859d-503134f5cc83 +Error: Cannot convert 'complex' objects with values that are multiple of 100 762d235a-1b99-46ed-ad7f-b9e28eea5822 +Error: Cannot convert 'complex' objects with values that are multiple of 100 b5b9992a-bcaa-4b9a-8754-29a60068ac6a +Error: Cannot convert 'complex' objects with values that are multiple of 100 f48be8be-0f44-4f2f-920d-d75ca5e970ae +Error: Cannot convert 'complex' objects with values that are multiple of 100 1369a5fb-8312-4bba-ab79-ea71a9fcd036 +Error: Cannot convert 'complex' objects with values that are multiple of 100 d11c8331-a724-4d85-90b8-aa084d8b258d +Error: Cannot convert 'complex' objects with values that are multiple of 100 ea9019d5-26d0-4477-8c9f-172dfc107f25 +Error: Cannot convert 'complex' objects with values that are multiple of 100 725ee684-9dbf-4ff8-8b8d-b8e2172500b8 +Error: Cannot convert 'complex' objects with values that are multiple of 100 04001900-1655-4f6e-86b8-bbd88504d50f +Error: Cannot convert 'complex' objects with values that are multiple of 100 37bebf97-9ce2-4589-a804-ff69527bafba +Error: Cannot convert 'complex' objects with values that are multiple of 100 cff8daf0-c177-49a6-961b-2cc32217af7b +Error: Cannot convert 'complex' objects with values that are multiple of 100 bbb15e49-0126-4e2d-9518-1c5c2b227263 +Error: Cannot convert 'complex' objects with values that are multiple of 100 b7664d87-c87d-4e06-94d2-c51d904c92fd +Error: Cannot convert 'complex' objects with values that are multiple of 100 534a357a-1f04-4243-aa89-c70879656c66 +Error: Cannot convert 'complex' objects with values that are multiple of 100 0393aa29-9cee-4168-b802-6519c004f49a +Error: Cannot convert 'complex' objects with values that are multiple of 100 1975467d-7832-47e5-a5be-cb24ea2cd405 +Error: Cannot convert 'complex' objects with values that are multiple of 100 27b07229-9c16-43d3-985e-af00336c1970 +Error: Cannot convert 'complex' objects with values that are multiple of 100 e382b6bd-c9a7-4f00-bf5d-b1a829c1486a +Error: Cannot convert 'complex' objects with values that are multiple of 100 8ce42577-57b7-4c3c-9d12-bf09899ec75a +Error: Cannot convert 'complex' objects with values that are multiple of 100 71ee1840-416e-4a24-901b-ac009814738f +Error: Cannot convert 'complex' objects with values that are multiple of 100 286fc315-4643-48ef-b0bb-f18ad22888a0 +Error: Cannot convert 'complex' objects with values that are multiple of 100 68bba5c0-23d5-41ef-b6f5-c094c152427d +Error: Cannot convert 'complex' objects with values that are multiple of 100 7272613f-ee42-4761-89e5-ed4eb10203e2 +Error: Cannot convert 'complex' objects with values that are multiple of 100 8d5aec0b-0dd1-4a82-a77c-d35d4db1daf7 +Error: Cannot convert 'complex' objects with values that are multiple of 100 24532830-c3ec-4d6b-8d98-59c38e6255c6 +Error: Cannot convert 'complex' objects with values that are multiple of 100 a203d1f2-71bc-4eb1-b479-f650fff57e91 +Error: Cannot convert 'complex' objects with values that are multiple of 100 e3abbd23-7f6b-4c19-a8c9-1faa4ec84cef +Error: Cannot convert 'complex' objects with values that are multiple of 100 229cd51f-526c-44b0-bb96-6aab38f58a4c +Error: Cannot convert 'complex' objects with values that are multiple of 100 e96389f1-03ff-471f-b507-bd666c5ea5cd +Error: Cannot convert 'complex' objects with values that are multiple of 100 b9a71b4f-37e6-4a73-975f-5b0c0fba5327 +Error: Cannot convert 'complex' objects with values that are multiple of 100 231b2ab5-c0bf-4d62-8d30-b83fb55230f1 +Error: Cannot convert 'complex' objects with values that are multiple of 100 d1786358-b15d-4e74-8cb1-131d2344bd37 +Error: Cannot convert 'complex' objects with values that are multiple of 100 af4cecfa-6005-4564-812e-61228f66dd62 +Error: Cannot convert 'complex' objects with values that are multiple of 100 ee1bedce-e0db-4a2f-be29-6301e6d136d1 +Error: Cannot convert 'complex' objects with values that are multiple of 100 c5f0a020-e588-4fb1-9b1c-57dca2644408 +Error: Cannot convert 'complex' objects with values that are multiple of 100 3969b1f7-3374-402b-a1cc-13ae0b85227a +Error: Cannot convert 'complex' objects with values that are multiple of 100 1db8da28-cb63-4a24-9010-3010d2ab15d2 +Error: Cannot convert 'complex' objects with values that are multiple of 100 fee08b75-21dc-4b18-9ef5-8a327798dc60 +Error: Cannot convert 'complex' objects with values that are multiple of 100 f728f4c9-bdee-42aa-a0bb-d9299d344fa5 +Error: Cannot convert 'complex' objects with values that are multiple of 100 231ffa66-55f8-4534-a1d1-738cd4462dfd +Error: Cannot convert 'complex' objects with values that are multiple of 100 2bdef19b-3373-48d9-a41c-87b98afd9651 +Error: Cannot convert 'complex' objects with values that are multiple of 100 2d90f0ae-4e99-4460-b8a6-c396841ff476 +Error: Cannot convert 'complex' objects with values that are multiple of 100 afd1c4ce-110d-4327-9910-04b1f9b83bc4 +Error: Cannot convert 'complex' objects with values that are multiple of 100 b01d65b1-38ec-4fc0-bbd1-416f4b941b17 +Error: Cannot convert 'complex' objects with values that are multiple of 100 341c61e2-e62c-4ff4-b761-2b98ac8188fc +Error: Cannot convert 'complex' objects with values that are multiple of 100 6996cad7-588b-4176-bd81-85d4d75fa1c5 +Error: Cannot convert 'complex' objects with values that are multiple of 100 6e7bd135-84a5-4ba2-805b-5bcb271e2a95 +Error: Cannot convert 'complex' objects with values that are multiple of 100 4564b434-4925-4745-b9a5-0f77c4f191e3 +Error: Cannot convert 'complex' objects with values that are multiple of 100 b16e3d8d-5c80-489c-9fd4-f88745d259b6 +Error: Cannot convert 'complex' objects with values that are multiple of 100 865d6ee7-dca7-47db-afc8-94b64a86b48d +Error: Cannot convert 'complex' objects with values that are multiple of 100 608e9069-0b56-45ea-8f0d-1a953d8cdc48 +Error: Cannot convert 'complex' objects with values that are multiple of 100 9e29e291-753d-46e7-af8a-5a75839cd1f6 +Error: Cannot convert 'complex' objects with values that are multiple of 100 e0d13e93-7834-42d7-9381-ff2c80c787c3 +Error: Cannot convert 'complex' objects with values that are multiple of 100 fe800046-61e8-495c-923e-85ed71aed4fd +Error: Cannot convert 'complex' objects with values that are multiple of 100 2db7c305-6ae1-462d-8c78-e862a54e50d2 +Error: Cannot convert 'complex' objects with values that are multiple of 100 a3f5c29b-707e-4ae2-b955-651580661246 +Error: Cannot convert 'complex' objects with values that are multiple of 100 3750d10d-d277-496a-8f9c-49592827a9fe +Error: Cannot convert 'complex' objects with values that are multiple of 100 3cb8f69c-19f8-48ad-9f3e-111f92fa9695 +Error: Cannot convert 'complex' objects with values that are multiple of 100 b52b53c7-4dc9-4c26-8027-af970a4664f6 +Error: Cannot convert 'complex' objects with values that are multiple of 100 f23836c4-ce59-4a01-bf58-d4ce2a6b4e8d +Error: Cannot convert 'complex' objects with values that are multiple of 100 fe76654b-fb1f-41cc-96c9-ee566f9754eb +Error: Cannot convert 'complex' objects with values that are multiple of 100 8ce8d356-20f0-4e87-b417-a53be0c55764 +Error: Cannot convert 'complex' objects with values that are multiple of 100 f8d692f9-1403-4aa3-b13b-03438cdc01fb +Error: Cannot convert 'complex' objects with values that are multiple of 100 018b0a78-63db-4898-b7e5-6c8d03f7dbaa +Error: Cannot convert 'complex' objects with values that are multiple of 100 0fd3dbb5-6929-473c-aeae-302fbbf47efd +Error: Cannot convert 'complex' objects with values that are multiple of 100 a1c3c2f6-0693-4925-ad24-f33e73edb24b +Error: Cannot convert 'complex' objects with values that are multiple of 100 e563fdae-26e3-4124-a38e-d2281182729f +Error: Cannot convert 'complex' objects with values that are multiple of 100 d32195f5-3312-4baf-82ee-48f322cf2484 +Error: Cannot convert 'complex' objects with values that are multiple of 100 564d8d76-6500-4bf4-8986-6f29f7f939c3 +Error: Cannot convert 'complex' objects with values that are multiple of 100 98cd0dc0-4e58-46e4-8b01-b732b7f4e6e7 +Error: Cannot convert 'complex' objects with values that are multiple of 100 e70a7175-6a62-4e45-bb99-8d203f32bf5e +Error: Cannot convert 'complex' objects with values that are multiple of 100 71826df0-ce85-4797-afae-dd6b85210dbe +Error: Cannot convert 'complex' objects with values that are multiple of 100 e98f260f-8d59-474f-8899-f98401b4e9c6 +Error: Cannot convert 'complex' objects with values that are multiple of 100 bec0c13f-3430-495d-9864-3983adfc733e +Error: Cannot convert 'complex' objects with values that are multiple of 100 12ea60a7-de0d-4062-be1a-c66987a55552 +Error: Cannot convert 'complex' objects with values that are multiple of 100 b75c805b-a614-4c82-a2c3-5bf7f756e617 +Error: Cannot convert 'complex' objects with values that are multiple of 100 4bf2ca7b-f1ad-4f0c-8ad9-858d841ca726 +Error: Cannot convert 'complex' objects with values that are multiple of 100 fdeb53ce-76f2-45c2-8d97-56c019ddc4c2 +Error: Cannot convert 'complex' objects with values that are multiple of 100 99d50e6a-a9fb-4699-a7a3-e4771393faf1 +Error: Cannot convert 'complex' objects with values that are multiple of 100 fe83ee65-dfb1-4ca0-877c-82437619b7b6 +Error: Cannot convert 'complex' objects with values that are multiple of 100 fda80a13-3f7f-4a0a-853a-a5a0b69aeee3 +Error: Cannot convert 'complex' objects with values that are multiple of 100 d59fa1ed-ab65-4428-b2c1-dc7737ba58aa +Error: Cannot convert 'complex' objects with values that are multiple of 100 7e26c6f6-09df-49f3-87bd-8e810abb1475 +Error: Cannot convert 'complex' objects with values that are multiple of 100 54dbb234-2cb4-4c91-bb2f-36f0d1dcbc71 +Error: Cannot convert 'complex' objects with values that are multiple of 100 91997e38-b01d-4197-945e-0a453cfc79f5 +Error: Cannot convert 'complex' objects with values that are multiple of 100 ed6c8871-f227-4476-b78d-5f8c30c5167f +Error: Cannot convert 'complex' objects with values that are multiple of 100 a06ab564-8762-4438-884a-425fb643ab4e +Error: Cannot convert 'complex' objects with values that are multiple of 100 cf9dcebb-ad19-4fe8-8f51-67533d20ab32 +Error: Cannot convert 'complex' objects with values that are multiple of 100 f5847615-a940-4fa5-885a-4085aef251c8 +Error: Cannot convert 'complex' objects with values that are multiple of 100 9e12ba9f-4d99-490e-8778-deff73ef71e7 +Error: Cannot convert 'complex' objects with values that are multiple of 100 35321be9-5341-434f-adaf-f729ee87e683 +Error: Cannot convert 'complex' objects with values that are multiple of 100 aa0365d7-d1c5-4788-933e-b27650cbfda4 +Error: Cannot convert 'complex' objects with values that are multiple of 100 f54adb98-cf22-4511-b78c-1b79344b5fe2 +Error: Cannot convert 'complex' objects with values that are multiple of 100 69060ee5-c9d1-4b78-98b0-c562b3793eae +Error: Cannot convert 'complex' objects with values that are multiple of 100 5f52db20-e3a0-49c6-9808-293dff5d2e0c +Error: Cannot convert 'complex' objects with values that are multiple of 100 755dde68-33dc-479d-a93a-406769187f23 +Error: Cannot convert 'complex' objects with values that are multiple of 100 4b50ec74-48a7-4e00-9f0d-1cda8bcdc4b9 +Error: Cannot convert 'complex' objects with values that are multiple of 100 48ec2e7d-cdd2-45d3-a166-01bd72f04b1b +Error: Cannot convert 'complex' objects with values that are multiple of 100 a7ec7e57-4289-4c25-899a-fe77a7c20ce6 +Error: Cannot convert 'complex' objects with values that are multiple of 100 d836e77c-94ae-456d-b03b-77efba0f30dd +Error: Cannot convert 'complex' objects with values that are multiple of 100 5fbed3af-b212-4c4e-a228-21fdf20aeb1d +Error: Cannot convert 'complex' objects with values that are multiple of 100 6a974c64-b794-438e-a866-0f8853ae80f7 +Error: Cannot convert 'complex' objects with values that are multiple of 100 38bc381b-d4de-4916-8962-ceb88130635e +Error: Cannot convert 'complex' objects with values that are multiple of 100 c160e280-9308-4712-adee-689884ebeaf6 +Error: Cannot convert 'complex' objects with values that are multiple of 100 52a193cd-f566-4ff2-878d-d70e1139df2e +Error: Cannot convert 'complex' objects with values that are multiple of 100 2f7c0d32-e3a1-4ff0-a3e4-3b45b7389b1f +Error: Cannot convert 'complex' objects with values that are multiple of 100 04f80d54-03f4-4e79-8c46-e5072d36007e +Error: Cannot convert 'complex' objects with values that are multiple of 100 05c5bbfb-8858-4439-a695-c854c4e28569 +Error: Cannot convert 'complex' objects with values that are multiple of 100 ab3bcc0d-8f88-4b5e-acb1-10cb89be118d +Error: Cannot convert 'complex' objects with values that are multiple of 100 588eebea-7807-424f-b1ae-192600f028f3 +Error: Cannot convert 'complex' objects with values that are multiple of 100 1a613324-9ebe-4370-a1be-5b8540b5e7e6 +Error: Cannot convert 'complex' objects with values that are multiple of 100 a79e69c6-56ea-46ac-8d19-29616c9f43ff +Error: Cannot convert 'complex' objects with values that are multiple of 100 45a8b609-1690-43dc-8fe9-73ed45a996d4 +Error: Cannot convert 'complex' objects with values that are multiple of 100 a0c66af2-c52d-4c74-8cf5-d6bd7aae57e1 +Error: Cannot convert 'complex' objects with values that are multiple of 100 3a547379-f469-4920-b1b3-0975bf3532e6 +Error: Cannot convert 'complex' objects with values that are multiple of 100 eb989e38-26ad-4173-bc37-23246af599e6 +Error: Cannot convert 'complex' objects with values that are multiple of 100 cc6e8592-1257-4791-810c-68fc59a67450 +Error: Cannot convert 'complex' objects with values that are multiple of 100 5adf294e-af67-447f-8ae8-1bf236cd61da +Error: Cannot convert 'complex' objects with values that are multiple of 100 c71e7626-1497-4727-a9c6-1fa438b57655 +Error: Cannot convert 'complex' objects with values that are multiple of 100 ed9eaec9-6984-42d2-b617-d417a5d8fd8a +Error: Cannot convert 'complex' objects with values that are multiple of 100 b053ad16-b8d1-47e3-a4e6-7d4e0c41013a +Error: Cannot convert 'complex' objects with values that are multiple of 100 368f4560-1acb-4011-a8c0-31f5b7ddb78e +Error: Cannot convert 'complex' objects with values that are multiple of 100 94651677-44bb-46c3-a041-66e9af680854 +Error: Cannot convert 'complex' objects with values that are multiple of 100 212907b6-8457-41e1-82d7-1f17ff8418f8 +Error: Cannot convert 'complex' objects with values that are multiple of 100 dd79b637-25db-4b9e-97a5-cd42efed3fca +Error: Cannot convert 'complex' objects with values that are multiple of 100 e55a86ff-1abd-4cc0-b3f1-186c94a21ca2 +Error: Cannot convert 'complex' objects with values that are multiple of 100 44f22c9a-f7d7-49f8-b105-ec4793913617 +Error: Cannot convert 'complex' objects with values that are multiple of 100 2d7b720d-0418-41b3-8992-d3a06aeb0d87 +Error: Cannot convert 'complex' objects with values that are multiple of 100 229a2508-2949-44e3-abef-b95a5314141a +Error: Cannot convert 'complex' objects with values that are multiple of 100 374ce0e5-9abb-4d1b-905e-3ad020b4e6ad +Error: Cannot convert 'complex' objects with values that are multiple of 100 9280b201-2c4d-4f1a-a2b3-563cef526ec6 +Error: Cannot convert 'complex' objects with values that are multiple of 100 239fea1a-5ce9-45c0-8c6f-7795b87f5005 +Error: Cannot convert 'complex' objects with values that are multiple of 100 e0b95739-008c-4303-b483-59778837edf7 +Error: Cannot convert 'complex' objects with values that are multiple of 100 23c1442d-5fe8-408e-8c91-6fe0b52c3880 +Error: Cannot convert 'complex' objects with values that are multiple of 100 57ec1c1d-604d-45c1-9505-eeee7c5272d3 +Error: Cannot convert 'complex' objects with values that are multiple of 100 9c34c4bb-783d-4527-bb40-62dc77ca9e9e +Error: Cannot convert 'complex' objects with values that are multiple of 100 ca0f3c78-c217-4089-9713-b7960c47e709 +Error: Cannot convert 'complex' objects with values that are multiple of 100 5e301cd0-18fc-413f-b89e-89b39c9b7d61 +Error: Cannot convert 'complex' objects with values that are multiple of 100 54a514cb-6a16-4ed5-bb7b-f1dab5e391ce +Error: Cannot convert 'complex' objects with values that are multiple of 100 c48229f1-b060-44f2-8e84-0094f3c52568 +Error: Cannot convert 'complex' objects with values that are multiple of 100 0adef79b-0a77-4e0c-9d45-ea7ac9efc99e +Error: Cannot convert 'complex' objects with values that are multiple of 100 41ae9d1a-19cc-43e2-bf43-74119c256171 +Error: Cannot convert 'complex' objects with values that are multiple of 100 0c8c5586-9310-448c-b769-57cd8b434f1b +Error: Cannot convert 'complex' objects with values that are multiple of 100 5abeddb1-b16e-4cae-9743-bd8b3967d68d +Error: Cannot convert 'complex' objects with values that are multiple of 100 9a16a8f0-5acb-4be4-b01b-ba70ee703d79 +Error: Cannot convert 'complex' objects with values that are multiple of 100 1ec57983-aa25-43fb-b619-75c05db355cb +Error: Cannot convert 'complex' objects with values that are multiple of 100 8219be58-e54e-4779-a46a-46af2e26dd65 +Error: Cannot convert 'complex' objects with values that are multiple of 100 36d49a42-405f-4489-98ee-15988fb02655 +Error: Cannot convert 'complex' objects with values that are multiple of 100 084945de-d37e-4363-81de-c5d80c6330d5 +Error: Cannot convert 'complex' objects with values that are multiple of 100 7804d5ed-5c7d-4836-9a1f-ad04160cc721 +Error: Cannot convert 'complex' objects with values that are multiple of 100 f00fa376-1e1e-40c1-96b9-32240c1f86e4 +Error: Cannot convert 'complex' objects with values that are multiple of 100 3fcb887f-e1a6-4353-8473-b1a9e036458e +Error: Cannot convert 'complex' objects with values that are multiple of 100 d5faec5c-9145-41f7-a0f8-d0e9f712d67c +Error: Cannot convert 'complex' objects with values that are multiple of 100 3cd05855-6007-47c8-a1ed-eeb0a0a3f610 +Error: Cannot convert 'complex' objects with values that are multiple of 100 d0398a7a-c268-4d74-8d78-d86fd8fce735 +Error: Cannot convert 'complex' objects with values that are multiple of 100 ac77927f-7557-4393-bd23-87ff74f91f3d +Error: Cannot convert 'complex' objects with values that are multiple of 100 a070e880-ef24-43fb-a9f0-1497bca8e13f +Error: Cannot convert 'complex' objects with values that are multiple of 100 841a6aa7-6d31-43ab-a40c-9d002f052f40 +Error: Cannot convert 'complex' objects with values that are multiple of 100 c5dc76ec-d278-48be-8bf7-68a7fb8ed76c +Error: Cannot convert 'complex' objects with values that are multiple of 100 aa9519cd-f645-4434-b72a-874c0f527303 +Error: Cannot convert 'complex' objects with values that are multiple of 100 c3865152-f2cb-4dad-8846-1a19346af136 +Error: Cannot convert 'complex' objects with values that are multiple of 100 48dd71da-8dc4-4c71-a4bf-5c9472b09bae +Error: Cannot convert 'complex' objects with values that are multiple of 100 7e91868d-55f3-4b56-965e-2a32d66a770a +Error: Cannot convert 'complex' objects with values that are multiple of 100 f4bf79b4-9370-443b-b17d-867982e36d87 +Error: Cannot convert 'complex' objects with values that are multiple of 100 01e803c0-5489-4fbb-9fc6-4dced641ad9e +Error: Cannot convert 'complex' objects with values that are multiple of 100 bf0eb3cb-db70-410e-8007-7a010e226622 +Error: Cannot convert 'complex' objects with values that are multiple of 100 8663a0e7-0597-43c6-85b0-0c107f06c929 +Error: Cannot convert 'complex' objects with values that are multiple of 100 fd613c6a-0c56-4cfd-80b9-fc437a630e7e +Error: Cannot convert 'complex' objects with values that are multiple of 100 615d25b9-df43-412f-bcd1-3110063dcc4f +Error: Cannot convert 'complex' objects with values that are multiple of 100 c931910e-3fd8-4c33-812f-fd49cea925e4 +Error: Cannot convert 'complex' objects with values that are multiple of 100 87565d23-6d9a-4465-b67b-667221c4df3b +Error: Cannot convert 'complex' objects with values that are multiple of 100 8a7652b0-c400-4b4e-af8b-a8da5e0dbd0b +Error: Cannot convert 'complex' objects with values that are multiple of 100 dc059883-7c96-4edd-9a7c-30b18a57b4b5 +Error: Cannot convert 'complex' objects with values that are multiple of 100 d0b6a1ca-0e5f-4648-b419-b6b9ec00e9b2 +Error: Cannot convert 'complex' objects with values that are multiple of 100 8817a5ba-a093-490b-a7f1-2e859bc0b345 +Error: Cannot convert 'complex' objects with values that are multiple of 100 4e08f021-3d6e-4786-b7a1-8b6d2d7dfa35 +Error: Cannot convert 'complex' objects with values that are multiple of 100 0a0b9977-b713-4357-a90d-c58bf18ea94e +Error: Cannot convert 'complex' objects with values that are multiple of 100 7c5f6a0c-5d52-4391-8f45-ba337aa7ce0f +Error: Cannot convert 'complex' objects with values that are multiple of 100 6742e115-ede8-4bcd-adf9-bf7b154be106 +Error: Cannot convert 'complex' objects with values that are multiple of 100 c68bbfa4-c190-43eb-bde7-d8567e65fea8 +Error: Cannot convert 'complex' objects with values that are multiple of 100 1ce1caee-6441-4372-9d95-406a025043f6 +Error: Cannot convert 'complex' objects with values that are multiple of 100 073ed719-6211-45cf-9c9b-341591230cb8 +Error: Cannot convert 'complex' objects with values that are multiple of 100 fa3a3fa7-5266-439e-82cf-3283a86fcd8a +Error: Cannot convert 'complex' objects with values that are multiple of 100 2acea925-7794-4f02-975d-800426fdc744 +Error: Cannot convert 'complex' objects with values that are multiple of 100 dd850e29-182a-4e75-ac56-f62f6389d8bc +Error: Cannot convert 'complex' objects with values that are multiple of 100 58289e3d-11b8-428a-ab3b-f34bc67820eb +Error: Cannot convert 'complex' objects with values that are multiple of 100 3ca1ac43-75db-4d32-ae67-1ca19baa7ebd +Error: Cannot convert 'complex' objects with values that are multiple of 100 589f4c6e-b82a-4c42-a1dc-232cc7ae30b5 +Error: Cannot convert 'complex' objects with values that are multiple of 100 965526db-d773-4f01-bfa1-f3208298e6ca +Error: Cannot convert 'complex' objects with values that are multiple of 100 9daa6a46-3033-4547-8b05-2da1b21710de +Error: Cannot convert 'complex' objects with values that are multiple of 100 fa03b3b2-feb5-41e9-a7fd-fa14b7a2ae74 +Error: Cannot convert 'complex' objects with values that are multiple of 100 8250e2f4-bcdf-46cc-8963-9d1f64a03bc0 +Error: Cannot convert 'complex' objects with values that are multiple of 100 388dba12-07a7-4d3d-88e5-fb5159499697 +Error: Cannot convert 'complex' objects with values that are multiple of 100 72e51f1d-6962-4670-94a9-dff3e8010067 +Error: Cannot convert 'complex' objects with values that are multiple of 100 01f68652-6627-451f-8293-966d19d1a654 +Error: Cannot convert 'complex' objects with values that are multiple of 100 18fd85dc-2843-41ba-b193-df23ce0cefc2 +Error: Cannot convert 'complex' objects with values that are multiple of 100 8f6161e8-f89c-47e8-9147-e4d90ab675ba +Error: Cannot convert 'complex' objects with values that are multiple of 100 eb2132af-a15a-452d-83df-b6974c48b592 +Error: Cannot convert 'complex' objects with values that are multiple of 100 1ee5c1bf-820f-4012-b0f3-5cd013974aeb +Error: Cannot convert 'complex' objects with values that are multiple of 100 37f6c099-3ae5-48de-8a32-002311f36560 +Error: Cannot convert 'complex' objects with values that are multiple of 100 dfd5562d-a445-4b00-8a03-42c3b29a7f6d +Error: Cannot convert 'complex' objects with values that are multiple of 100 f39d88ab-5d99-4c89-84ce-6ddda83fe359 +Error: Cannot convert 'complex' objects with values that are multiple of 100 ab1e91c2-b37c-4c98-a0c1-50424b19ede3 +Error: Cannot convert 'complex' objects with values that are multiple of 100 4172f9b5-54f7-4146-af18-f062e51c6fe8 +Error: Cannot convert 'complex' objects with values that are multiple of 100 67c046e1-87a6-44c6-8224-9d7a017e851a +Error: Cannot convert 'complex' objects with values that are multiple of 100 4ff6d019-a524-446c-9c7a-5eed5695411f +Error: Cannot convert 'complex' objects with values that are multiple of 100 88a75507-1fcb-45aa-bb3d-6e9e3f8cefe5 +Error: Cannot convert 'complex' objects with values that are multiple of 100 18d1f9c2-1ca9-47d5-8539-01c1523be687 +Error: Cannot convert 'complex' objects with values that are multiple of 100 0efec7af-4f1a-4c71-ba74-ad25a9446586 +Error: Cannot convert 'complex' objects with values that are multiple of 100 c30d943d-fafe-42c7-9886-976ee2ada664 +Error: Cannot convert 'complex' objects with values that are multiple of 100 ce6e9245-f2a4-48cc-b65e-0aa16bfa6c65 +Error: Cannot convert 'complex' objects with values that are multiple of 100 91bf39b0-e9d9-43c7-bdf2-67c2702acede +Error: Cannot convert 'complex' objects with values that are multiple of 100 3034c39f-ae49-47c7-aa85-55e9b6aa8bc3 +Error: Cannot convert 'complex' objects with values that are multiple of 100 53185dec-a065-4b83-a04e-4c385c931f3b +Error: Cannot convert 'complex' objects with values that are multiple of 100 23f8479f-4784-41d2-aecc-9bed73c8d13b +Error: Cannot convert 'complex' objects with values that are multiple of 100 5fff295a-72f2-48d7-b598-07bdfd0bda6f +Error: Cannot convert 'complex' objects with values that are multiple of 100 7882e9a7-6827-4c61-a930-29d5fc7acfe4 +Error: Cannot convert 'complex' objects with values that are multiple of 100 91a6b535-fee4-4a32-b4f0-528805ae6aaa +Error: Cannot convert 'complex' objects with values that are multiple of 100 a0a7d21f-ba2c-4887-b24d-89298be19b2c +Error: Cannot convert 'complex' objects with values that are multiple of 100 87a39676-e6c1-41ef-a1c7-058a0cb2baa7 +Error: Cannot convert 'complex' objects with values that are multiple of 100 5989e439-91d4-4f59-8bd9-4cc8d8cd5134 +Error: Cannot convert 'complex' objects with values that are multiple of 100 d024758f-9a32-4c8c-90f9-f7d48487b6a9 +Error: Cannot convert 'complex' objects with values that are multiple of 100 ae1fdfa2-5230-474e-bf4e-346266de3058 +Error: Cannot convert 'complex' objects with values that are multiple of 100 27f992f2-336a-4bb4-8a71-faa05d02b8d4 +Error: Cannot convert 'complex' objects with values that are multiple of 100 a69b163a-c0ff-45c3-974a-70b1fe651605 +Error: Cannot convert 'complex' objects with values that are multiple of 100 2fa56428-6c22-4da0-af9d-a0e4cdea372b +Error: Cannot convert 'complex' objects with values that are multiple of 100 cb4df774-3555-465f-9388-77a77527d071 +Error: Cannot convert 'complex' objects with values that are multiple of 100 e53a66db-4188-4e30-b0e0-257517103e6c +Error: Cannot convert 'complex' objects with values that are multiple of 100 44389dbc-d1ba-48a6-b247-317f26c2fcba +Error: Cannot convert 'complex' objects with values that are multiple of 100 0210e5db-a487-4137-88c1-b2be15c767a9 +Error: Cannot convert 'complex' objects with values that are multiple of 100 c07a8297-a53c-4da3-b8fd-e79bbe28eb58 +Error: Cannot convert 'complex' objects with values that are multiple of 100 bf3ce749-2bfa-4016-8990-179b7fa3f4e4 +Error: Cannot convert 'complex' objects with values that are multiple of 100 d309f6b1-3439-4ab2-bfde-10c835bf5fbc +Error: Cannot convert 'complex' objects with values that are multiple of 100 59ed9c39-fbbf-465c-ad52-3f7c920a3b22 +Error: Cannot convert 'complex' objects with values that are multiple of 100 3fe9bfd7-a802-4559-8e75-7029537eea62 +Error: Cannot convert 'complex' objects with values that are multiple of 100 3cbf3aef-2b29-4144-b3b1-339b4831bed6 +Error: Cannot convert 'complex' objects with values that are multiple of 100 7ba876c6-d99c-4bbd-9d2f-6b0dd2ebc022 +Error: Cannot convert 'complex' objects with values that are multiple of 100 35ce8d6c-853c-4a23-9858-6cda1c0ae9b4 +Error: Cannot convert 'complex' objects with values that are multiple of 100 682cb31c-19ee-42bd-b9f9-99a6a7d05be1 +Error: Cannot convert 'complex' objects with values that are multiple of 100 ebf6c057-88ee-42a4-adcc-564a696c45c5 +Error: Cannot convert 'complex' objects with values that are multiple of 100 ec10133d-b87a-4fdf-be5c-bd5cbed145ad +Error: Cannot convert 'complex' objects with values that are multiple of 100 20e6d960-ae74-4816-8111-bdd885e4ab4f +Error: Cannot convert 'complex' objects with values that are multiple of 100 28be65c9-d273-43ec-a516-42714f0c806f +Error: Cannot convert 'complex' objects with values that are multiple of 100 afaf66e2-5b90-4ef9-aa5e-8f5da044fadb +Error: Cannot convert 'complex' objects with values that are multiple of 100 0073e468-5a37-4b04-9126-e5cd82fb7c22 +Error: Cannot convert 'complex' objects with values that are multiple of 100 4758b312-86a8-49d5-a616-b12e8abea612 +Error: Cannot convert 'complex' objects with values that are multiple of 100 f843d079-1645-434c-b2e2-6744546bf518 +Error: Cannot convert 'complex' objects with values that are multiple of 100 22a1f87e-9e51-4bd0-b3f9-33edf9409dcd +Error: Cannot convert 'complex' objects with values that are multiple of 100 6754ee03-4dbd-453e-a571-a710008b8139 +Error: Cannot convert 'complex' objects with values that are multiple of 100 6134569a-59ac-4da6-b5db-60f14d5ea564 +Error: Cannot convert 'complex' objects with values that are multiple of 100 b465c27a-7fb0-4a86-bfec-f0ba8d68b325 +Error: Cannot convert 'complex' objects with values that are multiple of 100 eac4b609-c6cf-405f-b46a-8b0c3e59cd91 +Error: Cannot convert 'complex' objects with values that are multiple of 100 a749cb3c-25a6-4195-bc0e-a281ebdb1b0f +Error: Cannot convert 'complex' objects with values that are multiple of 100 33acd3cf-6b83-4aaa-8a0d-d429eeda042a +Error: Cannot convert 'complex' objects with values that are multiple of 100 bc0f3e94-7b4a-4330-8124-59e0b9ab10cd +Error: Cannot convert 'complex' objects with values that are multiple of 100 c87c35fb-2f70-473a-af97-5b74ed8891fb +Error: Cannot convert 'complex' objects with values that are multiple of 100 e0935148-92e1-427c-872a-92fe3114c6db +Error: Cannot convert 'complex' objects with values that are multiple of 100 e18cfdc8-dc65-4c70-82fb-b61fa05f2a97 +Error: Cannot convert 'complex' objects with values that are multiple of 100 b2bf8917-003f-4e4e-b4b1-2b95cf5d1e02 +Error: Cannot convert 'complex' objects with values that are multiple of 100 4f7cbdb2-7600-442f-beaf-304e80d7d632 +Error: Cannot convert 'complex' objects with values that are multiple of 100 9fbdbb8b-19c1-4fa3-befe-afad6f66196b +Error: Cannot convert 'complex' objects with values that are multiple of 100 b7328f10-bc80-4f5c-801b-c14ffaa99160 +Error: Cannot convert 'complex' objects with values that are multiple of 100 d9520d35-57c2-4f8a-ad32-28d473c2e3f0 +Error: Cannot convert 'complex' objects with values that are multiple of 100 454a86bd-1993-4cff-be5e-dc98cee9c658 +Error: Cannot convert 'complex' objects with values that are multiple of 100 d06dd080-cd3c-42de-8bcb-aa78fc3fed17 +Error: Cannot convert 'complex' objects with values that are multiple of 100 675c2090-6f88-427f-b357-424fcf2e8777 +Error: Cannot convert 'complex' objects with values that are multiple of 100 0655c1b2-24b9-43e4-a9d7-9b4711a80ff3 +Error: Cannot convert 'complex' objects with values that are multiple of 100 7518e96e-5fad-4286-bc04-b1273d2ef59a +Error: Cannot convert 'complex' objects with values that are multiple of 100 818eb169-1f16-410b-9524-e3422a5b626f +Error: Cannot convert 'complex' objects with values that are multiple of 100 618b9e7e-0d59-44a5-b233-25fa09475bfd +Error: Cannot convert 'complex' objects with values that are multiple of 100 a72700a8-dbcc-410e-9c3e-2b029f2a05d9 +Error: Cannot convert 'complex' objects with values that are multiple of 100 c106c66f-836d-4b3f-aed0-7a33dac4cc76 +Error: Cannot convert 'complex' objects with values that are multiple of 100 e8ea6f1b-ed4d-41b4-b1f6-7075c1dff0cf +Error: Cannot convert 'complex' objects with values that are multiple of 100 42fa8bc4-2ada-4523-a6ad-c1602649c744 +Error: Cannot convert 'complex' objects with values that are multiple of 100 87fc12a1-553b-45e4-bc74-0de6b5782350 +Error: Cannot convert 'complex' objects with values that are multiple of 100 ae5c4f18-74b6-4080-9c2c-1c596d984a23 +Error: Cannot convert 'complex' objects with values that are multiple of 100 63bc0d30-e592-4524-987f-6691970dc654 +Error: Cannot convert 'complex' objects with values that are multiple of 100 b3425416-92a2-427e-8323-c5df12536fb7 +Error: Cannot convert 'complex' objects with values that are multiple of 100 92dd95f3-033f-49ab-b42d-1f9e9b41dc13 +Error: Cannot convert 'complex' objects with values that are multiple of 100 b003a035-9097-42b7-91f9-7fa35309a082 +Error: Cannot convert 'complex' objects with values that are multiple of 100 c3c0c870-3081-411e-b487-8089859267de +Error: Cannot convert 'complex' objects with values that are multiple of 100 bf208759-db3f-4b74-aa94-d7d2bb9db63c +Error: Cannot convert 'complex' objects with values that are multiple of 100 2e478ba5-228d-428c-83b3-d1b18313908f +Error: Cannot convert 'complex' objects with values that are multiple of 100 af51f2f7-a382-4806-bc4e-69512a8d532a +Error: Cannot convert 'complex' objects with values that are multiple of 100 037c332e-110f-42bf-9001-a3caf8574ee6 +Error: Cannot convert 'complex' objects with values that are multiple of 100 a918d9a6-87e9-47e8-8a07-de4ebd94dae3 +Error: Cannot convert 'complex' objects with values that are multiple of 100 92750112-af31-41bd-ac90-7c9c830fccee +Error: Cannot convert 'complex' objects with values that are multiple of 100 8d320909-453b-4540-aa3e-2e50e09f12e1 +Error: Cannot convert 'complex' objects with values that are multiple of 100 76b14a34-6ea2-42ef-b3d6-7d9144325a15 +Error: Cannot convert 'complex' objects with values that are multiple of 100 2f6c93f8-f860-486c-8fbe-ed171504b251 +Error: Cannot convert 'complex' objects with values that are multiple of 100 0767434b-8092-4d94-be6f-34d85929dbda +Error: Cannot convert 'complex' objects with values that are multiple of 100 638b5ee8-209d-4bf1-93a0-33f23db7a8a0 +Error: Cannot convert 'complex' objects with values that are multiple of 100 648143b7-e3b1-4e32-a55c-26b017e03bd7 +Error: Cannot convert 'complex' objects with values that are multiple of 100 8fa6afc0-b651-4043-a94e-47250f8e9653 +Error: Cannot convert 'complex' objects with values that are multiple of 100 08b1cfb0-227b-4724-8f60-891d37c29cd8 +Error: Cannot convert 'complex' objects with values that are multiple of 100 1e6699fc-1049-4f57-9b64-e8ef22ec16f8 +Error: Cannot convert 'complex' objects with values that are multiple of 100 c0e01e41-9651-4079-9dc4-84e659c8772b +Error: Cannot convert 'complex' objects with values that are multiple of 100 f8ce5a55-3aae-444f-83ef-10d9b5c9b8d0 +Error: Cannot convert 'complex' objects with values that are multiple of 100 0edced15-3190-4466-8eac-25fd18f160c8 +Error: Cannot convert 'complex' objects with values that are multiple of 100 3db5ff36-9fbe-4e7e-8b61-d3302232b877 +Error: Cannot convert 'complex' objects with values that are multiple of 100 e17925ae-2d1e-48e5-b16e-9c353ea2e9ff +Error: Cannot convert 'complex' objects with values that are multiple of 100 2c7532b9-95c2-4f3c-8742-b7526a6cde42 +Error: Cannot convert 'complex' objects with values that are multiple of 100 b4caa59c-3e96-4316-aea8-b55148a1fc8b +Error: Cannot convert 'complex' objects with values that are multiple of 100 125dc8c0-4a09-462a-bdf8-19b944affaeb +Error: Cannot convert 'complex' objects with values that are multiple of 100 05234d6d-e547-4713-9069-0520c8b5d6a1 +Error: Cannot convert 'complex' objects with values that are multiple of 100 e5e5af91-3ebd-4707-b7a7-808c2fc87a33 +Error: Cannot convert 'complex' objects with values that are multiple of 100 b79430be-4be2-4e67-adb8-c7f593b7be12 +Error: Cannot convert 'complex' objects with values that are multiple of 100 6349acc6-f70b-474c-a4e1-d74eb5a31638 +Error: Cannot convert 'complex' objects with values that are multiple of 100 4b134f8b-192a-45c3-a543-bf76cd6716a0 +Error: Cannot convert 'complex' objects with values that are multiple of 100 c6468fe5-7ba4-4b6b-9994-a9648e0904ad +Error: Cannot convert 'complex' objects with values that are multiple of 100 0e10a6e2-08f2-44d6-a22d-8bd3e0800a79 +Error: Cannot convert 'complex' objects with values that are multiple of 100 111971d2-74df-4d2b-9fe8-328641d1ff17 +Error: Cannot convert 'complex' objects with values that are multiple of 100 1444dbd3-18c3-423f-b8ed-a01a41ee4731 +Error: Cannot convert 'complex' objects with values that are multiple of 100 9a3e6eff-82c4-441d-8345-fcd4352721bd +Error: Cannot convert 'complex' objects with values that are multiple of 100 20d7476b-bd4b-47a7-9937-87b0fa57d111 +Error: Cannot convert 'complex' objects with values that are multiple of 100 3d3ff888-81b9-47f7-ae2f-24d2c4d4713f +Error: Cannot convert 'complex' objects with values that are multiple of 100 c7041ac7-46bc-4b9b-bf88-a27a1734b154 +Error: Cannot convert 'complex' objects with values that are multiple of 100 0afaa370-f20a-4ae1-b871-2d2675bcdf23 +Error: Cannot convert 'complex' objects with values that are multiple of 100 2888ea65-718b-448b-a2dd-77c56924f630 +Error: Cannot convert 'complex' objects with values that are multiple of 100 9abefd72-25c0-48e3-9239-13a44dbddee6" +`; diff --git a/src/core/server/integration_tests/saved_objects/migrations/group1/v2_migration.test.ts b/src/core/server/integration_tests/saved_objects/migrations/group1/v2_migration.test.ts index a150a449db7fc..5981c2759d5ca 100644 --- a/src/core/server/integration_tests/saved_objects/migrations/group1/v2_migration.test.ts +++ b/src/core/server/integration_tests/saved_objects/migrations/group1/v2_migration.test.ts @@ -9,11 +9,11 @@ import { join } from 'path'; import { omit } from 'lodash'; +import JSON5 from 'json5'; import type { TestElasticsearchUtils } from '@kbn/core-test-helpers-kbn-server'; import type { MigrationResult } from '@kbn/core-saved-objects-base-server-internal'; import { - nextMinor, defaultKibanaIndex, defaultKibanaTaskIndex, startElasticsearch, @@ -21,34 +21,27 @@ import { type KibanaMigratorTestKit, readLog, clearLog, + currentVersion, } from '../kibana_migrator_test_kit'; import { BASELINE_DOCUMENTS_PER_TYPE_500K, BASELINE_TEST_ARCHIVE_500K, } from '../kibana_migrator_archive_utils'; -import { getReindexingMigratorTestKit } from '../kibana_migrator_test_kit.fixtures'; +import { + baselineTypes, + getReindexingBaselineTypes, + getReindexingMigratorTestKit, + getUpToDateMigratorTestKit, +} from '../kibana_migrator_test_kit.fixtures'; import { delay } from '../test_utils'; const logFilePath = join(__dirname, 'v2_migration.log'); describe('v2 migration', () => { let esServer: TestElasticsearchUtils; - let kit: KibanaMigratorTestKit; - let migrationResults: MigrationResult[]; beforeAll(async () => { esServer = await startElasticsearch({ dataArchive: BASELINE_TEST_ARCHIVE_500K }); - await clearLog(logFilePath); - kit = await getReindexingMigratorTestKit({ - logFilePath, - filterDeprecated: true, - settings: { - migrations: { - discardUnknownObjects: nextMinor, - }, - }, - }); - migrationResults = await kit.runMigrations(); }); afterAll(async () => { @@ -58,100 +51,328 @@ describe('v2 migration', () => { } }); - describe('a migrator performing a reindexing migration', () => { - describe('when an index contains SO types with incompatible mappings', () => { - it('executes the reindexing migration steps', async () => { - const logs = await readLog(logFilePath); - expect(logs).toMatch(`[${defaultKibanaIndex}] INIT -> WAIT_FOR_YELLOW_SOURCE.`); - expect(logs).toMatch( - `[${defaultKibanaIndex}] WAIT_FOR_YELLOW_SOURCE -> UPDATE_SOURCE_MAPPINGS_PROPERTIES.` - ); - expect(logs).toMatch( - `[${defaultKibanaIndex}] UPDATE_SOURCE_MAPPINGS_PROPERTIES -> CHECK_CLUSTER_ROUTING_ALLOCATION.` - ); - expect(logs).toMatch( - `[${defaultKibanaIndex}] CHECK_CLUSTER_ROUTING_ALLOCATION -> CHECK_UNKNOWN_DOCUMENTS.` - ); - expect(logs).toMatch( - `[${defaultKibanaIndex}] CHECK_TARGET_MAPPINGS -> UPDATE_TARGET_MAPPINGS_PROPERTIES.` - ); + describe('to the current stack version', () => { + let upToDateKit: KibanaMigratorTestKit; + let migrationResults: MigrationResult[]; + + beforeAll(async () => { + await clearLog(logFilePath); + upToDateKit = await getUpToDateMigratorTestKit({ + logFilePath, + kibanaVersion: currentVersion, + }); + migrationResults = await upToDateKit.runMigrations(); + }); + + it('skips UPDATE_TARGET_MAPPINGS_PROPERTIES if there are no changes in the mappings', async () => { + const logs = await readLog(logFilePath); + expect(logs).not.toMatch('CREATE_NEW_TARGET'); + expect(logs).toMatch( + `[${defaultKibanaIndex}] CHECK_TARGET_MAPPINGS -> CHECK_VERSION_INDEX_READY_ACTIONS` + ); + expect(logs).toMatch( + `[${defaultKibanaTaskIndex}] CHECK_TARGET_MAPPINGS -> CHECK_VERSION_INDEX_READY_ACTIONS` + ); + expect(logs).not.toMatch('UPDATE_TARGET_MAPPINGS_PROPERTIES'); + expect(logs).not.toMatch('UPDATE_TARGET_MAPPINGS_PROPERTIES_WAIT_FOR_TASK'); + expect(logs).not.toMatch('UPDATE_TARGET_MAPPINGS_META'); + }); + + it(`returns a 'patched' status for each SO index`, () => { + // omit elapsedMs as it varies in each execution + expect(migrationResults.map((result) => omit(result, 'elapsedMs'))).toMatchInlineSnapshot(` + Array [ + Object { + "destIndex": ".kibana_migrator_9.0.0_001", + "status": "patched", + }, + Object { + "destIndex": ".kibana_migrator_tasks_9.0.0_001", + "status": "patched", + }, + ] + `); + }); + + it('each migrator takes less than 10 seconds', () => { + expect( + (migrationResults as Array<{ elapsedMs?: number }>).every( + ({ elapsedMs }) => !elapsedMs || elapsedMs < 10000 + ) + ).toEqual(true); + }); + }); + + describe('to a newer stack version', () => { + describe('with unknown types', () => { + let unknownTypesKit: KibanaMigratorTestKit; + let logs: string; + + beforeAll(async () => { + await clearLog(logFilePath); + unknownTypesKit = await getReindexingMigratorTestKit({ + logFilePath, + // filter out 'task' objects in order to not spawn that migrator for this test + types: getReindexingBaselineTypes(true).filter(({ name }) => name !== 'task'), + settings: { + migrations: { + discardUnknownObjects: currentVersion, // instead of the actual target, 'nextMinor' + }, + }, + }); + }); + + it('fails if Kibana is not configured to discard unknown objects', async () => { + await expect(unknownTypesKit.runMigrations()).rejects.toThrowErrorMatchingInlineSnapshot(` + "Unable to complete saved object migrations for the [.kibana_migrator] index: Migration failed because some documents were found which use unknown saved object types: deprecated + To proceed with the migration you can configure Kibana to discard unknown saved objects for this migration. + Please refer to https://www.elastic.co/guide/en/kibana/master/resolve-migrations-failures.html for more information." + `); + logs = await readLog(logFilePath); expect(logs).toMatch( - `[${defaultKibanaIndex}] UPDATE_TARGET_MAPPINGS_META -> CHECK_VERSION_INDEX_READY_ACTIONS.` + 'The flag `migrations.discardUnknownObjects` is defined but does not match the current kibana version; unknown objects will NOT be discarded.' ); expect(logs).toMatch( - `[${defaultKibanaIndex}] CHECK_VERSION_INDEX_READY_ACTIONS -> MARK_VERSION_INDEX_READY.` + `[${defaultKibanaIndex}] Migration failed because some documents were found which use unknown saved object types: deprecated` ); - expect(logs).toMatch(`[${defaultKibanaIndex}] MARK_VERSION_INDEX_READY -> DONE.`); - - expect(logs).not.toMatch(`[${defaultKibanaIndex}] CREATE_NEW_TARGET`); - expect(logs).not.toMatch(`[${defaultKibanaIndex}] CLEANUP_UNKNOWN_AND_EXCLUDED`); - expect(logs).not.toMatch(`[${defaultKibanaIndex}] PREPARE_COMPATIBLE_MIGRATION`); + expect(logs).toMatch(`[${defaultKibanaIndex}] CHECK_UNKNOWN_DOCUMENTS -> FATAL.`); }); }); - describe('copies the right documents over to the target indices', () => { - let primaryIndexCounts: Record; - let taskIndexCounts: Record; + describe('with transform errors', () => { + let transformErrorsKit: KibanaMigratorTestKit; + let logs: string; beforeAll(async () => { - primaryIndexCounts = await getAggregatedTypesCount(kit.client, defaultKibanaIndex); - taskIndexCounts = await getAggregatedTypesCount(kit.client, defaultKibanaTaskIndex); + await clearLog(logFilePath); + transformErrorsKit = await getReindexingMigratorTestKit({ + logFilePath, + // filter out 'task' objects in order to not spawn that migrator for this test + types: getReindexingBaselineTypes(true).filter(({ name }) => name !== 'task'), + settings: { + migrations: { + discardCorruptObjects: currentVersion, // instead of the actual target, 'nextMinor' + }, + }, + }); }); - it('copies documents to the right indices depending on their types', () => { - expect(primaryIndexCounts.basic).toBeDefined(); - expect(primaryIndexCounts.complex).toBeDefined(); - expect(primaryIndexCounts.task).not.toBeDefined(); + it('collects corrupt saved object documents across batches', async () => { + try { + await transformErrorsKit.runMigrations(); + } catch (error) { + const lines = error.message + .split('\n') + .filter((line: string) => line.includes(`'complex'`)) + .join('\n'); + expect(lines).toMatchSnapshot(); + } + }); - expect(taskIndexCounts.basic).not.toBeDefined(); - expect(taskIndexCounts.complex).not.toBeDefined(); - expect(taskIndexCounts.task).toBeDefined(); + it('fails if Kibana is not configured to discard transform errors', async () => { + logs = await readLog(logFilePath); + expect(logs).toMatch( + `Cannot convert 'complex' objects with values that are multiple of 100` + ); + expect(logs).toMatch(`[${defaultKibanaIndex}] REINDEX_SOURCE_TO_TEMP_READ -> FATAL.`); }); - it('discards REMOVED_TYPES', () => { - expect(primaryIndexCounts.server).not.toBeDefined(); - expect(taskIndexCounts.server).not.toBeDefined(); + it('closes reindex PIT upon failure', async () => { + const lineWithPit = logs + .split('\n') + .find((line) => + line.includes(`[${defaultKibanaIndex}] REINDEX_SOURCE_TO_TEMP_OPEN_PIT PitId:`) + ); + + expect(lineWithPit).toBeTruthy(); + + const id = JSON5.parse(lineWithPit!).message.split(':')[1]; + expect(id).toBeTruthy(); + + await expect( + transformErrorsKit.client.search({ + pit: { id }, + }) + // throws an exception that cannot search with closed PIT + ).rejects.toThrow(/search_phase_execution_exception/); }); + }); + + describe('configured to discard transform errors and unknown types', () => { + let kit: KibanaMigratorTestKit; + let migrationResults: MigrationResult[]; + let logs: string; - it('discards unknown types', () => { - expect(primaryIndexCounts.deprecated).not.toBeDefined(); - expect(taskIndexCounts.deprecated).not.toBeDefined(); + beforeAll(async () => { + await clearLog(logFilePath); + kit = await getReindexingMigratorTestKit({ + logFilePath, + filterDeprecated: true, + }); + migrationResults = await kit.runMigrations(); + logs = await readLog(logFilePath); }); - it('copies all of the documents', () => { - expect(primaryIndexCounts.basic).toEqual(BASELINE_DOCUMENTS_PER_TYPE_500K); - expect(taskIndexCounts.task).toEqual(BASELINE_DOCUMENTS_PER_TYPE_500K); + it('migrates documents to the highest version', async () => { + const typeMigrationVersions: Record = { + basic: '10.1.0', // did not define any model versions + complex: '10.2.0', + task: '10.2.0', + }; + + const resultSets = await Promise.all( + baselineTypes.map(({ name: type }) => + kit.client.search({ + index: [defaultKibanaIndex, defaultKibanaTaskIndex], + query: { + bool: { + should: [ + { + term: { type }, + }, + ], + }, + }, + }) + ) + ); + + expect( + resultSets + .flatMap((result) => result.hits.hits) + .every( + (document) => + document._source.typeMigrationVersion === + typeMigrationVersions[document._source.type] + ) + ).toEqual(true); }); - it('executes the excludeOnUpgrade hook', () => { - expect(primaryIndexCounts.complex).toEqual(BASELINE_DOCUMENTS_PER_TYPE_500K / 2); + describe('a migrator performing a compatible upgrade migration', () => { + it('updates target mappings when mappings have changed', () => { + expect(logs).toMatch( + `[${defaultKibanaTaskIndex}] CHECK_TARGET_MAPPINGS -> UPDATE_TARGET_MAPPINGS_PROPERTIES.` + ); + expect(logs).toMatch( + `[${defaultKibanaTaskIndex}] UPDATE_TARGET_MAPPINGS_PROPERTIES -> UPDATE_TARGET_MAPPINGS_PROPERTIES_WAIT_FOR_TASK.` + ); + expect(logs).toMatch( + `[${defaultKibanaTaskIndex}] UPDATE_TARGET_MAPPINGS_PROPERTIES_WAIT_FOR_TASK -> UPDATE_TARGET_MAPPINGS_META.` + ); + expect(logs).toMatch( + `[${defaultKibanaTaskIndex}] UPDATE_TARGET_MAPPINGS_META -> CHECK_VERSION_INDEX_READY_ACTIONS.` + ); + }); + + it('updates the version aliases during the PREPARE_COMPATIBLE_MIGRATION step', () => { + expect(logs).toMatch(`[${defaultKibanaTaskIndex}] PREPARE_COMPATIBLE_MIGRATION`); + expect(logs).not.toMatch(`[${defaultKibanaTaskIndex}] MARK_VERSION_INDEX_READY`); + expect(logs).toMatch( + `[${defaultKibanaTaskIndex}] CHECK_VERSION_INDEX_READY_ACTIONS -> DONE.` + ); + }); }); - }); - it('returns a migrated status for each SO index', () => { - // omit elapsedMs as it varies in each execution - expect(migrationResults.map((result) => omit(result, 'elapsedMs'))).toMatchInlineSnapshot(` - Array [ - Object { - "destIndex": ".kibana_migrator_9.1.0_001", - "sourceIndex": ".kibana_migrator_9.0.0_001", - "status": "migrated", - }, - Object { - "destIndex": ".kibana_migrator_tasks_9.0.0_001", - "sourceIndex": ".kibana_migrator_tasks_9.0.0_001", - "status": "migrated", - }, - ] - `); - }); + describe('a migrator performing a reindexing migration', () => { + describe('when an index contains SO types with incompatible mappings', () => { + it('executes the reindexing migration steps', () => { + expect(logs).toMatch(`[${defaultKibanaIndex}] INIT -> WAIT_FOR_YELLOW_SOURCE.`); + expect(logs).toMatch( + `[${defaultKibanaIndex}] WAIT_FOR_YELLOW_SOURCE -> UPDATE_SOURCE_MAPPINGS_PROPERTIES.` + ); + expect(logs).toMatch( + `[${defaultKibanaIndex}] UPDATE_SOURCE_MAPPINGS_PROPERTIES -> CHECK_CLUSTER_ROUTING_ALLOCATION.` + ); + expect(logs).toMatch( + `[${defaultKibanaIndex}] CHECK_CLUSTER_ROUTING_ALLOCATION -> CHECK_UNKNOWN_DOCUMENTS.` + ); + expect(logs).toMatch( + `[${defaultKibanaIndex}] CHECK_TARGET_MAPPINGS -> UPDATE_TARGET_MAPPINGS_PROPERTIES.` + ); + expect(logs).toMatch( + `[${defaultKibanaIndex}] UPDATE_TARGET_MAPPINGS_META -> CHECK_VERSION_INDEX_READY_ACTIONS.` + ); + expect(logs).toMatch( + `[${defaultKibanaIndex}] CHECK_VERSION_INDEX_READY_ACTIONS -> MARK_VERSION_INDEX_READY.` + ); + expect(logs).toMatch(`[${defaultKibanaIndex}] MARK_VERSION_INDEX_READY -> DONE.`); - it('each migrator takes less than 60 seconds', () => { - expect( - (migrationResults as Array<{ elapsedMs?: number }>).every( - ({ elapsedMs }) => !elapsedMs || elapsedMs < 60000 - ) - ).toEqual(true); + expect(logs).not.toMatch(`[${defaultKibanaIndex}] CREATE_NEW_TARGET`); + expect(logs).not.toMatch(`[${defaultKibanaIndex}] CLEANUP_UNKNOWN_AND_EXCLUDED`); + expect(logs).not.toMatch(`[${defaultKibanaIndex}] PREPARE_COMPATIBLE_MIGRATION`); + }); + }); + + describe('copies the right documents over to the target indices', () => { + let primaryIndexCounts: Record; + let taskIndexCounts: Record; + + beforeAll(async () => { + primaryIndexCounts = await getAggregatedTypesCount(kit.client, defaultKibanaIndex); + taskIndexCounts = await getAggregatedTypesCount(kit.client, defaultKibanaTaskIndex); + }); + + it('copies documents to the right indices depending on their types', () => { + expect(primaryIndexCounts.basic).toBeDefined(); + expect(primaryIndexCounts.complex).toBeDefined(); + expect(primaryIndexCounts.task).not.toBeDefined(); + + expect(taskIndexCounts.basic).not.toBeDefined(); + expect(taskIndexCounts.complex).not.toBeDefined(); + expect(taskIndexCounts.task).toBeDefined(); + }); + + it('discards REMOVED_TYPES', () => { + expect(primaryIndexCounts.server).not.toBeDefined(); + expect(taskIndexCounts.server).not.toBeDefined(); + }); + + it('discards unknown types', () => { + expect(primaryIndexCounts.deprecated).not.toBeDefined(); + expect(taskIndexCounts.deprecated).not.toBeDefined(); + }); + + it('copies all of the documents', () => { + expect(primaryIndexCounts.basic).toEqual(BASELINE_DOCUMENTS_PER_TYPE_500K); + expect(taskIndexCounts.task).toEqual(BASELINE_DOCUMENTS_PER_TYPE_500K); + }); + + it('executes the excludeOnUpgrade hook', () => { + // we discard the second half with exclude on upgrade (firstHalf !== true) + // then we discard half all multiples of 100 (1% of them) + expect(primaryIndexCounts.complex).toEqual( + BASELINE_DOCUMENTS_PER_TYPE_500K / 2 - BASELINE_DOCUMENTS_PER_TYPE_500K / 2 / 100 + ); + }); + }); + + it('returns a migrated status for each SO index', () => { + // omit elapsedMs as it varies in each execution + expect(migrationResults.map((result) => omit(result, 'elapsedMs'))) + .toMatchInlineSnapshot(` + Array [ + Object { + "destIndex": ".kibana_migrator_9.1.0_001", + "sourceIndex": ".kibana_migrator_9.0.0_001", + "status": "migrated", + }, + Object { + "destIndex": ".kibana_migrator_tasks_9.0.0_001", + "sourceIndex": ".kibana_migrator_tasks_9.0.0_001", + "status": "migrated", + }, + ] + `); + }); + + it('each migrator takes less than 60 seconds', () => { + expect( + (migrationResults as Array<{ elapsedMs?: number }>).every( + ({ elapsedMs }) => !elapsedMs || elapsedMs < 60000 + ) + ).toEqual(true); + }); + }); }); }); }); diff --git a/src/core/server/integration_tests/saved_objects/migrations/group2/check_target_mappings.test.ts b/src/core/server/integration_tests/saved_objects/migrations/group2/check_target_mappings.test.ts deleted file mode 100644 index 4c0721eb04e6f..0000000000000 --- a/src/core/server/integration_tests/saved_objects/migrations/group2/check_target_mappings.test.ts +++ /dev/null @@ -1,168 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import Path from 'path'; -import fs from 'fs/promises'; -import { Env } from '@kbn/config'; -import { REPO_ROOT } from '@kbn/repo-info'; -import { getEnvOptions } from '@kbn/config-mocks'; -import { Root } from '@kbn/core-root-server-internal'; -import { - createRootWithCorePlugins, - createTestServers, - type TestElasticsearchUtils, -} from '@kbn/core-test-helpers-kbn-server'; - -const logFilePath = Path.join(__dirname, 'check_target_mappings.log'); - -// Failing 9.0 version update: https://github.com/elastic/kibana/issues/192624 -describe.skip('migration v2 - CHECK_TARGET_MAPPINGS', () => { - let esServer: TestElasticsearchUtils; - let root: Root; - let logs: string; - - beforeEach(async () => { - await fs.unlink(logFilePath).catch(() => {}); - }); - - afterEach(async () => { - await root?.shutdown(); - await esServer?.stop(); - }); - - it('is not run for new installations', async () => { - const { startES } = createTestServers({ - adjustTimeout: (t: number) => jest.setTimeout(t), - settings: { - es: { - license: 'basic', - }, - }, - }); - - root = createRoot(); - esServer = await startES(); - await root.preboot(); - await root.setup(); - await root.start(); - - // Check for migration steps present in the logs - logs = await fs.readFile(logFilePath, 'utf-8'); - - expect(logs).toMatch('CREATE_NEW_TARGET'); - expect(logs).not.toMatch('CHECK_TARGET_MAPPINGS'); - }); - - describe('when the indices are aligned with the stack version', () => { - it('skips UPDATE_TARGET_MAPPINGS_PROPERTIES if there are no changes in the mappings', async () => { - const { startES } = createTestServers({ - adjustTimeout: (t: number) => jest.setTimeout(t), - settings: { - es: { - license: 'basic', - }, - }, - }); - - esServer = await startES(); - - // start Kibana a first time to create the system indices - root = createRoot(); - await root.preboot(); - await root.setup(); - await root.start(); - - // stop Kibana and remove logs - await root.shutdown(); - await fs.unlink(logFilePath).catch(() => {}); - - root = createRoot(); - await root.preboot(); - await root.setup(); - await root.start(); - - // Check for migration steps present in the logs - logs = await fs.readFile(logFilePath, 'utf-8'); - expect(logs).not.toMatch('CREATE_NEW_TARGET'); - expect(logs).toMatch('CHECK_TARGET_MAPPINGS -> CHECK_VERSION_INDEX_READY_ACTIONS'); - expect(logs).not.toMatch('UPDATE_TARGET_MAPPINGS_PROPERTIES'); - expect(logs).not.toMatch('UPDATE_TARGET_MAPPINGS_PROPERTIES_WAIT_FOR_TASK'); - expect(logs).not.toMatch('UPDATE_TARGET_MAPPINGS_META'); - }); - }); - - describe('when upgrading to a newer stack version', () => { - const currentVersion = Env.createDefault(REPO_ROOT, getEnvOptions()).packageInfo.version; - - it('runs UPDATE_TARGET_MAPPINGS_PROPERTIES when mappings have changed', async () => { - const { startES } = createTestServers({ - adjustTimeout: (t: number) => jest.setTimeout(t), - settings: { - es: { - license: 'basic', - dataArchive: Path.join(__dirname, '..', 'archives', '8.4.0_with_sample_data_logs.zip'), - }, - }, - }); - - esServer = await startES(); - - // start Kibana a first time to create the system indices - root = createRoot(currentVersion); // we discard a bunch of SO that have become unknown since 8.4.0 - await root.preboot(); - await root.setup(); - await root.start(); - - // Check for migration steps present in the logs - logs = await fs.readFile(logFilePath, 'utf-8'); - expect(logs).not.toMatch('[.kibana] CREATE_NEW_TARGET'); - expect(logs).toMatch('CHECK_TARGET_MAPPINGS -> UPDATE_TARGET_MAPPINGS_PROPERTIES'); - expect(logs).toMatch( - 'UPDATE_TARGET_MAPPINGS_PROPERTIES -> UPDATE_TARGET_MAPPINGS_PROPERTIES_WAIT_FOR_TASK' - ); - expect(logs).toMatch( - 'UPDATE_TARGET_MAPPINGS_PROPERTIES_WAIT_FOR_TASK -> UPDATE_TARGET_MAPPINGS_META' - ); - expect(logs).toMatch('UPDATE_TARGET_MAPPINGS_META -> CHECK_VERSION_INDEX_READY_ACTIONS'); - expect(logs).toMatch('Migration completed'); - }); - }); -}); - -function createRoot(discardUnknownObjects?: string, customKibanaVersion?: string) { - return createRootWithCorePlugins( - { - migrations: { - discardUnknownObjects, - }, - logging: { - appenders: { - file: { - type: 'file', - fileName: logFilePath, - layout: { - type: 'json', - }, - }, - }, - loggers: [ - { - name: 'root', - level: 'info', - appenders: ['file'], - }, - ], - }, - }, - { - oss: true, - }, - customKibanaVersion - ); -} diff --git a/src/core/server/integration_tests/saved_objects/migrations/group2/cleanup.test.ts b/src/core/server/integration_tests/saved_objects/migrations/group2/cleanup.test.ts deleted file mode 100644 index 057db91eb7b55..0000000000000 --- a/src/core/server/integration_tests/saved_objects/migrations/group2/cleanup.test.ts +++ /dev/null @@ -1,192 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import Path from 'path'; -import Fs from 'fs'; -import Util from 'util'; -import JSON5 from 'json5'; -import { type TestElasticsearchUtils } from '@kbn/core-test-helpers-kbn-server'; -import { SavedObjectsType } from '@kbn/core-saved-objects-server'; -import { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; -import { getMigrationDocLink } from '../test_utils'; -import { - clearLog, - currentVersion, - defaultKibanaIndex, - getKibanaMigratorTestKit, - nextMinor, - startElasticsearch, -} from '../kibana_migrator_test_kit'; - -const migrationDocLink = getMigrationDocLink().resolveMigrationFailures; -const logFilePath = Path.join(__dirname, 'cleanup.log'); - -const asyncReadFile = Util.promisify(Fs.readFile); - -describe('migration v2', () => { - let esServer: TestElasticsearchUtils['es']; - let esClient: ElasticsearchClient; - - beforeAll(async () => { - esServer = await startElasticsearch(); - }); - - beforeEach(async () => { - esClient = await setupBaseline(); - await clearLog(logFilePath); - }); - - it('clean ups if migration fails', async () => { - const { runMigrations } = await setupNextMinor(); - - await expect(runMigrations()).rejects.toThrowErrorMatchingInlineSnapshot(` - "Unable to complete saved object migrations for the [${defaultKibanaIndex}] index: Migrations failed. Reason: 1 corrupt saved object documents were found: corrupt:2baf4de0-a6d4-11ed-ba5a-39196fc76e60 - - To allow migrations to proceed, please delete or fix these documents. - Note that you can configure Kibana to automatically discard corrupt documents and transform errors for this migration. - Please refer to ${migrationDocLink} for more information." - `); - - const logFileContent = await asyncReadFile(logFilePath, 'utf-8'); - const records = logFileContent - .split('\n') - .filter(Boolean) - .map((str) => JSON5.parse(str)); - - const logRecordWithPit = records.find( - (rec) => rec.message === `[${defaultKibanaIndex}] REINDEX_SOURCE_TO_TEMP_OPEN_PIT RESPONSE` - ); - - expect(logRecordWithPit).toBeTruthy(); - }); - - afterEach(async () => { - await esClient?.indices.delete({ index: `${defaultKibanaIndex}_${currentVersion}_001` }); - }); - - afterAll(async () => { - await esServer?.stop(); - }); -}); - -const setupBaseline = async () => { - const typesCurrent: SavedObjectsType[] = [ - { - name: 'complex', - hidden: false, - namespaceType: 'agnostic', - mappings: { - properties: { - name: { type: 'text' }, - value: { type: 'integer' }, - }, - }, - migrations: {}, - }, - ]; - - const savedObjects = [ - { - id: 'complex:4baf4de0-a6d4-11ed-ba5a-39196fc76e60', - body: { - type: 'complex', - complex: { - name: 'foo', - value: 5, - }, - references: [], - coreMigrationVersion: currentVersion, - updated_at: '2023-02-07T11:04:44.914Z', - created_at: '2023-02-07T11:04:44.914Z', - }, - }, - { - id: 'corrupt:2baf4de0-a6d4-11ed-ba5a-39196fc76e60', // incorrect id => corrupt object - body: { - type: 'complex', - complex: { - name: 'bar', - value: 3, - }, - references: [], - coreMigrationVersion: currentVersion, - updated_at: '2023-02-07T11:04:44.914Z', - created_at: '2023-02-07T11:04:44.914Z', - }, - }, - ]; - - const { runMigrations, client } = await getKibanaMigratorTestKit({ - types: typesCurrent, - logFilePath, - }); - - await runMigrations(); - - // inject corrupt saved objects directly using esClient - await Promise.all( - savedObjects.map((savedObject) => - client.create({ - index: defaultKibanaIndex, - refresh: 'wait_for', - ...savedObject, - }) - ) - ); - - return client; -}; - -const setupNextMinor = async () => { - const typesNextMinor: SavedObjectsType[] = [ - { - name: 'complex', - hidden: false, - namespaceType: 'agnostic', - mappings: { - properties: { - name: { type: 'keyword' }, - value: { type: 'long' }, - }, - }, - migrations: { - [nextMinor]: (doc) => doc, - }, - }, - ]; - - return await getKibanaMigratorTestKit({ - types: typesNextMinor, - kibanaVersion: nextMinor, - logFilePath, - settings: { - migrations: { - skip: false, - }, - logging: { - appenders: { - file: { - type: 'file', - fileName: logFilePath, - layout: { - type: 'json', - }, - }, - }, - loggers: [ - { - name: 'root', - appenders: ['file'], - level: 'debug', // DEBUG logs are required to retrieve the PIT _id from the action response logs - }, - ], - }, - }, - }); -}; diff --git a/src/core/server/integration_tests/saved_objects/migrations/group2/collects_corrupt_docs.test.ts b/src/core/server/integration_tests/saved_objects/migrations/group2/collects_corrupt_docs.test.ts deleted file mode 100644 index 2e78229709060..0000000000000 --- a/src/core/server/integration_tests/saved_objects/migrations/group2/collects_corrupt_docs.test.ts +++ /dev/null @@ -1,223 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import Path from 'path'; -import Fs from 'fs'; -import Util from 'util'; -import { - createTestServers, - createRootWithCorePlugins, - type TestElasticsearchUtils, -} from '@kbn/core-test-helpers-kbn-server'; -import { Root } from '@kbn/core-root-server-internal'; -import { getMigrationDocLink } from '../test_utils'; - -const migrationDocLink = getMigrationDocLink().resolveMigrationFailures; -const logFilePath = Path.join(__dirname, 'collects_corrupt_docs.log'); - -const asyncUnlink = Util.promisify(Fs.unlink); - -async function removeLogFile() { - // ignore errors if it doesn't exist - await asyncUnlink(logFilePath).catch(() => void 0); -} - -// Failing 9.0 version update: https://github.com/elastic/kibana/issues/192624 -describe.skip('migration v2 with corrupt saved object documents', () => { - let esServer: TestElasticsearchUtils; - let root: Root; - - beforeAll(async () => { - await removeLogFile(); - }); - - afterAll(async () => { - if (root) { - await root.shutdown(); - } - if (esServer) { - await esServer.stop(); - } - }); - - it('collects corrupt saved object documents across batches', async () => { - const { startES } = createTestServers({ - adjustTimeout: (t: number) => jest.setTimeout(t), - settings: { - es: { - license: 'basic', - // contains 4 `foo` objects, all with a `migrationVersion` of `7.13.0` - // - foo:1 and foo:2 have correct values for their `number` property (13 and 42 respectively) - // - foo:3 and foo:4 don't have the property, and will fail during the `7.14.0` registered migration - // contains migrated index with 8.0 aliases to skip migration, but run outdated doc search - dataArchive: Path.join( - __dirname, - '..', - 'archives', - '8.0.0_document_migration_failure.zip' - ), - }, - }, - }); - - root = createRoot(); - - esServer = await startES(); - await root.preboot(); - const coreSetup = await root.setup(); - - coreSetup.savedObjects.registerType({ - name: 'foo', - hidden: false, - mappings: { - properties: { - number: { type: 'integer' }, - }, - }, - namespaceType: 'agnostic', - migrations: { - '7.14.0': (doc) => { - if (doc.attributes.number === undefined) { - throw new Error('"number" attribute should be present'); - } - doc.attributes = { - ...doc.attributes, - number: doc.attributes.number + 9000, - }; - return doc; - }, - }, - }); - - try { - await root.start(); - expect(true).toEqual(false); - } catch (err) { - const errorMessage = err.message as string; - const errorLines = errorMessage.split('\n'); - const errorMessageWithoutStack = errorLines - .filter((line: string) => !line.includes(' at ')) - .join('\n'); - - expect(errorMessageWithoutStack).toMatchInlineSnapshot(` - "Unable to complete saved object migrations for the [.kibana] index: Migrations failed. Reason: 2 transformation errors were encountered: - - foo:3: Error: Migration function for version 7.14.0 threw an error - Caused by: - Error: \\"number\\" attribute should be present - - foo:4: Error: Migration function for version 7.14.0 threw an error - Caused by: - Error: \\"number\\" attribute should be present - - To allow migrations to proceed, please delete or fix these documents. - Note that you can configure Kibana to automatically discard corrupt documents and transform errors for this migration. - Please refer to ${migrationDocLink} for more information." - `); - - expectMatchOrder(errorLines, [ - { - mode: 'equal', - value: '- foo:3: Error: Migration function for version 7.14.0 threw an error', - }, - { - mode: 'contain', - value: 'at transform', - }, - { - mode: 'equal', - value: 'Caused by:', - }, - { - mode: 'equal', - value: 'Error: "number" attribute should be present', - }, - { - mode: 'contain', - value: 'at 7.14.0', - }, - { - mode: 'equal', - value: '- foo:4: Error: Migration function for version 7.14.0 threw an error', - }, - { - mode: 'contain', - value: 'at transform', - }, - { - mode: 'equal', - value: 'Caused by:', - }, - { - mode: 'equal', - value: 'Error: "number" attribute should be present', - }, - { - mode: 'contain', - value: 'at 7.14.0', - }, - ]); - } - }); -}); - -function createRoot() { - return createRootWithCorePlugins( - { - migrations: { - skip: false, - batchSize: 5, - }, - logging: { - appenders: { - file: { - type: 'file', - fileName: logFilePath, - layout: { - type: 'json', - }, - }, - }, - loggers: [ - { - name: 'root', - appenders: ['file'], - level: 'info', - }, - ], - }, - }, - { - oss: false, - } - ); -} - -type FindInOrderPattern = { mode: 'equal'; value: string } | { mode: 'contain'; value: string }; - -const expectMatchOrder = (lines: string[], patterns: FindInOrderPattern[]) => { - let lineIdx = 0; - let patternIdx = 0; - - while (lineIdx < lines.length && patternIdx < patterns.length) { - const line = lines[lineIdx]; - const pattern = patterns[patternIdx]; - if (lineMatch(line, pattern)) { - patternIdx++; - } - lineIdx++; - } - - expect(patternIdx).toEqual(patterns.length); -}; - -const lineMatch = (line: string, pattern: FindInOrderPattern) => { - if (pattern.mode === 'contain') { - return line.trim().includes(pattern.value.trim()); - } - return line.trim() === pattern.value.trim(); -}; diff --git a/src/core/server/integration_tests/saved_objects/migrations/group2/corrupt_outdated_docs.test.ts b/src/core/server/integration_tests/saved_objects/migrations/group2/corrupt_outdated_docs.test.ts deleted file mode 100644 index b342882f468b3..0000000000000 --- a/src/core/server/integration_tests/saved_objects/migrations/group2/corrupt_outdated_docs.test.ts +++ /dev/null @@ -1,185 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import Path from 'path'; -import Fs from 'fs'; -import Util from 'util'; -import { - createTestServers, - createRootWithCorePlugins, - type TestElasticsearchUtils, -} from '@kbn/core-test-helpers-kbn-server'; -import { Root } from '@kbn/core-root-server-internal'; - -const logFilePath = Path.join(__dirname, 'corrupt_outdated_docs.log'); - -const asyncUnlink = Util.promisify(Fs.unlink); - -async function removeLogFile() { - // ignore errors if it doesn't exist - await asyncUnlink(logFilePath).catch(() => void 0); -} - -// Failing 9.0 version update: https://github.com/elastic/kibana/issues/192624 -describe.skip('migration v2 with corrupt saved object documents', () => { - let esServer: TestElasticsearchUtils; - let root: Root; - - beforeAll(async () => { - await removeLogFile(); - }); - - afterAll(async () => { - if (root) { - await root.shutdown(); - } - if (esServer) { - await esServer.stop(); - } - }); - - it.skip('collects corrupt saved object documents across batches', async () => { - const { startES } = createTestServers({ - adjustTimeout: (t: number) => jest.setTimeout(t), - settings: { - es: { - license: 'basic', - // original uncorrupt SO: - // { - // type: 'foo', // 'bar', 'baz' - // foo: {}, // bar: {}, baz: {} - // migrationVersion: { - // foo: '7.13.0', - // }, - // }, - // original corrupt SO example: - // { - // id: 'bar:123' // '123' etc - // type: 'foo', - // foo: {}, - // migrationVersion: { - // foo: '7.13.0', - // }, - // }, - // contains migrated index with 8.0 aliases to skip migration, but run outdated doc search - dataArchive: Path.join( - __dirname, - 'archives', - '8.0.0_migrated_with_corrupt_outdated_docs.zip' - ), - }, - }, - }); - - root = createRoot(); - - esServer = await startES(); - await root.preboot(); - const coreSetup = await root.setup(); - - coreSetup.savedObjects.registerType({ - name: 'foo', - hidden: false, - mappings: { properties: {} }, - namespaceType: 'agnostic', - migrations: { - '7.14.0': (doc) => doc, - }, - }); - coreSetup.savedObjects.registerType({ - name: 'bar', - hidden: false, - mappings: { properties: {} }, - namespaceType: 'agnostic', - migrations: { - '7.14.0': (doc) => doc, - }, - }); - coreSetup.savedObjects.registerType({ - name: 'baz', - hidden: false, - mappings: { properties: {} }, - namespaceType: 'agnostic', - migrations: { - '7.14.0': (doc) => doc, - }, - }); - try { - await root.start(); - } catch (err) { - const errorMessage = err.message; - expect( - errorMessage.startsWith( - 'Unable to complete saved object migrations for the [.kibana] index: Migrations failed. Reason: 19 corrupt saved object documents were found: ' - ) - ).toBeTruthy(); - expect( - errorMessage.endsWith( - 'To allow migrations to proceed, please delete or fix these documents.' - ) - ).toBeTruthy(); - const expectedCorruptDocIds = [ - '"foo:my_name"', - '"123"', - '"456"', - '"789"', - '"foo:other_name"', - '"bar:123"', - '"baz:123"', - '"bar:345"', - '"bar:890"', - '"baz:456"', - '"baz:789"', - '"bar:other_name"', - '"baz:other_name"', - '"bar:my_name"', - '"baz:my_name"', - '"foo:123"', - '"foo:456"', - '"foo:789"', - '"foo:other"', - ]; - for (const corruptDocId of expectedCorruptDocIds) { - expect(errorMessage.includes(corruptDocId)).toBeTruthy(); - } - } - }); -}); - -function createRoot() { - return createRootWithCorePlugins( - { - migrations: { - skip: false, - batchSize: 5, - }, - logging: { - appenders: { - file: { - type: 'file', - fileName: logFilePath, - layout: { - type: 'json', - }, - }, - }, - loggers: [ - { - name: 'root', - appenders: ['file'], - level: 'info', - }, - ], - }, - }, - { - oss: true, - } - ); -} diff --git a/src/core/server/integration_tests/saved_objects/migrations/group2/multiple_kibana_nodes.test.ts b/src/core/server/integration_tests/saved_objects/migrations/group2/multiple_kibana_nodes.test.ts index 9550e46136a47..9bd876166c246 100644 --- a/src/core/server/integration_tests/saved_objects/migrations/group2/multiple_kibana_nodes.test.ts +++ b/src/core/server/integration_tests/saved_objects/migrations/group2/multiple_kibana_nodes.test.ts @@ -107,8 +107,7 @@ async function createRoot({ logFileName }: CreateRootConfig) { // suite is very long, the 10mins default can cause timeouts jest.setTimeout(15 * 60 * 1000); -// FLAKY: https://github.com/elastic/kibana/issues/156117 -describe.skip('migration v2', () => { +describe('migration v2', () => { let esServer: TestElasticsearchUtils; let rootA: Root; let rootB: Root; diff --git a/src/core/server/integration_tests/saved_objects/migrations/group2/outdated_docs.test.ts b/src/core/server/integration_tests/saved_objects/migrations/group2/outdated_docs.test.ts deleted file mode 100644 index 2c45dab10dff2..0000000000000 --- a/src/core/server/integration_tests/saved_objects/migrations/group2/outdated_docs.test.ts +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import Path from 'path'; -import Fs from 'fs'; -import Util from 'util'; -import { kibanaPackageJson as pkg } from '@kbn/repo-info'; -import { - createRootWithCorePlugins, - createTestServers, - type TestElasticsearchUtils, -} from '@kbn/core-test-helpers-kbn-server'; -import type { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; -import { Root } from '@kbn/core-root-server-internal'; - -const logFilePath = Path.join(__dirname, 'outdated_docs.log'); - -const asyncUnlink = Util.promisify(Fs.unlink); -async function removeLogFile() { - // ignore errors if it doesn't exist - await asyncUnlink(logFilePath).catch(() => void 0); -} - -// Failing 9.0 version update: https://github.com/elastic/kibana/issues/192624 -describe.skip('migration v2', () => { - let esServer: TestElasticsearchUtils; - let root: Root; - - beforeAll(async () => { - await removeLogFile(); - }); - - afterAll(async () => { - if (root) { - await root.shutdown(); - } - if (esServer) { - await esServer.stop(); - } - }); - - it('migrates the documents to the highest version', async () => { - const migratedIndexAlias = `.kibana_${pkg.version}`; - const { startES } = createTestServers({ - adjustTimeout: (t: number) => jest.setTimeout(t), - settings: { - es: { - license: 'basic', - // original SO: - // { - // type: 'foo', - // foo: {}, - // migrationVersion: { - // foo: '7.13.0', - // }, - // }, - // contains migrated index with 8.0 aliases to skip migration, but run outdated doc search - dataArchive: Path.join( - __dirname, - '..', - 'archives', - '8.0.0_migrated_with_outdated_docs.zip' - ), - }, - }, - }); - - root = createRoot(); - - esServer = await startES(); - await root.preboot(); - const coreSetup = await root.setup(); - - coreSetup.savedObjects.registerType({ - name: 'foo', - hidden: false, - mappings: { properties: {} }, - namespaceType: 'agnostic', - migrations: { - '7.14.0': (doc) => doc, - }, - }); - - const coreStart = await root.start(); - const esClient = coreStart.elasticsearch.client.asInternalUser; - - const migratedDocs = await fetchDocs(esClient, migratedIndexAlias); - - expect(migratedDocs.length).toBe(1); - const [doc] = migratedDocs; - expect(doc._source.coreMigrationVersion).toBe('8.8.0'); - expect(doc._source.typeMigrationVersion).toBe('7.14.0'); - }); -}); - -function createRoot() { - return createRootWithCorePlugins( - { - migrations: { - skip: false, - }, - logging: { - appenders: { - file: { - type: 'file', - fileName: logFilePath, - layout: { - type: 'json', - }, - }, - }, - loggers: [ - { - name: 'root', - level: 'info', - appenders: ['file'], - }, - ], - }, - }, - { - oss: true, - } - ); -} - -async function fetchDocs(esClient: ElasticsearchClient, index: string) { - const body = await esClient.search({ - index, - body: { - query: { - bool: { - should: [ - { - term: { type: 'foo' }, - }, - ], - }, - }, - }, - }); - - return body.hits.hits; -} diff --git a/src/core/server/integration_tests/saved_objects/migrations/group5/active_delete.test.ts b/src/core/server/integration_tests/saved_objects/migrations/group5/active_delete.test.ts index 8cfcb1ea5b745..9c42e966b329e 100644 --- a/src/core/server/integration_tests/saved_objects/migrations/group5/active_delete.test.ts +++ b/src/core/server/integration_tests/saved_objects/migrations/group5/active_delete.test.ts @@ -137,32 +137,6 @@ describe('when upgrading to a new stack version', () => { await clearLog(); }); - it('fails if unknown documents exist', async () => { - // remove the 'deprecated' type from the mappings, so that it is considered unknown - const { runMigrations } = await getUpToDateMigratorTestKit({ - filterDeprecated: true, - }); - - try { - await runMigrations(); - } catch (err) { - const errorMessage = err.message; - expect(errorMessage).toMatch( - `Unable to complete saved object migrations for the [${defaultKibanaIndex}] index: Migration failed because some documents were found which use unknown saved object types:` - ); - expect(errorMessage).toMatch( - 'To proceed with the migration you can configure Kibana to discard unknown saved objects for this migration.' - ); - expect(errorMessage).toMatch(/deprecated:.*\(type: "deprecated"\)/); - } - - const logs = await readLog(); - expect(logs).toMatch('INIT -> WAIT_FOR_YELLOW_SOURCE.'); - expect(logs).toMatch('WAIT_FOR_YELLOW_SOURCE -> UPDATE_SOURCE_MAPPINGS_PROPERTIES.'); - expect(logs).toMatch('UPDATE_SOURCE_MAPPINGS_PROPERTIES -> CLEANUP_UNKNOWN_AND_EXCLUDED.'); - expect(logs).toMatch('CLEANUP_UNKNOWN_AND_EXCLUDED -> FATAL.'); - }); - it('proceeds if there are no unknown documents', async () => { const { client, runMigrations } = await getUpToDateMigratorTestKit(); @@ -289,35 +263,6 @@ describe('when upgrading to a new stack version', () => { await clearLog(); }); - it('fails if unknown documents exist', async () => { - const { runMigrations } = await getCompatibleMigratorTestKit({ - filterDeprecated: true, // remove the 'deprecated' type from the mappings, so that it is considered unknown - }); - - try { - await runMigrations(); - } catch (err) { - const errorMessage = err.message; - expect(errorMessage).toMatch( - `Unable to complete saved object migrations for the [${defaultKibanaIndex}] index: Migration failed because some documents were found which use unknown saved object types:` - ); - expect(errorMessage).toMatch( - 'To proceed with the migration you can configure Kibana to discard unknown saved objects for this migration.' - ); - expect(errorMessage).toMatch(/deprecated:.*\(type: "deprecated"\)/); - } - - const logs = await readLog(); - expect(logs).toMatch(`[${defaultKibanaIndex}] INIT -> WAIT_FOR_YELLOW_SOURCE.`); - expect(logs).toMatch( - `[${defaultKibanaIndex}] WAIT_FOR_YELLOW_SOURCE -> UPDATE_SOURCE_MAPPINGS_PROPERTIES.` - ); // this step is run only if mappings are compatible but NOT equal - expect(logs).toMatch( - `[${defaultKibanaIndex}] UPDATE_SOURCE_MAPPINGS_PROPERTIES -> CLEANUP_UNKNOWN_AND_EXCLUDED.` - ); - expect(logs).toMatch(`[${defaultKibanaIndex}] CLEANUP_UNKNOWN_AND_EXCLUDED -> FATAL.`); - }); - it('proceeds if there are no unknown documents', async () => { const { client, runMigrations } = await getCompatibleMigratorTestKit(); @@ -376,10 +321,11 @@ describe('when upgrading to a new stack version', () => { expect(logs).toMatch('MARK_VERSION_INDEX_READY -> DONE'); const counts = await getAggregatedTypesCount(client); + // for 'complex' objects, we discard second half and also multiples of 100 expect(counts).toMatchInlineSnapshot(` Object { "basic": 10, - "complex": 5, + "complex": 4, "deprecated": 10, "task": 10, } diff --git a/src/core/server/integration_tests/saved_objects/migrations/group6/single_migrator_failures.test.ts b/src/core/server/integration_tests/saved_objects/migrations/group6/single_migrator_failures.test.ts index b000f0d775358..358ceea5c006a 100644 --- a/src/core/server/integration_tests/saved_objects/migrations/group6/single_migrator_failures.test.ts +++ b/src/core/server/integration_tests/saved_objects/migrations/group6/single_migrator_failures.test.ts @@ -26,8 +26,8 @@ import { getReindexingBaselineTypes, } from '../kibana_migrator_test_kit.fixtures'; -export const logFilePathFirstRun = join(__dirname, 'dot_kibana_split_1st_run.test.log'); -export const logFilePathSecondRun = join(__dirname, 'dot_kibana_split_2nd_run.test.log'); +export const logFilePathFirstRun = join(__dirname, 'single_migrator_failures_1st_run.test.log'); +export const logFilePathSecondRun = join(__dirname, 'single_migrator_failures_2nd_run.test.log'); const kibanaSplitIndex = `${defaultKibanaIndex}_split`; const tasksToNewIndex = getReindexingBaselineTypes(true).map((type) => { @@ -74,6 +74,7 @@ describe('split .kibana index into multiple system indices', () => { settings: { migrations: { discardUnknownObjects: nextMinor, + discardCorruptObjects: nextMinor, }, }, }); @@ -249,6 +250,7 @@ describe('split .kibana index into multiple system indices', () => { settings: { migrations: { discardUnknownObjects: nextMinor, + discardCorruptObjects: nextMinor, }, }, }); diff --git a/src/core/server/integration_tests/saved_objects/migrations/kibana_migrator_test_kit.fixtures.ts b/src/core/server/integration_tests/saved_objects/migrations/kibana_migrator_test_kit.fixtures.ts index 73437885fc7a7..1ec300c075ff2 100644 --- a/src/core/server/integration_tests/saved_objects/migrations/kibana_migrator_test_kit.fixtures.ts +++ b/src/core/server/integration_tests/saved_objects/migrations/kibana_migrator_test_kit.fixtures.ts @@ -141,6 +141,40 @@ export const getReindexingBaselineTypes = (filterDeprecated: boolean) => createdAt: { type: 'date' }, }, }, + { + type: 'unsafe_transform', + transformFn: (doc) => { + if (doc.attributes.value % 100 === 0) { + throw new Error( + `Cannot convert 'complex' objects with values that are multiple of 100 ${doc.id}` + ); + } + return { document: doc }; + }, + }, + ], + }, + }, + }; + } else if (type.name === 'task') { + return { + ...type, + mappings: { + properties: { + ...type.mappings.properties, + lastRun: { type: 'date' }, + }, + }, + modelVersions: { + ...type.modelVersions, + 2: { + changes: [ + { + type: 'mappings_addition', + addedMappings: { + lastRun: { type: 'date' }, + }, + }, ], }, }, @@ -268,13 +302,21 @@ export const getCompatibleMigratorTestKit = async ({ export const getReindexingMigratorTestKit = async ({ logFilePath = defaultLogFilePath, filterDeprecated = false, + types = getReindexingBaselineTypes(filterDeprecated), kibanaVersion = nextMinor, settings = {}, }: GetMutatedMigratorParams = {}) => { return await getKibanaMigratorTestKit({ logFilePath, - types: getReindexingBaselineTypes(filterDeprecated), + types, kibanaVersion, - settings, + settings: { + ...settings, + migrations: { + discardUnknownObjects: nextMinor, + discardCorruptObjects: nextMinor, + ...settings.migrations, + }, + }, }); }; diff --git a/src/plugins/console/common/text_object.ts b/src/plugins/console/common/text_object.ts index 26fa59ec437cc..2fb6830f260da 100644 --- a/src/plugins/console/common/text_object.ts +++ b/src/plugins/console/common/text_object.ts @@ -34,5 +34,5 @@ export interface TextObject { * * Used to re-populate a text editor buffer. */ - text: string; + text: string | undefined; } diff --git a/src/plugins/console/public/application/containers/config/config.tsx b/src/plugins/console/public/application/containers/config/config.tsx index 503fdbd9c7354..8605a099182ba 100644 --- a/src/plugins/console/public/application/containers/config/config.tsx +++ b/src/plugins/console/public/application/containers/config/config.tsx @@ -8,16 +8,20 @@ */ import React from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiSpacer } from '@elastic/eui'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiSpacer, + useIsWithinBreakpoints, +} from '@elastic/eui'; import { Settings } from './settings'; import { Variables } from './variables'; -export interface Props { - isVerticalLayout: boolean; -} +export function Config() { + const isVerticalLayout = useIsWithinBreakpoints(['xs', 's', 'm']); -export function Config({ isVerticalLayout }: Props) { return ( void; } -export const Editor = memo( - ({ loading, isVerticalLayout, inputEditorValue, setInputEditorValue }: Props) => { - const { - services: { storage, objectStorageClient }, - } = useServicesContext(); +export const Editor = memo(({ loading, inputEditorValue, setInputEditorValue }: Props) => { + const { + services: { storage, objectStorageClient }, + } = useServicesContext(); - const editorValueRef = useRef(null); - const { currentTextObject } = useEditorReadContext(); - const { - requestInFlight, - lastResult: { data: requestData, error: requestError }, - } = useRequestReadContext(); + const { currentTextObject } = useEditorReadContext(); - const dispatch = useRequestActionContext(); - const editorDispatch = useEditorActionContext(); + const { + requestInFlight, + lastResult: { data: requestData, error: requestError }, + } = useRequestReadContext(); - const [fetchingAutocompleteEntities, setFetchingAutocompleteEntities] = useState(false); + const dispatch = useRequestActionContext(); + const editorDispatch = useEditorActionContext(); - useEffect(() => { - const debouncedSetFechingAutocompleteEntities = debounce( - setFetchingAutocompleteEntities, - DEBOUNCE_DELAY - ); - const subscription = getAutocompleteInfo().isLoading$.subscribe( - debouncedSetFechingAutocompleteEntities - ); + const [fetchingAutocompleteEntities, setFetchingAutocompleteEntities] = useState(false); - return () => { - subscription.unsubscribe(); - debouncedSetFechingAutocompleteEntities.cancel(); - }; - }, []); + useEffect(() => { + const debouncedSetFechingAutocompleteEntities = debounce( + setFetchingAutocompleteEntities, + DEBOUNCE_DELAY + ); + const subscription = getAutocompleteInfo().isLoading$.subscribe( + debouncedSetFechingAutocompleteEntities + ); - const [firstPanelSize, secondPanelSize] = storage.get(StorageKeys.SIZE, [ - INITIAL_PANEL_SIZE, - INITIAL_PANEL_SIZE, - ]); + return () => { + subscription.unsubscribe(); + debouncedSetFechingAutocompleteEntities.cancel(); + }; + }, []); - /* eslint-disable-next-line react-hooks/exhaustive-deps */ - const onPanelSizeChange = useCallback( - debounce((sizes) => { - storage.set(StorageKeys.SIZE, Object.values(sizes)); - }, 300), - [] - ); + const [firstPanelSize, secondPanelSize] = storage.get(StorageKeys.SIZE, [ + INITIAL_PANEL_SIZE, + INITIAL_PANEL_SIZE, + ]); - /* eslint-disable-next-line react-hooks/exhaustive-deps */ - const debouncedUpdateLocalStorageValue = useCallback( - debounce((textObject: TextObject) => { - editorValueRef.current = textObject; - objectStorageClient.text.update(textObject); - }, DEBOUNCE_DELAY), - [] - ); + const isVerticalLayout = useIsWithinBreakpoints(['xs', 's', 'm']); - useEffect(() => { - return () => { - editorDispatch({ - type: 'setCurrentTextObject', - payload: editorValueRef.current!, - }); - }; - }, [editorDispatch]); + /* eslint-disable-next-line react-hooks/exhaustive-deps */ + const onPanelSizeChange = useCallback( + debounce((sizes) => { + storage.set(StorageKeys.SIZE, Object.values(sizes)); + }, 300), + [] + ); - // Always keep the localstorage in sync with the value in the editor - // to avoid losing the text object when the user navigates away from the shell - useEffect(() => { - // Only update when its not empty, this is to avoid setting the localstorage value - // to an empty string that will then be replaced by the example request. - if (inputEditorValue !== '') { - const textObject = { - ...currentTextObject, - text: inputEditorValue, - updatedAt: Date.now(), - } as TextObject; + /* eslint-disable-next-line react-hooks/exhaustive-deps */ + const debouncedUpdateLocalStorageValue = useCallback( + debounce((newValue: string | undefined) => { + const textObject = { + ...currentTextObject, + text: newValue, + updatedAt: Date.now(), + } as TextObject; - debouncedUpdateLocalStorageValue(textObject); - } - /* eslint-disable-next-line react-hooks/exhaustive-deps */ - }, [inputEditorValue, debouncedUpdateLocalStorageValue]); + objectStorageClient.text.update(textObject); - const data = getResponseWithMostSevereStatusCode(requestData) ?? requestError; - const isLoading = loading || requestInFlight; + editorDispatch({ + type: 'setCurrentTextObject', + payload: textObject, + }); + }, DEBOUNCE_DELAY), + [] + ); - if (!currentTextObject) return null; + // Always keep the localstorage value in sync with the value in the editor + // to avoid losing the text object when the user navigates away from the shell + useEffect(() => { + debouncedUpdateLocalStorageValue(inputEditorValue); + }, [debouncedUpdateLocalStorageValue, inputEditorValue]); - return ( - <> - {fetchingAutocompleteEntities ? ( -
- -
- ) : null} - onPanelSizeChange(sizes)} - data-test-subj="consoleEditorContainer" - > - {(EuiResizablePanel, EuiResizableButton) => ( - <> - + {fetchingAutocompleteEntities ? ( +
+ +
+ ) : null} + onPanelSizeChange(sizes)} + data-test-subj="consoleEditorContainer" + > + {(EuiResizablePanel, EuiResizableButton) => ( + <> + + - + {loading ? ( + + ) : ( + + )} + + + {!loading && ( - {loading ? ( - - ) : ( - - )} - - - {!loading && ( - { + setInputEditorValue(''); }} - className="consoleEditorPanel" > - setInputEditorValue('')} - > - {i18n.translate('console.editor.clearConsoleInputButton', { - defaultMessage: 'Clear this input', - })} - - - )} - - + {i18n.translate('console.editor.clearConsoleInputButton', { + defaultMessage: 'Clear this input', + })} + + + )} + +
- + - - + + + {data ? ( + + ) : isLoading ? ( + + ) : ( + + )} + + + {(data || isLoading) && ( - {data ? ( - - ) : isLoading ? ( - - ) : ( - - )} - + + + dispatch({ type: 'cleanRequest', payload: undefined })} + > + {i18n.translate('console.editor.clearConsoleOutputButton', { + defaultMessage: 'Clear this output', + })} + + - {(data || isLoading) && ( - - - - dispatch({ type: 'cleanRequest', payload: undefined })} - > - {i18n.translate('console.editor.clearConsoleOutputButton', { - defaultMessage: 'Clear this output', - })} - - - - - - - - - )} - - - - )} -
- - ); - } -); + + + + + + )} + + + + )} + + + ); +}); diff --git a/src/plugins/console/public/application/containers/editor/hooks/use_set_initial_value.ts b/src/plugins/console/public/application/containers/editor/hooks/use_set_initial_value.ts index 961ea586bc291..9e96c1af2734b 100644 --- a/src/plugins/console/public/application/containers/editor/hooks/use_set_initial_value.ts +++ b/src/plugins/console/public/application/containers/editor/hooks/use_set_initial_value.ts @@ -61,7 +61,7 @@ export const useSetInitialValue = (params: SetInitialValueParams) => { if (parsedURL.origin === 'https://www.elastic.co') { const resp = await fetch(parsedURL); const data = await resp.text(); - setValue(`${localStorageValue}\n\n${data}`); + setValue(`${localStorageValue ?? ''}\n\n${data}`); } else { toasts.addWarning( i18n.translate('console.monaco.loadFromDataUnrecognizedUrlErrorMessage', { @@ -107,7 +107,8 @@ export const useSetInitialValue = (params: SetInitialValueParams) => { if (loadFromParam) { loadBufferFromRemote(loadFromParam); } else { - setValue(localStorageValue || DEFAULT_INPUT_VALUE); + // Only set to default input value if the localstorage value is undefined + setValue(localStorageValue ?? DEFAULT_INPUT_VALUE); } return () => { diff --git a/src/plugins/console/public/application/containers/history/history.tsx b/src/plugins/console/public/application/containers/history/history.tsx index 93777a364356e..f6d18d6d06dd1 100644 --- a/src/plugins/console/public/application/containers/history/history.tsx +++ b/src/plugins/console/public/application/containers/history/history.tsx @@ -26,6 +26,7 @@ import { EuiFormFieldset, EuiCheckableCard, EuiResizableContainer, + useIsWithinBreakpoints, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; @@ -77,11 +78,7 @@ const CheckeableCardLabel = ({ historyItem }: { historyItem: HistoryProps }) => ); }; -interface Props { - isVerticalLayout: boolean; -} - -export function History({ isVerticalLayout }: Props) { +export function History() { const { euiTheme } = useEuiTheme(); const { services: { history, routeHistory }, @@ -99,6 +96,8 @@ export function History({ isVerticalLayout }: Props) { const [viewingReq, setViewingReq] = useState(null); + const isVerticalLayout = useIsWithinBreakpoints(['xs', 's', 'm']); + const initialize = useCallback(() => { const nextSelectedIndex = 0; setViewingReq(requests[nextSelectedIndex]); diff --git a/src/plugins/console/public/application/containers/main/main.tsx b/src/plugins/console/public/application/containers/main/main.tsx index ef55f5406edaf..1b5116b2307a6 100644 --- a/src/plugins/console/public/application/containers/main/main.tsx +++ b/src/plugins/console/public/application/containers/main/main.tsx @@ -18,7 +18,6 @@ import { EuiButtonEmpty, EuiHorizontalRule, EuiScreenReaderOnly, - useIsWithinBreakpoints, useEuiOverflowScroll, useEuiTheme, } from '@elastic/eui'; @@ -84,8 +83,6 @@ export function Main({ currentTabProp, isEmbeddable = false }: MainProps) { services: { notifications, routeHistory }, } = useServicesContext(); - const isVerticalLayout = useIsWithinBreakpoints(['xs', 's', 'm']); - const storageTourState = localStorage.getItem(TOUR_STORAGE_KEY); const initialTourState = storageTourState ? JSON.parse(storageTourState) : INITIAL_TOUR_CONFIG; const [tourStepProps, actions, tourState] = useEuiTour(getTourSteps(docLinks), initialTourState); @@ -186,6 +183,8 @@ export function Main({ currentTabProp, isEmbeddable = false }: MainProps) { ); } + if (!currentTextObject) return null; + const shortcutsButton = ( )} - {currentTab === HISTORY_TAB_ID && } - {currentTab === CONFIG_TAB_ID && } + {currentTab === HISTORY_TAB_ID && } + {currentTab === CONFIG_TAB_ID && } { const newObject = await objectStorageClient.text.create({ createdAt: Date.now(), updatedAt: Date.now(), - text: '', + text: undefined, }); dispatch({ type: 'setCurrentTextObject', payload: newObject }); } else { - dispatch({ - type: 'setCurrentTextObject', - // For backwards compatibility, we sort here according to date created to - // always take the first item created. - payload: results.sort((a, b) => a.createdAt - b.createdAt)[0], - }); + // For backwards compatibility, we sort here according to date created to + // always take the first item created. + const lastObject = results.sort((a, b) => a.createdAt - b.createdAt)[0]; + if (lastObject.text === '') { + // If the last stored text is empty, add a new object with undefined text so that the default input is displayed at initial render + const textObject = { + ...lastObject, + text: undefined, + updatedAt: Date.now(), + } as TextObject; + + objectStorageClient.text.update(textObject); + dispatch({ type: 'setCurrentTextObject', payload: textObject }); + } else { + dispatch({ + type: 'setCurrentTextObject', + payload: lastObject, + }); + } } } catch (e) { setError(e); diff --git a/src/plugins/console/tsconfig.json b/src/plugins/console/tsconfig.json index a13333dfa8b30..2b0f6127cd4af 100644 --- a/src/plugins/console/tsconfig.json +++ b/src/plugins/console/tsconfig.json @@ -33,7 +33,7 @@ "@kbn/react-kibana-mount", "@kbn/ui-theme", "@kbn/core-doc-links-browser", - "@kbn/shared-ux-router" + "@kbn/shared-ux-router", ], "exclude": [ "target/**/*", diff --git a/src/plugins/management/public/components/landing/classic_empty_prompt.tsx b/src/plugins/management/public/components/landing/classic_empty_prompt.tsx new file mode 100644 index 0000000000000..83bde6178644d --- /dev/null +++ b/src/plugins/management/public/components/landing/classic_empty_prompt.tsx @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ +import React, { type FC } from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { EuiHorizontalRule } from '@elastic/eui'; +import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template'; + +interface Props { + kibanaVersion: string; +} + +export const ClassicEmptyPrompt: FC = ({ kibanaVersion }) => { + return ( + + + + } + body={ + <> +

+ +

+ +

+ +

+ + } + /> + ); +}; diff --git a/src/plugins/management/public/components/landing/landing.test.tsx b/src/plugins/management/public/components/landing/landing.test.tsx index 99b499f0b8537..2f9371bbde031 100644 --- a/src/plugins/management/public/components/landing/landing.test.tsx +++ b/src/plugins/management/public/components/landing/landing.test.tsx @@ -9,6 +9,7 @@ import React from 'react'; import { merge } from 'lodash'; +import { coreMock } from '@kbn/core/public/mocks'; import { registerTestBed, AsyncTestBedConfig, TestBed } from '@kbn/test-jest-helpers'; import { AppContextProvider } from '../management_app/management_context'; @@ -45,6 +46,7 @@ export const WithAppDependencies = kibanaVersion: '8.10.0', cardsNavigationConfig: { enabled: true }, sections: sectionsMock, + chromeStyle: 'classic', }; return ( @@ -88,4 +90,32 @@ describe('Landing Page', () => { expect(exists('managementHome')).toBe(true); }); }); + + describe('Empty prompt', () => { + test('Renders the default empty prompt when chromeStyle is "classic"', async () => { + testBed = await setupLandingPage({ + chromeStyle: 'classic', + cardsNavigationConfig: { enabled: false }, + }); + + const { exists } = testBed; + + expect(exists('managementHome')).toBe(true); + }); + + test('Renders the solution empty prompt when chromeStyle is "project"', async () => { + const coreStart = coreMock.createStart(); + + testBed = await setupLandingPage({ + chromeStyle: 'project', + cardsNavigationConfig: { enabled: false }, + coreStart, + }); + + const { exists } = testBed; + + expect(exists('managementHome')).toBe(false); + expect(exists('managementHomeSolution')).toBe(true); + }); + }); }); diff --git a/src/plugins/management/public/components/landing/landing.tsx b/src/plugins/management/public/components/landing/landing.tsx index d17c03a5f0fa8..ffa5c7d0fd7e2 100644 --- a/src/plugins/management/public/components/landing/landing.tsx +++ b/src/plugins/management/public/components/landing/landing.tsx @@ -8,13 +8,13 @@ */ import React, { useEffect } from 'react'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { EuiHorizontalRule } from '@elastic/eui'; -import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template'; + import { EuiPageBody } from '@elastic/eui'; import { CardsNavigation } from '@kbn/management-cards-navigation'; import { useAppContext } from '../management_app/management_context'; +import { ClassicEmptyPrompt } from './classic_empty_prompt'; +import { SolutionEmptyPrompt } from './solution_empty_prompt'; interface ManagementLandingPageProps { onAppMounted: (id: string) => void; @@ -25,7 +25,8 @@ export const ManagementLandingPage = ({ setBreadcrumbs, onAppMounted, }: ManagementLandingPageProps) => { - const { appBasePath, sections, kibanaVersion, cardsNavigationConfig } = useAppContext(); + const { appBasePath, sections, kibanaVersion, cardsNavigationConfig, chromeStyle, coreStart } = + useAppContext(); setBreadcrumbs(); useEffect(() => { @@ -45,36 +46,11 @@ export const ManagementLandingPage = ({ ); } - return ( - - - - } - body={ - <> -

- -

- -

- -

- - } - /> - ); + if (!chromeStyle) return null; + + if (chromeStyle === 'project') { + return ; + } + + return ; }; diff --git a/src/plugins/management/public/components/landing/solution_empty_prompt.tsx b/src/plugins/management/public/components/landing/solution_empty_prompt.tsx new file mode 100644 index 0000000000000..63e3e91fd6ff3 --- /dev/null +++ b/src/plugins/management/public/components/landing/solution_empty_prompt.tsx @@ -0,0 +1,113 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ +import React, { type FC } from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { i18n } from '@kbn/i18n'; +import { EuiButton, EuiLink } from '@elastic/eui'; +import { type CoreStart } from '@kbn/core/public'; +import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template'; + +interface Props { + kibanaVersion: string; + coreStart: CoreStart; +} + +const IndicesLink: FC<{ coreStart: CoreStart }> = ({ coreStart }) => ( + + {i18n.translate('management.landing.subhead.indicesLink', { + defaultMessage: 'indices', + })} + +); + +const DataViewsLink: FC<{ coreStart: CoreStart }> = ({ coreStart }) => ( + + {i18n.translate('management.landing.subhead.dataViewsLink', { + defaultMessage: 'data views', + })} + +); + +const IngestPipelinesLink: FC<{ coreStart: CoreStart }> = ({ coreStart }) => ( + + {i18n.translate('management.landing.subhead.ingestPipelinesLink', { + defaultMessage: 'ingest pipelines', + })} + +); + +const UsersLink: FC<{ coreStart: CoreStart }> = ({ coreStart }) => ( + + {i18n.translate('management.landing.subhead.usersLink', { + defaultMessage: 'users', + })} + +); + +export const SolutionEmptyPrompt: FC = ({ kibanaVersion, coreStart }) => { + return ( + + + + } + body={ + <> +

+ , + dataViewsLink: , + ingestPipelinesLink: , + usersLink: , + }} + /> +

+ +

+ { + coreStart.chrome.sideNav.setPanelSelectedNode('stack_management'); + }} + data-test-subj="viewAllStackMngtPagesButton" + > + + +

+ + } + /> + ); +}; diff --git a/src/plugins/management/public/components/management_app/management_app.tsx b/src/plugins/management/public/components/management_app/management_app.tsx index e4ffd237b94c9..849d8f9eb0341 100644 --- a/src/plugins/management/public/components/management_app/management_app.tsx +++ b/src/plugins/management/public/components/management_app/management_app.tsx @@ -10,7 +10,7 @@ import './management_app.scss'; import React, { useState, useEffect, useCallback } from 'react'; -import { BehaviorSubject } from 'rxjs'; +import { BehaviorSubject, Observable } from 'rxjs'; import { i18n } from '@kbn/i18n'; import { AppMountParameters, ChromeBreadcrumb, ScopedHistory } from '@kbn/core/public'; @@ -21,6 +21,7 @@ import { reactRouterNavigate } from '@kbn/kibana-react-plugin/public'; import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render'; import { KibanaPageTemplate, KibanaPageTemplateProps } from '@kbn/shared-ux-page-kibana-template'; import useObservable from 'react-use/lib/useObservable'; +import type { ChromeStyle } from '@kbn/core-chrome-browser'; import { AppContextProvider } from './management_context'; import { ManagementSection, @@ -29,7 +30,7 @@ import { } from '../../utils'; import { ManagementRouter } from './management_router'; import { managementSidebarNav } from '../management_sidebar_nav/management_sidebar_nav'; -import { SectionsServiceStart, NavigationCardsSubject } from '../../types'; +import { SectionsServiceStart, NavigationCardsSubject, AppDependencies } from '../../types'; interface ManagementAppProps { appBasePath: string; @@ -44,14 +45,17 @@ export interface ManagementAppDependencies { setBreadcrumbs: (newBreadcrumbs: ChromeBreadcrumb[]) => void; isSidebarEnabled$: BehaviorSubject; cardsNavigationConfig$: BehaviorSubject; + chromeStyle$: Observable; } export const ManagementApp = ({ dependencies, history, appBasePath }: ManagementAppProps) => { - const { coreStart, setBreadcrumbs, isSidebarEnabled$, cardsNavigationConfig$ } = dependencies; + const { coreStart, setBreadcrumbs, isSidebarEnabled$, cardsNavigationConfig$, chromeStyle$ } = + dependencies; const [selectedId, setSelectedId] = useState(''); const [sections, setSections] = useState(); const isSidebarEnabled = useObservable(isSidebarEnabled$); const cardsNavigationConfig = useObservable(cardsNavigationConfig$); + const chromeStyle = useObservable(chromeStyle$); const onAppMounted = useCallback((id: string) => { setSelectedId(id); @@ -102,11 +106,13 @@ export const ManagementApp = ({ dependencies, history, appBasePath }: Management } : undefined; - const contextDependencies = { + const contextDependencies: AppDependencies = { appBasePath, sections, cardsNavigationConfig, kibanaVersion: dependencies.kibanaVersion, + coreStart, + chromeStyle, }; return ( diff --git a/src/plugins/management/public/plugin.tsx b/src/plugins/management/public/plugin.tsx index 8804e8010c2e9..8f8f0f6c0339b 100644 --- a/src/plugins/management/public/plugin.tsx +++ b/src/plugins/management/public/plugin.tsx @@ -119,6 +119,7 @@ export class ManagementPlugin async mount(params: AppMountParameters) { const { renderApp } = await import('./application'); const [coreStart, deps] = await core.getStartServices(); + const chromeStyle$ = coreStart.chrome.getChromeStyle$(); return renderApp(params, { sections: getSectionsServiceStartPrivate(), @@ -135,6 +136,7 @@ export class ManagementPlugin }, isSidebarEnabled$: managementPlugin.isSidebarEnabled$, cardsNavigationConfig$: managementPlugin.cardsNavigationConfig$, + chromeStyle$, }); }, }); diff --git a/src/plugins/management/public/types.ts b/src/plugins/management/public/types.ts index f5bb426ea689f..2e6f900de6298 100644 --- a/src/plugins/management/public/types.ts +++ b/src/plugins/management/public/types.ts @@ -8,10 +8,17 @@ */ import { Observable } from 'rxjs'; -import { ScopedHistory, Capabilities, ThemeServiceStart } from '@kbn/core/public'; +import { + ScopedHistory, + Capabilities, + ThemeServiceStart, + CoreStart, + ChromeBreadcrumb, + CoreTheme, +} from '@kbn/core/public'; import type { LocatorPublic } from '@kbn/share-plugin/common'; -import { ChromeBreadcrumb, CoreTheme } from '@kbn/core/public'; import type { CardsNavigationComponentProps } from '@kbn/management-cards-navigation'; +import type { ChromeStyle } from '@kbn/core-chrome-browser'; import { ManagementSection, RegisterManagementSectionArgs } from './utils'; import type { ManagementAppLocatorParams } from '../common/locator'; @@ -98,6 +105,8 @@ export interface AppDependencies { kibanaVersion: string; sections: ManagementSection[]; cardsNavigationConfig?: NavigationCardsSubject; + chromeStyle?: ChromeStyle; + coreStart: CoreStart; } export interface ConfigSchema { diff --git a/src/plugins/management/tsconfig.json b/src/plugins/management/tsconfig.json index 91e0e940c8fc6..01b1f62b3ba15 100644 --- a/src/plugins/management/tsconfig.json +++ b/src/plugins/management/tsconfig.json @@ -28,6 +28,7 @@ "@kbn/shared-ux-error-boundary", "@kbn/deeplinks-management", "@kbn/react-kibana-context-render", + "@kbn/core-chrome-browser", ], "exclude": [ "target/**/*" diff --git a/tsconfig.base.json b/tsconfig.base.json index 84d2b4a258c1c..7a66911a4bee5 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -1214,6 +1214,8 @@ "@kbn/ml-date-utils/*": ["x-pack/packages/ml/date_utils/*"], "@kbn/ml-error-utils": ["x-pack/packages/ml/error_utils"], "@kbn/ml-error-utils/*": ["x-pack/packages/ml/error_utils/*"], + "@kbn/ml-field-stats-flyout": ["x-pack/packages/ml/field_stats_flyout"], + "@kbn/ml-field-stats-flyout/*": ["x-pack/packages/ml/field_stats_flyout/*"], "@kbn/ml-in-memory-table": ["x-pack/packages/ml/in_memory_table"], "@kbn/ml-in-memory-table/*": ["x-pack/packages/ml/in_memory_table/*"], "@kbn/ml-is-defined": ["x-pack/packages/ml/is_defined"], @@ -1228,6 +1230,8 @@ "@kbn/ml-nested-property/*": ["x-pack/packages/ml/nested_property/*"], "@kbn/ml-number-utils": ["x-pack/packages/ml/number_utils"], "@kbn/ml-number-utils/*": ["x-pack/packages/ml/number_utils/*"], + "@kbn/ml-parse-interval": ["x-pack/packages/ml/parse_interval"], + "@kbn/ml-parse-interval/*": ["x-pack/packages/ml/parse_interval/*"], "@kbn/ml-plugin": ["x-pack/plugins/ml"], "@kbn/ml-plugin/*": ["x-pack/plugins/ml/*"], "@kbn/ml-query-utils": ["x-pack/packages/ml/query_utils"], @@ -1250,6 +1254,8 @@ "@kbn/ml-ui-actions/*": ["x-pack/packages/ml/ui_actions/*"], "@kbn/ml-url-state": ["x-pack/packages/ml/url_state"], "@kbn/ml-url-state/*": ["x-pack/packages/ml/url_state/*"], + "@kbn/ml-validators": ["x-pack/packages/ml/validators"], + "@kbn/ml-validators/*": ["x-pack/packages/ml/validators/*"], "@kbn/mock-idp-plugin": ["packages/kbn-mock-idp-plugin"], "@kbn/mock-idp-plugin/*": ["packages/kbn-mock-idp-plugin/*"], "@kbn/mock-idp-utils": ["packages/kbn-mock-idp-utils"], diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index 8082fa6f8ede2..97aa05deb4a42 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -79,6 +79,7 @@ "packages/ml/data_grid", "packages/ml/data_view_utils", "packages/ml/date_picker", + "packages/ml/field_stats_flyout", "packages/ml/inference_integration_flyout", "packages/ml/trained_models_utils", "packages/ml/category_validator", diff --git a/x-pack/packages/index-management/index_management_shared_types/src/types.ts b/x-pack/packages/index-management/index_management_shared_types/src/types.ts index 1413830f2931f..82dba5ed7e310 100644 --- a/x-pack/packages/index-management/index_management_shared_types/src/types.ts +++ b/x-pack/packages/index-management/index_management_shared_types/src/types.ts @@ -25,6 +25,9 @@ export interface IndexManagementPluginStart { getIndexMappingComponent: (deps: { history: ScopedHistory; }) => React.FC; + getIndexSettingsComponent: (deps: { + history: ScopedHistory; + }) => React.FC; } export interface Index { @@ -56,7 +59,9 @@ export interface IndexMappingProps { index?: Index; showAboutMappings?: boolean; } - +export interface IndexSettingProps { + indexName: string; +} export interface SendRequestResponse { data: D | null; error: E | null; diff --git a/x-pack/packages/ml/field_stats_flyout/README.md b/x-pack/packages/ml/field_stats_flyout/README.md new file mode 100644 index 0000000000000..23a8462c96491 --- /dev/null +++ b/x-pack/packages/ml/field_stats_flyout/README.md @@ -0,0 +1,3 @@ +# @kbn/ml-field-stats-flyout + +This package provides a field statistics flyout component. It is used in the Machine Learning and Transforms plugin. diff --git a/x-pack/plugins/ml/public/application/components/field_stats_flyout/eui_combo_box_with_field_stats.tsx b/x-pack/packages/ml/field_stats_flyout/eui_combo_box_with_field_stats.tsx similarity index 68% rename from x-pack/plugins/ml/public/application/components/field_stats_flyout/eui_combo_box_with_field_stats.tsx rename to x-pack/packages/ml/field_stats_flyout/eui_combo_box_with_field_stats.tsx index 8bb04b7597dd8..a09710da8e398 100644 --- a/x-pack/plugins/ml/public/application/components/field_stats_flyout/eui_combo_box_with_field_stats.tsx +++ b/x-pack/packages/ml/field_stats_flyout/eui_combo_box_with_field_stats.tsx @@ -25,9 +25,25 @@ export const optionCss = css` } `; -export const EuiComboBoxWithFieldStats: FC< - EuiComboBoxProps -> = ({ options, ...restProps }) => { +/** + * Props for the EuiComboBoxWithFieldStats component. + */ +export type EuiComboBoxWithFieldStatsProps = EuiComboBoxProps< + string | number | string[] | undefined +>; + +/** + * React component that wraps the EuiComboBox component and adds field statistics functionality. + * + * @component + * @example + * ```tsx + * + * ``` + * @param {EuiComboBoxWithFieldStatsProps} props - The component props. + */ +export const EuiComboBoxWithFieldStats: FC = (props) => { + const { options, ...restProps } = props; const { renderOption } = useFieldStatsTrigger(); const comboBoxOptions: EuiComboBoxOptionOption[] = useMemo( () => diff --git a/x-pack/plugins/ml/public/application/components/field_stats_flyout/field_stats_content.tsx b/x-pack/packages/ml/field_stats_flyout/field_stats_content.tsx similarity index 73% rename from x-pack/plugins/ml/public/application/components/field_stats_flyout/field_stats_content.tsx rename to x-pack/packages/ml/field_stats_flyout/field_stats_content.tsx index 8e48370c9c785..a649ed778d157 100644 --- a/x-pack/plugins/ml/public/application/components/field_stats_flyout/field_stats_content.tsx +++ b/x-pack/packages/ml/field_stats_flyout/field_stats_content.tsx @@ -15,20 +15,42 @@ import { FieldStats } from '@kbn/unified-field-list/src/components/field_stats'; import { isDefined } from '@kbn/ml-is-defined'; import type { DataView } from '@kbn/data-plugin/common'; import type { TimeRange as TimeRangeMs } from '@kbn/ml-date-picker'; +import { getDefaultDSLQuery } from '@kbn/ml-query-utils'; import moment from 'moment'; import { euiPaletteColorBlind } from '@elastic/eui'; -import { getDefaultDatafeedQuery } from '../../jobs/new_job/utils/new_job_utils'; -import { useFieldStatsFlyoutContext } from './use_field_stats_flytout_context'; +import { useFieldStatsFlyoutContext } from './use_field_stats_flyout_context'; -const DEFAULT_DSL_QUERY = getDefaultDatafeedQuery(); const DEFAULT_COLOR = euiPaletteColorBlind()[0]; -export const FieldStatsContent: FC<{ +/** + * Represents the props for the FieldStatsFlyout component. + */ +export interface FieldStatsFlyoutProps { + /** + * The data view object. + */ dataView: DataView; + /** + * Services required for field statistics. + */ fieldStatsServices: FieldStatsServices; + /** + * Optional time range in milliseconds. + */ timeRangeMs?: TimeRangeMs; + /** + * Optional DSL query for filtering field statistics. + */ dslQuery?: FieldStatsProps['dslQuery']; -}> = ({ dataView: selectedDataView, fieldStatsServices, timeRangeMs, dslQuery }) => { +} + +/** + * Renders the content for the field statistics flyout. + * @param props - The props for the FieldStatsContent component. + * @returns The rendered FieldStatsContent component. + */ +export const FieldStatsContent: FC = (props) => { + const { dataView: selectedDataView, fieldStatsServices, timeRangeMs, dslQuery } = props; const { fieldName } = useFieldStatsFlyoutContext(); // Format timestamp to ISO formatted date strings @@ -58,7 +80,7 @@ export const FieldStatsContent: FC<{ = ({ dataView, fieldStatsServices, timeRangeMs, dslQuery }) => { +} + +/** + * Renders a flyout component for displaying field statistics. + * + * @component + * @example + * ```tsx + * + * ``` + * + * @param {Object} props - The component props. + */ +export const FieldStatsFlyout: FC = (props) => { + const { dataView, fieldStatsServices, timeRangeMs, dslQuery } = props; const { setIsFlyoutVisible, isFlyoutVisible, fieldName } = useFieldStatsFlyoutContext(); const closeFlyout = useCallback(() => setIsFlyoutVisible(false), []); // eslint-disable-line react-hooks/exhaustive-deps diff --git a/x-pack/plugins/ml/public/application/components/field_stats_flyout/field_stats_flyout_provider.tsx b/x-pack/packages/ml/field_stats_flyout/field_stats_flyout_provider.tsx similarity index 72% rename from x-pack/plugins/ml/public/application/components/field_stats_flyout/field_stats_flyout_provider.tsx rename to x-pack/packages/ml/field_stats_flyout/field_stats_flyout_provider.tsx index 9b23ac89afcdb..9dd947f0872f3 100644 --- a/x-pack/plugins/ml/public/application/components/field_stats_flyout/field_stats_flyout_provider.tsx +++ b/x-pack/packages/ml/field_stats_flyout/field_stats_flyout_provider.tsx @@ -7,7 +7,9 @@ import type { PropsWithChildren, FC } from 'react'; import React, { useCallback, useState } from 'react'; +import type { CoreStart } from '@kbn/core/public'; import type { DataView } from '@kbn/data-plugin/common'; +import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; import type { FieldStatsServices } from '@kbn/unified-field-list/src/components/field_stats'; import type { TimeRange as TimeRangeMs } from '@kbn/ml-date-picker'; import type { FieldStatsProps } from '@kbn/unified-field-list/src/components/field_stats'; @@ -16,33 +18,73 @@ import { getProcessedFields } from '@kbn/ml-data-grid'; import { stringHash } from '@kbn/ml-string-hash'; import { lastValueFrom } from 'rxjs'; import { useRef } from 'react'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; import { getMergedSampleDocsForPopulatedFieldsQuery } from './populated_fields/get_merged_populated_fields_query'; -import { useMlKibana } from '../../contexts/kibana'; import { FieldStatsFlyout } from './field_stats_flyout'; -import { MLFieldStatsFlyoutContext } from './use_field_stats_flytout_context'; +import { MLFieldStatsFlyoutContext } from './use_field_stats_flyout_context'; import { PopulatedFieldsCacheManager } from './populated_fields/populated_fields_cache_manager'; -export const FieldStatsFlyoutProvider: FC< - PropsWithChildren<{ - dataView: DataView; - fieldStatsServices: FieldStatsServices; - timeRangeMs?: TimeRangeMs; - dslQuery?: FieldStatsProps['dslQuery']; - disablePopulatedFields?: boolean; - }> -> = ({ - dataView, - fieldStatsServices, - timeRangeMs, - dslQuery, - disablePopulatedFields = false, - children, -}) => { +type Services = CoreStart & { + data: DataPublicPluginStart; +}; + +function useDataSearch() { + const { data } = useKibana().services; + + if (!data) { + throw new Error('Kibana data service not available.'); + } + + return data.search; +} + +/** + * Props for the FieldStatsFlyoutProvider component. + * + * @typedef {Object} FieldStatsFlyoutProviderProps + * @property dataView - The data view object. + * @property fieldStatsServices - Services required for field statistics. + * @property [timeRangeMs] - Optional time range in milliseconds. + * @property [dslQuery] - Optional DSL query for filtering field statistics. + * @property [disablePopulatedFields] - Optional flag to disable populated fields. + */ +export type FieldStatsFlyoutProviderProps = PropsWithChildren<{ + dataView: DataView; + fieldStatsServices: FieldStatsServices; + timeRangeMs?: TimeRangeMs; + dslQuery?: FieldStatsProps['dslQuery']; + disablePopulatedFields?: boolean; +}>; + +/** + * Provides field statistics in a flyout component. + * + * @component + * @example + * ```tsx + * + * {children} + * + * ``` + * + * @param {FieldStatsFlyoutProviderProps} props - The component props. + */ +export const FieldStatsFlyoutProvider: FC = (props) => { const { - services: { - data: { search }, - }, - } = useMlKibana(); + dataView, + fieldStatsServices, + timeRangeMs, + dslQuery, + disablePopulatedFields = false, + children, + } = props; + const search = useDataSearch(); const [isFieldStatsFlyoutVisible, setFieldStatsIsFlyoutVisible] = useState(false); const [fieldName, setFieldName] = useState(); const [fieldValue, setFieldValue] = useState(); diff --git a/x-pack/plugins/ml/public/application/components/field_stats_flyout/field_stats_info_button.tsx b/x-pack/packages/ml/field_stats_flyout/field_stats_info_button.tsx similarity index 66% rename from x-pack/plugins/ml/public/application/components/field_stats_flyout/field_stats_info_button.tsx rename to x-pack/packages/ml/field_stats_flyout/field_stats_info_button.tsx index 0bdd1613a5d5d..936f9550cdda1 100644 --- a/x-pack/plugins/ml/public/application/components/field_stats_flyout/field_stats_info_button.tsx +++ b/x-pack/packages/ml/field_stats_flyout/field_stats_info_button.tsx @@ -7,30 +7,81 @@ import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiToolTip, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import React from 'react'; +import React, { type FC } from 'react'; import { FieldIcon } from '@kbn/react-field'; import { type Field } from '@kbn/ml-anomaly-utils'; -import { useCurrentThemeVars } from '../../contexts/kibana'; -import { getKbnFieldIconType } from '../../../../common/util/get_field_icon_types'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { useCurrentEuiThemeVars } from '@kbn/ml-kibana-theme'; +import { getKbnFieldIconType } from './get_kbn_field_icon_types'; + +function useThemeVars() { + const { theme } = useKibana().services; + + if (!theme) { + throw new TypeError('theme service not available in kibana-react context.'); + } + + return useCurrentEuiThemeVars(theme); +} + +/** + * Represents a field used for statistics. + */ export type FieldForStats = Pick; -export const FieldStatsInfoButton = ({ - field, - label, - onButtonClick, - disabled, - isEmpty = false, - hideTrigger = false, -}: { + +/** + * Represents the props for the FieldStatsInfoButton component. + */ +export interface FieldStatsInfoButtonProps { + /** + * The field for which to display statistics. + */ field: FieldForStats; + /** + * The label for the field. + */ label: string; - searchValue?: string; + /** + * Button click callback function. + * @param field - The field for which to display statistics. + * @returns void + */ + onButtonClick?: (field: FieldForStats) => void; + /** + * If true, the button is disabled. + */ disabled?: boolean; + /** + * If true, the field is empty. + */ isEmpty?: boolean; - onButtonClick?: (field: FieldForStats) => void; + /** + * If true, the trigger is hidden. + */ hideTrigger?: boolean; -}) => { - const themeVars = useCurrentThemeVars(); +} + +/** + * Renders a button component for field statistics information. + * + * @component + * @example + * ```tsx + * + * ``` + * @param {FieldStatsInfoButtonProps} props - The props for the FieldStatsInfoButton component. + */ +export const FieldStatsInfoButton: FC = (props) => { + const { field, label, onButtonClick, disabled, isEmpty, hideTrigger } = props; + const themeVars = useThemeVars(); const emptyFieldMessage = isEmpty ? ' ' + i18n.translate('xpack.ml.newJob.wizard.fieldContextPopover.emptyFieldInSampleDocsMsg', { diff --git a/x-pack/packages/ml/field_stats_flyout/get_kbn_field_icon_types.test.ts b/x-pack/packages/ml/field_stats_flyout/get_kbn_field_icon_types.test.ts new file mode 100644 index 0000000000000..5f930750ae8a8 --- /dev/null +++ b/x-pack/packages/ml/field_stats_flyout/get_kbn_field_icon_types.test.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getKbnFieldIconType } from './get_kbn_field_icon_types'; +import { ES_FIELD_TYPES } from '@kbn/field-types'; + +describe('getKbnFieldIconType', () => { + it('should return "number" for numeric field types', () => { + const numericTypes = [ + ES_FIELD_TYPES.FLOAT, + ES_FIELD_TYPES.HALF_FLOAT, + ES_FIELD_TYPES.SCALED_FLOAT, + ES_FIELD_TYPES.DOUBLE, + ES_FIELD_TYPES.INTEGER, + ES_FIELD_TYPES.LONG, + ES_FIELD_TYPES.SHORT, + ES_FIELD_TYPES.UNSIGNED_LONG, + ]; + + numericTypes.forEach((type) => { + expect(getKbnFieldIconType(type)).toBe('number'); + }); + }); + + it('should return "date" for date field types', () => { + const dateTypes = [ES_FIELD_TYPES.DATE, ES_FIELD_TYPES.DATE_NANOS]; + + dateTypes.forEach((type) => { + expect(getKbnFieldIconType(type)).toBe('date'); + }); + }); + + it('should return the same type for other field types', () => { + const otherTypes = ['keyword', 'text', 'boolean', 'geo_point', 'geo_shape']; + + otherTypes.forEach((type) => { + expect(getKbnFieldIconType(type)).toBe(type); + }); + }); +}); diff --git a/x-pack/plugins/ml/common/util/get_field_icon_types.ts b/x-pack/packages/ml/field_stats_flyout/get_kbn_field_icon_types.ts similarity index 79% rename from x-pack/plugins/ml/common/util/get_field_icon_types.ts rename to x-pack/packages/ml/field_stats_flyout/get_kbn_field_icon_types.ts index 517089d6b0fa6..7ae82d0c28ca8 100644 --- a/x-pack/plugins/ml/common/util/get_field_icon_types.ts +++ b/x-pack/packages/ml/field_stats_flyout/get_kbn_field_icon_types.ts @@ -8,6 +8,12 @@ import { ES_FIELD_TYPES } from '@kbn/field-types'; import type { FieldIconProps } from '@kbn/react-field'; +/** + * Returns the Kibana field icon type based on the provided field type. + * + * @param fieldType - The type of the field for which the icon type is needed. + * @returns The icon type corresponding to the provided field type. + */ export function getKbnFieldIconType(type: string): FieldIconProps['type'] { switch (type) { case ES_FIELD_TYPES.FLOAT: diff --git a/x-pack/packages/ml/field_stats_flyout/index.ts b/x-pack/packages/ml/field_stats_flyout/index.ts new file mode 100644 index 0000000000000..db4d3c5ee7b15 --- /dev/null +++ b/x-pack/packages/ml/field_stats_flyout/index.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { FieldStatsFlyout } from './field_stats_flyout'; +export { FieldStatsContent, type FieldStatsFlyoutProps } from './field_stats_content'; +export { + FieldStatsFlyoutProvider, + type FieldStatsFlyoutProviderProps, +} from './field_stats_flyout_provider'; +export { + MLFieldStatsFlyoutContext, + useFieldStatsFlyoutContext, +} from './use_field_stats_flyout_context'; +export { + FieldStatsInfoButton, + type FieldForStats, + type FieldStatsInfoButtonProps, +} from './field_stats_info_button'; +export { useFieldStatsTrigger } from './use_field_stats_trigger'; +export { + EuiComboBoxWithFieldStats, + type EuiComboBoxWithFieldStatsProps, +} from './eui_combo_box_with_field_stats'; diff --git a/x-pack/plugins/security_solution/common/api/timeline/clean_draft_timelines/clean_draft_timelines_route.ts b/x-pack/packages/ml/field_stats_flyout/jest.config.js similarity index 58% rename from x-pack/plugins/security_solution/common/api/timeline/clean_draft_timelines/clean_draft_timelines_route.ts rename to x-pack/packages/ml/field_stats_flyout/jest.config.js index fd967824370ea..92a117be85f1b 100644 --- a/x-pack/plugins/security_solution/common/api/timeline/clean_draft_timelines/clean_draft_timelines_route.ts +++ b/x-pack/packages/ml/field_stats_flyout/jest.config.js @@ -5,10 +5,8 @@ * 2.0. */ -import * as rt from 'io-ts'; - -import { TimelineTypeLiteralRt } from '../model/api'; - -export const cleanDraftTimelineSchema = rt.type({ - timelineType: TimelineTypeLiteralRt, -}); +module.exports = { + preset: '@kbn/test', + rootDir: '../../../..', + roots: ['/x-pack/packages/ml/field_stats_flyout'], +}; diff --git a/x-pack/packages/ml/field_stats_flyout/kibana.jsonc b/x-pack/packages/ml/field_stats_flyout/kibana.jsonc new file mode 100644 index 0000000000000..4c362fcc84e05 --- /dev/null +++ b/x-pack/packages/ml/field_stats_flyout/kibana.jsonc @@ -0,0 +1,5 @@ +{ + "type": "shared-browser", + "id": "@kbn/ml-field-stats-flyout", + "owner": "@elastic/ml-ui" +} diff --git a/x-pack/packages/ml/field_stats_flyout/package.json b/x-pack/packages/ml/field_stats_flyout/package.json new file mode 100644 index 0000000000000..effef071413a1 --- /dev/null +++ b/x-pack/packages/ml/field_stats_flyout/package.json @@ -0,0 +1,6 @@ +{ + "name": "@kbn/ml-field-stats-flyout", + "private": true, + "version": "1.0.0", + "license": "Elastic License 2.0" +} \ No newline at end of file diff --git a/x-pack/plugins/ml/public/application/components/field_stats_flyout/populated_fields/get_merged_populated_fields_query.test.ts b/x-pack/packages/ml/field_stats_flyout/populated_fields/get_merged_populated_fields_query.test.ts similarity index 100% rename from x-pack/plugins/ml/public/application/components/field_stats_flyout/populated_fields/get_merged_populated_fields_query.test.ts rename to x-pack/packages/ml/field_stats_flyout/populated_fields/get_merged_populated_fields_query.test.ts diff --git a/x-pack/plugins/ml/public/application/components/field_stats_flyout/populated_fields/get_merged_populated_fields_query.ts b/x-pack/packages/ml/field_stats_flyout/populated_fields/get_merged_populated_fields_query.ts similarity index 100% rename from x-pack/plugins/ml/public/application/components/field_stats_flyout/populated_fields/get_merged_populated_fields_query.ts rename to x-pack/packages/ml/field_stats_flyout/populated_fields/get_merged_populated_fields_query.ts diff --git a/x-pack/plugins/ml/public/application/components/field_stats_flyout/populated_fields/index.ts b/x-pack/packages/ml/field_stats_flyout/populated_fields/index.ts similarity index 100% rename from x-pack/plugins/ml/public/application/components/field_stats_flyout/populated_fields/index.ts rename to x-pack/packages/ml/field_stats_flyout/populated_fields/index.ts diff --git a/x-pack/plugins/ml/public/application/components/field_stats_flyout/populated_fields/populated_fields_cache_manager.ts b/x-pack/packages/ml/field_stats_flyout/populated_fields/populated_fields_cache_manager.ts similarity index 100% rename from x-pack/plugins/ml/public/application/components/field_stats_flyout/populated_fields/populated_fields_cache_manager.ts rename to x-pack/packages/ml/field_stats_flyout/populated_fields/populated_fields_cache_manager.ts diff --git a/x-pack/packages/ml/field_stats_flyout/tsconfig.json b/x-pack/packages/ml/field_stats_flyout/tsconfig.json new file mode 100644 index 0000000000000..b0920fac0ad2a --- /dev/null +++ b/x-pack/packages/ml/field_stats_flyout/tsconfig.json @@ -0,0 +1,36 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types", + "types": [ + "jest", + "node", + "react" + ] + }, + "include": [ + "**/*.ts", + "**/*.tsx", + ], + "exclude": [ + "target/**/*" + ], + "kbn_references": [ + "@kbn/i18n-react", + "@kbn/data-plugin", + "@kbn/unified-field-list", + "@kbn/ml-date-picker", + "@kbn/i18n", + "@kbn/react-field", + "@kbn/ml-anomaly-utils", + "@kbn/kibana-react-plugin", + "@kbn/ml-kibana-theme", + "@kbn/core", + "@kbn/ml-data-grid", + "@kbn/ml-string-hash", + "@kbn/ml-is-populated-object", + "@kbn/ml-query-utils", + "@kbn/ml-is-defined", + "@kbn/field-types", + ] +} diff --git a/x-pack/plugins/ml/public/application/components/field_stats_flyout/use_field_stats_flytout_context.ts b/x-pack/packages/ml/field_stats_flyout/use_field_stats_flyout_context.ts similarity index 82% rename from x-pack/plugins/ml/public/application/components/field_stats_flyout/use_field_stats_flytout_context.ts rename to x-pack/packages/ml/field_stats_flyout/use_field_stats_flyout_context.ts index 76b7be8d22f86..ec6c28873011c 100644 --- a/x-pack/plugins/ml/public/application/components/field_stats_flyout/use_field_stats_flytout_context.ts +++ b/x-pack/packages/ml/field_stats_flyout/use_field_stats_flyout_context.ts @@ -7,6 +7,10 @@ import { createContext, useContext } from 'react'; import type { TimeRange as TimeRangeMs } from '@kbn/ml-date-picker'; + +/** + * Represents the properties for the MLJobWizardFieldStatsFlyout component. + */ interface MLJobWizardFieldStatsFlyoutProps { isFlyoutVisible: boolean; setIsFlyoutVisible: (v: boolean) => void; @@ -18,6 +22,10 @@ interface MLJobWizardFieldStatsFlyoutProps { timeRangeMs?: TimeRangeMs; populatedFields?: Set; } + +/** + * Context for the ML Field Stats Flyout. + */ export const MLFieldStatsFlyoutContext = createContext({ isFlyoutVisible: false, setIsFlyoutVisible: () => {}, @@ -28,6 +36,10 @@ export const MLFieldStatsFlyoutContext = createContext { field: Field; } +/** + * Custom hook for managing field statistics trigger functionality. + * + * @returns An object containing the following properties and functions: + * - `renderOption`: A callback function for rendering options in a combo box. + * - `setIsFlyoutVisible`: A function for setting the visibility of the flyout. + * - `setFieldName`: A function for setting the field name. + * - `handleFieldStatsButtonClick`: A callback function for handling field stats button click. + * - `closeFlyout`: A callback function for closing the flyout. + * - `optionCss`: CSS styles for the options in the combo box. + * - `populatedFields`: A set of populated fields. + */ export const useFieldStatsTrigger = () => { const { setIsFlyoutVisible, setFieldName, populatedFields } = useFieldStatsFlyoutContext(); diff --git a/x-pack/packages/ml/parse_interval/README.md b/x-pack/packages/ml/parse_interval/README.md new file mode 100644 index 0000000000000..7fb1fc37db18f --- /dev/null +++ b/x-pack/packages/ml/parse_interval/README.md @@ -0,0 +1,14 @@ +# @kbn/ml-parse-interval + +The `parse_interval` package provides the `parseInterval` utility function for parsing time intervals in various formats. + +## Usage + +```javascript +import { parseInterval } = from '@kbn/ml-parse-interval'; + +const intervalString = '1d'; +const intervalInMs = parseInterval(intervalString); + +console.log(intervalInMs); // Output: 86400000 +``` diff --git a/x-pack/plugins/transform/common/shared_imports.ts b/x-pack/packages/ml/parse_interval/index.ts similarity index 76% rename from x-pack/plugins/transform/common/shared_imports.ts rename to x-pack/packages/ml/parse_interval/index.ts index 953e4a3431035..67aca45bab202 100644 --- a/x-pack/plugins/transform/common/shared_imports.ts +++ b/x-pack/packages/ml/parse_interval/index.ts @@ -5,4 +5,4 @@ * 2.0. */ -export { composeValidators, patternValidator } from '@kbn/ml-plugin/common'; +export { parseInterval } from './parse_interval'; diff --git a/x-pack/packages/ml/parse_interval/jest.config.js b/x-pack/packages/ml/parse_interval/jest.config.js new file mode 100644 index 0000000000000..01fc21724c5bf --- /dev/null +++ b/x-pack/packages/ml/parse_interval/jest.config.js @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +module.exports = { + preset: '@kbn/test/jest_node', + rootDir: '../../../..', + roots: ['/x-pack/packages/ml/parse_interval'], +}; diff --git a/x-pack/packages/ml/parse_interval/kibana.jsonc b/x-pack/packages/ml/parse_interval/kibana.jsonc new file mode 100644 index 0000000000000..6d8398c29b1cc --- /dev/null +++ b/x-pack/packages/ml/parse_interval/kibana.jsonc @@ -0,0 +1,5 @@ +{ + "type": "shared-common", + "id": "@kbn/ml-parse-interval", + "owner": "@elastic/ml-ui" +} diff --git a/x-pack/packages/ml/parse_interval/package.json b/x-pack/packages/ml/parse_interval/package.json new file mode 100644 index 0000000000000..7c2eb0c2de1fa --- /dev/null +++ b/x-pack/packages/ml/parse_interval/package.json @@ -0,0 +1,6 @@ +{ + "name": "@kbn/ml-parse-interval", + "private": true, + "version": "1.0.0", + "license": "Elastic License 2.0" +} \ No newline at end of file diff --git a/x-pack/plugins/ml/common/util/parse_interval.test.ts b/x-pack/packages/ml/parse_interval/parse_interval.test.ts similarity index 100% rename from x-pack/plugins/ml/common/util/parse_interval.test.ts rename to x-pack/packages/ml/parse_interval/parse_interval.test.ts diff --git a/x-pack/plugins/ml/common/util/parse_interval.ts b/x-pack/packages/ml/parse_interval/parse_interval.ts similarity index 55% rename from x-pack/plugins/ml/common/util/parse_interval.ts rename to x-pack/packages/ml/parse_interval/parse_interval.ts index 7ae00ab25a52a..ea84f4ba2e467 100644 --- a/x-pack/plugins/ml/common/util/parse_interval.ts +++ b/x-pack/packages/ml/parse_interval/parse_interval.ts @@ -11,30 +11,43 @@ import dateMath from '@kbn/datemath'; type SupportedUnits = unitOfTime.Base; -// Assume interval is in the form (value)(unit), such as "1h" +/** + * Assume interval is in the form (value)(unit), such as "1h" + */ const INTERVAL_STRING_RE = new RegExp('^([0-9]*)\\s*(' + dateMath.units.join('|') + ')$'); -// moment.js is only designed to allow fractional values between 0 and 1 -// for units of hour or less. +/** + * moment.js is only designed to allow fractional values between 0 and 1 + * for units of hour or less. + */ const SUPPORT_ZERO_DURATION_UNITS: SupportedUnits[] = ['ms', 's', 'm', 'h']; -// List of time units which are supported for use in Elasticsearch durations -// (such as anomaly detection job bucket spans) -// See https://www.elastic.co/guide/en/elasticsearch/reference/current/common-options.html#time-units +/** + * List of time units which are supported for use in Elasticsearch durations + * (such as anomaly detection job bucket spans) + * See https://www.elastic.co/guide/en/elasticsearch/reference/current/common-options.html#time-units + */ const SUPPORT_ES_DURATION_UNITS: SupportedUnits[] = ['ms', 's', 'm', 'h', 'd']; -// Parses an interval String, such as 7d, 1h or 30m to a moment duration. -// Optionally carries out an additional check that the interval is supported as a -// time unit by Elasticsearch, as units greater than 'd' for example cannot be used -// for anomaly detection job bucket spans. -// Differs from the Kibana ui/utils/parse_interval in the following ways: -// 1. A value-less interval such as 'm' is not allowed - in line with the ML back-end -// not accepting such interval Strings for the bucket span of a job. -// 2. Zero length durations 0ms, 0s, 0m and 0h are accepted as-is. -// Note that when adding or subtracting fractional durations, moment is only designed -// to work with units less than 'day'. -// 3. Fractional intervals e.g. 1.5h or 4.5d are not allowed, in line with the behaviour -// of the Elasticsearch date histogram aggregation. +/** + * Parses an interval string, such as 7d, 1h, or 30m to a moment duration. + * Optionally carries out an additional check that the interval is supported as a + * time unit by Elasticsearch, as units greater than 'd' for example cannot be used + * for anomaly detection job bucket spans. + * + * Differs from the Kibana ui/utils/parse_interval in the following ways: + * 1. A value-less interval such as 'm' is not allowed - in line with the ML back-end + * not accepting such interval strings for the bucket span of a job. + * 2. Zero length durations 0ms, 0s, 0m, and 0h are accepted as-is. + * Note that when adding or subtracting fractional durations, moment is only designed + * to work with units less than 'day'. + * 3. Fractional intervals e.g. 1.5h or 4.5d are not allowed, in line with the behaviour + * of the Elasticsearch date histogram aggregation. + * + * @param interval - The interval to parse. + * @param checkValidEsUnit - Optional. Specifies whether to check if the unit is a valid Elasticsearch duration unit. Default is false. + * @returns The parsed Duration object, or null if the interval is invalid. + */ export function parseInterval( interval: string | number, checkValidEsUnit = false diff --git a/x-pack/packages/ml/parse_interval/tsconfig.json b/x-pack/packages/ml/parse_interval/tsconfig.json new file mode 100644 index 0000000000000..e1bbb08bab509 --- /dev/null +++ b/x-pack/packages/ml/parse_interval/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types", + "types": [ + "jest", + "node" + ] + }, + "include": [ + "**/*.ts", + ], + "exclude": [ + "target/**/*" + ], + "kbn_references": [ + "@kbn/datemath", + ] +} diff --git a/x-pack/packages/ml/query_utils/index.ts b/x-pack/packages/ml/query_utils/index.ts index dfd7f6c08ca0c..9ddcaad02d78c 100644 --- a/x-pack/packages/ml/query_utils/index.ts +++ b/x-pack/packages/ml/query_utils/index.ts @@ -20,4 +20,4 @@ export type { SearchQueryVariant, SimpleQuery, } from './src/types'; -export { getDefaultDSLQuery } from './src/get_default_query'; +export { getDefaultDSLQuery } from './src/get_default_dsl_query'; diff --git a/x-pack/packages/ml/query_utils/src/get_default_query.ts b/x-pack/packages/ml/query_utils/src/get_default_dsl_query.ts similarity index 63% rename from x-pack/packages/ml/query_utils/src/get_default_query.ts rename to x-pack/packages/ml/query_utils/src/get_default_dsl_query.ts index f17746ddd2929..08ed5bb94ddaa 100644 --- a/x-pack/packages/ml/query_utils/src/get_default_query.ts +++ b/x-pack/packages/ml/query_utils/src/get_default_dsl_query.ts @@ -6,9 +6,10 @@ */ import { cloneDeep } from 'lodash'; -import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -const DEFAULT_QUERY = { +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; + +const DEFAULT_DSL_QUERY: estypes.QueryDslQueryContainer = { bool: { must: [ { @@ -21,6 +22,6 @@ const DEFAULT_QUERY = { /** * Default DSL query which matches all the results */ -export function getDefaultDSLQuery(): QueryDslQueryContainer { - return cloneDeep(DEFAULT_QUERY); +export function getDefaultDSLQuery(): estypes.QueryDslQueryContainer { + return cloneDeep(DEFAULT_DSL_QUERY); } diff --git a/x-pack/packages/ml/time_buckets/time_buckets.js b/x-pack/packages/ml/time_buckets/time_buckets.js index c70e7962f4250..d2e52fa475178 100644 --- a/x-pack/packages/ml/time_buckets/time_buckets.js +++ b/x-pack/packages/ml/time_buckets/time_buckets.js @@ -149,7 +149,7 @@ TimeBuckets.prototype.getDuration = function () { * * Input can be one of the following: * - "auto" - * - an interval String, such as 7d, 1h or 30m which can be parsed to a moment duration using ml/common/util/parse_interval + * - an interval String, such as 7d, 1h or 30m which can be parsed to a moment duration using @kbn/ml-parse-interval * - a moment.duration object. * * @param {string|moment.duration} input - see desc diff --git a/x-pack/packages/ml/validators/README.md b/x-pack/packages/ml/validators/README.md new file mode 100644 index 0000000000000..1aee8908113f1 --- /dev/null +++ b/x-pack/packages/ml/validators/README.md @@ -0,0 +1,3 @@ +# @kbn/ml-validators + +This package provides a set of validators for machine learning and transform related functionality in Kibana. diff --git a/x-pack/plugins/ml/common/constants/validation.ts b/x-pack/packages/ml/validators/constants.ts similarity index 52% rename from x-pack/plugins/ml/common/constants/validation.ts rename to x-pack/packages/ml/validators/constants.ts index eec37506c9597..6d6151a151ce7 100644 --- a/x-pack/plugins/ml/common/constants/validation.ts +++ b/x-pack/packages/ml/validators/constants.ts @@ -5,16 +5,40 @@ * 2.0. */ +/** + * Interface for a callout message. + */ export interface CalloutMessage { + /** + * Unique identifier for the callout message. + */ id: string; + /** + * Heading of the callout message. + */ heading: string; + /** + * Status of the callout message. + */ status: VALIDATION_STATUS; + /** + * Text of the callout message. + */ text: string; + /** + * Optional URL for the callout message. + */ url?: string; } +/** + * Type for the response of the validate analytics job API. + */ export type ValidateAnalyticsJobResponse = CalloutMessage[]; +/** + * Enum for the validation status. + */ export enum VALIDATION_STATUS { ERROR = 'error', INFO = 'info', @@ -22,17 +46,54 @@ export enum VALIDATION_STATUS { WARNING = 'warning', } +/** + * Boolean const for skipping the bucket span estimation. + */ export const SKIP_BUCKET_SPAN_ESTIMATION = true; +/** + * Const for allowed data units. + */ export const ALLOWED_DATA_UNITS = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']; +/** + * Const for the maximum length of a job ID. + */ export const JOB_ID_MAX_LENGTH = 64; // Data Frame Analytics + +/** + * Const for the upper limit of training documents. + */ export const TRAINING_DOCS_UPPER = 200000; + +/** + * Const for the lower limit of training documents. + */ export const TRAINING_DOCS_LOWER = 200; + +/** + * Const for the threshold of included fields. + */ export const INCLUDED_FIELDS_THRESHOLD = 100; + +/** + * Const for the minimum number of fields for check. + */ export const MINIMUM_NUM_FIELD_FOR_CHECK = 25; + +/** + * Const for the fraction empty limit. + */ export const FRACTION_EMPTY_LIMIT = 0.3; + +/** + * Const for the maximum length of categories. + */ export const NUM_CATEGORIES_THRESHOLD = 10; + +/** + * Const for all categories. + */ export const ALL_CATEGORIES = -1; diff --git a/x-pack/packages/ml/validators/index.ts b/x-pack/packages/ml/validators/index.ts new file mode 100644 index 0000000000000..da982fec00f33 --- /dev/null +++ b/x-pack/packages/ml/validators/index.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { + ALL_CATEGORIES, + ALLOWED_DATA_UNITS, + FRACTION_EMPTY_LIMIT, + INCLUDED_FIELDS_THRESHOLD, + JOB_ID_MAX_LENGTH, + MINIMUM_NUM_FIELD_FOR_CHECK, + NUM_CATEGORIES_THRESHOLD, + SKIP_BUCKET_SPAN_ESTIMATION, + TRAINING_DOCS_LOWER, + TRAINING_DOCS_UPPER, + VALIDATION_STATUS, + type CalloutMessage, + type ValidateAnalyticsJobResponse, +} from './constants'; +export { + composeValidators, + dictionaryValidator, + maxLengthValidator, + memoryInputValidator, + patternValidator, + requiredValidator, + timeIntervalInputValidator, + type MemoryInputValidatorResult, +} from './validators'; diff --git a/x-pack/packages/ml/validators/jest.config.js b/x-pack/packages/ml/validators/jest.config.js new file mode 100644 index 0000000000000..67f28da8589d9 --- /dev/null +++ b/x-pack/packages/ml/validators/jest.config.js @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +module.exports = { + preset: '@kbn/test/jest_node', + rootDir: '../../../..', + roots: ['/x-pack/packages/ml/validators'], +}; diff --git a/x-pack/packages/ml/validators/kibana.jsonc b/x-pack/packages/ml/validators/kibana.jsonc new file mode 100644 index 0000000000000..e747549d8e33c --- /dev/null +++ b/x-pack/packages/ml/validators/kibana.jsonc @@ -0,0 +1,5 @@ +{ + "type": "shared-common", + "id": "@kbn/ml-validators", + "owner": "@elastic/ml-ui" +} diff --git a/x-pack/packages/ml/validators/package.json b/x-pack/packages/ml/validators/package.json new file mode 100644 index 0000000000000..958c085a4d3d5 --- /dev/null +++ b/x-pack/packages/ml/validators/package.json @@ -0,0 +1,6 @@ +{ + "name": "@kbn/ml-validators", + "private": true, + "version": "1.0.0", + "license": "Elastic License 2.0" +} \ No newline at end of file diff --git a/x-pack/packages/ml/validators/tsconfig.json b/x-pack/packages/ml/validators/tsconfig.json new file mode 100644 index 0000000000000..ca25f09b5cd50 --- /dev/null +++ b/x-pack/packages/ml/validators/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types", + "types": [ + "jest", + "node" + ] + }, + "include": [ + "**/*.ts", + ], + "exclude": [ + "target/**/*" + ], + "kbn_references": [ + "@kbn/ml-parse-interval", + ] +} diff --git a/x-pack/plugins/ml/common/util/validators.test.ts b/x-pack/packages/ml/validators/validators.test.ts similarity index 100% rename from x-pack/plugins/ml/common/util/validators.test.ts rename to x-pack/packages/ml/validators/validators.test.ts diff --git a/x-pack/plugins/ml/common/util/validators.ts b/x-pack/packages/ml/validators/validators.ts similarity index 66% rename from x-pack/plugins/ml/common/util/validators.ts rename to x-pack/packages/ml/validators/validators.ts index 9ceeac4b29e82..39db75cd3dfce 100644 --- a/x-pack/plugins/ml/common/util/validators.ts +++ b/x-pack/packages/ml/validators/validators.ts @@ -5,8 +5,9 @@ * 2.0. */ -import { ALLOWED_DATA_UNITS } from '../constants/validation'; -import { parseInterval } from './parse_interval'; +import { parseInterval } from '@kbn/ml-parse-interval'; + +import { ALLOWED_DATA_UNITS } from './constants'; /** * Provides a validator function for maximum allowed input length. @@ -27,8 +28,9 @@ export function maxLengthValidator( } /** - * Provides a validator function for checking against pattern. - * @param pattern + * Factory that provides a validator function for checking against pattern. + * @param pattern Pattern to check against. + * @returns A validator function that checks if the value matches the pattern. */ export function patternValidator( pattern: RegExp @@ -44,8 +46,10 @@ export function patternValidator( } /** - * Composes multiple validators into a single function - * @param validators + * Factory that composes multiple validators into a single function. + * + * @param validators List of validators to compose. + * @returns A validator function that runs all the validators. */ export function composeValidators( ...validators: Array<(value: any) => { [key: string]: any } | null> @@ -58,16 +62,32 @@ export function composeValidators( }; } +/** + * Factory to create a required validator function. + * @returns A validator function that checks if the value is empty. + */ export function requiredValidator() { return (value: T) => { return value === '' || value === undefined || value === null ? { required: true } : null; }; } +/** + * Type for the result of a validation. + */ export type ValidationResult = Record | null; +/** + * Type for the result of a memory input validation. + */ export type MemoryInputValidatorResult = { invalidUnits: { allowedUnits: string } } | null; +/** + * Factory for creating a memory input validator function. + * + * @param allowedUnits Allowed units for the memory input. + * @returns A validator function that checks if the value is a valid memory input. + */ export function memoryInputValidator(allowedUnits = ALLOWED_DATA_UNITS) { return (value: T) => { if (typeof value !== 'string' || value === '') { @@ -80,6 +100,11 @@ export function memoryInputValidator(allowedUnits = ALLOWED_DATA_UNITS) { }; } +/** + * Factory for creating a time interval input validator function. + * + * @returns A validator function that checks if the value is a valid time interval. + */ export function timeIntervalInputValidator() { return (value: string) => { if (value === '') { @@ -97,6 +122,12 @@ export function timeIntervalInputValidator() { }; } +/** + * Factory to create a dictionary validator function. + * @param dict Dictionary to check against. + * @param shouldInclude Whether the value should be included in the dictionary. + * @returns A validator function that checks if the value is in the dictionary. + */ export function dictionaryValidator(dict: string[], shouldInclude: boolean = false) { const dictSet = new Set(dict); return (value: string) => { diff --git a/x-pack/plugins/cases/public/components/links/index.test.tsx b/x-pack/plugins/cases/public/components/links/index.test.tsx index 365502f5c02c1..5c450edc39d8a 100644 --- a/x-pack/plugins/cases/public/components/links/index.test.tsx +++ b/x-pack/plugins/cases/public/components/links/index.test.tsx @@ -38,7 +38,8 @@ describe('Configuration button', () => { expect(configureButton).toHaveAttribute('aria-label', 'My label'); }); - it('renders the tooltip correctly when hovering the button', async () => { + // Flaky: https://github.com/elastic/kibana/issues/193209 + it.skip('renders the tooltip correctly when hovering the button', async () => { jest.useFakeTimers(); const user = userEvent.setup({ diff --git a/x-pack/plugins/enterprise_search/public/navigation_tree.ts b/x-pack/plugins/enterprise_search/public/navigation_tree.ts index 9264bf5de9750..74db04a3141da 100644 --- a/x-pack/plugins/enterprise_search/public/navigation_tree.ts +++ b/x-pack/plugins/enterprise_search/public/navigation_tree.ts @@ -348,6 +348,7 @@ export const getNavigationTreeDefinition = ({ title: 'Stack', }, ], + id: 'stack_management', // This id can't be changed as we use it to open the panel programmatically link: 'management', renderAs: 'panelOpener', spaceBefore: null, diff --git a/x-pack/plugins/fleet/common/types/index.ts b/x-pack/plugins/fleet/common/types/index.ts index 3338c8c86f2f2..647a8b917d0c0 100644 --- a/x-pack/plugins/fleet/common/types/index.ts +++ b/x-pack/plugins/fleet/common/types/index.ts @@ -32,12 +32,12 @@ export interface FleetConfigType { }; agentless?: { enabled: boolean; - api: { - url: string; - tls: { - certificate: string; - key: string; - ca: string; + api?: { + url?: string; + tls?: { + certificate?: string; + key?: string; + ca?: string; }; }; }; diff --git a/x-pack/plugins/fleet/server/routes/agent/handlers.ts b/x-pack/plugins/fleet/server/routes/agent/handlers.ts index e328c73878980..67703bba5caae 100644 --- a/x-pack/plugins/fleet/server/routes/agent/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/agent/handlers.ts @@ -328,7 +328,8 @@ export const getAgentStatusForAgentPolicyHandler: FleetRequestHandler< soClient, request.query.policyId, request.query.kuery, - coreContext.savedObjects.client.getCurrentNamespace() + coreContext.savedObjects.client.getCurrentNamespace(), + request.query.policyIds ); const body: GetAgentStatusResponse = { results }; diff --git a/x-pack/plugins/fleet/server/services/agents/agentless_agent.test.ts b/x-pack/plugins/fleet/server/services/agents/agentless_agent.test.ts index 24c344c15eccc..e55b883e80029 100644 --- a/x-pack/plugins/fleet/server/services/agents/agentless_agent.test.ts +++ b/x-pack/plugins/fleet/server/services/agents/agentless_agent.test.ts @@ -9,6 +9,7 @@ import { securityMock } from '@kbn/security-plugin/server/mocks'; import { loggerMock } from '@kbn/logging-mocks'; import type { Logger } from '@kbn/core/server'; +import type { AxiosError } from 'axios'; import axios from 'axios'; import { AgentlessAgentCreateError } from '../../errors'; @@ -20,7 +21,14 @@ import { listFleetServerHosts } from '../fleet_server_host'; import { agentlessAgentService } from './agentless_agent'; -jest.mock('axios', () => jest.fn()); +jest.mock('axios'); +// Add a mock implementation for `isAxiosError` to simulate that the error is an Axios error +(axios.isAxiosError as unknown as jest.Mock).mockImplementation( + (error: any): error is AxiosError => { + return error.isAxiosError === true; // Simulate that the error is an Axios error if it has `isAxiosError` property + } +); + jest.mock('../fleet_server_host'); jest.mock('../api_keys'); jest.mock('../output'); @@ -361,4 +369,191 @@ describe('Agentless Agent service', () => { }) ); }); + + it('should redact sensitive information from debug logs', async () => { + const returnValue = { + id: 'mocked', + regional_id: 'mocked', + }; + + (axios as jest.MockedFunction).mockResolvedValueOnce(returnValue); + const soClient = getAgentPolicyCreateMock(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + + jest.spyOn(appContextService, 'getConfig').mockReturnValue({ + agentless: { + enabled: true, + api: { + url: 'http://api.agentless.com', + tls: { + certificate: '/path/to/cert', + key: '/path/to/key', + ca: '/path/to/ca', + }, + }, + }, + } as any); + jest.spyOn(appContextService, 'getCloud').mockReturnValue({ isCloudEnabled: true } as any); + jest + .spyOn(appContextService, 'getKibanaVersion') + .mockReturnValue('mocked-kibana-version-infinite'); + + mockedListFleetServerHosts.mockResolvedValue({ + items: [ + { + id: 'mocked-fleet-server-id', + host: 'http://fleetserver:8220', + active: true, + is_default: true, + host_urls: ['http://fleetserver:8220'], + }, + ], + } as any); + + mockedListEnrollmentApiKeys.mockResolvedValue({ + items: [ + { + id: 'mocked-fleet-enrollment-token-id', + policy_id: 'mocked-fleet-enrollment-policy-id', + api_key: 'mocked-fleet-enrollment-api-key', + }, + ], + } as any); + + await agentlessAgentService.createAgentlessAgent(esClient, soClient, { + id: 'mocked-agentless-agent-policy-id', + name: 'agentless agent policy', + namespace: 'default', + supports_agentless: true, + } as AgentPolicy); + + // Assert that sensitive information is redacted + expect(mockedLogger.debug).toHaveBeenCalledWith( + expect.stringContaining('fleet_token: [REDACTED]') + ); + expect(mockedLogger.debug).toHaveBeenCalledWith(expect.stringContaining('cert: [REDACTED]')); + expect(mockedLogger.debug).toHaveBeenCalledWith(expect.stringContaining('key: [REDACTED]')); + expect(mockedLogger.debug).toHaveBeenCalledWith(expect.stringContaining('ca: [REDACTED]')); + }); + + it('should log "undefined" on debug logs when tls configuration is missing', async () => { + const returnValue = { + id: 'mocked', + regional_id: 'mocked', + }; + + (axios as jest.MockedFunction).mockResolvedValueOnce(returnValue); + const soClient = getAgentPolicyCreateMock(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + + jest.spyOn(appContextService, 'getConfig').mockReturnValue({ + agentless: { + enabled: true, + api: { + url: 'http://api.agentless.com', + }, + }, + } as any); + jest.spyOn(appContextService, 'getCloud').mockReturnValue({ isCloudEnabled: true } as any); + jest + .spyOn(appContextService, 'getKibanaVersion') + .mockReturnValue('mocked-kibana-version-infinite'); + + mockedListFleetServerHosts.mockResolvedValue({ + items: [ + { + id: 'mocked-fleet-server-id', + host: 'http://fleetserver:8220', + active: true, + is_default: true, + host_urls: ['http://fleetserver:8220'], + }, + ], + } as any); + + mockedListEnrollmentApiKeys.mockResolvedValue({ + items: [ + { + id: 'mocked-fleet-enrollment-token-id', + policy_id: 'mocked-fleet-enrollment-policy-id', + api_key: 'mocked-fleet-enrollment-api-key', + }, + ], + } as any); + + await expect( + agentlessAgentService.createAgentlessAgent(esClient, soClient, { + id: 'mocked-agentless-agent-policy-id', + name: 'agentless agent policy', + namespace: 'default', + supports_agentless: true, + } as AgentPolicy) + ).rejects.toThrowError(); + + // Assert that tls configuration is missing + expect(mockedLogger.debug).toHaveBeenCalledWith(expect.stringContaining('cert: undefined')); + expect(mockedLogger.debug).toHaveBeenCalledWith(expect.stringContaining('key: undefined')); + expect(mockedLogger.debug).toHaveBeenCalledWith(expect.stringContaining('ca: undefined')); + }); + + it('should redact sensitive information from error logs', async () => { + const soClient = getAgentPolicyCreateMock(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + + jest.spyOn(appContextService, 'getConfig').mockReturnValue({ + agentless: { + enabled: true, + api: { + url: 'http://api.agentless.com', + tls: { + certificate: '/path/to/cert', + key: '/path/to/key', + ca: '/path/to/ca', + }, + }, + }, + } as any); + jest.spyOn(appContextService, 'getCloud').mockReturnValue({ isCloudEnabled: true } as any); + + mockedListFleetServerHosts.mockResolvedValue({ + items: [ + { + id: 'mocked-fleet-server-id', + host: 'http://fleetserver:8220', + active: true, + is_default: true, + host_urls: ['http://fleetserver:8220'], + }, + ], + } as any); + + mockedListEnrollmentApiKeys.mockResolvedValue({ + items: [ + { + id: 'mocked-fleet-enrollment-token-id', + policy_id: 'mocked-fleet-enrollment-policy-id', + api_key: 'mocked-fleet-enrollment-api-key', + }, + ], + } as any); + // Force axios to throw an AxiosError to simulate an error response + const axiosError = new Error('Test Error') as AxiosError; + axiosError.isAxiosError = true; // Mark it as an AxiosError + (axios as jest.MockedFunction).mockRejectedValueOnce(axiosError); + + await expect( + agentlessAgentService.createAgentlessAgent(esClient, soClient, { + id: 'mocked-agentless-agent-policy-id', + name: 'agentless agent policy', + namespace: 'default', + supports_agentless: true, + } as AgentPolicy) + ).rejects.toThrowError(); + + // Assert that sensitive information is redacted + expect(mockedLogger.error).toHaveBeenCalledWith( + expect.stringContaining(`\"fleet_token\":\"[REDACTED]\"`), + expect.any(Object) + ); + }); }); diff --git a/x-pack/plugins/fleet/server/services/agents/agentless_agent.ts b/x-pack/plugins/fleet/server/services/agents/agentless_agent.ts index 7f2c0dfaf1190..c98a5b63e0356 100644 --- a/x-pack/plugins/fleet/server/services/agents/agentless_agent.ts +++ b/x-pack/plugins/fleet/server/services/agents/agentless_agent.ts @@ -7,12 +7,14 @@ import https from 'https'; -import type { ElasticsearchClient, SavedObjectsClientContract } from '@kbn/core/server'; +import type { ElasticsearchClient, LogMeta, SavedObjectsClientContract } from '@kbn/core/server'; import { SslConfig, sslSchema } from '@kbn/server-http-tools'; import type { AxiosError, AxiosRequestConfig } from 'axios'; import axios from 'axios'; +import apm from 'elastic-apm-node'; + import { SO_SEARCH_LIMIT } from '../../constants'; import type { AgentPolicy } from '../../types'; import type { AgentlessApiResponse } from '../../../common/types'; @@ -30,12 +32,22 @@ class AgentlessAgentService { soClient: SavedObjectsClientContract, agentlessAgentPolicy: AgentPolicy ) { + const traceId = apm.currentTransaction?.traceparent; + const withRequestIdMessage = (message: string) => `${message} [Request Id: ${traceId}]`; + + const errorMetadata: LogMeta = { + trace: { + id: traceId, + }, + }; + const logger = appContextService.getLogger(); logger.debug(`Creating agentless agent ${agentlessAgentPolicy.id}`); if (!isAgentlessApiEnabled) { logger.error( - 'Creating agentless agent not supported in non-cloud or non-serverless environments' + 'Creating agentless agent not supported in non-cloud or non-serverless environments', + errorMetadata ); throw new AgentlessAgentCreateError('Agentless agent not supported'); } @@ -46,7 +58,7 @@ class AgentlessAgentService { const agentlessConfig = appContextService.getConfig()?.agentless; if (!agentlessConfig) { - logger.error('Missing agentless configuration'); + logger.error('Missing agentless configuration', errorMetadata); throw new AgentlessAgentCreateError('missing agentless configuration'); } @@ -57,17 +69,23 @@ class AgentlessAgentService { soClient ); - logger.debug(`Creating agentless agent with fleetUrl ${fleetUrl} and fleetToken ${fleetToken}`); + logger.debug( + `Creating agentless agent with fleet_url: ${fleetUrl} and fleet_token: [REDACTED]` + ); - logger.debug(`Creating agentless agent with TLS config with certificate: ${agentlessConfig.api.tls.certificate}, - and key: ${agentlessConfig.api.tls.key}`); + logger.debug( + `Creating agentless agent with TLS cert: ${ + agentlessConfig?.api?.tls?.certificate ? '[REDACTED]' : 'undefined' + } and TLS key: ${agentlessConfig?.api?.tls?.key ? '[REDACTED]' : 'undefined'} + and TLS ca: ${agentlessConfig?.api?.tls?.ca ? '[REDACTED]' : 'undefined'}` + ); const tlsConfig = new SslConfig( sslSchema.validate({ enabled: true, - certificate: agentlessConfig.api.tls.certificate, - key: agentlessConfig.api.tls.key, - certificateAuthorities: agentlessConfig.api.tls.ca, + certificate: agentlessConfig?.api?.tls?.certificate, + key: agentlessConfig?.api?.tls?.key, + certificateAuthorities: agentlessConfig?.api?.tls?.ca, }) ); @@ -81,6 +99,7 @@ class AgentlessAgentService { method: 'POST', headers: { 'Content-type': 'application/json', + 'X-Request-ID': traceId, }, httpsAgent: new https.Agent({ rejectUnauthorized: tlsConfig.rejectUnauthorized, @@ -95,30 +114,45 @@ class AgentlessAgentService { requestConfig.data.stack_version = appContextService.getKibanaVersion(); } - const requestConfigDebug = JSON.stringify({ + const requestConfigDebug = { ...requestConfig, + data: { + ...requestConfig.data, + fleet_token: '[REDACTED]', + }, httpsAgent: { ...requestConfig.httpsAgent, options: { ...requestConfig.httpsAgent.options, - cert: requestConfig.httpsAgent.options.cert ? 'REDACTED' : undefined, - key: requestConfig.httpsAgent.options.key ? 'REDACTED' : undefined, - ca: requestConfig.httpsAgent.options.ca ? 'REDACTED' : undefined, + cert: requestConfig.httpsAgent.options.cert ? '[REDACTED]' : undefined, + key: requestConfig.httpsAgent.options.key ? '[REDACTED]' : undefined, + ca: requestConfig.httpsAgent.options.ca ? '[REDACTED]' : undefined, }, }, - }); + }; + + const requestConfigDebugToString = JSON.stringify(requestConfigDebug); - logger.debug(`Creating agentless agent with request config ${requestConfigDebug}`); + logger.debug(`Creating agentless agent with request config ${requestConfigDebugToString}`); + + const errorMetadataWithRequestConfig: LogMeta = { + ...errorMetadata, + http: { + request: { + id: traceId, + body: requestConfigDebug.data, + }, + }, + }; const response = await axios(requestConfig).catch( (error: Error | AxiosError) => { if (!axios.isAxiosError(error)) { logger.error( - `Creating agentless failed with an error ${error} ${JSON.stringify( - requestConfigDebug - )}` + `Creating agentless failed with an error ${error} ${requestConfigDebugToString}`, + errorMetadataWithRequestConfig ); - throw new AgentlessAgentCreateError(error.message); + throw new AgentlessAgentCreateError(withRequestIdMessage(error.message)); } const errorLogCodeCause = `${error.code} ${this.convertCauseErrorsToString(error)}`; @@ -128,28 +162,38 @@ class AgentlessAgentService { logger.error( `Creating agentless failed because the Agentless API responding with a status code that falls out of the range of 2xx: ${JSON.stringify( error.response.status - )}} ${JSON.stringify(error.response.data)}} ${JSON.stringify(requestConfigDebug)}` + )}} ${JSON.stringify(error.response.data)}} ${requestConfigDebugToString}`, + { + ...errorMetadataWithRequestConfig, + http: { + ...errorMetadataWithRequestConfig.http, + response: { + status_code: error.response.status, + body: error.response.data, + }, + }, + } ); throw new AgentlessAgentCreateError( - `the Agentless API could not create the agentless agent` + withRequestIdMessage(`the Agentless API could not create the agentless agent`) ); } else if (error.request) { // The request was made but no response was received logger.error( - `Creating agentless agent failed while sending the request to the Agentless API: ${errorLogCodeCause} ${JSON.stringify( - requestConfigDebug - )}` + `Creating agentless agent failed while sending the request to the Agentless API: ${errorLogCodeCause} ${requestConfigDebugToString}`, + errorMetadataWithRequestConfig + ); + throw new AgentlessAgentCreateError( + withRequestIdMessage(`no response received from the Agentless API`) ); - throw new AgentlessAgentCreateError(`no response received from the Agentless API`); } else { // Something happened in setting up the request that triggered an Error logger.error( - `Creating agentless agent failed to be created ${errorLogCodeCause} ${JSON.stringify( - requestConfigDebug - )}` + `Creating agentless agent failed to be created ${errorLogCodeCause} ${requestConfigDebugToString}`, + errorMetadataWithRequestConfig ); throw new AgentlessAgentCreateError( - 'the Agentless API could not create the agentless agent' + withRequestIdMessage('the Agentless API could not create the agentless agent') ); } } diff --git a/x-pack/plugins/fleet/server/services/agents/status.test.ts b/x-pack/plugins/fleet/server/services/agents/status.test.ts index 346352f26de70..ae78985a40a1f 100644 --- a/x-pack/plugins/fleet/server/services/agents/status.test.ts +++ b/x-pack/plugins/fleet/server/services/agents/status.test.ts @@ -7,6 +7,8 @@ import { errors as EsErrors } from '@elastic/elasticsearch'; +import { AGENTS_INDEX } from '../../../common'; + import { createAppContextStartContractMock } from '../../mocks'; import { appContextService } from '../app_context'; @@ -168,4 +170,67 @@ describe('getAgentStatusForAgentPolicy', () => { expect(esClient.search).toHaveBeenCalledTimes(2); }); + + it('calls esClient.search with correct parameters when agentPolicyIds are provided', async () => { + const esClient = { + search: jest.fn().mockResolvedValue({ + aggregations: { + status: { + buckets: [ + { key: 'online', doc_count: 2 }, + { key: 'error', doc_count: 1 }, + ], + }, + }, + }), + }; + + const soClient = { + find: jest.fn().mockResolvedValue({ + saved_objects: [ + { id: 'agentPolicyId1', attributes: { name: 'Policy 1' } }, + { id: 'agentPolicyId2', attributes: { name: 'Policy 2' } }, + ], + }), + }; + + const agentPolicyIds = ['agentPolicyId1', 'agentPolicyId2']; + const filterKuery = 'filterKuery'; + const spaceId = 'spaceId'; + + await getAgentStatusForAgentPolicy( + esClient as any, + soClient as any, + undefined, + filterKuery, + spaceId, + agentPolicyIds + ); + + expect(esClient.search).toHaveBeenCalledWith( + expect.objectContaining({ + index: AGENTS_INDEX, + size: 0, + query: expect.objectContaining({ + bool: expect.objectContaining({ + must: expect.arrayContaining([ + expect.objectContaining({ + terms: { + policy_id: agentPolicyIds, + }, + }), + ]), + }), + }), + aggregations: expect.objectContaining({ + status: expect.objectContaining({ + terms: expect.objectContaining({ + field: 'status', + size: expect.any(Number), + }), + }), + }), + }) + ); + }); }); diff --git a/x-pack/plugins/fleet/server/services/agents/status.ts b/x-pack/plugins/fleet/server/services/agents/status.ts index 99d2d25b139c8..c413ee7e268c8 100644 --- a/x-pack/plugins/fleet/server/services/agents/status.ts +++ b/x-pack/plugins/fleet/server/services/agents/status.ts @@ -40,12 +40,23 @@ export async function getAgentStatusById( return (await getAgentById(esClient, soClient, agentId)).status!; } +/** + * getAgentStatusForAgentPolicy + * @param esClient + * @param soClient + * @param agentPolicyId @deprecated use agentPolicyIds instead since the move to multi-policy + * @param filterKuery + * @param spaceId + * @param agentPolicyIds + */ + export async function getAgentStatusForAgentPolicy( esClient: ElasticsearchClient, soClient: SavedObjectsClientContract, agentPolicyId?: string, filterKuery?: string, - spaceId?: string + spaceId?: string, + agentPolicyIds?: string[] ) { const logger = appContextService.getLogger(); const runtimeFields = await buildAgentStatusRuntimeField(soClient); @@ -71,8 +82,14 @@ export async function getAgentStatusForAgentPolicy( ); clauses.push(kueryAsElasticsearchQuery); } - - if (agentPolicyId) { + // If agentPolicyIds is provided, we filter by those, otherwise we filter by depreciated agentPolicyId + if (agentPolicyIds) { + clauses.push({ + terms: { + policy_id: agentPolicyIds, + }, + }); + } else if (agentPolicyId) { clauses.push({ term: { policy_id: agentPolicyId, diff --git a/x-pack/plugins/fleet/server/services/package_policy.ts b/x-pack/plugins/fleet/server/services/package_policy.ts index c457da64ead07..f1cf8bdd3b9a2 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.ts @@ -105,7 +105,12 @@ import { getAuthzFromRequest, doesNotHaveRequiredFleetAuthz } from './security'; import { storedPackagePolicyToAgentInputs } from './agent_policies'; import { agentPolicyService } from './agent_policy'; -import { getPackageInfo, getInstallation, ensureInstalledPackage } from './epm/packages'; +import { + getPackageInfo, + getInstallation, + ensureInstalledPackage, + getInstallationObject, +} from './epm/packages'; import { getAssetsDataFromAssetsMap } from './epm/packages/assets'; import { compileTemplate } from './epm/agent/agent'; import { escapeSearchQueryPhrase, normalizeKuery as _normalizeKuery } from './saved_object'; @@ -1874,9 +1879,25 @@ class PackagePolicyClientImpl implements PackagePolicyClient { public async buildPackagePolicyFromPackage( soClient: SavedObjectsClientContract, pkgName: string, - logger?: Logger + options?: { logger?: Logger; installMissingPackage?: boolean } ): Promise { - const pkgInstall = await getInstallation({ savedObjectsClient: soClient, pkgName, logger }); + const pkgInstallObj = await getInstallationObject({ + savedObjectsClient: soClient, + pkgName, + logger: options?.logger, + }); + let pkgInstall = pkgInstallObj?.attributes; + if (!pkgInstall && options?.installMissingPackage) { + const esClient = await appContextService.getInternalUserESClient(); + const result = await ensureInstalledPackage({ + esClient, + pkgName, + savedObjectsClient: soClient, + }); + if (result.package) { + pkgInstall = result.package; + } + } if (pkgInstall) { const packageInfo = await getPackageInfo({ savedObjectsClient: soClient, diff --git a/x-pack/plugins/fleet/server/services/package_policy_service.ts b/x-pack/plugins/fleet/server/services/package_policy_service.ts index 46913642843cf..8bd96fd855312 100644 --- a/x-pack/plugins/fleet/server/services/package_policy_service.ts +++ b/x-pack/plugins/fleet/server/services/package_policy_service.ts @@ -164,7 +164,7 @@ export interface PackagePolicyClient { buildPackagePolicyFromPackage( soClient: SavedObjectsClientContract, pkgName: string, - logger?: Logger + options?: { logger?: Logger; installMissingPackage?: boolean } ): Promise; runExternalCallbacks( diff --git a/x-pack/plugins/fleet/server/types/rest_spec/agent.ts b/x-pack/plugins/fleet/server/types/rest_spec/agent.ts index 4bbd065e23003..82cae68602e94 100644 --- a/x-pack/plugins/fleet/server/types/rest_spec/agent.ts +++ b/x-pack/plugins/fleet/server/types/rest_spec/agent.ts @@ -241,6 +241,7 @@ export const PostBulkUpdateAgentTagsRequestSchema = { export const GetAgentStatusRequestSchema = { query: schema.object({ policyId: schema.maybe(schema.string()), + policyIds: schema.maybe(schema.arrayOf(schema.string())), kuery: schema.maybe( schema.string({ validate: (value: string) => { diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/index_mapping_with_context.tsx b/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/with_context_components/index_mapping_with_context.tsx similarity index 78% rename from x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/index_mapping_with_context.tsx rename to x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/with_context_components/index_mapping_with_context.tsx index 7aa0f07e8e492..a341b0fb67813 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/index_mapping_with_context.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/with_context_components/index_mapping_with_context.tsx @@ -6,14 +6,14 @@ */ import React from 'react'; -import { documentationService } from '../../../../services'; -import { UIM_APP_NAME } from '../../../../../../common/constants/ui_metric'; -import { httpService } from '../../../../services/http'; -import { notificationService } from '../../../../services/notification'; -import { UiMetricService } from '../../../../services/ui_metric'; -import { AppDependencies, IndexManagementAppContext } from '../../../..'; +import { documentationService } from '../../../../../services'; +import { UIM_APP_NAME } from '../../../../../../../common/constants/ui_metric'; +import { httpService } from '../../../../../services/http'; +import { notificationService } from '../../../../../services/notification'; +import { UiMetricService } from '../../../../../services/ui_metric'; +import { AppDependencies, IndexManagementAppContext } from '../../../../..'; import { IndexMappingWithContextProps } from './index_mapping_with_context_types'; -import { DetailsPageMappings } from './details_page_mappings'; +import { DetailsPageMappings } from '../details_page_mappings'; export const IndexMappingWithContext: React.FC = ({ core, diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/index_mapping_with_context_types.tsx b/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/with_context_components/index_mapping_with_context_types.tsx similarity index 82% rename from x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/index_mapping_with_context_types.tsx rename to x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/with_context_components/index_mapping_with_context_types.tsx index 228f928f1ec74..86d16fc35e65b 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/index_mapping_with_context_types.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/with_context_components/index_mapping_with_context_types.tsx @@ -7,8 +7,8 @@ import { CoreStart } from '@kbn/core/public'; import { IndexMappingProps } from '@kbn/index-management-shared-types'; -import { AppDependencies } from '../../../../app_context'; -import { ExtensionsService } from '../../../../../services/extensions_service'; +import { AppDependencies } from '../../../../../app_context'; +import { ExtensionsService } from '../../../../../../services/extensions_service'; export type IndexMappingWithContextProps = { core: CoreStart; diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/index_mappings_embeddable.tsx b/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/with_context_components/index_mappings_embeddable.tsx similarity index 100% rename from x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/index_mappings_embeddable.tsx rename to x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/with_context_components/index_mappings_embeddable.tsx diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/with_context_components/index_settings_embeddable.tsx b/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/with_context_components/index_settings_embeddable.tsx new file mode 100644 index 0000000000000..70e738dbd5bd7 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/with_context_components/index_settings_embeddable.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiLoadingSpinner } from '@elastic/eui'; +import { dynamic } from '@kbn/shared-ux-utility'; +import React, { Suspense, ComponentType } from 'react'; +import { IndexSettingWithContextProps } from './index_settings_with_context_types'; + +const IndexSettingsWithContext = dynamic>(() => + import('./index_settings_with_context').then((mod) => ({ default: mod.IndexSettingsWithContext })) +); + +export const IndexSettings: React.FC = (props) => { + return ( + }> + + + ); +}; diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/with_context_components/index_settings_with_context.tsx b/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/with_context_components/index_settings_with_context.tsx new file mode 100644 index 0000000000000..d56c2c46e8ec4 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/with_context_components/index_settings_with_context.tsx @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { documentationService } from '../../../../../services'; +import { UIM_APP_NAME } from '../../../../../../../common/constants/ui_metric'; +import { httpService } from '../../../../../services/http'; +import { notificationService } from '../../../../../services/notification'; +import { UiMetricService } from '../../../../../services/ui_metric'; +import { AppDependencies, IndexManagementAppContext } from '../../../../..'; +import { DetailsPageSettings } from '../details_page_settings'; +import { IndexSettingWithContextProps } from './index_settings_with_context_types'; +import { setUiMetricService } from '../../../../../services/api'; + +export const IndexSettingsWithContext: React.FC = ({ + core, + dependencies, + indexName, + usageCollection, +}) => { + // this normally happens when the index management app is rendered + // but if components are embedded elsewhere that setup is skipped, so we have to do it here + // would do it in plugin.ts but that blows up the bundle size + // can't do it in an effect because then the first http call fails as the instantiation happens after first render + if (!httpService.httpClient) { + httpService.setup(core.http); + notificationService.setup(core.notifications); + } + documentationService.setup(core.docLinks); + + const uiMetricService = new UiMetricService(UIM_APP_NAME); + setUiMetricService(uiMetricService); + uiMetricService.setup(usageCollection); + + const newDependencies: AppDependencies = { + ...dependencies, + services: { + ...(dependencies.services || {}), + httpService, + notificationService, + uiMetricService, + }, + }; + return ( + + + + ); +}; diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/with_context_components/index_settings_with_context_types.tsx b/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/with_context_components/index_settings_with_context_types.tsx new file mode 100644 index 0000000000000..39600edcc1306 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/with_context_components/index_settings_with_context_types.tsx @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CoreStart } from '@kbn/core/public'; +import type { IndexSettingProps } from '@kbn/index-management-shared-types'; +import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public'; +import { AppDependencies } from '../../../../../app_context'; +import { ExtensionsService } from '../../../../../../services/extensions_service'; + +export type IndexSettingWithContextProps = { + core: CoreStart; + // omitting services here to constitute them inside the component + // this helps reduce bundle size significantly + dependencies: Omit & { + services: { extensionsService: ExtensionsService }; + }; + usageCollection: UsageCollectionSetup; +} & IndexSettingProps; diff --git a/x-pack/plugins/index_management/public/plugin.ts b/x-pack/plugins/index_management/public/plugin.ts index 4efe613fc2a04..5b1706fe807bd 100644 --- a/x-pack/plugins/index_management/public/plugin.ts +++ b/x-pack/plugins/index_management/public/plugin.ts @@ -26,8 +26,9 @@ import { ClientConfigType, SetupDependencies, StartDependencies } from './types' // avoid import from index files in plugin.ts, use specific import paths import { PLUGIN } from '../common/constants/plugin'; -import { IndexMapping } from './application/sections/home/index_list/details_page/index_mappings_embeddable'; +import { IndexMapping } from './application/sections/home/index_list/details_page/with_context_components/index_mappings_embeddable'; import { PublicApiService } from './services/public_api_service'; +import { IndexSettings } from './application/sections/home/index_list/details_page/with_context_components/index_settings_embeddable'; export class IndexMgmtUIPlugin implements @@ -159,6 +160,44 @@ export class IndexMgmtUIPlugin return IndexMapping({ dependencies: appDependencies, core: coreStart, ...props }); }; }, + getIndexSettingsComponent: (deps: { history: ScopedHistory }) => { + const { docLinks, fatalErrors, application, uiSettings, executionContext, settings, http } = + coreStart; + const { url } = share; + const appDependencies = { + core: { + fatalErrors, + getUrlForApp: application.getUrlForApp, + executionContext, + application, + http, + }, + plugins: { + usageCollection, + isFleetEnabled: Boolean(fleet), + share, + cloud, + console, + ml, + licensing, + }, + services: { + extensionsService: this.extensionsService, + }, + config: this.config, + history: deps.history, + setBreadcrumbs: undefined as any, // breadcrumbService.setBreadcrumbs, + uiSettings, + settings, + url, + docLinks, + kibanaVersion: this.kibanaVersion, + theme$: coreStart.theme.theme$, + }; + return (props: any) => { + return IndexSettings({ dependencies: appDependencies, core: coreStart, ...props }); + }; + }, }; } public stop() {} diff --git a/x-pack/plugins/ml/common/constants/messages.ts b/x-pack/plugins/ml/common/constants/messages.ts index 8eafbccc9bee9..57d6e51cd7b55 100644 --- a/x-pack/plugins/ml/common/constants/messages.ts +++ b/x-pack/plugins/ml/common/constants/messages.ts @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n'; import type { DocLinksStart } from '@kbn/core/public'; -import { JOB_ID_MAX_LENGTH, VALIDATION_STATUS } from './validation'; +import { JOB_ID_MAX_LENGTH, VALIDATION_STATUS } from '@kbn/ml-validators'; import { renderTemplate } from '../util/string_utils'; diff --git a/x-pack/plugins/ml/common/index.ts b/x-pack/plugins/ml/common/index.ts index ac0da68923e52..bc53fc4247f2d 100644 --- a/x-pack/plugins/ml/common/index.ts +++ b/x-pack/plugins/ml/common/index.ts @@ -5,7 +5,6 @@ * 2.0. */ -export { composeValidators, patternValidator } from './util/validators'; export { getDefaultCapabilities as getDefaultMlCapabilities } from './types/capabilities'; export { DATAFEED_STATE, JOB_STATE } from './constants/states'; export type { MlSummaryJob, SummaryJobState } from './types/anomaly_detection_jobs'; diff --git a/x-pack/plugins/ml/common/util/alerts.ts b/x-pack/plugins/ml/common/util/alerts.ts index b4be94ec93da5..c73c5a3ede39f 100644 --- a/x-pack/plugins/ml/common/util/alerts.ts +++ b/x-pack/plugins/ml/common/util/alerts.ts @@ -6,10 +6,12 @@ */ import { pick } from 'lodash'; + import { isDefined } from '@kbn/ml-is-defined'; +import { parseInterval } from '@kbn/ml-parse-interval'; + import type { CombinedJobWithStats, Datafeed, Job } from '../types/anomaly_detection_jobs'; import { resolveMaxTimeInterval } from './job_utils'; -import { parseInterval } from './parse_interval'; import type { JobsHealthRuleTestsConfig, JobsHealthTests } from '../types/alerts'; const narrowBucketLength = 60; diff --git a/x-pack/plugins/ml/common/util/job_utils.ts b/x-pack/plugins/ml/common/util/job_utils.ts index eb89fe695522f..cdde803a482e5 100644 --- a/x-pack/plugins/ml/common/util/job_utils.ts +++ b/x-pack/plugins/ml/common/util/job_utils.ts @@ -9,14 +9,15 @@ import { cloneDeep, each, isEmpty, isEqual, pick } from 'lodash'; import semverGte from 'semver/functions/gte'; import type { Duration } from 'moment'; import moment from 'moment'; + import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import numeral from '@elastic/numeral'; + import { i18n } from '@kbn/i18n'; import type { Filter } from '@kbn/es-query'; import { isPopulatedObject } from '@kbn/ml-is-populated-object'; import type { SerializableRecord } from '@kbn/utility-types'; import { FilterStateStore } from '@kbn/es-query'; -import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { isDefined } from '@kbn/ml-is-defined'; import { type MlEntityField, @@ -24,9 +25,9 @@ import { ML_JOB_AGGREGATION, MLCATEGORY, } from '@kbn/ml-anomaly-utils'; -import { ALLOWED_DATA_UNITS, JOB_ID_MAX_LENGTH } from '../constants/validation'; -import { parseInterval } from './parse_interval'; -import { maxLengthValidator } from './validators'; +import { maxLengthValidator, ALLOWED_DATA_UNITS, JOB_ID_MAX_LENGTH } from '@kbn/ml-validators'; +import { parseInterval } from '@kbn/ml-parse-interval'; + import { CREATED_BY_LABEL } from '../constants/new_job'; import type { CombinedJob, @@ -875,7 +876,7 @@ export function resolveMaxTimeInterval(timeIntervals: estypes.Duration[]): numbe } export function getFiltersForDSLQuery( - datafeedQuery: QueryDslQueryContainer, + datafeedQuery: estypes.QueryDslQueryContainer, dataViewId: string | undefined, alias?: string, store = FilterStateStore.APP_STATE @@ -903,7 +904,7 @@ export function getFiltersForDSLQuery( } // check to see if the query is a known "empty" shape -export function isKnownEmptyQuery(query: QueryDslQueryContainer) { +export function isKnownEmptyQuery(query: estypes.QueryDslQueryContainer) { const queries = [ // the default query used by the job wizards { bool: { must: [{ match_all: {} }] } }, diff --git a/x-pack/plugins/ml/common/util/validation_utils.ts b/x-pack/plugins/ml/common/util/validation_utils.ts index b31431cdb7d0d..ab5838be86ee9 100644 --- a/x-pack/plugins/ml/common/util/validation_utils.ts +++ b/x-pack/plugins/ml/common/util/validation_utils.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { VALIDATION_STATUS } from '../constants/validation'; +import { VALIDATION_STATUS } from '@kbn/ml-validators'; // get the most severe status level from a list of messages const contains = (arr: string[], str: string) => arr.indexOf(str) >= 0; diff --git a/x-pack/plugins/ml/public/alerting/anomaly_detection_rule/config_validator.tsx b/x-pack/plugins/ml/public/alerting/anomaly_detection_rule/config_validator.tsx index 04e96546196bb..1a2ba113aafe3 100644 --- a/x-pack/plugins/ml/public/alerting/anomaly_detection_rule/config_validator.tsx +++ b/x-pack/plugins/ml/public/alerting/anomaly_detection_rule/config_validator.tsx @@ -10,7 +10,7 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiCallOut, EuiSpacer } from '@elastic/eui'; -import { parseInterval } from '../../../common/util/parse_interval'; +import { parseInterval } from '@kbn/ml-parse-interval'; import { type CombinedJobWithStats } from '../../../common/types/anomaly_detection_jobs'; import { DATAFEED_STATE } from '../../../common/constants/states'; import { type MlAnomalyDetectionAlertParams } from '../../../common/types/alerts'; diff --git a/x-pack/plugins/ml/public/alerting/anomaly_detection_rule/ml_anomaly_alert_trigger.tsx b/x-pack/plugins/ml/public/alerting/anomaly_detection_rule/ml_anomaly_alert_trigger.tsx index 7404646db7195..3d70b4bbdc16d 100644 --- a/x-pack/plugins/ml/public/alerting/anomaly_detection_rule/ml_anomaly_alert_trigger.tsx +++ b/x-pack/plugins/ml/public/alerting/anomaly_detection_rule/ml_anomaly_alert_trigger.tsx @@ -14,6 +14,7 @@ import type { RuleTypeParamsExpressionProps } from '@kbn/triggers-actions-ui-plu import { isDefined } from '@kbn/ml-is-defined'; import { ML_ANOMALY_RESULT_TYPE, ML_ANOMALY_THRESHOLD } from '@kbn/ml-anomaly-utils'; import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { parseInterval } from '@kbn/ml-parse-interval'; import type { MlCapabilities } from '../../../common/types/capabilities'; import { ML_PAGES } from '../../../common/constants/locator'; import type { MlCoreSetup } from '../../plugin'; @@ -34,7 +35,6 @@ import { ConfigValidator } from './config_validator'; import { type CombinedJobWithStats } from '../../../common/types/anomaly_detection_jobs'; import { AdvancedSettings } from './advanced_settings'; import { getLookbackInterval, getTopNBuckets } from '../../../common/util/alerts'; -import { parseInterval } from '../../../common/util/parse_interval'; export type MlAnomalyAlertTriggerProps = RuleTypeParamsExpressionProps & { diff --git a/x-pack/plugins/ml/public/alerting/anomaly_detection_rule/preview_alert_condition.tsx b/x-pack/plugins/ml/public/alerting/anomaly_detection_rule/preview_alert_condition.tsx index e9e9c38940d61..4da8f5e63e075 100644 --- a/x-pack/plugins/ml/public/alerting/anomaly_detection_rule/preview_alert_condition.tsx +++ b/x-pack/plugins/ml/public/alerting/anomaly_detection_rule/preview_alert_condition.tsx @@ -24,10 +24,13 @@ import { EuiSpacer, EuiText, } from '@elastic/eui'; +import { + composeValidators, + requiredValidator, + timeIntervalInputValidator, +} from '@kbn/ml-validators'; import type { AlertingApiService } from '../../application/services/ml_api_service/alerting'; import type { MlAnomalyDetectionAlertParams, PreviewResponse } from '../../../common/types/alerts'; -import { composeValidators } from '../../../common'; -import { requiredValidator, timeIntervalInputValidator } from '../../../common/util/validators'; import { invalidTimeIntervalMessage } from '../../application/jobs/new_job/common/job_validator/util'; import { ALERT_PREVIEW_SAMPLE_SIZE } from '../../../common/constants/alerts'; diff --git a/x-pack/plugins/ml/public/alerting/time_interval_control.tsx b/x-pack/plugins/ml/public/alerting/time_interval_control.tsx index 43eafe7acaedc..6ec4e251ee136 100644 --- a/x-pack/plugins/ml/public/alerting/time_interval_control.tsx +++ b/x-pack/plugins/ml/public/alerting/time_interval_control.tsx @@ -5,13 +5,15 @@ * 2.0. */ -import type { EuiFieldTextProps } from '@elastic/eui'; -import { EuiFieldText, EuiFormRow } from '@elastic/eui'; import type { FC, ReactNode } from 'react'; import React, { useMemo } from 'react'; + +import type { EuiFieldTextProps } from '@elastic/eui'; +import { EuiFieldText, EuiFormRow } from '@elastic/eui'; + +import { composeValidators, timeIntervalInputValidator } from '@kbn/ml-validators'; + import { invalidTimeIntervalMessage } from '../application/jobs/new_job/common/job_validator/util'; -import { composeValidators } from '../../common'; -import { timeIntervalInputValidator } from '../../common/util/validators'; type TimeIntervalControlProps = Omit & { label: string | ReactNode; diff --git a/x-pack/plugins/ml/public/alerting/validators.ts b/x-pack/plugins/ml/public/alerting/validators.ts index 4999c8bf45a8f..283e5d473fe3d 100644 --- a/x-pack/plugins/ml/public/alerting/validators.ts +++ b/x-pack/plugins/ml/public/alerting/validators.ts @@ -6,6 +6,7 @@ */ import { numberValidator } from '@kbn/ml-agg-utils'; -import { timeIntervalInputValidator } from '../../common/util/validators'; +import { timeIntervalInputValidator } from '@kbn/ml-validators'; + export const validateLookbackInterval = timeIntervalInputValidator(); export const validateTopNBucket = numberValidator({ min: 1, integerOnly: true }); diff --git a/x-pack/plugins/ml/public/application/aiops/change_point_detection.tsx b/x-pack/plugins/ml/public/application/aiops/change_point_detection.tsx index 386210cc02d25..cbfb7fa3024c3 100644 --- a/x-pack/plugins/ml/public/application/aiops/change_point_detection.tsx +++ b/x-pack/plugins/ml/public/application/aiops/change_point_detection.tsx @@ -13,9 +13,9 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { ChangePointDetection } from '@kbn/aiops-plugin/public'; +import { useFieldStatsTrigger, FieldStatsFlyoutProvider } from '@kbn/ml-field-stats-flyout'; import { useDataSource } from '../contexts/ml/data_source_context'; -import { useFieldStatsTrigger, FieldStatsFlyoutProvider } from '../components/field_stats_flyout'; import { useMlKibana } from '../contexts/kibana'; import { HelpMenu } from '../components/help_menu'; import { TechnicalPreviewBadge } from '../components/technical_preview_badge'; diff --git a/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.tsx b/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.tsx index 42cf24b52cac4..89bb4b800eb91 100644 --- a/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.tsx +++ b/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.tsx @@ -45,10 +45,10 @@ import { escapeQuotes } from '@kbn/es-query'; import { isQuery } from '@kbn/data-plugin/public'; import type { TimeRangeBounds } from '@kbn/ml-time-buckets'; +import { parseInterval } from '@kbn/ml-parse-interval'; import { PLUGIN_ID } from '../../../../common/constants/app'; import { findMessageField } from '../../util/index_utils'; import { getInitialAnomaliesLayers, getInitialSourceIndexFieldLayers } from '../../../maps/util'; -import { parseInterval } from '../../../../common/util/parse_interval'; import { ML_APP_LOCATOR, ML_PAGES } from '../../../../common/constants/locator'; import { getFiltersForDSLQuery } from '../../../../common/util/job_utils'; diff --git a/x-pack/plugins/ml/public/application/components/callout/callout.tsx b/x-pack/plugins/ml/public/application/components/callout/callout.tsx index 8c60f87ef2427..e0ad8952057f3 100644 --- a/x-pack/plugins/ml/public/application/components/callout/callout.tsx +++ b/x-pack/plugins/ml/public/application/components/callout/callout.tsx @@ -9,8 +9,8 @@ import type { FC } from 'react'; import React from 'react'; import { EuiCallOut, EuiLink, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; -import type { CalloutMessage } from '../../../../common/constants/validation'; -import { VALIDATION_STATUS } from '../../../../common/constants/validation'; +import type { CalloutMessage } from '@kbn/ml-validators'; +import { VALIDATION_STATUS } from '@kbn/ml-validators'; export const defaultIconType = 'questionInCircle'; diff --git a/x-pack/plugins/ml/public/application/components/custom_urls/custom_url_editor/list.tsx b/x-pack/plugins/ml/public/application/components/custom_urls/custom_url_editor/list.tsx index bb913b8ecec34..e76b55a6f2369 100644 --- a/x-pack/plugins/ml/public/application/components/custom_urls/custom_url_editor/list.tsx +++ b/x-pack/plugins/ml/public/application/components/custom_urls/custom_url_editor/list.tsx @@ -28,13 +28,13 @@ import { type DataFrameAnalyticsConfig, } from '@kbn/ml-data-frame-analytics-utils'; import { parseUrlState } from '@kbn/ml-url-state'; +import { parseInterval } from '@kbn/ml-parse-interval'; import { useMlApi, useMlKibana } from '../../../contexts/kibana'; import { useToastNotificationService } from '../../../services/toast_notification_service'; import { isValidLabel, openCustomUrlWindow } from '../../../util/custom_url_utils'; import { getTestUrl } from './utils'; -import { parseInterval } from '../../../../../common/util/parse_interval'; import { TIME_RANGE_TYPE } from './constants'; import type { Job } from '../../../../../common/types/anomaly_detection_jobs'; diff --git a/x-pack/plugins/ml/public/application/components/custom_urls/custom_url_editor/utils.ts b/x-pack/plugins/ml/public/application/components/custom_urls/custom_url_editor/utils.ts index 0fc2a7b514b2d..c154abb6f5f69 100644 --- a/x-pack/plugins/ml/public/application/components/custom_urls/custom_url_editor/utils.ts +++ b/x-pack/plugins/ml/public/application/components/custom_urls/custom_url_editor/utils.ts @@ -25,8 +25,9 @@ import { type DataFrameAnalyticsConfig, DEFAULT_RESULTS_FIELD, } from '@kbn/ml-data-frame-analytics-utils'; - import { isDefined } from '@kbn/ml-is-defined'; +import { parseInterval } from '@kbn/ml-parse-interval'; + import type { DashboardItems } from '../../../services/dashboard_service'; import { categoryFieldTypes } from '../../../../../common/util/fields_utils'; import { TIME_RANGE_TYPE, URL_TYPE } from './constants'; @@ -35,7 +36,6 @@ import { getPartitioningFieldNames, getFiltersForDSLQuery, } from '../../../../../common/util/job_utils'; -import { parseInterval } from '../../../../../common/util/parse_interval'; import { replaceStringTokens } from '../../../util/string_utils'; import { replaceTokensInUrlValue, diff --git a/x-pack/plugins/ml/public/application/components/field_stats_flyout/index.ts b/x-pack/plugins/ml/public/application/components/field_stats_flyout/index.ts deleted file mode 100644 index d844dd037bd5b..0000000000000 --- a/x-pack/plugins/ml/public/application/components/field_stats_flyout/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export { FieldStatsFlyout } from './field_stats_flyout'; -export { FieldStatsContent } from './field_stats_content'; -export { FieldStatsFlyoutProvider } from './field_stats_flyout_provider'; -export { - MLFieldStatsFlyoutContext, - useFieldStatsFlyoutContext, -} from './use_field_stats_flytout_context'; -export { FieldStatsInfoButton } from './field_stats_info_button'; -export { useFieldStatsTrigger } from './use_field_stats_trigger'; -export { EuiComboBoxWithFieldStats } from './eui_combo_box_with_field_stats'; diff --git a/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/validate.ts b/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/validate.ts index 235c3d7b5ffe0..0431addf4b6fa 100644 --- a/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/validate.ts +++ b/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/validate.ts @@ -6,11 +6,13 @@ */ import { useCallback } from 'react'; + import { i18n } from '@kbn/i18n'; +import { JOB_ID_MAX_LENGTH } from '@kbn/ml-validators'; + import type { JobType } from '../../../../../common/types/saved_objects'; import { isValidIndexName } from '../../../../../common/util/es_utils'; import { isJobIdValid } from '../../../../../common/util/job_utils'; -import { JOB_ID_MAX_LENGTH } from '../../../../../common/constants/validation'; import type { JobIdObject } from './jobs_import_service'; import { useMlApi } from '../../../contexts/kibana'; diff --git a/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx index 639c101e7be6f..4cb7df0894899 100644 --- a/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx +++ b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx @@ -31,6 +31,7 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { timeFormatter } from '@kbn/ml-date-utils'; +import { parseInterval } from '@kbn/ml-parse-interval'; import type { ModelSnapshot, @@ -42,7 +43,6 @@ import { mlResultsServiceProvider } from '../../../services/results_service'; import type { LineChartPoint } from '../../../jobs/new_job/common/chart_loader'; import { EventRateChart } from '../../../jobs/new_job/pages/components/charts/event_rate_chart/event_rate_chart'; import type { Anomaly } from '../../../jobs/new_job/common/results_loader/results_loader'; -import { parseInterval } from '../../../../../common/util/parse_interval'; import type { CalendarEvent } from './create_calendar'; import { CreateCalendar } from './create_calendar'; diff --git a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_vega_lite_spec.test.ts b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_vega_lite_spec.test.ts index dfa2389c6e0a5..e2322ff7dd2b3 100644 --- a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_vega_lite_spec.test.ts +++ b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_vega_lite_spec.test.ts @@ -16,6 +16,7 @@ import { LEGEND_TYPES } from '../vega_chart/common'; import { getColorSpec, + getEscapedVegaFieldName, getScatterplotMatrixVegaLiteSpec, COLOR_RANGE_NOMINAL, COLOR_RANGE_OUTLIER, @@ -75,6 +76,56 @@ describe('getColorSpec()', () => { }); }); +describe('getEscapedVegaFieldName()', () => { + it('should escape dots in field names', () => { + const fieldName = 'field.name'; + const escapedFieldName = getEscapedVegaFieldName(fieldName); + expect(escapedFieldName).toBe('field\\.name'); + }); + + it('should escape brackets in field names', () => { + const fieldName = 'field[name]'; + const escapedFieldName = getEscapedVegaFieldName(fieldName); + expect(escapedFieldName).toBe('field\\[name\\]'); + }); + + it('should escape both dots and brackets in field names', () => { + const fieldName = 'field.name[0]'; + const escapedFieldName = getEscapedVegaFieldName(fieldName); + expect(escapedFieldName).toBe('field\\.name\\[0\\]'); + }); + + it('should return the same string if there are no special characters', () => { + const fieldName = 'fieldname'; + const escapedFieldName = getEscapedVegaFieldName(fieldName); + expect(escapedFieldName).toBe('fieldname'); + }); + + it('should prepend a string if provided', () => { + const fieldName = 'field.name'; + const prependString = 'prefix_'; + const escapedFieldName = getEscapedVegaFieldName(fieldName, prependString); + expect(escapedFieldName).toBe('prefix_field\\.name'); + }); + + it('should escape newlines in field names', () => { + // String quotes process backslashes, so we need to escape them for + // the test string to contain a backslash. For example, without the + // double backslash, this string would contain a newline character. + const fieldName = 'field\\name'; + const escapedFieldName = getEscapedVegaFieldName(fieldName); + expect(escapedFieldName).toBe('field\\\\name'); + }); + + it('should escape backslashes in field names', () => { + // String quotes process backslashes, so we need to escape them for + // the test string to contain a backslash. + const fieldName = 'fieldname\\withbackslash'; + const escapedFieldName = getEscapedVegaFieldName(fieldName); + expect(escapedFieldName).toBe('fieldname\\\\withbackslash'); + }); +}); + describe('getScatterplotMatrixVegaLiteSpec()', () => { const forCustomLink = false; diff --git a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_vega_lite_spec.ts b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_vega_lite_spec.ts index 96889679ed155..e7d7066339847 100644 --- a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_vega_lite_spec.ts +++ b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_vega_lite_spec.ts @@ -249,11 +249,28 @@ const getVegaSpecLayer = ( }; }; -// Escapes the characters .[] in field names with double backslashes +// Escapes the characters .[]\ in field names with double backslashes // since VEGA treats dots/brackets in field names as nested values. // See https://vega.github.io/vega-lite/docs/field.html for details. -function getEscapedVegaFieldName(fieldName: string, prependString: string = '') { - return `${prependString}${fieldName.replace(/([\.|\[|\]])/g, '\\$1')}`; +export function getEscapedVegaFieldName(fieldName: string, prependString: string = '') { + // Note the following isn't 100% ideal because there are cases when we may + // end up with an additional backslash being rendered for labels of the + // scatterplot. However, all other variations I tried caused rendering + // problems of the charts and rendering would fail completely. + + // For example, just escaping \n in the first replace without the general + // backslash escaping causes the following Vega error: + // Duplicate scale or projection name: "child__row_my_numbercolumn_my_number_x" + + // Escaping just the backslash without the additional \n escaping causes + // causes an "expression parse error" in Vega and the chart wouldn't render. + + // Escape newline characters + fieldName = fieldName.replace(/\n/g, '\\n'); + // Escape .[]\ + fieldName = fieldName.replace(/([\.|\[|\]|\\])/g, '\\$1'); + + return `${prependString}${fieldName}`; } type VegaValue = Record; diff --git a/x-pack/plugins/ml/public/application/components/validate_job/validate_job_view.js b/x-pack/plugins/ml/public/application/components/validate_job/validate_job_view.js index a959208fe0c31..4701937a272ed 100644 --- a/x-pack/plugins/ml/public/application/components/validate_job/validate_job_view.js +++ b/x-pack/plugins/ml/public/application/components/validate_job/validate_job_view.js @@ -25,9 +25,9 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; +import { VALIDATION_STATUS } from '@kbn/ml-validators'; import { parseMessages } from '../../../../common/constants/messages'; -import { VALIDATION_STATUS } from '../../../../common/constants/validation'; import { Callout, statusToEuiIconType } from '../callout'; import { getMostSevereMessageStatus } from '../../../../common/util/validation_utils'; import { toastNotificationServiceProvider } from '../../services/toast_notification_service'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/analysis_fields_table.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/analysis_fields_table.tsx index 82582e9b913bf..3ddd02d0052f9 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/analysis_fields_table.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/analysis_fields_table.tsx @@ -21,9 +21,12 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import type { ES_FIELD_TYPES } from '@kbn/field-types'; import type { FieldSelectionItem } from '@kbn/ml-data-frame-analytics-utils'; -import { useFieldStatsTrigger } from '../../../../../components/field_stats_flyout/use_field_stats_trigger'; -import type { FieldForStats } from '../../../../../components/field_stats_flyout/field_stats_info_button'; -import { FieldStatsInfoButton } from '../../../../../components/field_stats_flyout/field_stats_info_button'; +import { + useFieldStatsTrigger, + FieldStatsInfoButton, + type FieldForStats, +} from '@kbn/ml-field-stats-flyout'; + // @ts-ignore could not find declaration file import { CustomSelectionTable } from '../../../../../components/custom_selection_table'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx index c0920b39d47fc..b2b60e95dceda 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx @@ -29,12 +29,13 @@ import { } from '@kbn/ml-data-frame-analytics-utils'; import { DataGrid } from '@kbn/ml-data-grid'; import { SEARCH_QUERY_LANGUAGE } from '@kbn/ml-query-utils'; -import { useMlApi, useMlKibana } from '../../../../../contexts/kibana'; import { EuiComboBoxWithFieldStats, FieldStatsFlyoutProvider, -} from '../../../../../components/field_stats_flyout'; -import type { FieldForStats } from '../../../../../components/field_stats_flyout/field_stats_info_button'; + type FieldForStats, +} from '@kbn/ml-field-stats-flyout'; + +import { useMlApi, useMlKibana } from '../../../../../contexts/kibana'; import { useNewJobCapsServiceAnalytics } from '../../../../../services/new_job_capabilities/new_job_capabilities_service_analytics'; import { useDataSource } from '../../../../../contexts/ml'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_form.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_form.tsx index f43ce7bd2fb9b..7c40b121aa5fa 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_form.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_form.tsx @@ -13,10 +13,10 @@ import { i18n } from '@kbn/i18n'; import { extractErrorMessage } from '@kbn/ml-error-utils'; import { CreateDataViewForm } from '@kbn/ml-data-view-utils/components/create_data_view_form_row'; import { DestinationIndexForm } from '@kbn/ml-creation-wizard-utils/components/destination_index_form'; +import { JOB_ID_MAX_LENGTH } from '@kbn/ml-validators'; import { useMlApi, useMlKibana } from '../../../../../contexts/kibana'; import type { CreateAnalyticsStepProps } from '../../../analytics_management/hooks/use_create_analytics_form'; -import { JOB_ID_MAX_LENGTH } from '../../../../../../../common/constants/validation'; import { ContinueButton } from '../continue_button'; import { ANALYTICS_STEPS } from '../../page'; import { useCanCreateDataView } from '../../hooks/use_can_create_data_view'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/validation_step/validation_step.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/validation_step/validation_step.tsx index 81e4f20dac2ba..b080eacc43424 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/validation_step/validation_step.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/validation_step/validation_step.tsx @@ -9,7 +9,8 @@ import type { FC } from 'react'; import React from 'react'; import { EuiLoadingSpinner, EuiSpacer } from '@elastic/eui'; -import type { CalloutMessage } from '../../../../../../../common/constants/validation'; +import type { CalloutMessage } from '@kbn/ml-validators'; + import { Callout } from '../../../../../components/callout'; import { ANALYTICS_STEPS } from '../../page'; import { ContinueButton } from '../continue_button'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/validation_step/validation_step_wrapper.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/validation_step/validation_step_wrapper.tsx index 3c8cba36458a2..4dbce9dffd632 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/validation_step/validation_step_wrapper.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/validation_step/validation_step_wrapper.tsx @@ -13,6 +13,8 @@ import { EuiForm } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { extractErrorMessage } from '@kbn/ml-error-utils'; +import type { CalloutMessage, ValidateAnalyticsJobResponse } from '@kbn/ml-validators'; +import { VALIDATION_STATUS } from '@kbn/ml-validators'; import type { CreateAnalyticsStepProps } from '../../../analytics_management/hooks/use_create_analytics_form'; import { ValidationStep } from './validation_step'; @@ -20,11 +22,6 @@ import { ValidationStepDetails } from './validation_step_details'; import { ANALYTICS_STEPS } from '../../page'; import { useMlApi } from '../../../../../contexts/kibana'; import { getJobConfigFromFormState } from '../../../analytics_management/hooks/use_create_analytics_form/state'; -import type { - CalloutMessage, - ValidateAnalyticsJobResponse, -} from '../../../../../../../common/constants/validation'; -import { VALIDATION_STATUS } from '../../../../../../../common/constants/validation'; export interface ValidationSummary { warning: number; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/expandable_section/expandable_section_results.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/expandable_section/expandable_section_results.tsx index 212c0fa6d2cf7..cc296a42afbae 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/expandable_section/expandable_section_results.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/expandable_section/expandable_section_results.tsx @@ -39,6 +39,7 @@ import { import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { SEARCH_QUERY_LANGUAGE } from '@kbn/ml-query-utils'; +import { parseInterval } from '@kbn/ml-parse-interval'; import type { useColorRange } from '../../../../../components/color_range_legend'; import { ColorRangeLegend } from '../../../../../components/color_range_legend'; @@ -51,7 +52,6 @@ import { openCustomUrlWindow, } from '../../../../../util/custom_url_utils'; import { replaceStringTokens } from '../../../../../util/string_utils'; -import { parseInterval } from '../../../../../../../common/util/parse_interval'; import type { ExpandableSectionProps } from '.'; import { ExpandableSection, HEADER_ITEMS_LOADING } from '.'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_edit/edit_action_flyout.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_edit/edit_action_flyout.tsx index 77aa8b669011c..4922ae35e81d5 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_edit/edit_action_flyout.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_edit/edit_action_flyout.tsx @@ -35,11 +35,11 @@ import { type DataFrameAnalyticsConfig, type UpdateDataFrameAnalyticsConfig, } from '@kbn/ml-data-frame-analytics-utils'; +import type { MemoryInputValidatorResult } from '@kbn/ml-validators'; +import { memoryInputValidator } from '@kbn/ml-validators'; import { useMlKibana, useMlApi } from '../../../../../contexts/kibana'; import { useToastNotificationService } from '../../../../../services/toast_notification_service'; -import type { MemoryInputValidatorResult } from '../../../../../../../common/util/validators'; -import { memoryInputValidator } from '../../../../../../../common/util/validators'; import { useRefreshAnalyticsList } from '../../../../common/analytics'; import type { EditAction } from './use_edit_action'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts index 8181b6b6360c4..2fd395a5e20c5 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts @@ -24,6 +24,14 @@ import { TRAINING_PERCENT_MIN, TRAINING_PERCENT_MAX, } from '@kbn/ml-data-frame-analytics-utils'; +import { + composeValidators, + maxLengthValidator, + memoryInputValidator, + requiredValidator, + JOB_ID_MAX_LENGTH, + ALLOWED_DATA_UNITS, +} from '@kbn/ml-validators'; import { isValidIndexName } from '../../../../../../../common/util/es_utils'; @@ -35,16 +43,6 @@ import { isJobIdValid, validateModelMemoryLimitUnits, } from '../../../../../../../common/util/job_utils'; -import { - composeValidators, - maxLengthValidator, - memoryInputValidator, - requiredValidator, -} from '../../../../../../../common/util/validators'; -import { - JOB_ID_MAX_LENGTH, - ALLOWED_DATA_UNITS, -} from '../../../../../../../common/constants/validation'; import { isAdvancedConfig } from '../../components/action_clone/clone_action_name'; const { collapseLiteralStrings } = XJson; diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_utils.ts b/x-pack/plugins/ml/public/application/explorer/explorer_utils.ts index 90182b07152ad..abe921ee4352e 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_utils.ts +++ b/x-pack/plugins/ml/public/application/explorer/explorer_utils.ts @@ -27,6 +27,7 @@ import { import type { InfluencersFilterQuery } from '@kbn/ml-anomaly-utils'; import type { TimeRangeBounds } from '@kbn/ml-time-buckets'; import type { IUiSettingsClient } from '@kbn/core/public'; +import { parseInterval } from '@kbn/ml-parse-interval'; import { ANNOTATIONS_TABLE_DEFAULT_QUERY_SIZE, @@ -39,7 +40,6 @@ import { isModelPlotEnabled, isTimeSeriesViewJob, } from '../../../common/util/job_utils'; -import { parseInterval } from '../../../common/util/parse_interval'; import type { MlJobService } from '../services/job_service'; import type { SwimlaneType } from './explorer_constants'; diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/datafeed.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/datafeed.js index c62907052773c..5f6ba8f6c89be 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/datafeed.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/datafeed.js @@ -17,11 +17,12 @@ import { EuiCallOut, } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { parseInterval } from '@kbn/ml-parse-interval'; + import { calculateDatafeedFrequencyDefaultSeconds } from '../../../../../../../common/util/job_utils'; import { getNewJobDefaults } from '../../../../../services/ml_server_info'; -import { parseInterval } from '../../../../../../../common/util/parse_interval'; import { MLJobEditor, ML_EDITOR_MODE } from '../../ml_job_editor'; -import { FormattedMessage } from '@kbn/i18n-react'; function getDefaults(bucketSpan, jobDefaults) { const bucketSpanSeconds = bucketSpan !== undefined ? parseInterval(bucketSpan).asSeconds() : ''; diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js index 8e457e8b1e800..57ee1e5f3ef5f 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js @@ -6,13 +6,14 @@ */ import { each } from 'lodash'; + import { i18n } from '@kbn/i18n'; +import { parseInterval } from '@kbn/ml-parse-interval'; import { toastNotificationServiceProvider } from '../../../services/toast_notification_service'; import { stringMatch } from '../../../util/string_utils'; import { JOB_STATE, DATAFEED_STATE } from '../../../../../common/constants/states'; import { JOB_ACTION } from '../../../../../common/constants/job_actions'; -import { parseInterval } from '../../../../../common/util/parse_interval'; import { mlCalendarService } from '../../../services/calendar_service'; import { jobCloningService } from '../../../services/job_cloning_service'; import { ML_PAGES } from '../../../../../common/constants/locator'; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts index 103b789fe66b6..f43fa93368447 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts @@ -7,6 +7,7 @@ import { BehaviorSubject } from 'rxjs'; import { cloneDeep } from 'lodash'; + import { ES_FIELD_TYPES } from '@kbn/field-types'; import type { Query } from '@kbn/es-query'; import type { DataView } from '@kbn/data-views-plugin/public'; @@ -22,6 +23,8 @@ import { import type { RuntimeMappings } from '@kbn/ml-runtime-field-utils'; import type { SavedSearch } from '@kbn/saved-search-plugin/public'; import { isPopulatedObject } from '@kbn/ml-is-populated-object'; +import { parseInterval } from '@kbn/ml-parse-interval'; + import { createDatafeedId } from '../../../../../../common/util/job_utils'; import type { MlApi } from '../../../../services/ml_api_service'; import type { IndexPatternTitle } from '../../../../../../common/types/kibana'; @@ -42,7 +45,6 @@ import type { CREATED_BY_LABEL } from '../../../../../../common/constants/new_jo import { JOB_TYPE, SHARED_RESULTS_INDEX_NAME } from '../../../../../../common/constants/new_job'; import { collectAggs } from './util/general'; import { filterRuntimeMappings } from './util/filter_runtime_mappings'; -import { parseInterval } from '../../../../../../common/util/parse_interval'; import type { Calendar } from '../../../../../../common/types/calendars'; import { mlCalendarService } from '../../../../services/calendar_service'; import { getDatafeedAggregations } from '../../../../../../common/util/datafeed_utils'; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/single_metric_job_creator.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/single_metric_job_creator.ts index a90cca5153069..29ce6de6e54d7 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/single_metric_job_creator.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/single_metric_job_creator.ts @@ -15,8 +15,9 @@ import { ES_AGGREGATION, } from '@kbn/ml-anomaly-utils'; import type { SavedSearch } from '@kbn/saved-search-plugin/public'; +import { parseInterval } from '@kbn/ml-parse-interval'; + import type { MlApi } from '../../../../services/ml_api_service'; -import { parseInterval } from '../../../../../../common/util/parse_interval'; import { JobCreator } from './job_creator'; import type { Job, diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_validator/util.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_validator/util.ts index 91de9a04043e3..340ab047d6582 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_validator/util.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_validator/util.ts @@ -5,14 +5,13 @@ * 2.0. */ -import { i18n } from '@kbn/i18n'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; + +import { i18n } from '@kbn/i18n'; +import { ALLOWED_DATA_UNITS, JOB_ID_MAX_LENGTH } from '@kbn/ml-validators'; + import type { BasicValidations } from './job_validator'; import type { Job, Datafeed } from '../../../../../../common/types/anomaly_detection_jobs'; -import { - ALLOWED_DATA_UNITS, - JOB_ID_MAX_LENGTH, -} from '../../../../../../common/constants/validation'; import { getNewJobLimits } from '../../../../services/ml_server_info'; import type { ValidationResults } from '../../../../../../common/util/job_utils'; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/results_loader/results_loader.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/results_loader/results_loader.ts index 0492cb49ebedd..f3d1803415f10 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/results_loader/results_loader.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/results_loader/results_loader.ts @@ -10,8 +10,8 @@ import { BehaviorSubject, lastValueFrom } from 'rxjs'; import type { ML_ANOMALY_SEVERITY } from '@kbn/ml-anomaly-utils'; import { getSeverityType, ES_AGGREGATION } from '@kbn/ml-anomaly-utils'; import type { TimeBuckets } from '@kbn/ml-time-buckets'; +import { parseInterval } from '@kbn/ml-parse-interval'; -import { parseInterval } from '../../../../../../common/util/parse_interval'; import { JOB_TYPE } from '../../../../../../common/constants/new_job'; import type { ModelPlotOutputResults } from '../../../../services/results_service'; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/advanced_detector_modal.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/advanced_detector_modal.tsx index fbfaceafde822..667840a6ca486 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/advanced_detector_modal.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/advanced_detector_modal.tsx @@ -8,6 +8,7 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { FC } from 'react'; import React, { Fragment, useState, useContext, useEffect } from 'react'; + import type { EuiComboBoxOptionOption } from '@elastic/eui'; import { EuiComboBox, @@ -17,13 +18,15 @@ import { EuiHorizontalRule, EuiTextArea, } from '@elastic/eui'; + import { type Field, type Aggregation, EVENT_RATE_FIELD_ID, mlCategory, } from '@kbn/ml-anomaly-utils'; -import { useFieldStatsTrigger } from '../../../../../../../components/field_stats_flyout/use_field_stats_trigger'; +import { useFieldStatsTrigger } from '@kbn/ml-field-stats-flyout'; + import { JobCreatorContext } from '../../../job_creator_context'; import type { AdvancedJobCreator } from '../../../../../common/job_creator'; import { diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/agg_select/agg_select.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/agg_select/agg_select.tsx index 4ac460d997e43..257b075ed4511 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/agg_select/agg_select.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/agg_select/agg_select.tsx @@ -11,9 +11,8 @@ import type { EuiComboBoxOptionOption } from '@elastic/eui'; import { EuiComboBox, EuiFormRow } from '@elastic/eui'; import type { Field, Aggregation, AggFieldPair } from '@kbn/ml-anomaly-utils'; import { EVENT_RATE_FIELD_ID } from '@kbn/ml-anomaly-utils'; -import { FieldStatsInfoButton } from '../../../../../../../components/field_stats_flyout/field_stats_info_button'; +import { useFieldStatsTrigger, FieldStatsInfoButton } from '@kbn/ml-field-stats-flyout'; import { JobCreatorContext } from '../../../job_creator_context'; -import { useFieldStatsTrigger } from '../../../../../../../components/field_stats_flyout/use_field_stats_trigger'; // The display label used for an aggregation e.g. sum(bytes). export type Label = string; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_field/categorization_field_select.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_field/categorization_field_select.tsx index 8ecdd28113e3e..b2eb7a08d93be 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_field/categorization_field_select.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_field/categorization_field_select.tsx @@ -11,7 +11,8 @@ import type { EuiComboBoxOptionOption } from '@elastic/eui'; import { EuiComboBox } from '@elastic/eui'; import type { Field } from '@kbn/ml-anomaly-utils'; -import { useFieldStatsTrigger } from '../../../../../../../components/field_stats_flyout/use_field_stats_trigger'; +import { useFieldStatsTrigger } from '@kbn/ml-field-stats-flyout'; + import { JobCreatorContext } from '../../../job_creator_context'; import { createFieldOptions } from '../../../../../common/job_creator/util/general'; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_partition_field/categorization_per_partition_input.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_partition_field/categorization_per_partition_input.tsx index 513522c50bee3..237688b215511 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_partition_field/categorization_per_partition_input.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_partition_field/categorization_per_partition_input.tsx @@ -11,7 +11,7 @@ import type { EuiComboBoxOptionOption } from '@elastic/eui'; import { EuiComboBox } from '@elastic/eui'; import type { Field } from '@kbn/ml-anomaly-utils'; -import { useFieldStatsTrigger } from '../../../../../../../components/field_stats_flyout/use_field_stats_trigger'; +import { useFieldStatsTrigger } from '@kbn/ml-field-stats-flyout'; import { JobCreatorContext } from '../../../job_creator_context'; import { createFieldOptions } from '../../../../../common/job_creator/util/general'; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/geo_field/geo_field_select.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/geo_field/geo_field_select.tsx index a22b04ea7f4c9..f40e28f2fea28 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/geo_field/geo_field_select.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/geo_field/geo_field_select.tsx @@ -10,7 +10,7 @@ import React, { useCallback, useMemo } from 'react'; import type { EuiComboBoxOptionOption } from '@elastic/eui'; import { EuiComboBox } from '@elastic/eui'; import type { Field } from '@kbn/ml-anomaly-utils'; -import { useFieldStatsTrigger } from '../../../../../../../components/field_stats_flyout/use_field_stats_trigger'; +import { useFieldStatsTrigger } from '@kbn/ml-field-stats-flyout'; interface DropDownLabel { label: string; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/influencers/influencers_select.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/influencers/influencers_select.tsx index 2894072bacce5..4c0946657d8e6 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/influencers/influencers_select.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/influencers/influencers_select.tsx @@ -11,7 +11,8 @@ import type { EuiComboBoxOptionOption } from '@elastic/eui'; import { EuiComboBox } from '@elastic/eui'; import type { Field } from '@kbn/ml-anomaly-utils'; -import { useFieldStatsTrigger } from '../../../../../../../components/field_stats_flyout/use_field_stats_trigger'; +import { useFieldStatsTrigger } from '@kbn/ml-field-stats-flyout'; + import { JobCreatorContext } from '../../../job_creator_context'; import { createFieldOptions, diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/rare_field/rare_field_select.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/rare_field/rare_field_select.tsx index 76d9f63aef828..a834a0d3bbdd4 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/rare_field/rare_field_select.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/rare_field/rare_field_select.tsx @@ -10,7 +10,7 @@ import React from 'react'; import type { EuiComboBoxOptionOption } from '@elastic/eui'; import { EuiComboBox } from '@elastic/eui'; import type { Field, SplitField } from '@kbn/ml-anomaly-utils'; -import { useFieldStatsTrigger } from '../../../../../../../components/field_stats_flyout/use_field_stats_trigger'; +import { useFieldStatsTrigger } from '@kbn/ml-field-stats-flyout'; interface DropDownLabel { label: string; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field_select/split_field_select.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field_select/split_field_select.tsx index 20cf8bbc0a195..d621e85b3f56a 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field_select/split_field_select.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field_select/split_field_select.tsx @@ -11,7 +11,7 @@ import type { EuiComboBoxOptionOption } from '@elastic/eui'; import { EuiComboBox } from '@elastic/eui'; import type { Field, SplitField } from '@kbn/ml-anomaly-utils'; -import { useFieldStatsTrigger } from '../../../../../../../components/field_stats_flyout/use_field_stats_trigger'; +import { useFieldStatsTrigger } from '@kbn/ml-field-stats-flyout'; interface DropDownLabel { label: string; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/summary_count_field_select.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/summary_count_field_select.tsx index 9a7826533f082..8fc91b74d1a2c 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/summary_count_field_select.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/summary_count_field_select.tsx @@ -11,7 +11,8 @@ import type { EuiComboBoxOptionOption } from '@elastic/eui'; import { EuiComboBox } from '@elastic/eui'; import type { Field } from '@kbn/ml-anomaly-utils'; -import { useFieldStatsTrigger } from '../../../../../../../components/field_stats_flyout/use_field_stats_trigger'; +import { useFieldStatsTrigger } from '@kbn/ml-field-stats-flyout'; + import { JobCreatorContext } from '../../../job_creator_context'; import { createFieldOptions, diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/pick_fields.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/pick_fields.tsx index 76e35e1d5847d..5f85554470258 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/pick_fields.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/pick_fields.tsx @@ -8,7 +8,8 @@ import type { FC } from 'react'; import React, { Fragment, useContext, useEffect, useState } from 'react'; -import { useFieldStatsFlyoutContext } from '../../../../../components/field_stats_flyout'; +import { useFieldStatsFlyoutContext } from '@kbn/ml-field-stats-flyout'; + import { JobCreatorContext } from '../job_creator_context'; import { WizardNav } from '../wizard_nav'; import type { StepProps } from '../step_types'; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/new_job/wizard_steps.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/new_job/wizard_steps.tsx index e81650e17c646..6c4600be5d25e 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/new_job/wizard_steps.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/new_job/wizard_steps.tsx @@ -13,9 +13,9 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { EuiSpacer, EuiTitle, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import type { FieldStatsServices } from '@kbn/unified-field-list/src/components/field_stats'; +import { FieldStatsFlyoutProvider } from '@kbn/ml-field-stats-flyout'; import { JobCreatorContext } from '../components/job_creator_context'; import { useMlKibana } from '../../../../contexts/kibana'; -import { FieldStatsFlyoutProvider } from '../../../../components/field_stats_flyout'; import { WIZARD_STEPS } from '../components/step_types'; import { TimeRangeStep } from '../components/time_range_step'; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/edit_job.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/edit_job.tsx index ef036b2abb05f..68093350c1de8 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/edit_job.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/edit_job.tsx @@ -7,6 +7,7 @@ import type { FC } from 'react'; import React, { useEffect, useState } from 'react'; + import { EuiButton, EuiButtonEmpty, @@ -21,12 +22,14 @@ import { EuiSpacer, EuiTitle, } from '@elastic/eui'; + import { FormattedMessage } from '@kbn/i18n-react'; +import { composeValidators, maxLengthValidator } from '@kbn/ml-validators'; +import { JOB_ID_MAX_LENGTH } from '@kbn/ml-validators'; + import type { ModuleJobUI } from '../page'; import { usePartialState } from '../../../../components/custom_hooks'; -import { composeValidators, maxLengthValidator } from '../../../../../../common/util/validators'; import { isJobIdValid } from '../../../../../../common/util/job_utils'; -import { JOB_ID_MAX_LENGTH } from '../../../../../../common/constants/validation'; import { JobGroupsInput } from '../../common/components'; import type { JobOverride } from '../../../../../../common/types/modules'; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/job_settings_form.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/job_settings_form.tsx index a13664e663e5a..db9d8993f0430 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/job_settings_form.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/job_settings_form.tsx @@ -7,6 +7,7 @@ import type { FC } from 'react'; import React, { useEffect, useState, useCallback, useMemo } from 'react'; + import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { @@ -21,15 +22,16 @@ import { EuiTextAlign, } from '@elastic/eui'; import { getTimeFilterRange, useTimefilter } from '@kbn/ml-date-picker'; -import { useDataSource } from '../../../../contexts/ml/data_source_context'; -import type { ModuleJobUI } from '../page'; -import { SAVE_STATE } from '../page'; import { composeValidators, maxLengthValidator, patternValidator, -} from '../../../../../../common/util/validators'; -import { JOB_ID_MAX_LENGTH } from '../../../../../../common/constants/validation'; + JOB_ID_MAX_LENGTH, +} from '@kbn/ml-validators'; + +import { useDataSource } from '../../../../contexts/ml/data_source_context'; +import type { ModuleJobUI } from '../page'; +import { SAVE_STATE } from '../page'; import type { TimeRange } from '../../common/components'; import { TimeRangePicker } from '../../common/components'; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/utils/new_job_utils.ts b/x-pack/plugins/ml/public/application/jobs/new_job/utils/new_job_utils.ts index 531b4ad4d0ac4..e96643c7ba6c1 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/utils/new_job_utils.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/utils/new_job_utils.ts @@ -18,28 +18,18 @@ import type { Filter, Query, DataViewBase } from '@kbn/es-query'; import type { IUiSettingsClient } from '@kbn/core/public'; import { getEsQueryConfig } from '@kbn/data-plugin/public'; import type { SavedSearch } from '@kbn/saved-search-plugin/public'; -import { SEARCH_QUERY_LANGUAGE } from '@kbn/ml-query-utils'; +import { getDefaultDSLQuery, SEARCH_QUERY_LANGUAGE } from '@kbn/ml-query-utils'; import { getQueryFromSavedSearchObject } from '../../../util/index_utils'; // Provider for creating the items used for searching and job creation. -const DEFAULT_DSL_QUERY: estypes.QueryDslQueryContainer = { - bool: { - must: [ - { - match_all: {}, - }, - ], - }, -}; - export const DEFAULT_QUERY: Query = { query: '', language: 'lucene', }; export function getDefaultDatafeedQuery() { - return cloneDeep(DEFAULT_DSL_QUERY); + return getDefaultDSLQuery(); } export function getDefaultQuery() { diff --git a/x-pack/plugins/ml/public/application/model_management/deployment_setup.tsx b/x-pack/plugins/ml/public/application/model_management/deployment_setup.tsx index 96152e6726ba8..c62a1122c2d0f 100644 --- a/x-pack/plugins/ml/public/application/model_management/deployment_setup.tsx +++ b/x-pack/plugins/ml/public/application/model_management/deployment_setup.tsx @@ -32,12 +32,8 @@ import type { CoreStart, OverlayStart } from '@kbn/core/public'; import { css } from '@emotion/react'; import { numberValidator } from '@kbn/ml-agg-utils'; import { toMountPoint } from '@kbn/react-kibana-mount'; +import { composeValidators, dictionaryValidator, requiredValidator } from '@kbn/ml-validators'; import { getNewJobLimits, isCloudTrial } from '../services/ml_server_info'; -import { - composeValidators, - dictionaryValidator, - requiredValidator, -} from '../../../common/util/validators'; import type { ModelItem } from './models_list'; import { useEnabledFeatures } from '../contexts/ml'; diff --git a/x-pack/plugins/ml/public/application/services/job_service.js b/x-pack/plugins/ml/public/application/services/job_service.js index 936317675fbbb..3c896e5d00a88 100644 --- a/x-pack/plugins/ml/public/application/services/job_service.js +++ b/x-pack/plugins/ml/public/application/services/job_service.js @@ -7,7 +7,8 @@ import { cloneDeep, each, find, get } from 'lodash'; -import { parseInterval } from '../../../common/util/parse_interval'; +import { parseInterval } from '@kbn/ml-parse-interval'; + import { createDatafeedId } from '../../../common/util/job_utils'; import { isWebUrl } from '../util/url_utils'; diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.ts index e9b2e914fabe3..37709b7474978 100644 --- a/x-pack/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.ts +++ b/x-pack/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.ts @@ -16,15 +16,16 @@ import type { DeleteDataFrameAnalyticsWithIndexStatus, UpdateDataFrameAnalyticsConfig, } from '@kbn/ml-data-frame-analytics-utils'; +import type { ValidateAnalyticsJobResponse } from '@kbn/ml-validators'; import { ML_INTERNAL_BASE_PATH } from '../../../../common/constants/app'; -import type { HttpService } from '../http_service'; -import { useMlKibana } from '../../contexts/kibana'; - -import type { ValidateAnalyticsJobResponse } from '../../../../common/constants/validation'; import type { JobMessage } from '../../../../common/types/audit_message'; import type { PutDataFrameAnalyticsResponseSchema } from '../../../../server/routes/schemas/data_frame_analytics_schema'; +import { useMlKibana } from '../../contexts/kibana'; + +import type { HttpService } from '../http_service'; + export interface GetDataFrameAnalyticsStatsResponseOk { node_failures?: object; count: number; diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/forecasting_modal.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/forecasting_modal.js index fe6b132635301..5ac0dd68700d6 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/forecasting_modal.js +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/forecasting_modal.js @@ -19,11 +19,11 @@ import { EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { context } from '@kbn/kibana-react-plugin/public'; import { extractErrorMessage } from '@kbn/ml-error-utils'; +import { parseInterval } from '@kbn/ml-parse-interval'; import { FORECAST_REQUEST_STATE, JOB_STATE } from '../../../../../common/constants/states'; import { MESSAGE_LEVEL } from '../../../../../common/constants/message_levels'; import { isJobVersionGte } from '../../../../../common/util/job_utils'; -import { parseInterval } from '../../../../../common/util/parse_interval'; import { Modal } from './modal'; import { PROGRESS_STATES } from './progress_states'; import { forecastServiceFactory } from '../../../services/forecast_service'; diff --git a/x-pack/plugins/ml/public/application/util/custom_url_utils.ts b/x-pack/plugins/ml/public/application/util/custom_url_utils.ts index 8e9279688e91f..b71c78d466eb3 100644 --- a/x-pack/plugins/ml/public/application/util/custom_url_utils.ts +++ b/x-pack/plugins/ml/public/application/util/custom_url_utils.ts @@ -18,7 +18,8 @@ import type { MlCustomUrlAnomalyRecordDoc, } from '@kbn/ml-anomaly-utils'; import type { DataGridItem } from '@kbn/ml-data-grid'; -import { parseInterval } from '../../../common/util/parse_interval'; +import { parseInterval } from '@kbn/ml-parse-interval'; + import { escapeForElasticsearchQuery, replaceStringTokens } from './string_utils'; // Value of custom_url time_range property indicating drilldown time range is calculated automatically diff --git a/x-pack/plugins/ml/public/application/util/time_series_explorer_service.ts b/x-pack/plugins/ml/public/application/util/time_series_explorer_service.ts index c0c7995f1b310..a89cac8269b6f 100644 --- a/x-pack/plugins/ml/public/application/util/time_series_explorer_service.ts +++ b/x-pack/plugins/ml/public/application/util/time_series_explorer_service.ts @@ -6,18 +6,19 @@ */ import { useMemo } from 'react'; +import moment from 'moment'; +import type { Observable } from 'rxjs'; +import { forkJoin, of, catchError, map } from 'rxjs'; +import { each, get } from 'lodash'; + import type { IUiSettingsClient } from '@kbn/core/public'; import { aggregationTypeTransform } from '@kbn/ml-anomaly-utils'; import { isMultiBucketAnomaly, ML_JOB_AGGREGATION } from '@kbn/ml-anomaly-utils'; import { extractErrorMessage } from '@kbn/ml-error-utils'; -import moment from 'moment'; -import type { Observable } from 'rxjs'; -import { forkJoin, of } from 'rxjs'; -import { each, get } from 'lodash'; -import { catchError, map } from 'rxjs'; import { type MlAnomalyRecordDoc } from '@kbn/ml-anomaly-utils'; import type { TimeRangeBounds, TimeBucketsInterval } from '@kbn/ml-time-buckets'; -import { parseInterval } from '../../../common/util/parse_interval'; +import { parseInterval } from '@kbn/ml-parse-interval'; + import type { GetAnnotationsResponse } from '../../../common/types/annotations'; import { mlFunctionToESAggregation } from '../../../common/util/job_utils'; import { ANNOTATIONS_TABLE_DEFAULT_QUERY_SIZE } from '../../../common/constants/search'; diff --git a/x-pack/plugins/ml/public/embeddables/common/get_jobs_observable.ts b/x-pack/plugins/ml/public/embeddables/common/get_jobs_observable.ts index e1fc6a6d178c1..d050b40458519 100644 --- a/x-pack/plugins/ml/public/embeddables/common/get_jobs_observable.ts +++ b/x-pack/plugins/ml/public/embeddables/common/get_jobs_observable.ts @@ -8,8 +8,10 @@ import { isEqual } from 'lodash'; import type { Observable } from 'rxjs'; import { catchError, distinctUntilChanged, EMPTY, map, switchMap } from 'rxjs'; + +import { parseInterval } from '@kbn/ml-parse-interval'; + import type { JobId } from '../../../common/types/anomaly_detection_jobs'; -import { parseInterval } from '../../../common/util/parse_interval'; import type { ExplorerJob } from '../../application/explorer/explorer_utils'; import type { AnomalyDetectorService } from '../../application/services/anomaly_detector_service'; diff --git a/x-pack/plugins/ml/public/embeddables/job_creation/common/job_details.tsx b/x-pack/plugins/ml/public/embeddables/job_creation/common/job_details.tsx index 3bfa1a0caf799..0e843da22a74b 100644 --- a/x-pack/plugins/ml/public/embeddables/job_creation/common/job_details.tsx +++ b/x-pack/plugins/ml/public/embeddables/job_creation/common/job_details.tsx @@ -31,12 +31,13 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { extractErrorMessage } from '@kbn/ml-error-utils'; import type { TimeRange } from '@kbn/es-query'; +import { JOB_ID_MAX_LENGTH } from '@kbn/ml-validators'; + import type { QuickLensJobCreator } from '../../../application/jobs/new_job/job_from_lens'; import type { LayerResult } from '../../../application/jobs/new_job/job_from_lens'; import type { CreateState } from '../../../application/jobs/new_job/job_from_dashboard'; import { JOB_TYPE, DEFAULT_BUCKET_SPAN } from '../../../../common/constants/new_job'; import { basicJobValidation } from '../../../../common/util/job_utils'; -import { JOB_ID_MAX_LENGTH } from '../../../../common/constants/validation'; import { invalidTimeIntervalMessage } from '../../../application/jobs/new_job/common/job_validator/util'; import { ML_APP_LOCATOR, ML_PAGES } from '../../../../common/constants/locator'; import { useMlFromLensKibanaContext } from './context'; diff --git a/x-pack/plugins/ml/public/index.ts b/x-pack/plugins/ml/public/index.ts index bf293e2228570..d18b4f60a085f 100755 --- a/x-pack/plugins/ml/public/index.ts +++ b/x-pack/plugins/ml/public/index.ts @@ -41,15 +41,5 @@ export { CONTROLLED_BY_SWIM_LANE_FILTER } from './ui_actions/constants'; export type { MlLocator } from './locator'; export { useMlHref, ML_PAGES, MlLocatorDefinition } from './locator'; -// Bundled shared exports -// Exported this way so the code doesn't end up in ML's page load bundle -export const getMlSharedImports = async () => { - return await import('./shared'); -}; - -// Helper to get Type returned by getMlSharedImports. -type AwaitReturnType = T extends PromiseLike ? U : T; -export type GetMlSharedImportsReturnType = AwaitReturnType>; - export { MLJobsAwaitingNodeWarning } from './application/components/jobs_awaiting_node_warning/new_job_awaiting_node_shared'; export { MlNodeAvailableWarningShared } from './application/components/node_available_warning'; diff --git a/x-pack/plugins/ml/public/shared.ts b/x-pack/plugins/ml/public/shared.ts deleted file mode 100644 index fec15f3181a84..0000000000000 --- a/x-pack/plugins/ml/public/shared.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export { FieldStatsInfoButton } from './application/components/field_stats_flyout/field_stats_info_button'; -export { useFieldStatsTrigger } from './application/components/field_stats_flyout/use_field_stats_trigger'; -export { FieldStatsFlyoutProvider } from './application/components/field_stats_flyout/field_stats_flyout_provider'; -export { useFieldStatsFlyoutContext } from './application/components/field_stats_flyout/use_field_stats_flytout_context'; diff --git a/x-pack/plugins/ml/server/lib/alerts/jobs_health_service.ts b/x-pack/plugins/ml/server/lib/alerts/jobs_health_service.ts index 5e79e0f460b73..1e60dc1aeb3cf 100644 --- a/x-pack/plugins/ml/server/lib/alerts/jobs_health_service.ts +++ b/x-pack/plugins/ml/server/lib/alerts/jobs_health_service.ts @@ -6,11 +6,14 @@ */ import { groupBy, keyBy, memoize, partition } from 'lodash'; + import type { KibanaRequest, Logger, SavedObjectsClientContract } from '@kbn/core/server'; import { i18n } from '@kbn/i18n'; import type { MlJob } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { isDefined } from '@kbn/ml-is-defined'; import { ALERT_REASON } from '@kbn/rule-data-utils'; +import { parseInterval } from '@kbn/ml-parse-interval'; + import type { MlClient } from '../ml_client'; import type { JobSelection } from '../../routes/schemas/alerting_schema'; import { datafeedsProvider, type DatafeedsService } from '../../models/job_service/datafeeds'; @@ -39,7 +42,6 @@ import { } from '../../../common/util/alerts'; import type { AnnotationService } from '../../models/annotation_service/annotation'; import { annotationServiceProvider } from '../../models/annotation_service'; -import { parseInterval } from '../../../common/util/parse_interval'; import { jobAuditMessagesProvider, type JobAuditMessagesService, diff --git a/x-pack/plugins/ml/server/models/data_frame_analytics/validation.ts b/x-pack/plugins/ml/server/models/data_frame_analytics/validation.ts index d82b52920dbc6..515de87e79c56 100644 --- a/x-pack/plugins/ml/server/models/data_frame_analytics/validation.ts +++ b/x-pack/plugins/ml/server/models/data_frame_analytics/validation.ts @@ -27,7 +27,7 @@ import { TRAINING_DOCS_LOWER, TRAINING_DOCS_UPPER, VALIDATION_STATUS, -} from '../../../common/constants/validation'; +} from '@kbn/ml-validators'; interface MissingAgg { [key: string]: { diff --git a/x-pack/plugins/ml/server/models/fields_service/fields_service.ts b/x-pack/plugins/ml/server/models/fields_service/fields_service.ts index a7e1812888c2f..95e97ef01e3b3 100644 --- a/x-pack/plugins/ml/server/models/fields_service/fields_service.ts +++ b/x-pack/plugins/ml/server/models/fields_service/fields_service.ts @@ -5,14 +5,16 @@ * 2.0. */ -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import Boom from '@hapi/boom'; -import type { IScopedClusterClient } from '@kbn/core/server'; import { duration } from 'moment'; + +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type { IScopedClusterClient } from '@kbn/core/server'; import type { AggCardinality } from '@kbn/ml-agg-utils'; import { isPopulatedObject } from '@kbn/ml-is-populated-object'; import type { RuntimeMappings } from '@kbn/ml-runtime-field-utils'; -import { parseInterval } from '../../../common/util/parse_interval'; +import { parseInterval } from '@kbn/ml-parse-interval'; + import { initCardinalityFieldsCache } from './fields_aggs_cache'; import { isValidAggregationField } from '../../../common/util/validation_utils'; import { getDatafeedAggregations } from '../../../common/util/datafeed_utils'; diff --git a/x-pack/plugins/ml/server/models/job_service/jobs.ts b/x-pack/plugins/ml/server/models/job_service/jobs.ts index 172d5d1b7e582..9c4ea6b1d3307 100644 --- a/x-pack/plugins/ml/server/models/job_service/jobs.ts +++ b/x-pack/plugins/ml/server/models/job_service/jobs.ts @@ -7,9 +7,12 @@ import { uniq } from 'lodash'; import Boom from '@hapi/boom'; + import type { IScopedClusterClient } from '@kbn/core/server'; import type { RulesClient } from '@kbn/alerting-plugin/server'; import { isPopulatedObject } from '@kbn/ml-is-populated-object'; +import { parseInterval } from '@kbn/ml-parse-interval'; + import { getSingleMetricViewerJobErrorMessage, parseTimeIntervalForJob, @@ -52,7 +55,6 @@ import type { MlClient } from '../../lib/ml_client'; import { ML_ALERT_TYPES } from '../../../common/constants/alerts'; import type { MlAnomalyDetectionAlertParams } from '../../routes/schemas/alerting_schema'; import type { AuthorizationHeader } from '../../lib/request_authorization'; -import { parseInterval } from '../../../common/util/parse_interval'; interface Results { [id: string]: { diff --git a/x-pack/plugins/ml/server/models/job_validation/job_validation.ts b/x-pack/plugins/ml/server/models/job_validation/job_validation.ts index da751c9d33be4..fff24e7730f18 100644 --- a/x-pack/plugins/ml/server/models/job_validation/job_validation.ts +++ b/x-pack/plugins/ml/server/models/job_validation/job_validation.ts @@ -6,12 +6,14 @@ */ import Boom from '@hapi/boom'; + import type { IScopedClusterClient } from '@kbn/core/server'; import type { TypeOf } from '@kbn/config-schema'; +import { VALIDATION_STATUS } from '@kbn/ml-validators'; + import { fieldsServiceProvider } from '../fields_service'; import type { MessageId, JobValidationMessage } from '../../../common/constants/messages'; import { getMessages } from '../../../common/constants/messages'; -import { VALIDATION_STATUS } from '../../../common/constants/validation'; import { basicJobValidation, uniqWithIsEqual } from '../../../common/util/job_utils'; import { validateBucketSpan } from './validate_bucket_span'; diff --git a/x-pack/plugins/ml/server/models/job_validation/validate_bucket_span.js b/x-pack/plugins/ml/server/models/job_validation/validate_bucket_span.js index 75b1b98213036..bd3ab7c7e8769 100644 --- a/x-pack/plugins/ml/server/models/job_validation/validate_bucket_span.js +++ b/x-pack/plugins/ml/server/models/job_validation/validate_bucket_span.js @@ -5,9 +5,10 @@ * 2.0. */ +import { SKIP_BUCKET_SPAN_ESTIMATION } from '@kbn/ml-validators'; + import { estimateBucketSpanFactory } from '../bucket_span_estimator'; import { mlFunctionToESAggregation, parseTimeIntervalForJob } from '../../../common/util/job_utils'; -import { SKIP_BUCKET_SPAN_ESTIMATION } from '../../../common/constants/validation'; import { validateJobObject } from './validate_job_object'; diff --git a/x-pack/plugins/ml/server/models/job_validation/validate_bucket_span.test.ts b/x-pack/plugins/ml/server/models/job_validation/validate_bucket_span.test.ts index b660e6f8040c0..c8827be8bad2c 100644 --- a/x-pack/plugins/ml/server/models/job_validation/validate_bucket_span.test.ts +++ b/x-pack/plugins/ml/server/models/job_validation/validate_bucket_span.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { SKIP_BUCKET_SPAN_ESTIMATION } from '../../../common/constants/validation'; +import { SKIP_BUCKET_SPAN_ESTIMATION } from '@kbn/ml-validators'; import type { JobValidationMessage } from '../../../common/constants/messages'; // @ts-ignore diff --git a/x-pack/plugins/ml/server/models/job_validation/validate_model_memory_limit.ts b/x-pack/plugins/ml/server/models/job_validation/validate_model_memory_limit.ts index c22c514db73d6..85ce1cd8d3427 100644 --- a/x-pack/plugins/ml/server/models/job_validation/validate_model_memory_limit.ts +++ b/x-pack/plugins/ml/server/models/job_validation/validate_model_memory_limit.ts @@ -6,11 +6,13 @@ */ import numeral from '@elastic/numeral'; + import type { IScopedClusterClient } from '@kbn/core/server'; +import { ALLOWED_DATA_UNITS } from '@kbn/ml-validators'; + import type { CombinedJob } from '../../../common/types/anomaly_detection_jobs'; import { validateJobObject } from './validate_job_object'; import { calculateModelMemoryLimitProvider } from '../calculate_model_memory_limit'; -import { ALLOWED_DATA_UNITS } from '../../../common/constants/validation'; import type { MlClient } from '../../lib/ml_client'; // The minimum value the backend expects is 1MByte diff --git a/x-pack/plugins/ml/server/models/job_validation/validate_time_range.ts b/x-pack/plugins/ml/server/models/job_validation/validate_time_range.ts index 0c6af17a4a069..de9f624485bb7 100644 --- a/x-pack/plugins/ml/server/models/job_validation/validate_time_range.ts +++ b/x-pack/plugins/ml/server/models/job_validation/validate_time_range.ts @@ -7,7 +7,8 @@ import type { IScopedClusterClient } from '@kbn/core/server'; import { ES_FIELD_TYPES } from '@kbn/field-types'; -import { parseInterval } from '../../../common/util/parse_interval'; +import { parseInterval } from '@kbn/ml-parse-interval'; + import type { CombinedJob } from '../../../common/types/anomaly_detection_jobs'; import { validateJobObject } from './validate_job_object'; diff --git a/x-pack/plugins/ml/server/models/results_service/anomaly_charts.ts b/x-pack/plugins/ml/server/models/results_service/anomaly_charts.ts index 7e551b0624261..1690e2db74164 100644 --- a/x-pack/plugins/ml/server/models/results_service/anomaly_charts.ts +++ b/x-pack/plugins/ml/server/models/results_service/anomaly_charts.ts @@ -26,6 +26,8 @@ import { ML_JOB_AGGREGATION, } from '@kbn/ml-anomaly-utils'; import { isRuntimeMappings } from '@kbn/ml-runtime-field-utils'; +import { parseInterval } from '@kbn/ml-parse-interval'; + import type { MlClient } from '../../lib/ml_client'; import type { MetricData, @@ -48,7 +50,6 @@ import { } from '../../../common/util/job_utils'; import type { CriteriaField } from './results_service'; import type { CombinedJob, Datafeed } from '../../shared'; -import { parseInterval } from '../../../common/util/parse_interval'; import { getDatafeedAggregations } from '../../../common/util/datafeed_utils'; import { findAggField } from '../../../common/util/validation_utils'; diff --git a/x-pack/plugins/ml/tsconfig.json b/x-pack/plugins/ml/tsconfig.json index 2f980378de923..b625562ab2674 100644 --- a/x-pack/plugins/ml/tsconfig.json +++ b/x-pack/plugins/ml/tsconfig.json @@ -37,7 +37,6 @@ "@kbn/data-plugin", "@kbn/data-views-plugin", "@kbn/data-visualizer-plugin", - "@kbn/datemath", "@kbn/embeddable-plugin", "@kbn/es-query", "@kbn/es-types", @@ -70,7 +69,6 @@ "@kbn/ml-trained-models-utils", "@kbn/ml-url-state", "@kbn/monaco", - "@kbn/react-field", "@kbn/rison", "@kbn/saved-objects-finder-plugin", "@kbn/saved-objects-management-plugin", @@ -130,6 +128,9 @@ "@kbn/esql-utils", "@kbn/core-lifecycle-browser", "@kbn/observability-ai-assistant-plugin", - "@kbn/json-schemas" + "@kbn/json-schemas", + "@kbn/ml-field-stats-flyout", + "@kbn/ml-parse-interval", + "@kbn/ml-validators" ] } diff --git a/x-pack/plugins/observability_solution/inventory/public/components/app_root/header_action_menu/add_data_action_menu.tsx b/x-pack/plugins/observability_solution/inventory/public/components/app_root/header_action_menu/add_data_action_menu.tsx index ca4bc06df648a..cec6188a1553f 100644 --- a/x-pack/plugins/observability_solution/inventory/public/components/app_root/header_action_menu/add_data_action_menu.tsx +++ b/x-pack/plugins/observability_solution/inventory/public/components/app_root/header_action_menu/add_data_action_menu.tsx @@ -15,26 +15,18 @@ import { EuiIcon, EuiPopover, } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; import { OBSERVABILITY_ONBOARDING_LOCATOR, ObservabilityOnboardingLocatorParams, } from '@kbn/deeplinks-observability'; import { useKibana } from '../../../hooks/use_kibana'; import type { InventoryAddDataParams } from '../../../services/telemetry/types'; - -const addDataTitle = i18n.translate('xpack.inventory.addDataContextMenu.link', { - defaultMessage: 'Add data', -}); -const addDataItem = i18n.translate('xpack.inventory.add.apm.agent.button.', { - defaultMessage: 'Add data', -}); - -const associateServiceLogsItem = i18n.translate('xpack.inventory.associate.service.logs.button', { - defaultMessage: 'Associate existing service logs', -}); - -const ASSOCIATE_LOGS_LINK = 'https://ela.st/new-experience-associate-service-logs'; +import { + ASSOCIATE_LOGS_LINK, + addDataItem, + addDataTitle, + associateServiceLogsItem, +} from '../../shared/add_data_buttons/buttons'; export function AddDataContextMenu() { const [popoverOpen, setPopoverOpen] = useState(false); diff --git a/x-pack/plugins/observability_solution/inventory/public/components/empty_states/empty_state.tsx b/x-pack/plugins/observability_solution/inventory/public/components/empty_states/empty_state.tsx new file mode 100644 index 0000000000000..587812aa6c86e --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/public/components/empty_states/empty_state.tsx @@ -0,0 +1,137 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + COLOR_MODES_STANDARD, + EuiCallOut, + EuiEmptyPrompt, + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiImage, + EuiLink, + EuiText, + EuiTextColor, + useEuiTheme, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { dashboardsLight, dashboardsDark } from '@kbn/shared-svg'; +import useLocalStorage from 'react-use/lib/useLocalStorage'; +import { AddData, AssociateServiceLogs } from '../shared/add_data_buttons/buttons'; +import { useKibana } from '../../hooks/use_kibana'; +import { InventoryAddDataParams } from '../../services/telemetry/types'; + +export function EmptyState() { + const { services } = useKibana(); + + const [isDismissed, setDismissed] = useLocalStorage( + 'inventory.emptyStateDismissed', + false + ); + + function reportButtonClick(journey: InventoryAddDataParams['journey']) { + services.telemetry.reportInventoryAddData({ + view: 'empty_state', + journey, + }); + } + + const { colorMode } = useEuiTheme(); + + return ( + + {!isDismissed && ( + + setDismissed(true)} + title={i18n.translate('xpack.inventory.noEntitiesEmptyState.callout.title', { + defaultMessage: 'Trying for the first time?', + })} + > +

+ {i18n.translate('xpack.inventory.noEntitiesEmptyState.description', { + defaultMessage: + 'It can take a couple of minutes for your entities to show. Try refreshing in a minute or two.', + })} +

+ + {i18n.translate('xpack.inventory.noEntitiesEmptyState.learnMore.link', { + defaultMessage: 'Learn more', + })} + +
+
+ )} + + + } + title={ +

+ {i18n.translate('xpack.inventory.noEntitiesEmptyState.title', { + defaultMessage: 'No entities available', + })} +

+ } + layout={'horizontal'} + color="plain" + body={ + <> +

+ {i18n.translate('xpack.inventory.noEntitiesEmptyState.body.description', { + defaultMessage: + 'See all of your observed entities in one place by collecting some data.', + })} +

+ + +
+ + {i18n.translate('xpack.inventory.noEntitiesEmptyState.actions.title', { + defaultMessage: 'Start observing your entities:', + })} + +
+
+ + } + actions={ + + + { + reportButtonClick('add_data'); + }} + /> + { + reportButtonClick('associate_existing_service_logs'); + }} + /> + + + } + /> +
+
+ ); +} diff --git a/x-pack/plugins/observability_solution/inventory/public/components/inventory_page_template/index.tsx b/x-pack/plugins/observability_solution/inventory/public/components/inventory_page_template/index.tsx index fc0f3c03660a9..c2734d6643553 100644 --- a/x-pack/plugins/observability_solution/inventory/public/components/inventory_page_template/index.tsx +++ b/x-pack/plugins/observability_solution/inventory/public/components/inventory_page_template/index.tsx @@ -7,15 +7,24 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; +import { EuiEmptyPrompt, EuiLoadingLogo } from '@elastic/eui'; import { useKibana } from '../../hooks/use_kibana'; import { SearchBar } from '../search_bar'; import { getEntityManagerEnablement } from './no_data_config'; import { useEntityManager } from '../../hooks/use_entity_manager'; import { Welcome } from '../entity_enablement/welcome_modal'; +import { useInventoryAbortableAsync } from '../../hooks/use_inventory_abortable_async'; +import { EmptyState } from '../empty_states/empty_state'; + +const pageTitle = { + pageTitle: i18n.translate('xpack.inventory.inventoryPageHeaderLabel', { + defaultMessage: 'Inventory', + }), +}; export function InventoryPageTemplate({ children }: { children: React.ReactNode }) { const { - services: { observabilityShared }, + services: { observabilityShared, inventoryAPIClient }, } = useKibana(); const { PageTemplate: ObservabilityPageTemplate } = observabilityShared.navigation; @@ -32,6 +41,23 @@ export function InventoryPageTemplate({ children }: { children: React.ReactNode toggleWelcomedModal(); }; + const { value = { hasData: false }, loading: hasDataLoading } = useInventoryAbortableAsync( + ({ signal }) => { + return inventoryAPIClient.fetch('GET /internal/inventory/has_data', { + signal, + }); + }, + [inventoryAPIClient] + ); + + if (isEnablementLoading || hasDataLoading) { + return ( + + } /> + + ); + } + return ( - - - - - - {children} - {showWelcomedModal ? ( - - ) : null} - - + {value.hasData ? ( + + + + + + {children} + {showWelcomedModal ? ( + + ) : null} + + + ) : ( + + )} ); } diff --git a/x-pack/plugins/observability_solution/inventory/public/components/shared/add_data_buttons/buttons.tsx b/x-pack/plugins/observability_solution/inventory/public/components/shared/add_data_buttons/buttons.tsx new file mode 100644 index 0000000000000..90f8cdbba0946 --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/public/components/shared/add_data_buttons/buttons.tsx @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +// Disabling it for now until the EUI team fixes it + +/* eslint-disable @elastic/eui/href-or-on-click */ +import React from 'react'; +import { + OBSERVABILITY_ONBOARDING_LOCATOR, + ObservabilityOnboardingLocatorParams, +} from '@kbn/deeplinks-observability'; +import { EuiButton } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { useKibana } from '../../../hooks/use_kibana'; + +export const addDataTitle = i18n.translate('xpack.inventory.addDataContextMenu.link', { + defaultMessage: 'Add data', +}); +export const addDataItem = i18n.translate('xpack.inventory.add.apm.agent.button.', { + defaultMessage: 'Add data', +}); + +export const associateServiceLogsItem = i18n.translate( + 'xpack.inventory.associate.service.logs.button', + { + defaultMessage: 'Associate existing service logs', + } +); + +export const ASSOCIATE_LOGS_LINK = 'https://ela.st/new-experience-associate-service-logs'; + +export function AssociateServiceLogs({ onClick }: { onClick?: () => void }) { + return ( + + {associateServiceLogsItem} + + ); +} + +export function AddData({ onClick }: { onClick?: () => void }) { + const { + services: { share }, + } = useKibana(); + const onboardingLocator = share.url.locators.get( + OBSERVABILITY_ONBOARDING_LOCATOR + ); + return ( + + {addDataItem} + + ); +} diff --git a/x-pack/plugins/observability_solution/inventory/public/services/telemetry/types.ts b/x-pack/plugins/observability_solution/inventory/public/services/telemetry/types.ts index 494391aa1a7c1..e5fdf162b750c 100644 --- a/x-pack/plugins/observability_solution/inventory/public/services/telemetry/types.ts +++ b/x-pack/plugins/observability_solution/inventory/public/services/telemetry/types.ts @@ -12,7 +12,7 @@ export interface TelemetryServiceSetupParams { } export interface InventoryAddDataParams { - view: 'add_data_button'; + view: 'add_data_button' | 'empty_state'; journey?: 'add_data' | 'associate_existing_service_logs'; } diff --git a/x-pack/plugins/observability_solution/inventory/server/routes/get_global_inventory_route_repository.ts b/x-pack/plugins/observability_solution/inventory/server/routes/get_global_inventory_route_repository.ts index 190178cb25a95..598b69db90e5a 100644 --- a/x-pack/plugins/observability_solution/inventory/server/routes/get_global_inventory_route_repository.ts +++ b/x-pack/plugins/observability_solution/inventory/server/routes/get_global_inventory_route_repository.ts @@ -6,10 +6,12 @@ */ import { entitiesRoutes } from './entities/route'; +import { hasDataRoutes } from './has_data/route'; export function getGlobalInventoryServerRouteRepository() { return { ...entitiesRoutes, + ...hasDataRoutes, }; } diff --git a/x-pack/plugins/observability_solution/inventory/server/routes/has_data/get_has_data.ts b/x-pack/plugins/observability_solution/inventory/server/routes/has_data/get_has_data.ts new file mode 100644 index 0000000000000..465e720938b32 --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/server/routes/has_data/get_has_data.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import type { Logger } from '@kbn/core/server'; +import { esqlResultToPlainObjects } from '@kbn/observability-utils/es/utils/esql_result_to_plain_objects'; +import { type ObservabilityElasticsearchClient } from '@kbn/observability-utils/es/client/create_observability_es_client'; +import { + getEntityDefinitionIdWhereClause, + getEntityTypesWhereClause, +} from '../entities/query_helper'; +import { ENTITIES_LATEST_ALIAS } from '../../../common/entities'; + +export async function getHasData({ + inventoryEsClient, + logger, +}: { + inventoryEsClient: ObservabilityElasticsearchClient; + logger: Logger; +}) { + try { + const esqlResults = await inventoryEsClient.esql('get_has_data', { + query: `FROM ${ENTITIES_LATEST_ALIAS} + | ${getEntityDefinitionIdWhereClause()} + | ${getEntityTypesWhereClause()} + | STATS _count = COUNT(*) + | LIMIT 1`, + }); + + const totalCount = esqlResultToPlainObjects(esqlResults)?.[0]._count ?? 0; + + return { hasData: totalCount > 0 }; + } catch (e) { + logger.error(e); + return { hasData: false }; + } +} diff --git a/x-pack/plugins/observability_solution/inventory/server/routes/has_data/route.ts b/x-pack/plugins/observability_solution/inventory/server/routes/has_data/route.ts new file mode 100644 index 0000000000000..aae8be7f846f8 --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/server/routes/has_data/route.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { createObservabilityEsClient } from '@kbn/observability-utils/es/client/create_observability_es_client'; +import { INVENTORY_APP_ID } from '@kbn/deeplinks-observability/constants'; +import { createInventoryServerRoute } from '../create_inventory_server_route'; +import { getHasData } from './get_has_data'; + +export const hasDataRoute = createInventoryServerRoute({ + endpoint: 'GET /internal/inventory/has_data', + options: { + tags: ['access:inventory'], + }, + handler: async ({ context, logger }) => { + const coreContext = await context.core; + const inventoryEsClient = createObservabilityEsClient({ + client: coreContext.elasticsearch.client.asCurrentUser, + logger, + plugin: `@kbn/${INVENTORY_APP_ID}-plugin`, + }); + + return getHasData({ + inventoryEsClient, + logger, + }); + }, +}); + +export const hasDataRoutes = { + ...hasDataRoute, +}; diff --git a/x-pack/plugins/observability_solution/inventory/tsconfig.json b/x-pack/plugins/observability_solution/inventory/tsconfig.json index 009221ad0c0ca..a391737762c52 100644 --- a/x-pack/plugins/observability_solution/inventory/tsconfig.json +++ b/x-pack/plugins/observability_solution/inventory/tsconfig.json @@ -39,6 +39,7 @@ "@kbn/unified-search-plugin", "@kbn/data-plugin", "@kbn/core-analytics-browser", - "@kbn/core-http-browser" + "@kbn/core-http-browser", + "@kbn/shared-svg" ] } diff --git a/x-pack/plugins/observability_solution/observability/public/navigation_tree.ts b/x-pack/plugins/observability_solution/observability/public/navigation_tree.ts index 6d63c9c89eaf1..309d2f2264300 100644 --- a/x-pack/plugins/observability_solution/observability/public/navigation_tree.ts +++ b/x-pack/plugins/observability_solution/observability/public/navigation_tree.ts @@ -272,6 +272,7 @@ export function createNavTree(pluginsStart: ObservabilityPublicPluginsStart) { breadcrumbStatus: 'hidden', children: [ { + id: 'stack_management', // This id can't be changed as we use it to open the panel programmatically link: 'management', title: i18n.translate('xpack.observability.obltNav.stackManagement', { defaultMessage: 'Stack Management', diff --git a/x-pack/plugins/observability_solution/observability/public/utils/investigation_item_helper.ts b/x-pack/plugins/observability_solution/observability/public/utils/investigation_item_helper.ts index 5ffaa234b3d97..cddf3290ed370 100644 --- a/x-pack/plugins/observability_solution/observability/public/utils/investigation_item_helper.ts +++ b/x-pack/plugins/observability_solution/observability/public/utils/investigation_item_helper.ts @@ -22,12 +22,12 @@ const AggMappingForLens: Record = { const genLensEqForCustomThresholdRule = (criterion: MetricExpression) => { const metricNameResolver: Record = {}; - criterion.metrics.forEach( - (metric: CustomThresholdExpressionMetric) => - (metricNameResolver[metric.name] = `${ - AggMappingForLens[metric.aggType] ? AggMappingForLens[metric.aggType] : metric.aggType - }(${metric.field ? metric.field : metric.filter ? metric.filter : ''})`) - ); + criterion.metrics.forEach((metric: CustomThresholdExpressionMetric) => { + const metricFilter = metric.filter ? `kql='${metric.filter}'` : ''; + metricNameResolver[metric.name] = `${ + AggMappingForLens[metric.aggType] ? AggMappingForLens[metric.aggType] : metric.aggType + }(${metric.field ? metric.field : metricFilter})`; + }); let equation = criterion.equation ? criterion.equation diff --git a/x-pack/plugins/observability_solution/synthetics/common/runtime_types/monitor_management/monitor_types.ts b/x-pack/plugins/observability_solution/synthetics/common/runtime_types/monitor_management/monitor_types.ts index 7246ba2d9bac6..578d823aafae2 100644 --- a/x-pack/plugins/observability_solution/synthetics/common/runtime_types/monitor_management/monitor_types.ts +++ b/x-pack/plugins/observability_solution/synthetics/common/runtime_types/monitor_management/monitor_types.ts @@ -173,7 +173,8 @@ export type ICMPFields = t.TypeOf; export const HTTPSimpleFieldsCodec = t.intersection([ t.interface({ [ConfigKey.METADATA]: MetadataCodec, - [ConfigKey.MAX_REDIRECTS]: t.string, + // string is for yaml config and number for public api + [ConfigKey.MAX_REDIRECTS]: t.union([t.string, t.number]), [ConfigKey.URLS]: getNonEmptyStringCodec('url'), [ConfigKey.PORT]: t.union([t.number, t.null]), }), @@ -318,6 +319,11 @@ export const MonitorFieldsCodec = t.intersection([ BrowserFieldsCodec, ]); +export const MonitorFieldsResultCodec = t.intersection([ + MonitorFieldsCodec, + t.interface({ id: t.string, updated_at: t.string, created_at: t.string }), +]); + // Monitor, represents one of (Icmp | Tcp | Http | Browser) decrypted export const SyntheticsMonitorCodec = t.union([ HTTPFieldsCodec, @@ -336,7 +342,7 @@ export const EncryptedSyntheticsMonitorCodec = t.union([ export const SyntheticsMonitorWithIdCodec = t.intersection([ SyntheticsMonitorCodec, - t.interface({ id: t.string }), + t.interface({ id: t.string, updated_at: t.string, created_at: t.string }), ]); const HeartbeatFieldsCodec = t.intersection([ @@ -355,7 +361,10 @@ const HeartbeatFieldsCodec = t.intersection([ ]); export const HeartbeatConfigCodec = t.intersection([ - SyntheticsMonitorWithIdCodec, + SyntheticsMonitorCodec, + t.interface({ + id: t.string, + }), t.partial({ fields_under_root: t.boolean, fields: HeartbeatFieldsCodec, @@ -400,6 +409,7 @@ export type BrowserFields = t.TypeOf; export type BrowserSimpleFields = t.TypeOf; export type BrowserAdvancedFields = t.TypeOf; export type MonitorFields = t.TypeOf; +export type MonitorFieldsResult = t.TypeOf; export type HeartbeatFields = t.TypeOf; export type SyntheticsMonitor = t.TypeOf; export type SyntheticsMonitorWithId = t.TypeOf; diff --git a/x-pack/plugins/observability_solution/synthetics/common/runtime_types/monitor_management/monitor_types_project.ts b/x-pack/plugins/observability_solution/synthetics/common/runtime_types/monitor_management/monitor_types_project.ts index 8e25ec1a714e4..ed3e6b28eef26 100644 --- a/x-pack/plugins/observability_solution/synthetics/common/runtime_types/monitor_management/monitor_types_project.ts +++ b/x-pack/plugins/observability_solution/synthetics/common/runtime_types/monitor_management/monitor_types_project.ts @@ -62,7 +62,7 @@ export const ProjectMonitorCodec = t.intersection([ hash: t.string, namespace: t.string, retestOnFailure: t.boolean, - labels: t.record(t.string, t.string), + fields: t.record(t.string, t.string), }), ]); @@ -91,14 +91,10 @@ export const ProjectMonitorsResponseCodec = t.intersection([ }), ]); -export type ProjectMonitorThrottlingConfig = t.TypeOf; - export type ProjectMonitor = t.TypeOf; export type LegacyProjectMonitorsRequest = t.TypeOf; export type ProjectMonitorsRequest = t.TypeOf; -export type ProjectMonitorsResponse = t.TypeOf; - export type ProjectMonitorMetaData = t.TypeOf; diff --git a/x-pack/plugins/observability_solution/synthetics/e2e/config.ts b/x-pack/plugins/observability_solution/synthetics/e2e/config.ts index 4b2aef573d687..8e1e97e5c1d37 100644 --- a/x-pack/plugins/observability_solution/synthetics/e2e/config.ts +++ b/x-pack/plugins/observability_solution/synthetics/e2e/config.ts @@ -10,6 +10,7 @@ import { CA_CERT_PATH } from '@kbn/dev-utils'; import { get } from 'lodash'; import { commonFunctionalServices } from '@kbn/ftr-common-functional-services'; import { commonFunctionalUIServices } from '@kbn/ftr-common-functional-ui-services'; + import { readKibanaConfig } from './tasks/read_kibana_config'; const MANIFEST_KEY = 'xpack.uptime.service.manifestUrl'; const SERVICE_PASSWORD = 'xpack.uptime.service.password'; diff --git a/x-pack/plugins/observability_solution/synthetics/e2e/synthetics/journeys/project_monitor_read_only.journey.ts b/x-pack/plugins/observability_solution/synthetics/e2e/synthetics/journeys/project_monitor_read_only.journey.ts index 3e2a4467bd61a..edef44a21d051 100644 --- a/x-pack/plugins/observability_solution/synthetics/e2e/synthetics/journeys/project_monitor_read_only.journey.ts +++ b/x-pack/plugins/observability_solution/synthetics/e2e/synthetics/journeys/project_monitor_read_only.journey.ts @@ -6,9 +6,10 @@ */ import { after, before, expect, journey, step } from '@elastic/synthetics'; +import { omit } from 'lodash'; import { SyntheticsMonitor } from '@kbn/synthetics-plugin/common/runtime_types'; import { SyntheticsServices } from './services/synthetics_services'; -import { cleanTestMonitors, enableMonitorManagedViaApi } from './services/add_monitor'; +import { cleanTestMonitors } from './services/add_monitor'; import { addTestMonitorProject } from './services/add_monitor_project'; import { syntheticsAppPageProvider } from '../page_objects/synthetics_app'; @@ -23,14 +24,13 @@ journey('ProjectMonitorReadOnly', async ({ page, params }) => { before(async () => { await cleanTestMonitors(params); - await enableMonitorManagedViaApi(params.kibanaUrl); + }); + step('Go to monitor-management', async () => { await addTestMonitorProject(params.kibanaUrl, monitorName); await syntheticsApp.waitForLoadingToFinish(); - }); - step('Go to monitor-management', async () => { await syntheticsApp.navigateToMonitorManagement(); }); @@ -62,11 +62,16 @@ journey('ProjectMonitorReadOnly', async ({ page, params }) => { // hash is always reset to empty string when monitor is edited // this ensures that when the monitor is pushed again, the monitor // config in the process takes precedence - expect(newConfiguration).toEqual({ - ...originalMonitorConfiguration, - hash: '', - revision: 2, - }); + expect(omit(newConfiguration, ['updated_at'])).toEqual( + omit( + { + ...originalMonitorConfiguration, + hash: '', + revision: 2, + }, + ['updated_at'] + ) + ); }); step('Navigate to edit monitor', async () => { @@ -83,29 +88,39 @@ journey('ProjectMonitorReadOnly', async ({ page, params }) => { // hash is always reset to empty string when monitor is edited // this ensures that when the monitor is pushed again, the monitor // config in the process takes precedence - expect(newConfiguration).toEqual({ - ...originalMonitorConfiguration, - hash: '', - revision: 3, - alert: { - status: { - enabled: !(originalMonitorConfiguration?.alert?.status?.enabled as boolean), - }, - tls: { - enabled: originalMonitorConfiguration?.alert?.tls?.enabled as boolean, + expect(omit(newConfiguration, ['updated_at'])).toEqual( + omit( + { + ...originalMonitorConfiguration, + hash: '', + revision: 3, + alert: { + status: { + enabled: !(originalMonitorConfiguration?.alert?.status?.enabled as boolean), + }, + tls: { + enabled: originalMonitorConfiguration?.alert?.tls?.enabled as boolean, + }, + }, + enabled: !originalMonitorConfiguration?.enabled, }, - }, - enabled: !originalMonitorConfiguration?.enabled, - }); + ['updated_at'] + ) + ); }); step('Monitor can be re-pushed and overwrite any changes', async () => { await addTestMonitorProject(params.kibanaUrl, monitorName); const repushedConfiguration = await services.getMonitor(monitorId); - expect(repushedConfiguration).toEqual({ - ...originalMonitorConfiguration, - revision: 4, - }); + expect(omit(repushedConfiguration, ['updated_at'])).toEqual( + omit( + { + ...originalMonitorConfiguration, + revision: 4, + }, + ['updated_at'] + ) + ); }); step('Navigate to edit monitor', async () => { diff --git a/x-pack/plugins/observability_solution/synthetics/e2e/synthetics/journeys/services/add_monitor_project.ts b/x-pack/plugins/observability_solution/synthetics/e2e/synthetics/journeys/services/add_monitor_project.ts index 8cc9529e94c9f..85c0f512beb1b 100644 --- a/x-pack/plugins/observability_solution/synthetics/e2e/synthetics/journeys/services/add_monitor_project.ts +++ b/x-pack/plugins/observability_solution/synthetics/e2e/synthetics/journeys/services/add_monitor_project.ts @@ -17,23 +17,15 @@ export const addTestMonitorProject = async ( const testData = { ...testProjectMonitorBrowser(name, config), }; - try { - return await axios.put( - kibanaUrl + - SYNTHETICS_API_URLS.SYNTHETICS_MONITORS_PROJECT_UPDATE.replace( - '{projectName}', - projectName - ), - testData, - { - auth: { username: 'elastic', password: 'changeme' }, - headers: { 'kbn-xsrf': 'true', 'x-elastic-internal-origin': 'synthetics-e2e' }, - } - ); - } catch (e) { - // eslint-disable-next-line no-console - console.log(e); - } + return await axios.put( + kibanaUrl + + SYNTHETICS_API_URLS.SYNTHETICS_MONITORS_PROJECT_UPDATE.replace('{projectName}', projectName), + testData, + { + auth: { username: 'elastic', password: 'changeme' }, + headers: { 'kbn-xsrf': 'true', 'x-elastic-internal-origin': 'synthetics-e2e' }, + } + ); }; const testProjectMonitorBrowser = (name: string, config?: Record) => ({ diff --git a/x-pack/plugins/observability_solution/synthetics/e2e/synthetics/journeys/services/synthetics_services.ts b/x-pack/plugins/observability_solution/synthetics/e2e/synthetics/journeys/services/synthetics_services.ts index 988327ee3fc19..028f8a736e93c 100644 --- a/x-pack/plugins/observability_solution/synthetics/e2e/synthetics/journeys/services/synthetics_services.ts +++ b/x-pack/plugins/observability_solution/synthetics/e2e/synthetics/journeys/services/synthetics_services.ts @@ -28,10 +28,9 @@ export class SyntheticsServices { try { const { data } = await this.requester.request({ description: 'get monitor by id', - path: SYNTHETICS_API_URLS.GET_SYNTHETICS_MONITOR.replace('{monitorId}', monitorId), - query: { - decrypted: true, - }, + path: + SYNTHETICS_API_URLS.GET_SYNTHETICS_MONITOR.replace('{monitorId}', monitorId) + + '?internal=true', method: 'GET', }); return data as SyntheticsMonitor; diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/common/components/monitor_details_panel.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/common/components/monitor_details_panel.tsx index d0db68035b147..1222455443bbf 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/common/components/monitor_details_panel.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/common/components/monitor_details_panel.tsx @@ -28,6 +28,7 @@ import { EncryptedSyntheticsSavedMonitor, MonitorFields, Ping, + SyntheticsMonitorWithId, } from '../../../../../../common/runtime_types'; import { MonitorTypeBadge } from './monitor_type_badge'; import { useDateFormat } from '../../../../../hooks/use_date_format'; @@ -36,7 +37,7 @@ export interface MonitorDetailsPanelProps { latestPing?: Ping; loading: boolean; configId: string; - monitor: EncryptedSyntheticsSavedMonitor | null; + monitor: SyntheticsMonitorWithId | EncryptedSyntheticsSavedMonitor | null; hideEnabled?: boolean; hideLocations?: boolean; hasBorder?: boolean; diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/common/header/action_menu_content.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/common/header/action_menu_content.tsx index bc878a4388f02..84e536bfec0ee 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/common/header/action_menu_content.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/common/header/action_menu_content.tsx @@ -15,7 +15,12 @@ import { LastRefreshed } from '../components/last_refreshed'; import { AutoRefreshButton } from '../components/auto_refresh_button'; import { useSyntheticsSettingsContext } from '../../../contexts'; import { useGetUrlParams } from '../../../hooks'; -import { MONITOR_ROUTE, SETTINGS_ROUTE } from '../../../../../../common/constants'; +import { + MONITOR_ADD_ROUTE, + MONITOR_EDIT_ROUTE, + MONITOR_ROUTE, + SETTINGS_ROUTE, +} from '../../../../../../common/constants'; import { stringifyUrlParams } from '../../../utils/url_params'; import { InspectorHeaderLink } from './inspector_header_link'; import { ToggleAlertFlyoutButton } from '../../alerts/toggle_alert_flyout_button'; @@ -41,6 +46,8 @@ export function ActionMenuContent(): React.ReactElement { }; /* useSelector(monitorStatusSelector) TODO: Implement state for monitor status */ const detailRouteMatch = useRouteMatch(MONITOR_ROUTE); + const isEditRoute = useRouteMatch(MONITOR_EDIT_ROUTE); + const isAddRoute = useRouteMatch(MONITOR_ADD_ROUTE); const monitorId = selectedMonitor?.monitor?.id; const syntheticExploratoryViewLink = createExploratoryViewUrl( @@ -69,8 +76,12 @@ export function ActionMenuContent(): React.ReactElement { return ( - - + {!isEditRoute && !isAddRoute && ( + <> + + + + )} { schedule.number = `${schedule.number}s`; } + const params = monitorWithFormMonitorType[ConfigKey.PARAMS]; + if (typeof params !== 'string' && params) { + try { + monitorWithFormMonitorType[ConfigKey.PARAMS] = JSON.stringify(params); + } catch (e) { + // ignore + } + } + const browserMonitor = monitor as BrowserFields; + + const pwOptions = browserMonitor[ConfigKey.PLAYWRIGHT_OPTIONS]; + if (typeof pwOptions !== 'string' && pwOptions) { + try { + (monitorWithFormMonitorType as BrowserFields)[ConfigKey.PLAYWRIGHT_OPTIONS] = + JSON.stringify(pwOptions); + } catch (e) { + // ignore + } + } + // handle default monitor types from Uptime, which don't contain `ConfigKey.FORM_MONITOR_TYPE` if (!formMonitorType) { formMonitorType = @@ -81,7 +101,6 @@ export const formatDefaultFormValues = (monitor?: SyntheticsMonitor) => { switch (formMonitorType) { case FormMonitorType.MULTISTEP: - const browserMonitor = monitor as BrowserFields; return { ...monitorWithFormMonitorType, 'source.inline': { diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_add_edit/form/form_config.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_add_edit/form/form_config.tsx index 7e29504aca41f..470c344deb5e0 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_add_edit/form/form_config.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_add_edit/form/form_config.tsx @@ -148,6 +148,7 @@ export const BROWSER_ADVANCED = (readOnly: boolean) => [ FIELD(readOnly)[ConfigKey.IGNORE_HTTPS_ERRORS], FIELD(readOnly)[ConfigKey.SYNTHETICS_ARGS], FIELD(readOnly)[ConfigKey.PLAYWRIGHT_OPTIONS], + FIELD(readOnly)[ConfigKey.PARAMS], ], }, ]; diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_add_edit/form/validation.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_add_edit/form/validation.tsx index fbd792fd02238..3cd741fc44ce7 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_add_edit/form/validation.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_add_edit/form/validation.tsx @@ -117,8 +117,7 @@ const validateHTTP: ValidationLibrary = { return validateHeaders(headers); }, [ConfigKey.MAX_REDIRECTS]: ({ [ConfigKey.MAX_REDIRECTS]: value }) => - (!!value && !`${value}`.match(DIGITS_ONLY)) || - parseFloat(value as MonitorFields[ConfigKey.MAX_REDIRECTS]) < 0, + (!!value && !`${value}`.match(DIGITS_ONLY)) || parseFloat(value as string) < 0, [ConfigKey.URLS]: ({ [ConfigKey.URLS]: value }) => !value, ...validateCommon, }; diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_add_edit/hooks/use_clone_monitor.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_add_edit/hooks/use_clone_monitor.ts index ff871ae5fad98..637166af20155 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_add_edit/hooks/use_clone_monitor.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_add_edit/hooks/use_clone_monitor.ts @@ -6,13 +6,13 @@ */ import { useFetcher } from '@kbn/observability-shared-plugin/public'; +import { fetchSyntheticsMonitor } from '../../../state/monitor_details/api'; import { useGetUrlParams } from '../../../hooks'; -import { getDecryptedMonitorAPI } from '../../../state/monitor_management/api'; export const useCloneMonitor = () => { const { cloneId } = useGetUrlParams(); return useFetcher(() => { if (!cloneId) return Promise.resolve(undefined); - return getDecryptedMonitorAPI({ id: cloneId }); + return fetchSyntheticsMonitor({ monitorId: cloneId }); }, [cloneId]); }; diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_add_edit/hooks/use_monitor_not_found.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_add_edit/hooks/use_monitor_not_found.tsx index 6577b039e2290..6a27d43808f84 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_add_edit/hooks/use_monitor_not_found.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_add_edit/hooks/use_monitor_not_found.tsx @@ -6,10 +6,10 @@ */ import { useEffect } from 'react'; -import { IHttpFetchError, ResponseErrorBody } from '@kbn/core-http-browser'; import { useGetUrlParams, useUrlParams } from '../../../hooks'; +import { IHttpSerializedFetchError } from '../../../state'; -export const useMonitorNotFound = (error?: IHttpFetchError, id?: string) => { +export const useMonitorNotFound = (error?: IHttpSerializedFetchError | null, id?: string) => { const { packagePolicyId } = useGetUrlParams(); const updateUrlParams = useUrlParams()[1]; diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_add_edit/monitor_details_portal.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_add_edit/monitor_details_portal.tsx index fbdd823b49682..d63106bb42a7f 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_add_edit/monitor_details_portal.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_add_edit/monitor_details_portal.tsx @@ -22,17 +22,21 @@ interface Props { export const MonitorDetailsLinkPortal = ({ name, configId, locationId, updateUrl }: Props) => { return ( - + {locationId ? ( + + ) : ( + + )} ); }; -export const MonitorDetailsLink = ({ name, configId, locationId, updateUrl }: Props) => { +const MonitorDetailsLinkWithLocation = ({ name, configId, locationId, updateUrl }: Props) => { const selectedLocation = useSelectedLocation(updateUrl); let locId = locationId; @@ -45,6 +49,18 @@ export const MonitorDetailsLink = ({ name, configId, locationId, updateUrl }: Pr const href = history.createHref({ pathname: locId ? `monitor/${configId}?locationId=${locId}` : `monitor/${configId}`, }); + return ; +}; + +const MonitorDetailsLink = ({ name, configId }: Props) => { + const history = useHistory(); + const href = history.createHref({ + pathname: `monitor/${configId}`, + }); + return ; +}; + +const MonitorLink = ({ href, name }: { href: string; name: string }) => { return ( {name} diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_add_edit/monitor_edit_page.test.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_add_edit/monitor_edit_page.test.tsx index abcb69c5da9ba..6c4c7d5103c19 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_add_edit/monitor_edit_page.test.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_add_edit/monitor_edit_page.test.tsx @@ -115,6 +115,9 @@ describe('MonitorEditPage', () => { locationsLoaded: true, loading: false, }, + monitorDetails: { + syntheticsMonitorLoading: true, + }, }, }); @@ -173,6 +176,10 @@ describe('MonitorEditPage', () => { locationsLoaded: true, loading: false, }, + monitorDetails: { + syntheticsMonitorLoading: false, + syntheticsMonitorError: new Error('test error'), + }, }, }); diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_add_edit/monitor_edit_page.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_add_edit/monitor_edit_page.tsx index f339c69610672..9bfd306c4a6d9 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_add_edit/monitor_edit_page.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_add_edit/monitor_edit_page.tsx @@ -10,22 +10,27 @@ import { useParams } from 'react-router-dom'; import { useDispatch, useSelector } from 'react-redux'; import { EuiEmptyPrompt } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { useTrackPageview, useFetcher } from '@kbn/observability-shared-plugin/public'; -import { IHttpFetchError, ResponseErrorBody } from '@kbn/core-http-browser'; +import { useTrackPageview } from '@kbn/observability-shared-plugin/public'; import { CanUsePublicLocationsCallout } from './steps/can_use_public_locations_callout'; import { DisabledCallout } from '../monitors_page/management/disabled_callout'; import { useCanUsePublicLocations } from '../../../../hooks/use_capabilities'; import { EditMonitorNotFound } from './edit_monitor_not_found'; import { LoadingState } from '../monitors_page/overview/overview/monitor_detail_flyout'; import { ConfigKey, SourceType } from '../../../../../common/runtime_types'; -import { getServiceLocations, selectServiceLocationsState } from '../../state'; +import { + getMonitorAction, + getServiceLocations, + selectServiceLocationsState, + selectSyntheticsMonitor, + selectSyntheticsMonitorError, + selectSyntheticsMonitorLoading, +} from '../../state'; import { AlertingCallout } from '../common/alerting_callout/alerting_callout'; import { MonitorSteps } from './steps'; import { MonitorForm } from './form'; import { LocationsLoadingError } from './locations_loading_error'; import { MonitorDetailsLinkPortal } from './monitor_details_portal'; import { useMonitorAddEditBreadcrumbs } from './use_breadcrumbs'; -import { getDecryptedMonitorAPI } from '../../state/monitor_management/api'; import { EDIT_MONITOR_STEPS } from './steps/step_config'; import { useMonitorNotFound } from './hooks/use_monitor_not_found'; @@ -43,17 +48,15 @@ export const MonitorEditPage: React.FC = () => { } }, [locationsLoaded, dispatch]); - const { data, loading, error } = useFetcher(() => { - return getDecryptedMonitorAPI({ id: monitorId }); - // FIXME: Dario thinks there is a better way to do this but - // he's getting tired and maybe the Synthetics folks can fix it - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + const data = useSelector(selectSyntheticsMonitor); + const isLoading = useSelector(selectSyntheticsMonitorLoading); + const error = useSelector(selectSyntheticsMonitorError); - const monitorNotFoundError = useMonitorNotFound( - error as IHttpFetchError, - data?.id - ); + useEffect(() => { + dispatch(getMonitorAction.get({ monitorId })); + }, [dispatch, monitorId]); + + const monitorNotFoundError = useMonitorNotFound(error, data?.id); const canUsePublicLocations = useCanUsePublicLocations(data?.[ConfigKey.LOCATIONS]); @@ -93,7 +96,7 @@ export const MonitorEditPage: React.FC = () => { ); } - return data && locationsLoaded && !loading && !error ? ( + return data && locationsLoaded && !isLoading && !error ? ( <> diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_details/hooks/use_selected_monitor.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_details/hooks/use_selected_monitor.tsx index 1f2eadb7c09fc..1f4fab7e0d977 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_details/hooks/use_selected_monitor.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_details/hooks/use_selected_monitor.tsx @@ -7,7 +7,7 @@ import { useEffect, useMemo } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useParams } from 'react-router-dom'; -import { ConfigKey, EncryptedSyntheticsSavedMonitor } from '../../../../../../common/runtime_types'; +import { ConfigKey } from '../../../../../../common/runtime_types'; import { useSyntheticsRefreshContext } from '../../../contexts'; import { getMonitorAction, @@ -40,7 +40,8 @@ export const useSelectedMonitor = (monId?: string) => { monitorId && monitorFromList && monitorFromList[ConfigKey.CONFIG_ID] === monitorId; const isLoadedSyntheticsMonitorValid = monitorId && syntheticsMonitor && syntheticsMonitor[ConfigKey.CONFIG_ID] === monitorId; - const availableMonitor: EncryptedSyntheticsSavedMonitor | null = isLoadedSyntheticsMonitorValid + + const availableMonitor = isLoadedSyntheticsMonitorValid ? syntheticsMonitor : isMonitorFromListValid ? monitorFromList diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/monitor_detail_flyout.test.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/monitor_detail_flyout.test.tsx index dad59de6d630b..a575ecc110eb4 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/monitor_detail_flyout.test.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/monitor_detail_flyout.test.tsx @@ -23,11 +23,6 @@ TagsListMock.mockReturnValue(
Tags list
); describe('Monitor Detail Flyout', () => { beforeEach(() => { - jest.spyOn(observabilitySharedPublic, 'useFetcher').mockReturnValue({ - status: observabilitySharedPublic.FETCH_STATUS.PENDING, - data: null, - refetch: () => null, - }); jest .spyOn(observabilitySharedPublic, 'useTheme') .mockReturnValue({ eui: { euiColorVis0: 'red', euiColorVis9: 'red' } } as any); @@ -76,11 +71,6 @@ describe('Monitor Detail Flyout', () => { it('renders error boundary for fetch failure', () => { const testErrorText = 'This is a test error'; - jest.spyOn(observabilitySharedPublic, 'useFetcher').mockReturnValue({ - status: observabilitySharedPublic.FETCH_STATUS.FAILURE, - error: new Error('This is a test error'), - refetch: () => null, - }); const { getByText } = render( { onClose={jest.fn()} onEnabledChange={jest.fn()} onLocationChange={jest.fn()} - /> + />, + { + state: { + monitorDetails: { + syntheticsMonitorError: { body: { message: 'This is a test error' } }, + }, + }, + } ); getByText(testErrorText); }); it('renders loading state while fetching', () => { - jest.spyOn(observabilitySharedPublic, 'useFetcher').mockReturnValue({ - status: observabilitySharedPublic.FETCH_STATUS.LOADING, - refetch: jest.fn(), - }); - const { getByRole } = render( { onClose={jest.fn()} onEnabledChange={jest.fn()} onLocationChange={jest.fn()} - /> + />, + { + state: { + monitorDetails: { + syntheticsMonitorLoading: true, + }, + }, + } ); expect(getByRole('progressbar')); }); it('renders details for fetch success', () => { - jest.spyOn(observabilitySharedPublic, 'useFetcher').mockReturnValue({ - status: observabilitySharedPublic.FETCH_STATUS.SUCCESS, - data: { - enabled: true, - type: 'http', - name: 'test-monitor', - schedule: { - number: '1', - unit: 'm', - }, - tags: ['prod'], - config_id: 'test-id', - }, - refetch: jest.fn(), - }); const detailLink = '/app/synthetics/monitor/test-id'; jest.spyOn(monitorDetailLocator, 'useMonitorDetailLocator').mockReturnValue(detailLink); jest.spyOn(monitorDetailLocator, 'useMonitorDetailLocator').mockReturnValue(detailLink); @@ -146,7 +130,24 @@ describe('Monitor Detail Flyout', () => { onClose={jest.fn()} onEnabledChange={jest.fn()} onLocationChange={jest.fn()} - /> + />, + { + state: { + monitorDetails: { + syntheticsMonitor: { + enabled: true, + type: 'http', + name: 'test-monitor', + schedule: { + number: '1', + unit: 'm', + }, + tags: ['prod'], + config_id: 'test-id', + } as any, + }, + }, + } ); expect(getByText('Every 1 minute')); diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/monitor_detail_flyout.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/monitor_detail_flyout.tsx index 8c07b3e0cad2d..22dff2d8ddef2 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/monitor_detail_flyout.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/monitor_detail_flyout.tsx @@ -29,7 +29,7 @@ import { i18n } from '@kbn/i18n'; import { useKibana } from '@kbn/kibana-react-plugin/public'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { useTheme, FETCH_STATUS, useFetcher } from '@kbn/observability-shared-plugin/public'; +import { useTheme } from '@kbn/observability-shared-plugin/public'; import { useOverviewStatus } from '../../hooks/use_overview_status'; import { MonitorDetailsPanel } from '../../../common/components/monitor_details_panel'; import { ClientPluginsStart } from '../../../../../../plugin'; @@ -37,8 +37,12 @@ import { LocationsStatus, useStatusByLocation } from '../../../../hooks/use_stat import { MonitorEnabled } from '../../management/monitor_list_table/monitor_enabled'; import { ActionsPopover } from './actions_popover'; import { + getMonitorAction, selectMonitorUpsertStatus, selectServiceLocationsState, + selectSyntheticsMonitor, + selectSyntheticsMonitorError, + selectSyntheticsMonitorLoading, setFlyoutConfig, } from '../../../../state'; import { useMonitorDetail } from '../../../../hooks/use_monitor_detail'; @@ -46,7 +50,6 @@ import { ConfigKey, EncryptedSyntheticsMonitor, OverviewStatusMetaData } from '. import { useMonitorDetailLocator } from '../../../../hooks/use_monitor_detail_locator'; import { MonitorStatus } from '../../../common/components/monitor_status'; import { MonitorLocationSelect } from '../../../common/components/monitor_location_select'; -import { fetchSyntheticsMonitor } from '../../../../state/monitor_details/api'; interface Props { configId: string; @@ -250,21 +253,15 @@ export function MonitorDetailFlyout(props: Props) { }, [dispatch]); const upsertStatus = useSelector(selectMonitorUpsertStatus(configId)); + const monitorObject = useSelector(selectSyntheticsMonitor); + const isLoading = useSelector(selectSyntheticsMonitorLoading); + const error = useSelector(selectSyntheticsMonitorError); const upsertSuccess = upsertStatus?.status === 'success'; - const { - data: monitorObject, - error, - status, - loading, - } = useFetcher( - () => fetchSyntheticsMonitor({ monitorId: configId }), - // FIXME: Dario thinks there is a better way to do this but - // he's getting tired and maybe the Synthetics folks can fix it - // eslint-disable-next-line react-hooks/exhaustive-deps - [configId, upsertSuccess] - ); + useEffect(() => { + dispatch(getMonitorAction.get({ monitorId: configId })); + }, [configId, dispatch, upsertSuccess]); const [isActionsPopoverOpen, setIsActionsPopoverOpen] = useState(false); @@ -283,9 +280,9 @@ export function MonitorDetailFlyout(props: Props) { onClose={props.onClose} paddingSize="none" > - {status === FETCH_STATUS.FAILURE && {error?.message}} - {status === FETCH_STATUS.LOADING && } - {status === FETCH_STATUS.SUCCESS && monitorObject && ( + {error && !isLoading && {error?.body?.message}} + {isLoading && } + {monitorObject && ( <> @@ -332,7 +329,7 @@ export function MonitorDetailFlyout(props: Props) { ...monitorObject, id, }} - loading={Boolean(loading)} + loading={Boolean(isLoading)} /> diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/monitor_details/actions.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/monitor_details/actions.ts index 26e0a4c7f4c41..dd0db45112113 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/monitor_details/actions.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/monitor_details/actions.ts @@ -7,21 +7,16 @@ import { createAction } from '@reduxjs/toolkit'; import { MostRecentPingsRequest } from './api'; -import { - Ping, - PingsResponse, - EncryptedSyntheticsSavedMonitor, -} from '../../../../../common/runtime_types'; +import { Ping, PingsResponse, SyntheticsMonitorWithId } from '../../../../../common/runtime_types'; import { createAsyncAction } from '../utils/actions'; export const setMonitorDetailsLocationAction = createAction( '[MONITOR SUMMARY] SET LOCATION' ); -export const getMonitorAction = createAsyncAction< - { monitorId: string }, - EncryptedSyntheticsSavedMonitor ->('[MONITOR DETAILS] GET MONITOR'); +export const getMonitorAction = createAsyncAction<{ monitorId: string }, SyntheticsMonitorWithId>( + '[MONITOR DETAILS] GET MONITOR' +); export const getMonitorLastRunAction = createAsyncAction< { monitorId: string; locationId: string }, diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/monitor_details/api.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/monitor_details/api.ts index 8a75ab6884560..6fa59e4c175d1 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/monitor_details/api.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/monitor_details/api.ts @@ -8,10 +8,10 @@ import moment from 'moment'; import { apiService } from '../../../../utils/api_service'; import { - EncryptedSyntheticsSavedMonitor, EncryptedSyntheticsMonitorCodec, PingsResponse, PingsResponseType, + SyntheticsMonitorWithId, } from '../../../../../common/runtime_types'; import { INITIAL_REST_VERSION, SYNTHETICS_API_URLS } from '../../../../../common/constants'; @@ -67,11 +67,13 @@ export const fetchSyntheticsMonitor = async ({ monitorId, }: { monitorId: string; -}): Promise => - apiService.get( +}): Promise => { + return apiService.get( SYNTHETICS_API_URLS.GET_SYNTHETICS_MONITOR.replace('{monitorId}', monitorId), { + internal: true, version: INITIAL_REST_VERSION, }, EncryptedSyntheticsMonitorCodec ); +}; diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/monitor_details/index.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/monitor_details/index.ts index 162ed90dfb928..d247acba5d626 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/monitor_details/index.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/monitor_details/index.ts @@ -6,7 +6,7 @@ */ import { createReducer } from '@reduxjs/toolkit'; -import { EncryptedSyntheticsSavedMonitor, Ping } from '../../../../../common/runtime_types'; +import { Ping, SyntheticsMonitorWithId } from '../../../../../common/runtime_types'; import { checkIsStalePing } from '../../utils/monitor_test_result/check_pings'; import { enableMonitorAlertAction } from '../monitor_list/actions'; @@ -34,7 +34,8 @@ export interface MonitorDetailsState { loaded: boolean; }; syntheticsMonitorLoading: boolean; - syntheticsMonitor: EncryptedSyntheticsSavedMonitor | null; + syntheticsMonitor: SyntheticsMonitorWithId | null; + syntheticsMonitorError?: IHttpSerializedFetchError | null; syntheticsMonitorDispatchedAt: number; error: IHttpSerializedFetchError | null; selectedLocationId: string | null; @@ -97,15 +98,15 @@ export const monitorDetailsReducer = createReducer(initialState, (builder) => { .addCase(getMonitorAction.get, (state, action) => { state.syntheticsMonitorDispatchedAt = action.meta.dispatchedAt; state.syntheticsMonitorLoading = true; - state.error = null; + state.syntheticsMonitorError = null; }) .addCase(getMonitorAction.success, (state, action) => { state.syntheticsMonitor = action.payload; state.syntheticsMonitorLoading = false; - state.error = null; + state.syntheticsMonitorError = null; }) .addCase(getMonitorAction.fail, (state, action) => { - state.error = action.payload; + state.syntheticsMonitorError = action.payload; state.syntheticsMonitorLoading = false; }) .addCase(enableMonitorAlertAction.success, (state, action) => { diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/monitor_details/selectors.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/monitor_details/selectors.ts index af30569836e44..47d116594eb3b 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/monitor_details/selectors.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/monitor_details/selectors.ts @@ -27,3 +27,14 @@ export const selectMonitorPingsMetadata = createSelector(getState, (state) => st export const selectPingsError = createSelector(getState, (state) => state.error); export const selectStatusFilter = createSelector(getState, (state) => state.statusFilter); + +export const selectSyntheticsMonitor = createSelector(getState, (state) => state.syntheticsMonitor); +export const selectSyntheticsMonitorError = createSelector( + getState, + (state) => state.syntheticsMonitorError +); + +export const selectSyntheticsMonitorLoading = createSelector( + getState, + (state) => state.syntheticsMonitorLoading +); diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/monitor_list/actions.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/monitor_list/actions.ts index d15ea77d0d1a4..e914b27a26b67 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/monitor_list/actions.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/monitor_list/actions.ts @@ -11,6 +11,7 @@ import { MonitorManagementListResult, MonitorFiltersResult, EncryptedSyntheticsSavedMonitor, + SyntheticsMonitorWithId, } from '../../../../../common/runtime_types'; import { createAsyncAction } from '../utils/actions'; @@ -34,7 +35,7 @@ export const fetchUpsertFailureAction = createAction( export const enableMonitorAlertAction = createAsyncAction< UpsertMonitorRequest, - EncryptedSyntheticsSavedMonitor, + SyntheticsMonitorWithId, UpsertMonitorError >('enableMonitorAlertAction'); diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/monitor_list/api.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/monitor_list/api.ts index 4994921598997..bab1062a683a4 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/monitor_list/api.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/monitor_list/api.ts @@ -70,7 +70,7 @@ export const fetchUpsertMonitor = async ({ null, { version: INITIAL_REST_VERSION, - ui: true, + internal: true, } ); } else { diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/monitor_list/effects.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/monitor_list/effects.ts index ac142d7c52055..bbec528db6de2 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/monitor_list/effects.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/monitor_list/effects.ts @@ -9,7 +9,11 @@ import { PayloadAction } from '@reduxjs/toolkit'; import { call, put, takeEvery, select, takeLatest, debounce } from 'redux-saga/effects'; import { quietFetchOverviewStatusAction } from '../overview_status'; import { enableDefaultAlertingAction } from '../alert_rules'; -import { ConfigKey, EncryptedSyntheticsSavedMonitor } from '../../../../../common/runtime_types'; +import { + ConfigKey, + EncryptedSyntheticsSavedMonitor, + SyntheticsMonitorWithId, +} from '../../../../../common/runtime_types'; import { kibanaService } from '../../../../utils/kibana_service'; import { MonitorOverviewPageState } from '../overview'; import { selectOverviewState } from '../overview/selectors'; @@ -48,7 +52,7 @@ export function* enableMonitorAlertEffect() { function* (action: PayloadAction): Generator { try { const response = yield call(fetchUpsertMonitor, action.payload); - yield put(enableMonitorAlertAction.success(response as EncryptedSyntheticsSavedMonitor)); + yield put(enableMonitorAlertAction.success(response as SyntheticsMonitorWithId)); sendSuccessToast(action.payload.success); if ( (response as EncryptedSyntheticsSavedMonitor)[ConfigKey.ALERT_CONFIG]?.status?.enabled diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/monitor_management/api.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/monitor_management/api.ts index 69101f77e8291..f4118bed3b14c 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/monitor_management/api.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/monitor_management/api.ts @@ -11,12 +11,12 @@ import { apiService } from '../../../../utils/api_service'; import { EncryptedSyntheticsMonitor, SyntheticsMonitor, - SyntheticsMonitorCodec, ServiceLocationErrorsResponse, + SyntheticsMonitorWithId, } from '../../../../../common/runtime_types'; import { INITIAL_REST_VERSION, SYNTHETICS_API_URLS } from '../../../../../common/constants'; -export type UpsertMonitorResponse = ServiceLocationErrorsResponse | EncryptedSyntheticsMonitor; +export type UpsertMonitorResponse = ServiceLocationErrorsResponse | SyntheticsMonitorWithId; export const createMonitorAPI = async ({ monitor, @@ -25,6 +25,7 @@ export const createMonitorAPI = async ({ }): Promise => { return await apiService.post(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS, monitor, null, { version: INITIAL_REST_VERSION, + internal: true, }); }; @@ -53,21 +54,11 @@ export const updateMonitorAPI = async ({ id: string; }): Promise => { return await apiService.put(`${SYNTHETICS_API_URLS.SYNTHETICS_MONITORS}/${id}`, monitor, null, { - ui: true, + internal: true, version: INITIAL_REST_VERSION, }); }; -export const getDecryptedMonitorAPI = async ({ id }: { id: string }): Promise => - apiService.get( - SYNTHETICS_API_URLS.GET_SYNTHETICS_MONITOR.replace('{monitorId}', id), - { - decrypted: true, - version: INITIAL_REST_VERSION, - }, - SyntheticsMonitorCodec - ); - export const fetchProjectAPIKey = async ( accessToElasticManagedLocations: boolean ): Promise => { diff --git a/x-pack/plugins/observability_solution/synthetics/server/queries/get_monitor.ts b/x-pack/plugins/observability_solution/synthetics/server/queries/get_monitor.ts index 64e425951f5ff..2ccc8fe84d89c 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/queries/get_monitor.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/queries/get_monitor.ts @@ -5,12 +5,11 @@ * 2.0. */ -import { SavedObjectsClientContract } from '@kbn/core/server'; +import { SavedObject } from '@kbn/core/server'; import { EncryptedSavedObjectsClient } from '@kbn/encrypted-saved-objects-plugin/server'; import { syntheticsMonitorType } from '../../common/types/saved_objects'; import { SyntheticsMonitorWithSecretsAttributes, - EncryptedSyntheticsMonitorAttributes, SyntheticsMonitor, } from '../../common/runtime_types'; import { normalizeSecrets } from '../synthetics_service/utils/secrets'; @@ -18,28 +17,22 @@ import { normalizeSecrets } from '../synthetics_service/utils/secrets'; export const getSyntheticsMonitor = async ({ monitorId, encryptedSavedObjectsClient, - savedObjectsClient, + spaceId, }: { monitorId: string; + spaceId: string; encryptedSavedObjectsClient: EncryptedSavedObjectsClient; - savedObjectsClient: SavedObjectsClientContract; -}): Promise => { +}): Promise> => { try { - const encryptedMonitor = await savedObjectsClient.get( - syntheticsMonitorType, - monitorId - ); - const decryptedMonitor = await encryptedSavedObjectsClient.getDecryptedAsInternalUser( syntheticsMonitorType, monitorId, { - namespace: encryptedMonitor.namespaces?.[0], + namespace: spaceId, } ); - const { attributes } = normalizeSecrets(decryptedMonitor); - return attributes; + return normalizeSecrets(decryptedMonitor); } catch (e) { throw e; } diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/add_monitor.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/add_monitor.ts index 57f305b0ec2ef..f2c8f0974a6b4 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/add_monitor.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/add_monitor.ts @@ -13,7 +13,7 @@ import { AddEditMonitorAPI, CreateMonitorPayLoad } from './add_monitor/add_monit import { SyntheticsRestApiRouteFactory } from '../types'; import { SYNTHETICS_API_URLS } from '../../../common/constants'; import { normalizeAPIConfig, validateMonitor } from './monitor_validation'; -import { mapSavedObjectToMonitor } from './helper'; +import { mapSavedObjectToMonitor } from './formatters/saved_object_to_monitor'; export const addSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ({ method: 'POST', @@ -26,13 +26,18 @@ export const addSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ({ id: schema.maybe(schema.string()), preserve_namespace: schema.maybe(schema.boolean()), gettingStarted: schema.maybe(schema.boolean()), + internal: schema.maybe( + schema.boolean({ + defaultValue: false, + }) + ), }), }, }, handler: async (routeContext): Promise => { const { request, response, server } = routeContext; // usually id is auto generated, but this is useful for testing - const { id } = request.query; + const { id, internal } = request.query; const addMonitorAPI = new AddEditMonitorAPI(routeContext); @@ -113,7 +118,7 @@ export const addSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ({ addMonitorAPI.initDefaultAlerts(newMonitor.attributes.name); addMonitorAPI.setupGettingStarted(newMonitor.id); - return mapSavedObjectToMonitor(newMonitor); + return mapSavedObjectToMonitor({ monitor: newMonitor, internal }); } catch (getErr) { server.logger.error(getErr); if (getErr instanceof InvalidLocationError) { diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/add_monitor/add_monitor_api.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/add_monitor/add_monitor_api.ts index 359f3373cfd35..b80a4f5be6825 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/add_monitor/add_monitor_api.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/add_monitor/add_monitor_api.ts @@ -188,7 +188,7 @@ export class AddEditMonitorAPI { prevLocations?: MonitorFields['locations'] ) { const { savedObjectsClient, syntheticsMonitorClient, request } = this.routeContext; - const ui = Boolean((request.query as { ui?: boolean })?.ui); + const internal = Boolean((request.query as { internal?: boolean })?.internal); const { locations, private_locations: privateLocations, @@ -212,7 +212,7 @@ export class AddEditMonitorAPI { if (!locations && !privateLocations && prevLocations) { locationsVal = prevLocations; } else { - const monitorLocations = parseMonitorLocations(monitorPayload, prevLocations, ui); + const monitorLocations = parseMonitorLocations(monitorPayload, prevLocations, internal); if (monitorLocations.privateLocations.length > 0) { this.allPrivateLocations = await getPrivateLocations(savedObjectsClient); diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/add_monitor/utils.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/add_monitor/utils.ts index cd3c05855629d..0d096ee21745f 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/add_monitor/utils.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/add_monitor/utils.ts @@ -25,7 +25,7 @@ export const getPrivateLocationsForMonitor = async ( export const parseMonitorLocations = ( monitorPayload: CreateMonitorPayLoad, prevLocations?: MonitorFields['locations'], - ui: boolean = false + internal: boolean = false ) => { const { locations, private_locations: privateLocations } = monitorPayload; @@ -37,11 +37,11 @@ export const parseMonitorLocations = ( let pvtLocs = [...(privateLocations ?? []), ...extractPvtLocs]?.map((loc) => typeof loc === 'string' ? loc : loc.id ); - if (ui && !privateLocations && !locations && prevLocations) { + if (internal && !privateLocations && !locations && prevLocations) { locs = prevLocations.filter((loc) => loc.isServiceManaged).map((loc) => loc.id); pvtLocs = prevLocations.filter((loc) => !loc.isServiceManaged).map((loc) => loc.id); } else { - if (prevLocations && !ui) { + if (prevLocations && !internal) { if (!locations && !privateLocations) { locs = prevLocations.filter((loc) => loc.isServiceManaged).map((loc) => loc.id); pvtLocs = prevLocations.filter((loc) => !loc.isServiceManaged).map((loc) => loc.id); diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/edit_monitor.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/edit_monitor.ts index b2871f6a9accb..d9261847db1ab 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/edit_monitor.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/edit_monitor.ts @@ -14,7 +14,7 @@ import { AddEditMonitorAPI, CreateMonitorPayLoad } from './add_monitor/add_monit import { ELASTIC_MANAGED_LOCATIONS_DISABLED } from './add_monitor_project'; import { getDecryptedMonitor } from '../../saved_objects/synthetics_monitor'; import { getPrivateLocations } from '../../synthetics_service/get_private_locations'; -import { mergeSourceMonitor } from './helper'; +import { mergeSourceMonitor } from './formatters/saved_object_to_monitor'; import { RouteContext, SyntheticsRestApiRouteFactory } from '../types'; import { syntheticsMonitorType } from '../../../common/types/saved_objects'; import { @@ -33,7 +33,7 @@ import { formatTelemetryUpdateEvent, } from '../telemetry/monitor_upgrade_sender'; import { formatSecrets, normalizeSecrets } from '../../synthetics_service/utils/secrets'; -import { mapSavedObjectToMonitor } from './helper'; +import { mapSavedObjectToMonitor } from './formatters/saved_object_to_monitor'; // Simplify return promise type and type it with runtime_types export const editSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ({ @@ -46,7 +46,11 @@ export const editSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ( monitorId: schema.string(), }), query: schema.object({ - ui: schema.maybe(schema.boolean()), + internal: schema.maybe( + schema.boolean({ + defaultValue: false, + }) + ), }), body: schema.any(), }, @@ -55,7 +59,7 @@ export const editSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ( const { request, response, spaceId, server } = routeContext; const { logger } = server; const monitor = request.body as SyntheticsMonitor; - const reqQuery = request.query as { ui?: boolean }; + const reqQuery = request.query as { internal?: boolean }; const { monitorId } = request.params; if (!monitor || typeof monitor !== 'object' || isEmpty(monitor) || Array.isArray(monitor)) { @@ -86,7 +90,7 @@ export const editSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ( const previousMonitor = await getDecryptedMonitor(server, monitorId, spaceId); const normalizedPreviousMonitor = normalizeSecrets(previousMonitor).attributes; - if (normalizedPreviousMonitor.origin !== 'ui' && !reqQuery.ui) { + if (normalizedPreviousMonitor.origin !== 'ui' && !reqQuery.internal) { return response.badRequest(getInvalidOriginError(monitor)); } @@ -170,9 +174,13 @@ export const editSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ( }); } - return mapSavedObjectToMonitor( - editedMonitorSavedObject as SavedObject - ); + return mapSavedObjectToMonitor({ + internal: reqQuery.internal, + monitor: { + ...(editedMonitorSavedObject as SavedObject), + created_at: previousMonitor.created_at, + }, + }); } catch (updateErr) { if (SavedObjectsErrorHelpers.isNotFoundError(updateErr)) { return getMonitorNotFoundResponse(response, monitorId); diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/helper.test.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/formatters/saved_object_to_monitor.test.ts similarity index 64% rename from x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/helper.test.ts rename to x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/formatters/saved_object_to_monitor.test.ts index 9e0bbf10f45e0..0ddfda0f77874 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/helper.test.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/formatters/saved_object_to_monitor.test.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { mapSavedObjectToMonitor, mergeSourceMonitor } from './helper'; -import { EncryptedSyntheticsMonitor } from '../../../common/runtime_types'; +import { mapSavedObjectToMonitor, mergeSourceMonitor } from './saved_object_to_monitor'; +import { EncryptedSyntheticsMonitor } from '../../../../common/runtime_types'; describe('mergeSourceMonitor', () => { it('should merge keys', function () { @@ -66,8 +66,82 @@ describe('mergeSourceMonitor', () => { ]); }); - it('should omit null or undefined values', () => { - const result = mapSavedObjectToMonitor({ attributes: testMonitor } as any); + it('should not omit null or undefined values', () => { + const result = mapSavedObjectToMonitor({ monitor: { attributes: testMonitor } } as any); + + expect(result).toEqual({ + alert: { + status: { + enabled: true, + }, + tls: { + enabled: true, + }, + }, + config_id: 'ae88f0aa-9c7d-4a5f-96dc-89d65a0ca947', + custom_heartbeat_id: 'todos-lightweight-test-projects-default', + enabled: true, + id: 'todos-lightweight-test-projects-default', + ipv4: true, + ipv6: true, + locations: [ + { + geo: { + lat: 41.25, + lon: -95.86, + }, + id: 'us_central', + isServiceManaged: true, + label: 'North America - US Central', + }, + ], + max_redirects: 0, + mode: 'any', + name: 'Todos Lightweight', + namespace: 'default', + original_space: 'default', + proxy_url: '', + retest_on_failure: true, + revision: 21, + schedule: { + number: '3', + unit: 'm', + }, + 'service.name': '', + tags: [], + timeout: '16', + type: 'http', + url: '${devUrl}', + 'url.port': null, + ssl: { + certificate: '', + certificate_authorities: '', + supported_protocols: ['TLSv1.1', 'TLSv1.2', 'TLSv1.3'], + verification_mode: 'full', + key: 'test-key', + }, + response: { + include_body: 'on_error', + include_body_max_bytes: '1024', + include_headers: true, + }, + check: { + request: { + method: 'GET', + }, + response: { + status: ['404'], + }, + }, + params: {}, + }); + }); + + it('should not omit null or undefined values with ui', () => { + const result = mapSavedObjectToMonitor({ + monitor: { attributes: { ...testMonitor } }, + internal: true, + } as any); expect(result).toEqual({ __ui: { @@ -100,27 +174,37 @@ describe('mergeSourceMonitor', () => { label: 'North America - US Central', }, ], - max_attempts: 2, max_redirects: '0', mode: 'any', name: 'Todos Lightweight', namespace: 'default', - origin: 'project', original_space: 'default', project_id: 'test-projects', - 'response.include_body': 'on_error', - 'response.include_body_max_bytes': '1024', - 'response.include_headers': true, + proxy_url: '', revision: 21, schedule: { number: '3', unit: 'm', }, + 'service.name': '', + 'ssl.certificate': '', + 'ssl.certificate_authorities': '', + 'ssl.key': 'test-key', 'ssl.supported_protocols': ['TLSv1.1', 'TLSv1.2', 'TLSv1.3'], 'ssl.verification_mode': 'full', + 'response.include_body': 'on_error', + 'response.include_body_max_bytes': '1024', + 'response.include_headers': true, + tags: [], timeout: '16', type: 'http', - url: '${devUrl}', + 'url.port': null, + form_monitor_type: 'http', + hash: 'f4b6u3Q/PMK5KzEtPeMNzXJBA46rt+yilohaAoqMzqk=', + journey_id: 'todos-lightweight', + max_attempts: 2, + origin: 'project', + urls: '${devUrl}', }); }); }); @@ -179,6 +263,7 @@ const testMonitor = { ipv4: true, ipv6: true, 'ssl.certificate_authorities': '', + 'ssl.key': 'test-key', 'ssl.certificate': '', 'ssl.verification_mode': 'full', 'ssl.supported_protocols': ['TLSv1.1', 'TLSv1.2', 'TLSv1.3'], diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/formatters/saved_object_to_monitor.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/formatters/saved_object_to_monitor.ts new file mode 100644 index 0000000000000..4f5a3b194c896 --- /dev/null +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/formatters/saved_object_to_monitor.ts @@ -0,0 +1,158 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SavedObject } from '@kbn/core/server'; +import { mergeWith, omit, omitBy } from 'lodash'; +import { + ConfigKey, + EncryptedSyntheticsMonitor, + MonitorFields, + MonitorFieldsResult, +} from '../../../../common/runtime_types'; + +const keysToOmit = [ + ConfigKey.URLS, + ConfigKey.SOURCE_INLINE, + ConfigKey.HOSTS, + ConfigKey.CONFIG_HASH, + ConfigKey.JOURNEY_ID, + ConfigKey.FORM_MONITOR_TYPE, + ConfigKey.MAX_ATTEMPTS, + ConfigKey.MONITOR_SOURCE_TYPE, + ConfigKey.METADATA, + ConfigKey.SOURCE_PROJECT_CONTENT, + ConfigKey.PROJECT_ID, + ConfigKey.JOURNEY_FILTERS_MATCH, + ConfigKey.JOURNEY_FILTERS_TAGS, + ConfigKey.MONITOR_SOURCE_TYPE, +]; + +type Result = MonitorFieldsResult & { + url?: string; + host?: string; + inline_script?: string; + ssl: Record; + response: Record; + check: Record; +}; + +export const transformPublicKeys = (result: Result) => { + let formattedResult = { + ...result, + [ConfigKey.PARAMS]: formatParams(result), + retest_on_failure: (result[ConfigKey.MAX_ATTEMPTS] ?? 1) > 1, + ...(result[ConfigKey.HOSTS] && { host: result[ConfigKey.HOSTS] }), + ...(result[ConfigKey.URLS] && { url: result[ConfigKey.URLS] }), + }; + if (formattedResult[ConfigKey.MONITOR_TYPE] === 'browser') { + formattedResult = { + ...formattedResult, + ...(result[ConfigKey.SOURCE_INLINE] && { inline_script: result[ConfigKey.SOURCE_INLINE] }), + [ConfigKey.PLAYWRIGHT_OPTIONS]: formatPWOptions(result), + }; + } else { + formattedResult.ssl = formatNestedFields(formattedResult, 'ssl'); + formattedResult.response = formatNestedFields(formattedResult, 'response'); + formattedResult.check = formatNestedFields(formattedResult, 'check'); + if (formattedResult[ConfigKey.MAX_REDIRECTS]) { + formattedResult[ConfigKey.MAX_REDIRECTS] = Number(formattedResult[ConfigKey.MAX_REDIRECTS]); + } + } + const res = omit(formattedResult, keysToOmit) as Result; + + return omitBy( + res, + (value, key) => + key.startsWith('response.') || key.startsWith('ssl.') || key.startsWith('check.') + ); +}; + +export function mapSavedObjectToMonitor({ + monitor, + internal = false, +}: { + monitor: SavedObject; + internal?: boolean; +}) { + const result = { + ...monitor.attributes, + created_at: monitor.created_at, + updated_at: monitor.updated_at, + } as Result; + if (internal) { + return result; + } + return transformPublicKeys(result); +} +export function mergeSourceMonitor( + normalizedPreviousMonitor: EncryptedSyntheticsMonitor, + monitor: EncryptedSyntheticsMonitor +) { + return mergeWith({ ...normalizedPreviousMonitor }, monitor, customizer); +} + +// Ensure that METADATA is merged deeply, to protect AAD and prevent decryption errors +const customizer = (destVal: any, srcValue: any, key: string) => { + if (key === ConfigKey.ALERT_CONFIG) { + return { ...destVal, ...srcValue }; + } + if (key !== ConfigKey.METADATA) { + return srcValue; + } +}; + +const formatParams = (config: MonitorFields) => { + if (config[ConfigKey.PARAMS]) { + try { + return (config[ConfigKey.PARAMS] = JSON.parse(config[ConfigKey.PARAMS] ?? '{}')); + } catch (e) { + // ignore + return {}; + } + } + return {}; +}; + +const formatPWOptions = (config: MonitorFields) => { + if (config[ConfigKey.PLAYWRIGHT_OPTIONS]) { + try { + return (config[ConfigKey.PLAYWRIGHT_OPTIONS] = JSON.parse( + config[ConfigKey.PLAYWRIGHT_OPTIONS] ?? '{}' + )); + } catch (e) { + // ignore + return {}; + } + } + return {}; +}; + +// combine same nested fields into same object +const formatNestedFields = ( + config: MonitorFields | Record, + nestedKey: 'ssl' | 'response' | 'check' | 'request' +): Record => { + const nestedFields = Object.keys(config).filter((key) => + key.startsWith(`${nestedKey}.`) + ) as ConfigKey[]; + const obj: Record = {}; + + nestedFields.forEach((key) => { + const newKey = key.replace(`${nestedKey}.`, ''); + obj[newKey] = config[key]; + delete config[key]; + }); + + if (nestedKey === 'check') { + return { + request: formatNestedFields(obj, 'request'), + response: formatNestedFields(obj, 'response'), + }; + } + + return obj; +}; diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/get_monitor.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/get_monitor.ts index 69e7491a86b2b..1a07ddc832561 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/get_monitor.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/get_monitor.ts @@ -12,7 +12,7 @@ import { isStatusEnabled } from '../../../common/runtime_types/monitor_managemen import { ConfigKey, EncryptedSyntheticsMonitorAttributes } from '../../../common/runtime_types'; import { SYNTHETICS_API_URLS } from '../../../common/constants'; import { getMonitorNotFoundResponse } from '../synthetics_service/service_errors'; -import { mapSavedObjectToMonitor } from './helper'; +import { mapSavedObjectToMonitor } from './formatters/saved_object_to_monitor'; import { getSyntheticsMonitor } from '../../queries/get_monitor'; export const getSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ({ @@ -25,7 +25,11 @@ export const getSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ({ monitorId: schema.string({ minLength: 1, maxLength: 1024 }), }), query: schema.object({ - decrypted: schema.maybe(schema.boolean()), + internal: schema.maybe( + schema.boolean({ + defaultValue: false, + }) + ), }), }, }, @@ -34,36 +38,36 @@ export const getSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ({ response, server: { encryptedSavedObjects, coreStart }, savedObjectsClient, + spaceId, }): Promise => { const { monitorId } = request.params; try { - const { decrypted } = request.query; + const { internal } = request.query; - if (!decrypted) { - return mapSavedObjectToMonitor( - await savedObjectsClient.get( - syntheticsMonitorType, - monitorId - ) - ); - } else { - // only user with write permissions can decrypt the monitor - const canSave = - ( - await coreStart?.capabilities.resolveCapabilities(request, { - capabilityPath: 'uptime.*', - }) - ).uptime.save ?? false; - if (!canSave) { - return response.forbidden(); - } + const canSave = + ( + await coreStart?.capabilities.resolveCapabilities(request, { + capabilityPath: 'uptime.*', + }) + ).uptime.save ?? false; + if (Boolean(canSave)) { + // only user with write permissions can decrypt the monitor const encryptedSavedObjectsClient = encryptedSavedObjects.getClient(); - return await getSyntheticsMonitor({ + const monitor = await getSyntheticsMonitor({ monitorId, encryptedSavedObjectsClient, - savedObjectsClient, + spaceId, + }); + return mapSavedObjectToMonitor({ monitor, internal }); + } else { + return mapSavedObjectToMonitor({ + monitor: await savedObjectsClient.get( + syntheticsMonitorType, + monitorId + ), + internal, }); } } catch (getErr) { diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/get_monitors_list.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/get_monitors_list.ts index 563a7fde740bb..22584df3a7090 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/get_monitors_list.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/get_monitors_list.ts @@ -4,11 +4,11 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { mapSavedObjectToMonitor } from './formatters/saved_object_to_monitor'; import { SyntheticsRestApiRouteFactory } from '../types'; import { SYNTHETICS_API_URLS } from '../../../common/constants'; import { getMonitors, isMonitorsQueryFiltered, QuerySchema } from '../common'; import { syntheticsMonitorType } from '../../../common/types/saved_objects'; -import { mapSavedObjectToMonitor } from './helper'; export const getAllSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ({ method: 'GET', @@ -39,7 +39,11 @@ export const getAllSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => return { ...rest, - monitors: savedObjects.map(mapSavedObjectToMonitor), + monitors: savedObjects.map((monitor) => + mapSavedObjectToMonitor({ + monitor, + }) + ), absoluteTotal, perPage: perPageT, syncErrors: syntheticsMonitorClient.syntheticsService.syncErrors, diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/helper.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/helper.ts deleted file mode 100644 index d5480e0013b70..0000000000000 --- a/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/helper.ts +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { SavedObject } from '@kbn/core/server'; -import { mergeWith, omit, omitBy } from 'lodash'; -import { - ConfigKey, - EncryptedSyntheticsMonitor, - MonitorFields, -} from '../../../common/runtime_types'; - -const keysToOmit = [ - ConfigKey.URLS, - ConfigKey.SOURCE_INLINE, - ConfigKey.HOSTS, - ConfigKey.CONFIG_HASH, - ConfigKey.JOURNEY_ID, - ConfigKey.FORM_MONITOR_TYPE, -]; - -type Result = MonitorFields & { url?: string; host?: string; inline_script?: string }; - -export const transformPublicKeys = (result: Result) => { - if (result[ConfigKey.URLS]) { - result.url = result[ConfigKey.URLS]; - } - if (result[ConfigKey.SOURCE_INLINE]) { - result.inline_script = result[ConfigKey.SOURCE_INLINE]; - } - if (result[ConfigKey.HOSTS]) { - result.host = result[ConfigKey.HOSTS]; - } - if (result[ConfigKey.PARAMS]) { - try { - result[ConfigKey.PARAMS] = JSON.parse(result[ConfigKey.PARAMS] ?? '{}'); - } catch (e) { - // ignore - } - } - if (result[ConfigKey.PLAYWRIGHT_OPTIONS]) { - try { - result[ConfigKey.PLAYWRIGHT_OPTIONS] = JSON.parse( - result[ConfigKey.PLAYWRIGHT_OPTIONS] ?? '{}' - ); - } catch (e) { - // ignore - } - } - return omit(result, keysToOmit) as Result; -}; - -export function mapSavedObjectToMonitor( - so: SavedObject -) { - let result = Object.assign(so.attributes, { - created_at: so.created_at, - updated_at: so.updated_at, - }) as Result; - result = transformPublicKeys(result); - - // omit undefined value or null value - return omitBy(result, removeMonitorEmptyValues); -} -export function mergeSourceMonitor( - normalizedPreviousMonitor: EncryptedSyntheticsMonitor, - monitor: EncryptedSyntheticsMonitor -) { - return mergeWith({ ...normalizedPreviousMonitor }, monitor, customizer); -} - -// Ensure that METADATA is merged deeply, to protect AAD and prevent decryption errors -const customizer = (destVal: any, srcValue: any, key: string) => { - if (key === ConfigKey.ALERT_CONFIG) { - return { ...destVal, ...srcValue }; - } - if (key !== ConfigKey.METADATA) { - return srcValue; - } -}; - -export const removeMonitorEmptyValues = (v: any) => { - // value is falsy - return ( - v === undefined || - v === null || - // value is empty string - (typeof v === 'string' && v.trim() === '') || - // is empty array - (Array.isArray(v) && v.length === 0) || - // object is has no values - (typeof v === 'object' && Object.keys(v).length === 0) - ); -}; diff --git a/x-pack/plugins/observability_solution/synthetics/server/synthetics_service/private_location/synthetics_private_location.ts b/x-pack/plugins/observability_solution/synthetics/server/synthetics_service/private_location/synthetics_private_location.ts index 52bb0eefcb84e..fe5f74529121e 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/synthetics_service/private_location/synthetics_private_location.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/synthetics_service/private_location/synthetics_private_location.ts @@ -52,7 +52,7 @@ export class SyntheticsPrivateLocation { const newPolicy = await this.server.fleet.packagePolicyService.buildPackagePolicyFromPackage( soClient, 'synthetics', - this.server.logger + { logger: this.server.logger, installMissingPackage: true } ); if (!newPolicy) { diff --git a/x-pack/plugins/observability_solution/synthetics/server/synthetics_service/project_monitor/normalizers/common_fields.test.ts b/x-pack/plugins/observability_solution/synthetics/server/synthetics_service/project_monitor/normalizers/common_fields.test.ts index 650c9b964aaec..b4dd34952c7a8 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/synthetics_service/project_monitor/normalizers/common_fields.test.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/synthetics_service/project_monitor/normalizers/common_fields.test.ts @@ -163,6 +163,7 @@ describe('getNormalizeCommonFields', () => { timeout: '16', params: '', max_attempts: 2, + labels: {}, }, }); } @@ -228,6 +229,7 @@ describe('getNormalizeCommonFields', () => { timeout: '16', params: '', max_attempts: 2, + labels: {}, }, }); }); diff --git a/x-pack/plugins/observability_solution/synthetics/server/synthetics_service/project_monitor/normalizers/common_fields.ts b/x-pack/plugins/observability_solution/synthetics/server/synthetics_service/project_monitor/normalizers/common_fields.ts index d657ad376b38d..0a3aa8295a94d 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/synthetics_service/project_monitor/normalizers/common_fields.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/synthetics_service/project_monitor/normalizers/common_fields.ts @@ -94,6 +94,7 @@ export const getNormalizeCommonFields = ({ : defaultFields[ConfigKey.PARAMS], // picking out keys specifically, so users can't add arbitrary fields [ConfigKey.ALERT_CONFIG]: getAlertConfig(monitor), + [ConfigKey.LABELS]: monitor.fields || defaultFields[ConfigKey.LABELS], }; return { normalizedFields, errors }; }; @@ -459,7 +460,9 @@ export const flattenAndFormatObject = (obj: Record, prefix = '' return acc; }, {}); -export const normalizeYamlConfig = (monitor: NormalizedProjectProps['monitor']) => { +export const normalizeYamlConfig = (data: NormalizedProjectProps['monitor']) => { + // we map fields to labels + const { fields: _fields, ...monitor } = data; const defaultFields = DEFAULT_FIELDS[monitor.type as MonitorTypeEnum]; const supportedKeys = Object.keys(defaultFields); const flattenedConfig = flattenAndFormatObject(monitor, '', supportedKeys); diff --git a/x-pack/plugins/observability_solution/synthetics/server/synthetics_service/project_monitor/normalizers/http_monitor.ts b/x-pack/plugins/observability_solution/synthetics/server/synthetics_service/project_monitor/normalizers/http_monitor.ts index caef8a1ba3381..3e8e8647568c4 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/synthetics_service/project_monitor/normalizers/http_monitor.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/synthetics_service/project_monitor/normalizers/http_monitor.ts @@ -124,5 +124,5 @@ export const formatMaxRedirects = (value?: string | number): string => { const defaultFields = DEFAULT_FIELDS[MonitorTypeEnum.HTTP]; - return value ?? defaultFields[ConfigKey.MAX_REDIRECTS]; + return value ?? String(defaultFields[ConfigKey.MAX_REDIRECTS]); }; diff --git a/x-pack/plugins/search_indices/public/components/indices/details_page.tsx b/x-pack/plugins/search_indices/public/components/indices/details_page.tsx index bca02f1ef1264..46e595a3d2207 100644 --- a/x-pack/plugins/search_indices/public/components/indices/details_page.tsx +++ b/x-pack/plugins/search_indices/public/components/indices/details_page.tsx @@ -34,8 +34,9 @@ import { useIndexMapping } from '../../hooks/api/use_index_mappings'; import { IndexDocuments } from '../index_documents/index_documents'; import { DeleteIndexModal } from './delete_index_modal'; import { IndexloadingError } from './details_page_loading_error'; -import { SearchIndicesDetailsMappingsTabs } from '../../routes'; +import { SearchIndexDetailsTabs } from '../../routes'; import { SearchIndexDetailsMappings } from './details_page_mappings'; +import { SearchIndexDetailsSettings } from './details_page_settings'; export const SearchIndexDetailsPage = () => { const indexName = decodeURIComponent(useParams<{ indexName: string }>().indexName); @@ -49,33 +50,40 @@ export const SearchIndexDetailsPage = () => { isInitialLoading: isMappingsInitialLoading, } = useIndexMapping(indexName); - const SearchIndexDetailsTabs: EuiTabbedContentTab[] = useMemo(() => { + const detailsPageTabs: EuiTabbedContentTab[] = useMemo(() => { return [ { - id: SearchIndicesDetailsMappingsTabs.DATA, + id: SearchIndexDetailsTabs.DATA, name: i18n.translate('xpack.searchIndices.documentsTabLabel', { defaultMessage: 'Data', }), content: , - 'data-test-subj': `${SearchIndicesDetailsMappingsTabs.DATA}Tab`, + 'data-test-subj': `${SearchIndexDetailsTabs.DATA}Tab`, }, { - id: SearchIndicesDetailsMappingsTabs.MAPPINGS, + id: SearchIndexDetailsTabs.MAPPINGS, name: i18n.translate('xpack.searchIndices.mappingsTabLabel', { defaultMessage: 'Mappings', }), content: , - 'data-test-subj': `${SearchIndicesDetailsMappingsTabs.MAPPINGS}Tab`, + 'data-test-subj': `${SearchIndexDetailsTabs.MAPPINGS}Tab`, + }, + { + id: SearchIndexDetailsTabs.SETTINGS, + name: i18n.translate('xpack.searchIndices.settingsTabLabel', { + defaultMessage: 'Settings', + }), + content: , + 'data-test-subj': `${SearchIndexDetailsTabs.SETTINGS}Tab`, }, ]; }, [index, indexName]); - - const [selectedTab, setSelectedTab] = useState(SearchIndexDetailsTabs[0]); + const [selectedTab, setSelectedTab] = useState(detailsPageTabs[0]); useEffect(() => { - const newTab = SearchIndexDetailsTabs.find((tab) => tab.id === tabId); + const newTab = detailsPageTabs.find((tab) => tab.id === tabId); if (newTab) setSelectedTab(newTab); - }, [SearchIndexDetailsTabs, tabId]); + }, [detailsPageTabs, tabId]); const handleTabClick = useCallback( (tab) => { @@ -215,7 +223,7 @@ export const SearchIndexDetailsPage = () => { diff --git a/x-pack/plugins/search_indices/public/components/indices/details_page_settings.tsx b/x-pack/plugins/search_indices/public/components/indices/details_page_settings.tsx new file mode 100644 index 0000000000000..d70591c065b13 --- /dev/null +++ b/x-pack/plugins/search_indices/public/components/indices/details_page_settings.tsx @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { useMemo } from 'react'; +import { useKibana } from '../../hooks/use_kibana'; + +interface SearchIndexDetailsSettingsProps { + indexName: string; +} +export const SearchIndexDetailsSettings = ({ indexName }: SearchIndexDetailsSettingsProps) => { + const { indexManagement, history } = useKibana().services; + + const IndexSettingsComponent = useMemo( + () => indexManagement.getIndexSettingsComponent({ history }), + [indexManagement, history] + ); + + return ; +}; diff --git a/x-pack/plugins/search_indices/public/components/indices/indices_router.tsx b/x-pack/plugins/search_indices/public/components/indices/indices_router.tsx index 73aec33f80b5c..f6c931d73a99e 100644 --- a/x-pack/plugins/search_indices/public/components/indices/indices_router.tsx +++ b/x-pack/plugins/search_indices/public/components/indices/indices_router.tsx @@ -9,7 +9,7 @@ import { Route, Router, Routes } from '@kbn/shared-ux-router'; import { Redirect } from 'react-router-dom'; import { useKibana } from '../../hooks/use_kibana'; import { - SearchIndicesDetailsMappingsTabs, + SearchIndexDetailsTabs, SEARCH_INDICES_DETAILS_PATH, SEARCH_INDICES_DETAILS_TABS_PATH, } from '../../routes'; @@ -25,7 +25,7 @@ export const SearchIndicesRouter: React.FC = () => { diff --git a/x-pack/plugins/search_indices/public/routes.ts b/x-pack/plugins/search_indices/public/routes.ts index 3e347881d1219..9afa046385576 100644 --- a/x-pack/plugins/search_indices/public/routes.ts +++ b/x-pack/plugins/search_indices/public/routes.ts @@ -8,7 +8,8 @@ export const ROOT_PATH = '/'; export const SEARCH_INDICES_DETAILS_PATH = `${ROOT_PATH}index_details/:indexName`; export const SEARCH_INDICES_DETAILS_TABS_PATH = `${SEARCH_INDICES_DETAILS_PATH}/:tabId`; -export enum SearchIndicesDetailsMappingsTabs { +export enum SearchIndexDetailsTabs { DATA = 'data', MAPPINGS = 'mappings', + SETTINGS = 'settings', } diff --git a/x-pack/plugins/security/public/management/users/users_grid/users_grid_page.tsx b/x-pack/plugins/security/public/management/users/users_grid/users_grid_page.tsx index a9861850cf13e..6e4c30c3fdec5 100644 --- a/x-pack/plugins/security/public/management/users/users_grid/users_grid_page.tsx +++ b/x-pack/plugins/security/public/management/users/users_grid/users_grid_page.tsx @@ -236,6 +236,7 @@ export class UsersGridPage extends Component { defaultMessage="Users" /> } + data-test-subj="securityUsersPageHeader" rightSideItems={ this.props.readOnly ? undefined diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/delete.gen.ts b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/delete.gen.ts index 34acf2a802076..af147e63d05fc 100644 --- a/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/delete.gen.ts +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/delete.gen.ts @@ -10,7 +10,7 @@ * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator. * * info: - * title: Delete the entity store engine + * title: Delete an Entity Engine * version: 2023-10-31 */ @@ -19,25 +19,25 @@ import { BooleanFromString } from '@kbn/zod-helpers'; import { EntityType } from '../common.gen'; -export type DeleteEntityStoreRequestQuery = z.infer; -export const DeleteEntityStoreRequestQuery = z.object({ +export type DeleteEntityEngineRequestQuery = z.infer; +export const DeleteEntityEngineRequestQuery = z.object({ /** * Control flag to also delete the entity data. */ data: BooleanFromString.optional(), }); -export type DeleteEntityStoreRequestQueryInput = z.input; +export type DeleteEntityEngineRequestQueryInput = z.input; -export type DeleteEntityStoreRequestParams = z.infer; -export const DeleteEntityStoreRequestParams = z.object({ +export type DeleteEntityEngineRequestParams = z.infer; +export const DeleteEntityEngineRequestParams = z.object({ /** - * The entity type of the store (either 'user' or 'host'). + * The entity type of the engine (either 'user' or 'host'). */ entityType: EntityType, }); -export type DeleteEntityStoreRequestParamsInput = z.input; +export type DeleteEntityEngineRequestParamsInput = z.input; -export type DeleteEntityStoreResponse = z.infer; -export const DeleteEntityStoreResponse = z.object({ +export type DeleteEntityEngineResponse = z.infer; +export const DeleteEntityEngineResponse = z.object({ deleted: z.boolean().optional(), }); diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/delete.schema.yaml b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/delete.schema.yaml index c766d9895c5fa..4de339ea14e1c 100644 --- a/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/delete.schema.yaml +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/delete.schema.yaml @@ -1,22 +1,22 @@ openapi: 3.0.0 info: - title: Delete the entity store engine + title: Delete an Entity Engine version: '2023-10-31' paths: /api/entity_store/engines/{entityType}: delete: x-labels: [ess, serverless] x-codegen-enabled: true - operationId: DeleteEntityStore - summary: Delete the Entity Store engine + operationId: DeleteEntityEngine + summary: Delete the Entity Engine parameters: - name: entityType in: path required: true schema: $ref: '../common.schema.yaml#/components/schemas/EntityType' - description: The entity type of the store (either 'user' or 'host'). + description: The entity type of the engine (either 'user' or 'host'). - name: data in: query diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/get.gen.ts b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/get.gen.ts index 44f6f45844fc1..c5ebbb1aebb99 100644 --- a/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/get.gen.ts +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/get.gen.ts @@ -10,7 +10,7 @@ * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator. * * info: - * title: Get Entity Store engine + * title: Get Entity Engine * version: 2023-10-31 */ @@ -18,16 +18,14 @@ import { z } from '@kbn/zod'; import { EntityType, EngineDescriptor } from '../common.gen'; -export type GetEntityStoreEngineRequestParams = z.infer; -export const GetEntityStoreEngineRequestParams = z.object({ +export type GetEntityEngineRequestParams = z.infer; +export const GetEntityEngineRequestParams = z.object({ /** - * The entity type of the store (either 'user' or 'host'). + * The entity type of the engine (either 'user' or 'host'). */ entityType: EntityType, }); -export type GetEntityStoreEngineRequestParamsInput = z.input< - typeof GetEntityStoreEngineRequestParams ->; +export type GetEntityEngineRequestParamsInput = z.input; -export type GetEntityStoreEngineResponse = z.infer; -export const GetEntityStoreEngineResponse = EngineDescriptor; +export type GetEntityEngineResponse = z.infer; +export const GetEntityEngineResponse = EngineDescriptor; diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/get.schema.yaml b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/get.schema.yaml index d65a5906e54d9..4108f3f4594ae 100644 --- a/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/get.schema.yaml +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/get.schema.yaml @@ -1,21 +1,21 @@ openapi: 3.0.0 info: - title: Get Entity Store engine + title: Get Entity Engine version: '2023-10-31' paths: /api/entity_store/engines/{entityType}: get: x-labels: [ess, serverless] x-codegen-enabled: true - operationId: GetEntityStoreEngine - summary: Get the Entity Store engine + operationId: GetEntityEngine + summary: Get an Entity Engine parameters: - name: entityType in: path required: true schema: $ref: '../common.schema.yaml#/components/schemas/EntityType' - description: The entity type of the store (either 'user' or 'host'). + description: The entity type of the engine (either 'user' or 'host'). responses: '200': description: Successful response diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/init.gen.ts b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/init.gen.ts index 07f32f4cb7144..3812d8a6a580b 100644 --- a/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/init.gen.ts +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/init.gen.ts @@ -10,7 +10,7 @@ * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator. * * info: - * title: Init Entity Store types + * title: Init Entity Engine * version: 2023-10-31 */ @@ -18,21 +18,21 @@ import { z } from '@kbn/zod'; import { EntityType, IndexPattern, EngineDescriptor } from '../common.gen'; -export type InitEntityStoreRequestParams = z.infer; -export const InitEntityStoreRequestParams = z.object({ +export type InitEntityEngineRequestParams = z.infer; +export const InitEntityEngineRequestParams = z.object({ /** - * The entity type of the store (either 'user' or 'host'). + * The entity type of the engine (either 'user' or 'host'). */ entityType: EntityType, }); -export type InitEntityStoreRequestParamsInput = z.input; +export type InitEntityEngineRequestParamsInput = z.input; -export type InitEntityStoreRequestBody = z.infer; -export const InitEntityStoreRequestBody = z.object({ +export type InitEntityEngineRequestBody = z.infer; +export const InitEntityEngineRequestBody = z.object({ indexPattern: IndexPattern.optional(), filter: z.string().optional(), }); -export type InitEntityStoreRequestBodyInput = z.input; +export type InitEntityEngineRequestBodyInput = z.input; -export type InitEntityStoreResponse = z.infer; -export const InitEntityStoreResponse = EngineDescriptor; +export type InitEntityEngineResponse = z.infer; +export const InitEntityEngineResponse = EngineDescriptor; diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/init.schema.yaml b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/init.schema.yaml index 8e826d57ce40a..7410dcba7ef21 100644 --- a/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/init.schema.yaml +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/init.schema.yaml @@ -1,22 +1,22 @@ openapi: 3.0.0 info: - title: Init Entity Store types + title: Init Entity Engine version: '2023-10-31' paths: /api/entity_store/engines/{entityType}/init: post: x-labels: [ess, serverless] x-codegen-enabled: true - operationId: InitEntityStore - summary: Initialize the Entity Store + operationId: InitEntityEngine + summary: Initialize an Entity Engine parameters: - name: entityType in: path required: true schema: $ref: '../common.schema.yaml#/components/schemas/EntityType' - description: The entity type of the store (either 'user' or 'host'). + description: The entity type of the engine (either 'user' or 'host'). requestBody: description: Schema for the engine initialization required: true diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/list.gen.ts b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/list.gen.ts index 926549a329a4b..0a35fa49f6b33 100644 --- a/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/list.gen.ts +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/list.gen.ts @@ -10,7 +10,7 @@ * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator. * * info: - * title: List Entity Store engines + * title: List Entity Engines * version: 2023-10-31 */ @@ -18,8 +18,8 @@ import { z } from '@kbn/zod'; import { EngineDescriptor } from '../common.gen'; -export type ListEntityStoreEnginesResponse = z.infer; -export const ListEntityStoreEnginesResponse = z.object({ +export type ListEntityEnginesResponse = z.infer; +export const ListEntityEnginesResponse = z.object({ count: z.number().int().optional(), engines: z.array(EngineDescriptor).optional(), }); diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/list.schema.yaml b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/list.schema.yaml index efad1a4380352..deee32a8b2bb7 100644 --- a/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/list.schema.yaml +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/list.schema.yaml @@ -1,14 +1,14 @@ openapi: 3.0.0 info: - title: List Entity Store engines + title: List Entity Engines version: '2023-10-31' paths: /api/entity_store/engines: get: x-labels: [ess, serverless] x-codegen-enabled: true - operationId: ListEntityStoreEngines - summary: List the Entity Store engines + operationId: ListEntityEngines + summary: List the Entity Engines responses: '200': description: Successful response diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/start.gen.ts b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/start.gen.ts index b8e94d00551c0..8260597d044fc 100644 --- a/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/start.gen.ts +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/start.gen.ts @@ -10,7 +10,7 @@ * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator. * * info: - * title: Start the entity store engine + * title: Start an Entity Engine * version: 2023-10-31 */ @@ -18,16 +18,16 @@ import { z } from '@kbn/zod'; import { EntityType } from '../common.gen'; -export type StartEntityStoreRequestParams = z.infer; -export const StartEntityStoreRequestParams = z.object({ +export type StartEntityEngineRequestParams = z.infer; +export const StartEntityEngineRequestParams = z.object({ /** - * The entity type of the store (either 'user' or 'host'). + * The entity type of the engine (either 'user' or 'host'). */ entityType: EntityType, }); -export type StartEntityStoreRequestParamsInput = z.input; +export type StartEntityEngineRequestParamsInput = z.input; -export type StartEntityStoreResponse = z.infer; -export const StartEntityStoreResponse = z.object({ +export type StartEntityEngineResponse = z.infer; +export const StartEntityEngineResponse = z.object({ started: z.boolean().optional(), }); diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/start.schema.yaml b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/start.schema.yaml index 5c048bf3d973c..595fd1e074441 100644 --- a/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/start.schema.yaml +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/start.schema.yaml @@ -1,22 +1,22 @@ openapi: 3.0.0 info: - title: Start the entity store engine + title: Start an Entity Engine version: '2023-10-31' paths: /api/entity_store/engines/{entityType}/start: post: x-labels: [ess, serverless] x-codegen-enabled: true - operationId: StartEntityStore - summary: Start the Entity Store engine + operationId: StartEntityEngine + summary: Start an Entity Engine parameters: - name: entityType in: path required: true schema: $ref: '../common.schema.yaml#/components/schemas/EntityType' - description: The entity type of the store (either 'user' or 'host'). + description: The entity type of the engine (either 'user' or 'host'). responses: '200': description: Successful response diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/stats.gen.ts b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/stats.gen.ts index 631399faebc96..8b2cb44947535 100644 --- a/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/stats.gen.ts +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/stats.gen.ts @@ -10,7 +10,7 @@ * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator. * * info: - * title: Get the entity store engine stats + * title: Get Entity Engine stats * version: 2023-10-31 */ @@ -18,19 +18,19 @@ import { z } from '@kbn/zod'; import { EntityType, IndexPattern, EngineStatus } from '../common.gen'; -export type GetEntityStoreStatsRequestParams = z.infer; -export const GetEntityStoreStatsRequestParams = z.object({ +export type GetEntityEngineStatsRequestParams = z.infer; +export const GetEntityEngineStatsRequestParams = z.object({ /** - * The entity type of the store (either 'user' or 'host'). + * The entity type of the engine (either 'user' or 'host'). */ entityType: EntityType, }); -export type GetEntityStoreStatsRequestParamsInput = z.input< - typeof GetEntityStoreStatsRequestParams +export type GetEntityEngineStatsRequestParamsInput = z.input< + typeof GetEntityEngineStatsRequestParams >; -export type GetEntityStoreStatsResponse = z.infer; -export const GetEntityStoreStatsResponse = z.object({ +export type GetEntityEngineStatsResponse = z.infer; +export const GetEntityEngineStatsResponse = z.object({ type: EntityType.optional(), indexPattern: IndexPattern.optional(), status: EngineStatus.optional(), diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/stats.schema.yaml b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/stats.schema.yaml index 8d8327d5e6468..25c010acc92ce 100644 --- a/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/stats.schema.yaml +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/stats.schema.yaml @@ -1,22 +1,22 @@ openapi: 3.0.0 info: - title: Get the entity store engine stats + title: Get Entity Engine stats version: '2023-10-31' paths: /api/entity_store/engines/{entityType}/stats: post: x-labels: [ess, serverless] x-codegen-enabled: true - operationId: GetEntityStoreStats - summary: Get the Entity Store engine stats + operationId: GetEntityEngineStats + summary: Get Entity Engine stats parameters: - name: entityType in: path required: true schema: $ref: '../common.schema.yaml#/components/schemas/EntityType' - description: The entity type of the store (either 'user' or 'host'). + description: The entity type of the engine (either 'user' or 'host'). responses: '200': description: Successful response diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/stop.gen.ts b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/stop.gen.ts index ff3ef7a2f3eac..c2bb1bcc834be 100644 --- a/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/stop.gen.ts +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/stop.gen.ts @@ -10,7 +10,7 @@ * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator. * * info: - * title: Stop the entity store engine + * title: Stop an Entity Engine * version: 2023-10-31 */ @@ -18,16 +18,16 @@ import { z } from '@kbn/zod'; import { EntityType } from '../common.gen'; -export type StopEntityStoreRequestParams = z.infer; -export const StopEntityStoreRequestParams = z.object({ +export type StopEntityEngineRequestParams = z.infer; +export const StopEntityEngineRequestParams = z.object({ /** - * The entity type of the store (either 'user' or 'host'). + * The entity type of the engine (either 'user' or 'host'). */ entityType: EntityType, }); -export type StopEntityStoreRequestParamsInput = z.input; +export type StopEntityEngineRequestParamsInput = z.input; -export type StopEntityStoreResponse = z.infer; -export const StopEntityStoreResponse = z.object({ +export type StopEntityEngineResponse = z.infer; +export const StopEntityEngineResponse = z.object({ stopped: z.boolean().optional(), }); diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/stop.schema.yaml b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/stop.schema.yaml index 214f803a76e34..7c2c16c94fcc8 100644 --- a/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/stop.schema.yaml +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/engine/stop.schema.yaml @@ -1,22 +1,22 @@ openapi: 3.0.0 info: - title: Stop the entity store engine + title: Stop an Entity Engine version: '2023-10-31' paths: /api/entity_store/engines/{entityType}/stop: post: x-labels: [ess, serverless] x-codegen-enabled: true - operationId: StopEntityStore - summary: Stop the Entity Store engine + operationId: StopEntityEngine + summary: Stop an Entity Engine parameters: - name: entityType in: path required: true schema: $ref: '../common.schema.yaml#/components/schemas/EntityType' - description: The entity type of the store (either 'user' or 'host'). + description: The entity type of the engine (either 'user' or 'host'). responses: '200': description: Successful response diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/engine_status_route.gen.ts b/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/engine_status_route.gen.ts index b06f8960a4516..0faf22c032e06 100644 --- a/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/engine_status_route.gen.ts +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/engine_status_route.gen.ts @@ -45,10 +45,6 @@ export type RiskEngineStatusResponse = z.infer; export const RiskEngineStatusResponse = z.object({ legacy_risk_engine_status: RiskEngineStatus, risk_engine_status: RiskEngineStatus, - /** - * Indicates whether the maximum amount of risk engines has been reached - */ - is_max_amount_of_risk_engines_reached: z.boolean(), risk_engine_task_status: RiskEngineTaskStatus.optional(), }); diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/engine_status_route.schema.yaml b/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/engine_status_route.schema.yaml index d502848911f89..f4d074d67c2a4 100644 --- a/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/engine_status_route.schema.yaml +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/risk_engine/engine_status_route.schema.yaml @@ -59,14 +59,10 @@ components: required: - legacy_risk_engine_status - risk_engine_status - - is_max_amount_of_risk_engines_reached properties: legacy_risk_engine_status: $ref: '#/components/schemas/RiskEngineStatus' risk_engine_status: $ref: '#/components/schemas/RiskEngineStatus' - is_max_amount_of_risk_engines_reached: - description: Indicates whether the maximum amount of risk engines has been reached - type: boolean risk_engine_task_status: $ref: '#/components/schemas/RiskEngineTaskStatus' diff --git a/x-pack/plugins/security_solution/common/api/quickstart_client.gen.ts b/x-pack/plugins/security_solution/common/api/quickstart_client.gen.ts index 0e4b7456547cc..1af4e60124ef1 100644 --- a/x-pack/plugins/security_solution/common/api/quickstart_client.gen.ts +++ b/x-pack/plugins/security_solution/common/api/quickstart_client.gen.ts @@ -244,31 +244,31 @@ import type { UploadAssetCriticalityRecordsResponse, } from './entity_analytics/asset_criticality/upload_asset_criticality_csv.gen'; import type { - DeleteEntityStoreRequestQueryInput, - DeleteEntityStoreRequestParamsInput, - DeleteEntityStoreResponse, + DeleteEntityEngineRequestQueryInput, + DeleteEntityEngineRequestParamsInput, + DeleteEntityEngineResponse, } from './entity_analytics/entity_store/engine/delete.gen'; import type { - GetEntityStoreEngineRequestParamsInput, - GetEntityStoreEngineResponse, + GetEntityEngineRequestParamsInput, + GetEntityEngineResponse, } from './entity_analytics/entity_store/engine/get.gen'; import type { - InitEntityStoreRequestParamsInput, - InitEntityStoreRequestBodyInput, - InitEntityStoreResponse, + InitEntityEngineRequestParamsInput, + InitEntityEngineRequestBodyInput, + InitEntityEngineResponse, } from './entity_analytics/entity_store/engine/init.gen'; -import type { ListEntityStoreEnginesResponse } from './entity_analytics/entity_store/engine/list.gen'; +import type { ListEntityEnginesResponse } from './entity_analytics/entity_store/engine/list.gen'; import type { - StartEntityStoreRequestParamsInput, - StartEntityStoreResponse, + StartEntityEngineRequestParamsInput, + StartEntityEngineResponse, } from './entity_analytics/entity_store/engine/start.gen'; import type { - GetEntityStoreStatsRequestParamsInput, - GetEntityStoreStatsResponse, + GetEntityEngineStatsRequestParamsInput, + GetEntityEngineStatsResponse, } from './entity_analytics/entity_store/engine/stats.gen'; import type { - StopEntityStoreRequestParamsInput, - StopEntityStoreResponse, + StopEntityEngineRequestParamsInput, + StopEntityEngineResponse, } from './entity_analytics/entity_store/engine/stop.gen'; import type { ListEntitiesRequestQueryInput, @@ -315,7 +315,10 @@ import type { GetDraftTimelinesRequestQueryInput, GetDraftTimelinesResponse, } from './timeline/get_draft_timelines/get_draft_timelines_route.gen'; -import type { GetNotesRequestQueryInput } from './timeline/get_notes/get_notes_route.gen'; +import type { + GetNotesRequestQueryInput, + GetNotesResponse, +} from './timeline/get_notes/get_notes_route.gen'; import type { GetTimelineRequestQueryInput, GetTimelineResponse, @@ -663,10 +666,10 @@ If a record already exists for the specified entity, that record is overwritten }) .catch(catchAxiosErrorFormatAndThrow); } - async deleteEntityStore(props: DeleteEntityStoreProps) { - this.log.info(`${new Date().toISOString()} Calling API DeleteEntityStore`); + async deleteEntityEngine(props: DeleteEntityEngineProps) { + this.log.info(`${new Date().toISOString()} Calling API DeleteEntityEngine`); return this.kbnClient - .request({ + .request({ path: replaceParams('/api/entity_store/engines/{entityType}', props.params), headers: { [ELASTIC_HTTP_VERSION_HEADER]: '2023-10-31', @@ -1224,10 +1227,10 @@ finalize it. }) .catch(catchAxiosErrorFormatAndThrow); } - async getEntityStoreEngine(props: GetEntityStoreEngineProps) { - this.log.info(`${new Date().toISOString()} Calling API GetEntityStoreEngine`); + async getEntityEngine(props: GetEntityEngineProps) { + this.log.info(`${new Date().toISOString()} Calling API GetEntityEngine`); return this.kbnClient - .request({ + .request({ path: replaceParams('/api/entity_store/engines/{entityType}', props.params), headers: { [ELASTIC_HTTP_VERSION_HEADER]: '2023-10-31', @@ -1236,10 +1239,10 @@ finalize it. }) .catch(catchAxiosErrorFormatAndThrow); } - async getEntityStoreStats(props: GetEntityStoreStatsProps) { - this.log.info(`${new Date().toISOString()} Calling API GetEntityStoreStats`); + async getEntityEngineStats(props: GetEntityEngineStatsProps) { + this.log.info(`${new Date().toISOString()} Calling API GetEntityEngineStats`); return this.kbnClient - .request({ + .request({ path: replaceParams('/api/entity_store/engines/{entityType}/stats', props.params), headers: { [ELASTIC_HTTP_VERSION_HEADER]: '2023-10-31', @@ -1254,7 +1257,7 @@ finalize it. async getNotes(props: GetNotesProps) { this.log.info(`${new Date().toISOString()} Calling API GetNotes`); return this.kbnClient - .request({ + .request({ path: '/api/note', headers: { [ELASTIC_HTTP_VERSION_HEADER]: '2023-10-31', @@ -1413,10 +1416,10 @@ finalize it. }) .catch(catchAxiosErrorFormatAndThrow); } - async initEntityStore(props: InitEntityStoreProps) { - this.log.info(`${new Date().toISOString()} Calling API InitEntityStore`); + async initEntityEngine(props: InitEntityEngineProps) { + this.log.info(`${new Date().toISOString()} Calling API InitEntityEngine`); return this.kbnClient - .request({ + .request({ path: replaceParams('/api/entity_store/engines/{entityType}/init', props.params), headers: { [ELASTIC_HTTP_VERSION_HEADER]: '2023-10-31', @@ -1502,10 +1505,10 @@ finalize it. }) .catch(catchAxiosErrorFormatAndThrow); } - async listEntityStoreEngines() { - this.log.info(`${new Date().toISOString()} Calling API ListEntityStoreEngines`); + async listEntityEngines() { + this.log.info(`${new Date().toISOString()} Calling API ListEntityEngines`); return this.kbnClient - .request({ + .request({ path: '/api/entity_store/engines', headers: { [ELASTIC_HTTP_VERSION_HEADER]: '2023-10-31', @@ -1859,10 +1862,10 @@ detection engine rules. }) .catch(catchAxiosErrorFormatAndThrow); } - async startEntityStore(props: StartEntityStoreProps) { - this.log.info(`${new Date().toISOString()} Calling API StartEntityStore`); + async startEntityEngine(props: StartEntityEngineProps) { + this.log.info(`${new Date().toISOString()} Calling API StartEntityEngine`); return this.kbnClient - .request({ + .request({ path: replaceParams('/api/entity_store/engines/{entityType}/start', props.params), headers: { [ELASTIC_HTTP_VERSION_HEADER]: '2023-10-31', @@ -1871,10 +1874,10 @@ detection engine rules. }) .catch(catchAxiosErrorFormatAndThrow); } - async stopEntityStore(props: StopEntityStoreProps) { - this.log.info(`${new Date().toISOString()} Calling API StopEntityStore`); + async stopEntityEngine(props: StopEntityEngineProps) { + this.log.info(`${new Date().toISOString()} Calling API StopEntityEngine`); return this.kbnClient - .request({ + .request({ path: replaceParams('/api/entity_store/engines/{entityType}/stop', props.params), headers: { [ELASTIC_HTTP_VERSION_HEADER]: '2023-10-31', @@ -1993,9 +1996,9 @@ export interface CreateUpdateProtectionUpdatesNoteProps { export interface DeleteAssetCriticalityRecordProps { query: DeleteAssetCriticalityRecordRequestQueryInput; } -export interface DeleteEntityStoreProps { - query: DeleteEntityStoreRequestQueryInput; - params: DeleteEntityStoreRequestParamsInput; +export interface DeleteEntityEngineProps { + query: DeleteEntityEngineRequestQueryInput; + params: DeleteEntityEngineRequestParamsInput; } export interface DeleteNoteProps { body: DeleteNoteRequestBodyInput; @@ -2090,11 +2093,11 @@ export interface GetEndpointSuggestionsProps { params: GetEndpointSuggestionsRequestParamsInput; body: GetEndpointSuggestionsRequestBodyInput; } -export interface GetEntityStoreEngineProps { - params: GetEntityStoreEngineRequestParamsInput; +export interface GetEntityEngineProps { + params: GetEntityEngineRequestParamsInput; } -export interface GetEntityStoreStatsProps { - params: GetEntityStoreStatsRequestParamsInput; +export interface GetEntityEngineStatsProps { + params: GetEntityEngineStatsRequestParamsInput; } export interface GetNotesProps { query: GetNotesRequestQueryInput; @@ -2126,9 +2129,9 @@ export interface ImportRulesProps { export interface ImportTimelinesProps { body: ImportTimelinesRequestBodyInput; } -export interface InitEntityStoreProps { - params: InitEntityStoreRequestParamsInput; - body: InitEntityStoreRequestBodyInput; +export interface InitEntityEngineProps { + params: InitEntityEngineRequestParamsInput; + body: InitEntityEngineRequestBodyInput; } export interface InstallPrepackedTimelinesProps { body: InstallPrepackedTimelinesRequestBodyInput; @@ -2186,11 +2189,11 @@ export interface SetAlertsStatusProps { export interface SetAlertTagsProps { body: SetAlertTagsRequestBodyInput; } -export interface StartEntityStoreProps { - params: StartEntityStoreRequestParamsInput; +export interface StartEntityEngineProps { + params: StartEntityEngineRequestParamsInput; } -export interface StopEntityStoreProps { - params: StopEntityStoreRequestParamsInput; +export interface StopEntityEngineProps { + params: StopEntityEngineRequestParamsInput; } export interface SuggestUserProfilesProps { query: SuggestUserProfilesRequestQueryInput; diff --git a/x-pack/plugins/security_solution/common/api/timeline/delete_note/delete_note_route.gen.ts b/x-pack/plugins/security_solution/common/api/timeline/delete_note/delete_note_route.gen.ts index fc06d819ab39a..d98455c1fdb59 100644 --- a/x-pack/plugins/security_solution/common/api/timeline/delete_note/delete_note_route.gen.ts +++ b/x-pack/plugins/security_solution/common/api/timeline/delete_note/delete_note_route.gen.ts @@ -23,9 +23,11 @@ export const DeleteNoteRequestBody = z.union([ noteId: z.string(), }) .nullable(), - z.object({ - noteIds: z.array(z.string()).nullable(), - }), + z + .object({ + noteIds: z.array(z.string()).nullable(), + }) + .nullable(), ]); export type DeleteNoteRequestBodyInput = z.input; diff --git a/x-pack/plugins/security_solution/common/api/timeline/delete_note/delete_note_route.schema.yaml b/x-pack/plugins/security_solution/common/api/timeline/delete_note/delete_note_route.schema.yaml index 02a75d4e0ac77..380029bff8070 100644 --- a/x-pack/plugins/security_solution/common/api/timeline/delete_note/delete_note_route.schema.yaml +++ b/x-pack/plugins/security_solution/common/api/timeline/delete_note/delete_note_route.schema.yaml @@ -34,6 +34,7 @@ paths: type: string - type: object required: [noteIds] + nullable: true properties: noteIds: type: array diff --git a/x-pack/plugins/security_solution/common/api/timeline/delete_note/delete_note_route.ts b/x-pack/plugins/security_solution/common/api/timeline/delete_note/delete_note_route.ts deleted file mode 100644 index 717440fa0717a..0000000000000 --- a/x-pack/plugins/security_solution/common/api/timeline/delete_note/delete_note_route.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as runtimeTypes from 'io-ts'; -import { unionWithNullType } from '../../../utility_types'; - -export const deleteNoteSchema = runtimeTypes.partial({ - noteId: unionWithNullType(runtimeTypes.string), - noteIds: unionWithNullType(runtimeTypes.array(runtimeTypes.string)), -}); diff --git a/x-pack/plugins/security_solution/common/api/timeline/delete_timelines/delete_timelines_route.ts b/x-pack/plugins/security_solution/common/api/timeline/delete_timelines/delete_timelines_route.ts deleted file mode 100644 index c6b8f1baf6974..0000000000000 --- a/x-pack/plugins/security_solution/common/api/timeline/delete_timelines/delete_timelines_route.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as rt from 'io-ts'; - -const searchId = rt.partial({ searchIds: rt.array(rt.string) }); - -const baseDeleteTimelinesSchema = rt.type({ - savedObjectIds: rt.array(rt.string), -}); - -export const deleteTimelinesSchema = rt.intersection([baseDeleteTimelinesSchema, searchId]); diff --git a/x-pack/plugins/security_solution/common/api/timeline/export_timelines/export_timelines_route.ts b/x-pack/plugins/security_solution/common/api/timeline/export_timelines/export_timelines_route.ts deleted file mode 100644 index cc391a47e0b9e..0000000000000 --- a/x-pack/plugins/security_solution/common/api/timeline/export_timelines/export_timelines_route.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as rt from 'io-ts'; -import { unionWithNullType } from '../../../utility_types'; - -export const exportTimelinesQuerySchema = rt.type({ - file_name: rt.string, -}); - -export const exportTimelinesRequestBodySchema = rt.partial({ - ids: unionWithNullType(rt.array(rt.string)), -}); diff --git a/x-pack/plugins/security_solution/common/api/timeline/get_notes/get_notes_route.gen.ts b/x-pack/plugins/security_solution/common/api/timeline/get_notes/get_notes_route.gen.ts index b20b713bc3ed3..5851b95d4d606 100644 --- a/x-pack/plugins/security_solution/common/api/timeline/get_notes/get_notes_route.gen.ts +++ b/x-pack/plugins/security_solution/common/api/timeline/get_notes/get_notes_route.gen.ts @@ -16,17 +16,28 @@ import { z } from '@kbn/zod'; +import { Note } from '../model/components.gen'; + export type DocumentIds = z.infer; export const DocumentIds = z.union([z.array(z.string()), z.string()]); +export type GetNotesResult = z.infer; +export const GetNotesResult = z.object({ + totalCount: z.number(), + notes: z.array(Note), +}); + export type GetNotesRequestQuery = z.infer; export const GetNotesRequestQuery = z.object({ - documentIds: DocumentIds, - page: z.coerce.number().optional(), - perPage: z.coerce.number().optional(), + documentIds: DocumentIds.optional(), + page: z.string().nullable().optional(), + perPage: z.string().nullable().optional(), search: z.string().nullable().optional(), sortField: z.string().nullable().optional(), sortOrder: z.string().nullable().optional(), filter: z.string().nullable().optional(), }); export type GetNotesRequestQueryInput = z.input; + +export type GetNotesResponse = z.infer; +export const GetNotesResponse = z.union([GetNotesResult, z.object({})]); diff --git a/x-pack/plugins/security_solution/common/api/timeline/get_notes/get_notes_route.schema.yaml b/x-pack/plugins/security_solution/common/api/timeline/get_notes/get_notes_route.schema.yaml index 19b5d2074d18e..5942fd76c5d51 100644 --- a/x-pack/plugins/security_solution/common/api/timeline/get_notes/get_notes_route.schema.yaml +++ b/x-pack/plugins/security_solution/common/api/timeline/get_notes/get_notes_route.schema.yaml @@ -22,18 +22,17 @@ paths: parameters: - name: documentIds in: query - required: true schema: $ref: '#/components/schemas/DocumentIds' - name: page in: query schema: - type: number + type: string nullable: true - name: perPage in: query schema: - type: number + type: string nullable: true - name: search in: query @@ -58,6 +57,12 @@ paths: responses: '200': description: Indicates the requested notes were returned. + content: + application/json: + schema: + oneOf: + - $ref: '#/components/schemas/GetNotesResult' + - type: object components: schemas: @@ -67,3 +72,13 @@ components: items: type: string - type: string + GetNotesResult: + type: object + required: [totalCount, notes] + properties: + totalCount: + type: number + notes: + type: array + items: + $ref: '../model/components.schema.yaml#/components/schemas/Note' diff --git a/x-pack/plugins/security_solution/common/api/timeline/get_notes/get_notes_route.ts b/x-pack/plugins/security_solution/common/api/timeline/get_notes/get_notes_route.ts deleted file mode 100644 index 219632fc522e9..0000000000000 --- a/x-pack/plugins/security_solution/common/api/timeline/get_notes/get_notes_route.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as runtimeTypes from 'io-ts'; -import { unionWithNullType } from '../../../utility_types'; - -export const getNotesSchema = runtimeTypes.partial({ - documentIds: runtimeTypes.union([runtimeTypes.array(runtimeTypes.string), runtimeTypes.string]), - page: unionWithNullType(runtimeTypes.string), - perPage: unionWithNullType(runtimeTypes.string), - search: unionWithNullType(runtimeTypes.string), - sortField: unionWithNullType(runtimeTypes.string), - sortOrder: unionWithNullType(runtimeTypes.string), - filter: unionWithNullType(runtimeTypes.string), -}); diff --git a/x-pack/plugins/security_solution/common/api/timeline/import_timelines/import_timelines_route.ts b/x-pack/plugins/security_solution/common/api/timeline/import_timelines/import_timelines_route.ts index ecd40bab9476b..2ad6f3f8c7333 100644 --- a/x-pack/plugins/security_solution/common/api/timeline/import_timelines/import_timelines_route.ts +++ b/x-pack/plugins/security_solution/common/api/timeline/import_timelines/import_timelines_route.ts @@ -10,7 +10,7 @@ import * as rt from 'io-ts'; import { BareNoteSchema, SavedTimelineRuntimeType } from '../model/api'; import { unionWithNullType } from '../../../utility_types'; -import { pinnedEventIds } from '../pinned_events/pinned_events_route'; +const pinnedEventIds = unionWithNullType(rt.array(rt.string)); export const eventNotes = unionWithNullType(rt.array(BareNoteSchema)); export const globalNotes = unionWithNullType(rt.array(BareNoteSchema)); diff --git a/x-pack/plugins/security_solution/common/api/timeline/index.ts b/x-pack/plugins/security_solution/common/api/timeline/index.ts index c2901b96417db..806c0c8539d97 100644 --- a/x-pack/plugins/security_solution/common/api/timeline/index.ts +++ b/x-pack/plugins/security_solution/common/api/timeline/index.ts @@ -6,19 +6,14 @@ */ export * from './model/api'; -export * from './clean_draft_timelines/clean_draft_timelines_route'; +export * from './routes'; + export * from './get_draft_timelines/get_draft_timelines_route'; export * from './create_timelines/create_timelines_route'; -export * from './delete_note/delete_note_route'; -export * from './delete_timelines/delete_timelines_route'; -export * from './export_timelines/export_timelines_route'; export * from './get_timeline/get_timeline_route'; export * from './get_timelines/get_timelines_route'; export * from './import_timelines/import_timelines_route'; export * from './patch_timelines/patch_timelines_schema'; -export * from './persist_favorite/persist_favorite_route'; -export * from './persist_note/persist_note_route'; export * from './pinned_events/pinned_events_route'; export * from './install_prepackaged_timelines/install_prepackaged_timelines'; export * from './copy_timeline/copy_timeline_route'; -export * from './get_notes/get_notes_route'; diff --git a/x-pack/plugins/security_solution/common/api/timeline/model/api.ts b/x-pack/plugins/security_solution/common/api/timeline/model/api.ts index 1e40484505d1b..ff6707b700626 100644 --- a/x-pack/plugins/security_solution/common/api/timeline/model/api.ts +++ b/x-pack/plugins/security_solution/common/api/timeline/model/api.ts @@ -12,12 +12,17 @@ import { stringEnum, unionWithNullType } from '../../../utility_types'; import type { Maybe } from '../../../search_strategy'; import { Direction } from '../../../search_strategy'; -import type { PinnedEvent } from '../pinned_events/pinned_events_route'; import { PinnedEventRuntimeType } from '../pinned_events/pinned_events_route'; import { ErrorSchema } from './error_schema'; import type { DataProviderType } from './components.gen'; import { + BareNote, + BarePinnedEvent, DataProviderTypeEnum, + FavoriteTimelineResponse, + type FavoriteTimelineResult, + type Note, + PinnedEvent, RowRendererId, RowRendererIdEnum, SortFieldTimeline, @@ -31,8 +36,13 @@ import { } from './components.gen'; export { + BareNote, + BarePinnedEvent, DataProviderType, DataProviderTypeEnum, + FavoriteTimelineResponse, + Note, + PinnedEvent, RowRendererId, RowRendererIdEnum, SortFieldTimeline, @@ -45,6 +55,8 @@ export { TimelineTypeEnum, }; +export type BarePinnedEventWithoutExternalRefs = Omit; + /** * Outcome is a property of the saved object resolve api * will tell us info about the rule after 8.0 migrations @@ -83,24 +95,12 @@ export const BareNoteSchema = runtimeTypes.intersection([ }), ]); -export type BareNote = runtimeTypes.TypeOf; - /** * This type represents a note type stored in a saved object that does not include any fields that reference * other saved objects. */ export type BareNoteWithoutExternalRefs = Omit; -export const BareNoteWithoutExternalRefsSchema = runtimeTypes.partial({ - timelineId: unionWithNullType(runtimeTypes.string), - eventId: unionWithNullType(runtimeTypes.string), - note: unionWithNullType(runtimeTypes.string), - created: unionWithNullType(runtimeTypes.number), - createdBy: unionWithNullType(runtimeTypes.string), - updated: unionWithNullType(runtimeTypes.number), - updatedBy: unionWithNullType(runtimeTypes.string), -}); - export const NoteRuntimeType = runtimeTypes.intersection([ BareNoteSchema, runtimeTypes.type({ @@ -109,16 +109,6 @@ export const NoteRuntimeType = runtimeTypes.intersection([ }), ]); -export type Note = runtimeTypes.TypeOf; - -export interface ResponseNote { - code?: Maybe; - - message?: Maybe; - - note: Note; -} - /* * ColumnHeader Types */ @@ -489,27 +479,6 @@ export const importTimelineResultSchema = runtimeTypes.exact( export type ImportTimelineResultSchema = runtimeTypes.TypeOf; -const favoriteTimelineResult = runtimeTypes.partial({ - fullName: unionWithNullType(runtimeTypes.string), - userName: unionWithNullType(runtimeTypes.string), - favoriteDate: unionWithNullType(runtimeTypes.number), -}); - -export type FavoriteTimelineResult = runtimeTypes.TypeOf; - -export const responseFavoriteTimeline = runtimeTypes.partial({ - savedObjectId: runtimeTypes.string, - version: runtimeTypes.string, - code: unionWithNullType(runtimeTypes.number), - message: unionWithNullType(runtimeTypes.string), - templateTimelineId: unionWithNullType(runtimeTypes.string), - templateTimelineVersion: unionWithNullType(runtimeTypes.number), - timelineType: unionWithNullType(TimelineTypeLiteralRt), - favorite: unionWithNullType(runtimeTypes.array(favoriteTimelineResult)), -}); - -export type ResponseFavoriteTimeline = runtimeTypes.TypeOf; - export const pageInfoTimeline = runtimeTypes.type({ pageIndex: runtimeTypes.number, pageSize: runtimeTypes.number, diff --git a/x-pack/plugins/security_solution/common/api/timeline/model/components.gen.ts b/x-pack/plugins/security_solution/common/api/timeline/model/components.gen.ts index 392699b711ecf..990b19d6f3bab 100644 --- a/x-pack/plugins/security_solution/common/api/timeline/model/components.gen.ts +++ b/x-pack/plugins/security_solution/common/api/timeline/model/components.gen.ts @@ -217,7 +217,7 @@ export type BareNote = z.infer; export const BareNote = z.object({ eventId: z.string().nullable().optional(), note: z.string().nullable().optional(), - timelineId: z.string().nullable(), + timelineId: z.string(), created: z.number().nullable().optional(), createdBy: z.string().nullable().optional(), updated: z.number().nullable().optional(), @@ -227,23 +227,29 @@ export const BareNote = z.object({ export type Note = z.infer; export const Note = BareNote.merge( z.object({ - noteId: z.string().optional(), - version: z.string().optional(), + noteId: z.string(), + version: z.string(), }) ); -export type PinnedEvent = z.infer; -export const PinnedEvent = z.object({ - pinnedEventId: z.string(), +export type BarePinnedEvent = z.infer; +export const BarePinnedEvent = z.object({ eventId: z.string(), timelineId: z.string(), created: z.number().nullable().optional(), createdBy: z.string().nullable().optional(), updated: z.number().nullable().optional(), updatedBy: z.string().nullable().optional(), - version: z.string(), }); +export type PinnedEvent = z.infer; +export const PinnedEvent = BarePinnedEvent.merge( + z.object({ + pinnedEventId: z.string(), + version: z.string(), + }) +); + export type TimelineResponse = z.infer; export const TimelineResponse = SavedTimeline.merge( z.object({ @@ -269,6 +275,17 @@ export const FavoriteTimelineResponse = z.object({ favorite: z.array(FavoriteTimelineResult).optional(), }); +export type BareNoteWithoutExternalRefs = z.infer; +export const BareNoteWithoutExternalRefs = z.object({ + eventId: z.string().nullable().optional(), + note: z.string().nullable().optional(), + timelineId: z.string().nullable().optional(), + created: z.number().nullable().optional(), + createdBy: z.string().nullable().optional(), + updated: z.number().nullable().optional(), + updatedBy: z.string().nullable().optional(), +}); + export type GlobalNote = z.infer; export const GlobalNote = z.object({ noteId: z.string().optional(), diff --git a/x-pack/plugins/security_solution/common/api/timeline/model/components.schema.yaml b/x-pack/plugins/security_solution/common/api/timeline/model/components.schema.yaml index d5d4af4cb1e24..c8ba2e6019f16 100644 --- a/x-pack/plugins/security_solution/common/api/timeline/model/components.schema.yaml +++ b/x-pack/plugins/security_solution/common/api/timeline/model/components.schema.yaml @@ -305,6 +305,30 @@ components: nullable: true queryMatch: $ref: '#/components/schemas/QueryMatchResult' + BareNoteWithoutExternalRefs: + type: object + properties: + eventId: + type: string + nullable: true + note: + type: string + nullable: true + timelineId: + type: string + nullable: true + created: + type: number + nullable: true + createdBy: + type: string + nullable: true + updated: + type: number + nullable: true + updatedBy: + type: string + nullable: true BareNote: type: object required: [timelineId] @@ -317,7 +341,6 @@ components: nullable: true timelineId: type: string - nullable: true created: type: number nullable: true @@ -334,6 +357,7 @@ components: allOf: - $ref: '#/components/schemas/BareNote' - type: object + required: [noteId, version] properties: noteId: type: string @@ -451,12 +475,10 @@ components: serializedQuery: type: string nullable: true - PinnedEvent: + BarePinnedEvent: type: object - required: [eventId, pinnedEventId, timelineId, version] + required: [eventId, timelineId] properties: - pinnedEventId: - type: string eventId: type: string timelineId: @@ -473,8 +495,16 @@ components: updatedBy: type: string nullable: true - version: - type: string + PinnedEvent: + allOf: + - $ref: '#/components/schemas/BarePinnedEvent' + - type: object + required: [pinnedEventId, version] + properties: + pinnedEventId: + type: string + version: + type: string Sort: oneOf: - $ref: '#/components/schemas/SortObject' diff --git a/x-pack/plugins/security_solution/common/api/timeline/persist_favorite/persist_favorite_route.ts b/x-pack/plugins/security_solution/common/api/timeline/persist_favorite/persist_favorite_route.ts deleted file mode 100644 index 0f4adff41c910..0000000000000 --- a/x-pack/plugins/security_solution/common/api/timeline/persist_favorite/persist_favorite_route.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as rt from 'io-ts'; - -import { TimelineTypeLiteralRt } from '../model/api'; -import { unionWithNullType } from '../../../utility_types'; - -export const persistFavoriteSchema = rt.type({ - timelineId: unionWithNullType(rt.string), - templateTimelineId: unionWithNullType(rt.string), - templateTimelineVersion: unionWithNullType(rt.number), - timelineType: unionWithNullType(TimelineTypeLiteralRt), -}); diff --git a/x-pack/plugins/security_solution/common/api/timeline/persist_note/persist_note_route.gen.ts b/x-pack/plugins/security_solution/common/api/timeline/persist_note/persist_note_route.gen.ts index 002c8df84e86a..36def713d4994 100644 --- a/x-pack/plugins/security_solution/common/api/timeline/persist_note/persist_note_route.gen.ts +++ b/x-pack/plugins/security_solution/common/api/timeline/persist_note/persist_note_route.gen.ts @@ -18,6 +18,13 @@ import { z } from '@kbn/zod'; import { BareNote, Note } from '../model/components.gen'; +export type ResponseNote = z.infer; +export const ResponseNote = z.object({ + code: z.number(), + message: z.string(), + note: Note, +}); + export type PersistNoteRouteRequestBody = z.infer; export const PersistNoteRouteRequestBody = z.object({ note: BareNote, @@ -33,10 +40,6 @@ export type PersistNoteRouteRequestBodyInput = z.input; export const PersistNoteRouteResponse = z.object({ data: z.object({ - persistNote: z.object({ - code: z.number(), - message: z.string(), - note: Note, - }), + persistNote: ResponseNote, }), }); diff --git a/x-pack/plugins/security_solution/common/api/timeline/persist_note/persist_note_route.schema.yaml b/x-pack/plugins/security_solution/common/api/timeline/persist_note/persist_note_route.schema.yaml index 41ae12c974d6f..4ca14d2b15b13 100644 --- a/x-pack/plugins/security_solution/common/api/timeline/persist_note/persist_note_route.schema.yaml +++ b/x-pack/plugins/security_solution/common/api/timeline/persist_note/persist_note_route.schema.yaml @@ -65,12 +65,16 @@ paths: required: [persistNote] properties: persistNote: - type: object - required: [code, message, note] - properties: - code: - type: number - message: - type: string - note: - $ref: '../model/components.schema.yaml#/components/schemas/Note' + $ref: '#/components/schemas/ResponseNote' +components: + schemas: + ResponseNote: + type: object + required: [code, message, note] + properties: + code: + type: number + message: + type: string + note: + $ref: '../model/components.schema.yaml#/components/schemas/Note' diff --git a/x-pack/plugins/security_solution/common/api/timeline/persist_note/persist_note_route.ts b/x-pack/plugins/security_solution/common/api/timeline/persist_note/persist_note_route.ts deleted file mode 100644 index e6f8b9cc94fd3..0000000000000 --- a/x-pack/plugins/security_solution/common/api/timeline/persist_note/persist_note_route.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as runtimeTypes from 'io-ts'; -import { unionWithNullType } from '../../../utility_types'; -import { BareNoteSchema, BareNoteWithoutExternalRefsSchema } from '../model/api'; - -export const persistNoteWithRefSchema = runtimeTypes.intersection([ - runtimeTypes.type({ - note: BareNoteSchema, - }), - runtimeTypes.partial({ - overrideOwner: unionWithNullType(runtimeTypes.boolean), - noteId: unionWithNullType(runtimeTypes.string), - version: unionWithNullType(runtimeTypes.string), - }), -]); - -export const persistNoteWithoutRefSchema = runtimeTypes.intersection([ - runtimeTypes.type({ - note: BareNoteWithoutExternalRefsSchema, - }), - runtimeTypes.partial({ - overrideOwner: unionWithNullType(runtimeTypes.boolean), - noteId: unionWithNullType(runtimeTypes.string), - version: unionWithNullType(runtimeTypes.string), - eventIngested: unionWithNullType(runtimeTypes.string), - eventTimestamp: unionWithNullType(runtimeTypes.string), - eventDataView: unionWithNullType(runtimeTypes.string), - }), -]); - -export const persistNoteSchema = runtimeTypes.union([ - persistNoteWithRefSchema, - persistNoteWithoutRefSchema, -]); diff --git a/x-pack/plugins/security_solution/common/api/timeline/pinned_events/pinned_events_route.gen.ts b/x-pack/plugins/security_solution/common/api/timeline/pinned_events/pinned_events_route.gen.ts index 905e2740d32f2..6fd628e5a258e 100644 --- a/x-pack/plugins/security_solution/common/api/timeline/pinned_events/pinned_events_route.gen.ts +++ b/x-pack/plugins/security_solution/common/api/timeline/pinned_events/pinned_events_route.gen.ts @@ -18,6 +18,18 @@ import { z } from '@kbn/zod'; import { PinnedEvent } from '../model/components.gen'; +export type PinnedEventBaseResponseBody = z.infer; +export const PinnedEventBaseResponseBody = z.object({ + code: z.number(), + message: z.string().optional(), +}); + +export type PersistPinnedEventResponse = z.infer; +export const PersistPinnedEventResponse = z.union([ + PinnedEvent.merge(PinnedEventBaseResponseBody), + z.object({}).nullable(), +]); + export type PersistPinnedEventRouteRequestBody = z.infer; export const PersistPinnedEventRouteRequestBody = z.object({ eventId: z.string(), @@ -31,11 +43,6 @@ export type PersistPinnedEventRouteRequestBodyInput = z.input< export type PersistPinnedEventRouteResponse = z.infer; export const PersistPinnedEventRouteResponse = z.object({ data: z.object({ - persistPinnedEventOnTimeline: PinnedEvent.merge( - z.object({ - code: z.number().optional(), - message: z.string().optional(), - }) - ), + persistPinnedEventOnTimeline: PersistPinnedEventResponse, }), }); diff --git a/x-pack/plugins/security_solution/common/api/timeline/pinned_events/pinned_events_route.schema.yaml b/x-pack/plugins/security_solution/common/api/timeline/pinned_events/pinned_events_route.schema.yaml index de6ff200257c4..4ef83ebe04183 100644 --- a/x-pack/plugins/security_solution/common/api/timeline/pinned_events/pinned_events_route.schema.yaml +++ b/x-pack/plugins/security_solution/common/api/timeline/pinned_events/pinned_events_route.schema.yaml @@ -52,11 +52,22 @@ paths: required: [persistPinnedEventOnTimeline] properties: persistPinnedEventOnTimeline: - allOf: - - $ref: '../model/components.schema.yaml#/components/schemas/PinnedEvent' - - type: object - properties: - code: - type: number - message: - type: string + $ref: '#/components/schemas/PersistPinnedEventResponse' + +components: + schemas: + PersistPinnedEventResponse: + oneOf: + - allOf: + - $ref: '../model/components.schema.yaml#/components/schemas/PinnedEvent' + - $ref: '#/components/schemas/PinnedEventBaseResponseBody' + - type: object + nullable: true + PinnedEventBaseResponseBody: + type: object + required: [code] + properties: + code: + type: number + message: + type: string diff --git a/x-pack/plugins/security_solution/common/api/timeline/pinned_events/pinned_events_route.ts b/x-pack/plugins/security_solution/common/api/timeline/pinned_events/pinned_events_route.ts index 369cc6cc57089..31c3233e9b8ca 100644 --- a/x-pack/plugins/security_solution/common/api/timeline/pinned_events/pinned_events_route.ts +++ b/x-pack/plugins/security_solution/common/api/timeline/pinned_events/pinned_events_route.ts @@ -5,26 +5,14 @@ * 2.0. */ -/* eslint-disable @typescript-eslint/no-empty-interface */ - import * as runtimeTypes from 'io-ts'; import { unionWithNullType } from '../../../utility_types'; -export const pinnedEventIds = unionWithNullType(runtimeTypes.array(runtimeTypes.string)); -export const persistPinnedEventSchema = runtimeTypes.intersection([ - runtimeTypes.type({ - eventId: runtimeTypes.string, - timelineId: runtimeTypes.string, - }), - runtimeTypes.partial({ - pinnedEventId: unionWithNullType(runtimeTypes.string), - }), -]); - /* - * Note Types + * Pinned Event Types + * TODO: remove these when the timeline types are moved to zod */ -export const BarePinnedEventType = runtimeTypes.intersection([ +const BarePinnedEventType = runtimeTypes.intersection([ runtimeTypes.type({ timelineId: runtimeTypes.string, eventId: runtimeTypes.string, @@ -37,14 +25,6 @@ export const BarePinnedEventType = runtimeTypes.intersection([ }), ]); -export interface BarePinnedEvent extends runtimeTypes.TypeOf {} - -/** - * This type represents a pinned event type stored in a saved object that does not include any fields that reference - * other saved objects. - */ -export type BarePinnedEventWithoutExternalRefs = Omit; - export const PinnedEventRuntimeType = runtimeTypes.intersection([ runtimeTypes.type({ pinnedEventId: runtimeTypes.string, @@ -52,7 +32,3 @@ export const PinnedEventRuntimeType = runtimeTypes.intersection([ }), BarePinnedEventType, ]); - -export interface PinnedEvent extends runtimeTypes.TypeOf {} - -export type PinnedEventResponse = PinnedEvent & { code: number; message?: string }; diff --git a/x-pack/plugins/security_solution/common/api/timeline/routes.ts b/x-pack/plugins/security_solution/common/api/timeline/routes.ts new file mode 100644 index 0000000000000..9d3aec839a5c1 --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/timeline/routes.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { + DeleteTimelinesRequestBody, + DeleteTimelinesResponse, +} from './delete_timelines/delete_timelines_route.gen'; + +export { + PersistNoteRouteRequestBody, + PersistNoteRouteResponse, + ResponseNote, +} from './persist_note/persist_note_route.gen'; +export { DeleteNoteRequestBody, DeleteNoteResponse } from './delete_note/delete_note_route.gen'; + +export { CleanDraftTimelinesRequestBody } from './clean_draft_timelines/clean_draft_timelines_route.gen'; + +export { + ExportTimelinesRequestQuery, + ExportTimelinesRequestBody, +} from './export_timelines/export_timelines_route.gen'; + +export { + PersistFavoriteRouteResponse, + PersistFavoriteRouteRequestBody, +} from './persist_favorite/persist_favorite_route.gen'; + +export { + PersistPinnedEventRouteRequestBody, + PersistPinnedEventResponse, + PersistPinnedEventRouteResponse, +} from './pinned_events/pinned_events_route.gen'; + +export { + GetNotesRequestQuery, + GetNotesResponse, + GetNotesResult, +} from './get_notes/get_notes_route.gen'; diff --git a/x-pack/plugins/security_solution/common/entity_analytics/risk_engine/constants.ts b/x-pack/plugins/security_solution/common/entity_analytics/risk_engine/constants.ts index 08ffb774c2aac..17cfcf1da8e84 100644 --- a/x-pack/plugins/security_solution/common/entity_analytics/risk_engine/constants.ts +++ b/x-pack/plugins/security_solution/common/entity_analytics/risk_engine/constants.ts @@ -17,8 +17,6 @@ export const RISK_ENGINE_SETTINGS_URL = `${RISK_ENGINE_URL}/settings` as const; export const PUBLIC_RISK_ENGINE_URL = `${PUBLIC_RISK_SCORE_URL}/engine` as const; export const RISK_ENGINE_SCHEDULE_NOW_URL = `${RISK_ENGINE_URL}/schedule_now` as const; -export const MAX_SPACES_COUNT = 1; - type ClusterPrivilege = 'manage_index_templates' | 'manage_transform'; export const RISK_ENGINE_REQUIRED_ES_CLUSTER_PRIVILEGES = [ 'manage_index_templates', diff --git a/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_entity_analytics_api_2023_10_31.bundled.schema.yaml b/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_entity_analytics_api_2023_10_31.bundled.schema.yaml index c72b38d04c6cd..79e00fef8db8e 100644 --- a/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_entity_analytics_api_2023_10_31.bundled.schema.yaml +++ b/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_entity_analytics_api_2023_10_31.bundled.schema.yaml @@ -269,7 +269,7 @@ paths: - Security Solution Entity Analytics API /api/entity_store/engines: get: - operationId: ListEntityStoreEngines + operationId: ListEntityEngines responses: '200': content: @@ -284,14 +284,14 @@ paths: $ref: '#/components/schemas/EngineDescriptor' type: array description: Successful response - summary: List the Entity Store engines + summary: List the Entity Engines tags: - Security Solution Entity Analytics API /api/entity_store/engines/{entityType}: delete: - operationId: DeleteEntityStore + operationId: DeleteEntityEngine parameters: - - description: The entity type of the store (either 'user' or 'host'). + - description: The entity type of the engine (either 'user' or 'host'). in: path name: entityType required: true @@ -313,13 +313,13 @@ paths: deleted: type: boolean description: Successful response - summary: Delete the Entity Store engine + summary: Delete the Entity Engine tags: - Security Solution Entity Analytics API get: - operationId: GetEntityStoreEngine + operationId: GetEntityEngine parameters: - - description: The entity type of the store (either 'user' or 'host'). + - description: The entity type of the engine (either 'user' or 'host'). in: path name: entityType required: true @@ -332,14 +332,14 @@ paths: schema: $ref: '#/components/schemas/EngineDescriptor' description: Successful response - summary: Get the Entity Store engine + summary: Get an Entity Engine tags: - Security Solution Entity Analytics API /api/entity_store/engines/{entityType}/init: post: - operationId: InitEntityStore + operationId: InitEntityEngine parameters: - - description: The entity type of the store (either 'user' or 'host'). + - description: The entity type of the engine (either 'user' or 'host'). in: path name: entityType required: true @@ -364,14 +364,14 @@ paths: schema: $ref: '#/components/schemas/EngineDescriptor' description: Successful response - summary: Initialize the Entity Store + summary: Initialize an Entity Engine tags: - Security Solution Entity Analytics API /api/entity_store/engines/{entityType}/start: post: - operationId: StartEntityStore + operationId: StartEntityEngine parameters: - - description: The entity type of the store (either 'user' or 'host'). + - description: The entity type of the engine (either 'user' or 'host'). in: path name: entityType required: true @@ -387,14 +387,14 @@ paths: started: type: boolean description: Successful response - summary: Start the Entity Store engine + summary: Start an Entity Engine tags: - Security Solution Entity Analytics API /api/entity_store/engines/{entityType}/stats: post: - operationId: GetEntityStoreStats + operationId: GetEntityEngineStats parameters: - - description: The entity type of the store (either 'user' or 'host'). + - description: The entity type of the engine (either 'user' or 'host'). in: path name: entityType required: true @@ -422,14 +422,14 @@ paths: type: $ref: '#/components/schemas/EntityType' description: Successful response - summary: Get the Entity Store engine stats + summary: Get Entity Engine stats tags: - Security Solution Entity Analytics API /api/entity_store/engines/{entityType}/stop: post: - operationId: StopEntityStore + operationId: StopEntityEngine parameters: - - description: The entity type of the store (either 'user' or 'host'). + - description: The entity type of the engine (either 'user' or 'host'). in: path name: entityType required: true @@ -445,7 +445,7 @@ paths: stopped: type: boolean description: Successful response - summary: Stop the Entity Store engine + summary: Stop an Entity Engine tags: - Security Solution Entity Analytics API /api/entity_store/entities/list: diff --git a/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_timeline_api_2023_10_31.bundled.schema.yaml b/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_timeline_api_2023_10_31.bundled.schema.yaml index 5d55fac18c402..b7b63316b421a 100644 --- a/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_timeline_api_2023_10_31.bundled.schema.yaml +++ b/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_timeline_api_2023_10_31.bundled.schema.yaml @@ -29,7 +29,8 @@ paths: type: string required: - noteId - - type: object + - nullable: true + type: object properties: noteIds: items: @@ -60,19 +61,18 @@ paths: parameters: - in: query name: documentIds - required: true schema: $ref: '#/components/schemas/DocumentIds' - in: query name: page schema: nullable: true - type: number + type: string - in: query name: perPage schema: nullable: true - type: number + type: string - in: query name: search schema: @@ -95,6 +95,12 @@ paths: type: string responses: '200': + content: + application/json: + schema: + oneOf: + - $ref: '#/components/schemas/GetNotesResult' + - type: object description: Indicates the requested notes were returned. summary: Get notes tags: @@ -144,18 +150,7 @@ paths: type: object properties: persistNote: - type: object - properties: - code: - type: number - message: - type: string - note: - $ref: '#/components/schemas/Note' - required: - - code - - message - - note + $ref: '#/components/schemas/ResponseNote' required: - persistNote required: @@ -198,14 +193,7 @@ paths: type: object properties: persistPinnedEventOnTimeline: - allOf: - - $ref: '#/components/schemas/PinnedEvent' - - type: object - properties: - code: - type: number - message: - type: string + $ref: '#/components/schemas/PersistPinnedEventResponse' required: - persistPinnedEventOnTimeline required: @@ -1002,8 +990,28 @@ components: nullable: true type: string timelineId: + type: string + updated: + nullable: true + type: number + updatedBy: + nullable: true + type: string + required: + - timelineId + BarePinnedEvent: + type: object + properties: + created: + nullable: true + type: number + createdBy: nullable: true type: string + eventId: + type: string + timelineId: + type: string updated: nullable: true type: number @@ -1011,6 +1019,7 @@ components: nullable: true type: string required: + - eventId - timelineId ColumnHeaderResult: type: object @@ -1184,6 +1193,18 @@ components: type: string script: type: string + GetNotesResult: + type: object + properties: + notes: + items: + $ref: '#/components/schemas/Note' + type: array + totalCount: + type: number + required: + - totalCount + - notes ImportTimelineResult: type: object properties: @@ -1244,34 +1265,37 @@ components: type: string version: type: string + required: + - noteId + - version + PersistPinnedEventResponse: + oneOf: + - allOf: + - $ref: '#/components/schemas/PinnedEvent' + - $ref: '#/components/schemas/PinnedEventBaseResponseBody' + - nullable: true + type: object PinnedEvent: + allOf: + - $ref: '#/components/schemas/BarePinnedEvent' + - type: object + properties: + pinnedEventId: + type: string + version: + type: string + required: + - pinnedEventId + - version + PinnedEventBaseResponseBody: type: object properties: - created: - nullable: true - type: number - createdBy: - nullable: true - type: string - eventId: - type: string - pinnedEventId: - type: string - timelineId: - type: string - updated: - nullable: true + code: type: number - updatedBy: - nullable: true - type: string - version: + message: type: string required: - - eventId - - pinnedEventId - - timelineId - - version + - code QueryMatchResult: type: object properties: @@ -1316,6 +1340,19 @@ components: type: object readable: type: boolean + ResponseNote: + type: object + properties: + code: + type: number + message: + type: string + note: + $ref: '#/components/schemas/Note' + required: + - code + - message + - note RowRendererId: enum: - alert diff --git a/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_entity_analytics_api_2023_10_31.bundled.schema.yaml b/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_entity_analytics_api_2023_10_31.bundled.schema.yaml index 9df11c7eb9e10..accaeb48acb26 100644 --- a/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_entity_analytics_api_2023_10_31.bundled.schema.yaml +++ b/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_entity_analytics_api_2023_10_31.bundled.schema.yaml @@ -269,7 +269,7 @@ paths: - Security Solution Entity Analytics API /api/entity_store/engines: get: - operationId: ListEntityStoreEngines + operationId: ListEntityEngines responses: '200': content: @@ -284,14 +284,14 @@ paths: $ref: '#/components/schemas/EngineDescriptor' type: array description: Successful response - summary: List the Entity Store engines + summary: List the Entity Engines tags: - Security Solution Entity Analytics API /api/entity_store/engines/{entityType}: delete: - operationId: DeleteEntityStore + operationId: DeleteEntityEngine parameters: - - description: The entity type of the store (either 'user' or 'host'). + - description: The entity type of the engine (either 'user' or 'host'). in: path name: entityType required: true @@ -313,13 +313,13 @@ paths: deleted: type: boolean description: Successful response - summary: Delete the Entity Store engine + summary: Delete the Entity Engine tags: - Security Solution Entity Analytics API get: - operationId: GetEntityStoreEngine + operationId: GetEntityEngine parameters: - - description: The entity type of the store (either 'user' or 'host'). + - description: The entity type of the engine (either 'user' or 'host'). in: path name: entityType required: true @@ -332,14 +332,14 @@ paths: schema: $ref: '#/components/schemas/EngineDescriptor' description: Successful response - summary: Get the Entity Store engine + summary: Get an Entity Engine tags: - Security Solution Entity Analytics API /api/entity_store/engines/{entityType}/init: post: - operationId: InitEntityStore + operationId: InitEntityEngine parameters: - - description: The entity type of the store (either 'user' or 'host'). + - description: The entity type of the engine (either 'user' or 'host'). in: path name: entityType required: true @@ -364,14 +364,14 @@ paths: schema: $ref: '#/components/schemas/EngineDescriptor' description: Successful response - summary: Initialize the Entity Store + summary: Initialize an Entity Engine tags: - Security Solution Entity Analytics API /api/entity_store/engines/{entityType}/start: post: - operationId: StartEntityStore + operationId: StartEntityEngine parameters: - - description: The entity type of the store (either 'user' or 'host'). + - description: The entity type of the engine (either 'user' or 'host'). in: path name: entityType required: true @@ -387,14 +387,14 @@ paths: started: type: boolean description: Successful response - summary: Start the Entity Store engine + summary: Start an Entity Engine tags: - Security Solution Entity Analytics API /api/entity_store/engines/{entityType}/stats: post: - operationId: GetEntityStoreStats + operationId: GetEntityEngineStats parameters: - - description: The entity type of the store (either 'user' or 'host'). + - description: The entity type of the engine (either 'user' or 'host'). in: path name: entityType required: true @@ -422,14 +422,14 @@ paths: type: $ref: '#/components/schemas/EntityType' description: Successful response - summary: Get the Entity Store engine stats + summary: Get Entity Engine stats tags: - Security Solution Entity Analytics API /api/entity_store/engines/{entityType}/stop: post: - operationId: StopEntityStore + operationId: StopEntityEngine parameters: - - description: The entity type of the store (either 'user' or 'host'). + - description: The entity type of the engine (either 'user' or 'host'). in: path name: entityType required: true @@ -445,7 +445,7 @@ paths: stopped: type: boolean description: Successful response - summary: Stop the Entity Store engine + summary: Stop an Entity Engine tags: - Security Solution Entity Analytics API /api/entity_store/entities/list: diff --git a/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_timeline_api_2023_10_31.bundled.schema.yaml b/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_timeline_api_2023_10_31.bundled.schema.yaml index d8536c1703ed7..ec37c6fe5bf3f 100644 --- a/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_timeline_api_2023_10_31.bundled.schema.yaml +++ b/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_timeline_api_2023_10_31.bundled.schema.yaml @@ -29,7 +29,8 @@ paths: type: string required: - noteId - - type: object + - nullable: true + type: object properties: noteIds: items: @@ -60,19 +61,18 @@ paths: parameters: - in: query name: documentIds - required: true schema: $ref: '#/components/schemas/DocumentIds' - in: query name: page schema: nullable: true - type: number + type: string - in: query name: perPage schema: nullable: true - type: number + type: string - in: query name: search schema: @@ -95,6 +95,12 @@ paths: type: string responses: '200': + content: + application/json: + schema: + oneOf: + - $ref: '#/components/schemas/GetNotesResult' + - type: object description: Indicates the requested notes were returned. summary: Get notes tags: @@ -144,18 +150,7 @@ paths: type: object properties: persistNote: - type: object - properties: - code: - type: number - message: - type: string - note: - $ref: '#/components/schemas/Note' - required: - - code - - message - - note + $ref: '#/components/schemas/ResponseNote' required: - persistNote required: @@ -198,14 +193,7 @@ paths: type: object properties: persistPinnedEventOnTimeline: - allOf: - - $ref: '#/components/schemas/PinnedEvent' - - type: object - properties: - code: - type: number - message: - type: string + $ref: '#/components/schemas/PersistPinnedEventResponse' required: - persistPinnedEventOnTimeline required: @@ -1002,8 +990,28 @@ components: nullable: true type: string timelineId: + type: string + updated: + nullable: true + type: number + updatedBy: + nullable: true + type: string + required: + - timelineId + BarePinnedEvent: + type: object + properties: + created: + nullable: true + type: number + createdBy: nullable: true type: string + eventId: + type: string + timelineId: + type: string updated: nullable: true type: number @@ -1011,6 +1019,7 @@ components: nullable: true type: string required: + - eventId - timelineId ColumnHeaderResult: type: object @@ -1184,6 +1193,18 @@ components: type: string script: type: string + GetNotesResult: + type: object + properties: + notes: + items: + $ref: '#/components/schemas/Note' + type: array + totalCount: + type: number + required: + - totalCount + - notes ImportTimelineResult: type: object properties: @@ -1244,34 +1265,37 @@ components: type: string version: type: string + required: + - noteId + - version + PersistPinnedEventResponse: + oneOf: + - allOf: + - $ref: '#/components/schemas/PinnedEvent' + - $ref: '#/components/schemas/PinnedEventBaseResponseBody' + - nullable: true + type: object PinnedEvent: + allOf: + - $ref: '#/components/schemas/BarePinnedEvent' + - type: object + properties: + pinnedEventId: + type: string + version: + type: string + required: + - pinnedEventId + - version + PinnedEventBaseResponseBody: type: object properties: - created: - nullable: true - type: number - createdBy: - nullable: true - type: string - eventId: - type: string - pinnedEventId: - type: string - timelineId: - type: string - updated: - nullable: true + code: type: number - updatedBy: - nullable: true - type: string - version: + message: type: string required: - - eventId - - pinnedEventId - - timelineId - - version + - code QueryMatchResult: type: object properties: @@ -1316,6 +1340,19 @@ components: type: object readable: type: boolean + ResponseNote: + type: object + properties: + code: + type: number + message: + type: string + note: + $ref: '#/components/schemas/Note' + required: + - code + - message + - note RowRendererId: enum: - alert diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/json_diff/json_diff.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/json_diff/json_diff.test.tsx index 8e9c465d265b8..2a769e4be87d4 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/json_diff/json_diff.test.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/json_diff/json_diff.test.tsx @@ -19,7 +19,7 @@ import { COLORS } from './constants'; /* Finds an element with a text content that exactly matches the passed argument. - Handly because React Testing Library's doesn't provide an easy way to search by + Handy because React Testing Library's doesn't provide an easy way to search by text if the text is split into multiple DOM elements. */ function findChildByTextContent(parent: Element, textContent: string): HTMLElement { diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_about_section.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_about_section.tsx index 1d2e93df8e7f7..396811892610b 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_about_section.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_about_section.tsx @@ -72,11 +72,11 @@ interface AuthorProps { author: string[]; } -const Author = ({ author }: AuthorProps) => ( +export const Author = ({ author }: AuthorProps) => ( ); -const BuildingBlock = () => ( +export const BuildingBlock = () => ( {i18n.BUILDING_BLOCK_FIELD_DESCRIPTION} @@ -124,7 +124,7 @@ interface RiskScoreProps { riskScore: number; } -const RiskScore = ({ riskScore }: RiskScoreProps) => ( +export const RiskScore = ({ riskScore }: RiskScoreProps) => ( {riskScore} @@ -157,7 +157,7 @@ interface ReferencesProps { references: string[]; } -const References = ({ references }: ReferencesProps) => ( +export const References = ({ references }: ReferencesProps) => (
    {references @@ -173,7 +173,7 @@ const References = ({ references }: ReferencesProps) => ( ); -const FalsePositives = ({ falsePositives }: { falsePositives: string[] }) => ( +export const FalsePositives = ({ falsePositives }: { falsePositives: string[] }) => (
      {falsePositives.map((falsePositivesItem) => ( @@ -192,7 +192,7 @@ interface InvestigationFieldsProps { investigationFields: string[]; } -const InvestigationFields = ({ investigationFields }: InvestigationFieldsProps) => ( +export const InvestigationFields = ({ investigationFields }: InvestigationFieldsProps) => ( ); @@ -200,7 +200,7 @@ interface LicenseProps { license: string; } -const License = ({ license }: LicenseProps) => ( +export const License = ({ license }: LicenseProps) => ( {license} @@ -210,7 +210,7 @@ interface RuleNameOverrideProps { ruleNameOverride: string; } -const RuleNameOverride = ({ ruleNameOverride }: RuleNameOverrideProps) => ( +export const RuleNameOverride = ({ ruleNameOverride }: RuleNameOverrideProps) => ( {ruleNameOverride} @@ -236,7 +236,7 @@ interface TimestampOverrideProps { timestampOverride: string; } -const TimestampOverride = ({ timestampOverride }: TimestampOverrideProps) => ( +export const TimestampOverride = ({ timestampOverride }: TimestampOverrideProps) => ( {timestampOverride} @@ -246,7 +246,7 @@ interface MaxSignalsProps { maxSignals: number; } -const MaxSignals = ({ maxSignals }: MaxSignalsProps) => ( +export const MaxSignals = ({ maxSignals }: MaxSignalsProps) => ( {maxSignals} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_definition_section.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_definition_section.tsx index e0b0431138508..184633d813675 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_definition_section.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_definition_section.tsx @@ -179,7 +179,7 @@ interface ThresholdProps { threshold: ThresholdType; } -const Threshold = ({ threshold }: ThresholdProps) => ( +export const Threshold = ({ threshold }: ThresholdProps) => (
      {isEmpty(threshold.field[0]) ? `${descriptionStepI18n.THRESHOLD_RESULTS_ALL} >= ${threshold.value}` @@ -193,7 +193,7 @@ interface AnomalyThresholdProps { anomalyThreshold: number; } -const AnomalyThreshold = ({ anomalyThreshold }: AnomalyThresholdProps) => ( +export const AnomalyThreshold = ({ anomalyThreshold }: AnomalyThresholdProps) => ( {anomalyThreshold} @@ -258,7 +258,7 @@ interface RuleTypeProps { type: Type; } -const RuleType = ({ type }: RuleTypeProps) => ( +export const RuleType = ({ type }: RuleTypeProps) => ( {getRuleTypeDescription(type)} ); @@ -298,7 +298,7 @@ interface TimelineTitleProps { timelineTitle: string; } -const TimelineTitle = ({ timelineTitle }: TimelineTitleProps) => ( +export const TimelineTitle = ({ timelineTitle }: TimelineTitleProps) => ( {timelineTitle} @@ -354,7 +354,7 @@ interface SuppressAlertsByFieldProps { fields: string[]; } -const SuppressAlertsByField = ({ fields }: SuppressAlertsByFieldProps) => ( +export const SuppressAlertsByField = ({ fields }: SuppressAlertsByFieldProps) => ( ); @@ -362,7 +362,7 @@ interface SuppressAlertsDurationProps { duration?: Duration; } -const SuppressAlertsDuration = ({ duration }: SuppressAlertsDurationProps) => { +export const SuppressAlertsDuration = ({ duration }: SuppressAlertsDurationProps) => { const durationDescription = duration ? `${duration.value}${duration.unit}` : descriptionStepI18n.ALERT_SUPPRESSION_PER_RULE_EXECUTION; @@ -378,7 +378,7 @@ interface MissingFieldsStrategyProps { missingFieldsStrategy?: AlertSuppressionMissingFieldsStrategy; } -const MissingFieldsStrategy = ({ missingFieldsStrategy }: MissingFieldsStrategyProps) => { +export const MissingFieldsStrategy = ({ missingFieldsStrategy }: MissingFieldsStrategyProps) => { const missingFieldsDescription = missingFieldsStrategy === AlertSuppressionMissingFieldsStrategyEnum.suppress ? descriptionStepI18n.ALERT_SUPPRESSION_SUPPRESS_ON_MISSING_FIELDS @@ -395,7 +395,7 @@ interface NewTermsFieldsProps { newTermsFields: string[]; } -const NewTermsFields = ({ newTermsFields }: NewTermsFieldsProps) => ( +export const NewTermsFields = ({ newTermsFields }: NewTermsFieldsProps) => ( ); @@ -403,7 +403,7 @@ interface HistoryWindowSizeProps { historyWindowStart?: string; } -const HistoryWindowSize = ({ historyWindowStart }: HistoryWindowSizeProps) => { +export const HistoryWindowSize = ({ historyWindowStart }: HistoryWindowSizeProps) => { const size = historyWindowStart ? convertHistoryStartToSize(historyWindowStart) : '7d'; return ( diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_schedule_section.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_schedule_section.tsx index 7d7579521c82d..5ed99e4328136 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_schedule_section.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_schedule_section.tsx @@ -14,14 +14,26 @@ import { getHumanizedDuration } from '../../../../detections/pages/detection_eng import { DEFAULT_DESCRIPTION_LIST_COLUMN_WIDTHS } from './constants'; import * as i18n from './translations'; +interface AccessibleTimeValueProps { + timeValue: string; + 'data-test-subj'?: string; +} + +export const AccessibleTimeValue = ({ + timeValue, + 'data-test-subj': dataTestSubj, +}: AccessibleTimeValueProps) => ( + + + +); + interface IntervalProps { interval: string; } const Interval = ({ interval }: IntervalProps) => ( - - - + ); interface FromProps { @@ -30,9 +42,10 @@ interface FromProps { } const From = ({ from, interval }: FromProps) => ( - - - + ); export interface RuleScheduleSectionProps extends React.ComponentProps { diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/common_rule_field_readonly.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/common_rule_field_readonly.tsx index 35bde351bbeb6..9471a17b216b3 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/common_rule_field_readonly.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/common_rule_field_readonly.tsx @@ -19,6 +19,21 @@ import { NameReadOnly } from './fields/name/name'; import { TagsReadOnly } from './fields/tags/tags'; import { DescriptionReadOnly } from './fields/description/description'; import { assertUnreachable } from '../../../../../../../common/utility_types'; +import { AuthorReadOnly } from './fields/author/author'; +import { BuildingBlockReadOnly } from './fields/building_block/building_block'; +import { InvestigationFieldsReadOnly } from './fields/investigation_fields/investigation_fields'; +import { FalsePositivesReadOnly } from './fields/false_positives/false_positives'; +import { LicenseReadOnly } from './fields/license/license'; +import { MaxSignalsReadOnly } from './fields/max_signals/max_signals'; +import { NoteReadOnly } from './fields/note/note'; +import { RuleScheduleReadOnly } from './fields/rule_schedule/rule_schedule'; +import { ReferencesReadOnly } from './fields/references/references'; +import { RiskScoreReadOnly } from './fields/risk_score/risk_score'; +import { RuleNameOverrideReadOnly } from './fields/rule_name_override/rule_name_override'; +import { SetupReadOnly } from './fields/setup/setup'; +import { SeverityReadOnly } from './fields/severity/severity'; +import { TimestampOverrideReadOnly } from './fields/timestamp_override/timestamp_override'; +import { TimelineTemplateReadOnly } from './fields/timeline_template/timeline_template'; interface CommonRuleFieldReadOnlyProps { fieldName: keyof DiffableCommonFields; @@ -32,25 +47,28 @@ export function CommonRuleFieldReadOnly({ }: CommonRuleFieldReadOnlyProps) { switch (fieldName) { case 'author': - return null; + return ; case 'building_block': - return null; + return ; case 'description': return ; case 'exceptions_list': + /* Exceptions are not used in prebuilt rules */ return null; case 'investigation_fields': - return null; + return ( + + ); case 'false_positives': - return null; + return ; case 'license': - return null; + return ; case 'max_signals': - return null; + return ; case 'name': return ; case 'note': - return null; + return ; case 'related_integrations': return ( @@ -60,7 +78,7 @@ export function CommonRuleFieldReadOnly({ case 'risk_score_mapping': return ; case 'rule_schedule': - return null; + return ; case 'severity_mapping': return ; case 'tags': @@ -68,22 +86,24 @@ export function CommonRuleFieldReadOnly({ case 'threat': return ; case 'references': - return null; + return ; case 'risk_score': - return null; + return ; case 'rule_id': + /* Rule ID is not displayed in the UI */ return null; case 'rule_name_override': - return null; + return ; case 'setup': - return null; + return ; case 'severity': - return null; + return ; case 'timestamp_override': - return null; + return ; case 'timeline_template': - return null; + return ; case 'version': + /* Version is not displayed in the UI */ return null; default: return assertUnreachable(fieldName); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/custom_query_rule_field_readonly.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/custom_query_rule_field_readonly.tsx index 3d22d268438c4..da435b8092cd6 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/custom_query_rule_field_readonly.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/custom_query_rule_field_readonly.tsx @@ -9,6 +9,9 @@ import React from 'react'; import type { DiffableCustomQueryFields } from '../../../../../../../common/api/detection_engine'; import { DataSourceReadOnly } from './fields/data_source/data_source'; import { KqlQueryReadOnly } from './fields/kql_query'; +import { assertUnreachable } from '../../../../../../../common/utility_types'; +import { TypeReadOnly } from './fields/type/type'; +import { AlertSuppressionReadOnly } from './fields/alert_suppression/alert_suppression'; interface CustomQueryRuleFieldReadOnlyProps { fieldName: keyof DiffableCustomQueryFields; @@ -20,6 +23,13 @@ export function CustomQueryRuleFieldReadOnly({ finalDiffableRule, }: CustomQueryRuleFieldReadOnlyProps) { switch (fieldName) { + case 'alert_suppression': + return ( + + ); case 'data_source': return ; case 'kql_query': @@ -30,7 +40,9 @@ export function CustomQueryRuleFieldReadOnly({ ruleType={finalDiffableRule.type} /> ); + case 'type': + return ; default: - return null; // Will replace with `assertUnreachable(fieldName)` once all fields are implemented + return assertUnreachable(fieldName); } } diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/eql_rule_field_readonly.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/eql_rule_field_readonly.tsx index 126775965724b..b72fce91f198c 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/eql_rule_field_readonly.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/eql_rule_field_readonly.tsx @@ -9,6 +9,12 @@ import React from 'react'; import type { DiffableEqlFields } from '../../../../../../../common/api/detection_engine'; import { DataSourceReadOnly } from './fields/data_source/data_source'; import { EqlQueryReadOnly } from './fields/eql_query/eql_query'; +import { TypeReadOnly } from './fields/type/type'; +import { AlertSuppressionReadOnly } from './fields/alert_suppression/alert_suppression'; +import { assertUnreachable } from '../../../../../../../common/utility_types'; +import { EventCategoryOverrideReadOnly } from './fields/event_category_override/event_category_override'; +import { TimestampFieldReadOnly } from './fields/timestamp_field/timestamp_field'; +import { TiebreakerFieldReadOnly } from './fields/tiebreaker_field/tiebreaker_field'; interface EqlRuleFieldReadOnlyProps { fieldName: keyof DiffableEqlFields; @@ -17,6 +23,13 @@ interface EqlRuleFieldReadOnlyProps { export function EqlRuleFieldReadOnly({ fieldName, finalDiffableRule }: EqlRuleFieldReadOnlyProps) { switch (fieldName) { + case 'alert_suppression': + return ( + + ); case 'data_source': return ; case 'eql_query': @@ -26,9 +39,19 @@ export function EqlRuleFieldReadOnly({ fieldName, finalDiffableRule }: EqlRuleFi dataSource={finalDiffableRule.data_source} /> ); + case 'event_category_override': + return ( + + ); + case 'tiebreaker_field': + return ; + case 'timestamp_field': + return ; case 'type': - return null; + return ; default: - return null; // Will replace with `assertUnreachable(fieldName)` once all fields are implemented + return assertUnreachable(fieldName); } } diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/esql_rule_field_readonly.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/esql_rule_field_readonly.tsx index 755ad6b1b4789..04280ebaacc5b 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/esql_rule_field_readonly.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/esql_rule_field_readonly.tsx @@ -8,6 +8,9 @@ import React from 'react'; import type { DiffableEsqlFields } from '../../../../../../../common/api/detection_engine'; import { EsqlQueryReadOnly } from './fields/esql_query/esql_query'; +import { TypeReadOnly } from './fields/type/type'; +import { AlertSuppressionReadOnly } from './fields/alert_suppression/alert_suppression'; +import { assertUnreachable } from '../../../../../../../common/utility_types'; interface EsqlRuleFieldReadOnlyProps { fieldName: keyof DiffableEsqlFields; @@ -19,11 +22,18 @@ export function EsqlRuleFieldReadOnly({ finalDiffableRule, }: EsqlRuleFieldReadOnlyProps) { switch (fieldName) { + case 'alert_suppression': + return ( + + ); case 'esql_query': return ; case 'type': - return null; + return ; default: - return null; // Will replace with `assertUnreachable(fieldName)` once all fields are implemented + return assertUnreachable(fieldName); } } diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/alert_suppression/alert_suppression.stories.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/alert_suppression/alert_suppression.stories.tsx new file mode 100644 index 0000000000000..4f6739a3af481 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/alert_suppression/alert_suppression.stories.tsx @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import type { Story } from '@storybook/react'; +import { AlertSuppressionReadOnly } from './alert_suppression'; +import { FieldReadOnly } from '../../field_readonly'; +import type { DiffableRule } from '../../../../../../../../../common/api/detection_engine'; +import { mockCustomQueryRule } from '../../storybook/mocks'; +import { ThreeWayDiffStorybookProviders } from '../../storybook/three_way_diff_storybook_providers'; + +export default { + component: AlertSuppressionReadOnly, + title: + 'Rule Management/Prebuilt Rules/Upgrade Flyout/ThreeWayDiff/FieldReadOnly/alert_suppression', +}; + +interface TemplateProps { + finalDiffableRule: DiffableRule; +} + +const Template: Story = (args) => { + return ( + + + + ); +}; + +export const Default = Template.bind({}); + +Default.args = { + finalDiffableRule: mockCustomQueryRule({ + alert_suppression: { + group_by: ['host.name'], + duration: { value: 5, unit: 'm' }, + missing_fields_strategy: 'suppress', + }, + }), +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/alert_suppression/alert_suppression.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/alert_suppression/alert_suppression.tsx new file mode 100644 index 0000000000000..8c15347bc6d90 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/alert_suppression/alert_suppression.tsx @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiDescriptionList } from '@elastic/eui'; +import * as ruleDetailsI18n from '../../../../translations'; +import type { + AlertSuppression, + DiffableRuleTypes, + ThresholdAlertSuppression, +} from '../../../../../../../../../common/api/detection_engine'; +import { AlertSuppressionLabel } from '../../../../../../../rule_creation_ui/components/description_step/alert_suppression_label'; +import { + MissingFieldsStrategy, + SuppressAlertsByField, + SuppressAlertsDuration, +} from '../../../../rule_definition_section'; + +interface AlertSuppressionReadOnlyProps { + alertSuppression?: AlertSuppression | ThresholdAlertSuppression; + ruleType: DiffableRuleTypes; +} + +export function AlertSuppressionReadOnly({ + alertSuppression, + ruleType, +}: AlertSuppressionReadOnlyProps) { + if (!alertSuppression) { + return null; + } + + const listItems = []; + + if ('group_by' in alertSuppression) { + listItems.push({ + title: ( + + ), + description: , + }); + } + + if ('duration' in alertSuppression) { + listItems.push({ + title: ( + + + + ), + description: , + }); + } + + if ('missing_fields_strategy' in alertSuppression) { + listItems.push({ + title: ( + + + + ), + description: ( + + ), + }); + } + + return ; +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/anomaly_threshold/anomaly_threshold.stories.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/anomaly_threshold/anomaly_threshold.stories.tsx new file mode 100644 index 0000000000000..8203e015d11db --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/anomaly_threshold/anomaly_threshold.stories.tsx @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import type { Story } from '@storybook/react'; +import { AnomalyThresholdReadOnly } from './anomaly_threshold'; +import { FieldReadOnly } from '../../field_readonly'; +import type { DiffableRule } from '../../../../../../../../../common/api/detection_engine'; +import { mockMachineLearningRule } from '../../storybook/mocks'; + +export default { + component: AnomalyThresholdReadOnly, + title: + 'Rule Management/Prebuilt Rules/Upgrade Flyout/ThreeWayDiff/FieldReadOnly/anomaly_threshold', +}; + +interface TemplateProps { + finalDiffableRule: DiffableRule; +} + +const Template: Story = (args) => { + return ; +}; + +export const Default = Template.bind({}); + +Default.args = { + finalDiffableRule: mockMachineLearningRule({ + anomaly_threshold: 50, + }), +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/anomaly_threshold/anomaly_threshold.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/anomaly_threshold/anomaly_threshold.tsx new file mode 100644 index 0000000000000..20b9026c2a8ae --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/anomaly_threshold/anomaly_threshold.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiDescriptionList } from '@elastic/eui'; +import * as ruleDetailsI18n from '../../../../translations'; +import type { AnomalyThreshold as AnomalyThresholdType } from '../../../../../../../../../common/api/detection_engine'; +import { AnomalyThreshold } from '../../../../rule_definition_section'; + +interface TagsReadOnlyProps { + anomalyThreshold: AnomalyThresholdType; +} + +export function AnomalyThresholdReadOnly({ anomalyThreshold }: TagsReadOnlyProps) { + return ( + , + }, + ]} + /> + ); +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/author/author.stories.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/author/author.stories.tsx new file mode 100644 index 0000000000000..2700517c1c3ec --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/author/author.stories.tsx @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import type { Story } from '@storybook/react'; +import { AuthorReadOnly } from './author'; +import { FieldReadOnly } from '../../field_readonly'; +import type { DiffableRule } from '../../../../../../../../../common/api/detection_engine'; +import { mockCustomQueryRule } from '../../storybook/mocks'; + +export default { + component: AuthorReadOnly, + title: 'Rule Management/Prebuilt Rules/Upgrade Flyout/ThreeWayDiff/FieldReadOnly/author', +}; + +interface TemplateProps { + finalDiffableRule: DiffableRule; +} + +const Template: Story = (args) => { + return ; +}; + +export const Default = Template.bind({}); + +Default.args = { + finalDiffableRule: mockCustomQueryRule({ + author: ['Elastic', 'John Doe'], + }), +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/author/author.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/author/author.tsx new file mode 100644 index 0000000000000..c284275bd4db4 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/author/author.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiDescriptionList } from '@elastic/eui'; +import * as ruleDetailsI18n from '../../../../translations'; +import type { RuleAuthorArray } from '../../../../../../../../../common/api/detection_engine'; +import { Author } from '../../../../rule_about_section'; + +interface AuthorReadOnlyProps { + author: RuleAuthorArray; +} + +export function AuthorReadOnly({ author }: AuthorReadOnlyProps) { + return ( + , + }, + ]} + /> + ); +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/building_block/building_block.stories.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/building_block/building_block.stories.tsx new file mode 100644 index 0000000000000..f927b753c17ab --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/building_block/building_block.stories.tsx @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import type { Story } from '@storybook/react'; +import { BuildingBlockReadOnly } from './building_block'; +import { FieldReadOnly } from '../../field_readonly'; +import type { DiffableRule } from '../../../../../../../../../common/api/detection_engine'; +import { mockCustomQueryRule } from '../../storybook/mocks'; + +export default { + component: BuildingBlockReadOnly, + title: 'Rule Management/Prebuilt Rules/Upgrade Flyout/ThreeWayDiff/FieldReadOnly/building_block', +}; + +interface TemplateProps { + finalDiffableRule: DiffableRule; +} + +const Template: Story = (args) => { + return ; +}; + +export const Default = Template.bind({}); + +Default.args = { + finalDiffableRule: mockCustomQueryRule({ + building_block: { + type: 'default', + }, + }), +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/building_block/building_block.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/building_block/building_block.tsx new file mode 100644 index 0000000000000..84edd7932a2d7 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/building_block/building_block.tsx @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiDescriptionList } from '@elastic/eui'; +import * as ruleDetailsI18n from '../../../../translations'; +import { BuildingBlock } from '../../../../rule_about_section'; + +export function BuildingBlockReadOnly() { + return ( + , + }, + ]} + /> + ); +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/data_source/data_source.stories.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/data_source/data_source.stories.tsx index 9deebf794c241..091a874385156 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/data_source/data_source.stories.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/data_source/data_source.stories.tsx @@ -24,12 +24,12 @@ export default { interface TemplateProps { finalDiffableRule: DiffableRule; - kibanaServicesMock?: Record; + kibanaServicesOverrides?: Record; } const Template: Story = (args) => { return ( - + ); @@ -49,7 +49,7 @@ DataSourceWithDataView.args = { finalDiffableRule: mockCustomQueryRule({ data_source: dataSourceWithDataView, }), - kibanaServicesMock: { + kibanaServicesOverrides: { data: { dataViews: { get: async () => mockDataView(), diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/eql_query/eql_query.stories.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/eql_query/eql_query.stories.tsx index af835c5a92779..84ea98047bcee 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/eql_query/eql_query.stories.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/eql_query/eql_query.stories.tsx @@ -26,12 +26,12 @@ export default { interface TemplateProps { finalDiffableRule: DiffableRule; - kibanaServicesMock?: Record; + kibanaServicesOverrides?: Record; } const Template: Story = (args) => { return ( - + ); @@ -44,7 +44,7 @@ EqlQueryWithIndexPatterns.args = { eql_query: eqlQuery, data_source: dataSourceWithIndexPatterns, }), - kibanaServicesMock: { + kibanaServicesOverrides: { data: { dataViews: { create: async () => mockDataView(), @@ -60,7 +60,7 @@ EqlQueryWithDataView.args = { eql_query: eqlQuery, data_source: dataSourceWithDataView, }), - kibanaServicesMock: { + kibanaServicesOverrides: { data: { dataViews: { get: async () => mockDataView(), diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/event_category_override/event_category_override.stories.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/event_category_override/event_category_override.stories.tsx new file mode 100644 index 0000000000000..1eb29ef1084d9 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/event_category_override/event_category_override.stories.tsx @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import type { Story } from '@storybook/react'; +import { EventCategoryOverrideReadOnly } from './event_category_override'; +import { FieldReadOnly } from '../../field_readonly'; +import type { DiffableRule } from '../../../../../../../../../common/api/detection_engine'; +import { mockEqlRule } from '../../storybook/mocks'; + +export default { + component: EventCategoryOverrideReadOnly, + title: + 'Rule Management/Prebuilt Rules/Upgrade Flyout/ThreeWayDiff/FieldReadOnly/event_category_override', +}; + +interface TemplateProps { + finalDiffableRule: DiffableRule; +} + +const Template: Story = (args) => { + return ( + + ); +}; + +export const Default = Template.bind({}); + +Default.args = { + finalDiffableRule: mockEqlRule({ + event_category_override: 'event.action', + }), +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/event_category_override/event_category_override.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/event_category_override/event_category_override.tsx new file mode 100644 index 0000000000000..910e639049f96 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/event_category_override/event_category_override.tsx @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiDescriptionList, EuiText } from '@elastic/eui'; +import * as descriptionStepI18n from '../../../../../../../rule_creation_ui/components/description_step/translations'; +import type { EventCategoryOverride as EventCategoryOverrideType } from '../../../../../../../../../common/api/detection_engine'; + +interface EventCategoryOverrideReadOnlyProps { + eventCategoryOverride?: EventCategoryOverrideType; +} + +export function EventCategoryOverrideReadOnly({ + eventCategoryOverride, +}: EventCategoryOverrideReadOnlyProps) { + if (!eventCategoryOverride) { + return null; + } + + return ( + , + }, + ]} + /> + ); +} + +interface EventCategoryOverrideProps { + eventCategoryOverride: EventCategoryOverrideType; +} + +function EventCategoryOverride({ eventCategoryOverride }: EventCategoryOverrideProps) { + return {eventCategoryOverride}; +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/false_positives/false_positives.stories.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/false_positives/false_positives.stories.tsx new file mode 100644 index 0000000000000..60d8a34616652 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/false_positives/false_positives.stories.tsx @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import type { Story } from '@storybook/react'; +import { FalsePositivesReadOnly } from './false_positives'; +import { FieldReadOnly } from '../../field_readonly'; +import type { DiffableRule } from '../../../../../../../../../common/api/detection_engine'; +import { mockCustomQueryRule } from '../../storybook/mocks'; + +export default { + component: FalsePositivesReadOnly, + title: 'Rule Management/Prebuilt Rules/Upgrade Flyout/ThreeWayDiff/FieldReadOnly/false_positives', +}; + +interface TemplateProps { + finalDiffableRule: DiffableRule; +} + +const Template: Story = (args) => { + return ; +}; + +export const Default = Template.bind({}); + +Default.args = { + finalDiffableRule: mockCustomQueryRule({ + false_positives: [ + 'WAF rules or rule groups may be deleted by a system or network administrator. Verify whether the user identity, user agent, and/or hostname should be making changes in your environment. Rule deletions by unfamiliar users or hosts should be investigated. If known behavior is causing false positives, it can be exempted from the rule.', + 'Uncommon user command activity can be due to an engineer logging onto a server instance in order to perform manual troubleshooting or reconfiguration.', + ], + }), +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/false_positives/false_positives.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/false_positives/false_positives.tsx new file mode 100644 index 0000000000000..f026609b6c850 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/false_positives/false_positives.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiDescriptionList } from '@elastic/eui'; +import * as ruleDetailsI18n from '../../../../translations'; +import type { RuleFalsePositiveArray } from '../../../../../../../../../common/api/detection_engine'; +import { FalsePositives } from '../../../../rule_about_section'; + +interface FalsePositivesReadOnlyProps { + falsePositives: RuleFalsePositiveArray; +} + +export function FalsePositivesReadOnly({ falsePositives }: FalsePositivesReadOnlyProps) { + return ( + , + }, + ]} + /> + ); +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/history_window_start/history_window_start.stories.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/history_window_start/history_window_start.stories.tsx new file mode 100644 index 0000000000000..c84868e97ee0c --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/history_window_start/history_window_start.stories.tsx @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import type { Story } from '@storybook/react'; +import { HistoryWindowStartReadOnly } from './history_window_start'; +import { FieldReadOnly } from '../../field_readonly'; +import type { DiffableRule } from '../../../../../../../../../common/api/detection_engine'; +import { mockNewTermsRule } from '../../storybook/mocks'; + +export default { + component: HistoryWindowStartReadOnly, + title: + 'Rule Management/Prebuilt Rules/Upgrade Flyout/ThreeWayDiff/FieldReadOnly/history_window_start', +}; + +interface TemplateProps { + finalDiffableRule: DiffableRule; +} + +const Template: Story = (args) => { + return ( + + ); +}; + +export const Default = Template.bind({}); + +Default.args = { + finalDiffableRule: mockNewTermsRule({ + history_window_start: 'now-14d', + }), +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/history_window_start/history_window_start.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/history_window_start/history_window_start.tsx new file mode 100644 index 0000000000000..c6bd8ebf7b972 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/history_window_start/history_window_start.tsx @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiDescriptionList } from '@elastic/eui'; +import * as ruleDetailsI18n from '../../../../translations'; +import type { HistoryWindowStart as HistoryWindowStartType } from '../../../../../../../../../common/api/detection_engine'; +import { HistoryWindowSize } from '../../../../rule_definition_section'; + +interface HistoryWindowStartReadOnlyProps { + historyWindowStart: HistoryWindowStartType; +} + +export function HistoryWindowStartReadOnly({ + historyWindowStart, +}: HistoryWindowStartReadOnlyProps) { + return ( + , + }, + ]} + /> + ); +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/investigation_fields/investigation_fields.stories.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/investigation_fields/investigation_fields.stories.tsx new file mode 100644 index 0000000000000..e73b01a719f8a --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/investigation_fields/investigation_fields.stories.tsx @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import type { Story } from '@storybook/react'; +import { InvestigationFieldsReadOnly } from './investigation_fields'; +import { FieldReadOnly } from '../../field_readonly'; +import type { DiffableRule } from '../../../../../../../../../common/api/detection_engine'; +import { mockCustomQueryRule } from '../../storybook/mocks'; + +export default { + component: InvestigationFieldsReadOnly, + title: + 'Rule Management/Prebuilt Rules/Upgrade Flyout/ThreeWayDiff/FieldReadOnly/investigation_fields', +}; + +interface TemplateProps { + finalDiffableRule: DiffableRule; +} + +const Template: Story = (args) => { + return ( + + ); +}; + +export const Default = Template.bind({}); + +Default.args = { + finalDiffableRule: mockCustomQueryRule({ + investigation_fields: { + field_names: ['host.name', 'source.ip'], + }, + }), +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/investigation_fields/investigation_fields.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/investigation_fields/investigation_fields.tsx new file mode 100644 index 0000000000000..b567b32ada533 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/investigation_fields/investigation_fields.tsx @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiDescriptionList } from '@elastic/eui'; +import * as ruleDetailsI18n from '../../../../translations'; +import type { InvestigationFields } from '../../../../../../../../../common/api/detection_engine'; +import { InvestigationFields as InvestigationFieldsComponent } from '../../../../rule_about_section'; + +interface InvestigationFieldsReadOnlyProps { + investigationFields?: InvestigationFields; +} + +export function InvestigationFieldsReadOnly({ + investigationFields, +}: InvestigationFieldsReadOnlyProps) { + if (!investigationFields || !investigationFields.field_names.length) { + return null; + } + + return ( + + ), + }, + ]} + /> + ); +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/kql_query/kql_query.stories.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/kql_query/kql_query.stories.tsx index 2b4844ceac5d5..4fd102fc0627f 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/kql_query/kql_query.stories.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/kql_query/kql_query.stories.tsx @@ -28,12 +28,12 @@ export default { interface TemplateProps { finalDiffableRule: DiffableRule; - kibanaServicesMock?: Record; + kibanaServicesOverrides?: Record; } const Template: Story = (args) => { return ( - + ); @@ -46,7 +46,7 @@ InlineKqlQueryWithIndexPatterns.args = { kql_query: inlineKqlQuery, data_source: dataSourceWithIndexPatterns, }), - kibanaServicesMock: { + kibanaServicesOverrides: { data: { dataViews: { create: async () => mockDataView(), @@ -62,7 +62,7 @@ InlineKqlQueryWithDataView.args = { kql_query: inlineKqlQuery, data_source: dataSourceWithDataView, }), - kibanaServicesMock: { + kibanaServicesOverrides: { data: { dataViews: { get: async () => mockDataView(), @@ -81,7 +81,7 @@ InlineKqlQueryWithoutDataSource.args = { finalDiffableRule: mockCustomQueryRule({ kql_query: inlineKqlQuery, }), - kibanaServicesMock: { + kibanaServicesOverrides: { data: { dataViews: { create: async () => mockDataView(), @@ -98,7 +98,7 @@ SavedKqlQueryWithIndexPatterns.args = { data_source: dataSourceWithIndexPatterns, type: 'saved_query', }), - kibanaServicesMock: { + kibanaServicesOverrides: { data: { dataViews: { create: async () => mockDataView(), @@ -118,7 +118,7 @@ SavedKqlQueryWithDataView.args = { data_source: dataSourceWithDataView, type: 'saved_query', }), - kibanaServicesMock: { + kibanaServicesOverrides: { data: { dataViews: { get: async () => mockDataView(), diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/license/license.stories.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/license/license.stories.tsx new file mode 100644 index 0000000000000..044564acd91d8 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/license/license.stories.tsx @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import type { Story } from '@storybook/react'; +import { LicenseReadOnly } from './license'; +import { FieldReadOnly } from '../../field_readonly'; +import type { DiffableRule } from '../../../../../../../../../common/api/detection_engine'; +import { mockCustomQueryRule } from '../../storybook/mocks'; + +export default { + component: LicenseReadOnly, + title: 'Rule Management/Prebuilt Rules/Upgrade Flyout/ThreeWayDiff/FieldReadOnly/license', +}; + +interface TemplateProps { + finalDiffableRule: DiffableRule; +} + +const Template: Story = (args) => { + return ; +}; + +export const Default = Template.bind({}); + +Default.args = { + finalDiffableRule: mockCustomQueryRule({ + license: 'Elastic License 2.0', + }), +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/license/license.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/license/license.tsx new file mode 100644 index 0000000000000..18032f66ab81d --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/license/license.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiDescriptionList } from '@elastic/eui'; +import * as ruleDetailsI18n from '../../../../translations'; +import type { RuleLicense } from '../../../../../../../../../common/api/detection_engine'; +import { License } from '../../../../rule_about_section'; + +interface LicenseReadOnlyProps { + license: RuleLicense; +} + +export function LicenseReadOnly({ license }: LicenseReadOnlyProps) { + return ( + , + }, + ]} + /> + ); +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/max_signals/max_signals.stories.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/max_signals/max_signals.stories.tsx new file mode 100644 index 0000000000000..d6f369ff47ce6 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/max_signals/max_signals.stories.tsx @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import type { Story } from '@storybook/react'; +import { MaxSignalsReadOnly } from './max_signals'; +import { FieldReadOnly } from '../../field_readonly'; +import type { DiffableRule } from '../../../../../../../../../common/api/detection_engine'; +import { mockCustomQueryRule } from '../../storybook/mocks'; + +export default { + component: MaxSignalsReadOnly, + title: 'Rule Management/Prebuilt Rules/Upgrade Flyout/ThreeWayDiff/FieldReadOnly/max_signals', +}; + +interface TemplateProps { + finalDiffableRule: DiffableRule; +} + +const Template: Story = (args) => { + return ; +}; + +export const Default = Template.bind({}); + +Default.args = { + finalDiffableRule: mockCustomQueryRule({ + max_signals: 100, + }), +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/max_signals/max_signals.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/max_signals/max_signals.tsx new file mode 100644 index 0000000000000..d0bda317a416a --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/max_signals/max_signals.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiDescriptionList } from '@elastic/eui'; +import * as ruleDetailsI18n from '../../../../translations'; +import type { MaxSignals as MaxSignalsType } from '../../../../../../../../../common/api/detection_engine'; +import { MaxSignals } from '../../../../rule_about_section'; + +interface MaxSignalsReadOnlyProps { + maxSignals: MaxSignalsType; +} + +export function MaxSignalsReadOnly({ maxSignals }: MaxSignalsReadOnlyProps) { + return ( + , + }, + ]} + /> + ); +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/new_terms_fields/new_terms_fields.stories.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/new_terms_fields/new_terms_fields.stories.tsx new file mode 100644 index 0000000000000..8f77a747fa06c --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/new_terms_fields/new_terms_fields.stories.tsx @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import type { Story } from '@storybook/react'; +import { NewTermsFieldsReadOnly } from './new_terms_fields'; +import { FieldReadOnly } from '../../field_readonly'; +import type { DiffableRule } from '../../../../../../../../../common/api/detection_engine'; +import { mockNewTermsRule } from '../../storybook/mocks'; + +export default { + component: NewTermsFieldsReadOnly, + title: + 'Rule Management/Prebuilt Rules/Upgrade Flyout/ThreeWayDiff/FieldReadOnly/new_terms_fields', +}; + +interface TemplateProps { + finalDiffableRule: DiffableRule; +} + +const Template: Story = (args) => { + return ; +}; + +export const Default = Template.bind({}); + +Default.args = { + finalDiffableRule: mockNewTermsRule({ + new_terms_fields: ['user.name', 'source.ip'], + }), +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/new_terms_fields/new_terms_fields.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/new_terms_fields/new_terms_fields.tsx new file mode 100644 index 0000000000000..b77f7b0736482 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/new_terms_fields/new_terms_fields.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiDescriptionList } from '@elastic/eui'; +import * as ruleDetailsI18n from '../../../../translations'; +import type { NewTermsFields as NewTermsFieldsType } from '../../../../../../../../../common/api/detection_engine'; +import { NewTermsFields } from '../../../../rule_definition_section'; + +interface NewTermsFieldsReadOnlyProps { + newTermsFields: NewTermsFieldsType; +} + +export function NewTermsFieldsReadOnly({ newTermsFields }: NewTermsFieldsReadOnlyProps) { + return ( + , + }, + ]} + /> + ); +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/note/note.stories.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/note/note.stories.tsx new file mode 100644 index 0000000000000..501faeb1cfbad --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/note/note.stories.tsx @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import type { Story } from '@storybook/react'; +import { NoteReadOnly } from './note'; +import { FieldReadOnly } from '../../field_readonly'; +import type { DiffableRule } from '../../../../../../../../../common/api/detection_engine'; +import { mockCustomQueryRule } from '../../storybook/mocks'; + +export default { + component: NoteReadOnly, + title: 'Rule Management/Prebuilt Rules/Upgrade Flyout/ThreeWayDiff/FieldReadOnly/note', +}; + +interface TemplateProps { + finalDiffableRule: DiffableRule; +} + +const Template: Story = (args) => { + return ; +}; + +export const Default = Template.bind({}); + +Default.args = { + finalDiffableRule: mockCustomQueryRule({ + note: '## Triage and analysis\n\n### Investigating Unusual Network Activity\nDetection alerts from this rule indicate the presence of network activity from a Linux process for which network activity is rare and unusual. Here are some possible avenues of investigation:\n- Consider the IP addresses and ports. Are these used by normal but infrequent network workflows? Are they expected or unexpected?\n- If the destination IP address is remote or external, does it associate with an expected domain, organization or geography? Note: avoid interacting directly with suspected malicious IP addresses.\n- Consider the user as identified by the username field. Is this network activity part of an expected workflow for the user who ran the program?\n- Examine the history of execution. If this process only manifested recently, it might be part of a new software package. If it has a consistent cadence (for example if it runs monthly or quarterly), it might be part of a monthly or quarterly business or maintenance process.\n- Examine the process arguments, title and working directory. These may provide indications as to the source of the program or the nature of the tasks it is performing.', + }), +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/note/note.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/note/note.tsx new file mode 100644 index 0000000000000..37184df117110 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/note/note.tsx @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiDescriptionList } from '@elastic/eui'; +import * as i18n from './translations'; +import type { InvestigationGuide } from '../../../../../../../../../common/api/detection_engine'; +import { MarkdownRenderer } from '../../../../../../../../common/components/markdown_editor'; + +interface NoteReadOnlyProps { + note: InvestigationGuide; +} + +export function NoteReadOnly({ note }: NoteReadOnlyProps) { + return ( + , + }, + ]} + /> + ); +} + +interface NoteProps { + note: InvestigationGuide; +} + +function Note({ note }: NoteProps) { + return {note}; +} diff --git a/x-pack/plugins/transform/public/__mocks__/shared_imports.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/note/translations.ts similarity index 51% rename from x-pack/plugins/transform/public/__mocks__/shared_imports.ts rename to x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/note/translations.ts index ac4b7fe49a38c..7a36a7d117d8d 100644 --- a/x-pack/plugins/transform/public/__mocks__/shared_imports.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/note/translations.ts @@ -5,10 +5,11 @@ * 2.0. */ -// actual mocks -export const expandLiteralStrings = jest.fn(); -export const XJsonMode = jest.fn(); -export const getSavedSearch = jest.fn(); +import { i18n } from '@kbn/i18n'; -// just passing through the reimports -export { getMlSharedImports } from '@kbn/ml-plugin/public'; +export const NOTE_LABEL = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.upgradeRules.fieldReadOnly.noteLabel', + { + defaultMessage: 'Investigation guide', + } +); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/references/references.stories.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/references/references.stories.tsx new file mode 100644 index 0000000000000..561d5b36cf4f3 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/references/references.stories.tsx @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import type { Story } from '@storybook/react'; +import { ReferencesReadOnly } from './references'; +import { FieldReadOnly } from '../../field_readonly'; +import type { DiffableRule } from '../../../../../../../../../common/api/detection_engine'; +import { mockCustomQueryRule } from '../../storybook/mocks'; + +export default { + component: ReferencesReadOnly, + title: 'Rule Management/Prebuilt Rules/Upgrade Flyout/ThreeWayDiff/FieldReadOnly/references', +}; + +interface TemplateProps { + finalDiffableRule: DiffableRule; +} + +const Template: Story = (args) => { + return ; +}; + +export const Default = Template.bind({}); + +Default.args = { + finalDiffableRule: mockCustomQueryRule({ + references: [ + 'https://www.elastic.co/guide/en/security/current/prebuilt-ml-jobs.html', + 'https://docs.elastic.co/en/integrations/beaconing', + 'https://www.elastic.co/security-labs/identifying-beaconing-malware-using-elastic', + ], + }), +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/references/references.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/references/references.tsx new file mode 100644 index 0000000000000..c98c5a53bb2e8 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/references/references.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiDescriptionList } from '@elastic/eui'; +import * as ruleDetailsI18n from '../../../../translations'; +import type { RuleReferenceArray } from '../../../../../../../../../common/api/detection_engine'; +import { References } from '../../../../rule_about_section'; + +interface ReferencesReadOnlyProps { + references: RuleReferenceArray; +} + +export function ReferencesReadOnly({ references }: ReferencesReadOnlyProps) { + return ( + , + }, + ]} + /> + ); +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/risk_score/risk_score.stories.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/risk_score/risk_score.stories.tsx new file mode 100644 index 0000000000000..c0be0fb6190f6 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/risk_score/risk_score.stories.tsx @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import type { Story } from '@storybook/react'; +import { RiskScoreReadOnly } from './risk_score'; +import { FieldReadOnly } from '../../field_readonly'; +import type { DiffableRule } from '../../../../../../../../../common/api/detection_engine'; +import { mockCustomQueryRule } from '../../storybook/mocks'; + +export default { + component: RiskScoreReadOnly, + title: 'Rule Management/Prebuilt Rules/Upgrade Flyout/ThreeWayDiff/FieldReadOnly/risk_score', +}; + +interface TemplateProps { + finalDiffableRule: DiffableRule; +} + +const Template: Story = (args) => { + return ; +}; + +export const Default = Template.bind({}); + +Default.args = { + finalDiffableRule: mockCustomQueryRule({ + risk_score: 96, + }), +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/risk_score/risk_score.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/risk_score/risk_score.tsx new file mode 100644 index 0000000000000..ef6ddbd7d8217 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/risk_score/risk_score.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiDescriptionList } from '@elastic/eui'; +import * as ruleDetailsI18n from '../../../../translations'; +import type { RiskScore as RiskScoreType } from '../../../../../../../../../common/api/detection_engine'; +import { RiskScore } from '../../../../rule_about_section'; + +interface RiskScoreReadOnlyProps { + riskScore: RiskScoreType; +} + +export function RiskScoreReadOnly({ riskScore }: RiskScoreReadOnlyProps) { + return ( + , + }, + ]} + /> + ); +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/rule_name_override/rule_name_override.stories.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/rule_name_override/rule_name_override.stories.tsx new file mode 100644 index 0000000000000..fa46e313fd664 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/rule_name_override/rule_name_override.stories.tsx @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import type { Story } from '@storybook/react'; +import { RuleNameOverrideReadOnly } from './rule_name_override'; +import { FieldReadOnly } from '../../field_readonly'; +import type { DiffableRule } from '../../../../../../../../../common/api/detection_engine'; +import { mockCustomQueryRule } from '../../storybook/mocks'; + +export default { + component: RuleNameOverrideReadOnly, + title: + 'Rule Management/Prebuilt Rules/Upgrade Flyout/ThreeWayDiff/FieldReadOnly/rule_name_override', +}; + +interface TemplateProps { + finalDiffableRule: DiffableRule; +} + +const Template: Story = (args) => { + return ( + + ); +}; + +export const Default = Template.bind({}); + +Default.args = { + finalDiffableRule: mockCustomQueryRule({ + rule_name_override: { + field_name: 'event.action', + }, + }), +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/rule_name_override/rule_name_override.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/rule_name_override/rule_name_override.tsx new file mode 100644 index 0000000000000..9090138ab91ee --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/rule_name_override/rule_name_override.tsx @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiDescriptionList } from '@elastic/eui'; +import * as ruleDetailsI18n from '../../../../translations'; +import type { RuleNameOverrideObject } from '../../../../../../../../../common/api/detection_engine'; +import { RuleNameOverride } from '../../../../rule_about_section'; + +interface RuleNameOverrideReadOnlyProps { + ruleNameOverride?: RuleNameOverrideObject; +} + +export function RuleNameOverrideReadOnly({ ruleNameOverride }: RuleNameOverrideReadOnlyProps) { + if (!ruleNameOverride) { + return null; + } + + return ( + , + }, + ]} + /> + ); +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/rule_schedule/rule_schedule.stories.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/rule_schedule/rule_schedule.stories.tsx new file mode 100644 index 0000000000000..9e6be8a3cbe82 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/rule_schedule/rule_schedule.stories.tsx @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import type { Story } from '@storybook/react'; +import { RuleScheduleReadOnly } from './rule_schedule'; +import { FieldReadOnly } from '../../field_readonly'; +import type { DiffableRule } from '../../../../../../../../../common/api/detection_engine'; +import { mockCustomQueryRule } from '../../storybook/mocks'; + +export default { + component: RuleScheduleReadOnly, + title: 'Rule Management/Prebuilt Rules/Upgrade Flyout/ThreeWayDiff/FieldReadOnly/rule_schedule', +}; + +interface TemplateProps { + finalDiffableRule: DiffableRule; +} + +const Template: Story = (args) => { + return ; +}; + +export const Default = Template.bind({}); + +Default.args = { + finalDiffableRule: mockCustomQueryRule({ + rule_schedule: { + interval: '5m', + lookback: '60s', + }, + }), +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/rule_schedule/rule_schedule.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/rule_schedule/rule_schedule.tsx new file mode 100644 index 0000000000000..24d0e5d6e05f1 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/rule_schedule/rule_schedule.tsx @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiDescriptionList } from '@elastic/eui'; +import { parseDuration } from '@kbn/alerting-plugin/common'; +import * as i18n from '../../../../translations'; +import type { RuleSchedule } from '../../../../../../../../../common/api/detection_engine'; +import { AccessibleTimeValue } from '../../../../rule_schedule_section'; +import { secondsToDurationString } from '../../../../../../../../detections/pages/detection_engine/rules/helpers'; + +interface RuleScheduleReadOnlyProps { + ruleSchedule: RuleSchedule; +} + +export function RuleScheduleReadOnly({ ruleSchedule }: RuleScheduleReadOnlyProps) { + const lookbackSeconds = parseDuration(ruleSchedule.lookback) / 1000; + const lookbackHumanized = secondsToDurationString(lookbackSeconds); + + return ( + , + }, + { + title: i18n.FROM_FIELD_LABEL, + description: , + }, + ]} + /> + ); +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/setup/setup.stories.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/setup/setup.stories.tsx new file mode 100644 index 0000000000000..f610cbf9eafbb --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/setup/setup.stories.tsx @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import type { Story } from '@storybook/react'; +import { SetupReadOnly } from './setup'; +import { FieldReadOnly } from '../../field_readonly'; +import type { DiffableRule } from '../../../../../../../../../common/api/detection_engine'; +import { mockCustomQueryRule } from '../../storybook/mocks'; + +export default { + component: SetupReadOnly, + title: 'Rule Management/Prebuilt Rules/Upgrade Flyout/ThreeWayDiff/FieldReadOnly/setup', +}; + +interface TemplateProps { + finalDiffableRule: DiffableRule; +} + +const Template: Story = (args) => { + return ; +}; + +export const Default = Template.bind({}); + +Default.args = { + finalDiffableRule: mockCustomQueryRule({ + setup: + 'The \'PowerShell Script Block Logging\' logging policy must be enabled.\nSteps to implement the logging policy with Advanced Audit Configuration:\n\n```\nComputer Configuration >\nAdministrative Templates >\nWindows PowerShell >\nTurn on PowerShell Script Block Logging (Enable)\n```\n\nSteps to implement the logging policy via registry:\n\n```\nreg add "hklm\\SOFTWARE\\Policies\\Microsoft\\Windows\\PowerShell\\ScriptBlockLogging" /v EnableScriptBlockLogging /t REG_DWORD /d 1\n```', + }), +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/setup/setup.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/setup/setup.tsx new file mode 100644 index 0000000000000..a4ed7ba9539f1 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/setup/setup.tsx @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiDescriptionList } from '@elastic/eui'; +import * as ruleDetailsI18n from '../../../../translations'; +import type { SetupGuide } from '../../../../../../../../../common/api/detection_engine'; +import { MarkdownRenderer } from '../../../../../../../../common/components/markdown_editor'; + +interface SetupReadOnlyProps { + setup: SetupGuide; +} + +export function SetupReadOnly({ setup }: SetupReadOnlyProps) { + return ( + , + }, + ]} + /> + ); +} + +interface SetupProps { + setup: SetupGuide; +} + +function Setup({ setup }: SetupProps) { + return {setup}; +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/severity/severity.stories.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/severity/severity.stories.tsx new file mode 100644 index 0000000000000..51956bb27fa0e --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/severity/severity.stories.tsx @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import type { Story } from '@storybook/react'; +import { SeverityReadOnly } from './severity'; +import { FieldReadOnly } from '../../field_readonly'; +import type { DiffableRule } from '../../../../../../../../../common/api/detection_engine'; +import { mockCustomQueryRule } from '../../storybook/mocks'; + +export default { + component: SeverityReadOnly, + title: 'Rule Management/Prebuilt Rules/Upgrade Flyout/ThreeWayDiff/FieldReadOnly/severity', +}; + +interface TemplateProps { + finalDiffableRule: DiffableRule; +} + +const Template: Story = (args) => { + return ; +}; + +export const Default = Template.bind({}); + +Default.args = { + finalDiffableRule: mockCustomQueryRule({ + severity: 'high', + }), +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/severity/severity.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/severity/severity.tsx new file mode 100644 index 0000000000000..71ab7bf07d793 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/severity/severity.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiDescriptionList } from '@elastic/eui'; +import * as ruleDetailsI18n from '../../../../translations'; +import type { Severity } from '../../../../../../../../../common/api/detection_engine'; +import { SeverityBadge } from '../../../../../../../../common/components/severity_badge'; + +interface SeverityReadOnlyProps { + severity: Severity; +} + +export function SeverityReadOnly({ severity }: SeverityReadOnlyProps) { + return ( + , + }, + ]} + /> + ); +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/threat_language/threat_language.stories.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/threat_language/threat_language.stories.tsx new file mode 100644 index 0000000000000..0e3408aae043e --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/threat_language/threat_language.stories.tsx @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import type { Story } from '@storybook/react'; +import { ThreatLanguageReadOnly } from './threat_language'; +import { FieldReadOnly } from '../../field_readonly'; +import type { DiffableRule } from '../../../../../../../../../common/api/detection_engine'; +import { mockThreatMatchRule } from '../../storybook/mocks'; + +export default { + component: ThreatLanguageReadOnly, + title: 'Rule Management/Prebuilt Rules/Upgrade Flyout/ThreeWayDiff/FieldReadOnly/threat_language', +}; + +interface TemplateProps { + finalDiffableRule: DiffableRule; +} + +const Template: Story = (args) => { + return ; +}; + +export const Default = Template.bind({}); + +Default.args = { + finalDiffableRule: mockThreatMatchRule({ + threat_language: 'lucene', + }), +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/threat_language/threat_language.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/threat_language/threat_language.tsx new file mode 100644 index 0000000000000..df43373783b1b --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/threat_language/threat_language.tsx @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiDescriptionList } from '@elastic/eui'; +import * as ruleDetailsI18n from '../../../../translations'; +import type { KqlQueryLanguage } from '../../../../../../../../../common/api/detection_engine'; +import { getQueryLanguageLabel } from '../../../../helpers'; + +interface ThreatLanguageReadOnlyProps { + threatLanguage?: KqlQueryLanguage; +} + +export function ThreatLanguageReadOnly({ threatLanguage }: ThreatLanguageReadOnlyProps) { + if (!threatLanguage) { + return null; + } + + return ( + + ); +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/threat_query/threat_query.stories.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/threat_query/threat_query.stories.tsx index 28b4cd65ba78b..bbc5b19d7e66a 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/threat_query/threat_query.stories.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/threat_query/threat_query.stories.tsx @@ -26,12 +26,12 @@ export default { interface TemplateProps { finalDiffableRule: DiffableRule; - kibanaServicesMock?: Record; + kibanaServicesOverrides?: Record; } const Template: Story = (args) => { return ( - + ); @@ -44,7 +44,7 @@ ThreatQueryWithIndexPatterns.args = { threat_query: inlineKqlQuery, data_source: dataSourceWithIndexPatterns, }), - kibanaServicesMock: { + kibanaServicesOverrides: { data: { dataViews: { create: async () => mockDataView(), @@ -60,7 +60,7 @@ ThreatQueryWithDataView.args = { threat_query: inlineKqlQuery, data_source: dataSourceWithDataView, }), - kibanaServicesMock: { + kibanaServicesOverrides: { data: { dataViews: { get: async () => mockDataView(), diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/threshold/threshold.stories.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/threshold/threshold.stories.tsx new file mode 100644 index 0000000000000..8dfee48ba7d23 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/threshold/threshold.stories.tsx @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import type { Story } from '@storybook/react'; +import { ThresholdReadOnly } from './threshold'; +import { FieldReadOnly } from '../../field_readonly'; +import type { DiffableRule } from '../../../../../../../../../common/api/detection_engine'; +import { mockThresholdRule } from '../../storybook/mocks'; + +export default { + component: ThresholdReadOnly, + title: 'Rule Management/Prebuilt Rules/Upgrade Flyout/ThreeWayDiff/FieldReadOnly/threshold', +}; + +interface TemplateProps { + finalDiffableRule: DiffableRule; +} + +const Template: Story = (args) => { + return ; +}; + +export const Default = Template.bind({}); + +Default.args = { + finalDiffableRule: mockThresholdRule({ + threshold: { + field: ['Responses.process.pid'], + value: 100, + cardinality: [{ field: 'host.id', value: 2 }], + }, + }), +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/threshold/threshold.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/threshold/threshold.tsx new file mode 100644 index 0000000000000..19e2b824a9ce5 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/threshold/threshold.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiDescriptionList } from '@elastic/eui'; +import * as ruleDetailsI18n from '../../../../translations'; +import type { Threshold as ThresholdType } from '../../../../../../../../../common/api/detection_engine'; +import { Threshold } from '../../../../rule_definition_section'; + +interface ThresholdReadOnlyProps { + threshold: ThresholdType; +} + +export function ThresholdReadOnly({ threshold }: ThresholdReadOnlyProps) { + return ( + , + }, + ]} + /> + ); +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/tiebreaker_field/tiebreaker_field.stories.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/tiebreaker_field/tiebreaker_field.stories.tsx new file mode 100644 index 0000000000000..2a41b6c928963 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/tiebreaker_field/tiebreaker_field.stories.tsx @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import type { Story } from '@storybook/react'; +import { TiebreakerFieldReadOnly } from './tiebreaker_field'; +import { FieldReadOnly } from '../../field_readonly'; +import type { DiffableRule } from '../../../../../../../../../common/api/detection_engine'; +import { mockEqlRule } from '../../storybook/mocks'; + +export default { + component: TiebreakerFieldReadOnly, + title: + 'Rule Management/Prebuilt Rules/Upgrade Flyout/ThreeWayDiff/FieldReadOnly/tiebreaker_field', +}; + +interface TemplateProps { + finalDiffableRule: DiffableRule; +} + +const Template: Story = (args) => { + return ; +}; + +export const Default = Template.bind({}); + +Default.args = { + finalDiffableRule: mockEqlRule({ + tiebreaker_field: 'process.name', + }), +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/tiebreaker_field/tiebreaker_field.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/tiebreaker_field/tiebreaker_field.tsx new file mode 100644 index 0000000000000..10e52240748c7 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/tiebreaker_field/tiebreaker_field.tsx @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiDescriptionList, EuiText } from '@elastic/eui'; +import * as descriptionStepI18n from '../../../../../../../rule_creation_ui/components/description_step/translations'; +import type { TiebreakerField as TiebreakerFieldType } from '../../../../../../../../../common/api/detection_engine'; + +interface TiebreakerFieldReadOnlyProps { + tiebreakerField?: TiebreakerFieldType; +} + +export function TiebreakerFieldReadOnly({ tiebreakerField }: TiebreakerFieldReadOnlyProps) { + if (!tiebreakerField) { + return null; + } + + return ( + , + }, + ]} + /> + ); +} + +interface TiebreakerFieldProps { + tiebreakerField: TiebreakerFieldType; +} + +function TiebreakerField({ tiebreakerField }: TiebreakerFieldProps) { + return {tiebreakerField}; +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/timeline_template/timeline_template.stories.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/timeline_template/timeline_template.stories.tsx new file mode 100644 index 0000000000000..e8646a562dadd --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/timeline_template/timeline_template.stories.tsx @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import type { Story } from '@storybook/react'; +import { TimelineTemplateReadOnly } from './timeline_template'; +import { FieldReadOnly } from '../../field_readonly'; +import type { DiffableRule } from '../../../../../../../../../common/api/detection_engine'; +import { mockCustomQueryRule } from '../../storybook/mocks'; + +export default { + component: TimelineTemplateReadOnly, + title: + 'Rule Management/Prebuilt Rules/Upgrade Flyout/ThreeWayDiff/FieldReadOnly/timeline_template', +}; + +interface TemplateProps { + finalDiffableRule: DiffableRule; +} + +const Template: Story = (args) => { + return ; +}; + +export const Default = Template.bind({}); + +Default.args = { + finalDiffableRule: mockCustomQueryRule({ + timeline_template: { + timeline_title: 'Alerts Involving a Single User Timeline', + timeline_id: 'some-timeline-id-123', + }, + }), +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/timeline_template/timeline_template.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/timeline_template/timeline_template.tsx new file mode 100644 index 0000000000000..d6cc75cec5843 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/timeline_template/timeline_template.tsx @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiDescriptionList } from '@elastic/eui'; +import * as ruleDetailsI18n from '../../../../translations'; +import type { TimelineTemplateReference } from '../../../../../../../../../common/api/detection_engine'; +import { TimelineTitle } from '../../../../rule_definition_section'; + +interface TimelineTemplateReadOnlyProps { + timelineTemplate?: TimelineTemplateReference; +} + +export function TimelineTemplateReadOnly({ timelineTemplate }: TimelineTemplateReadOnlyProps) { + if (!timelineTemplate) { + return null; + } + + return ( + , + }, + ]} + /> + ); +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/timestamp_field/timestamp_field.stories.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/timestamp_field/timestamp_field.stories.tsx new file mode 100644 index 0000000000000..5d6b6c0a7bc3a --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/timestamp_field/timestamp_field.stories.tsx @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import type { Story } from '@storybook/react'; +import { TimestampFieldReadOnly } from './timestamp_field'; +import { FieldReadOnly } from '../../field_readonly'; +import type { DiffableRule } from '../../../../../../../../../common/api/detection_engine'; +import { mockEqlRule } from '../../storybook/mocks'; + +export default { + component: TimestampFieldReadOnly, + title: 'Rule Management/Prebuilt Rules/Upgrade Flyout/ThreeWayDiff/FieldReadOnly/timestamp_field', +}; + +interface TemplateProps { + finalDiffableRule: DiffableRule; +} + +const Template: Story = (args) => { + return ; +}; + +export const Default = Template.bind({}); + +Default.args = { + finalDiffableRule: mockEqlRule({ + timestamp_field: 'event.created', + }), +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/timestamp_field/timestamp_field.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/timestamp_field/timestamp_field.tsx new file mode 100644 index 0000000000000..cd27bfde3db60 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/timestamp_field/timestamp_field.tsx @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiDescriptionList, EuiText } from '@elastic/eui'; +import * as descriptionStepI18n from '../../../../../../../rule_creation_ui/components/description_step/translations'; +import type { TimestampField as TimestampFieldType } from '../../../../../../../../../common/api/detection_engine'; + +interface TimestampFieldReadOnlyProps { + timestampField?: TimestampFieldType; +} + +export function TimestampFieldReadOnly({ timestampField }: TimestampFieldReadOnlyProps) { + if (!timestampField) { + return null; + } + + return ( + , + }, + ]} + /> + ); +} + +interface TimestampFieldProps { + timestampField: TimestampFieldType; +} + +function TimestampField({ timestampField }: TimestampFieldProps) { + return {timestampField}; +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/timestamp_override/timestamp_override.stories.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/timestamp_override/timestamp_override.stories.tsx new file mode 100644 index 0000000000000..eaba3bda0c2e7 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/timestamp_override/timestamp_override.stories.tsx @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import type { Story } from '@storybook/react'; +import { TimestampOverrideReadOnly } from './timestamp_override'; +import { FieldReadOnly } from '../../field_readonly'; +import type { DiffableRule } from '../../../../../../../../../common/api/detection_engine'; +import { mockCustomQueryRule } from '../../storybook/mocks'; + +export default { + component: TimestampOverrideReadOnly, + title: + 'Rule Management/Prebuilt Rules/Upgrade Flyout/ThreeWayDiff/FieldReadOnly/timestamp_override', +}; + +interface TemplateProps { + finalDiffableRule: DiffableRule; +} + +const Template: Story = (args) => { + return ( + + ); +}; + +export const Default = Template.bind({}); + +Default.args = { + finalDiffableRule: mockCustomQueryRule({ + timestamp_override: { + field_name: 'event.ingested', + fallback_disabled: true, + }, + }), +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/timestamp_override/timestamp_override.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/timestamp_override/timestamp_override.tsx new file mode 100644 index 0000000000000..5d4c6ba3e1d2b --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/timestamp_override/timestamp_override.tsx @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiDescriptionList } from '@elastic/eui'; +import * as ruleDetailsI18n from '../../../../translations'; +import type { TimestampOverrideObject } from '../../../../../../../../../common/api/detection_engine'; +import { TimestampOverride } from '../../../../rule_about_section'; + +interface TimestampOverrideReadOnlyProps { + timestampOverride?: TimestampOverrideObject; +} + +export function TimestampOverrideReadOnly({ timestampOverride }: TimestampOverrideReadOnlyProps) { + if (!timestampOverride) { + return null; + } + + return ( + , + }, + ]} + /> + ); +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/type/type.stories.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/type/type.stories.tsx new file mode 100644 index 0000000000000..e3f901c958788 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/type/type.stories.tsx @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import type { Story } from '@storybook/react'; +import { TypeReadOnly } from './type'; +import { FieldReadOnly } from '../../field_readonly'; +import type { DiffableRule } from '../../../../../../../../../common/api/detection_engine'; +import { mockCustomQueryRule } from '../../storybook/mocks'; + +export default { + component: TypeReadOnly, + title: 'Rule Management/Prebuilt Rules/Upgrade Flyout/ThreeWayDiff/FieldReadOnly/type', +}; + +interface TemplateProps { + finalDiffableRule: DiffableRule; +} + +const Template: Story = (args) => { + return ; +}; + +export const Default = Template.bind({}); + +Default.args = { + finalDiffableRule: mockCustomQueryRule({ + type: 'query', + }), +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/type/type.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/type/type.tsx new file mode 100644 index 0000000000000..c08a77a2c1d0c --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/type/type.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiDescriptionList } from '@elastic/eui'; +import * as ruleDetailsI18n from '../../../../translations'; +import type { DiffableRuleTypes } from '../../../../../../../../../common/api/detection_engine'; +import { RuleType } from '../../../../rule_definition_section'; + +interface TypeReadOnlyProps { + type: DiffableRuleTypes; +} + +export function TypeReadOnly({ type }: TypeReadOnlyProps) { + return ( + , + }, + ]} + /> + ); +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/machine_learning_rule_field_readonly.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/machine_learning_rule_field_readonly.tsx index 5ebc6f80b13f2..1212905c930c7 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/machine_learning_rule_field_readonly.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/machine_learning_rule_field_readonly.tsx @@ -8,6 +8,10 @@ import React from 'react'; import type { DiffableMachineLearningFields } from '../../../../../../../common/api/detection_engine'; import { MachineLearningJobIdReadOnly } from './fields/machine_learning_job_id/machine_learning_job_id'; +import { TypeReadOnly } from './fields/type/type'; +import { AlertSuppressionReadOnly } from './fields/alert_suppression/alert_suppression'; +import { assertUnreachable } from '../../../../../../../common/utility_types'; +import { AnomalyThresholdReadOnly } from './fields/anomaly_threshold/anomaly_threshold'; interface MachineLearningRuleFieldReadOnlyProps { fieldName: keyof DiffableMachineLearningFields; @@ -19,6 +23,15 @@ export function MachineLearningRuleFieldReadOnly({ finalDiffableRule, }: MachineLearningRuleFieldReadOnlyProps) { switch (fieldName) { + case 'anomaly_threshold': + return ; + case 'alert_suppression': + return ( + + ); case 'machine_learning_job_id': return ( ); case 'type': - return null; + return ; default: - return null; // Will replace with `assertUnreachable(fieldName)` once all fields are implemented + return assertUnreachable(fieldName); } } diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/new_terms_rule_field_readonly.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/new_terms_rule_field_readonly.tsx index 0e2b52c71ba2b..45eeb17f730fb 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/new_terms_rule_field_readonly.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/new_terms_rule_field_readonly.tsx @@ -9,6 +9,11 @@ import React from 'react'; import type { DiffableNewTermsFields } from '../../../../../../../common/api/detection_engine'; import { DataSourceReadOnly } from './fields/data_source/data_source'; import { KqlQueryReadOnly } from './fields/kql_query'; +import { TypeReadOnly } from './fields/type/type'; +import { AlertSuppressionReadOnly } from './fields/alert_suppression/alert_suppression'; +import { NewTermsFieldsReadOnly } from './fields/new_terms_fields/new_terms_fields'; +import { HistoryWindowStartReadOnly } from './fields/history_window_start/history_window_start'; +import { assertUnreachable } from '../../../../../../../common/utility_types'; interface NewTermsRuleFieldReadOnlyProps { fieldName: keyof DiffableNewTermsFields; @@ -20,8 +25,19 @@ export function NewTermsRuleFieldReadOnly({ finalDiffableRule, }: NewTermsRuleFieldReadOnlyProps) { switch (fieldName) { + case 'alert_suppression': + return ( + + ); case 'data_source': return ; + case 'history_window_start': + return ( + + ); case 'kql_query': return ( ); + case 'new_terms_fields': + return ; case 'type': - return null; + return ; default: - return null; // Will replace with `assertUnreachable(fieldName)` once all fields are implemented + return assertUnreachable(fieldName); } } diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/saved_query_rule_field_readonly.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/saved_query_rule_field_readonly.tsx index 41e2e0c32108b..e4a5dbf7242d1 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/saved_query_rule_field_readonly.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/saved_query_rule_field_readonly.tsx @@ -9,6 +9,9 @@ import React from 'react'; import type { DiffableSavedQueryFields } from '../../../../../../../common/api/detection_engine'; import { DataSourceReadOnly } from './fields/data_source/data_source'; import { KqlQueryReadOnly } from './fields/kql_query'; +import { TypeReadOnly } from './fields/type/type'; +import { AlertSuppressionReadOnly } from './fields/alert_suppression/alert_suppression'; +import { assertUnreachable } from '../../../../../../../common/utility_types'; interface SavedQueryRuleFieldReadOnlyProps { fieldName: keyof DiffableSavedQueryFields; @@ -20,6 +23,13 @@ export function SavedQueryRuleFieldReadOnly({ finalDiffableRule, }: SavedQueryRuleFieldReadOnlyProps) { switch (fieldName) { + case 'alert_suppression': + return ( + + ); case 'data_source': return ; case 'kql_query': @@ -31,8 +41,8 @@ export function SavedQueryRuleFieldReadOnly({ /> ); case 'type': - return null; + return ; default: - return null; // Will replace with `assertUnreachable(fieldName)` once all fields are implemented + return assertUnreachable(fieldName); } } diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/storybook/mocks.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/storybook/mocks.ts index 854251450809f..4612852c0ff7e 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/storybook/mocks.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/storybook/mocks.ts @@ -16,9 +16,11 @@ import type { DiffableEqlFields, DiffableEsqlFields, DiffableMachineLearningFields, + DiffableNewTermsFields, DiffableRule, DiffableSavedQueryFields, DiffableThreatMatchFields, + DiffableThresholdFields, InlineKqlQuery, RuleEqlQuery, SavedKqlQuery, @@ -274,3 +276,49 @@ export function mockThreatMatchRule( ...overrides, }; } + +const newTermsDiffableRuleFields: DiffableNewTermsFields = { + type: 'new_terms', + kql_query: { + type: KqlQueryType.inline_query, + query: '*', + language: 'kuery', + filters: [], + }, + new_terms_fields: ['host.name'], + history_window_start: 'now-7d', +}; + +export function mockNewTermsRule( + overrides: Partial +): DiffableRule { + return { + ...commonDiffableRuleFields, + ...newTermsDiffableRuleFields, + ...overrides, + }; +} + +export const thresholdDiffableRuleFields: DiffableThresholdFields = { + type: 'threshold', + kql_query: { + type: KqlQueryType.inline_query, + query: '*', + language: 'kuery', + filters: [], + }, + threshold: { + field: ['user.name'], + value: 200, + }, +}; + +export function mockThresholdRule( + overrides: Partial +): DiffableRule { + return { + ...commonDiffableRuleFields, + ...thresholdDiffableRuleFields, + ...overrides, + }; +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/storybook/three_way_diff_storybook_providers.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/storybook/three_way_diff_storybook_providers.tsx index 483624b6fb408..4eb14440c056c 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/storybook/three_way_diff_storybook_providers.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/storybook/three_way_diff_storybook_providers.tsx @@ -11,8 +11,10 @@ import { merge } from 'lodash'; import { Subject } from 'rxjs'; import { Provider as ReduxStoreProvider } from 'react-redux'; import type { CoreStart } from '@kbn/core/public'; +import type { UpsellingService } from '@kbn/security-solution-upselling/service'; import { createKibanaReactContext } from '@kbn/kibana-react-plugin/public'; import { ReactQueryClientProvider } from '../../../../../../../common/containers/query_client/query_client_provider'; +import { UpsellingProvider } from '../../../../../../../common/components/upselling_provider'; function createKibanaServicesMock(overrides?: Partial) { const baseMock = { @@ -44,6 +46,10 @@ function createKibanaServicesMock(overrides?: Partial) { }, }, uiSettings: {}, + upsellingService: { + messages$: new Subject(), + getMessagesValue: () => new Map(), + } as unknown as UpsellingService, }; return merge(baseMock, overrides); @@ -65,21 +71,26 @@ function createMockStore() { interface StorybookProvidersProps { children: React.ReactNode; - kibanaServicesMock?: Record; + kibanaServicesOverrides?: Record; } export function ThreeWayDiffStorybookProviders({ children, - kibanaServicesMock, + kibanaServicesOverrides, }: StorybookProvidersProps) { - const KibanaReactContext = createKibanaReactContext(createKibanaServicesMock(kibanaServicesMock)); + const kibanaServicesMock = createKibanaServicesMock(kibanaServicesOverrides); + const KibanaReactContext = createKibanaReactContext(kibanaServicesMock); const store = createMockStore(); return ( - {children} + + + {children} + + ); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/threat_match_rule_field_readonly.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/threat_match_rule_field_readonly.tsx index 11fd941601922..3e23a064d14f2 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/threat_match_rule_field_readonly.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/threat_match_rule_field_readonly.tsx @@ -13,6 +13,10 @@ import { ThreatIndexReadOnly } from './fields/threat_index/threat_index'; import { ThreatIndicatorPathReadOnly } from './fields/threat_indicator_path/threat_indicator_path'; import { ThreatMappingReadOnly } from './fields/threat_mapping/threat_mapping'; import { ThreatQueryReadOnly } from './fields/threat_query/threat_query'; +import { TypeReadOnly } from './fields/type/type'; +import { AlertSuppressionReadOnly } from './fields/alert_suppression/alert_suppression'; +import { assertUnreachable } from '../../../../../../../common/utility_types'; +import { ThreatLanguageReadOnly } from './fields/threat_language/threat_language'; interface ThreatMatchRuleFieldReadOnlyProps { fieldName: keyof DiffableThreatMatchFields; @@ -24,6 +28,13 @@ export function ThreatMatchRuleFieldReadOnly({ finalDiffableRule, }: ThreatMatchRuleFieldReadOnlyProps) { switch (fieldName) { + case 'alert_suppression': + return ( + + ); case 'data_source': return ; case 'kql_query': @@ -42,6 +53,8 @@ export function ThreatMatchRuleFieldReadOnly({ threatIndicatorPath={finalDiffableRule.threat_indicator_path} /> ); + case 'threat_language': + return ; case 'threat_mapping': return ; case 'threat_query': @@ -52,8 +65,8 @@ export function ThreatMatchRuleFieldReadOnly({ /> ); case 'type': - return null; + return ; default: - return null; // Will replace with `assertUnreachable(fieldName)` once all fields are implemented + return assertUnreachable(fieldName); } } diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/threshold_rule_field_readonly.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/threshold_rule_field_readonly.tsx index da7d3984d7ccb..d3208a055fa48 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/threshold_rule_field_readonly.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/threshold_rule_field_readonly.tsx @@ -9,6 +9,10 @@ import React from 'react'; import type { DiffableThresholdFields } from '../../../../../../../common/api/detection_engine'; import { DataSourceReadOnly } from './fields/data_source/data_source'; import { KqlQueryReadOnly } from './fields/kql_query'; +import { TypeReadOnly } from './fields/type/type'; +import { AlertSuppressionReadOnly } from './fields/alert_suppression/alert_suppression'; +import { assertUnreachable } from '../../../../../../../common/utility_types'; +import { ThresholdReadOnly } from './fields/threshold/threshold'; interface ThresholdRuleFieldReadOnlyProps { fieldName: keyof DiffableThresholdFields; @@ -20,6 +24,13 @@ export function ThresholdRuleFieldReadOnly({ finalDiffableRule, }: ThresholdRuleFieldReadOnlyProps) { switch (fieldName) { + case 'alert_suppression': + return ( + + ); case 'data_source': return ; case 'kql_query': @@ -30,9 +41,11 @@ export function ThresholdRuleFieldReadOnly({ ruleType={finalDiffableRule.type} /> ); + case 'threshold': + return ; case 'type': - return null; + return ; default: - return null; // Will replace with `assertUnreachable(fieldName)` once all fields are implemented + return assertUnreachable(fieldName); } } diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx index 24f871125ce4d..8fccd1fb8ac73 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx @@ -199,6 +199,23 @@ export const getScheduleStepsData = (rule: RuleResponse): ScheduleStepRule => { }; }; +/** + * Converts seconds to duration string, like "1h", "30m" or "15s" + */ +export const secondsToDurationString = (seconds: number): string => { + if (seconds === 0) { + return `0s`; + } + + if (seconds % 3600 === 0) { + return `${seconds / 3600}h`; + } else if (seconds % 60 === 0) { + return `${seconds / 60}m`; + } else { + return `${seconds}s`; + } +}; + export const getHumanizedDuration = (from: string, interval: string): string => { const fromValue = dateMath.parse(from) ?? moment(); const intervalValue = dateMath.parse(`now-${interval}`) ?? moment(); @@ -208,17 +225,8 @@ export const getHumanizedDuration = (from: string, interval: string): string => // Basing calculations off floored seconds count as moment durations weren't precise const intervalDuration = Math.floor(fromDuration.asSeconds()); // For consistency of display value - if (intervalDuration === 0) { - return `0s`; - } - if (intervalDuration % 3600 === 0) { - return `${intervalDuration / 3600}h`; - } else if (intervalDuration % 60 === 0) { - return `${intervalDuration / 60}m`; - } else { - return `${intervalDuration}s`; - } + return secondsToDurationString(intervalDuration); }; export const getAboutStepsData = (rule: RuleResponse, detailsView: boolean): AboutStepRule => { diff --git a/x-pack/plugins/security_solution/public/entity_analytics/api/hooks/use_risk_engine_status.ts b/x-pack/plugins/security_solution/public/entity_analytics/api/hooks/use_risk_engine_status.ts index 7dc5b27d2ead5..a9c9dc0939b03 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/api/hooks/use_risk_engine_status.ts +++ b/x-pack/plugins/security_solution/public/entity_analytics/api/hooks/use_risk_engine_status.ts @@ -62,7 +62,6 @@ export const useRiskEngineStatus = ( isNewRiskScoreModuleAvailable, risk_engine_status: null, legacy_risk_engine_status: null, - is_max_amount_of_risk_engines_reached: false, risk_engine_task_status: null, }; } diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/risk_score_enable_section.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/risk_score_enable_section.tsx index b5faebe500dac..63ff39ebca7dc 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/risk_score_enable_section.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/risk_score_enable_section.tsx @@ -37,7 +37,6 @@ import { useRiskEngineStatus } from '../api/hooks/use_risk_engine_status'; import { useInitRiskEngineMutation } from '../api/hooks/use_init_risk_engine_mutation'; import { useEnableRiskEngineMutation } from '../api/hooks/use_enable_risk_engine_mutation'; import { useDisableRiskEngineMutation } from '../api/hooks/use_disable_risk_engine_mutation'; -import { MAX_SPACES_COUNT } from '../../../common/entity_analytics/risk_engine'; import { useAppToasts } from '../../common/hooks/use_app_toasts'; import { RiskInformationFlyout } from './risk_information'; import { useOnOpenCloseHandler } from '../../helper_hooks'; @@ -252,22 +251,6 @@ export const RiskScoreEnableSection: React.FC<{ const errorBody = initRiskEngineMutation.error.body; initRiskEngineErrors = [errorBody.message]; } - - if ( - currentRiskEngineStatus !== RiskEngineStatusEnum.ENABLED && - riskEngineStatus?.is_max_amount_of_risk_engines_reached - ) { - return ( - -

      {i18n.MAX_SPACE_PANEL_MESSAGE}

      -
      - ); - } return ( <> <> diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/models/advanced_policy_schema.ts b/x-pack/plugins/security_solution/public/management/pages/policy/models/advanced_policy_schema.ts index 5cefd39ace739..a169d43d4e374 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/models/advanced_policy_schema.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/models/advanced_policy_schema.ts @@ -1941,4 +1941,37 @@ export const AdvancedPolicySchema: AdvancedPolicySchemaType[] = [ } ), }, + { + key: 'windows.advanced.events.file.max_hash_size_mb', + first_supported_version: '8.16', + documentation: i18n.translate( + 'xpack.securitySolution.endpoint.policy.advanced.windows.advanced.events.file.max_hash_size_mb', + { + defaultMessage: + "Attempt to include file.hash.sha256 in file events. Hashing is asynchronous, best-effort, and is not guaranteed to succeed, especially on network drives. WARNING: File hashing is a very CPU- and I/O-intensive process. WARNING: This feature will increase Endpoint's CPU and I/O, and may adversely affect system responsiveness, especially during I/O-intensive activity such as directory copies and compilation. WARNING: Event processing will be delayed due to the time spent hashing, causing Endpoint's Behavioral and Ransomware protections to fire later than normal, potentially allowing threats to inflect additional damage. Set to 'off' to disable this feature. Set to '0' to hash all files up to 1 GiB. Otherwise, this sets the maximum to-be-hashed file size in MiB. Default: off", + } + ), + }, + { + key: 'linux.advanced.events.file.max_hash_size_mb', + first_supported_version: '8.16', + documentation: i18n.translate( + 'xpack.securitySolution.endpoint.policy.advanced.linux.advanced.events.file.max_hash_size_mb', + { + defaultMessage: + "Attempt to include file.hash.sha256 in file events. Hashing is asynchronous, best-effort, and is not guaranteed to succeed, especially on network drives. WARNING: File hashing is a very CPU- and I/O-intensive process. WARNING: This feature will increase Endpoint's CPU and I/O, and may adversely affect system responsiveness, especially during I/O-intensive activity such as directory copies and compilation. WARNING: Event processing will be delayed due to the time spent hashing, causing Endpoint's Behavioral and Ransomware protections to fire later than normal, potentially allowing threats to inflect additional damage. Set to 'off' to disable this feature. Set to '0' to hash all files up to 1 GiB. Otherwise, this sets the maximum to-be-hashed file size in MiB. Default: off", + } + ), + }, + { + key: 'mac.advanced.events.file.max_hash_size_mb', + first_supported_version: '8.16', + documentation: i18n.translate( + 'xpack.securitySolution.endpoint.policy.advanced.mac.advanced.events.file.max_hash_size_mb', + { + defaultMessage: + "Attempt to include file.hash.sha256 in file events. Hashing is asynchronous, best-effort, and is not guaranteed to succeed, especially on network drives. WARNING: File hashing is a very CPU- and I/O-intensive process. WARNING: This feature will increase Endpoint's CPU and I/O, and may adversely affect system responsiveness, especially during I/O-intensive activity such as directory copies and compilation. WARNING: Event processing will be delayed due to the time spent hashing, causing Endpoint's Behavioral and Ransomware protections to fire later than normal, potentially allowing threats to inflect additional damage. Set to 'off' to disable this feature. Set to '0' to hash all files up to 1 GiB. Otherwise, this sets the maximum to-be-hashed file size in MiB. Default: off", + } + ), + }, ]; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware/policy_settings_middleware.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware/policy_settings_middleware.ts index a22dd2a5edfdf..e74403777523c 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware/policy_settings_middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware/policy_settings_middleware.ts @@ -97,7 +97,7 @@ export const policySettingsMiddlewareRunner: MiddlewareRunner = async ( // Agent summary is secondary data, so its ok for it to come after the details // page is populated with the main content if (policyItem.policy_id) { - const { results } = await sendGetFleetAgentStatusForPolicy(http, policyItem.policy_id); + const { results } = await sendGetFleetAgentStatusForPolicy(http, policyItem.policy_ids); dispatch({ type: 'serverReturnedPolicyDetailsAgentSummaryData', payload: { diff --git a/x-pack/plugins/security_solution/public/management/services/policies/ingest.ts b/x-pack/plugins/security_solution/public/management/services/policies/ingest.ts index 4a2690a112e24..523b1b9a858b1 100644 --- a/x-pack/plugins/security_solution/public/management/services/policies/ingest.ts +++ b/x-pack/plugins/security_solution/public/management/services/policies/ingest.ts @@ -86,20 +86,20 @@ export const sendPutPackagePolicy = ( * Get a status summary for all Agents that are currently assigned to a given agent policy * * @param http - * @param policyId + * @param policyIds * @param options */ export const sendGetFleetAgentStatusForPolicy = ( http: HttpStart, /** the Agent (fleet) policy id */ - policyId: string, + policyIds: string[], options: Exclude = {} ): Promise => { return http.get(INGEST_API_FLEET_AGENT_STATUS, { ...options, version: API_VERSIONS.public.v1, query: { - policyId, + policyIds, }, }); }; diff --git a/x-pack/plugins/security_solution/public/timelines/containers/api.ts b/x-pack/plugins/security_solution/public/timelines/containers/api.ts index a6e1f448f5191..155d95c5acef2 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/api.ts +++ b/x-pack/plugins/security_solution/public/timelines/containers/api.ts @@ -17,7 +17,6 @@ import type { TimelineResponse, TimelineErrorResponse, ImportTimelineResultSchema, - ResponseFavoriteTimeline, AllTimelinesResponse, SingleTimelineResponse, SingleTimelineResolveResponse, @@ -29,7 +28,7 @@ import { TimelineErrorResponseType, importTimelineResultSchema, allTimelinesResponse, - responseFavoriteTimeline, + PersistFavoriteRouteResponse, SingleTimelineResponseType, type TimelineType, TimelineTypeEnum, @@ -105,11 +104,8 @@ const decodePrepackedTimelineResponse = (respTimeline?: ImportTimelineResultSche fold(throwErrors(createToasterPlainError), identity) ); -const decodeResponseFavoriteTimeline = (respTimeline?: ResponseFavoriteTimeline) => - pipe( - responseFavoriteTimeline.decode(respTimeline), - fold(throwErrors(createToasterPlainError), identity) - ); +const decodeResponseFavoriteTimeline = (respTimeline?: PersistFavoriteRouteResponse) => + PersistFavoriteRouteResponse.parse(respTimeline); const postTimeline = async ({ timeline, @@ -469,7 +465,7 @@ export const persistFavorite = async ({ return Promise.reject(new Error(`Failed to stringify query: ${JSON.stringify(err)}`)); } - const response = await KibanaServices.get().http.patch( + const response = await KibanaServices.get().http.patch( TIMELINE_FAVORITE_URL, { method: 'PATCH', diff --git a/x-pack/plugins/security_solution/public/timelines/containers/pinned_event/api.ts b/x-pack/plugins/security_solution/public/timelines/containers/pinned_event/api.ts index 8eb149f0f43fb..7df7fb7c62b62 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/pinned_event/api.ts +++ b/x-pack/plugins/security_solution/public/timelines/containers/pinned_event/api.ts @@ -5,7 +5,7 @@ * 2.0. */ import { PINNED_EVENT_URL } from '../../../../common/constants'; -import type { PinnedEvent } from '../../../../common/api/timeline'; +import type { PersistPinnedEventRouteResponse } from '../../../../common/api/timeline'; import { KibanaServices } from '../../../common/lib/kibana'; export const persistPinnedEvent = async ({ @@ -23,10 +23,13 @@ export const persistPinnedEvent = async ({ } catch (err) { return Promise.reject(new Error(`Failed to stringify query: ${JSON.stringify(err)}`)); } - const response = await KibanaServices.get().http.patch(PINNED_EVENT_URL, { - method: 'PATCH', - body: requestBody, - version: '2023-10-31', - }); + const response = await KibanaServices.get().http.patch( + PINNED_EVENT_URL, + { + method: 'PATCH', + body: requestBody, + version: '2023-10-31', + } + ); return response; }; diff --git a/x-pack/plugins/security_solution/public/timelines/store/middlewares/timeline_favorite.ts b/x-pack/plugins/security_solution/public/timelines/store/middlewares/timeline_favorite.ts index 17fd55ee194c1..bf4854e60666b 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/middlewares/timeline_favorite.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/middlewares/timeline_favorite.ts @@ -17,7 +17,7 @@ import { startTimelineSaving, showCallOutUnauthorizedMsg, } from '../actions'; -import type { ResponseFavoriteTimeline } from '../../../../common/api/timeline'; +import type { FavoriteTimelineResponse } from '../../../../common/api/timeline'; import { TimelineTypeEnum } from '../../../../common/api/timeline'; import { persistFavorite } from '../../containers/api'; import { selectTimelineById } from '../selectors'; @@ -49,7 +49,7 @@ export const favoriteTimelineMiddleware: (kibana: CoreStart) => Middleware<{}, S timelineType: timeline.timelineType ?? TimelineTypeEnum.default, }); - const response: ResponseFavoriteTimeline = get('data.persistFavorite', result); + const response: FavoriteTimelineResponse = get('data.persistFavorite', result); if (response.code === 403) { store.dispatch(showCallOutUnauthorizedMsg()); diff --git a/x-pack/plugins/security_solution/public/timelines/store/middlewares/timeline_pinned_event.ts b/x-pack/plugins/security_solution/public/timelines/store/middlewares/timeline_pinned_event.ts index c26c458042dad..8461ed3c2fc17 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/middlewares/timeline_pinned_event.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/middlewares/timeline_pinned_event.ts @@ -5,14 +5,13 @@ * 2.0. */ -import { get, omit } from 'lodash/fp'; +import { omit } from 'lodash/fp'; import type { Action, Middleware } from 'redux'; import type { CoreStart } from '@kbn/core/public'; import type { State } from '../../../common/store/types'; import { selectTimelineById } from '../selectors'; import * as i18n from '../../pages/translations'; -import type { PinnedEventResponse } from '../../../../common/api/timeline'; import { pinEvent, endTimelineSaving, @@ -65,17 +64,17 @@ export const addPinnedEventToTimelineMiddleware: (kibana: CoreStart) => Middlewa timelineId: timeline.savedObjectId, }); - const response: PinnedEventResponse = get('data.persistPinnedEventOnTimeline', result); - if (response && response.code === 403) { + const response = result.data.persistPinnedEventOnTimeline; + if (response && 'code' in response && response.code === 403) { store.dispatch(showCallOutUnauthorizedMsg()); } refreshTimelines(store.getState()); const currentTimeline = selectTimelineById(store.getState(), action.payload.id); - // The response is null in case we unpinned an event. + // The response is null or empty in case we unpinned an event. // In that case we want to remove the locally pinned event. - if (!response) { + if (!response || !('code' in response)) { return store.dispatch( updateTimeline({ id: action.payload.id, diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/definition.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/definition.ts index 17412ee7cf716..a72e00bf7aceb 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/definition.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/definition.ts @@ -39,6 +39,7 @@ export const buildUserEntityDefinition = (space: string): EntityDefinition => entityDefinitionSchema.parse({ id: buildEntityDefinitionId('user', space), name: 'EA User Store', + type: 'user', indexPatterns: ENTITY_STORE_DEFAULT_SOURCE_INDICES, identityFields: ['user.name'], displayNameTemplate: '{{user.name}}', diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_store_data_client.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_store_data_client.ts index f530706398a96..ac1a99b761ed8 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_store_data_client.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_store_data_client.ts @@ -12,8 +12,8 @@ import type { SortOrder } from '@elastic/elasticsearch/lib/api/types'; import type { Entity } from '../../../../common/api/entity_analytics/entity_store/entities/common.gen'; import { createQueryFilterClauses } from '../../../utils/build_query'; import type { - InitEntityStoreRequestBody, - InitEntityStoreResponse, + InitEntityEngineRequestBody, + InitEntityEngineResponse, } from '../../../../common/api/entity_analytics/entity_store/engine/init.gen'; import type { @@ -53,8 +53,8 @@ export class EntityStoreDataClient { public async init( entityType: EntityType, - { indexPattern = '', filter = '' }: InitEntityStoreRequestBody - ): Promise { + { indexPattern = '', filter = '' }: InitEntityEngineRequestBody + ): Promise { const definition = getEntityDefinition(entityType, this.options.namespace); this.options.logger.info(`Initializing entity store for ${entityType}`); diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/delete.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/delete.ts index 44352cfa47c57..b57aa5b992dff 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/delete.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/delete.ts @@ -10,10 +10,10 @@ import { buildSiemResponse } from '@kbn/lists-plugin/server/routes/utils'; import { transformError } from '@kbn/securitysolution-es-utils'; import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; -import type { DeleteEntityStoreResponse } from '../../../../../common/api/entity_analytics/entity_store/engine/delete.gen'; +import type { DeleteEntityEngineResponse } from '../../../../../common/api/entity_analytics/entity_store/engine/delete.gen'; import { - DeleteEntityStoreRequestQuery, - DeleteEntityStoreRequestParams, + DeleteEntityEngineRequestQuery, + DeleteEntityEngineRequestParams, } from '../../../../../common/api/entity_analytics/entity_store/engine/delete.gen'; import { API_VERSIONS, APP_ID } from '../../../../../common/constants'; import type { EntityAnalyticsRoutesDeps } from '../../types'; @@ -35,13 +35,13 @@ export const deleteEntityEngineRoute = ( version: API_VERSIONS.public.v1, validate: { request: { - query: buildRouteValidationWithZod(DeleteEntityStoreRequestQuery), - params: buildRouteValidationWithZod(DeleteEntityStoreRequestParams), + query: buildRouteValidationWithZod(DeleteEntityEngineRequestQuery), + params: buildRouteValidationWithZod(DeleteEntityEngineRequestParams), }, }, }, - async (context, request, response): Promise> => { + async (context, request, response): Promise> => { const siemResponse = buildSiemResponse(response); try { @@ -52,7 +52,7 @@ export const deleteEntityEngineRoute = ( return response.ok({ body }); } catch (e) { - logger.error('Error in DeleteEntityStore:', e); + logger.error('Error in DeleteEntityEngine:', e); const error = transformError(e); return siemResponse.error({ statusCode: error.statusCode, diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/get.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/get.ts index 79a74303c49c2..23f013598b476 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/get.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/get.ts @@ -10,8 +10,8 @@ import { buildSiemResponse } from '@kbn/lists-plugin/server/routes/utils'; import { transformError } from '@kbn/securitysolution-es-utils'; import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; -import type { GetEntityStoreEngineResponse } from '../../../../../common/api/entity_analytics/entity_store/engine/get.gen'; -import { GetEntityStoreEngineRequestParams } from '../../../../../common/api/entity_analytics/entity_store/engine/get.gen'; +import type { GetEntityEngineResponse } from '../../../../../common/api/entity_analytics/entity_store/engine/get.gen'; +import { GetEntityEngineRequestParams } from '../../../../../common/api/entity_analytics/entity_store/engine/get.gen'; import { API_VERSIONS, APP_ID } from '../../../../../common/constants'; import type { EntityAnalyticsRoutesDeps } from '../../types'; @@ -32,16 +32,12 @@ export const getEntityEngineRoute = ( version: API_VERSIONS.public.v1, validate: { request: { - params: buildRouteValidationWithZod(GetEntityStoreEngineRequestParams), + params: buildRouteValidationWithZod(GetEntityEngineRequestParams), }, }, }, - async ( - context, - request, - response - ): Promise> => { + async (context, request, response): Promise> => { const siemResponse = buildSiemResponse(response); try { @@ -50,7 +46,7 @@ export const getEntityEngineRoute = ( return response.ok({ body }); } catch (e) { - logger.error('Error in GetEntityStoreEngine:', e); + logger.error('Error in GetEntityEngine:', e); const error = transformError(e); return siemResponse.error({ statusCode: error.statusCode, diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/init.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/init.ts index 6159cd584b06d..58b33d9fdf25a 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/init.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/init.ts @@ -10,10 +10,10 @@ import { buildSiemResponse } from '@kbn/lists-plugin/server/routes/utils'; import { transformError } from '@kbn/securitysolution-es-utils'; import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; -import type { InitEntityStoreResponse } from '../../../../../common/api/entity_analytics/entity_store/engine/init.gen'; +import type { InitEntityEngineResponse } from '../../../../../common/api/entity_analytics/entity_store/engine/init.gen'; import { - InitEntityStoreRequestBody, - InitEntityStoreRequestParams, + InitEntityEngineRequestBody, + InitEntityEngineRequestParams, } from '../../../../../common/api/entity_analytics/entity_store/engine/init.gen'; import { API_VERSIONS, APP_ID } from '../../../../../common/constants'; import type { EntityAnalyticsRoutesDeps } from '../../types'; @@ -35,25 +35,25 @@ export const initEntityEngineRoute = ( version: API_VERSIONS.public.v1, validate: { request: { - params: buildRouteValidationWithZod(InitEntityStoreRequestParams), - body: buildRouteValidationWithZod(InitEntityStoreRequestBody), + params: buildRouteValidationWithZod(InitEntityEngineRequestParams), + body: buildRouteValidationWithZod(InitEntityEngineRequestBody), }, }, }, - async (context, request, response): Promise> => { + async (context, request, response): Promise> => { const siemResponse = buildSiemResponse(response); try { const secSol = await context.securitySolution; - const body: InitEntityStoreResponse = await secSol + const body: InitEntityEngineResponse = await secSol .getEntityStoreDataClient() .init(request.params.entityType, request.body); return response.ok({ body }); } catch (e) { - logger.error('Error in InitEntityStore:', e); + logger.error('Error in InitEntityEngine:', e); const error = transformError(e); return siemResponse.error({ statusCode: error.statusCode, diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/list.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/list.ts index 53d9a8521ce00..7cec67bcdf5cd 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/list.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/list.ts @@ -9,7 +9,7 @@ import type { IKibanaResponse, Logger } from '@kbn/core/server'; import { buildSiemResponse } from '@kbn/lists-plugin/server/routes/utils'; import { transformError } from '@kbn/securitysolution-es-utils'; -import type { ListEntityStoreEnginesResponse } from '../../../../../common/api/entity_analytics/entity_store/engine/list.gen'; +import type { ListEntityEnginesResponse } from '../../../../../common/api/entity_analytics/entity_store/engine/list.gen'; import { API_VERSIONS, APP_ID } from '../../../../../common/constants'; import type { EntityAnalyticsRoutesDeps } from '../../types'; @@ -32,11 +32,7 @@ export const listEntityEnginesRoute = ( validate: {}, }, - async ( - context, - request, - response - ): Promise> => { + async (context, request, response): Promise> => { const siemResponse = buildSiemResponse(response); try { @@ -45,7 +41,7 @@ export const listEntityEnginesRoute = ( return response.ok({ body }); } catch (e) { - logger.error('Error in ListEntityStoreEngines:', e); + logger.error('Error in ListEntityEngines:', e); const error = transformError(e); return siemResponse.error({ statusCode: error.statusCode, diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/start.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/start.ts index 6ec6674a5473d..1872de211cb8f 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/start.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/start.ts @@ -10,8 +10,8 @@ import { buildSiemResponse } from '@kbn/lists-plugin/server/routes/utils'; import { transformError } from '@kbn/securitysolution-es-utils'; import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; -import type { StartEntityStoreResponse } from '../../../../../common/api/entity_analytics/entity_store/engine/start.gen'; -import { StartEntityStoreRequestParams } from '../../../../../common/api/entity_analytics/entity_store/engine/start.gen'; +import type { StartEntityEngineResponse } from '../../../../../common/api/entity_analytics/entity_store/engine/start.gen'; +import { StartEntityEngineRequestParams } from '../../../../../common/api/entity_analytics/entity_store/engine/start.gen'; import { API_VERSIONS, APP_ID } from '../../../../../common/constants'; import type { EntityAnalyticsRoutesDeps } from '../../types'; import { ENGINE_STATUS } from '../constants'; @@ -33,12 +33,12 @@ export const startEntityEngineRoute = ( version: API_VERSIONS.public.v1, validate: { request: { - params: buildRouteValidationWithZod(StartEntityStoreRequestParams), + params: buildRouteValidationWithZod(StartEntityEngineRequestParams), }, }, }, - async (context, request, response): Promise> => { + async (context, request, response): Promise> => { const siemResponse = buildSiemResponse(response); try { @@ -47,7 +47,7 @@ export const startEntityEngineRoute = ( return response.ok({ body: { started: engine.status === ENGINE_STATUS.STARTED } }); } catch (e) { - logger.error('Error in StartEntityStore:', e); + logger.error('Error in StartEntityEngine:', e); const error = transformError(e); return siemResponse.error({ statusCode: error.statusCode, diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/stats.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/stats.ts index 1d7534c17f747..9ca3cd906f016 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/stats.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/stats.ts @@ -10,8 +10,8 @@ import { buildSiemResponse } from '@kbn/lists-plugin/server/routes/utils'; import { transformError } from '@kbn/securitysolution-es-utils'; import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; -import type { GetEntityStoreStatsResponse } from '../../../../../common/api/entity_analytics/entity_store/engine/stats.gen'; -import { GetEntityStoreStatsRequestParams } from '../../../../../common/api/entity_analytics/entity_store/engine/stats.gen'; +import type { GetEntityEngineStatsResponse } from '../../../../../common/api/entity_analytics/entity_store/engine/stats.gen'; +import { GetEntityEngineStatsRequestParams } from '../../../../../common/api/entity_analytics/entity_store/engine/stats.gen'; import { API_VERSIONS, APP_ID } from '../../../../../common/constants'; import type { EntityAnalyticsRoutesDeps } from '../../types'; @@ -32,12 +32,16 @@ export const getEntityEngineStatsRoute = ( version: API_VERSIONS.public.v1, validate: { request: { - params: buildRouteValidationWithZod(GetEntityStoreStatsRequestParams), + params: buildRouteValidationWithZod(GetEntityEngineStatsRequestParams), }, }, }, - async (context, request, response): Promise> => { + async ( + context, + request, + response + ): Promise> => { const siemResponse = buildSiemResponse(response); try { @@ -46,7 +50,7 @@ export const getEntityEngineStatsRoute = ( // return response.ok({ body }); } catch (e) { - logger.error('Error in GetEntityStoreStats:', e); + logger.error('Error in GetEntityEngineStats:', e); const error = transformError(e); return siemResponse.error({ statusCode: error.statusCode, diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/stop.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/stop.ts index e1ddb464d1204..e1c28bc2cc073 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/stop.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/stop.ts @@ -10,8 +10,8 @@ import { buildSiemResponse } from '@kbn/lists-plugin/server/routes/utils'; import { transformError } from '@kbn/securitysolution-es-utils'; import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; -import type { StopEntityStoreResponse } from '../../../../../common/api/entity_analytics/entity_store/engine/stop.gen'; -import { StopEntityStoreRequestParams } from '../../../../../common/api/entity_analytics/entity_store/engine/stop.gen'; +import type { StopEntityEngineResponse } from '../../../../../common/api/entity_analytics/entity_store/engine/stop.gen'; +import { StopEntityEngineRequestParams } from '../../../../../common/api/entity_analytics/entity_store/engine/stop.gen'; import { API_VERSIONS, APP_ID } from '../../../../../common/constants'; import type { EntityAnalyticsRoutesDeps } from '../../types'; import { ENGINE_STATUS } from '../constants'; @@ -33,12 +33,12 @@ export const stopEntityEngineRoute = ( version: API_VERSIONS.public.v1, validate: { request: { - params: buildRouteValidationWithZod(StopEntityStoreRequestParams), + params: buildRouteValidationWithZod(StopEntityEngineRequestParams), }, }, }, - async (context, request, response): Promise> => { + async (context, request, response): Promise> => { const siemResponse = buildSiemResponse(response); try { @@ -47,7 +47,7 @@ export const stopEntityEngineRoute = ( return response.ok({ body: { stopped: engine.status === ENGINE_STATUS.STOPPED } }); } catch (e) { - logger.error('Error in StopEntityStore:', e); + logger.error('Error in StopEntityEngine:', e); const error = transformError(e); return siemResponse.error({ statusCode: error.statusCode, diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/risk_engine_data_client.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/risk_engine_data_client.ts index c520e3fa07763..241523f62e12c 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/risk_engine_data_client.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/risk_engine_data_client.ts @@ -10,13 +10,12 @@ import type { TaskManagerStartContract } from '@kbn/task-manager-plugin/server'; import type { AuditLogger } from '@kbn/security-plugin-types-server'; import { RiskEngineStatusEnum } from '../../../../common/api/entity_analytics'; import type { InitRiskEngineResult } from '../../../../common/entity_analytics/risk_engine'; -import { MAX_SPACES_COUNT, RiskScoreEntity } from '../../../../common/entity_analytics/risk_engine'; +import { RiskScoreEntity } from '../../../../common/entity_analytics/risk_engine'; import { removeLegacyTransforms, getLegacyTransforms } from '../utils/transforms'; import { updateSavedObjectAttribute, getConfiguration, initSavedObjects, - getEnabledRiskEngineAmount, deleteSavedObjects, } from './utils/saved_object_configuration'; import { bulkDeleteSavedObjects } from '../../risk_score/prebuilt_saved_objects/helpers/bulk_delete_saved_objects'; @@ -119,7 +118,6 @@ export class RiskEngineDataClient { }) { const riskEngineStatus = await this.getCurrentStatus(); const legacyRiskEngineStatus = await this.getLegacyStatus({ namespace }); - const isMaxAmountOfRiskEnginesReached = await this.getIsMaxAmountOfRiskEnginesReached(); const taskStatus = riskEngineStatus === 'ENABLED' && taskManager @@ -139,7 +137,6 @@ export class RiskEngineDataClient { return { riskEngineStatus, legacyRiskEngineStatus, - isMaxAmountOfRiskEnginesReached, taskStatus, }; } @@ -303,29 +300,6 @@ export class RiskEngineDataClient { return RiskEngineStatusEnum.NOT_INSTALLED; } - private async getIsMaxAmountOfRiskEnginesReached() { - try { - const amountOfEnabledConfigurations = await getEnabledRiskEngineAmount({ - savedObjectsClient: this.options.soClient, - }); - - this.options.auditLogger?.log({ - message: 'System checked if the risk engine is enabled in each space', - event: { - action: RiskEngineAuditActions.RISK_ENGINE_STATUS_FOR_ALL_SPACES_GET, - category: AUDIT_CATEGORY.DATABASE, - type: AUDIT_TYPE.ACCESS, - outcome: AUDIT_OUTCOME.SUCCESS, - }, - }); - - return amountOfEnabledConfigurations >= MAX_SPACES_COUNT; - } catch (e) { - this.options.logger.error(`Error while getting amount of enabled risk engines: ${e.message}`); - return false; - } - } - private async getLegacyStatus({ namespace }: { namespace: string }) { const transforms = await getLegacyTransforms({ namespace, esClient: this.options.esClient }); diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/status.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/status.ts index eac63e8a04b6b..9b69ddec6b005 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/status.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/status.ts @@ -35,20 +35,15 @@ export const riskEngineStatusRoute = ( const [_, { taskManager }] = await getStartServices(); try { - const { - riskEngineStatus, - legacyRiskEngineStatus, - isMaxAmountOfRiskEnginesReached, - taskStatus, - } = await riskEngineClient.getStatus({ - namespace: spaceId, - taskManager, - }); + const { riskEngineStatus, legacyRiskEngineStatus, taskStatus } = + await riskEngineClient.getStatus({ + namespace: spaceId, + taskManager, + }); const body: RiskEngineStatusResponse = { risk_engine_status: riskEngineStatus, legacy_risk_engine_status: legacyRiskEngineStatus, - is_max_amount_of_risk_engines_reached: isMaxAmountOfRiskEnginesReached, risk_engine_task_status: taskStatus, }; diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/utils/saved_object_configuration.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/utils/saved_object_configuration.ts index c9ef818aa0d90..4282e0a793f47 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/utils/saved_object_configuration.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/utils/saved_object_configuration.ts @@ -37,18 +37,6 @@ const getConfigurationSavedObject = async ({ return savedObjectsResponse.saved_objects?.[0]; }; -export const getEnabledRiskEngineAmount = async ({ - savedObjectsClient, -}: SavedObjectsClientArg): Promise => { - const savedObjectsResponse = await savedObjectsClient.find({ - type: riskEngineConfigurationTypeName, - namespaces: ['*'], - }); - - return savedObjectsResponse.saved_objects?.filter((config) => config?.attributes?.enabled) - ?.length; -}; - export const updateSavedObjectAttribute = async ({ savedObjectsClient, attributes, diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/draft_timelines/clean_draft_timelines/index.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/draft_timelines/clean_draft_timelines/index.ts index f8b9ad8392982..387720b4a3b4f 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/draft_timelines/clean_draft_timelines/index.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/draft_timelines/clean_draft_timelines/index.ts @@ -7,13 +7,13 @@ import { v4 as uuidv4 } from 'uuid'; import { transformError } from '@kbn/securitysolution-es-utils'; +import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; import type { SecuritySolutionPluginRouter } from '../../../../../types'; import type { ConfigType } from '../../../../..'; import { buildSiemResponse } from '../../../../detection_engine/routes/utils'; import { TIMELINE_DRAFT_URL } from '../../../../../../common/constants'; import { buildFrameworkRequest } from '../../../utils/common'; -import { buildRouteValidationWithExcess } from '../../../../../utils/build_validation/route_validation'; import { getDraftTimeline, resetTimeline, @@ -21,7 +21,10 @@ import { persistTimeline, } from '../../../saved_object/timelines'; import { draftTimelineDefaults } from '../../../utils/default_timeline'; -import { cleanDraftTimelineSchema, TimelineTypeEnum } from '../../../../../../common/api/timeline'; +import { + CleanDraftTimelinesRequestBody, + TimelineTypeEnum, +} from '../../../../../../common/api/timeline'; export const cleanDraftTimelinesRoute = (router: SecuritySolutionPluginRouter, _: ConfigType) => { router.versioned @@ -35,7 +38,7 @@ export const cleanDraftTimelinesRoute = (router: SecuritySolutionPluginRouter, _ .addVersion( { validate: { - request: { body: buildRouteValidationWithExcess(cleanDraftTimelineSchema) }, + request: { body: buildRouteValidationWithZod(CleanDraftTimelinesRequestBody) }, }, version: '2023-10-31', }, diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/notes/delete_note.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/notes/delete_note.ts index 318d8950bc619..92be926453403 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/notes/delete_note.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/notes/delete_note.ts @@ -6,17 +6,17 @@ */ import { transformError } from '@kbn/securitysolution-es-utils'; +import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; import type { SecuritySolutionPluginRouter } from '../../../../types'; import { NOTE_URL } from '../../../../../common/constants'; -import { buildRouteValidationWithExcess } from '../../../../utils/build_validation/route_validation'; import type { ConfigType } from '../../../..'; import { buildSiemResponse } from '../../../detection_engine/routes/utils'; import { buildFrameworkRequest } from '../../utils/common'; -import { deleteNoteSchema } from '../../../../../common/api/timeline'; +import { DeleteNoteRequestBody, type DeleteNoteResponse } from '../../../../../common/api/timeline'; import { deleteNote } from '../../saved_object/notes'; export const deleteNoteRoute = (router: SecuritySolutionPluginRouter, config: ConfigType) => { @@ -31,7 +31,7 @@ export const deleteNoteRoute = (router: SecuritySolutionPluginRouter, config: Co .addVersion( { validate: { - request: { body: buildRouteValidationWithExcess(deleteNoteSchema) }, + request: { body: buildRouteValidationWithZod(DeleteNoteRequestBody) }, }, version: '2023-10-31', }, @@ -40,27 +40,25 @@ export const deleteNoteRoute = (router: SecuritySolutionPluginRouter, config: Co try { const frameworkRequest = await buildFrameworkRequest(context, request); - const noteId = request.body?.noteId ?? ''; - const noteIds = request.body?.noteIds ?? null; - if (noteIds != null) { - await deleteNote({ - request: frameworkRequest, - noteIds, - }); + if (!request.body) { + throw new Error('Missing request body'); + } + let noteIds: string[] = []; + if ('noteId' in request.body) { + noteIds = [request.body.noteId]; + } else if ('noteIds' in request.body && Array.isArray(request.body.noteIds)) { + noteIds = request.body.noteIds; + } - return response.ok({ - body: { data: {} }, - }); - } else { - await deleteNote({ - request: frameworkRequest, - noteIds: [noteId], - }); + await deleteNote({ + request: frameworkRequest, + noteIds, + }); - return response.ok({ - body: { data: {} }, - }); - } + const body: DeleteNoteResponse = { data: {} }; + return response.ok({ + body, + }); } catch (err) { const error = transformError(err); return siemResponse.error({ diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/notes/get_notes.test.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/notes/get_notes.test.ts index db62235ea592f..30ae41c1da820 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/notes/get_notes.test.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/notes/get_notes.test.ts @@ -13,10 +13,10 @@ import { requestMock, } from '../../../detection_engine/routes/__mocks__'; import { NOTE_URL } from '../../../../../common/constants'; -import type { getNotesSchema } from '../../../../../common/api/timeline'; +import type { GetNotesRequestQuery } from '../../../../../common/api/timeline'; import { mockGetCurrentUser } from '../../__mocks__/import_timelines'; -const getAllNotesRequest = (query?: typeof getNotesSchema) => +const getAllNotesRequest = (query?: GetNotesRequestQuery) => requestMock.create({ method: 'get', path: NOTE_URL, diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/notes/get_notes.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/notes/get_notes.ts index 8e8a88a4a6aa9..9cc8435d6aae0 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/notes/get_notes.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/notes/get_notes.ts @@ -7,6 +7,7 @@ import { transformError } from '@kbn/securitysolution-es-utils'; import type { SortOrder } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; import type { SecuritySolutionPluginRouter } from '../../../../types'; import { NOTE_URL } from '../../../../../common/constants'; @@ -14,10 +15,9 @@ import type { ConfigType } from '../../../..'; import { buildSiemResponse } from '../../../detection_engine/routes/utils'; import { buildFrameworkRequest } from '../../utils/common'; -import { getNotesSchema } from '../../../../../common/api/timeline'; -import { buildRouteValidationWithExcess } from '../../../../utils/build_validation/route_validation'; import { getAllSavedNote, MAX_UNASSOCIATED_NOTES } from '../../saved_object/notes'; import { noteSavedObjectType } from '../../saved_object_mappings/notes'; +import { GetNotesRequestQuery, type GetNotesResponse } from '../../../../../common/api/timeline'; export const getNotesRoute = (router: SecuritySolutionPluginRouter, _: ConfigType) => { router.versioned @@ -31,7 +31,7 @@ export const getNotesRoute = (router: SecuritySolutionPluginRouter, _: ConfigTyp .addVersion( { validate: { - request: { query: buildRouteValidationWithExcess(getNotesSchema) }, + request: { query: buildRouteValidationWithZod(GetNotesRequestQuery) }, }, version: '2023-10-31', }, @@ -50,8 +50,8 @@ export const getNotesRoute = (router: SecuritySolutionPluginRouter, _: ConfigTyp perPage: MAX_UNASSOCIATED_NOTES, }; const res = await getAllSavedNote(frameworkRequest, options); - - return response.ok({ body: res ?? {} }); + const body: GetNotesResponse = res ?? {}; + return response.ok({ body }); } else { const options = { type: noteSavedObjectType, @@ -60,8 +60,8 @@ export const getNotesRoute = (router: SecuritySolutionPluginRouter, _: ConfigTyp perPage: MAX_UNASSOCIATED_NOTES, }; const res = await getAllSavedNote(frameworkRequest, options); - - return response.ok({ body: res ?? {} }); + const body: GetNotesResponse = res ?? {}; + return response.ok({ body }); } } else { const perPage = queryParams?.perPage ? parseInt(queryParams.perPage, 10) : 10; @@ -80,7 +80,8 @@ export const getNotesRoute = (router: SecuritySolutionPluginRouter, _: ConfigTyp filter, }; const res = await getAllSavedNote(frameworkRequest, options); - return response.ok({ body: res ?? {} }); + const body: GetNotesResponse = res ?? {}; + return response.ok({ body }); } } catch (err) { const error = transformError(err); diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/notes/persist_note.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/notes/persist_note.ts index 1bd91306a7419..7ee0dc886a787 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/notes/persist_note.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/notes/persist_note.ts @@ -6,17 +6,20 @@ */ import { transformError } from '@kbn/securitysolution-es-utils'; +import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; import type { SecuritySolutionPluginRouter } from '../../../../types'; import { NOTE_URL } from '../../../../../common/constants'; -import { buildRouteValidation } from '../../../../utils/build_validation/route_validation'; import type { ConfigType } from '../../../..'; import { buildSiemResponse } from '../../../detection_engine/routes/utils'; import { buildFrameworkRequest } from '../../utils/common'; -import { persistNoteWithoutRefSchema } from '../../../../../common/api/timeline'; +import { + PersistNoteRouteRequestBody, + type PersistNoteRouteResponse, +} from '../../../../../common/api/timeline'; import { persistNote } from '../../saved_object/notes'; export const persistNoteRoute = (router: SecuritySolutionPluginRouter, _: ConfigType) => { @@ -31,7 +34,7 @@ export const persistNoteRoute = (router: SecuritySolutionPluginRouter, _: Config .addVersion( { validate: { - request: { body: buildRouteValidation(persistNoteWithoutRefSchema) }, + request: { body: buildRouteValidationWithZod(PersistNoteRouteRequestBody) }, }, version: '2023-10-31', }, @@ -49,9 +52,10 @@ export const persistNoteRoute = (router: SecuritySolutionPluginRouter, _: Config note, overrideOwner: true, }); + const body: PersistNoteRouteResponse = { data: { persistNote: res } }; return response.ok({ - body: { data: { persistNote: res } }, + body, }); } catch (err) { const error = transformError(err); diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/pinned_events/persist_pinned_event.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/pinned_events/persist_pinned_event.ts index a5739c49a34ea..c1e245cda40fb 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/pinned_events/persist_pinned_event.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/pinned_events/persist_pinned_event.ts @@ -6,17 +6,21 @@ */ import { transformError } from '@kbn/securitysolution-es-utils'; +import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; + import type { SecuritySolutionPluginRouter } from '../../../../types'; import { PINNED_EVENT_URL } from '../../../../../common/constants'; -import { buildRouteValidationWithExcess } from '../../../../utils/build_validation/route_validation'; import type { ConfigType } from '../../../..'; import { buildSiemResponse } from '../../../detection_engine/routes/utils'; import { buildFrameworkRequest } from '../../utils/common'; -import { persistPinnedEventSchema } from '../../../../../common/api/timeline'; +import { + type PersistPinnedEventRouteResponse, + PersistPinnedEventRouteRequestBody, +} from '../../../../../common/api/timeline'; import { persistPinnedEventOnTimeline } from '../../saved_object/pinned_events'; export const persistPinnedEventRoute = ( @@ -34,7 +38,7 @@ export const persistPinnedEventRoute = ( .addVersion( { validate: { - request: { body: buildRouteValidationWithExcess(persistPinnedEventSchema) }, + request: { body: buildRouteValidationWithZod(PersistPinnedEventRouteRequestBody) }, }, version: '2023-10-31', }, @@ -54,8 +58,12 @@ export const persistPinnedEventRoute = ( timelineId ); + const body: PersistPinnedEventRouteResponse = { + data: { persistPinnedEventOnTimeline: res }, + }; + return response.ok({ - body: { data: { persistPinnedEventOnTimeline: res } }, + body, }); } catch (err) { const error = transformError(err); diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/delete_timelines/index.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/delete_timelines/index.ts index 7f6339ee25929..a11b16c96fb1f 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/delete_timelines/index.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/delete_timelines/index.ts @@ -6,9 +6,12 @@ */ import { transformError } from '@kbn/securitysolution-es-utils'; -import { buildRouteValidationWithExcess } from '../../../../../utils/build_validation/route_validation'; +import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; import type { ConfigType } from '../../../../..'; -import { deleteTimelinesSchema } from '../../../../../../common/api/timeline'; +import { + DeleteTimelinesRequestBody, + type DeleteTimelinesResponse, +} from '../../../../../../common/api/timeline'; import type { SecuritySolutionPluginRouter } from '../../../../../types'; import { TIMELINE_URL } from '../../../../../../common/constants'; import { buildSiemResponse } from '../../../../detection_engine/routes/utils'; @@ -29,7 +32,7 @@ export const deleteTimelinesRoute = (router: SecuritySolutionPluginRouter, confi { version: '2023-10-31', validate: { - request: { body: buildRouteValidationWithExcess(deleteTimelinesSchema) }, + request: { body: buildRouteValidationWithZod(DeleteTimelinesRequestBody) }, }, }, async (context, request, response) => { @@ -40,7 +43,8 @@ export const deleteTimelinesRoute = (router: SecuritySolutionPluginRouter, confi const { savedObjectIds, searchIds } = request.body; await deleteTimeline(frameworkRequest, savedObjectIds, searchIds); - return response.ok({ body: { data: { deleteTimeline: true } } }); + const body: DeleteTimelinesResponse = { data: { deleteTimeline: true } }; + return response.ok({ body }); } catch (err) { const error = transformError(err); return siemResponse.error({ diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/export_timelines/index.test.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/export_timelines/index.test.ts index 13b6e0f68fefc..ade95ba75c837 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/export_timelines/index.test.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/export_timelines/index.test.ts @@ -93,9 +93,7 @@ describe('export timelines', () => { }); const result = server.validate(request); - expect(result.badRequest.mock.calls[0][0]).toEqual( - 'Invalid value {"id":"someId"}, excess properties: ["id"]' - ); + expect(result.badRequest.mock.calls[0][0]).toEqual('file_name: Required'); }); test('return validation error for request params', async () => { @@ -107,9 +105,7 @@ describe('export timelines', () => { }); const result = server.validate(request); - expect(result.badRequest.mock.calls[0][0]).toEqual( - 'Invalid value "someId" supplied to "ids"' - ); + expect(result.badRequest.mock.calls[0][0]).toEqual('ids: Expected array, received string'); }); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/export_timelines/index.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/export_timelines/index.ts index 7af6b7be0cdd0..163b212840423 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/export_timelines/index.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/export_timelines/index.ts @@ -6,16 +6,16 @@ */ import { transformError } from '@kbn/securitysolution-es-utils'; +import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; import { TIMELINE_EXPORT_URL } from '../../../../../../common/constants'; import type { SecuritySolutionPluginRouter } from '../../../../../types'; import type { ConfigType } from '../../../../../config'; import { buildSiemResponse } from '../../../../detection_engine/routes/utils'; import { - exportTimelinesQuerySchema, - exportTimelinesRequestBodySchema, + ExportTimelinesRequestQuery, + ExportTimelinesRequestBody, } from '../../../../../../common/api/timeline'; -import { buildRouteValidationWithExcess } from '../../../../../utils/build_validation/route_validation'; import { buildFrameworkRequest } from '../../../utils/common'; import { getExportTimelineByObjectIds } from './helpers'; @@ -35,8 +35,8 @@ export const exportTimelinesRoute = (router: SecuritySolutionPluginRouter, confi { validate: { request: { - query: buildRouteValidationWithExcess(exportTimelinesQuerySchema), - body: buildRouteValidationWithExcess(exportTimelinesRequestBodySchema), + query: buildRouteValidationWithZod(ExportTimelinesRequestQuery), + body: buildRouteValidationWithZod(ExportTimelinesRequestBody), }, }, version: '2023-10-31', diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/persist_favorite/index.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/persist_favorite/index.ts index f53acf25803a8..b8416b9ffe7bc 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/persist_favorite/index.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/persist_favorite/index.ts @@ -6,18 +6,22 @@ */ import { transformError } from '@kbn/securitysolution-es-utils'; +import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; import type { SecuritySolutionPluginRouter } from '../../../../../types'; import { TIMELINE_FAVORITE_URL } from '../../../../../../common/constants'; -import { buildRouteValidationWithExcess } from '../../../../../utils/build_validation/route_validation'; import type { ConfigType } from '../../../../..'; import { buildSiemResponse } from '../../../../detection_engine/routes/utils'; import { buildFrameworkRequest } from '../../../utils/common'; import { persistFavorite } from '../../../saved_object/timelines'; -import { TimelineTypeEnum, persistFavoriteSchema } from '../../../../../../common/api/timeline'; +import { + type PersistFavoriteRouteResponse, + PersistFavoriteRouteRequestBody, + TimelineTypeEnum, +} from '../../../../../../common/api/timeline'; export const persistFavoriteRoute = (router: SecuritySolutionPluginRouter, _: ConfigType) => { router.versioned @@ -32,7 +36,7 @@ export const persistFavoriteRoute = (router: SecuritySolutionPluginRouter, _: Co { version: '2023-10-31', validate: { - request: { body: buildRouteValidationWithExcess(persistFavoriteSchema) }, + request: { body: buildRouteValidationWithZod(PersistFavoriteRouteRequestBody) }, }, }, async (context, request, response) => { @@ -51,12 +55,14 @@ export const persistFavoriteRoute = (router: SecuritySolutionPluginRouter, _: Co timelineType || TimelineTypeEnum.default ); - return response.ok({ - body: { - data: { - persistFavorite: timeline, - }, + const body: PersistFavoriteRouteResponse = { + data: { + persistFavorite: timeline, }, + }; + + return response.ok({ + body, }); } catch (err) { const error = transformError(err); diff --git a/x-pack/plugins/security_solution/server/lib/timeline/saved_object/notes/saved_object.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object/notes/saved_object.ts index ff277a4bea9e0..300903f8b22ee 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/saved_object/notes/saved_object.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object/notes/saved_object.ts @@ -22,6 +22,7 @@ import type { BareNote, BareNoteWithoutExternalRefs, ResponseNote, + GetNotesResult, } from '../../../../../common/api/timeline'; import { SavedObjectNoteRuntimeType } from '../../../../../common/types/timeline/note/saved_object'; import type { SavedObjectNoteWithoutExternalRefs } from '../../../../../common/types/timeline/note/saved_object'; @@ -133,7 +134,7 @@ export const createNote = async ({ noteId: string | null; note: BareNote | BareNoteWithoutExternalRefs; overrideOwner?: boolean; -}) => { +}): Promise => { const savedObjectsClient = (await request.context.core).savedObjects.client; const userInfo = request.user; @@ -201,7 +202,7 @@ export const updateNote = async ({ noteId: string; note: BareNote | BareNoteWithoutExternalRefs; overrideOwner?: boolean; -}) => { +}): Promise => { const savedObjectsClient = (await request.context.core).savedObjects.client; const userInfo = request.user; @@ -261,7 +262,7 @@ const getSavedNote = async (request: FrameworkRequest, NoteId: string) => { export const getAllSavedNote = async ( request: FrameworkRequest, options: SavedObjectsFindOptions -) => { +): Promise => { const savedObjectsClient = (await request.context.core).savedObjects.client; const savedObjects = await savedObjectsClient.find(options); diff --git a/x-pack/plugins/security_solution/server/lib/timeline/saved_object/pinned_events/index.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object/pinned_events/index.ts index 4cb37fd6d6d89..5181a099ae7fb 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/saved_object/pinned_events/index.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object/pinned_events/index.ts @@ -17,8 +17,7 @@ import { UNAUTHENTICATED_USER } from '../../../../../common/constants'; import type { BarePinnedEvent, PinnedEvent, - PinnedEventResponse, - BarePinnedEventWithoutExternalRefs, + PersistPinnedEventResponse, } from '../../../../../common/api/timeline'; import { SavedObjectPinnedEventRuntimeType } from '../../../../../common/types/timeline/pinned_event/saved_object'; import type { SavedObjectPinnedEventWithoutExternalRefs } from '../../../../../common/types/timeline/pinned_event/saved_object'; @@ -77,7 +76,7 @@ export const persistPinnedEventOnTimeline = async ( pinnedEventId: string | null, // pinned event saved object id eventId: string, timelineId: string -): Promise => { +): Promise => { try { if (pinnedEventId != null) { // Delete Pinned Event on Timeline @@ -111,9 +110,6 @@ export const persistPinnedEventOnTimeline = async ( code: 403, message: err.message, pinnedEventId: eventId, - timelineId: '', - version: '', - eventId: '', } : null; } @@ -140,7 +136,7 @@ const createPinnedEvent = async ({ request: FrameworkRequest; eventId: string; timelineId: string; -}): Promise => { +}): Promise => { const savedObjectsClient = (await request.context.core).savedObjects.client; const savedPinnedEvent: BarePinnedEvent = { @@ -151,7 +147,7 @@ const createPinnedEvent = async ({ const pinnedEventWithCreator = pickSavedPinnedEvent(null, savedPinnedEvent, request.user); const { transformedFields: migratedAttributes, references } = - pinnedEventFieldsMigrator.extractFieldsToReferences({ + pinnedEventFieldsMigrator.extractFieldsToReferences>({ data: pinnedEventWithCreator, }); diff --git a/x-pack/plugins/security_solution/server/lib/timeline/saved_object/timelines/index.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object/timelines/index.ts index 71d61c22ab33c..08eeda3d8ab56 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/saved_object/timelines/index.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object/timelines/index.ts @@ -23,7 +23,7 @@ import type { ExportTimelineNotFoundError, PageInfoTimeline, ResponseTimelines, - ResponseFavoriteTimeline, + FavoriteTimelineResponse, ResponseTimeline, SortTimeline, TimelineResult, @@ -312,7 +312,7 @@ export const persistFavorite = async ( templateTimelineId: string | null, templateTimelineVersion: number | null, timelineType: TimelineType -): Promise => { +): Promise => { const userName = request.user?.username ?? UNAUTHENTICATED_USER; const fullName = request.user?.full_name ?? ''; try { diff --git a/x-pack/plugins/security_solution_ess/public/navigation/index.ts b/x-pack/plugins/security_solution_ess/public/navigation/index.ts index b32adde0e211c..9b28bef8c403d 100644 --- a/x-pack/plugins/security_solution_ess/public/navigation/index.ts +++ b/x-pack/plugins/security_solution_ess/public/navigation/index.ts @@ -7,11 +7,9 @@ import type { Services } from '../common/services'; import { subscribeBreadcrumbs } from './breadcrumbs'; -import { enableManagementCardsLanding } from './management_cards'; import { initSideNavigation } from './side_navigation'; export const startNavigation = (services: Services) => { initSideNavigation(services); subscribeBreadcrumbs(services); - enableManagementCardsLanding(services); }; diff --git a/x-pack/plugins/security_solution_ess/public/navigation/management_cards.ts b/x-pack/plugins/security_solution_ess/public/navigation/management_cards.ts deleted file mode 100644 index c2b302956384a..0000000000000 --- a/x-pack/plugins/security_solution_ess/public/navigation/management_cards.ts +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import type { CardNavExtensionDefinition } from '@kbn/management-cards-navigation'; -import { - getNavigationPropsFromId, - SecurityPageName, - ExternalPageName, -} from '@kbn/security-solution-navigation'; -import { combineLatestWith } from 'rxjs'; -import type { Services } from '../common/services'; - -const SecurityManagementCards = new Map([ - [ExternalPageName.visualize, 'content'], - [ExternalPageName.maps, 'content'], - [SecurityPageName.entityAnalyticsManagement, 'alerts'], - [SecurityPageName.entityAnalyticsAssetClassification, 'alerts'], -]); -export const enableManagementCardsLanding = (services: Services) => { - const { securitySolution, management, application, navigation } = services; - - securitySolution - .getNavLinks$() - .pipe(combineLatestWith(navigation.isSolutionNavEnabled$)) - .subscribe(([navLinks, isSolutionNavEnabled]) => { - const cardNavDefinitions = navLinks.reduce>( - (acc, navLink) => { - if (SecurityManagementCards.has(navLink.id)) { - const { appId, deepLinkId, path } = getNavigationPropsFromId(navLink.id); - acc[navLink.id] = { - category: SecurityManagementCards.get(navLink.id) ?? 'other', - title: navLink.title, - description: navLink.description ?? '', - icon: navLink.landingIcon ?? '', - href: application.getUrlForApp(appId, { deepLinkId, path }), - skipValidation: true, - }; - } - return acc; - }, - {} - ); - - management.setupCardsNavigation({ - enabled: isSolutionNavEnabled, - extendCardNavDefinitions: cardNavDefinitions, - }); - }); -}; diff --git a/x-pack/plugins/security_solution_ess/tsconfig.json b/x-pack/plugins/security_solution_ess/tsconfig.json index f38811b701391..6409a15bd7d27 100644 --- a/x-pack/plugins/security_solution_ess/tsconfig.json +++ b/x-pack/plugins/security_solution_ess/tsconfig.json @@ -24,7 +24,6 @@ "@kbn/security-solution-upselling", "@kbn/i18n", "@kbn/navigation-plugin", - "@kbn/management-cards-navigation", "@kbn/management-plugin", "@kbn/core-chrome-browser", ] diff --git a/x-pack/plugins/transform/kibana.jsonc b/x-pack/plugins/transform/kibana.jsonc index 499206b4db924..1f8ab0fe72f40 100644 --- a/x-pack/plugins/transform/kibana.jsonc +++ b/x-pack/plugins/transform/kibana.jsonc @@ -40,7 +40,6 @@ "discover", "kibanaUtils", "kibanaReact", - "ml" ], "extraPublicDirs": [ "common" diff --git a/x-pack/plugins/transform/public/app/__mocks__/app_dependencies.tsx b/x-pack/plugins/transform/public/app/__mocks__/app_dependencies.tsx index 6af01088b70db..e0536a3e1b4d6 100644 --- a/x-pack/plugins/transform/public/app/__mocks__/app_dependencies.tsx +++ b/x-pack/plugins/transform/public/app/__mocks__/app_dependencies.tsx @@ -5,7 +5,6 @@ * 2.0. */ -import { useContext } from 'react'; import { of } from 'rxjs'; import type { IKibanaSearchResponse, @@ -25,8 +24,6 @@ import { savedSearchPluginMock } from '@kbn/saved-search-plugin/public/mocks'; import { contentManagementMock } from '@kbn/content-management-plugin/public/mocks'; import type { AppDependencies } from '../app_dependencies'; -import { MlSharedContext } from './shared_context'; -import type { GetMlSharedImportsReturnType } from '../../shared_imports'; import type { SavedObjectsManagementPluginStart } from '@kbn/saved-objects-management-plugin/public'; import { settingsServiceMock } from '@kbn/core-ui-settings-browser-mocks'; import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks'; @@ -88,7 +85,6 @@ const appDependencies: AppDependencies = { http: coreSetup.http, history: {} as ScopedHistory, share: { urlGenerators: { getUrlGenerator: jest.fn() } } as unknown as SharePluginStart, - ml: {} as GetMlSharedImportsReturnType, triggersActionsUi: {} as jest.Mocked, unifiedSearch: unifiedSearchPluginMock.createStartContract(), savedObjectsManagement: {} as jest.Mocked, @@ -98,8 +94,7 @@ const appDependencies: AppDependencies = { }; export const useAppDependencies = () => { - const ml = useContext(MlSharedContext); - return { ...appDependencies, ml }; + return appDependencies; }; export const useToastNotifications = () => { diff --git a/x-pack/plugins/transform/public/app/__mocks__/shared_context.ts b/x-pack/plugins/transform/public/app/__mocks__/shared_context.ts deleted file mode 100644 index 33e43ee54cee7..0000000000000 --- a/x-pack/plugins/transform/public/app/__mocks__/shared_context.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { createContext } from 'react'; - -import type { GetMlSharedImportsReturnType } from '../../shared_imports'; - -// This code is a workaround to provide dependencies that are -// loaded dynamically during runtime. -export const MlSharedContext = createContext({} as GetMlSharedImportsReturnType); diff --git a/x-pack/plugins/transform/public/app/app_dependencies.tsx b/x-pack/plugins/transform/public/app/app_dependencies.tsx index 1f8b2373f0f7d..9033540ee0d4c 100644 --- a/x-pack/plugins/transform/public/app/app_dependencies.tsx +++ b/x-pack/plugins/transform/public/app/app_dependencies.tsx @@ -37,7 +37,6 @@ import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; import type { SavedObjectsManagementPluginStart } from '@kbn/saved-objects-management-plugin/public'; import type { SettingsStart } from '@kbn/core-ui-settings-browser'; import type { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public'; -import type { GetMlSharedImportsReturnType } from '../shared_imports'; export interface AppDependencies { analytics: AnalyticsServiceStart; @@ -60,7 +59,6 @@ export interface AppDependencies { theme: ThemeServiceStart; history: ScopedHistory; share: SharePluginStart; - ml: GetMlSharedImportsReturnType; spaces?: SpacesPluginStart; triggersActionsUi: TriggersAndActionsUIPublicPluginStart; unifiedSearch: UnifiedSearchPublicPluginStart; diff --git a/x-pack/plugins/transform/public/app/common/aggregations.ts b/x-pack/plugins/transform/public/app/common/aggregations.ts index 50582d2f278ea..3fd41f1dd4111 100644 --- a/x-pack/plugins/transform/public/app/common/aggregations.ts +++ b/x-pack/plugins/transform/public/app/common/aggregations.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { composeValidators, patternValidator } from '../../../common/shared_imports'; +import { composeValidators, patternValidator } from '@kbn/ml-validators'; import type { AggName } from '../../../common/types/aggregations'; diff --git a/x-pack/plugins/transform/public/app/components/toast_notification_text.test.tsx b/x-pack/plugins/transform/public/app/components/toast_notification_text.test.tsx index f3781bc97b4aa..c058bc521653e 100644 --- a/x-pack/plugins/transform/public/app/components/toast_notification_text.test.tsx +++ b/x-pack/plugins/transform/public/app/components/toast_notification_text.test.tsx @@ -13,7 +13,6 @@ import { themeServiceMock } from '@kbn/core/public/mocks'; import { ToastNotificationText } from './toast_notification_text'; -jest.mock('../../shared_imports'); jest.mock('../app_dependencies'); describe('ToastNotificationText', () => { diff --git a/x-pack/plugins/transform/public/app/hooks/use_index_data.test.tsx b/x-pack/plugins/transform/public/app/hooks/use_index_data.test.tsx index 52fc601db27fc..ee7e2ca1e7256 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_index_data.test.tsx +++ b/x-pack/plugins/transform/public/app/hooks/use_index_data.test.tsx @@ -16,16 +16,11 @@ import { DataGrid, type UseIndexDataReturnType } from '@kbn/ml-data-grid'; import type { RuntimeMappings } from '@kbn/ml-runtime-field-utils'; import type { SimpleQuery } from '@kbn/ml-query-utils'; -import { getMlSharedImports } from '../../shared_imports'; - import type { SearchItems } from './use_search_items'; import { useIndexData } from './use_index_data'; -jest.mock('../../shared_imports'); jest.mock('../app_dependencies'); -import { MlSharedContext } from '../__mocks__/shared_context'; - const query: SimpleQuery = { query_string: { query: '*', @@ -46,12 +41,9 @@ const queryClient = new QueryClient(); describe('Transform: useIndexData()', () => { test('dataView set triggers loading', async () => { - const mlShared = await getMlSharedImports(); const wrapper: FC> = ({ children }) => ( - - {children} - + {children} ); @@ -87,8 +79,6 @@ describe('Transform: with useIndexData()', () => { fields: [] as any[], } as SearchItems['dataView']; - const mlSharedImports = await getMlSharedImports(); - const Wrapper = () => { const props = { ...useIndexData(dataView, { match_all: {} }, runtimeMappings), @@ -105,9 +95,7 @@ describe('Transform: with useIndexData()', () => { render( - - - + ); @@ -129,8 +117,6 @@ describe('Transform: with useIndexData()', () => { fields: [] as any[], } as SearchItems['dataView']; - const mlSharedImports = await getMlSharedImports(); - const Wrapper = () => { const props = { ...useIndexData(dataView, { match_all: {} }, runtimeMappings), @@ -147,9 +133,7 @@ describe('Transform: with useIndexData()', () => { render( - - - + ); diff --git a/x-pack/plugins/transform/public/app/mount_management_section.ts b/x-pack/plugins/transform/public/app/mount_management_section.ts index 8d59c9ce2d0f2..35ae95feb9a44 100644 --- a/x-pack/plugins/transform/public/app/mount_management_section.ts +++ b/x-pack/plugins/transform/public/app/mount_management_section.ts @@ -11,7 +11,6 @@ import { Storage } from '@kbn/kibana-utils-plugin/public'; import { type TransformEnabledFeatures } from './serverless_context'; import type { PluginsDependencies } from '../plugin'; -import { getMlSharedImports } from '../shared_imports'; import type { ExperimentalFeatures } from '../../server/config'; import type { AppDependencies } from './app_dependencies'; @@ -87,7 +86,6 @@ export async function mountManagementSection( history, share, spaces, - ml: await getMlSharedImports(), triggersActionsUi, unifiedSearch, charts, diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/pivot_configuration/pivot_configuration.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/pivot_configuration/pivot_configuration.tsx index ed63b8791be59..798837a1a693f 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/pivot_configuration/pivot_configuration.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/pivot_configuration/pivot_configuration.tsx @@ -11,10 +11,10 @@ import React, { memo, createContext, useMemo } from 'react'; import { EuiFormRow, type EuiComboBoxOptionOption } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { useFieldStatsTrigger, FieldStatsInfoButton } from '@kbn/ml-field-stats-flyout'; import { type DropDownOptionWithField } from '../step_define/common/get_pivot_dropdown_options'; import type { DropDownOption } from '../../../../common'; -import { useAppDependencies } from '../../../../app_dependencies'; import { AggListForm } from '../aggregation_list'; import { DropDown } from '../aggregation_dropdown'; import { GroupByListForm } from '../group_by_list'; @@ -26,9 +26,6 @@ export const PivotConfigurationContext = createContext< export const PivotConfiguration: FC = memo( ({ actions, state }) => { - const { - ml: { useFieldStatsTrigger, FieldStatsInfoButton }, - } = useAppDependencies(); const { handleFieldStatsButtonClick, closeFlyout, renderOption, populatedFields } = useFieldStatsTrigger(); @@ -64,7 +61,7 @@ export const PivotConfiguration: FC = memo( }; return aggOption; }), - [aggOptions, FieldStatsInfoButton, handleFieldStatsButtonClick, populatedFields] + [aggOptions, handleFieldStatsButtonClick, populatedFields] ); return ( diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.test.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.test.tsx index 6ce8984a8230c..f045d553abb6c 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.test.tsx @@ -12,7 +12,6 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import type { StepCreateFormProps } from './step_create_form'; import { StepCreateForm } from './step_create_form'; -jest.mock('../../../../../shared_imports'); jest.mock('../../../../app_dependencies'); describe('Transform: ', () => { diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/latest_function_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/latest_function_form.tsx index f97b6cda583a5..9ded43a82c71a 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/latest_function_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/latest_function_form.tsx @@ -10,7 +10,7 @@ import React, { type FC } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiButtonIcon, EuiCallOut, EuiComboBox, EuiCopy, EuiFormRow } from '@elastic/eui'; -import { useAppDependencies } from '../../../../app_dependencies'; +import { useFieldStatsTrigger } from '@kbn/ml-field-stats-flyout'; import type { LatestFunctionService } from './hooks/use_latest_function_config'; interface LatestFunctionFormProps { @@ -24,9 +24,6 @@ export const LatestFunctionForm: FC = ({ copyToClipboardDescription, latestFunctionService, }) => { - const { - ml: { useFieldStatsTrigger }, - } = useAppDependencies(); const { renderOption, closeFlyout } = useFieldStatsTrigger(); return ( <> diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.test.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.test.tsx index fa1ce2f2c7e99..7490493bcfe56 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.test.tsx @@ -25,11 +25,8 @@ import type { SearchItems } from '../../../../hooks/use_search_items'; import { getAggNameConflictToastMessages } from './common'; import { StepDefineForm } from './step_define_form'; -import { MlSharedContext } from '../../../../__mocks__/shared_context'; -import { getMlSharedImports } from '../../../../../shared_imports'; import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks'; -jest.mock('../../../../../shared_imports'); jest.mock('../../../../app_dependencies'); const startMock = coreMock.createStart(); @@ -66,7 +63,6 @@ describe('Transform: ', () => { test('Minimal initialization', async () => { // Arrange const queryClient = new QueryClient(); - const mlSharedImports = await getMlSharedImports(); const searchItems = { dataView: { @@ -90,11 +86,9 @@ describe('Transform: ', () => { - - - - - + + + diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx index ac8ccabc539cb..84ca627904a51 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx @@ -35,6 +35,7 @@ import { } from '@kbn/ml-date-picker'; import { useStorage } from '@kbn/ml-local-storage'; import { useUrlState } from '@kbn/ml-url-state'; +import { useFieldStatsFlyoutContext } from '@kbn/ml-field-stats-flyout'; import type { PivotAggDict } from '../../../../../../common/types/pivot_aggs'; import type { PivotGroupByDict } from '../../../../../../common/types/pivot_group_by'; @@ -59,7 +60,7 @@ import { getPreviewTransformRequestBody } from '../../../../common'; import { useDocumentationLinks } from '../../../../hooks/use_documentation_links'; import { useIndexData } from '../../../../hooks/use_index_data'; import { useTransformConfigData } from '../../../../hooks/use_transform_config_data'; -import { useAppDependencies, useToastNotifications } from '../../../../app_dependencies'; +import { useToastNotifications } from '../../../../app_dependencies'; import type { SearchItems } from '../../../../hooks/use_search_items'; import { getAggConfigFromEsAgg } from '../../../../common/pivot_aggs'; @@ -124,11 +125,6 @@ export const StepDefineForm: FC = React.memo((props) => { const { transformConfigQuery } = stepDefineForm.searchBar.state; const { runtimeMappings } = stepDefineForm.runtimeMappingsEditor.state; - const appDependencies = useAppDependencies(); - const { - ml: { useFieldStatsFlyoutContext }, - } = appDependencies; - const fieldStatsContext = useFieldStatsFlyoutContext(); const indexPreviewProps = { ...useIndexData( diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.test.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.test.tsx index 8fef5cf0b934d..28efd40ad03fd 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.test.tsx @@ -18,18 +18,13 @@ import type { SearchItems } from '../../../../hooks/use_search_items'; import type { StepDefineExposedState } from './common'; import { StepDefineSummary } from './step_define_summary'; -jest.mock('../../../../../shared_imports'); jest.mock('../../../../app_dependencies'); -import { MlSharedContext } from '../../../../__mocks__/shared_context'; -import { getMlSharedImports } from '../../../../../shared_imports'; - describe('Transform: ', () => { // Using the async/await wait()/done() pattern to avoid act() errors. test('Minimal initialization', async () => { // Arrange const queryClient = new QueryClient(); - const mlSharedImports = await getMlSharedImports(); const searchItems = { dataView: { @@ -78,9 +73,7 @@ describe('Transform: ', () => { const { queryByText } = render( - - - + ); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/wizard/wizard.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/wizard/wizard.tsx index 606a027f4542a..ab2865b85eb8a 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/wizard/wizard.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/wizard/wizard.tsx @@ -20,6 +20,7 @@ import { UrlStateProvider } from '@kbn/ml-url-state'; import { UI_SETTINGS } from '@kbn/data-plugin/common'; import type { FieldStatsServices } from '@kbn/unified-field-list/src/components/field_stats'; import type { RuntimeMappings } from '@kbn/ml-runtime-field-utils'; +import { FieldStatsFlyoutProvider } from '@kbn/ml-field-stats-flyout'; import { useEnabledFeatures } from '../../../../serverless_context'; import type { TransformConfigUnion } from '../../../../../../common/types/transform'; @@ -110,13 +111,7 @@ export const CreateTransformWizardContext = createContext<{ export const Wizard: FC = React.memo(({ cloneConfig, searchItems }) => { const { showNodeInfo } = useEnabledFeatures(); const appDependencies = useAppDependencies(); - const { - ml: { FieldStatsFlyoutProvider }, - uiSettings, - data, - fieldFormats, - charts, - } = appDependencies; + const { uiSettings, data, fieldFormats, charts } = appDependencies; const { dataView } = searchItems; // The current WIZARD_STEP diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/delete_action_name.test.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/delete_action_name.test.tsx index cf4e6aef4e537..d5caa80792b0e 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/delete_action_name.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/delete_action_name.test.tsx @@ -11,7 +11,6 @@ import { render } from '@testing-library/react'; import type { DeleteActionNameProps } from './delete_action_name'; import { DeleteActionName } from './delete_action_name'; -jest.mock('../../../../../shared_imports'); jest.mock('../../../../app_dependencies'); describe('Transform: Transform List Actions ', () => { diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_discover/discover_action_name.test.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_discover/discover_action_name.test.tsx index 0717dea8a4d0d..b4dbcad166583 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_discover/discover_action_name.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_discover/discover_action_name.test.tsx @@ -16,7 +16,6 @@ import { isDiscoverActionDisabled, DiscoverActionName } from './discover_action_ import transformListRow from '../../../../common/__mocks__/transform_list_row.json'; import type { TransformListRowWithStats } from '../../../../common/transform_list'; -jest.mock('../../../../../shared_imports'); jest.mock('../../../../app_dependencies'); // @ts-expect-error mock data is too loosely typed diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/start_action_name.test.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/start_action_name.test.tsx index 9a1a7b2e77cb2..4f3bd2a87f919 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/start_action_name.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/start_action_name.test.tsx @@ -15,7 +15,6 @@ import { StartActionName } from './start_action_name'; import transformListRow from '../../../../common/__mocks__/transform_list_row.json'; -jest.mock('../../../../../shared_imports'); jest.mock('../../../../app_dependencies'); const queryClient = new QueryClient(); diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_stop/stop_action_name.test.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_stop/stop_action_name.test.tsx index c7118b9d4150c..0ef47ae51debc 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_stop/stop_action_name.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_stop/stop_action_name.test.tsx @@ -15,7 +15,6 @@ import { StopActionName } from './stop_action_name'; import transformListRow from '../../../../common/__mocks__/transform_list_row.json'; -jest.mock('../../../../../shared_imports'); jest.mock('../../../../app_dependencies'); const queryClient = new QueryClient(); diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/create_transform_button/create_transform_button.test.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/create_transform_button/create_transform_button.test.tsx index 644971bc32931..fceb52cb0cd4e 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/create_transform_button/create_transform_button.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/create_transform_button/create_transform_button.test.tsx @@ -12,8 +12,6 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { CreateTransformButton } from './create_transform_button'; -jest.mock('../../../../../shared_imports'); - const queryClient = new QueryClient(); describe('Transform: Transform List ', () => { diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row.test.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row.test.tsx index 119ce5dd92dba..28c82d11a1c46 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row.test.tsx @@ -15,12 +15,8 @@ import { ExpandedRow } from './expanded_row'; import transformListRow from '../../../../common/__mocks__/transform_list_row.json'; -jest.mock('../../../../../shared_imports'); jest.mock('../../../../app_dependencies'); -import { MlSharedContext } from '../../../../__mocks__/shared_context'; -import { getMlSharedImports } from '../../../../../shared_imports'; - const queryClient = new QueryClient(); describe('Transform: Transform List ', () => { @@ -35,15 +31,12 @@ describe('Transform: Transform List ', () => { }); test('Minimal initialization', async () => { - const mlShared = await getMlSharedImports(); // @ts-expect-error mock data is too loosely typed const item: TransformListRow = transformListRow; renderReactTestingLibraryWithI18n( - - - + ); diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.test.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.test.tsx index ede0470d917f2..28e2cca179a19 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.test.tsx @@ -25,7 +25,6 @@ const useQueryMock = jest.spyOn(ReactQuery, 'useQuery').mockImplementation((quer const queryClient = new QueryClient(); -jest.mock('../../../../../shared_imports'); jest.mock('../../../../app_dependencies'); describe('Transform: Transform List ', () => { diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_actions.test.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_actions.test.tsx index 02064c76c95ce..5007065113782 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_actions.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_actions.test.tsx @@ -9,7 +9,6 @@ import React, { type FC, type PropsWithChildren } from 'react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { renderHook } from '@testing-library/react-hooks'; -jest.mock('../../../../../shared_imports'); jest.mock('../../../../app_dependencies'); import { useActions } from './use_actions'; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_columns.test.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_columns.test.tsx index b1d2b6dcb0d42..dcab70822ecd9 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_columns.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_columns.test.tsx @@ -11,7 +11,6 @@ import { renderHook } from '@testing-library/react-hooks'; import { useColumns } from './use_columns'; -jest.mock('../../../../../shared_imports'); jest.mock('../../../../app_dependencies'); describe('Transform: Job List Columns', () => { diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/transform_management_section.test.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/transform_management_section.test.tsx index 20f5f4c27b19f..be4bd374eb673 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/transform_management_section.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/transform_management_section.test.tsx @@ -11,7 +11,6 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { TransformManagementSection } from './transform_management_section'; -jest.mock('../../../shared_imports'); jest.mock('../../services/navigation'); const queryClient = new QueryClient(); diff --git a/x-pack/plugins/transform/public/shared_imports.ts b/x-pack/plugins/transform/public/shared_imports.ts deleted file mode 100644 index 63276cecc7a86..0000000000000 --- a/x-pack/plugins/transform/public/shared_imports.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export { XJsonMode } from '@kbn/ace'; - -export type { GetMlSharedImportsReturnType } from '@kbn/ml-plugin/public'; -export { getMlSharedImports } from '@kbn/ml-plugin/public'; - -import { XJson } from '@kbn/es-ui-shared-plugin/public'; -const { expandLiteralStrings, collapseLiteralStrings } = XJson; -export { expandLiteralStrings, collapseLiteralStrings }; diff --git a/x-pack/plugins/transform/tsconfig.json b/x-pack/plugins/transform/tsconfig.json index e3ccb0cc1d403..c04d45922aa40 100644 --- a/x-pack/plugins/transform/tsconfig.json +++ b/x-pack/plugins/transform/tsconfig.json @@ -15,7 +15,6 @@ "@kbn/core", "@kbn/features-plugin", "@kbn/licensing-plugin", - "@kbn/ml-plugin", "@kbn/unified-search-plugin", "@kbn/data-plugin", "@kbn/config-schema", @@ -78,7 +77,9 @@ "@kbn/core-elasticsearch-server-mocks", "@kbn/test-jest-helpers", "@kbn/monaco", - "@kbn/json-schemas" + "@kbn/json-schemas", + "@kbn/ml-field-stats-flyout", + "@kbn/ml-validators" ], "exclude": [ "target/**/*", diff --git a/x-pack/test/api_integration/apis/synthetics/add_monitor.ts b/x-pack/test/api_integration/apis/synthetics/add_monitor.ts index 311e8072c4aea..89fc77c034072 100644 --- a/x-pack/test/api_integration/apis/synthetics/add_monitor.ts +++ b/x-pack/test/api_integration/apis/synthetics/add_monitor.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; import epct from 'expect'; import moment from 'moment/moment'; import { v4 as uuidv4 } from 'uuid'; -import { omit, omitBy } from 'lodash'; +import { omit } from 'lodash'; import { ConfigKey, MonitorTypeEnum, @@ -23,10 +23,7 @@ import { format as formatUrl } from 'url'; import supertest from 'supertest'; import { getServiceApiKeyPrivileges } from '@kbn/synthetics-plugin/server/synthetics_service/get_api_key'; import { syntheticsMonitorType } from '@kbn/synthetics-plugin/common/types/saved_objects'; -import { - removeMonitorEmptyValues, - transformPublicKeys, -} from '@kbn/synthetics-plugin/server/routes/monitor_cruds/helper'; +import { transformPublicKeys } from '@kbn/synthetics-plugin/server/routes/monitor_cruds/formatters/saved_object_to_monitor'; import { FtrProviderContext } from '../../ftr_provider_context'; import { getFixtureJson } from './helper/get_fixture_json'; import { SyntheticsMonitorTestService } from './services/synthetics_monitor_test_service'; @@ -54,8 +51,10 @@ export const addMonitorAPIHelper = async (supertestAPI: any, monitor: any, statu return result.body; }; +export const keyToOmitList = ['created_at', 'updated_at', 'id', 'config_id', 'form_monitor_type']; + export const omitMonitorKeys = (monitor: any) => { - return omitBy(transformPublicKeys(monitor), removeMonitorEmptyValues); + return omit(transformPublicKeys(monitor), keyToOmitList); }; export default function ({ getService }: FtrProviderContext) { @@ -154,10 +153,10 @@ export default function ({ getService }: FtrProviderContext) { const { body: apiResponse } = await addMonitorAPI(newMonitor); - epct(apiResponse).toEqual(epct.objectContaining({ max_attempts: maxAttempts })); + epct(apiResponse).toEqual(epct.objectContaining({ retest_on_failure: false })); }); - it('can enable retries', async () => { + it('can enable retries with max attempts', async () => { const maxAttempts = 2; const newMonitor = { max_attempts: maxAttempts, @@ -169,7 +168,21 @@ export default function ({ getService }: FtrProviderContext) { const { body: apiResponse } = await addMonitorAPI(newMonitor); - epct(apiResponse).toEqual(epct.objectContaining({ max_attempts: maxAttempts })); + epct(apiResponse).toEqual(epct.objectContaining({ retest_on_failure: true })); + }); + + it('can enable retries', async () => { + const newMonitor = { + retest_on_failure: false, + urls: 'https://elastic.co', + name: `Sample name ${uuidv4()}`, + type: 'http', + locations: [localLoc], + }; + + const { body: apiResponse } = await addMonitorAPI(newMonitor); + + epct(apiResponse).toEqual(epct.objectContaining({ retest_on_failure: false })); }); it('cannot create a invalid monitor without a monitor type', async () => { diff --git a/x-pack/test/api_integration/apis/synthetics/add_monitor_private_location.ts b/x-pack/test/api_integration/apis/synthetics/add_monitor_private_location.ts index 3840d19fbc3a7..044e66fe239f7 100644 --- a/x-pack/test/api_integration/apis/synthetics/add_monitor_private_location.ts +++ b/x-pack/test/api_integration/apis/synthetics/add_monitor_private_location.ts @@ -26,7 +26,7 @@ import { INSTALLED_VERSION, PrivateLocationTestService, } from './services/private_location_test_service'; -import { addMonitorAPIHelper, omitMonitorKeys } from './add_monitor'; +import { addMonitorAPIHelper, keyToOmitList, omitMonitorKeys } from './add_monitor'; import { SyntheticsMonitorTestService } from './services/synthetics_monitor_test_service'; export default function ({ getService }: FtrProviderContext) { @@ -211,12 +211,10 @@ export default function ({ getService }: FtrProviderContext) { const { created_at: createdAt, updated_at: updatedAt } = apiResponse.body; expect([createdAt, updatedAt].map((d) => moment(d).isValid())).eql([true, true]); - expect(apiResponse.body).eql( + expect(omit(apiResponse.body, keyToOmitList)).eql( omitMonitorKeys({ ...omit(httpMonitorJson, ['urls']), url: httpMonitorJson.urls, - [ConfigKey.MONITOR_QUERY_ID]: apiResponse.body.id, - [ConfigKey.CONFIG_ID]: apiResponse.body.id, updated_at: updatedAt, revision: 2, }) @@ -269,7 +267,7 @@ export default function ({ getService }: FtrProviderContext) { ); await supertestAPI - .put(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS + '/' + newMonitorId + '?ui=true') + .put(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS + '/' + newMonitorId + '?internal=true') .set('kbn-xsrf', 'true') .send(httpMonitorJson) .expect(200); @@ -370,15 +368,11 @@ export default function ({ getService }: FtrProviderContext) { const { created_at: createdAt, updated_at: updatedAt } = apiResponse.body; expect([createdAt, updatedAt].map((d) => moment(d).isValid())).eql([true, true]); - expect(apiResponse.body).eql( + expect(omit(apiResponse.body, keyToOmitList)).eql( omitMonitorKeys({ ...monitor, - [ConfigKey.MONITOR_QUERY_ID]: apiResponse.body.id, - [ConfigKey.CONFIG_ID]: apiResponse.body.id, [ConfigKey.NAMESPACE]: formatKibanaNamespace(SPACE_ID), url: apiResponse.body.url, - created_at: createdAt, - updated_at: updatedAt, }) ); monitorId = apiResponse.body.id; diff --git a/x-pack/test/api_integration/apis/synthetics/add_monitor_project.ts b/x-pack/test/api_integration/apis/synthetics/add_monitor_project.ts index 074f318e77fcf..bd6f94a44f6c1 100644 --- a/x-pack/test/api_integration/apis/synthetics/add_monitor_project.ts +++ b/x-pack/test/api_integration/apis/synthetics/add_monitor_project.ts @@ -170,17 +170,20 @@ export default function ({ getService }: FtrProviderContext) { .expect(200); const decryptedCreatedMonitor = await monitorTestService.getMonitor( - createdMonitorsResponse.body.monitors[0].config_id + createdMonitorsResponse.body.monitors[0].config_id, + { + internal: true, + } ); - expect(decryptedCreatedMonitor.body).to.eql({ + expect(decryptedCreatedMonitor.rawBody).to.eql({ __ui: { script_source: { file_name: '', is_generated_script: false, }, }, - config_id: decryptedCreatedMonitor.body.config_id, + config_id: decryptedCreatedMonitor.rawBody.config_id, custom_heartbeat_id: `${journeyId}-${project}-default`, enabled: true, alert: { @@ -241,6 +244,8 @@ export default function ({ getService }: FtrProviderContext) { id: `${journeyId}-${project}-default`, hash: 'ekrjelkjrelkjre', max_attempts: 2, + updated_at: decryptedCreatedMonitor.rawBody.updated_at, + created_at: decryptedCreatedMonitor.rawBody.created_at, labels: {}, }); } @@ -341,17 +346,20 @@ export default function ({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'true') .expect(200); - const decryptedCreatedMonitor = await monitorTestService.getMonitor( - createdMonitorsResponse.body.monitors[0].config_id + const { rawBody: decryptedCreatedMonitor } = await monitorTestService.getMonitor( + createdMonitorsResponse.body.monitors[0].config_id, + { + internal: true, + } ); - expect(decryptedCreatedMonitor.body).to.eql({ + expect(decryptedCreatedMonitor).to.eql({ __ui: { is_tls_enabled: isTLSEnabled, }, 'check.request.method': 'POST', 'check.response.status': ['200'], - config_id: decryptedCreatedMonitor.body.config_id, + config_id: decryptedCreatedMonitor.config_id, custom_heartbeat_id: `${journeyId}-${project}-default`, 'check.response.body.negative': [], 'check.response.body.positive': ['${testLocal1}', 'saved'], @@ -364,8 +372,10 @@ export default function ({ getService }: FtrProviderContext) { type: 'text', value: '', }, - params: - '{"testLocal1":"testLocalParamsValue","testGlobalParam2":"testGlobalParamOverwrite"}', + params: JSON.stringify({ + testLocal1: 'testLocalParamsValue', + testGlobalParam2: 'testGlobalParamOverwrite', + }), 'check.request.headers': { 'Content-Type': 'application/x-www-form-urlencoded', }, @@ -427,6 +437,8 @@ export default function ({ getService }: FtrProviderContext) { ipv4: true, max_attempts: 2, labels: {}, + updated_at: decryptedCreatedMonitor.updated_at, + created_at: decryptedCreatedMonitor.created_at, }); } } finally { @@ -478,15 +490,18 @@ export default function ({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'true') .expect(200); - const decryptedCreatedMonitor = await monitorTestService.getMonitor( - createdMonitorsResponse.body.monitors[0].config_id + const { rawBody: decryptedCreatedMonitor } = await monitorTestService.getMonitor( + createdMonitorsResponse.body.monitors[0].config_id, + { + internal: true, + } ); - expect(decryptedCreatedMonitor.body).to.eql({ + expect(decryptedCreatedMonitor).to.eql({ __ui: { is_tls_enabled: isTLSEnabled, }, - config_id: decryptedCreatedMonitor.body.config_id, + config_id: decryptedCreatedMonitor.config_id, custom_heartbeat_id: `${journeyId}-${project}-default`, 'check.receive': '', 'check.send': '', @@ -545,6 +560,8 @@ export default function ({ getService }: FtrProviderContext) { params: '', max_attempts: 2, labels: {}, + updated_at: decryptedCreatedMonitor.updated_at, + created_at: decryptedCreatedMonitor.created_at, }); } } finally { @@ -594,12 +611,15 @@ export default function ({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'true') .expect(200); - const decryptedCreatedMonitor = await monitorTestService.getMonitor( - createdMonitorsResponse.body.monitors[0].config_id + const { rawBody: decryptedCreatedMonitor } = await monitorTestService.getMonitor( + createdMonitorsResponse.body.monitors[0].config_id, + { + internal: true, + } ); - expect(decryptedCreatedMonitor.body).to.eql({ - config_id: decryptedCreatedMonitor.body.config_id, + expect(decryptedCreatedMonitor).to.eql({ + config_id: decryptedCreatedMonitor.config_id, custom_heartbeat_id: `${journeyId}-${project}-default`, enabled: true, alert: { @@ -659,6 +679,8 @@ export default function ({ getService }: FtrProviderContext) { ipv6: true, params: '', max_attempts: 2, + updated_at: decryptedCreatedMonitor.updated_at, + created_at: decryptedCreatedMonitor.created_at, labels: {}, }); } @@ -1106,8 +1128,7 @@ export default function ({ getService }: FtrProviderContext) { const decryptedCreatedMonitor = await monitorTestService.getMonitor( getResponse.body.monitors[0].config_id, - true, - SPACE_ID + { internal: true, space: SPACE_ID } ); const { monitors } = getResponse.body; expect(monitors.length).eql(1); @@ -1144,8 +1165,7 @@ export default function ({ getService }: FtrProviderContext) { const decryptedUpdatedMonitor = await monitorTestService.getMonitor( monitorsUpdated[0].config_id, - true, - SPACE_ID + { internal: true, space: SPACE_ID } ); expect(decryptedUpdatedMonitor.body[ConfigKey.SOURCE_PROJECT_CONTENT]).eql(updatedSource); } finally { diff --git a/x-pack/test/api_integration/apis/synthetics/add_monitor_public_api.ts b/x-pack/test/api_integration/apis/synthetics/add_monitor_public_api.ts index df6a0b0df9056..5da370d1c634f 100644 --- a/x-pack/test/api_integration/apis/synthetics/add_monitor_public_api.ts +++ b/x-pack/test/api_integration/apis/synthetics/add_monitor_public_api.ts @@ -6,13 +6,8 @@ */ import expect from '@kbn/expect'; import { v4 as uuidv4 } from 'uuid'; -import { omitBy } from 'lodash'; import { DEFAULT_FIELDS } from '@kbn/synthetics-plugin/common/constants/monitor_defaults'; -import { - removeMonitorEmptyValues, - transformPublicKeys, -} from '@kbn/synthetics-plugin/server/routes/monitor_cruds/helper'; import { LOCATION_REQUIRED_ERROR } from '@kbn/synthetics-plugin/server/routes/monitor_cruds/monitor_validation'; import { FtrProviderContext } from '../../ftr_provider_context'; import { addMonitorAPIHelper, omitMonitorKeys } from './add_monitor'; @@ -101,7 +96,7 @@ export default function ({ getService }: FtrProviderContext) { }); describe('HTTP Monitor', () => { - const defaultFields = omitBy(DEFAULT_FIELDS.http, removeMonitorEmptyValues); + const defaultFields = DEFAULT_FIELDS.http; it('return error empty http', async () => { const { message, attributes } = await addMonitorAPI( { @@ -154,8 +149,7 @@ export default function ({ getService }: FtrProviderContext) { ...monitor, locations: [localLoc], name, - max_attempts: 2, - retest_on_failure: undefined, // this key is not part of the SO and should not be defined + retest_on_failure: true, }) ); }); @@ -185,7 +179,7 @@ export default function ({ getService }: FtrProviderContext) { }); describe('TCP Monitor', () => { - const defaultFields = omitBy(DEFAULT_FIELDS.tcp, removeMonitorEmptyValues); + const defaultFields = DEFAULT_FIELDS.tcp; it('base tcp monitor', async () => { const monitor = { @@ -207,7 +201,7 @@ export default function ({ getService }: FtrProviderContext) { }); describe('ICMP Monitor', () => { - const defaultFields = omitBy(DEFAULT_FIELDS.icmp, removeMonitorEmptyValues); + const defaultFields = DEFAULT_FIELDS.icmp; it('base icmp monitor', async () => { const monitor = { @@ -229,7 +223,7 @@ export default function ({ getService }: FtrProviderContext) { }); describe('Browser Monitor', () => { - const defaultFields = omitBy(DEFAULT_FIELDS.browser, removeMonitorEmptyValues); + const defaultFields = DEFAULT_FIELDS.browser; it('empty browser monitor', async () => { const monitor = { @@ -259,7 +253,7 @@ export default function ({ getService }: FtrProviderContext) { }; const { body: result } = await addMonitorAPI(monitor); - expect(transformPublicKeys(result)).eql( + expect(result).eql( omitMonitorKeys({ ...defaultFields, ...monitor, diff --git a/x-pack/test/api_integration/apis/synthetics/edit_monitor.ts b/x-pack/test/api_integration/apis/synthetics/edit_monitor.ts index 1504384cef030..522a359c6d51b 100644 --- a/x-pack/test/api_integration/apis/synthetics/edit_monitor.ts +++ b/x-pack/test/api_integration/apis/synthetics/edit_monitor.ts @@ -6,7 +6,7 @@ */ import moment from 'moment'; import { v4 as uuidv4 } from 'uuid'; -import { omit, omitBy } from 'lodash'; +import { omit } from 'lodash'; import { ConfigKey, EncryptedSyntheticsSavedMonitor, @@ -15,7 +15,6 @@ import { } from '@kbn/synthetics-plugin/common/runtime_types'; import { SYNTHETICS_API_URLS } from '@kbn/synthetics-plugin/common/constants'; import expect from '@kbn/expect'; -import { removeMonitorEmptyValues } from '@kbn/synthetics-plugin/server/routes/monitor_cruds/helper'; import { FtrProviderContext } from '../../ftr_provider_context'; import { getFixtureJson } from './helper/get_fixture_json'; import { omitResponseTimestamps, omitEmptyValues } from './helper/monitor'; @@ -40,12 +39,11 @@ export default function ({ getService }: FtrProviderContext) { let testPolicyId = ''; const saveMonitor = async (monitor: MonitorFields, spaceId?: string) => { + const apiURL = spaceId + ? `/s/${spaceId}${SYNTHETICS_API_URLS.SYNTHETICS_MONITORS}` + : SYNTHETICS_API_URLS.SYNTHETICS_MONITORS; const res = await supertest - .post( - spaceId - ? `/s/${spaceId}${SYNTHETICS_API_URLS.SYNTHETICS_MONITORS}` - : SYNTHETICS_API_URLS.SYNTHETICS_MONITORS - ) + .post(apiURL + '?internal=true') .set('kbn-xsrf', 'true') .send(monitor); @@ -55,21 +53,21 @@ export default function ({ getService }: FtrProviderContext) { expect([createdAt, updatedAt].map((d) => moment(d).isValid())).eql([true, true]); - return { ...rest, urls: url } as EncryptedSyntheticsSavedMonitor; + return rest as EncryptedSyntheticsSavedMonitor; }; const editMonitor = async (modifiedMonitor: MonitorFields, monitorId: string) => { const res = await supertest - .put(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS + '/' + monitorId) + .put(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS + '/' + monitorId + '?internal=true') .set('kbn-xsrf', 'true') .send(modifiedMonitor); expect(res.status).eql(200, JSON.stringify(res.body)); - const { url, ...rest } = res.body; + const { created_at: createdAt, updated_at: updatedAt } = res.body; + expect([createdAt, updatedAt].map((d) => moment(d).isValid())).eql([true, true]); - const result = { ...rest, urls: url } as EncryptedSyntheticsSavedMonitor; - return omitBy(omit(result, ['created_at', 'updated_at']), removeMonitorEmptyValues); + return omit(res.body, ['created_at', 'updated_at']); }; before(async () => { @@ -101,9 +99,6 @@ export default function ({ getService }: FtrProviderContext) { const savedMonitor = await saveMonitor(newMonitor as MonitorFields); const monitorId = savedMonitor[ConfigKey.CONFIG_ID]; - const { created_at: createdAt, updated_at: updatedAt } = savedMonitor; - expect([createdAt, updatedAt].map((d) => moment(d).isValid())).eql([true, true]); - expect(omitResponseTimestamps(savedMonitor)).eql( omitEmptyValues({ ...newMonitor, @@ -269,6 +264,7 @@ export default function ({ getService }: FtrProviderContext) { [ConfigKey.CONFIG_ID]: monitorId, [ConfigKey.MONITOR_QUERY_ID]: monitorId, name: 'test monitor - 12', + hash: configHash, }) ); @@ -399,7 +395,10 @@ export default function ({ getService }: FtrProviderContext) { .send(toUpdate) .expect(200); - const updatedResponse = await monitorTestService.getMonitor(monitorId, true, SPACE_ID); + const updatedResponse = await monitorTestService.getMonitor(monitorId, { + space: SPACE_ID, + internal: true, + }); // ensure monitor was updated expect(updatedResponse.body.urls).eql(toUpdate.urls); @@ -416,7 +415,10 @@ export default function ({ getService }: FtrProviderContext) { .send(toUpdate2) .expect(200); - const updatedResponse2 = await monitorTestService.getMonitor(monitorId, true, SPACE_ID); + const updatedResponse2 = await monitorTestService.getMonitor(monitorId, { + space: SPACE_ID, + internal: true, + }); // ensure monitor was updated expect(updatedResponse2.body.urls).eql(toUpdate2.urls); diff --git a/x-pack/test/api_integration/apis/synthetics/edit_monitor_public_api.ts b/x-pack/test/api_integration/apis/synthetics/edit_monitor_public_api.ts index ffb44dd1b00d9..fa7c780a2d971 100644 --- a/x-pack/test/api_integration/apis/synthetics/edit_monitor_public_api.ts +++ b/x-pack/test/api_integration/apis/synthetics/edit_monitor_public_api.ts @@ -5,10 +5,9 @@ * 2.0. */ import expect from '@kbn/expect'; -import { omit, omitBy } from 'lodash'; +import { omit } from 'lodash'; import { DEFAULT_FIELDS } from '@kbn/synthetics-plugin/common/constants/monitor_defaults'; -import { removeMonitorEmptyValues } from '@kbn/synthetics-plugin/server/routes/monitor_cruds/helper'; import { SYNTHETICS_API_URLS } from '@kbn/synthetics-plugin/common/constants'; import moment from 'moment'; import { PrivateLocation } from '@kbn/synthetics-plugin/common/runtime_types'; @@ -70,7 +69,7 @@ export default function ({ getService }: FtrProviderContext) { }); let monitorId = 'test-id'; - const defaultFields = omitBy(DEFAULT_FIELDS.http, removeMonitorEmptyValues); + const defaultFields = DEFAULT_FIELDS.http; it('adds test monitor', async () => { const monitor = { type: 'http', diff --git a/x-pack/test/api_integration/apis/synthetics/fixtures/browser_monitor.json b/x-pack/test/api_integration/apis/synthetics/fixtures/browser_monitor.json index 991fded06e477..1cb2d39685bf2 100644 --- a/x-pack/test/api_integration/apis/synthetics/fixtures/browser_monitor.json +++ b/x-pack/test/api_integration/apis/synthetics/fixtures/browser_monitor.json @@ -55,5 +55,6 @@ "ssl.supported_protocols": ["TLSv1.1", "TLSv1.2", "TLSv1.3"], "ssl.verification_mode": "full", "revision": 1, - "max_attempts": 2 + "max_attempts": 2, + "labels": {} } diff --git a/x-pack/test/api_integration/apis/synthetics/get_monitor.ts b/x-pack/test/api_integration/apis/synthetics/get_monitor.ts index 114f7666e7965..9f266fa42fc31 100644 --- a/x-pack/test/api_integration/apis/synthetics/get_monitor.ts +++ b/x-pack/test/api_integration/apis/synthetics/get_monitor.ts @@ -15,6 +15,8 @@ import { import { SYNTHETICS_API_URLS } from '@kbn/synthetics-plugin/common/constants'; import expect from '@kbn/expect'; import { secretKeys } from '@kbn/synthetics-plugin/common/constants/monitor_management'; +import { SyntheticsMonitorTestService } from './services/synthetics_monitor_test_service'; +import { omitMonitorKeys } from './add_monitor'; import { FtrProviderContext } from '../../ftr_provider_context'; import { getFixtureJson } from './helper/get_fixture_json'; import { LOCAL_LOCATION } from './get_filters'; @@ -26,6 +28,7 @@ export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const kibanaServer = getService('kibanaServer'); const retry = getService('retry'); + const monitorTestService = new SyntheticsMonitorTestService(getService); let _monitors: MonitorFields[]; let monitors: MonitorFields[]; @@ -189,22 +192,45 @@ export default function ({ getService }: FtrProviderContext) { monitors.map((mon) => ({ ...mon, name: mon.name + '4' })).map(saveMonitor) ); - const apiResponse = await supertest - .get( - SYNTHETICS_API_URLS.GET_SYNTHETICS_MONITOR.replace('{monitorId}', id1) + - '?decrypted=true' - ) - .expect(200); + const apiResponse = await monitorTestService.getMonitor(id1); + + expect(apiResponse.body).eql( + omitMonitorKeys({ + ...monitors[0], + [ConfigKey.MONITOR_QUERY_ID]: apiResponse.body.id, + [ConfigKey.CONFIG_ID]: apiResponse.body.id, + revision: 1, + locations: [LOCAL_LOCATION], + name: 'Test HTTP Monitor 044', + labels: {}, + }) + ); + }); - expect(apiResponse.body).eql({ - ...monitors[0], - [ConfigKey.MONITOR_QUERY_ID]: apiResponse.body.id, - [ConfigKey.CONFIG_ID]: apiResponse.body.id, - revision: 1, - locations: [LOCAL_LOCATION], - name: 'Test HTTP Monitor 044', - labels: {}, - }); + it('should get by id with ui query param', async () => { + const [{ id: id1 }] = await Promise.all( + monitors.map((mon) => ({ ...mon, name: mon.name + '5' })).map(saveMonitor) + ); + + const apiResponse = await monitorTestService.getMonitor(id1, { internal: true }); + + expect(apiResponse.body).eql( + omit( + { + ...monitors[0], + form_monitor_type: 'icmp', + revision: 1, + locations: [LOCAL_LOCATION], + name: 'Test HTTP Monitor 045', + hosts: '192.33.22.111:3333', + hash: '', + journey_id: '', + max_attempts: 2, + labels: {}, + }, + ['config_id', 'id', 'form_monitor_type'] + ) + ); }); it('returns 404 if monitor id is not found', async () => { diff --git a/x-pack/test/api_integration/apis/synthetics/helper/monitor.ts b/x-pack/test/api_integration/apis/synthetics/helper/monitor.ts index 25a5d4c438575..8c10fa78d9834 100644 --- a/x-pack/test/api_integration/apis/synthetics/helper/monitor.ts +++ b/x-pack/test/api_integration/apis/synthetics/helper/monitor.ts @@ -5,21 +5,17 @@ * 2.0. */ -import { omit, omitBy } from 'lodash'; -import { removeMonitorEmptyValues } from '@kbn/synthetics-plugin/server/routes/monitor_cruds/helper'; +import { omit } from 'lodash'; export function omitResponseTimestamps(monitor: object) { - return omitBy(omit(monitor, ['created_at', 'updated_at']), removeMonitorEmptyValues); + return omit(monitor, ['created_at', 'updated_at']); } export function omitEmptyValues(monitor: object) { - const { url, ...rest } = omit(monitor, ['created_at', 'updated_at', 'form_monitor_type']) as any; + const { url, ...rest } = omit(monitor, ['created_at', 'updated_at']) as any; - return omitBy( - { - ...rest, - ...(url ? { url } : {}), - }, - removeMonitorEmptyValues - ); + return { + ...rest, + ...(url ? { url } : {}), + }; } diff --git a/x-pack/test/api_integration/apis/synthetics/services/synthetics_monitor_test_service.ts b/x-pack/test/api_integration/apis/synthetics/services/synthetics_monitor_test_service.ts index 7bc246804f42c..e11a5523ed7b4 100644 --- a/x-pack/test/api_integration/apis/synthetics/services/synthetics_monitor_test_service.ts +++ b/x-pack/test/api_integration/apis/synthetics/services/synthetics_monitor_test_service.ts @@ -12,6 +12,8 @@ import { MonitorInspectResponse } from '@kbn/synthetics-plugin/public/apps/synth import { v4 as uuidv4 } from 'uuid'; import expect from '@kbn/expect'; import { ProjectAPIKeyResponse } from '@kbn/synthetics-plugin/server/routes/monitor_cruds/get_api_key'; +import moment from 'moment/moment'; +import { omit } from 'lodash'; import { KibanaSupertestProvider } from '@kbn/ftr-common-functional-services'; import { FtrProviderContext } from '../../../ftr_provider_context'; @@ -45,14 +47,53 @@ export class SyntheticsMonitorTestService { return apiKey; }; - async getMonitor(monitorId: string, decrypted: boolean = true, space?: string) { - let url = - SYNTHETICS_API_URLS.GET_SYNTHETICS_MONITOR.replace('{monitorId}', monitorId) + - (decrypted ? '?decrypted=true' : ''); + async getMonitor( + monitorId: string, + { + statusCode = 200, + space, + internal, + }: { + statusCode?: number; + space?: string; + internal?: boolean; + } = {} + ) { + let url = SYNTHETICS_API_URLS.GET_SYNTHETICS_MONITOR.replace('{monitorId}', monitorId); if (space) { url = '/s/' + space + url; } - return this.supertest.get(url).set('kbn-xsrf', 'true').expect(200); + if (internal) { + url += `?internal=${internal}`; + } + const apiResponse = await this.supertest.get(url).expect(200); + + expect(apiResponse.status).eql(statusCode, JSON.stringify(apiResponse.body)); + + if (statusCode === 200) { + const { + created_at: createdAt, + updated_at: updatedAt, + id, + config_id: configId, + } = apiResponse.body; + expect(id).not.empty(); + expect(configId).not.empty(); + expect([createdAt, updatedAt].map((d) => moment(d).isValid())).eql([true, true]); + return { + rawBody: apiResponse.body, + body: { + ...omit(apiResponse.body, [ + 'created_at', + 'updated_at', + 'id', + 'config_id', + 'form_monitor_type', + ]), + }, + }; + } + return apiResponse.body; } async addMonitor(monitor: any) { diff --git a/x-pack/test/api_integration/services/security_solution_api.gen.ts b/x-pack/test/api_integration/services/security_solution_api.gen.ts index 9c92a7a095dae..bb229ddcd693f 100644 --- a/x-pack/test/api_integration/services/security_solution_api.gen.ts +++ b/x-pack/test/api_integration/services/security_solution_api.gen.ts @@ -38,8 +38,8 @@ import { } from '@kbn/security-solution-plugin/common/api/endpoint/protection_updates_note/protection_updates_note.gen'; import { DeleteAssetCriticalityRecordRequestQueryInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/asset_criticality/delete_asset_criticality.gen'; import { - DeleteEntityStoreRequestQueryInput, - DeleteEntityStoreRequestParamsInput, + DeleteEntityEngineRequestQueryInput, + DeleteEntityEngineRequestParamsInput, } from '@kbn/security-solution-plugin/common/api/entity_analytics/entity_store/engine/delete.gen'; import { DeleteNoteRequestBodyInput } from '@kbn/security-solution-plugin/common/api/timeline/delete_note/delete_note_route.gen'; import { DeleteRuleRequestQueryInput } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_management/crud/delete_rule/delete_rule_route.gen'; @@ -80,8 +80,8 @@ import { GetEndpointSuggestionsRequestParamsInput, GetEndpointSuggestionsRequestBodyInput, } from '@kbn/security-solution-plugin/common/api/endpoint/suggestions/get_suggestions.gen'; -import { GetEntityStoreEngineRequestParamsInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/entity_store/engine/get.gen'; -import { GetEntityStoreStatsRequestParamsInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/entity_store/engine/stats.gen'; +import { GetEntityEngineRequestParamsInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/entity_store/engine/get.gen'; +import { GetEntityEngineStatsRequestParamsInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/entity_store/engine/stats.gen'; import { GetNotesRequestQueryInput } from '@kbn/security-solution-plugin/common/api/timeline/get_notes/get_notes_route.gen'; import { GetPolicyResponseRequestQueryInput } from '@kbn/security-solution-plugin/common/api/endpoint/policy/policy_response.gen'; import { GetProtectionUpdatesNoteRequestParamsInput } from '@kbn/security-solution-plugin/common/api/endpoint/protection_updates_note/protection_updates_note.gen'; @@ -98,8 +98,8 @@ import { GetTimelinesRequestQueryInput } from '@kbn/security-solution-plugin/com import { ImportRulesRequestQueryInput } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_management/import_rules/import_rules_route.gen'; import { ImportTimelinesRequestBodyInput } from '@kbn/security-solution-plugin/common/api/timeline/import_timelines/import_timelines_route.gen'; import { - InitEntityStoreRequestParamsInput, - InitEntityStoreRequestBodyInput, + InitEntityEngineRequestParamsInput, + InitEntityEngineRequestBodyInput, } from '@kbn/security-solution-plugin/common/api/entity_analytics/entity_store/engine/init.gen'; import { InstallPrepackedTimelinesRequestBodyInput } from '@kbn/security-solution-plugin/common/api/timeline/install_prepackaged_timelines/install_prepackaged_timelines_route.gen'; import { ListEntitiesRequestQueryInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/entity_store/entities/list_entities.gen'; @@ -124,8 +124,8 @@ import { SearchAlertsRequestBodyInput } from '@kbn/security-solution-plugin/comm import { SetAlertAssigneesRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/alert_assignees/set_alert_assignees_route.gen'; import { SetAlertsStatusRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/signals/set_signal_status/set_signals_status_route.gen'; import { SetAlertTagsRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/alert_tags/set_alert_tags/set_alert_tags.gen'; -import { StartEntityStoreRequestParamsInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/entity_store/engine/start.gen'; -import { StopEntityStoreRequestParamsInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/entity_store/engine/stop.gen'; +import { StartEntityEngineRequestParamsInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/entity_store/engine/start.gen'; +import { StopEntityEngineRequestParamsInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/entity_store/engine/stop.gen'; import { SuggestUserProfilesRequestQueryInput } from '@kbn/security-solution-plugin/common/api/detection_engine/users/suggest_user_profiles_route.gen'; import { TriggerRiskScoreCalculationRequestBodyInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/risk_engine/entity_calculation_route.gen'; import { UpdateRuleRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_management/crud/update_rule/update_rule_route.gen'; @@ -340,7 +340,7 @@ If a record already exists for the specified entity, that record is overwritten .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .query(props.query); }, - deleteEntityStore(props: DeleteEntityStoreProps) { + deleteEntityEngine(props: DeleteEntityEngineProps) { return supertest .delete(replaceParams('/api/entity_store/engines/{entityType}', props.params)) .set('kbn-xsrf', 'true') @@ -715,14 +715,14 @@ finalize it. .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .send(props.body as object); }, - getEntityStoreEngine(props: GetEntityStoreEngineProps) { + getEntityEngine(props: GetEntityEngineProps) { return supertest .get(replaceParams('/api/entity_store/engines/{entityType}', props.params)) .set('kbn-xsrf', 'true') .set(ELASTIC_HTTP_VERSION_HEADER, '2023-10-31') .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana'); }, - getEntityStoreStats(props: GetEntityStoreStatsProps) { + getEntityEngineStats(props: GetEntityEngineStatsProps) { return supertest .post(replaceParams('/api/entity_store/engines/{entityType}/stats', props.params)) .set('kbn-xsrf', 'true') @@ -834,7 +834,7 @@ finalize it. .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .send(props.body as object); }, - initEntityStore(props: InitEntityStoreProps) { + initEntityEngine(props: InitEntityEngineProps) { return supertest .post(replaceParams('/api/entity_store/engines/{entityType}/init', props.params)) .set('kbn-xsrf', 'true') @@ -891,7 +891,7 @@ finalize it. .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .query(props.query); }, - listEntityStoreEngines() { + listEntityEngines() { return supertest .get('/api/entity_store/engines') .set('kbn-xsrf', 'true') @@ -1130,14 +1130,14 @@ detection engine rules. .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .send(props.body as object); }, - startEntityStore(props: StartEntityStoreProps) { + startEntityEngine(props: StartEntityEngineProps) { return supertest .post(replaceParams('/api/entity_store/engines/{entityType}/start', props.params)) .set('kbn-xsrf', 'true') .set(ELASTIC_HTTP_VERSION_HEADER, '2023-10-31') .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana'); }, - stopEntityStore(props: StopEntityStoreProps) { + stopEntityEngine(props: StopEntityEngineProps) { return supertest .post(replaceParams('/api/entity_store/engines/{entityType}/stop', props.params)) .set('kbn-xsrf', 'true') @@ -1233,9 +1233,9 @@ export interface CreateUpdateProtectionUpdatesNoteProps { export interface DeleteAssetCriticalityRecordProps { query: DeleteAssetCriticalityRecordRequestQueryInput; } -export interface DeleteEntityStoreProps { - query: DeleteEntityStoreRequestQueryInput; - params: DeleteEntityStoreRequestParamsInput; +export interface DeleteEntityEngineProps { + query: DeleteEntityEngineRequestQueryInput; + params: DeleteEntityEngineRequestParamsInput; } export interface DeleteNoteProps { body: DeleteNoteRequestBodyInput; @@ -1330,11 +1330,11 @@ export interface GetEndpointSuggestionsProps { params: GetEndpointSuggestionsRequestParamsInput; body: GetEndpointSuggestionsRequestBodyInput; } -export interface GetEntityStoreEngineProps { - params: GetEntityStoreEngineRequestParamsInput; +export interface GetEntityEngineProps { + params: GetEntityEngineRequestParamsInput; } -export interface GetEntityStoreStatsProps { - params: GetEntityStoreStatsRequestParamsInput; +export interface GetEntityEngineStatsProps { + params: GetEntityEngineStatsRequestParamsInput; } export interface GetNotesProps { query: GetNotesRequestQueryInput; @@ -1365,9 +1365,9 @@ export interface ImportRulesProps { export interface ImportTimelinesProps { body: ImportTimelinesRequestBodyInput; } -export interface InitEntityStoreProps { - params: InitEntityStoreRequestParamsInput; - body: InitEntityStoreRequestBodyInput; +export interface InitEntityEngineProps { + params: InitEntityEngineRequestParamsInput; + body: InitEntityEngineRequestBodyInput; } export interface InstallPrepackedTimelinesProps { body: InstallPrepackedTimelinesRequestBodyInput; @@ -1422,11 +1422,11 @@ export interface SetAlertsStatusProps { export interface SetAlertTagsProps { body: SetAlertTagsRequestBodyInput; } -export interface StartEntityStoreProps { - params: StartEntityStoreRequestParamsInput; +export interface StartEntityEngineProps { + params: StartEntityEngineRequestParamsInput; } -export interface StopEntityStoreProps { - params: StopEntityStoreRequestParamsInput; +export interface StopEntityEngineProps { + params: StopEntityEngineRequestParamsInput; } export interface SuggestUserProfilesProps { query: SuggestUserProfilesRequestQueryInput; diff --git a/x-pack/test/functional/apps/management/config.ts b/x-pack/test/functional/apps/management/config.ts index d0d07ff200281..e4a06d30f260d 100644 --- a/x-pack/test/functional/apps/management/config.ts +++ b/x-pack/test/functional/apps/management/config.ts @@ -13,5 +13,12 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { return { ...functionalConfig.getAll(), testFiles: [require.resolve('.')], + kbnTestServer: { + ...functionalConfig.get('kbnTestServer'), + serverArgs: [ + ...functionalConfig.get('kbnTestServer.serverArgs'), + '--xpack.spaces.experimental.forceSolutionVisibility=true', + ], + }, }; } diff --git a/x-pack/test/functional/apps/management/index.ts b/x-pack/test/functional/apps/management/index.ts index 72da3e0fd739a..7f84137b81be8 100644 --- a/x-pack/test/functional/apps/management/index.ts +++ b/x-pack/test/functional/apps/management/index.ts @@ -11,5 +11,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { describe('management', function () { loadTestFile(require.resolve('./create_index_pattern_wizard')); loadTestFile(require.resolve('./feature_controls')); + loadTestFile(require.resolve('./landing_page')); }); } diff --git a/x-pack/test/functional/apps/management/landing_page.ts b/x-pack/test/functional/apps/management/landing_page.ts new file mode 100644 index 0000000000000..54a360c2e674d --- /dev/null +++ b/x-pack/test/functional/apps/management/landing_page.ts @@ -0,0 +1,104 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import type { SolutionView } from '@kbn/spaces-plugin/common'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const browser = getService('browser'); + const testSubjects = getService('testSubjects'); + const spaces = getService('spaces'); + const PageObjects = getPageObjects(['settings', 'common', 'dashboard', 'timePicker', 'header']); + + describe('landing page', function describeIndexTests() { + let cleanUp: () => Promise = () => Promise.resolve(); + let spaceCreated: { id: string } = { id: '' }; + + it('should render the "classic" prompt', async function () { + await PageObjects.common.navigateToApp('management'); + await testSubjects.existOrFail('managementHome', { timeout: 3000 }); + }); + + describe('solution empty prompt', () => { + const createSpaceWithSolutionAndNavigateToManagement = async (solution: SolutionView) => { + ({ cleanUp, space: spaceCreated } = await spaces.create({ solution })); + + await PageObjects.common.navigateToApp('management', { basePath: `/s/${spaceCreated.id}` }); + + return async () => { + await cleanUp(); + cleanUp = () => Promise.resolve(); + }; + }; + + afterEach(async function afterEach() { + await cleanUp(); + }); + + /** Test that the empty prompt has a button to open the stack managment panel */ + const testStackManagmentPanel = async () => { + await testSubjects.missingOrFail('~sideNavPanel-id-stack_management', { timeout: 1000 }); + await testSubjects.click('~viewAllStackMngtPagesButton'); // open the side nav + await testSubjects.existOrFail('~sideNavPanel-id-stack_management', { timeout: 3000 }); + }; + + const testCorrectEmptyPrompt = async () => { + await testSubjects.missingOrFail('managementHome', { timeout: 3000 }); + await testSubjects.existOrFail('managementHomeSolution', { timeout: 3000 }); + }; + + it('should render the "solution" prompt when the space has a solution set', async function () { + { + const deleteSpace = await createSpaceWithSolutionAndNavigateToManagement('es'); + await testCorrectEmptyPrompt(); + await testStackManagmentPanel(); + await deleteSpace(); + } + + { + const deleteSpace = await createSpaceWithSolutionAndNavigateToManagement('oblt'); + await testCorrectEmptyPrompt(); + await testStackManagmentPanel(); + await deleteSpace(); + } + + { + const deleteSpace = await createSpaceWithSolutionAndNavigateToManagement('security'); + await testCorrectEmptyPrompt(); + await testStackManagmentPanel(); + await deleteSpace(); + } + }); + + it('should have links to pages in management', async function () { + await createSpaceWithSolutionAndNavigateToManagement('es'); + + await testSubjects.click('~managementLinkToIndices', 3000); + await testSubjects.existOrFail('~indexManagementHeaderContent', { timeout: 3000 }); + await browser.goBack(); + await testSubjects.existOrFail('managementHomeSolution', { timeout: 3000 }); + + await testSubjects.click('~managementLinkToDataViews', 3000); + await testSubjects.existOrFail('~indexPatternTable', { timeout: 3000 }); + await browser.goBack(); + await testSubjects.existOrFail('managementHomeSolution', { timeout: 3000 }); + + await testSubjects.click('~managementLinkToIngestPipelines', 3000); + const appTitle = await testSubjects.getVisibleText('appTitle'); + expect(appTitle).to.be('Ingest Pipelines'); + // Note: for some reason, browser.goBack() does not work from Ingest Pipelines + // so using navigateToApp instead; + await PageObjects.common.navigateToApp('management', { basePath: `/s/${spaceCreated.id}` }); + await testSubjects.existOrFail('managementHomeSolution', { timeout: 3000 }); + + await testSubjects.click('~managementLinkToUsers', 3000); + await testSubjects.existOrFail('~securityUsersPageHeader', { timeout: 3000 }); + }); + }); + }); +} diff --git a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/engine.ts b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/engine.ts new file mode 100644 index 0000000000000..8d57ff428e507 --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/engine.ts @@ -0,0 +1,242 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { EntityType } from '@kbn/security-solution-plugin/common/api/entity_analytics/entity_store/common.gen'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; +import { cleanEngines } from '../../utils'; +export default ({ getService }: FtrProviderContext) => { + const api = getService('securitySolutionApi'); + const es = getService('es'); + + const initEntityEngineForEntityType = async (entityType: EntityType) => { + return api + .initEntityEngine({ + params: { entityType }, + body: {}, + }) + .expect(200); + }; + + const expectTransformExists = async (transformId: string) => { + return expectTransformStatus(transformId, true); + }; + + const expectTransformNotFound = async (transformId: string, attempts: number = 5) => { + return expectTransformStatus(transformId, false); + }; + + const expectTransformStatus = async ( + transformId: string, + exists: boolean, + attempts: number = 5, + delayMs: number = 2000 + ) => { + let currentAttempt = 1; + while (currentAttempt <= attempts) { + try { + await es.transform.getTransform({ transform_id: transformId }); + if (!exists) { + throw new Error(`Expected transform ${transformId} to not exist, but it does`); + } + return; // Transform exists, exit the loop + } catch (e) { + if (currentAttempt === attempts) { + if (exists) { + throw new Error(`Expected transform ${transformId} to exist, but it does not: ${e}`); + } else { + return; // Transform does not exist, exit the loop + } + } + await new Promise((resolve) => setTimeout(resolve, delayMs)); + currentAttempt++; + } + } + }; + + const expectTransformsExist = async (transformIds: string[]) => + Promise.all(transformIds.map((id) => expectTransformExists(id))); + + describe('@ess @serverless @skipInServerlessMKI Entity Store Engine APIs', () => { + before(async () => { + await cleanEngines({ getService }); + }); + + describe('init', () => { + afterEach(async () => { + await cleanEngines({ getService }); + }); + + it('should have installed the expected user resources', async () => { + await initEntityEngineForEntityType('user'); + + const expectedTransforms = [ + 'entities-v1-history-ea_default_user_entity_store', + 'entities-v1-latest-ea_default_user_entity_store', + ]; + + await expectTransformsExist(expectedTransforms); + }); + + it('should have installed the expected host resources', async () => { + await initEntityEngineForEntityType('host'); + + const expectedTransforms = [ + 'entities-v1-history-ea_default_host_entity_store', + 'entities-v1-latest-ea_default_host_entity_store', + ]; + + await expectTransformsExist(expectedTransforms); + }); + }); + + describe('get and list', () => { + before(async () => { + await Promise.all([ + initEntityEngineForEntityType('host'), + initEntityEngineForEntityType('user'), + ]); + }); + + after(async () => { + await cleanEngines({ getService }); + }); + + describe('get', () => { + it('should return the host entity engine', async () => { + const getResponse = await api + .getEntityEngine({ + params: { entityType: 'host' }, + }) + .expect(200); + + expect(getResponse.body).to.eql({ + status: 'started', + type: 'host', + indexPattern: + 'apm-*-transaction*,auditbeat-*,endgame-*,filebeat-*,logs-*,packetbeat-*,traces-apm*,winlogbeat-*,-*elastic-cloud-logs-*', + filter: '', + }); + }); + + it('should return the user entity engine', async () => { + const getResponse = await api + .getEntityEngine({ + params: { entityType: 'user' }, + }) + .expect(200); + + expect(getResponse.body).to.eql({ + status: 'started', + type: 'user', + indexPattern: + 'apm-*-transaction*,auditbeat-*,endgame-*,filebeat-*,logs-*,packetbeat-*,traces-apm*,winlogbeat-*,-*elastic-cloud-logs-*', + filter: '', + }); + }); + }); + + describe('list', () => { + it('should return the list of entity engines', async () => { + const { body } = await api.listEntityEngines().expect(200); + + // @ts-expect-error body is any + const sortedEngines = body.engines.sort((a, b) => a.type.localeCompare(b.type)); + + expect(sortedEngines).to.eql([ + { + status: 'started', + type: 'host', + indexPattern: + 'apm-*-transaction*,auditbeat-*,endgame-*,filebeat-*,logs-*,packetbeat-*,traces-apm*,winlogbeat-*,-*elastic-cloud-logs-*', + filter: '', + }, + { + status: 'started', + type: 'user', + indexPattern: + 'apm-*-transaction*,auditbeat-*,endgame-*,filebeat-*,logs-*,packetbeat-*,traces-apm*,winlogbeat-*,-*elastic-cloud-logs-*', + filter: '', + }, + ]); + }); + }); + }); + + describe('start and stop', () => { + before(async () => { + await initEntityEngineForEntityType('host'); + }); + + after(async () => { + await cleanEngines({ getService }); + }); + + it('should stop the entity engine', async () => { + await api + .stopEntityEngine({ + params: { entityType: 'host' }, + }) + .expect(200); + + const { body } = await api + .getEntityEngine({ + params: { entityType: 'host' }, + }) + .expect(200); + + expect(body.status).to.eql('stopped'); + }); + + it('should start the entity engine', async () => { + await api + .startEntityEngine({ + params: { entityType: 'host' }, + }) + .expect(200); + + const { body } = await api + .getEntityEngine({ + params: { entityType: 'host' }, + }) + .expect(200); + + expect(body.status).to.eql('started'); + }); + }); + + describe('delete', () => { + it('should delete the host entity engine', async () => { + await initEntityEngineForEntityType('host'); + + await api + .deleteEntityEngine({ + params: { entityType: 'host' }, + query: { data: true }, + }) + .expect(200); + + await expectTransformNotFound('entities-v1-history-ea_host_entity_store'); + await expectTransformNotFound('entities-v1-latest-ea_host_entity_store'); + }); + + it('should delete the user entity engine', async () => { + await initEntityEngineForEntityType('user'); + + await api + .deleteEntityEngine({ + params: { entityType: 'user' }, + query: { data: true }, + }) + .expect(200); + + await expectTransformNotFound('entities-v1-history-ea_user_entity_store'); + await expectTransformNotFound('entities-v1-latest-ea_user_entity_store'); + }); + }); + }); +}; diff --git a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/index.ts b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/index.ts index a043ea866d5eb..6e730b465350a 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/index.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/index.ts @@ -10,5 +10,6 @@ import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('Entity Analytics - Entity Store', function () { loadTestFile(require.resolve('./entities_list')); + loadTestFile(require.resolve('./engine')); }); } diff --git a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/risk_engine/trial_license_complete_tier/init_and_status_apis.ts b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/risk_engine/trial_license_complete_tier/init_and_status_apis.ts index 399a2fdb6de72..ef9785793324f 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/risk_engine/trial_license_complete_tier/init_and_status_apis.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/risk_engine/trial_license_complete_tier/init_and_status_apis.ts @@ -347,7 +347,6 @@ export default ({ getService }: FtrProviderContext) => { expect(status1.body).to.eql({ risk_engine_status: 'NOT_INSTALLED', legacy_risk_engine_status: 'NOT_INSTALLED', - is_max_amount_of_risk_engines_reached: false, }); await riskEngineRoutes.init(); @@ -356,7 +355,6 @@ export default ({ getService }: FtrProviderContext) => { expect(status2.body.risk_engine_status).to.be('ENABLED'); expect(status2.body.legacy_risk_engine_status).to.be('NOT_INSTALLED'); - expect(status2.body.is_max_amount_of_risk_engines_reached).to.be(true); expect(status2.body.risk_engine_task_status.runAt).to.be.a('string'); expect(status2.body.risk_engine_task_status.status).to.be('idle'); @@ -368,7 +366,6 @@ export default ({ getService }: FtrProviderContext) => { expect(status3.body).to.eql({ risk_engine_status: 'DISABLED', legacy_risk_engine_status: 'NOT_INSTALLED', - is_max_amount_of_risk_engines_reached: false, }); await riskEngineRoutes.enable(); @@ -376,7 +373,6 @@ export default ({ getService }: FtrProviderContext) => { expect(status4.body.risk_engine_status).to.be('ENABLED'); expect(status4.body.legacy_risk_engine_status).to.be('NOT_INSTALLED'); - expect(status4.body.is_max_amount_of_risk_engines_reached).to.be(true); expect(status4.body.risk_engine_task_status.runAt).to.be.a('string'); expect(status4.body.risk_engine_task_status.status).to.be('idle'); @@ -390,7 +386,6 @@ export default ({ getService }: FtrProviderContext) => { expect(status1.body).to.eql({ risk_engine_status: 'NOT_INSTALLED', legacy_risk_engine_status: 'ENABLED', - is_max_amount_of_risk_engines_reached: false, }); await riskEngineRoutes.init(); @@ -399,7 +394,6 @@ export default ({ getService }: FtrProviderContext) => { expect(status2.body.risk_engine_status).to.be('ENABLED'); expect(status2.body.legacy_risk_engine_status).to.be('NOT_INSTALLED'); - expect(status2.body.is_max_amount_of_risk_engines_reached).to.be(true); expect(status2.body.risk_engine_task_status.runAt).to.be.a('string'); expect(status2.body.risk_engine_task_status.status).to.be('idle'); diff --git a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/risk_engine/trial_license_complete_tier/risk_scoring_task/task_execution.ts b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/risk_engine/trial_license_complete_tier/risk_scoring_task/task_execution.ts index a097c170de51c..7710a1f7045cd 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/risk_engine/trial_license_complete_tier/risk_scoring_task/task_execution.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/risk_engine/trial_license_complete_tier/risk_scoring_task/task_execution.ts @@ -105,6 +105,9 @@ export default ({ getService }: FtrProviderContext): void => { }); it('@skipInServerlessMKI @skipInServerless starts the latest transform', async () => { + // Transform states that indicate the transform is running happily + const TRANSFORM_STARTED_STATES = ['started', 'indexing']; + await waitForRiskScoresToBePresent({ es, log, scoreCount: 10 }); const transformStats = await es.transform.getTransformStats({ @@ -113,12 +116,12 @@ export default ({ getService }: FtrProviderContext): void => { expect(transformStats.transforms.length).to.eql(1); const latestTransform = transformStats.transforms[0]; - if (latestTransform.state !== 'started') { - log.error('Transform state is not started, logging the transform'); + if (!TRANSFORM_STARTED_STATES.includes(latestTransform.state)) { + log.error('Transform state is not in the started states, logging the transform'); log.info(`latestTransform: ${JSON.stringify(latestTransform)}`); } - expect(latestTransform.state).to.eql('started'); + expect(TRANSFORM_STARTED_STATES).to.contain(latestTransform.state); }); describe('@skipInServerlessMKI disabling and re-enabling the risk engine', () => { diff --git a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/utils/entity_store.ts b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/utils/entity_store.ts new file mode 100644 index 0000000000000..3f0ba0698a494 --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/utils/entity_store.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../../api_integration/ftr_provider_context'; + +export const cleanEngines = async ({ + getService, +}: { + getService: FtrProviderContext['getService']; +}) => { + const log = getService('log'); + const api = getService('securitySolutionApi'); + + const { body } = await api.listEntityEngines().expect(200); + + // @ts-expect-error body is any + const engineTypes = body.engines.map((engine) => engine.type); + + log.info(`Cleaning engines: ${engineTypes.join(', ')}`); + try { + await Promise.all( + engineTypes.map((entityType: 'user' | 'host') => + api.deleteEntityEngine({ params: { entityType }, query: { data: true } }) + ) + ); + } catch (e) { + log.warning(`Error deleting engines: ${e.message}`); + } +}; diff --git a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/utils/index.ts b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/utils/index.ts index 7ff049a997da1..8f6a368807241 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/utils/index.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/utils/index.ts @@ -8,3 +8,4 @@ export * from './risk_engine'; export * from './get_risk_engine_stats'; export * from './asset_criticality'; +export * from './entity_store'; diff --git a/x-pack/test/security_solution_api_integration/test_suites/investigation/saved_objects/trial_license_complete_tier/pinned_events.ts b/x-pack/test/security_solution_api_integration/test_suites/investigation/saved_objects/trial_license_complete_tier/pinned_events.ts index 9273a5d6c6a5e..9506e7345a00e 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/investigation/saved_objects/trial_license_complete_tier/pinned_events.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/investigation/saved_objects/trial_license_complete_tier/pinned_events.ts @@ -39,7 +39,7 @@ export default function ({ getService }: FtrProviderContext) { }); describe('Unpinned an event', () => { - it('return null', async () => { + it('returns null', async () => { const response = await supertest .patch('/api/pinned_event') .set('elastic-api-version', '2023-10-31') diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/entity_analytics.ts b/x-pack/test/security_solution_cypress/cypress/tasks/entity_analytics.ts index 33599c47ccb5d..2265f228c2ce7 100644 --- a/x-pack/test/security_solution_cypress/cypress/tasks/entity_analytics.ts +++ b/x-pack/test/security_solution_cypress/cypress/tasks/entity_analytics.ts @@ -65,7 +65,6 @@ export const mockRiskEngineEnabled = () => { body: { risk_engine_status: 'ENABLED', legacy_risk_engine_status: 'INSTALLED', - is_max_amount_of_risk_engines_reached: false, }, }).as('riskEngineStatus'); diff --git a/x-pack/test_serverless/functional/page_objects/svl_search_index_detail_page.ts b/x-pack/test_serverless/functional/page_objects/svl_search_index_detail_page.ts index 8ffbfd2bcb8c1..f19a713eece5b 100644 --- a/x-pack/test_serverless/functional/page_objects/svl_search_index_detail_page.ts +++ b/x-pack/test_serverless/functional/page_objects/svl_search_index_detail_page.ts @@ -115,11 +115,17 @@ export function SvlSearchIndexDetailPageProvider({ getService }: FtrProviderCont async expectShouldDefaultToDataTab() { expect(await browser.getCurrentUrl()).contain('/data'); }, - async withDataChangeTabs(tab: 'dataTab' | 'mappingsTab') { + async withDataChangeTabs(tab: 'dataTab' | 'mappingsTab' | 'settingsTab') { await testSubjects.click(tab); }, - async expectUrlShouldChangeTo(tab: 'data' | 'mappings') { + async expectUrlShouldChangeTo(tab: 'data' | 'mappings' | 'settings') { expect(await browser.getCurrentUrl()).contain(`/${tab}`); }, + async expectMappingsComponentIsVisible() { + await testSubjects.existOrFail('indexDetailsMappingsToggleViewButton', { timeout: 2000 }); + }, + async expectSettingsComponentIsVisible() { + await testSubjects.existOrFail('indexDetailsSettingsEditModeSwitch', { timeout: 2000 }); + }, }; } diff --git a/x-pack/test_serverless/functional/test_suites/observability/onboarding/firehose.ts b/x-pack/test_serverless/functional/test_suites/observability/onboarding/firehose.ts index 91b536c79c5e8..22c700d16be2d 100644 --- a/x-pack/test_serverless/functional/test_suites/observability/onboarding/firehose.ts +++ b/x-pack/test_serverless/functional/test_suites/observability/onboarding/firehose.ts @@ -20,8 +20,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const synthtrace = getService('svlLogsSynthtraceClient'); - // Failing: See https://github.com/elastic/kibana/issues/193294 - describe.skip('Onboarding Firehose Quickstart Flow', () => { + describe('Onboarding Firehose Quickstart Flow', () => { before(async () => { await PageObjects.svlCommonPage.loginAsAdmin(); // Onboarding requires admin role await PageObjects.common.navigateToUrlWithBrowserHistory( @@ -39,7 +38,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); beforeEach(async () => { - await (await testSubjects.find('createCloudFormationOptionAWSCLI')).click(); + await (await testSubjects.find('createCloudFormationOptionAWSCLI', 20000)).click(); await testSubjects.existOrFail('observabilityOnboardingFirehoseCreateStackCommand'); }); diff --git a/x-pack/test_serverless/functional/test_suites/search/search_index_detail.ts b/x-pack/test_serverless/functional/test_suites/search/search_index_detail.ts index 7f40ec9127c6c..a4d58d32c751b 100644 --- a/x-pack/test_serverless/functional/test_suites/search/search_index_detail.ts +++ b/x-pack/test_serverless/functional/test_suites/search/search_index_detail.ts @@ -91,9 +91,15 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await pageObjects.svlSearchIndexDetailPage.expectWithDataTabsExists(); await pageObjects.svlSearchIndexDetailPage.expectShouldDefaultToDataTab(); }); - it('should be able to change tabs', async () => { + it('should be able to change tabs to mappings and mappings is shown', async () => { await pageObjects.svlSearchIndexDetailPage.withDataChangeTabs('mappingsTab'); await pageObjects.svlSearchIndexDetailPage.expectUrlShouldChangeTo('mappings'); + await pageObjects.svlSearchIndexDetailPage.expectMappingsComponentIsVisible(); + }); + it('should be able to change tabs to settings and settings is shown', async () => { + await pageObjects.svlSearchIndexDetailPage.withDataChangeTabs('settingsTab'); + await pageObjects.svlSearchIndexDetailPage.expectUrlShouldChangeTo('settings'); + await pageObjects.svlSearchIndexDetailPage.expectSettingsComponentIsVisible(); }); }); diff --git a/yarn.lock b/yarn.lock index 666bf8d3e984d..0d4d285392664 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5699,6 +5699,10 @@ version "0.0.0" uid "" +"@kbn/ml-field-stats-flyout@link:x-pack/packages/ml/field_stats_flyout": + version "0.0.0" + uid "" + "@kbn/ml-in-memory-table@link:x-pack/packages/ml/in_memory_table": version "0.0.0" uid "" @@ -5727,6 +5731,10 @@ version "0.0.0" uid "" +"@kbn/ml-parse-interval@link:x-pack/packages/ml/parse_interval": + version "0.0.0" + uid "" + "@kbn/ml-plugin@link:x-pack/plugins/ml": version "0.0.0" uid "" @@ -5771,6 +5779,10 @@ version "0.0.0" uid "" +"@kbn/ml-validators@link:x-pack/packages/ml/validators": + version "0.0.0" + uid "" + "@kbn/mock-idp-plugin@link:packages/kbn-mock-idp-plugin": version "0.0.0" uid "" @@ -7698,10 +7710,10 @@ express "^4.18.2" strict-event-emitter "^0.5.1" -"@mswjs/interceptors@^0.35.6": - version "0.35.6" - resolved "https://registry.yarnpkg.com/@mswjs/interceptors/-/interceptors-0.35.6.tgz#66c522036bc01fa6be87b46b49cc378b837bf510" - integrity sha512-PpD687w7qLxVMK176bpQjbzU9O0VC75QnBK5U1lKd29s4hIuxfTItUD6raNKyQ6BN8b64/8HE34RuYTkwH9uPQ== +"@mswjs/interceptors@^0.35.8": + version "0.35.8" + resolved "https://registry.yarnpkg.com/@mswjs/interceptors/-/interceptors-0.35.8.tgz#f36e5907e05593e33037ef4519aac7815fa3509f" + integrity sha512-PFfqpHplKa7KMdoQdj5td03uG05VK2Ng1dG0sP4pT9h0dGSX2v9txYt/AnrzPb/vAmfyBBC0NQV7VaBEX+efgQ== dependencies: "@open-draft/deferred-promise" "^2.2.0" "@open-draft/logger" "^0.3.0" @@ -23822,16 +23834,16 @@ msgpackr@^1.9.9: optionalDependencies: msgpackr-extract "^3.0.2" -msw@^2.4.5: - version "2.4.8" - resolved "https://registry.yarnpkg.com/msw/-/msw-2.4.8.tgz#306b6ba1ae09d0d09bac08009a83eeb622578d73" - integrity sha512-a+FUW1m5yT8cV9GBy0L/cbNg0EA4//SKEzgu3qFrpITrWYeZmqfo7dqtM74T2lAl69jjUjjCaEhZKaxG2Ns8DA== +msw@^2.4.7: + version "2.4.9" + resolved "https://registry.yarnpkg.com/msw/-/msw-2.4.9.tgz#350a84cedb90b578a32c7764431e3750900f8407" + integrity sha512-1m8xccT6ipN4PTqLinPwmzhxQREuxaEJYdx4nIbggxP8aM7r1e71vE7RtOUSQoAm1LydjGfZKy7370XD/tsuYg== dependencies: "@bundled-es-modules/cookie" "^2.0.0" "@bundled-es-modules/statuses" "^1.0.1" "@bundled-es-modules/tough-cookie" "^0.1.6" "@inquirer/confirm" "^3.0.0" - "@mswjs/interceptors" "^0.35.6" + "@mswjs/interceptors" "^0.35.8" "@open-draft/until" "^2.1.0" "@types/cookie" "^0.6.0" "@types/statuses" "^2.0.4"