From 0ead26a742b28759019eb9f78a318cf34af30546 Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Wed, 3 Jul 2024 20:22:49 +0300 Subject: [PATCH 01/13] fix: [Obs Synthetics > Monitor detail][KEYBOARD]: Test run screenshots (in modal) need more comprehensive alt text (#187363) Closes: https://github.com/elastic/observability-dev/issues/3688 ## Description The synthetics monitors include thumbnail screenshots that open a larger preview window. The alt text on these larger screenshots is very generic and could be improved by concatenating more information. Screenshot and suggested copy attached below. ### Steps to recreate 1. Open the [Synthetics](https://keep-serverless-fyzdg-f07c50.kb.eu-west-1.aws.qa.elastic.cloud/app/synthetics) view 2. Create a monitor if none exist 3. Click on that monitor and navigate to the [full monitor detail](https://keep-serverless-fyzdg-f07c50.kb.eu-west-1.aws.qa.elastic.cloud/app/synthetics/monitor/8b88e937-f917-4f12-9325-8ab005cffea5?locationId=us_central_qa) view 4. Click on a thumbnail and verify the modal opens 5. Turn on the screen reader of your choosing 6. Navigate to the image and verify the generic alt text ### What was changed?: 1. The `label` attribute for `ScreenshotImage` calling was changed to a unified version in all places. Now it's always follow next rule: `"{stepName}", {stepNumber} of {totalSteps}` ### Screen: image --- .../journey_screenshot_preview.test.tsx | 6 ++-- .../journey_screenshot_preview.tsx | 10 ++++++- .../screenshot/journey_screenshot_dialog.tsx | 30 +++++++++++-------- ...journey_step_screenshot_container.test.tsx | 4 +-- 4 files changed, 31 insertions(+), 19 deletions(-) diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/common/monitor_test_result/journey_screenshot_preview.test.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/common/monitor_test_result/journey_screenshot_preview.test.tsx index cedf240b36115..d98f8e2dd2edf 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/common/monitor_test_result/journey_screenshot_preview.test.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/common/monitor_test_result/journey_screenshot_preview.test.tsx @@ -62,7 +62,7 @@ describe('JourneyScreenshotPreview', () => { const { getByAltText, getByText, queryByAltText } = render( ); - const img = getByAltText('First step'); + const img = getByAltText('"First step", 1 of 2'); fireEvent.click(img); expect(dialogProps.checkGroup).toEqual(defaultProps.checkGroup); expect(getByAltText('img-in-dialog')).not.toBeNull(); @@ -75,7 +75,7 @@ describe('JourneyScreenshotPreview', () => { ); - const img = getByAltText('First step'); + const img = getByAltText('"First step", 1 of 2'); const euiPopoverMessage = 'You are in a dialog. Press Escape, or tap/click outside the dialog to close.'; // Helps to detect if popover is open expect(queryByText(euiPopoverMessage)).toBeNull(); @@ -88,7 +88,7 @@ describe('JourneyScreenshotPreview', () => { it('renders the correct image', () => { const { getByAltText } = render(); - const img = getByAltText('First step'); + const img = getByAltText('"First step", 1 of 2'); expect(img).toHaveAttribute('src', testImgUrl1); }); }); diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/common/monitor_test_result/journey_screenshot_preview.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/common/monitor_test_result/journey_screenshot_preview.tsx index f16586141971d..62bb880a0f6be 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/common/monitor_test_result/journey_screenshot_preview.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/common/monitor_test_result/journey_screenshot_preview.tsx @@ -8,6 +8,7 @@ import React, { useCallback, useState, MouseEvent } from 'react'; import { EuiPopover, useEuiTheme } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { POPOVER_SCREENSHOT_SIZE, ScreenshotImageSize } from '../screenshot/screenshot_size'; import { JourneyScreenshotDialog } from '../screenshot/journey_screenshot_dialog'; import { ScreenshotImage } from '../screenshot/screenshot_image'; @@ -77,7 +78,14 @@ export const JourneyScreenshotPreview: React.FC = ({ const renderScreenshotImage = (screenshotSize: ScreenshotImageSize) => ( { if (isOpen) { setStepNumber(initialStepNumber); @@ -122,7 +120,14 @@ export const JourneyScreenshotDialog = ({ > - {stepCountLabel} + + {i18n.translate('xpack.synthetics.monitor.stepOfSteps', { + defaultMessage: 'Step: {stepNumber} of {totalSteps}', + values: { + stepNumber, + totalSteps: maxSteps ?? stepNumber, + }, + })} + - i18n.translate('xpack.synthetics.monitor.stepOfSteps', { - defaultMessage: 'Step: {stepNumber} of {totalSteps}', - values: { - stepNumber, - totalSteps, - }, - }); - const prevAriaLabel = i18n.translate('xpack.synthetics.monitor.step.previousStep', { defaultMessage: 'Previous step', }); diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/common/screenshot/journey_step_screenshot_container.test.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/common/screenshot/journey_step_screenshot_container.test.tsx index c2886934a8481..454747ae2dd13 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/common/screenshot/journey_step_screenshot_container.test.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/common/screenshot/journey_step_screenshot_container.test.tsx @@ -69,7 +69,7 @@ describe('JourneyStepScreenshotContainer', () => { ); - const img = getByAltText('First step'); + const img = getByAltText('"First step", 1 of 2'); const euiPopoverMessage = 'You are in a dialog. Press Escape, or tap/click outside the dialog to close.'; expect(queryByText(euiPopoverMessage)).toBeNull(); @@ -88,7 +88,7 @@ describe('JourneyStepScreenshotContainer', () => { ); - const img = getByAltText('First step'); + const img = getByAltText('"First step", 1 of 2'); await waitFor(() => img); fireEvent.click(img); From aad2239c32bc4cc82599c1fb5f93e3046ecd69c0 Mon Sep 17 00:00:00 2001 From: Jan Monschke Date: Wed, 3 Jul 2024 19:25:24 +0200 Subject: [PATCH 02/13] [Security] Timeline OpenAPI documentation fixes (#186458) ## Summary - Fix issues that came up during validation with `spectral lint` running with the [recommended settings](https://docs.elastic.dev/content-architecture/oas#openapi-version). - Made sure all return and request types match with the code - Fixed incorrect descriptions and links to documenation Fixes https://github.com/elastic/kibana/issues/183812. --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../clean_draft_timelines_route_schema.yaml | 6 +++--- .../create_timelines_route_schema.yaml | 6 +++--- .../delete_note/delete_note_route_schema.yaml | 13 ++++++++++--- .../delete_timelines_route_schema.yaml | 7 ++++--- .../export_timelines_route_schema.yaml | 8 ++++---- .../get_draft_timelines_route_schema.yaml | 11 ++++------- .../get_timeline/get_timeline_route_schema.yaml | 8 ++++---- .../get_timelines/get_timelines_route_schema.yaml | 8 ++++---- .../import_timelines_route_schema.yaml | 10 +++++----- .../install_prepackaged_timelines_route_schema.yaml | 6 +++--- .../patch_timeline_route_schema.yaml | 4 ++-- .../persist_favorite_route_schema.yaml | 10 +++++----- .../persist_note/persist_note_route_schema.yaml | 8 ++++---- .../pinned_events/pinned_events_route_schema.yaml | 9 ++++++--- .../server/lib/timeline/routes/notes/delete_note.ts | 8 ++++---- 15 files changed, 65 insertions(+), 57 deletions(-) diff --git a/x-pack/plugins/security_solution/common/api/timeline/clean_draft_timelines/clean_draft_timelines_route_schema.yaml b/x-pack/plugins/security_solution/common/api/timeline/clean_draft_timelines/clean_draft_timelines_route_schema.yaml index 7e839763d52e0..2a5d004507fb8 100644 --- a/x-pack/plugins/security_solution/common/api/timeline/clean_draft_timelines/clean_draft_timelines_route_schema.yaml +++ b/x-pack/plugins/security_solution/common/api/timeline/clean_draft_timelines/clean_draft_timelines_route_schema.yaml @@ -29,7 +29,7 @@ paths: timelineType: $ref: '../model/components.yaml#/components/schemas/TimelineType' responses: - 200: + '200': description: Indicates that the draft timeline was successfully created. In the event the user already has a draft timeline, the existing draft timeline is cleared and returned. content: application/json: @@ -46,7 +46,7 @@ paths: $ref: '../model/components.yaml#/components/schemas/TimelineResponse' required: - data - 403: + '403': description: Indicates that the user does not have the required permissions to create a draft timeline. content: application:json: @@ -57,7 +57,7 @@ paths: type: string status_code: type: number - 409: + '409': description: Indicates that there is already a draft timeline with the given timelineId. content: application:json: diff --git a/x-pack/plugins/security_solution/common/api/timeline/create_timelines/create_timelines_route_schema.yaml b/x-pack/plugins/security_solution/common/api/timeline/create_timelines/create_timelines_route_schema.yaml index 8c4585c83926a..d2e2817642943 100644 --- a/x-pack/plugins/security_solution/common/api/timeline/create_timelines/create_timelines_route_schema.yaml +++ b/x-pack/plugins/security_solution/common/api/timeline/create_timelines/create_timelines_route_schema.yaml @@ -16,7 +16,7 @@ paths: /api/timeline: post: operationId: createTimelines - description: Creates a new timeline. + summary: Creates a new timeline. tags: - access:securitySolution requestBody: @@ -52,7 +52,7 @@ paths: timeline: $ref: '../model/components.yaml#/components/schemas/SavedTimeline' responses: - 200: + '200': description: Indicates the timeline was successfully created. content: application/json: @@ -69,7 +69,7 @@ paths: $ref: '../model/components.yaml#/components/schemas/TimelineResponse' required: - data - 405: + '405': description: Indicates that there was an error in the timeline creation. content: application/json: 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 0f39551e757b6..16901b9b0f804 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 @@ -13,7 +13,7 @@ paths: /api/note: delete: operationId: deleteNote - description: Deletes a note from a timeline. + summary: Deletes a note from a timeline. tags: - access:securitySolution requestBody: @@ -36,5 +36,12 @@ paths: type: string nullable: true responses: - 200: - description: Indicates the note was successfully deleted. \ No newline at end of file + '200': + description: Indicates the note was successfully deleted. + content: + application/json: + schema: + type: object + properties: + data: + type: object diff --git a/x-pack/plugins/security_solution/common/api/timeline/delete_timelines/delete_timelines_route_schema.yaml b/x-pack/plugins/security_solution/common/api/timeline/delete_timelines/delete_timelines_route_schema.yaml index dba0471992729..5be42a6696d63 100644 --- a/x-pack/plugins/security_solution/common/api/timeline/delete_timelines/delete_timelines_route_schema.yaml +++ b/x-pack/plugins/security_solution/common/api/timeline/delete_timelines/delete_timelines_route_schema.yaml @@ -16,7 +16,7 @@ paths: /api/timeline: delete: operationId: deleteTimelines - description: Deletes one or more timelines or timeline templates. + summary: Deletes one or more timelines or timeline templates. tags: - access:securitySolution requestBody: @@ -33,12 +33,13 @@ paths: type: array items: type: string - searchId: + searchIds: type: array + description: Saved search ids that should be deleted alongside the timelines items: type: string responses: - 200: + '200': description: Indicates the timeline was successfully deleted. content: application/json: diff --git a/x-pack/plugins/security_solution/common/api/timeline/export_timelines/export_timelines_route_schema.yaml b/x-pack/plugins/security_solution/common/api/timeline/export_timelines/export_timelines_route_schema.yaml index c846de607b909..360c53e6d72e8 100644 --- a/x-pack/plugins/security_solution/common/api/timeline/export_timelines/export_timelines_route_schema.yaml +++ b/x-pack/plugins/security_solution/common/api/timeline/export_timelines/export_timelines_route_schema.yaml @@ -16,7 +16,7 @@ paths: /api/timeline/_export: post: operationId: exportTimelines - description: Exports timelines as an NDJSON file + summary: Exports timelines as an NDJSON file tags: - access:securitySolution parameters: @@ -26,7 +26,7 @@ paths: type: string description: The name of the file to export requestBody: - description: The id of the timelines to export + description: The ids of the timelines to export required: true content: application/json: @@ -39,14 +39,14 @@ paths: items: type: string responses: - 200: + '200': description: Indicates the timelines were successfully exported content: application/ndjson: schema: type: string description: NDJSON of the exported timelines - 400: + '400': description: Indicates that the export size limit was exceeded content: application/ndjson: diff --git a/x-pack/plugins/security_solution/common/api/timeline/get_draft_timelines/get_draft_timelines_route_schema.yaml b/x-pack/plugins/security_solution/common/api/timeline/get_draft_timelines/get_draft_timelines_route_schema.yaml index 722f6c3ad0a53..c0a73bcaefee2 100644 --- a/x-pack/plugins/security_solution/common/api/timeline/get_draft_timelines/get_draft_timelines_route_schema.yaml +++ b/x-pack/plugins/security_solution/common/api/timeline/get_draft_timelines/get_draft_timelines_route_schema.yaml @@ -2,9 +2,6 @@ openapi: 3.0.0 info: title: Elastic Security - Timeline - Get Draft Timelines API version: 8.9.0 -externalDocs: - url: https://www.elastic.co/guide/en/security/current/_get_timeline_timeline_template_by_savedobjectid.html - description: Documentation servers: - url: 'http://{kibana_host}:{port}' variables: @@ -16,7 +13,7 @@ paths: /api/timeline/_draft: get: operationId: getDraftTimelines - description: Retrieves the draft timeline for the current user. If the user does not have a draft timeline, an empty timeline is returned. + summary: Retrieves the draft timeline for the current user. If the user does not have a draft timeline, an empty timeline is returned. tags: - access:securitySolution parameters: @@ -25,7 +22,7 @@ paths: schema: $ref: '../model/components.yaml#/components/schemas/TimelineType' responses: - 200: + '200': description: Indicates that the draft timeline was successfully retrieved. content: application/json: @@ -40,7 +37,7 @@ paths: properties: timeline: $ref: '../model/components.yaml#/components/schemas/TimelineResponse' - 403: + '403': description: If a draft timeline was not found and we attempted to create one, it indicates that the user does not have the required permissions to create a draft timeline. content: application:json: @@ -51,7 +48,7 @@ paths: type: string status_code: type: number - 409: + '409': description: This should never happen, but if a draft timeline was not found and we attempted to create one, it indicates that there is already a draft timeline with the given timelineId. content: application:json: diff --git a/x-pack/plugins/security_solution/common/api/timeline/get_timeline/get_timeline_route_schema.yaml b/x-pack/plugins/security_solution/common/api/timeline/get_timeline/get_timeline_route_schema.yaml index 9415a8ce2b60c..0341a26c356cf 100644 --- a/x-pack/plugins/security_solution/common/api/timeline/get_timeline/get_timeline_route_schema.yaml +++ b/x-pack/plugins/security_solution/common/api/timeline/get_timeline/get_timeline_route_schema.yaml @@ -3,7 +3,7 @@ info: title: Elastic Security - Timeline - Get Timeline API version: 8.9.0 externalDocs: - url: https://www.elastic.co/guide/en/security/current/_get_timeline_timeline_template_by_savedobjectid.html + url: https://www.elastic.co/guide/en/security/current/_get_timeline_or_timeline_template_by_savedobjectid.html description: Documentation servers: - url: 'http://{kibana_host}:{port}' @@ -16,7 +16,7 @@ paths: /api/timeline: get: operationId: getTimeline - description: Get an existing saved timeline or timeline template. This API is used to retrieve an existing saved timeline or timeline template. + summary: Get an existing saved timeline or timeline template. This API is used to retrieve an existing saved timeline or timeline template. tags: - access:securitySolution parameters: @@ -31,8 +31,8 @@ paths: type: string description: The ID of the timeline to retrieve responses: - 200: - description: Indicates that the draft timeline was successfully created. In the event the user already has a draft timeline, the existing draft timeline is cleared and returned. + '200': + description: Indicates that the (template) timeline was found and returned. content: application/json: schema: diff --git a/x-pack/plugins/security_solution/common/api/timeline/get_timelines/get_timelines_route_schema.yaml b/x-pack/plugins/security_solution/common/api/timeline/get_timelines/get_timelines_route_schema.yaml index ac501f0d7729e..beb452557cd8e 100644 --- a/x-pack/plugins/security_solution/common/api/timeline/get_timelines/get_timelines_route_schema.yaml +++ b/x-pack/plugins/security_solution/common/api/timeline/get_timelines/get_timelines_route_schema.yaml @@ -16,7 +16,7 @@ paths: /api/timelines: get: operationId: getTimelines - description: This API is used to retrieve a list of existing saved timelines or timeline templates. + summary: This API is used to retrieve a list of existing saved timelines or timeline templates. tags: - access:securitySolution parameters: @@ -68,8 +68,8 @@ paths: - $ref: '../model/components.yaml#/components/schemas/TimelineStatus' - nullable: true responses: - 200: - description: Indicates that the draft timeline was successfully created. In the event the user already has a draft timeline, the existing draft timeline is cleared and returned. + '200': + description: Indicates that the (template) timelines were found and returned. content: application/json: schema: @@ -96,7 +96,7 @@ paths: type: number required: - data - 400: + '400': description: Bad request. The user supplied invalid data. content: application:json: diff --git a/x-pack/plugins/security_solution/common/api/timeline/import_timelines/import_timelines_route_schema.yaml b/x-pack/plugins/security_solution/common/api/timeline/import_timelines/import_timelines_route_schema.yaml index d12a9ca1b9210..7ada9a0e4a148 100644 --- a/x-pack/plugins/security_solution/common/api/timeline/import_timelines/import_timelines_route_schema.yaml +++ b/x-pack/plugins/security_solution/common/api/timeline/import_timelines/import_timelines_route_schema.yaml @@ -16,7 +16,7 @@ paths: /api/timeline/_import: post: operationId: importTimelines - description: Imports timelines. + summary: Imports timelines. tags: - access:securitySolution requestBody: @@ -40,7 +40,7 @@ paths: headers: type: object responses: - 200: + '200': description: Indicates the import of timelines was successful. content: application/json: @@ -52,7 +52,7 @@ paths: required: - data - 400: + '400': description: Indicates the import of timelines was unsuccessful because of an invalid file extension. content: application/json: @@ -66,7 +66,7 @@ paths: statusCode: type: number - 404: + '404': description: Indicates that we were unable to locate the saved object client necessary to handle the import. content: application/json: @@ -77,7 +77,7 @@ paths: type: string statusCode: type: number - 409: + '409': description: Indicates the import of timelines was unsuccessful. content: application/json: diff --git a/x-pack/plugins/security_solution/common/api/timeline/install_prepackaged_timelines/install_prepackaged_timelines_route_schema.yaml b/x-pack/plugins/security_solution/common/api/timeline/install_prepackaged_timelines/install_prepackaged_timelines_route_schema.yaml index ffad91cab9635..247e6aa8e3f68 100644 --- a/x-pack/plugins/security_solution/common/api/timeline/install_prepackaged_timelines/install_prepackaged_timelines_route_schema.yaml +++ b/x-pack/plugins/security_solution/common/api/timeline/install_prepackaged_timelines/install_prepackaged_timelines_route_schema.yaml @@ -13,7 +13,7 @@ paths: /api/timeline/_prepackaged: post: operationId: installPrepackedTimelines - description: Installs prepackaged timelines. + summary: Installs prepackaged timelines. tags: - access:securitySolution requestBody: @@ -41,7 +41,7 @@ paths: items: $ref: '../model/components.yaml#/components/schemas/SavedTimeline' responses: - 200: + '200': description: Indicates the installation of prepackaged timelines was successful. content: application/json: @@ -52,7 +52,7 @@ paths: $ref: '../model/components.yaml#/components/schemas/ImportTimelineResult' required: - data - 500: + '500': description: Indicates the installation of prepackaged timelines was unsuccessful. content: application:json: diff --git a/x-pack/plugins/security_solution/common/api/timeline/patch_timelines/patch_timeline_route_schema.yaml b/x-pack/plugins/security_solution/common/api/timeline/patch_timelines/patch_timeline_route_schema.yaml index 4783e42411973..2a4f1e1fadfa7 100644 --- a/x-pack/plugins/security_solution/common/api/timeline/patch_timelines/patch_timeline_route_schema.yaml +++ b/x-pack/plugins/security_solution/common/api/timeline/patch_timelines/patch_timeline_route_schema.yaml @@ -32,7 +32,7 @@ paths: timeline: $ref: '../model/components.yaml#/components/schemas/SavedTimeline' responses: - 200: + '200': description: Indicates that the draft timeline was successfully created. In the event the user already has a draft timeline, the existing draft timeline is cleared and returned. content: application/json: @@ -49,7 +49,7 @@ paths: $ref: '../model/components.yaml#/components/schemas/TimelineResponse' required: - data - 405: + '405': description: Indicates that the user does not have the required access to create a draft timeline. content: application/json: diff --git a/x-pack/plugins/security_solution/common/api/timeline/persist_favorite/persist_favorite_route_schema.yaml b/x-pack/plugins/security_solution/common/api/timeline/persist_favorite/persist_favorite_route_schema.yaml index aef8f2b2cf4c1..88eced8f9a843 100644 --- a/x-pack/plugins/security_solution/common/api/timeline/persist_favorite/persist_favorite_route_schema.yaml +++ b/x-pack/plugins/security_solution/common/api/timeline/persist_favorite/persist_favorite_route_schema.yaml @@ -1,6 +1,6 @@ openapi: 3.0.0 info: - title: Elastic Security - Timeline - Favorite API (https://www.elastic.co/guide/en/security/current/timeline-api-delete.html) + title: Elastic Security - Timeline - Favorite API version: 8.9.0 servers: - url: 'http://{kibana_host}:{port}' @@ -13,11 +13,11 @@ paths: /api/timeline/_favorite: patch: operationId: persistFavoriteRoute - description: Persists a given users favorite status of a timeline. + summary: Persists a given users favorite status of a timeline. tags: - access:securitySolution requestBody: - description: The required timeline fields used to create a new timeline along with optional fields that will be created if not provided. + description: The required fields used to favorite a (template) timeline. required: true content: application/json: @@ -38,7 +38,7 @@ paths: - $ref: '../model/components.yaml#/components/schemas/TimelineType' - nullable: true responses: - 200: + '200': description: Indicates the favorite status was successfully updated. content: application/json: @@ -52,7 +52,7 @@ paths: $ref: '../model/components.yaml#/components/schemas/FavoriteTimelineResponse' required: - data - 403: + '403': description: Indicates the user does not have the required permissions to persist the favorite status. content: application:json: 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 f8ba6ecc9747c..f0c9e140f241e 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 @@ -15,8 +15,8 @@ servers: paths: /api/note: patch: - operationId: persistNoteroute - description: Persists a note to a timeline. + operationId: persistNoteRoute + summary: Persists a note to a timeline. tags: - access:securitySolution requestBody: @@ -41,8 +41,8 @@ paths: type: string nullable: true responses: - 200: - description: Indicates the favorite status was successfully updated. + '200': + description: Indicates the note was successfully created. content: application/json: schema: 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 6c39b80a782c6..506cd0cc15544 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 @@ -1,7 +1,10 @@ openapi: 3.0.0 info: - title: Elastic Security - Timeline - Pinned Event API (https://www.elastic.co/guide/en/security/current/_pin_an_event_to_an_existing_timeline.html) + title: Elastic Security - Timeline - Pinned Event API version: 8.14.0 +externalDocs: + url: https://www.elastic.co/guide/en/security/current/_pin_an_event_to_an_existing_timeline.html + description: Documentation servers: - url: 'http://{kibana_host}:{port}' variables: @@ -13,7 +16,7 @@ paths: /api/pinned_event: patch: operationId: persistPinnedEventRoute - description: Persists a pinned event to a timeline. + summary: Persists a pinned event to a timeline. tags: - access:securitySolution requestBody: @@ -34,7 +37,7 @@ paths: timelineId: type: string responses: - 200: + '200': description: Indicate the event was successfully pinned in the timeline. content: application/json: 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 f9476a14c2a08..6cab82872b924 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 @@ -48,22 +48,22 @@ export const deleteNoteRoute = ( const noteId = request.body?.noteId ?? ''; const noteIds = request.body?.noteIds ?? null; if (noteIds != null) { - const res = await deleteNote({ + await deleteNote({ request: frameworkRequest, noteIds, }); return response.ok({ - body: { data: { persistNote: res } }, + body: { data: {} }, }); } else { - const res = await deleteNote({ + await deleteNote({ request: frameworkRequest, noteIds: [noteId], }); return response.ok({ - body: { data: { persistNote: res } }, + body: { data: {} }, }); } } catch (err) { From 0a0bb1498e1ff78c4d3277853ebcda24a31543f9 Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Wed, 3 Jul 2024 10:28:15 -0700 Subject: [PATCH 03/13] [Security AI Assistant] Persist prompts (#187040) Moving prompts persistence layer from the local storage to the server side data stream `.kibana-elastic-ai-assistant-prompts` --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../impl/schemas/index.ts | 3 + .../prompts/bulk_crud_prompts_route.gen.ts | 24 ++- .../bulk_crud_prompts_route.schema.yaml | 41 ++++- .../impl/assistant/api/index.tsx | 1 + .../api/prompts/bulk_update_prompts.test.ts | 137 +++++++++++++++ .../api/prompts/bulk_update_prompts.ts | 54 ++++++ .../impl/assistant/api/prompts/index.tsx | 9 + .../api/prompts/use_fetch_prompts.test.tsx | 61 +++++++ .../api/prompts/use_fetch_prompts.ts | 101 ++++++++++++ .../assistant_header_flyout.tsx | 6 + .../assistant/assistant_header/index.test.tsx | 1 + .../impl/assistant/assistant_header/index.tsx | 10 ++ .../assistant/chat_send/use_chat_send.tsx | 6 +- .../conversation_selector/index.test.tsx | 1 + .../conversation_selector/index.tsx | 13 +- .../conversation_settings.tsx | 5 +- .../conversation_settings_editor.tsx | 7 +- .../use_conversation_changed.tsx | 5 +- .../index.tsx | 4 +- .../use_conversations_table.tsx | 12 +- .../impl/assistant/index.tsx | 54 ++++-- .../impl/assistant/prompt/helpers.ts | 7 +- .../assistant/prompt_editor/helpers.test.tsx | 4 +- .../impl/assistant/prompt_editor/helpers.tsx | 6 +- .../assistant/prompt_editor/index.test.tsx | 1 + .../impl/assistant/prompt_editor/index.tsx | 15 +- .../system_prompt/helpers.test.tsx | 8 +- .../prompt_editor/system_prompt/helpers.tsx | 11 +- .../system_prompt/index.test.tsx | 19 ++- .../prompt_editor/system_prompt/index.tsx | 17 +- .../select_system_prompt/index.test.tsx | 27 ++- .../select_system_prompt/index.tsx | 30 ++-- .../system_prompt_editor.tsx | 94 ++++++++++- .../system_prompt_selector.test.tsx | 4 +- .../system_prompt_selector.tsx | 14 +- .../system_prompt_settings.test.tsx | 3 + .../system_prompt_settings.tsx | 4 + .../system_prompt_modal/types.ts | 16 +- .../use_system_prompt_editor.test.tsx | 23 ++- .../use_system_prompt_editor.tsx | 48 +++++- .../index.tsx | 27 ++- .../use_system_prompt_table.test.tsx | 9 +- .../use_system_prompt_table.tsx | 8 +- .../utils.test.tsx | 4 +- .../utils.tsx | 4 +- .../quick_prompt_selector.test.tsx | 12 +- .../quick_prompt_selector.tsx | 22 ++- .../quick_prompt_editor.tsx | 156 +++++++++++++++--- .../quick_prompt_settings.test.tsx | 12 +- .../quick_prompt_settings.tsx | 19 ++- .../use_quick_prompt_editor.test.tsx | 35 +++- .../use_quick_prompt_editor.tsx | 61 +++++-- .../index.tsx | 35 ++-- .../use_quick_prompt_table.test.tsx | 10 +- .../use_quick_prompt_table.tsx | 24 +-- .../quick_prompts/quick_prompts.test.tsx | 3 +- .../assistant/quick_prompts/quick_prompts.tsx | 39 +++-- .../impl/assistant/quick_prompts/types.tsx | 25 --- .../assistant/settings/assistant_settings.tsx | 25 ++- .../settings/assistant_settings_button.tsx | 10 +- .../assistant_settings_management.test.tsx | 3 + .../assistant_settings_management.tsx | 46 +++++- .../use_settings_updater.test.tsx | 104 ++++++++---- .../use_settings_updater.tsx | 124 +++++++++----- .../impl/assistant/types.ts | 13 -- .../use_conversation/helpers.test.ts | 25 +-- .../assistant/use_conversation/helpers.ts | 16 +- .../impl/assistant/use_conversation/index.tsx | 10 +- .../impl/assistant_context/index.tsx | 51 +----- .../impl/content/prompts/system/index.tsx | 36 ---- .../impl/content/prompts/user/translations.ts | 26 --- .../impl/mock/quick_prompt.ts | 49 ++++-- .../impl/mock/system_prompt/index.ts | 24 ++- .../mock/test_providers/test_providers.tsx | 1 + .../impl/mock/user_prompt/index.ts | 6 +- .../packages/kbn-elastic-assistant/index.ts | 17 +- .../mock/test_providers/test_providers.tsx | 1 + .../server/__mocks__/prompts_schema.mock.ts | 30 ++-- .../prompts/field_maps_configuration.ts | 14 +- .../prompts/helpers.ts | 41 +++-- .../prompts/types.ts | 11 +- .../server/routes/prompts/find_route.ts | 2 +- .../content/prompts/system/index.tsx | 14 +- .../assistant/content/quick_prompts/index.tsx | 57 +++++-- .../public/assistant/provider.tsx | 73 ++++++-- .../common/mock/mock_assistant_provider.tsx | 1 + .../rule_status_failed_callout.test.tsx | 1 + .../translations/translations/fr-FR.json | 2 - .../translations/translations/ja-JP.json | 2 - .../translations/translations/zh-CN.json | 2 - 90 files changed, 1627 insertions(+), 621 deletions(-) create mode 100644 x-pack/packages/kbn-elastic-assistant/impl/assistant/api/prompts/bulk_update_prompts.test.ts create mode 100644 x-pack/packages/kbn-elastic-assistant/impl/assistant/api/prompts/bulk_update_prompts.ts create mode 100644 x-pack/packages/kbn-elastic-assistant/impl/assistant/api/prompts/index.tsx create mode 100644 x-pack/packages/kbn-elastic-assistant/impl/assistant/api/prompts/use_fetch_prompts.test.tsx create mode 100644 x-pack/packages/kbn-elastic-assistant/impl/assistant/api/prompts/use_fetch_prompts.ts delete mode 100644 x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/types.tsx delete mode 100644 x-pack/packages/kbn-elastic-assistant/impl/content/prompts/system/index.tsx delete mode 100644 x-pack/packages/kbn-elastic-assistant/impl/content/prompts/user/translations.ts diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/index.ts b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/index.ts index eb5d0738f378b..1cbe8bbf8e792 100644 --- a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/index.ts +++ b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/index.ts @@ -52,3 +52,6 @@ export * from './knowledge_base/bulk_crud_knowledge_base_route.gen'; export * from './knowledge_base/common_attributes.gen'; export * from './knowledge_base/crud_knowledge_base_route.gen'; export * from './knowledge_base/find_knowledge_base_entries_route.gen'; + +export * from './prompts/find_prompts_route.gen'; +export { PromptResponse, PromptTypeEnum } from './prompts/bulk_crud_prompts_route.gen'; diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/prompts/bulk_crud_prompts_route.gen.ts b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/prompts/bulk_crud_prompts_route.gen.ts index d0bd99e063d0c..2d7a4d762eecc 100644 --- a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/prompts/bulk_crud_prompts_route.gen.ts +++ b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/prompts/bulk_crud_prompts_route.gen.ts @@ -34,6 +34,14 @@ export const PromptDetailsInError = z.object({ name: z.string().optional(), }); +/** + * Prompt type + */ +export type PromptType = z.infer; +export const PromptType = z.enum(['system', 'quick']); +export type PromptTypeEnum = typeof PromptType.enum; +export const PromptTypeEnum = PromptType.enum; + export type NormalizedPromptError = z.infer; export const NormalizedPromptError = z.object({ message: z.string(), @@ -47,11 +55,13 @@ export const PromptResponse = z.object({ id: NonEmptyString, timestamp: NonEmptyString.optional(), name: z.string(), - promptType: z.string(), + promptType: PromptType, content: z.string(), + categories: z.array(z.string()).optional(), + color: z.string().optional(), isNewConversationDefault: z.boolean().optional(), isDefault: z.boolean().optional(), - isShared: z.boolean().optional(), + consumer: z.string().optional(), updatedAt: z.string().optional(), updatedBy: z.string().optional(), createdAt: z.string().optional(), @@ -107,20 +117,24 @@ export const BulkActionBase = z.object({ export type PromptCreateProps = z.infer; export const PromptCreateProps = z.object({ name: z.string(), - promptType: z.string(), + promptType: PromptType, content: z.string(), + color: z.string().optional(), + categories: z.array(z.string()).optional(), isNewConversationDefault: z.boolean().optional(), isDefault: z.boolean().optional(), - isShared: z.boolean().optional(), + consumer: z.string().optional(), }); export type PromptUpdateProps = z.infer; export const PromptUpdateProps = z.object({ id: z.string(), content: z.string().optional(), + color: z.string().optional(), + categories: z.array(z.string()).optional(), isNewConversationDefault: z.boolean().optional(), isDefault: z.boolean().optional(), - isShared: z.boolean().optional(), + consumer: z.string().optional(), }); export type PerformBulkActionRequestBody = z.infer; diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/prompts/bulk_crud_prompts_route.schema.yaml b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/prompts/bulk_crud_prompts_route.schema.yaml index ede0136ba710a..5be6bde140b85 100644 --- a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/prompts/bulk_crud_prompts_route.schema.yaml +++ b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/prompts/bulk_crud_prompts_route.schema.yaml @@ -78,6 +78,13 @@ components: required: - id + PromptType: + type: string + description: Prompt type + enum: + - system + - quick + NormalizedPromptError: type: object properties: @@ -111,15 +118,21 @@ components: name: type: string promptType: - type: string + $ref: '#/components/schemas/PromptType' content: type: string + categories: + type: array + items: + type: string + color: + type: string isNewConversationDefault: type: boolean isDefault: type: boolean - isShared: - type: boolean + consumer: + type: string updatedAt: type: string updatedBy: @@ -231,15 +244,21 @@ components: name: type: string promptType: - type: string + $ref: '#/components/schemas/PromptType' content: type: string + color: + type: string + categories: + type: array + items: + type: string isNewConversationDefault: type: boolean isDefault: type: boolean - isShared: - type: boolean + consumer: + type: string PromptUpdateProps: type: object @@ -250,9 +269,15 @@ components: type: string content: type: string + color: + type: string + categories: + type: array + items: + type: string isNewConversationDefault: type: boolean isDefault: type: boolean - isShared: - type: boolean + consumer: + type: string diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/index.tsx index b8c42a787621b..8b6bba33f680a 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/index.tsx @@ -11,6 +11,7 @@ import { API_ERROR } from '../translations'; import { getOptionalRequestParams } from '../helpers'; import { TraceOptions } from '../types'; export * from './conversations'; +export * from './prompts'; export interface FetchConnectorExecuteAction { conversationId: string; diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/prompts/bulk_update_prompts.test.ts b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/prompts/bulk_update_prompts.test.ts new file mode 100644 index 0000000000000..d6ba2e726a76f --- /dev/null +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/prompts/bulk_update_prompts.test.ts @@ -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 { + API_VERSIONS, + ELASTIC_AI_ASSISTANT_PROMPTS_URL_BULK_ACTION, +} from '@kbn/elastic-assistant-common'; +import { httpServiceMock } from '@kbn/core-http-browser-mocks'; +import { IToasts } from '@kbn/core-notifications-browser'; +import { bulkUpdatePrompts } from './bulk_update_prompts'; +import { PromptTypeEnum } from '@kbn/elastic-assistant-common/impl/schemas/prompts/bulk_crud_prompts_route.gen'; + +const prompt1 = { + id: 'field1', + content: 'Prompt 1', + name: 'test', + promptType: PromptTypeEnum.system, +}; +const prompt2 = { + ...prompt1, + id: 'field2', + content: 'Prompt 2', + name: 'test2', + promptType: PromptTypeEnum.system, +}; +const toasts = { + addError: jest.fn(), +}; +describe('bulkUpdatePrompts', () => { + let httpMock: ReturnType; + + beforeEach(() => { + httpMock = httpServiceMock.createSetupContract(); + + jest.clearAllMocks(); + }); + it('should send a POST request with the correct parameters and receive a successful response', async () => { + const promptsActions = { + create: [], + update: [], + delete: { ids: [] }, + }; + + await bulkUpdatePrompts(httpMock, promptsActions); + + expect(httpMock.fetch).toHaveBeenCalledWith(ELASTIC_AI_ASSISTANT_PROMPTS_URL_BULK_ACTION, { + method: 'POST', + version: API_VERSIONS.internal.v1, + body: JSON.stringify({ + create: [], + update: [], + delete: { ids: [] }, + }), + }); + }); + + it('should transform the prompts dictionary to an array of fields to create', async () => { + const promptsActions = { + create: [prompt1, prompt2], + update: [], + delete: { ids: [] }, + }; + + await bulkUpdatePrompts(httpMock, promptsActions); + + expect(httpMock.fetch).toHaveBeenCalledWith(ELASTIC_AI_ASSISTANT_PROMPTS_URL_BULK_ACTION, { + method: 'POST', + version: API_VERSIONS.internal.v1, + body: JSON.stringify({ + create: [prompt1, prompt2], + update: [], + delete: { ids: [] }, + }), + }); + }); + + it('should transform the prompts dictionary to an array of fields to update', async () => { + const promptsActions = { + update: [prompt1, prompt2], + delete: { ids: [] }, + }; + + await bulkUpdatePrompts(httpMock, promptsActions); + + expect(httpMock.fetch).toHaveBeenCalledWith(ELASTIC_AI_ASSISTANT_PROMPTS_URL_BULK_ACTION, { + method: 'POST', + version: API_VERSIONS.internal.v1, + body: JSON.stringify({ + update: [prompt1, prompt2], + delete: { ids: [] }, + }), + }); + }); + + it('should throw an error with the correct message when receiving an unsuccessful response', async () => { + httpMock.fetch.mockResolvedValue({ + success: false, + attributes: { + errors: [ + { + statusCode: 400, + message: 'Error updating prompt', + prompts: [{ id: prompt1.id, name: prompt1.content }], + }, + ], + }, + }); + const promptsActions = { + create: [], + update: [prompt1], + delete: { ids: [] }, + }; + await bulkUpdatePrompts(httpMock, promptsActions, toasts as unknown as IToasts); + expect(toasts.addError.mock.calls[0][0]).toEqual( + new Error('Error message: Error updating prompt for prompt Prompt 1') + ); + }); + + it('should handle cases where result.attributes.errors is undefined', async () => { + httpMock.fetch.mockResolvedValue({ + success: false, + attributes: {}, + }); + const promptsActions = { + create: [], + update: [], + delete: { ids: [] }, + }; + + await bulkUpdatePrompts(httpMock, promptsActions, toasts as unknown as IToasts); + expect(toasts.addError.mock.calls[0][0]).toEqual(new Error('')); + }); +}); diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/prompts/bulk_update_prompts.ts b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/prompts/bulk_update_prompts.ts new file mode 100644 index 0000000000000..2d7b053d7acbd --- /dev/null +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/prompts/bulk_update_prompts.ts @@ -0,0 +1,54 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { HttpSetup, IToasts } from '@kbn/core/public'; +import { + ELASTIC_AI_ASSISTANT_PROMPTS_URL_BULK_ACTION, + API_VERSIONS, +} from '@kbn/elastic-assistant-common'; +import { + PerformBulkActionRequestBody, + PerformBulkActionResponse, +} from '@kbn/elastic-assistant-common/impl/schemas/prompts/bulk_crud_prompts_route.gen'; + +export const bulkUpdatePrompts = async ( + http: HttpSetup, + prompts: PerformBulkActionRequestBody, + toasts?: IToasts +) => { + try { + const result = await http.fetch( + ELASTIC_AI_ASSISTANT_PROMPTS_URL_BULK_ACTION, + { + method: 'POST', + version: API_VERSIONS.internal.v1, + body: JSON.stringify(prompts), + } + ); + + if (!result.success) { + const serverError = result.attributes.errors + ?.map( + (e) => + `${e.status_code ? `Error code: ${e.status_code}. ` : ''}Error message: ${ + e.message + } for prompt ${e.prompts.map((c) => c.name).join(',')}` + ) + .join(',\n'); + throw new Error(serverError); + } + return result; + } catch (error) { + toasts?.addError(error.body && error.body.message ? new Error(error.body.message) : error, { + title: i18n.translate('xpack.elasticAssistant.prompts.bulkActionspromptsError', { + defaultMessage: 'Error updating prompts {error}', + values: { error }, + }), + }); + } +}; diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/prompts/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/prompts/index.tsx new file mode 100644 index 0000000000000..5a2f6bb80992b --- /dev/null +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/prompts/index.tsx @@ -0,0 +1,9 @@ +/* + * 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 * from './bulk_update_prompts'; +export * from './use_fetch_prompts'; diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/prompts/use_fetch_prompts.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/prompts/use_fetch_prompts.test.tsx new file mode 100644 index 0000000000000..3ec1586d6cb44 --- /dev/null +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/prompts/use_fetch_prompts.test.tsx @@ -0,0 +1,61 @@ +/* + * 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 { act, renderHook } from '@testing-library/react-hooks'; + +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import type { ReactNode } from 'react'; +import React from 'react'; +import { useFetchPrompts } from './use_fetch_prompts'; +import { HttpSetup } from '@kbn/core-http-browser'; +import { useAssistantContext } from '../../../assistant_context'; +import { API_VERSIONS, defaultAssistantFeatures } from '@kbn/elastic-assistant-common'; + +const http = { + fetch: jest.fn().mockResolvedValue(defaultAssistantFeatures), +} as unknown as HttpSetup; + +jest.mock('../../../assistant_context'); + +const createWrapper = () => { + const queryClient = new QueryClient(); + // eslint-disable-next-line react/display-name + return ({ children }: { children: ReactNode }) => ( + {children} + ); +}; + +describe('useFetchPrompts', () => { + (useAssistantContext as jest.Mock).mockReturnValue({ + http, + assistantAvailability: { + isAssistantEnabled: true, + }, + }); + it(`should make http request to fetch prompts`, async () => { + renderHook(() => useFetchPrompts(), { + wrapper: createWrapper(), + }); + + await act(async () => { + const { waitForNextUpdate } = renderHook(() => useFetchPrompts()); + await waitForNextUpdate(); + expect(http.fetch).toHaveBeenCalledWith('/internal/elastic_assistant/prompts/_find', { + method: 'GET', + query: { + page: 1, + per_page: 1000, + filter: 'consumer:*', + }, + version: API_VERSIONS.internal.v1, + signal: undefined, + }); + + expect(http.fetch).toHaveBeenCalled(); + }); + }); +}); diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/prompts/use_fetch_prompts.ts b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/prompts/use_fetch_prompts.ts new file mode 100644 index 0000000000000..18a229e524dc7 --- /dev/null +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/prompts/use_fetch_prompts.ts @@ -0,0 +1,101 @@ +/* + * 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 { FindPromptsResponse } from '@kbn/elastic-assistant-common/impl/schemas/prompts/find_prompts_route.gen'; +import { useQuery } from '@tanstack/react-query'; +import { API_VERSIONS, ELASTIC_AI_ASSISTANT_PROMPTS_URL_FIND } from '@kbn/elastic-assistant-common'; +import { HttpSetup, IToasts } from '@kbn/core/public'; +import { i18n } from '@kbn/i18n'; +import { useAssistantContext } from '../../../assistant_context'; + +export interface UseFetchPromptsParams { + signal?: AbortSignal | undefined; + consumer?: string; +} + +/** + * API call for fetching prompts for current spaceId + * + * @param {Object} options - The options object. + * @param {string} options.consumer - prompt consumer + * @param {AbortSignal} [options.signal] - AbortSignal + * + * @returns {useQuery} hook for getting the status of the prompts + */ + +export const useFetchPrompts = (payload?: UseFetchPromptsParams) => { + const { + assistantAvailability: { isAssistantEnabled }, + http, + } = useAssistantContext(); + + const QUERY = { + page: 1, + per_page: 1000, // Continue use in-memory paging till the new design will be ready + filter: `consumer:${payload?.consumer ?? '*'}`, + }; + + const CACHING_KEYS = [ + ELASTIC_AI_ASSISTANT_PROMPTS_URL_FIND, + QUERY.page, + QUERY.per_page, + QUERY.filter, + API_VERSIONS.internal.v1, + ]; + + return useQuery( + CACHING_KEYS, + async () => + http.fetch(ELASTIC_AI_ASSISTANT_PROMPTS_URL_FIND, { + method: 'GET', + version: API_VERSIONS.internal.v1, + query: QUERY, + signal: payload?.signal, + }), + { + initialData: { + data: [], + page: 1, + perPage: 5, + total: 0, + }, + placeholderData: { + data: [], + page: 1, + perPage: 5, + total: 0, + }, + keepPreviousData: true, + enabled: isAssistantEnabled, + } + ); +}; + +export const getPrompts = async ({ + http, + signal, + toasts, +}: { + http: HttpSetup; + toasts: IToasts; + signal?: AbortSignal | undefined; +}) => { + try { + return await http.fetch(ELASTIC_AI_ASSISTANT_PROMPTS_URL_FIND, { + method: 'GET', + version: API_VERSIONS.internal.v1, + signal, + }); + } catch (error) { + toasts.addError(error.body && error.body.message ? new Error(error.body.message) : error, { + title: i18n.translate('xpack.elasticAssistant.prompts.getPromptsError', { + defaultMessage: 'Error fetching prompts', + }), + }); + throw error; + } +}; diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_header/assistant_header_flyout.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_header/assistant_header_flyout.tsx index bd75e80aef0ca..5725d983eff33 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_header/assistant_header_flyout.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_header/assistant_header_flyout.tsx @@ -6,6 +6,7 @@ */ import React, { useState, useMemo, useCallback } from 'react'; +import { QueryObserverResult, RefetchOptions, RefetchQueryFilters } from '@tanstack/react-query'; import { EuiFlexGroup, EuiFlexItem, @@ -47,6 +48,9 @@ interface OwnProps { refetchConversationsState: () => Promise; onConversationCreate: () => Promise; isAssistantEnabled: boolean; + refetchPrompts?: ( + options?: RefetchOptions & RefetchQueryFilters + ) => Promise>; } type Props = OwnProps; @@ -74,6 +78,7 @@ export const AssistantHeaderFlyout: React.FC = ({ refetchConversationsState, onConversationCreate, isAssistantEnabled, + refetchPrompts, }) => { const showAnonymizedValuesChecked = useMemo( () => @@ -164,6 +169,7 @@ export const AssistantHeaderFlyout: React.FC = ({ conversationsLoaded={conversationsLoaded} refetchConversationsState={refetchConversationsState} isFlyoutMode={true} + refetchPrompts={refetchPrompts} /> diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_header/index.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_header/index.test.tsx index 2301078d57ba1..f806f5d1ef7c6 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_header/index.test.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_header/index.test.tsx @@ -38,6 +38,7 @@ const testProps = { refetchConversationsState: jest.fn(), anonymizationFields: { total: 0, page: 1, perPage: 1000, data: [] }, refetchAnonymizationFieldsResults: jest.fn(), + allPrompts: [], }; jest.mock('../../connectorland/use_load_connectors', () => ({ diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_header/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_header/index.tsx index ac7ca235b36ee..7507c14648614 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_header/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_header/index.tsx @@ -14,9 +14,11 @@ import { EuiSwitch, EuiToolTip, } from '@elastic/eui'; +import { QueryObserverResult, RefetchOptions, RefetchQueryFilters } from '@tanstack/react-query'; import { css } from '@emotion/react'; import { DocLinksStart } from '@kbn/core-doc-links-browser'; import { isEmpty } from 'lodash'; +import { PromptResponse } from '@kbn/elastic-assistant-common'; import { AIConnector } from '../../connectorland/connector_selector'; import { Conversation } from '../../..'; import { AssistantTitle } from '../assistant_title'; @@ -40,6 +42,10 @@ interface OwnProps { conversations: Record; conversationsLoaded: boolean; refetchConversationsState: () => Promise; + allPrompts: PromptResponse[]; + refetchPrompts?: ( + options?: RefetchOptions & RefetchQueryFilters + ) => Promise>; } type Props = OwnProps; @@ -64,6 +70,8 @@ export const AssistantHeader: React.FC = ({ conversations, conversationsLoaded, refetchConversationsState, + allPrompts, + refetchPrompts, }) => { const showAnonymizedValuesChecked = useMemo( () => @@ -122,6 +130,7 @@ export const AssistantHeader: React.FC = ({ isDisabled={isDisabled} conversations={conversations} onConversationDeleted={onConversationDeleted} + allPrompts={allPrompts} /> <> @@ -156,6 +165,7 @@ export const AssistantHeader: React.FC = ({ conversationsLoaded={conversationsLoaded} refetchConversationsState={refetchConversationsState} isFlyoutMode={false} + refetchPrompts={refetchPrompts} /> diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/chat_send/use_chat_send.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/chat_send/use_chat_send.tsx index f42fe17242d86..9d5e822fcdf55 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/chat_send/use_chat_send.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/chat_send/use_chat_send.tsx @@ -8,18 +8,18 @@ import React, { useCallback } from 'react'; import { HttpSetup } from '@kbn/core-http-browser'; import { i18n } from '@kbn/i18n'; -import { Replacements } from '@kbn/elastic-assistant-common'; +import { PromptResponse, Replacements } from '@kbn/elastic-assistant-common'; import type { ClientMessage } from '../../assistant_context/types'; import { SelectedPromptContext } from '../prompt_context/types'; import { useSendMessage } from '../use_send_message'; import { useConversation } from '../use_conversation'; import { getCombinedMessage } from '../prompt/helpers'; -import { Conversation, Prompt, useAssistantContext } from '../../..'; +import { Conversation, useAssistantContext } from '../../..'; import { getMessageFromRawResponse } from '../helpers'; import { getDefaultSystemPrompt } from '../use_conversation/helpers'; export interface UseChatSendProps { - allSystemPrompts: Prompt[]; + allSystemPrompts: PromptResponse[]; currentConversation?: Conversation; editingSystemPromptId: string | undefined; http: HttpSetup; diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_selector/index.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_selector/index.test.tsx index 6c2ae3b6af211..613163db196ae 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_selector/index.test.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_selector/index.test.tsx @@ -50,6 +50,7 @@ const defaultProps = { defaultProvider: OpenAiProviderType.OpenAi, conversations: mockConversations, onConversationDeleted, + allPrompts: [], }; describe('Conversation selector', () => { beforeAll(() => { diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_selector/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_selector/index.tsx index c55540b55b6c0..fd9cddc39dbbe 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_selector/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_selector/index.tsx @@ -18,10 +18,13 @@ import { import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { css } from '@emotion/react'; +import { + PromptResponse, + PromptTypeEnum, +} from '@kbn/elastic-assistant-common/impl/schemas/prompts/bulk_crud_prompts_route.gen'; import { getGenAiConfig } from '../../../connectorland/helpers'; import { AIConnector } from '../../../connectorland/connector_selector'; import { Conversation } from '../../../..'; -import { useAssistantContext } from '../../../assistant_context'; import * as i18n from './translations'; import { DEFAULT_CONVERSATION_TITLE } from '../../use_conversation/translations'; import { useConversation } from '../../use_conversation'; @@ -35,6 +38,7 @@ interface Props { shouldDisableKeyboardShortcut?: () => boolean; isDisabled?: boolean; conversations: Record; + allPrompts: PromptResponse[]; } const getPreviousConversationId = (conversationIds: string[], selectedConversationId: string) => { @@ -64,10 +68,13 @@ export const ConversationSelector: React.FC = React.memo( shouldDisableKeyboardShortcut = () => false, isDisabled = false, conversations, + allPrompts, }) => { - const { allSystemPrompts } = useAssistantContext(); - const { createConversation } = useConversation(); + const allSystemPrompts = useMemo( + () => allPrompts.filter((p) => p.promptType === PromptTypeEnum.system), + [allPrompts] + ); const conversationIds = useMemo(() => Object.keys(conversations), [conversations]); const conversationOptions = useMemo(() => { return Object.values(conversations).map((conversation) => ({ diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings/conversation_settings.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings/conversation_settings.tsx index adf2e012c0b8f..cba17030e1577 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings/conversation_settings.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings/conversation_settings.tsx @@ -18,7 +18,8 @@ import React, { useMemo } from 'react'; import { HttpSetup } from '@kbn/core-http-browser'; import { ActionTypeRegistryContract } from '@kbn/triggers-actions-ui-plugin/public'; -import { Conversation, Prompt } from '../../../..'; +import { PromptResponse } from '@kbn/elastic-assistant-common'; +import { Conversation } from '../../../..'; import * as i18n from './translations'; import { AIConnector } from '../../../connectorland/connector_selector'; @@ -33,7 +34,7 @@ import { getConversationApiConfig } from '../../use_conversation/helpers'; export interface ConversationSettingsProps { actionTypeRegistry: ActionTypeRegistryContract; - allSystemPrompts: Prompt[]; + allSystemPrompts: PromptResponse[]; connectors?: AIConnector[]; conversationSettings: Record; conversationsSettingsBulkActions: ConversationsBulkActions; diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings/conversation_settings_editor.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings/conversation_settings_editor.tsx index a0ddd33b34d05..41da376d21b73 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings/conversation_settings_editor.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings/conversation_settings_editor.tsx @@ -12,7 +12,8 @@ import { HttpSetup } from '@kbn/core-http-browser'; import { FormattedMessage } from '@kbn/i18n-react'; import { OpenAiProviderType } from '@kbn/stack-connectors-plugin/public/common'; import { noop } from 'lodash/fp'; -import { Conversation, Prompt } from '../../../..'; +import { PromptResponse } from '@kbn/elastic-assistant-common'; +import { Conversation } from '../../../..'; import * as i18n from './translations'; import * as i18nModel from '../../../connectorland/models/model_selector/translations'; @@ -25,7 +26,7 @@ import { ConversationsBulkActions } from '../../api'; import { getDefaultSystemPrompt } from '../../use_conversation/helpers'; export interface ConversationSettingsEditorProps { - allSystemPrompts: Prompt[]; + allSystemPrompts: PromptResponse[]; conversationSettings: Record; conversationsSettingsBulkActions: ConversationsBulkActions; http: HttpSetup; @@ -268,7 +269,7 @@ export const ConversationSettingsEditor: React.FC ; conversationsSettingsBulkActions: ConversationsBulkActions; defaultConnector?: AIConnector; diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings_management/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings_management/index.tsx index c4b2f834ecec5..485f89358f57a 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings_management/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings_management/index.tsx @@ -8,13 +8,13 @@ import { EuiPanel, EuiSpacer, EuiConfirmModal, EuiInMemoryTable } from '@elastic/eui'; import React, { useCallback, useMemo, useState } from 'react'; +import { PromptResponse } from '@kbn/elastic-assistant-common'; import { Conversation } from '../../../assistant_context/types'; import { ConversationTableItem, useConversationsTable } from './use_conversations_table'; import { ConversationStreamingSwitch } from '../conversation_settings/conversation_streaming_switch'; import { AIConnector } from '../../../connectorland/connector_selector'; import * as i18n from './translations'; -import { Prompt } from '../../types'; import { ConversationsBulkActions } from '../../api'; import { useAssistantContext } from '../../../assistant_context'; import { useConversationDeleted } from '../conversation_settings/use_conversation_deleted'; @@ -27,7 +27,7 @@ import { CONVERSATION_TABLE_SESSION_STORAGE_KEY } from '../../../assistant_conte import { useSessionPagination } from '../../common/components/assistant_settings_management/pagination/use_session_pagination'; import { DEFAULT_PAGE_SIZE } from '../../settings/const'; interface Props { - allSystemPrompts: Prompt[]; + allSystemPrompts: PromptResponse[]; assistantStreamingEnabled: boolean; connectors: AIConnector[] | undefined; conversationSettings: Record; diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings_management/use_conversations_table.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings_management/use_conversations_table.tsx index fb705db6bb33c..446fb33ebb9e9 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings_management/use_conversations_table.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/conversations/conversation_settings_management/use_conversations_table.tsx @@ -11,10 +11,10 @@ import { ActionTypeRegistryContract } from '@kbn/triggers-actions-ui-plugin/publ import { EuiBadge, EuiBasicTableColumn, EuiLink } from '@elastic/eui'; import { FormattedDate } from '@kbn/i18n-react'; +import { PromptResponse } from '@kbn/elastic-assistant-common'; import { Conversation } from '../../../assistant_context/types'; import { AIConnector } from '../../../connectorland/connector_selector'; import { getConnectorTypeTitle } from '../../../connectorland/helpers'; -import { Prompt } from '../../../..'; import { getConversationApiConfig, getInitialDefaultSystemPrompt, @@ -25,7 +25,7 @@ import { RowActions } from '../../common/components/assistant_settings_managemen const emptyConversations = {}; export interface GetConversationsListParams { - allSystemPrompts: Prompt[]; + allSystemPrompts: PromptResponse[]; actionTypeRegistry: ActionTypeRegistryContract; connectors: AIConnector[] | undefined; conversations: Record; @@ -126,7 +126,7 @@ export const useConversationsTable = () => { ); const connectorTypeTitle = getConnectorTypeTitle(connector, actionTypeRegistry); - const systemPrompt: Prompt | undefined = allSystemPrompts.find( + const systemPrompt: PromptResponse | undefined = allSystemPrompts.find( ({ id }) => id === conversation.apiConfig?.defaultSystemPromptId ); const defaultSystemPrompt = getInitialDefaultSystemPrompt({ @@ -135,10 +135,10 @@ export const useConversationsTable = () => { }); const systemPromptTitle = - systemPrompt?.label || systemPrompt?.name || - defaultSystemPrompt?.label || - defaultSystemPrompt?.name; + systemPrompt?.id || + defaultSystemPrompt?.name || + defaultSystemPrompt?.id; return { ...conversation, diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/index.tsx index 907e4d70accd5..6892fdcaf48bd 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/index.tsx @@ -39,6 +39,7 @@ import deepEqual from 'fast-deep-equal'; import { find, isEmpty, uniqBy } from 'lodash'; import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { PromptTypeEnum } from '@kbn/elastic-assistant-common/impl/schemas/prompts/bulk_crud_prompts_route.gen'; import { useChatSend } from './chat_send/use_chat_send'; import { ChatSend } from './chat_send'; import { BlockBotCallToAction } from './block_bot/cta'; @@ -91,6 +92,7 @@ import { getGenAiConfig } from '../connectorland/helpers'; import { AssistantAnimatedIcon } from './assistant_animated_icon'; import { useFetchAnonymizationFields } from './api/anonymization_fields/use_fetch_anonymization_fields'; import { InstallKnowledgeBaseButton } from '../knowledge_base/install_knowledge_base_button'; +import { useFetchPrompts } from './api/prompts/use_fetch_prompts'; export interface Props { conversationTitle?: string; @@ -135,7 +137,6 @@ const AssistantComponent: React.FC = ({ setLastConversationId, getLastConversationId, title, - allSystemPrompts, baseConversations, } = useAssistantContext(); @@ -182,6 +183,19 @@ const AssistantComponent: React.FC = ({ isFetched: isFetchedAnonymizationFields, } = useFetchAnonymizationFields(); + const { + data: { data: allPrompts }, + refetch: refetchPrompts, + isLoading: isLoadingPrompts, + } = useFetchPrompts(); + + const allSystemPrompts = useMemo(() => { + if (!isLoadingPrompts) { + return allPrompts.filter((p) => p.promptType === PromptTypeEnum.system); + } + return []; + }, [allPrompts, isLoadingPrompts]); + // Connector details const { data: connectors, isFetchedAfterMount: areConnectorsFetched } = useLoadConnectors({ http, @@ -397,7 +411,11 @@ const AssistantComponent: React.FC = ({ // End Scrolling const selectedSystemPrompt = useMemo( - () => getDefaultSystemPrompt({ allSystemPrompts, conversation: currentConversation }), + () => + getDefaultSystemPrompt({ + allSystemPrompts, + conversation: currentConversation, + }), [allSystemPrompts, currentConversation] ); @@ -409,20 +427,21 @@ const AssistantComponent: React.FC = ({ async ({ cId, cTitle }: { cId: string; cTitle: string }) => { const updatedConv = await refetchResults(); + let selectedConversation; if (cId === '') { setCurrentConversationId(cTitle); - setEditingSystemPromptId( - getDefaultSystemPrompt({ allSystemPrompts, conversation: updatedConv?.data?.[cTitle] }) - ?.id - ); + selectedConversation = updatedConv?.data?.[cTitle]; setCurrentConversationId(cTitle); } else { - const refetchedConversation = await refetchCurrentConversation({ cId }); - setEditingSystemPromptId( - getDefaultSystemPrompt({ allSystemPrompts, conversation: refetchedConversation })?.id - ); + selectedConversation = await refetchCurrentConversation({ cId }); setCurrentConversationId(cId); } + setEditingSystemPromptId( + getDefaultSystemPrompt({ + allSystemPrompts, + conversation: selectedConversation, + })?.id + ); }, [allSystemPrompts, refetchCurrentConversation, refetchResults] ); @@ -639,18 +658,18 @@ const AssistantComponent: React.FC = ({ setIsSettingsModalVisible={setIsSettingsModalVisible} setSelectedPromptContexts={setSelectedPromptContexts} isFlyoutMode={isFlyoutMode} + allSystemPrompts={allSystemPrompts} /> )} ), [ + getComments, abortStream, - refetchCurrentConversation, currentConversation, - editingSystemPromptId, - getComments, showAnonymizedValues, + refetchCurrentConversation, handleRegenerateResponse, isEnabledKnowledgeBase, isEnabledRAGAlerts, @@ -658,12 +677,14 @@ const AssistantComponent: React.FC = ({ currentUserAvatar, isFlyoutMode, selectedPromptContextsCount, + editingSystemPromptId, isNewConversation, isSettingsModalVisible, promptContexts, promptTextPreview, handleOnSystemPromptSelectionChange, selectedPromptContexts, + allSystemPrompts, ] ); @@ -859,6 +880,7 @@ const AssistantComponent: React.FC = ({ isSettingsModalVisible={isSettingsModalVisible} setIsSettingsModalVisible={setIsSettingsModalVisible} isFlyoutMode + allSystemPrompts={allSystemPrompts} /> @@ -882,6 +904,7 @@ const AssistantComponent: React.FC = ({ ); }, [ + allSystemPrompts, comments, connectorPrompt, currentConversation, @@ -948,6 +971,7 @@ const AssistantComponent: React.FC = ({ refetchConversationsState={refetchConversationsState} onConversationCreate={handleCreateConversation} isAssistantEnabled={isAssistantEnabled} + refetchPrompts={refetchPrompts} /> {/* Create portals for each EuiCodeBlock to add the `Investigate in Timeline` action */} @@ -1080,6 +1104,7 @@ const AssistantComponent: React.FC = ({ setIsSettingsModalVisible={setIsSettingsModalVisible} trackPrompt={trackPrompt} isFlyoutMode={isFlyoutMode} + allPrompts={allPrompts} /> )} @@ -1116,6 +1141,8 @@ const AssistantComponent: React.FC = ({ conversationsLoaded={conversationsLoaded} onConversationDeleted={handleOnConversationDeleted} refetchConversationsState={refetchConversationsState} + allPrompts={allPrompts} + refetchPrompts={refetchPrompts} /> )} @@ -1194,6 +1221,7 @@ const AssistantComponent: React.FC = ({ setIsSettingsModalVisible={setIsSettingsModalVisible} trackPrompt={trackPrompt} isFlyoutMode={isFlyoutMode} + allPrompts={allPrompts} /> )} diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt/helpers.ts b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt/helpers.ts index bd00058b4bbd3..4868eff04b4e7 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt/helpers.ts +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt/helpers.ts @@ -5,11 +5,10 @@ * 2.0. */ -import { Replacements, transformRawData } from '@kbn/elastic-assistant-common'; +import { Replacements, transformRawData, PromptResponse } from '@kbn/elastic-assistant-common'; import type { ClientMessage } from '../../assistant_context/types'; import { getAnonymizedValue as defaultGetAnonymizedValue } from '../get_anonymized_value'; import type { SelectedPromptContext } from '../prompt_context/types'; -import type { Prompt } from '../types'; import { SYSTEM_PROMPT_CONTEXT_NON_I18N } from '../../content/prompts/system/translations'; export const getSystemMessages = ({ @@ -17,7 +16,7 @@ export const getSystemMessages = ({ selectedSystemPrompt, }: { isNewChat: boolean; - selectedSystemPrompt: Prompt | undefined; + selectedSystemPrompt: PromptResponse | undefined; }): ClientMessage[] => { if (!isNewChat || selectedSystemPrompt == null) { return []; @@ -53,7 +52,7 @@ export function getCombinedMessage({ isNewChat: boolean; promptText: string; selectedPromptContexts: Record; - selectedSystemPrompt: Prompt | undefined; + selectedSystemPrompt: PromptResponse | undefined; }): ClientMessageWithReplacements { let replacements: Replacements = currentReplacements ?? {}; const onNewReplacements = (newReplacements: Replacements) => { diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/helpers.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/helpers.test.tsx index c055d9bd6bb95..2171b13273a28 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/helpers.test.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/helpers.test.tsx @@ -7,11 +7,11 @@ import { getPromptById } from './helpers'; import { mockSystemPrompt, mockSuperheroSystemPrompt } from '../../mock/system_prompt'; -import type { Prompt } from '../types'; +import { PromptResponse } from '@kbn/elastic-assistant-common'; describe('helpers', () => { describe('getPromptById', () => { - const prompts: Prompt[] = [mockSystemPrompt, mockSuperheroSystemPrompt]; + const prompts: PromptResponse[] = [mockSystemPrompt, mockSuperheroSystemPrompt]; it('returns the correct prompt by id', () => { const result = getPromptById({ prompts, id: mockSuperheroSystemPrompt.id }); diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/helpers.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/helpers.tsx index 7b3e91147635f..f11d2e0af641a 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/helpers.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/helpers.tsx @@ -5,12 +5,12 @@ * 2.0. */ -import type { Prompt } from '../types'; +import { PromptResponse } from '@kbn/elastic-assistant-common'; export const getPromptById = ({ prompts, id, }: { - prompts: Prompt[]; + prompts: PromptResponse[]; id: string; -}): Prompt | undefined => prompts.find((p) => p.id === id); +}): PromptResponse | undefined => prompts.find((p) => p.id === id); diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/index.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/index.test.tsx index 6a18b9794c597..6d421b649a380 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/index.test.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/index.test.tsx @@ -40,6 +40,7 @@ const defaultProps: Props = { setIsSettingsModalVisible: jest.fn(), setSelectedPromptContexts: jest.fn(), isFlyoutMode: false, + allSystemPrompts: [], }; describe('PromptEditorComponent', () => { diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/index.tsx index b2f5c1a3a2e33..1528435764acd 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/index.tsx @@ -10,6 +10,7 @@ import React, { useMemo } from 'react'; // eslint-disable-next-line @kbn/eslint/module_migration import styled from 'styled-components'; +import { PromptResponse } from '@kbn/elastic-assistant-common'; import { Conversation } from '../../..'; import type { PromptContext, SelectedPromptContext } from '../prompt_context/types'; import { SystemPrompt } from './system_prompt'; @@ -31,6 +32,7 @@ export interface Props { React.SetStateAction> >; isFlyoutMode: boolean; + allSystemPrompts: PromptResponse[]; } const PreviewText = styled(EuiText)` @@ -49,12 +51,14 @@ const PromptEditorComponent: React.FC = ({ setIsSettingsModalVisible, setSelectedPromptContexts, isFlyoutMode, + allSystemPrompts, }) => { const commentBody = useMemo( () => ( <> {isNewConversation && ( = ({ ), [ + isNewConversation, + allSystemPrompts, conversation, editingSystemPromptId, - isNewConversation, - isSettingsModalVisible, onSystemPromptSelectionChange, + isSettingsModalVisible, + setIsSettingsModalVisible, + isFlyoutMode, promptContexts, - promptTextPreview, selectedPromptContexts, - setIsSettingsModalVisible, setSelectedPromptContexts, - isFlyoutMode, + promptTextPreview, ] ); diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/helpers.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/helpers.test.tsx index 4a26b22b0b2d3..82b04c60a569c 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/helpers.test.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/helpers.test.tsx @@ -16,13 +16,13 @@ import { getOptions, getOptionFromPrompt } from './helpers'; describe('helpers', () => { describe('getOptionFromPrompt', () => { it('returns an EuiSuperSelectOption with the correct value', () => { - const option = getOptionFromPrompt(mockSystemPrompt); + const option = getOptionFromPrompt({ ...mockSystemPrompt, isFlyoutMode: true }); expect(option.value).toBe(mockSystemPrompt.id); }); it('returns an EuiSuperSelectOption with the correct inputDisplay', () => { - const option = getOptionFromPrompt(mockSystemPrompt); + const option = getOptionFromPrompt({ ...mockSystemPrompt, isFlyoutMode: false }); render(<>{option.inputDisplay}); @@ -30,7 +30,7 @@ describe('helpers', () => { }); it('shows the expected name in the dropdownDisplay', () => { - const option = getOptionFromPrompt(mockSystemPrompt); + const option = getOptionFromPrompt({ ...mockSystemPrompt, isFlyoutMode: true }); render({option.dropdownDisplay}); @@ -38,7 +38,7 @@ describe('helpers', () => { }); it('shows the expected prompt content in the dropdownDisplay', () => { - const option = getOptionFromPrompt(mockSystemPrompt); + const option = getOptionFromPrompt({ ...mockSystemPrompt, isFlyoutMode: true }); render({option.dropdownDisplay}); diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/helpers.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/helpers.tsx index a52ed303d4a63..bd217bb54e9f6 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/helpers.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/helpers.tsx @@ -13,7 +13,7 @@ import styled from 'styled-components'; import { css } from '@emotion/react'; import { isEmpty } from 'lodash/fp'; -import type { Prompt } from '../../types'; +import { PromptResponse } from '@kbn/elastic-assistant-common'; import { EMPTY_PROMPT } from './translations'; const Strong = styled.strong` @@ -26,7 +26,10 @@ export const getOptionFromPrompt = ({ name, showTitles = false, isFlyoutMode, -}: Prompt & { showTitles?: boolean }): EuiSuperSelectOption => ({ +}: PromptResponse & { + showTitles?: boolean; + isFlyoutMode: boolean; +}): EuiSuperSelectOption => ({ value: id, inputDisplay: isFlyoutMode ? ( name @@ -60,13 +63,13 @@ export const getOptionFromPrompt = ({ }); interface GetOptionsProps { - prompts: Prompt[] | undefined; + prompts: PromptResponse[] | undefined; showTitles?: boolean; isFlyoutMode: boolean; } export const getOptions = ({ prompts, showTitles = false, - isFlyoutMode = false, + isFlyoutMode, }: GetOptionsProps): Array> => prompts?.map((p) => getOptionFromPrompt({ ...p, showTitles, isFlyoutMode })) ?? []; diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/index.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/index.test.tsx index e0fed34795dfc..34d40852ba505 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/index.test.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/index.test.tsx @@ -13,11 +13,11 @@ import { mockSystemPrompt } from '../../../mock/system_prompt'; import { SystemPrompt } from '.'; import { Conversation } from '../../../..'; import { DEFAULT_CONVERSATION_TITLE } from '../../use_conversation/translations'; -import { Prompt } from '../../types'; import { TestProviders } from '../../../mock/test_providers/test_providers'; import { TEST_IDS } from '../../constants'; import { useAssistantContext } from '../../../assistant_context'; import { WELCOME_CONVERSATION } from '../../use_conversation/sample_conversations'; +import { PromptResponse } from '@kbn/elastic-assistant-common'; const BASE_CONVERSATION: Conversation = { ...WELCOME_CONVERSATION, @@ -32,7 +32,7 @@ const mockConversations = { [DEFAULT_CONVERSATION_TITLE]: BASE_CONVERSATION, }; -const mockSystemPrompts: Prompt[] = [mockSystemPrompt]; +const mockSystemPrompts: PromptResponse[] = [mockSystemPrompt]; const mockUseAssistantContext = { conversations: mockConversations, @@ -91,6 +91,7 @@ describe('SystemPrompt', () => { onSystemPromptSelectionChange={onSystemPromptSelectionChange} setIsSettingsModalVisible={setIsSettingsModalVisible} isFlyoutMode={false} + allSystemPrompts={mockSystemPrompts} /> ); }); @@ -117,11 +118,12 @@ describe('SystemPrompt', () => { render( ); }); @@ -157,6 +159,7 @@ describe('SystemPrompt', () => { onSystemPromptSelectionChange={onSystemPromptSelectionChange} setIsSettingsModalVisible={setIsSettingsModalVisible} isFlyoutMode={false} + allSystemPrompts={mockSystemPrompts} /> ); @@ -204,6 +207,7 @@ describe('SystemPrompt', () => { onSystemPromptSelectionChange={onSystemPromptSelectionChange} setIsSettingsModalVisible={setIsSettingsModalVisible} isFlyoutMode={false} + allSystemPrompts={mockSystemPrompts} /> ); @@ -265,6 +269,7 @@ describe('SystemPrompt', () => { onSystemPromptSelectionChange={onSystemPromptSelectionChange} setIsSettingsModalVisible={setIsSettingsModalVisible} isFlyoutMode={false} + allSystemPrompts={mockSystemPrompts} /> ); @@ -333,6 +338,7 @@ describe('SystemPrompt', () => { onSystemPromptSelectionChange={onSystemPromptSelectionChange} setIsSettingsModalVisible={setIsSettingsModalVisible} isFlyoutMode={false} + allSystemPrompts={mockSystemPrompts} /> ); @@ -416,6 +422,7 @@ describe('SystemPrompt', () => { onSystemPromptSelectionChange={onSystemPromptSelectionChange} setIsSettingsModalVisible={setIsSettingsModalVisible} isFlyoutMode={false} + allSystemPrompts={mockSystemPrompts} /> ); @@ -481,11 +488,12 @@ describe('SystemPrompt', () => { ); @@ -500,11 +508,12 @@ describe('SystemPrompt', () => { ); diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/index.tsx index d6a507b6deeed..f2808c3e204f1 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/index.tsx @@ -10,7 +10,7 @@ import React, { useCallback, useMemo } from 'react'; import { css } from '@emotion/react'; import { isEmpty } from 'lodash/fp'; -import { useAssistantContext } from '../../../assistant_context'; +import { PromptResponse } from '@kbn/elastic-assistant-common'; import { Conversation } from '../../../..'; import * as i18n from './translations'; import { SelectSystemPrompt } from './select_system_prompt'; @@ -22,6 +22,7 @@ interface Props { onSystemPromptSelectionChange: (systemPromptId: string | undefined) => void; setIsSettingsModalVisible: React.Dispatch>; isFlyoutMode: boolean; + allSystemPrompts: PromptResponse[]; } const SystemPromptComponent: React.FC = ({ @@ -31,17 +32,13 @@ const SystemPromptComponent: React.FC = ({ onSystemPromptSelectionChange, setIsSettingsModalVisible, isFlyoutMode, + allSystemPrompts, }) => { - const { allSystemPrompts } = useAssistantContext(); - const selectedPrompt = useMemo(() => { if (editingSystemPromptId !== undefined) { - return ( - allSystemPrompts?.find((p) => p.id === editingSystemPromptId) ?? - allSystemPrompts?.find((p) => p.id === conversation?.apiConfig?.defaultSystemPromptId) - ); + return allSystemPrompts.find((p) => p.id === editingSystemPromptId); } else { - return undefined; + return allSystemPrompts.find((p) => p.id === conversation?.apiConfig?.defaultSystemPromptId); } }, [allSystemPrompts, conversation?.apiConfig?.defaultSystemPromptId, editingSystemPromptId]); @@ -58,7 +55,7 @@ const SystemPromptComponent: React.FC = ({ if (isFlyoutMode) { return ( = ({
{selectedPrompt == null || isEditing ? ( ); const props: Props = { - allSystemPrompts: [ + allPrompts: [ { id: 'default-system-prompt', content: 'default', @@ -31,6 +54,8 @@ const props: Props = { }; const mockUseAssistantContext = { + http, + assistantAvailability: { isAssistantEnabled: true }, allSystemPrompts: [ { id: 'default-system-prompt', diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/select_system_prompt/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/select_system_prompt/index.tsx index 2cbbdf68b307c..0296fa3e636ca 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/select_system_prompt/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/select_system_prompt/index.tsx @@ -18,10 +18,13 @@ import { import React, { useCallback, useMemo, useState } from 'react'; import { euiThemeVars } from '@kbn/ui-theme'; +import { + PromptResponse, + PromptTypeEnum, +} from '@kbn/elastic-assistant-common/impl/schemas/prompts/bulk_crud_prompts_route.gen'; import { Conversation } from '../../../../..'; import { getOptions } from '../helpers'; import * as i18n from '../translations'; -import type { Prompt } from '../../../types'; import { useAssistantContext } from '../../../../assistant_context'; import { useConversation } from '../../../use_conversation'; import { TEST_IDS } from '../../../constants'; @@ -29,10 +32,10 @@ import { PROMPT_CONTEXT_SELECTOR_PREFIX } from '../../../quick_prompts/prompt_co import { SYSTEM_PROMPTS_TAB } from '../../../settings/const'; export interface Props { - allSystemPrompts: Prompt[]; + allPrompts: PromptResponse[]; compressed?: boolean; conversation?: Conversation; - selectedPrompt: Prompt | undefined; + selectedPrompt: PromptResponse | undefined; clearSelectedSystemPrompt?: () => void; isClearable?: boolean; isEditing?: boolean; @@ -49,7 +52,7 @@ export interface Props { const ADD_NEW_SYSTEM_PROMPT = 'ADD_NEW_SYSTEM_PROMPT'; const SelectSystemPromptComponent: React.FC = ({ - allSystemPrompts, + allPrompts, compressed = false, conversation, selectedPrompt, @@ -68,21 +71,24 @@ const SelectSystemPromptComponent: React.FC = ({ const { setSelectedSettingsTab } = useAssistantContext(); const { setApiConfig } = useConversation(); - const [isOpenLocal, setIsOpenLocal] = useState(isOpen); - const [valueOfSelected, setValueOfSelected] = useState( - selectedPrompt?.id ?? allSystemPrompts?.[0]?.id + const allSystemPrompts = useMemo( + () => allPrompts.filter((p) => p.promptType === PromptTypeEnum.system), + [allPrompts] ); + + const [isOpenLocal, setIsOpenLocal] = useState(isOpen); const handleOnBlur = useCallback(() => setIsOpenLocal(false), []); + const valueOfSelected = useMemo(() => selectedPrompt?.id, [selectedPrompt?.id]); // Write the selected system prompt to the conversation config const setSelectedSystemPrompt = useCallback( - (prompt: Prompt | undefined) => { + (promptId?: string) => { if (conversation && conversation.apiConfig) { setApiConfig({ conversation, apiConfig: { ...conversation.apiConfig, - defaultSystemPromptId: prompt?.id, + defaultSystemPromptId: promptId, }, }); } @@ -126,14 +132,11 @@ const SelectSystemPromptComponent: React.FC = ({ // Note: if callback is provided, this component does not persist. Extract to separate component if (onSystemPromptSelectionChange != null) { onSystemPromptSelectionChange(selectedSystemPromptId); - } else { - setSelectedSystemPrompt(allSystemPrompts.find((sp) => sp.id === selectedSystemPromptId)); } - setValueOfSelected(selectedSystemPromptId); + setSelectedSystemPrompt(selectedSystemPromptId); setIsEditing?.(false); }, [ - allSystemPrompts, onSystemPromptSelectionChange, setIsEditing, setIsSettingsModalVisible, @@ -146,7 +149,6 @@ const SelectSystemPromptComponent: React.FC = ({ setSelectedSystemPrompt(undefined); setIsEditing?.(false); clearSelectedSystemPrompt?.(); - setValueOfSelected(undefined); }, [clearSelectedSystemPrompt, setIsEditing, setSelectedSystemPrompt]); const onShowSelectSystemPrompt = useCallback(() => { diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_modal/system_prompt_editor.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_modal/system_prompt_editor.tsx index 3fd7dfeb00e73..fecb2ed401a4b 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_modal/system_prompt_editor.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_modal/system_prompt_editor.tsx @@ -18,9 +18,13 @@ import { import { keyBy } from 'lodash/fp'; import { css } from '@emotion/react'; +import { + PromptResponse, + PerformBulkActionRequestBody as PromptsPerformBulkActionRequestBody, +} from '@kbn/elastic-assistant-common/impl/schemas/prompts/bulk_crud_prompts_route.gen'; import { ApiConfig } from '@kbn/elastic-assistant-common'; import { AIConnector } from '../../../../connectorland/connector_selector'; -import { Conversation, Prompt } from '../../../../..'; +import { Conversation } from '../../../../..'; import * as i18n from './translations'; import { ConversationMultiSelector } from './conversation_multi_selector/conversation_multi_selector'; import { SystemPromptSelector } from './system_prompt_selector/system_prompt_selector'; @@ -34,16 +38,18 @@ interface Props { connectors: AIConnector[] | undefined; conversationSettings: Record; conversationsSettingsBulkActions: ConversationsBulkActions; - onSelectedSystemPromptChange: (systemPrompt?: Prompt) => void; - selectedSystemPrompt: Prompt | undefined; - setUpdatedSystemPromptSettings: React.Dispatch>; + onSelectedSystemPromptChange: (systemPrompt?: PromptResponse) => void; + selectedSystemPrompt: PromptResponse | undefined; + setUpdatedSystemPromptSettings: React.Dispatch>; setConversationSettings: React.Dispatch>>; - systemPromptSettings: Prompt[]; + systemPromptSettings: PromptResponse[]; setConversationsSettingsBulkActions: React.Dispatch< React.SetStateAction >; defaultConnector?: AIConnector; resetSettings?: () => void; + promptsBulkActions: PromptsPerformBulkActionRequestBody; + setPromptsBulkActions: React.Dispatch>; } /** @@ -61,6 +67,8 @@ export const SystemPromptEditorComponent: React.FC = ({ setConversationsSettingsBulkActions, defaultConnector, resetSettings, + promptsBulkActions, + setPromptsBulkActions, }) => { // Prompt const promptContent = useMemo( @@ -72,11 +80,11 @@ export const SystemPromptEditorComponent: React.FC = ({ const handlePromptContentChange = useCallback( (e: React.ChangeEvent) => { if (selectedSystemPrompt != null) { - setUpdatedSystemPromptSettings((prev): Prompt[] => { + setUpdatedSystemPromptSettings((prev): PromptResponse[] => { const alreadyExists = prev.some((sp) => sp.id === selectedSystemPrompt.id); if (alreadyExists) { - return prev.map((sp): Prompt => { + return prev.map((sp): PromptResponse => { if (sp.id === selectedSystemPrompt.id) { return { ...sp, @@ -89,9 +97,44 @@ export const SystemPromptEditorComponent: React.FC = ({ return prev; }); + const existingPrompt = systemPromptSettings.find((sp) => sp.id === selectedSystemPrompt.id); + if (existingPrompt) { + setPromptsBulkActions({ + ...promptsBulkActions, + ...(selectedSystemPrompt.name !== selectedSystemPrompt.id + ? { + update: [ + ...(promptsBulkActions.update ?? []).filter( + (p) => p.id !== selectedSystemPrompt.id + ), + { + ...selectedSystemPrompt, + content: e.target.value, + }, + ], + } + : { + create: [ + ...(promptsBulkActions.create ?? []).filter( + (p) => p.name !== selectedSystemPrompt.name + ), + { + ...selectedSystemPrompt, + content: e.target.value, + }, + ], + }), + }); + } } }, - [selectedSystemPrompt, setUpdatedSystemPromptSettings] + [ + promptsBulkActions, + selectedSystemPrompt, + setPromptsBulkActions, + setUpdatedSystemPromptSettings, + systemPromptSettings, + ] ); const conversationsWithApiConfig = Object.entries(conversationSettings).reduce< @@ -258,14 +301,47 @@ export const SystemPromptEditorComponent: React.FC = ({ }; }); }); + setPromptsBulkActions({ + ...promptsBulkActions, + ...(selectedSystemPrompt.name !== selectedSystemPrompt.id + ? { + update: [ + ...(promptsBulkActions.update ?? []).filter( + (p) => p.id !== selectedSystemPrompt.id + ), + { + ...selectedSystemPrompt, + isNewConversationDefault: isChecked, + }, + ], + } + : { + create: [ + ...(promptsBulkActions.create ?? []).filter( + (p) => p.name !== selectedSystemPrompt.name + ), + { + ...selectedSystemPrompt, + isNewConversationDefault: isChecked, + }, + ], + }), + }); } }, - [selectedSystemPrompt, setUpdatedSystemPromptSettings] + [ + promptsBulkActions, + selectedSystemPrompt, + setPromptsBulkActions, + setUpdatedSystemPromptSettings, + ] ); const { onSystemPromptSelectionChange, onSystemPromptDeleted } = useSystemPromptEditor({ setUpdatedSystemPromptSettings, onSelectedSystemPromptChange, + promptsBulkActions, + setPromptsBulkActions, }); return ( diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_modal/system_prompt_selector/system_prompt_selector.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_modal/system_prompt_selector/system_prompt_selector.test.tsx index 45f320528ec64..cbf5efe79213f 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_modal/system_prompt_selector/system_prompt_selector.test.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_modal/system_prompt_selector/system_prompt_selector.test.tsx @@ -35,7 +35,7 @@ describe('SystemPromptSelector', () => { fireEvent.click(getByTestId('comboBoxToggleListButton')); // there is only one delete system prompt because there is only one custom option fireEvent.click(getAllByTestId('delete-prompt')[1]); - expect(onSystemPromptDeleted).toHaveBeenCalledWith(mockSystemPrompts[1].name); + expect(onSystemPromptDeleted).toHaveBeenCalledWith(mockSystemPrompts[1].id); expect(onSystemPromptSelectionChange).not.toHaveBeenCalled(); }); it('Deletes a system prompt that is selected', () => { @@ -43,7 +43,7 @@ describe('SystemPromptSelector', () => { fireEvent.click(getByTestId('comboBoxToggleListButton')); // there is only one delete system prompt because there is only one custom option fireEvent.click(getAllByTestId('delete-prompt')[0]); - expect(onSystemPromptDeleted).toHaveBeenCalledWith(mockSystemPrompts[0].name); + expect(onSystemPromptDeleted).toHaveBeenCalledWith(mockSystemPrompts[0].id); expect(onSystemPromptSelectionChange).toHaveBeenCalledWith(undefined); }); it('Selects existing system prompt from the search input', () => { diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_modal/system_prompt_selector/system_prompt_selector.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_modal/system_prompt_selector/system_prompt_selector.tsx index 53b6414d05b53..2c4826940a7ca 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_modal/system_prompt_selector/system_prompt_selector.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_modal/system_prompt_selector/system_prompt_selector.tsx @@ -18,8 +18,8 @@ import { } from '@elastic/eui'; import { css } from '@emotion/react'; +import { PromptResponse } from '@kbn/elastic-assistant-common'; import { TEST_IDS } from '../../../../constants'; -import { Prompt } from '../../../../../..'; import * as i18n from './translations'; import { SYSTEM_PROMPT_DEFAULT_NEW_CONVERSATION } from '../translations'; @@ -28,10 +28,10 @@ export const SYSTEM_PROMPT_SELECTOR_CLASSNAME = 'systemPromptSelector'; interface Props { autoFocus?: boolean; onSystemPromptDeleted: (systemPromptTitle: string) => void; - onSystemPromptSelectionChange: (systemPrompt?: Prompt | string) => void; + onSystemPromptSelectionChange: (systemPrompt?: PromptResponse | string) => void; + systemPrompts: PromptResponse[]; + selectedSystemPrompt?: PromptResponse; resetSettings?: () => void; - selectedSystemPrompt?: Prompt; - systemPrompts: Prompt[]; } export type SystemPromptSelectorOption = EuiComboBoxOptionOption<{ @@ -59,6 +59,7 @@ export const SystemPromptSelector: React.FC = React.memo( isNewConversationDefault: sp.isNewConversationDefault ?? false, }, label: sp.name, + id: sp.id, 'data-test-subj': `${TEST_IDS.SYSTEM_PROMPT_SELECTOR}-${sp.id}`, })) ); @@ -70,6 +71,7 @@ export const SystemPromptSelector: React.FC = React.memo( isDefault: selectedSystemPrompt.isDefault ?? false, isNewConversationDefault: selectedSystemPrompt.isNewConversationDefault ?? false, }, + id: selectedSystemPrompt.id, label: selectedSystemPrompt.name, }, ] @@ -106,6 +108,7 @@ export const SystemPromptSelector: React.FC = React.memo( const newOption = { value: searchValue, + id: searchValue, label: searchValue, }; @@ -132,11 +135,12 @@ export const SystemPromptSelector: React.FC = React.memo( // Callback for when user deletes a quick prompt const onDelete = useCallback( (label: string) => { + const deleteId = options.find((o) => o.label === label)?.id; setOptions(options.filter((o) => o.label !== label)); if (selectedOptions?.[0]?.label === label) { handleSelectionChange([]); } - onSystemPromptDeleted(label); + onSystemPromptDeleted(deleteId ?? label); }, [handleSelectionChange, onSystemPromptDeleted, options, selectedOptions] ); diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_modal/system_prompt_settings.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_modal/system_prompt_settings.test.tsx index be9e33f615e4b..5116da2a56207 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_modal/system_prompt_settings.test.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_modal/system_prompt_settings.test.tsx @@ -36,6 +36,8 @@ const testProps = { systemPromptSettings: mockSystemPrompts, conversationsSettingsBulkActions: {}, setConversationsSettingsBulkActions: jest.fn(), + promptsBulkActions: {}, + setPromptsBulkActions: jest.fn(), }; jest.mock('./system_prompt_selector/system_prompt_selector', () => ({ @@ -96,6 +98,7 @@ describe('SystemPromptSettings', () => { ); fireEvent.click(getByTestId('change-sp-custom')); const customOption = { + consumer: 'test', content: '', id: 'sooper custom prompt', name: 'sooper custom prompt', diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_modal/system_prompt_settings.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_modal/system_prompt_settings.tsx index b7f66acba85c7..7b8e451449884 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_modal/system_prompt_settings.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_modal/system_prompt_settings.tsx @@ -26,7 +26,9 @@ export const SystemPromptSettings: React.FC = React.m systemPromptSettings, conversationsSettingsBulkActions, setConversationsSettingsBulkActions, + promptsBulkActions, defaultConnector, + setPromptsBulkActions, }) => { return ( <> @@ -48,6 +50,8 @@ export const SystemPromptSettings: React.FC = React.m conversationsSettingsBulkActions={conversationsSettingsBulkActions} setConversationsSettingsBulkActions={setConversationsSettingsBulkActions} defaultConnector={defaultConnector} + setPromptsBulkActions={setPromptsBulkActions} + promptsBulkActions={promptsBulkActions} /> ); diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_modal/types.ts b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_modal/types.ts index 63025566c9400..e92961cb1763f 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_modal/types.ts +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_modal/types.ts @@ -4,21 +4,27 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { + PromptResponse, + PerformBulkActionRequestBody as PromptsPerformBulkActionRequestBody, +} from '@kbn/elastic-assistant-common/impl/schemas/prompts/bulk_crud_prompts_route.gen'; import { AIConnector } from '../../../../connectorland/connector_selector'; -import { Conversation, Prompt } from '../../../../..'; +import { Conversation } from '../../../../..'; import { ConversationsBulkActions } from '../../../api'; export interface SystemPromptSettingsProps { connectors: AIConnector[] | undefined; conversationSettings: Record; conversationsSettingsBulkActions: ConversationsBulkActions; - onSelectedSystemPromptChange: (systemPrompt?: Prompt) => void; - selectedSystemPrompt: Prompt | undefined; - setUpdatedSystemPromptSettings: React.Dispatch>; + onSelectedSystemPromptChange: (systemPrompt?: PromptResponse) => void; + selectedSystemPrompt: PromptResponse | undefined; + setUpdatedSystemPromptSettings: React.Dispatch>; setConversationSettings: React.Dispatch>>; - systemPromptSettings: Prompt[]; + systemPromptSettings: PromptResponse[]; setConversationsSettingsBulkActions: React.Dispatch< React.SetStateAction >; defaultConnector?: AIConnector; + promptsBulkActions: PromptsPerformBulkActionRequestBody; + setPromptsBulkActions: React.Dispatch>; } diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_modal/use_system_prompt_editor.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_modal/use_system_prompt_editor.test.tsx index 85efe99979650..009ee6c5a83cd 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_modal/use_system_prompt_editor.test.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_modal/use_system_prompt_editor.test.tsx @@ -6,21 +6,27 @@ */ import { renderHook, act } from '@testing-library/react-hooks'; import { useSystemPromptEditor } from './use_system_prompt_editor'; -import { Prompt } from '../../../types'; import { mockSystemPrompt, mockSuperheroSystemPrompt, mockSystemPrompts, } from '../../../../mock/system_prompt'; +import { PromptResponse } from '@kbn/elastic-assistant-common'; +import { useAssistantContext } from '../../../../assistant_context'; +jest.mock('../../../../assistant_context'); // Mock functions for the tests const mockOnSelectedSystemPromptChange = jest.fn(); const mockSetUpdatedSystemPromptSettings = jest.fn(); +const mockSetPromptsBulkActions = jest.fn(); const mockPreviousSystemPrompts = [...mockSystemPrompts]; describe('useSystemPromptEditor', () => { beforeEach(() => { jest.clearAllMocks(); + (useAssistantContext as jest.Mock).mockReturnValue({ + currentAppId: 'securitySolutionUI', + }); }); test('should delete a system prompt by id', () => { @@ -28,6 +34,8 @@ describe('useSystemPromptEditor', () => { useSystemPromptEditor({ onSelectedSystemPromptChange: mockOnSelectedSystemPromptChange, setUpdatedSystemPromptSettings: mockSetUpdatedSystemPromptSettings, + setPromptsBulkActions: mockSetPromptsBulkActions, + promptsBulkActions: {}, }) ); @@ -41,11 +49,13 @@ describe('useSystemPromptEditor', () => { }); test('should handle selection of an existing system prompt', () => { - const existingPrompt: Prompt = mockSystemPrompt; + const existingPrompt: PromptResponse = mockSystemPrompt; const { result } = renderHook(() => useSystemPromptEditor({ onSelectedSystemPromptChange: mockOnSelectedSystemPromptChange, setUpdatedSystemPromptSettings: mockSetUpdatedSystemPromptSettings, + setPromptsBulkActions: mockSetPromptsBulkActions, + promptsBulkActions: {}, }) ); @@ -65,6 +75,8 @@ describe('useSystemPromptEditor', () => { useSystemPromptEditor({ onSelectedSystemPromptChange: mockOnSelectedSystemPromptChange, setUpdatedSystemPromptSettings: mockSetUpdatedSystemPromptSettings, + setPromptsBulkActions: mockSetPromptsBulkActions, + promptsBulkActions: {}, }) ); @@ -72,11 +84,12 @@ describe('useSystemPromptEditor', () => { result.current.onSystemPromptSelectionChange(newPromptId); }); - const newPrompt: Prompt = { + const newPrompt: PromptResponse = { id: newPromptId, content: '', name: newPromptId, promptType: 'system', + consumer: 'securitySolutionUI', }; expect(mockOnSelectedSystemPromptChange).toHaveBeenCalledWith(newPrompt); @@ -90,10 +103,12 @@ describe('useSystemPromptEditor', () => { useSystemPromptEditor({ onSelectedSystemPromptChange: mockOnSelectedSystemPromptChange, setUpdatedSystemPromptSettings: mockSetUpdatedSystemPromptSettings, + setPromptsBulkActions: mockSetPromptsBulkActions, + promptsBulkActions: {}, }) ); - const expectedPrompt: Prompt = mockSuperheroSystemPrompt; + const expectedPrompt: PromptResponse = mockSuperheroSystemPrompt; act(() => { result.current.onSystemPromptSelectionChange(expectedPrompt); diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_modal/use_system_prompt_editor.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_modal/use_system_prompt_editor.tsx index 87e284d6dcf25..ec77de113b5d9 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_modal/use_system_prompt_editor.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_modal/use_system_prompt_editor.tsx @@ -5,28 +5,38 @@ * 2.0. */ +import { + PromptResponse, + PerformBulkActionRequestBody as PromptsPerformBulkActionRequestBody, +} from '@kbn/elastic-assistant-common/impl/schemas/prompts/bulk_crud_prompts_route.gen'; import { useCallback } from 'react'; -import { Prompt } from '../../../types'; +import { useAssistantContext } from '../../../../..'; interface Props { - setUpdatedSystemPromptSettings: React.Dispatch>; - onSelectedSystemPromptChange: (systemPrompt?: Prompt) => void; + setUpdatedSystemPromptSettings: React.Dispatch>; + onSelectedSystemPromptChange: (systemPrompt?: PromptResponse) => void; + promptsBulkActions: PromptsPerformBulkActionRequestBody; + setPromptsBulkActions: React.Dispatch>; } export const useSystemPromptEditor = ({ setUpdatedSystemPromptSettings, onSelectedSystemPromptChange, + promptsBulkActions, + setPromptsBulkActions, }: Props) => { + const { currentAppId } = useAssistantContext(); // When top level system prompt selection changes const onSystemPromptSelectionChange = useCallback( - (systemPrompt?: Prompt | string) => { + (systemPrompt?: PromptResponse | string) => { const isNew = typeof systemPrompt === 'string'; - const newSelectedSystemPrompt: Prompt | undefined = isNew + const newSelectedSystemPrompt: PromptResponse | undefined = isNew ? { id: systemPrompt ?? '', content: '', name: systemPrompt ?? '', promptType: 'system', + consumer: currentAppId, } : systemPrompt; @@ -40,18 +50,42 @@ export const useSystemPromptEditor = ({ return prev; }); + + if (isNew) { + setPromptsBulkActions({ + ...promptsBulkActions, + create: [ + ...(promptsBulkActions.create ?? []), + { + ...newSelectedSystemPrompt, + }, + ], + }); + } } onSelectedSystemPromptChange(newSelectedSystemPrompt); }, - [onSelectedSystemPromptChange, setUpdatedSystemPromptSettings] + [ + currentAppId, + onSelectedSystemPromptChange, + promptsBulkActions, + setPromptsBulkActions, + setUpdatedSystemPromptSettings, + ] ); const onSystemPromptDeleted = useCallback( (id: string) => { setUpdatedSystemPromptSettings((prev) => prev.filter((sp) => sp.id !== id)); + setPromptsBulkActions({ + ...promptsBulkActions, + delete: { + ids: [...(promptsBulkActions.delete?.ids ?? []), id], + }, + }); }, - [setUpdatedSystemPromptSettings] + [promptsBulkActions, setPromptsBulkActions, setUpdatedSystemPromptSettings] ); return { onSystemPromptSelectionChange, onSystemPromptDeleted }; diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_settings_management/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_settings_management/index.tsx index e0f27f3fa8c7d..14b6ecb868ead 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_settings_management/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_settings_management/index.tsx @@ -16,6 +16,10 @@ import { } from '@elastic/eui'; import React, { useCallback, useMemo, useState } from 'react'; +import { + PromptResponse, + PerformBulkActionRequestBody as PromptsPerformBulkActionRequestBody, +} from '@kbn/elastic-assistant-common/impl/schemas/prompts/bulk_crud_prompts_route.gen'; import { Conversation, ConversationsBulkActions, useAssistantContext } from '../../../../..'; import { SYSTEM_PROMPT_TABLE_SESSION_STORAGE_KEY } from '../../../../assistant_context/constants'; import { AIConnector } from '../../../../connectorland/connector_selector'; @@ -26,7 +30,6 @@ import { useSessionPagination, } from '../../../common/components/assistant_settings_management/pagination/use_session_pagination'; import { CANCEL, DELETE } from '../../../settings/translations'; -import { Prompt } from '../../../types'; import { SystemPromptEditor } from '../system_prompt_modal/system_prompt_editor'; import { SETTINGS_TITLE } from '../system_prompt_modal/translations'; import { useSystemPromptEditor } from '../system_prompt_modal/use_system_prompt_editor'; @@ -37,11 +40,11 @@ interface Props { connectors: AIConnector[] | undefined; conversationSettings: Record; conversationsSettingsBulkActions: ConversationsBulkActions; - onSelectedSystemPromptChange: (systemPrompt?: Prompt) => void; - selectedSystemPrompt: Prompt | undefined; - setUpdatedSystemPromptSettings: React.Dispatch>; + onSelectedSystemPromptChange: (systemPrompt?: PromptResponse) => void; + selectedSystemPrompt: PromptResponse | undefined; + setUpdatedSystemPromptSettings: React.Dispatch>; setConversationSettings: React.Dispatch>>; - systemPromptSettings: Prompt[]; + systemPromptSettings: PromptResponse[]; setConversationsSettingsBulkActions: React.Dispatch< React.SetStateAction >; @@ -49,6 +52,8 @@ interface Props { handleSave: (shouldRefetchConversation?: boolean) => void; onCancelClick: () => void; resetSettings: () => void; + promptsBulkActions: PromptsPerformBulkActionRequestBody; + setPromptsBulkActions: React.Dispatch>; } const SystemPromptSettingsManagementComponent = ({ @@ -65,6 +70,8 @@ const SystemPromptSettingsManagementComponent = ({ handleSave, onCancelClick, resetSettings, + promptsBulkActions, + setPromptsBulkActions, }: Props) => { const { nameSpace } = useAssistantContext(); const { isFlyoutOpen: editFlyoutVisible, openFlyout, closeFlyout } = useFlyoutModalVisibility(); @@ -73,7 +80,7 @@ const SystemPromptSettingsManagementComponent = ({ openFlyout: openConfirmModal, closeFlyout: closeConfirmModal, } = useFlyoutModalVisibility(); - const [deletedPrompt, setDeletedPrompt] = useState(); + const [deletedPrompt, setDeletedPrompt] = useState(); const onCreate = useCallback(() => { onSelectedSystemPromptChange({ @@ -88,10 +95,12 @@ const SystemPromptSettingsManagementComponent = ({ const { onSystemPromptSelectionChange, onSystemPromptDeleted } = useSystemPromptEditor({ setUpdatedSystemPromptSettings, onSelectedSystemPromptChange, + promptsBulkActions, + setPromptsBulkActions, }); const onEditActionClicked = useCallback( - (prompt: Prompt) => { + (prompt: PromptResponse) => { onSystemPromptSelectionChange(prompt); openFlyout(); }, @@ -99,7 +108,7 @@ const SystemPromptSettingsManagementComponent = ({ ); const onDeleteActionClicked = useCallback( - (prompt: Prompt) => { + (prompt: PromptResponse) => { setDeletedPrompt(prompt); onSystemPromptDeleted(prompt.id); openConfirmModal(); @@ -200,6 +209,8 @@ const SystemPromptSettingsManagementComponent = ({ setConversationsSettingsBulkActions={setConversationsSettingsBulkActions} defaultConnector={defaultConnector} resetSettings={resetSettings} + promptsBulkActions={promptsBulkActions} + setPromptsBulkActions={setPromptsBulkActions} /> {deleteConfirmModalVisibility && deletedPrompt?.name && ( diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_settings_management/use_system_prompt_table.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_settings_management/use_system_prompt_table.test.tsx index 90cea2319714d..48d3232f0ae38 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_settings_management/use_system_prompt_table.test.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_settings_management/use_system_prompt_table.test.tsx @@ -7,26 +7,25 @@ import { renderHook } from '@testing-library/react-hooks'; import { useSystemPromptTable } from './use_system_prompt_table'; -import { Prompt } from '../../../types'; import { Conversation } from '../../../../assistant_context/types'; import { AIConnector } from '../../../../connectorland/connector_selector'; import { customConvo, welcomeConvo } from '../../../../mock/conversation'; import { mockConnectors } from '../../../../mock/connectors'; -import { ApiConfig } from '@kbn/elastic-assistant-common'; +import { ApiConfig, PromptResponse } from '@kbn/elastic-assistant-common'; // Mock data for tests -const mockSystemPrompts: Prompt[] = [ +const mockSystemPrompts: PromptResponse[] = [ { id: 'prompt-1', content: 'Prompt 1', name: 'Prompt 1', - promptType: 'user', + promptType: 'quick', }, { id: 'prompt-2', content: 'Prompt 2', name: 'Prompt 2', - promptType: 'user', + promptType: 'quick', isNewConversationDefault: true, }, ]; diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_settings_management/use_system_prompt_table.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_settings_management/use_system_prompt_table.tsx index 7cf907bb7adf5..46e082b86f2c0 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_settings_management/use_system_prompt_table.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_settings_management/use_system_prompt_table.tsx @@ -6,11 +6,11 @@ */ import { EuiBasicTableColumn, EuiIcon, EuiLink } from '@elastic/eui'; import React, { useCallback } from 'react'; +import { PromptResponse } from '@kbn/elastic-assistant-common'; import { Conversation } from '../../../../assistant_context/types'; import { AIConnector } from '../../../../connectorland/connector_selector'; import { BadgesColumn } from '../../../common/components/assistant_settings_management/badges'; import { RowActions } from '../../../common/components/assistant_settings_management/row_actions'; -import { Prompt } from '../../../types'; import { getConversationApiConfig, getInitialDefaultSystemPrompt, @@ -21,10 +21,10 @@ import { getSelectedConversations } from './utils'; type ConversationsWithSystemPrompt = Record< string, - Conversation & { systemPrompt: Prompt | undefined } + Conversation & { systemPrompt: PromptResponse | undefined } >; -type SystemPromptTableItem = Prompt & { defaultConversations: string[] }; +type SystemPromptTableItem = PromptResponse & { defaultConversations: string[] }; export const useSystemPromptTable = () => { const getColumns = useCallback( @@ -97,7 +97,7 @@ export const useSystemPromptTable = () => { connectors: AIConnector[] | undefined; conversationSettings: Record; defaultConnector: AIConnector | undefined; - systemPromptSettings: Prompt[]; + systemPromptSettings: PromptResponse[]; }): SystemPromptTableItem[] => { const conversationsWithApiConfig = Object.entries( conversationSettings diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_settings_management/utils.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_settings_management/utils.test.tsx index 5f10e3bb59c65..9fbfb3a8782e0 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_settings_management/utils.test.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_settings_management/utils.test.tsx @@ -6,8 +6,8 @@ */ import { ProviderEnum } from '@kbn/elastic-assistant-common'; import { mockSystemPrompts } from '../../../../mock/system_prompt'; -import { PromptType } from '../../../types'; import { getSelectedConversations } from './utils'; +import { PromptTypeEnum } from '@kbn/elastic-assistant-common/impl/schemas/prompts/bulk_crud_prompts_route.gen'; describe('getSelectedConversations', () => { const allSystemPrompts = [...mockSystemPrompts]; const conversationSettings = { @@ -39,7 +39,7 @@ describe('getSelectedConversations', () => { content: 'You are a helpful, expert assistant who answers questions about Elastic Security. Do not answer questions unrelated to Elastic Security.\nProvide the most detailed and relevant answer possible, as if you were relaying this information back to a cyber security expert.\nIf you answer a question related to KQL, EQL, or ES|QL, it should be immediately usable within an Elastic Security timeline; please always format the output correctly with back ticks. Any answer provided for Query DSL should also be usable in a security timeline. This means you should only ever include the "filter" portion of the query. xxx', name: 'Enhanced system prompt', - promptType: 'system' as PromptType, + promptType: PromptTypeEnum.system, isDefault: true, isNewConversationDefault: true, }, diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_settings_management/utils.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_settings_management/utils.tsx index 5fde200db9b17..fd01b8eb318a6 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_settings_management/utils.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/system_prompt_settings_management/utils.tsx @@ -5,11 +5,11 @@ * 2.0. */ +import { PromptResponse } from '@kbn/elastic-assistant-common'; import { Conversation } from '../../../../assistant_context/types'; -import { Prompt } from '../../../types'; export const getSelectedConversations = ( - allSystemPrompts: Prompt[], + allSystemPrompts: PromptResponse[], conversationSettings: Record, systemPromptId: string ) => { diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_selector/quick_prompt_selector.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_selector/quick_prompt_selector.test.tsx index 04ccd478e3bc5..941b442ce4d48 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_selector/quick_prompt_selector.test.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_selector/quick_prompt_selector.test.tsx @@ -25,9 +25,9 @@ describe('QuickPromptSelector', () => { }); it('Selects an existing quick prompt', () => { const { getByTestId } = render(); - expect(getByTestId('euiComboBoxPill')).toHaveTextContent(MOCK_QUICK_PROMPTS[0].title); + expect(getByTestId('euiComboBoxPill')).toHaveTextContent(MOCK_QUICK_PROMPTS[0].name); fireEvent.click(getByTestId('comboBoxToggleListButton')); - fireEvent.click(getByTestId(MOCK_QUICK_PROMPTS[1].title)); + fireEvent.click(getByTestId(MOCK_QUICK_PROMPTS[1].name)); expect(onQuickPromptSelectionChange).toHaveBeenCalledWith(MOCK_QUICK_PROMPTS[1]); }); it('Only custom option can be deleted', () => { @@ -49,8 +49,10 @@ describe('QuickPromptSelector', () => { expect(onQuickPromptSelectionChange).toHaveBeenCalledWith({ categories: [], color: '#D36086', - prompt: 'quickly prompt please', - title: 'A_CUSTOM_OPTION', + content: 'quickly prompt please', + id: 'A_CUSTOM_OPTION', + name: 'A_CUSTOM_OPTION', + promptType: 'quick', }); }); it('Reset settings every time before selecting an system prompt from the input if resetSettings is provided', () => { @@ -60,7 +62,7 @@ describe('QuickPromptSelector', () => { ); // changing the selection fireEvent.change(getByTestId('comboBoxSearchInput'), { - target: { value: MOCK_QUICK_PROMPTS[1].title }, + target: { value: MOCK_QUICK_PROMPTS[1].name }, }); fireEvent.keyDown(getByTestId('comboBoxSearchInput'), { key: 'Enter', diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_selector/quick_prompt_selector.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_selector/quick_prompt_selector.tsx index 3fb0ba17cf4bf..d29887e8c4f6a 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_selector/quick_prompt_selector.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_selector/quick_prompt_selector.tsx @@ -18,16 +18,16 @@ import { } from '@elastic/eui'; import { css } from '@emotion/react'; +import { PromptResponse } from '@kbn/elastic-assistant-common'; import * as i18n from './translations'; -import { QuickPrompt } from '../types'; interface Props { isDisabled?: boolean; onQuickPromptDeleted: (quickPromptTitle: string) => void; - onQuickPromptSelectionChange: (quickPrompt?: QuickPrompt | string) => void; - quickPrompts: QuickPrompt[]; + onQuickPromptSelectionChange: (quickPrompt?: PromptResponse | string) => void; + quickPrompts: PromptResponse[]; + selectedQuickPrompt?: PromptResponse; resetSettings?: () => void; - selectedQuickPrompt?: QuickPrompt; } export type QuickPromptSelectorOption = EuiComboBoxOptionOption<{ isDefault: boolean }>; @@ -50,8 +50,9 @@ export const QuickPromptSelector: React.FC = React.memo( value: { isDefault: qp.isDefault ?? false, }, - label: qp.title, - 'data-test-subj': qp.title, + label: qp.name, + 'data-test-subj': qp.name, + id: qp.id, color: qp.color, })) ); @@ -62,7 +63,8 @@ export const QuickPromptSelector: React.FC = React.memo( value: { isDefault: true, }, - label: selectedQuickPrompt.title, + label: selectedQuickPrompt.name, + id: selectedQuickPrompt.id, color: selectedQuickPrompt.color, }, ] @@ -76,7 +78,7 @@ export const QuickPromptSelector: React.FC = React.memo( const newQuickPrompt = quickPromptSelectorOption.length === 0 ? undefined - : quickPrompts.find((qp) => qp.title === quickPromptSelectorOption[0]?.label) ?? + : quickPrompts.find((qp) => qp.name === quickPromptSelectorOption[0]?.label) ?? quickPromptSelectorOption[0]?.label; onQuickPromptSelectionChange(newQuickPrompt); }, @@ -100,6 +102,7 @@ export const QuickPromptSelector: React.FC = React.memo( const newOption = { value: searchValue, label: searchValue, + id: searchValue, }; if (!optionExists) { @@ -125,11 +128,12 @@ export const QuickPromptSelector: React.FC = React.memo( // Callback for when user deletes a quick prompt const onDelete = useCallback( (label: string) => { + const deleteId = options.find((o) => o.label === label)?.id; setOptions(options.filter((o) => o.label !== label)); if (selectedOptions?.[0]?.label === label) { handleSelectionChange([]); } - onQuickPromptDeleted(label); + onQuickPromptDeleted(deleteId ?? label); }, [handleSelectionChange, onQuickPromptDeleted, options, selectedOptions] ); diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings/quick_prompt_editor.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings/quick_prompt_editor.tsx index 4300e53525b33..01ffe00d11100 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings/quick_prompt_editor.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings/quick_prompt_editor.tsx @@ -10,9 +10,12 @@ import { EuiFormRow, EuiColorPicker, EuiTextArea } from '@elastic/eui'; import { EuiSetColorMethod } from '@elastic/eui/src/services/color_picker/color_picker'; import { css } from '@emotion/react'; +import { + PromptResponse, + PerformBulkActionRequestBody as PromptsPerformBulkActionRequestBody, +} from '@kbn/elastic-assistant-common/impl/schemas/prompts/bulk_crud_prompts_route.gen'; import { PromptContextTemplate } from '../../../..'; import * as i18n from './translations'; -import { QuickPrompt } from '../types'; import { QuickPromptSelector } from '../quick_prompt_selector/quick_prompt_selector'; import { PromptContextSelector } from '../prompt_context_selector/prompt_context_selector'; import { useAssistantContext } from '../../../assistant_context'; @@ -21,11 +24,13 @@ import { useQuickPromptEditor } from './use_quick_prompt_editor'; const DEFAULT_COLOR = '#D36086'; interface Props { - onSelectedQuickPromptChange: (quickPrompt?: QuickPrompt) => void; - quickPromptSettings: QuickPrompt[]; + onSelectedQuickPromptChange: (quickPrompt?: PromptResponse) => void; + quickPromptSettings: PromptResponse[]; resetSettings?: () => void; - selectedQuickPrompt: QuickPrompt | undefined; - setUpdatedQuickPromptSettings: React.Dispatch>; + selectedQuickPrompt: PromptResponse | undefined; + setUpdatedQuickPromptSettings: React.Dispatch>; + promptsBulkActions: PromptsPerformBulkActionRequestBody; + setPromptsBulkActions: React.Dispatch>; } const QuickPromptSettingsEditorComponent = ({ @@ -34,28 +39,30 @@ const QuickPromptSettingsEditorComponent = ({ resetSettings, selectedQuickPrompt, setUpdatedQuickPromptSettings, + promptsBulkActions, + setPromptsBulkActions, }: Props) => { const { basePromptContexts } = useAssistantContext(); // Prompt - const prompt = useMemo( + const promptContent = useMemo( // Fixing Cursor Jump in text area - () => quickPromptSettings.find((p) => p.title === selectedQuickPrompt?.title)?.prompt ?? '', - [selectedQuickPrompt?.title, quickPromptSettings] + () => quickPromptSettings.find((p) => p.id === selectedQuickPrompt?.id)?.content ?? '', + [selectedQuickPrompt?.id, quickPromptSettings] ); const handlePromptChange = useCallback( (e: React.ChangeEvent) => { if (selectedQuickPrompt != null) { - setUpdatedQuickPromptSettings((prev) => { - const alreadyExists = prev.some((qp) => qp.title === selectedQuickPrompt.title); + setUpdatedQuickPromptSettings((prev): PromptResponse[] => { + const alreadyExists = prev.some((qp) => qp.id === selectedQuickPrompt.id); if (alreadyExists) { return prev.map((qp) => { - if (qp.title === selectedQuickPrompt.title) { + if (qp.id === selectedQuickPrompt.id) { return { ...qp, - prompt: e.target.value, + content: e.target.value, }; } return qp; @@ -64,9 +71,45 @@ const QuickPromptSettingsEditorComponent = ({ return prev; }); + + const existingPrompt = quickPromptSettings.find((sp) => sp.id === selectedQuickPrompt.id); + if (existingPrompt) { + setPromptsBulkActions({ + ...promptsBulkActions, + ...(selectedQuickPrompt.name !== selectedQuickPrompt.id + ? { + update: [ + ...(promptsBulkActions.update ?? []).filter( + (p) => p.id !== selectedQuickPrompt.id + ), + { + ...selectedQuickPrompt, + content: e.target.value, + }, + ], + } + : { + create: [ + ...(promptsBulkActions.create ?? []).filter( + (p) => p.name !== selectedQuickPrompt.name + ), + { + ...selectedQuickPrompt, + content: e.target.value, + }, + ], + }), + }); + } } }, - [selectedQuickPrompt, setUpdatedQuickPromptSettings] + [ + promptsBulkActions, + quickPromptSettings, + selectedQuickPrompt, + setPromptsBulkActions, + setUpdatedQuickPromptSettings, + ] ); // Color @@ -79,11 +122,11 @@ const QuickPromptSettingsEditorComponent = ({ (color, { hex, isValid }) => { if (selectedQuickPrompt != null) { setUpdatedQuickPromptSettings((prev) => { - const alreadyExists = prev.some((qp) => qp.title === selectedQuickPrompt.title); + const alreadyExists = prev.some((qp) => qp.name === selectedQuickPrompt.name); if (alreadyExists) { return prev.map((qp) => { - if (qp.title === selectedQuickPrompt.title) { + if (qp.name === selectedQuickPrompt.name) { return { ...qp, color, @@ -94,9 +137,44 @@ const QuickPromptSettingsEditorComponent = ({ } return prev; }); + const existingPrompt = quickPromptSettings.find((sp) => sp.id === selectedQuickPrompt.id); + if (existingPrompt) { + setPromptsBulkActions({ + ...promptsBulkActions, + ...(selectedQuickPrompt.name !== selectedQuickPrompt.id + ? { + update: [ + ...(promptsBulkActions.update ?? []).filter( + (p) => p.id !== selectedQuickPrompt.id + ), + { + ...selectedQuickPrompt, + color, + }, + ], + } + : { + create: [ + ...(promptsBulkActions.create ?? []).filter( + (p) => p.name !== selectedQuickPrompt.name + ), + { + ...selectedQuickPrompt, + color, + }, + ], + }), + }); + } } }, - [selectedQuickPrompt, setUpdatedQuickPromptSettings] + [ + promptsBulkActions, + quickPromptSettings, + selectedQuickPrompt, + setPromptsBulkActions, + setUpdatedQuickPromptSettings, + ] ); // Prompt Contexts @@ -112,11 +190,11 @@ const QuickPromptSettingsEditorComponent = ({ (pc: PromptContextTemplate[]) => { if (selectedQuickPrompt != null) { setUpdatedQuickPromptSettings((prev) => { - const alreadyExists = prev.some((qp) => qp.title === selectedQuickPrompt.title); + const alreadyExists = prev.some((qp) => qp.name === selectedQuickPrompt.name); if (alreadyExists) { return prev.map((qp) => { - if (qp.title === selectedQuickPrompt.title) { + if (qp.name === selectedQuickPrompt.name) { return { ...qp, categories: pc.map((p) => p.category), @@ -127,15 +205,53 @@ const QuickPromptSettingsEditorComponent = ({ } return prev; }); + + const existingPrompt = quickPromptSettings.find((sp) => sp.id === selectedQuickPrompt.id); + if (existingPrompt) { + setPromptsBulkActions({ + ...promptsBulkActions, + ...(selectedQuickPrompt.name !== selectedQuickPrompt.id + ? { + update: [ + ...(promptsBulkActions.update ?? []).filter( + (p) => p.id !== selectedQuickPrompt.id + ), + { + ...selectedQuickPrompt, + categories: pc.map((p) => p.category), + }, + ], + } + : { + create: [ + ...(promptsBulkActions.create ?? []).filter( + (p) => p.name !== selectedQuickPrompt.name + ), + { + ...selectedQuickPrompt, + categories: pc.map((p) => p.category), + }, + ], + }), + }); + } } }, - [selectedQuickPrompt, setUpdatedQuickPromptSettings] + [ + promptsBulkActions, + quickPromptSettings, + selectedQuickPrompt, + setPromptsBulkActions, + setUpdatedQuickPromptSettings, + ] ); // When top level quick prompt selection changes const { onQuickPromptDeleted, onQuickPromptSelectionChange } = useQuickPromptEditor({ onSelectedQuickPromptChange, setUpdatedQuickPromptSettings, + promptsBulkActions, + setPromptsBulkActions, }); return ( @@ -158,7 +274,7 @@ const QuickPromptSettingsEditorComponent = ({ data-test-subj="quick-prompt-prompt" onChange={handlePromptChange} placeholder={i18n.QUICK_PROMPT_PROMPT_PLACEHOLDER} - value={prompt} + value={promptContent} css={css` min-height: 150px; `} diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings/quick_prompt_settings.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings/quick_prompt_settings.test.tsx index 6aa939934d585..eb8cc2cb2569c 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings/quick_prompt_settings.test.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings/quick_prompt_settings.test.tsx @@ -13,6 +13,7 @@ import { MOCK_QUICK_PROMPTS } from '../../../mock/quick_prompt'; import { mockPromptContexts } from '../../../mock/prompt_context'; const onSelectedQuickPromptChange = jest.fn(); +const setPromptsBulkActions = jest.fn(); const setUpdatedQuickPromptSettings = jest.fn().mockImplementation((fn) => { return fn(MOCK_QUICK_PROMPTS); }); @@ -22,6 +23,8 @@ const testProps = { quickPromptSettings: MOCK_QUICK_PROMPTS, selectedQuickPrompt: MOCK_QUICK_PROMPTS[0], setUpdatedQuickPromptSettings, + promptsBulkActions: {}, + setPromptsBulkActions, }; const mockContext = { basePromptContexts: MOCK_QUICK_PROMPTS, @@ -91,8 +94,11 @@ describe('QuickPromptSettings', () => { const customOption = { categories: [], color: '#D36086', - prompt: '', - title: 'sooper custom prompt', + consumer: undefined, + content: '', + id: 'sooper custom prompt', + name: 'sooper custom prompt', + promptType: 'quick', }; expect(setUpdatedQuickPromptSettings).toHaveReturnedWith([...MOCK_QUICK_PROMPTS, customOption]); expect(onSelectedQuickPromptChange).toHaveBeenCalledWith(customOption); @@ -130,7 +136,7 @@ describe('QuickPromptSettings', () => { const previousFirstElementOfTheArray = mutatableQuickPrompts.shift(); expect(setUpdatedQuickPromptSettings).toHaveReturnedWith([ - { ...previousFirstElementOfTheArray, prompt: 'what does this do' }, + { ...previousFirstElementOfTheArray, content: 'what does this do' }, ...mutatableQuickPrompts, ]); }); diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings/quick_prompt_settings.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings/quick_prompt_settings.tsx index 4b8b6a8f8039d..61496c64fd73a 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings/quick_prompt_settings.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings/quick_prompt_settings.tsx @@ -8,15 +8,20 @@ import React from 'react'; import { EuiTitle, EuiText, EuiHorizontalRule, EuiSpacer } from '@elastic/eui'; +import { + PromptResponse, + PerformBulkActionRequestBody as PromptsPerformBulkActionRequestBody, +} from '@kbn/elastic-assistant-common/impl/schemas/prompts/bulk_crud_prompts_route.gen'; import * as i18n from './translations'; -import { QuickPrompt } from '../types'; import { QuickPromptSettingsEditor } from './quick_prompt_editor'; interface Props { - onSelectedQuickPromptChange: (quickPrompt?: QuickPrompt) => void; - quickPromptSettings: QuickPrompt[]; - selectedQuickPrompt: QuickPrompt | undefined; - setUpdatedQuickPromptSettings: React.Dispatch>; + onSelectedQuickPromptChange: (quickPrompt?: PromptResponse) => void; + quickPromptSettings: PromptResponse[]; + selectedQuickPrompt: PromptResponse | undefined; + setUpdatedQuickPromptSettings: React.Dispatch>; + promptsBulkActions: PromptsPerformBulkActionRequestBody; + setPromptsBulkActions: React.Dispatch>; } /** @@ -28,6 +33,8 @@ export const QuickPromptSettings: React.FC = React.memo( quickPromptSettings, selectedQuickPrompt, setUpdatedQuickPromptSettings, + promptsBulkActions, + setPromptsBulkActions, }) => { return ( <> @@ -43,6 +50,8 @@ export const QuickPromptSettings: React.FC = React.memo( quickPromptSettings={quickPromptSettings} selectedQuickPrompt={selectedQuickPrompt} setUpdatedQuickPromptSettings={setUpdatedQuickPromptSettings} + promptsBulkActions={promptsBulkActions} + setPromptsBulkActions={setPromptsBulkActions} /> ); diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings/use_quick_prompt_editor.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings/use_quick_prompt_editor.test.tsx index ec3a0256716ae..509db5991455f 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings/use_quick_prompt_editor.test.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings/use_quick_prompt_editor.test.tsx @@ -7,18 +7,24 @@ import { renderHook, act } from '@testing-library/react-hooks'; import { useQuickPromptEditor, DEFAULT_COLOR } from './use_quick_prompt_editor'; -import { QuickPrompt } from '../types'; import { mockAlertPromptContext } from '../../../mock/prompt_context'; import { MOCK_QUICK_PROMPTS } from '../../../mock/quick_prompt'; +import { PromptResponse } from '@kbn/elastic-assistant-common'; +import { useAssistantContext } from '../../../assistant_context'; +jest.mock('../../../assistant_context'); // Mock functions for the tests const mockOnSelectedQuickPromptChange = jest.fn(); const mockSetUpdatedQuickPromptSettings = jest.fn(); const mockPreviousQuickPrompts = [...MOCK_QUICK_PROMPTS]; +const setPromptsBulkActions = jest.fn(); describe('useQuickPromptEditor', () => { beforeEach(() => { jest.clearAllMocks(); + (useAssistantContext as jest.Mock).mockReturnValue({ + currentAppId: 'securitySolutionUI', + }); }); test('should delete a quick prompt by title', () => { @@ -26,6 +32,8 @@ describe('useQuickPromptEditor', () => { useQuickPromptEditor({ onSelectedQuickPromptChange: mockOnSelectedQuickPromptChange, setUpdatedQuickPromptSettings: mockSetUpdatedQuickPromptSettings, + setPromptsBulkActions, + promptsBulkActions: {}, }) ); @@ -34,7 +42,7 @@ describe('useQuickPromptEditor', () => { }); expect(mockSetUpdatedQuickPromptSettings.mock.calls[0][0]?.(mockPreviousQuickPrompts)).toEqual( - MOCK_QUICK_PROMPTS.filter((qp) => qp.title !== 'ALERT_SUMMARIZATION_TITLE') + MOCK_QUICK_PROMPTS.filter((qp) => qp.name !== 'ALERT_SUMMARIZATION_TITLE') ); }); @@ -44,6 +52,8 @@ describe('useQuickPromptEditor', () => { useQuickPromptEditor({ onSelectedQuickPromptChange: mockOnSelectedQuickPromptChange, setUpdatedQuickPromptSettings: mockSetUpdatedQuickPromptSettings, + setPromptsBulkActions, + promptsBulkActions: {}, }) ); @@ -51,11 +61,14 @@ describe('useQuickPromptEditor', () => { result.current.onQuickPromptSelectionChange(newPromptTitle); }); - const newPrompt: QuickPrompt = { - title: newPromptTitle, - prompt: '', + const newPrompt: PromptResponse = { + name: newPromptTitle, + content: '', color: DEFAULT_COLOR, categories: [], + id: newPromptTitle, + promptType: 'quick', + consumer: 'securitySolutionUI', }; expect(mockOnSelectedQuickPromptChange).toHaveBeenCalledWith(newPrompt); @@ -70,17 +83,21 @@ describe('useQuickPromptEditor', () => { useQuickPromptEditor({ onSelectedQuickPromptChange: mockOnSelectedQuickPromptChange, setUpdatedQuickPromptSettings: mockSetUpdatedQuickPromptSettings, + setPromptsBulkActions, + promptsBulkActions: {}, }) ); const alertData = await mockAlertPromptContext.getPromptContext(); - const expectedPrompt: QuickPrompt = { - title: mockAlertPromptContext.description, - prompt: alertData, + const expectedPrompt: PromptResponse = { + name: mockAlertPromptContext.description, + content: JSON.stringify(alertData ?? {}), color: DEFAULT_COLOR, categories: [mockAlertPromptContext.category], - } as QuickPrompt; + id: '', + promptType: 'quick', + }; act(() => { result.current.onQuickPromptSelectionChange(expectedPrompt); diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings/use_quick_prompt_editor.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings/use_quick_prompt_editor.tsx index 716298afb21da..d96c4fca716d1 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings/use_quick_prompt_editor.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings/use_quick_prompt_editor.tsx @@ -5,41 +5,60 @@ * 2.0. */ +import { + PromptResponse, + PromptTypeEnum, + PerformBulkActionRequestBody as PromptsPerformBulkActionRequestBody, +} from '@kbn/elastic-assistant-common/impl/schemas/prompts/bulk_crud_prompts_route.gen'; import { useCallback } from 'react'; -import { QuickPrompt } from '../types'; +import { useAssistantContext } from '../../../..'; export const DEFAULT_COLOR = '#D36086'; export const useQuickPromptEditor = ({ onSelectedQuickPromptChange, setUpdatedQuickPromptSettings, + promptsBulkActions, + setPromptsBulkActions, }: { - onSelectedQuickPromptChange: (quickPrompt?: QuickPrompt) => void; - setUpdatedQuickPromptSettings: React.Dispatch>; + onSelectedQuickPromptChange: (quickPrompt?: PromptResponse) => void; + setUpdatedQuickPromptSettings: React.Dispatch>; + promptsBulkActions: PromptsPerformBulkActionRequestBody; + setPromptsBulkActions: React.Dispatch>; }) => { + const { currentAppId } = useAssistantContext(); const onQuickPromptDeleted = useCallback( - (title: string) => { - setUpdatedQuickPromptSettings((prev) => prev.filter((qp) => qp.title !== title)); + (id: string) => { + setUpdatedQuickPromptSettings((prev) => prev.filter((qp) => qp.id !== id)); + setPromptsBulkActions({ + ...promptsBulkActions, + delete: { + ids: [...(promptsBulkActions.delete?.ids ?? []), id], + }, + }); }, - [setUpdatedQuickPromptSettings] + [promptsBulkActions, setPromptsBulkActions, setUpdatedQuickPromptSettings] ); // When top level quick prompt selection changes const onQuickPromptSelectionChange = useCallback( - (quickPrompt?: QuickPrompt | string) => { + (quickPrompt?: PromptResponse | string) => { const isNew = typeof quickPrompt === 'string'; - const newSelectedQuickPrompt: QuickPrompt | undefined = isNew + const newSelectedQuickPrompt: PromptResponse | undefined = isNew ? { - title: quickPrompt ?? '', - prompt: '', + name: quickPrompt, + id: quickPrompt, + content: '', color: DEFAULT_COLOR, categories: [], + promptType: PromptTypeEnum.quick, + consumer: currentAppId, } : quickPrompt; if (newSelectedQuickPrompt != null) { setUpdatedQuickPromptSettings((prev) => { - const alreadyExists = prev.some((qp) => qp.title === newSelectedQuickPrompt.title); + const alreadyExists = prev.some((qp) => qp.name === newSelectedQuickPrompt.name); if (!alreadyExists) { return [...prev, newSelectedQuickPrompt]; @@ -47,11 +66,29 @@ export const useQuickPromptEditor = ({ return prev; }); + + if (isNew) { + setPromptsBulkActions({ + ...promptsBulkActions, + create: [ + ...(promptsBulkActions.create ?? []), + { + ...newSelectedQuickPrompt, + }, + ], + }); + } } onSelectedQuickPromptChange(newSelectedQuickPrompt); }, - [onSelectedQuickPromptChange, setUpdatedQuickPromptSettings] + [ + currentAppId, + onSelectedQuickPromptChange, + promptsBulkActions, + setPromptsBulkActions, + setUpdatedQuickPromptSettings, + ] ); return { onQuickPromptDeleted, onQuickPromptSelectionChange }; diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings_management/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings_management/index.tsx index e8362db441719..ac93161d35c17 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings_management/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings_management/index.tsx @@ -14,7 +14,10 @@ import { EuiPanel, EuiSpacer, } from '@elastic/eui'; -import { QuickPrompt } from '../types'; +import { + PromptResponse, + PerformBulkActionRequestBody as PromptsPerformBulkActionRequestBody, +} from '@kbn/elastic-assistant-common/impl/schemas/prompts/bulk_crud_prompts_route.gen'; import { QuickPromptSettingsEditor } from '../quick_prompt_settings/quick_prompt_editor'; import * as i18n from './translations'; import { useFlyoutModalVisibility } from '../../common/components/assistant_settings_management/flyout/use_flyout_modal_visibility'; @@ -32,11 +35,13 @@ import { useAssistantContext } from '../../../assistant_context'; interface Props { handleSave: (shouldRefetchConversation?: boolean) => void; onCancelClick: () => void; - onSelectedQuickPromptChange: (quickPrompt?: QuickPrompt) => void; - quickPromptSettings: QuickPrompt[]; + onSelectedQuickPromptChange: (quickPrompt?: PromptResponse) => void; + quickPromptSettings: PromptResponse[]; resetSettings?: () => void; - selectedQuickPrompt: QuickPrompt | undefined; - setUpdatedQuickPromptSettings: React.Dispatch>; + selectedQuickPrompt: PromptResponse | undefined; + setUpdatedQuickPromptSettings: React.Dispatch>; + promptsBulkActions: PromptsPerformBulkActionRequestBody; + setPromptsBulkActions: React.Dispatch>; } const QuickPromptSettingsManagementComponent = ({ @@ -47,11 +52,13 @@ const QuickPromptSettingsManagementComponent = ({ resetSettings, selectedQuickPrompt, setUpdatedQuickPromptSettings, + promptsBulkActions, + setPromptsBulkActions, }: Props) => { const { nameSpace, basePromptContexts } = useAssistantContext(); const { isFlyoutOpen: editFlyoutVisible, openFlyout, closeFlyout } = useFlyoutModalVisibility(); - const [deletedQuickPrompt, setDeletedQuickPrompt] = useState(); + const [deletedQuickPrompt, setDeletedQuickPrompt] = useState(); const { isFlyoutOpen: deleteConfirmModalVisibility, openFlyout: openConfirmModal, @@ -61,10 +68,12 @@ const QuickPromptSettingsManagementComponent = ({ const { onQuickPromptDeleted, onQuickPromptSelectionChange } = useQuickPromptEditor({ onSelectedQuickPromptChange, setUpdatedQuickPromptSettings, + promptsBulkActions, + setPromptsBulkActions, }); const onEditActionClicked = useCallback( - (prompt: QuickPrompt) => { + (prompt: PromptResponse) => { onQuickPromptSelectionChange(prompt); openFlyout(); }, @@ -72,9 +81,9 @@ const QuickPromptSettingsManagementComponent = ({ ); const onDeleteActionClicked = useCallback( - (prompt: QuickPrompt) => { + (prompt: PromptResponse) => { setDeletedQuickPrompt(prompt); - onQuickPromptDeleted(prompt.title); + onQuickPromptDeleted(prompt.id); openConfirmModal(); }, [onQuickPromptDeleted, openConfirmModal] @@ -123,10 +132,10 @@ const QuickPromptSettingsManagementComponent = ({ const confirmationTitle = useMemo( () => - deletedQuickPrompt?.title - ? i18n.DELETE_QUICK_PROMPT_MODAL_TITLE(deletedQuickPrompt.title) + deletedQuickPrompt?.name + ? i18n.DELETE_QUICK_PROMPT_MODAL_TITLE(deletedQuickPrompt.name) : i18n.DELETE_QUICK_PROMPT_MODAL_DEFAULT_TITLE, - [deletedQuickPrompt?.title] + [deletedQuickPrompt?.name] ); return ( @@ -161,6 +170,8 @@ const QuickPromptSettingsManagementComponent = ({ resetSettings={resetSettings} selectedQuickPrompt={selectedQuickPrompt} setUpdatedQuickPromptSettings={setUpdatedQuickPromptSettings} + promptsBulkActions={promptsBulkActions} + setPromptsBulkActions={setPromptsBulkActions} /> {deleteConfirmModalVisibility && deletedQuickPrompt && ( diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings_management/use_quick_prompt_table.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings_management/use_quick_prompt_table.test.tsx index 316b43f6cfb3d..ca647dc530265 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings_management/use_quick_prompt_table.test.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings_management/use_quick_prompt_table.test.tsx @@ -8,9 +8,9 @@ import { renderHook } from '@testing-library/react-hooks'; import { useQuickPromptTable } from './use_quick_prompt_table'; import { EuiTableComputedColumnType } from '@elastic/eui'; -import { QuickPrompt } from '../types'; import { MOCK_QUICK_PROMPTS } from '../../../mock/quick_prompt'; import { mockPromptContexts } from '../../../mock/prompt_context'; +import { PromptResponse } from '@kbn/elastic-assistant-common'; const mockOnEditActionClicked = jest.fn(); const mockOnDeleteActionClicked = jest.fn(); @@ -43,7 +43,7 @@ describe('useQuickPromptTable', () => { }); const mockQuickPrompt = { ...MOCK_QUICK_PROMPTS[0], categories: ['alert'] }; - const mockBadgesColumn = (columns[1] as EuiTableComputedColumnType).render( + const mockBadgesColumn = (columns[1] as EuiTableComputedColumnType).render( mockQuickPrompt ); const selectedPromptContexts = mockPromptContexts @@ -51,7 +51,7 @@ describe('useQuickPromptTable', () => { .map((bpc) => bpc.description); expect(mockBadgesColumn).toHaveProperty('props', { items: selectedPromptContexts, - prefix: MOCK_QUICK_PROMPTS[0].title, + prefix: MOCK_QUICK_PROMPTS[0].name, }); }); @@ -62,7 +62,7 @@ describe('useQuickPromptTable', () => { onDeleteActionClicked: mockOnDeleteActionClicked, }); - const mockRowActions = (columns[2] as EuiTableComputedColumnType).render( + const mockRowActions = (columns[2] as EuiTableComputedColumnType).render( MOCK_QUICK_PROMPTS[0] ); @@ -83,7 +83,7 @@ describe('useQuickPromptTable', () => { const nonDefaultPrompt = MOCK_QUICK_PROMPTS.find((qp) => !qp.isDefault); if (nonDefaultPrompt) { - const mockRowActions = (columns[2] as EuiTableComputedColumnType).render( + const mockRowActions = (columns[2] as EuiTableComputedColumnType).render( nonDefaultPrompt ); expect(mockRowActions).toHaveProperty('props', { diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings_management/use_quick_prompt_table.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings_management/use_quick_prompt_table.tsx index 9ec334f817340..1899905db0ea1 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings_management/use_quick_prompt_table.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings_management/use_quick_prompt_table.tsx @@ -7,10 +7,10 @@ import { EuiBasicTableColumn, EuiLink } from '@elastic/eui'; import React, { useCallback } from 'react'; +import { PromptResponse } from '@kbn/elastic-assistant-common'; import { BadgesColumn } from '../../common/components/assistant_settings_management/badges'; import { RowActions } from '../../common/components/assistant_settings_management/row_actions'; import { PromptContextTemplate } from '../../prompt_context/types'; -import { QuickPrompt } from '../types'; import * as i18n from './translations'; export const useQuickPromptTable = () => { @@ -21,29 +21,29 @@ export const useQuickPromptTable = () => { onDeleteActionClicked, }: { basePromptContexts: PromptContextTemplate[]; - onEditActionClicked: (prompt: QuickPrompt) => void; - onDeleteActionClicked: (prompt: QuickPrompt) => void; - }): Array> => [ + onEditActionClicked: (prompt: PromptResponse) => void; + onDeleteActionClicked: (prompt: PromptResponse) => void; + }): Array> => [ { align: 'left', name: i18n.QUICK_PROMPTS_TABLE_COLUMN_NAME, - render: (prompt: QuickPrompt) => - prompt?.title ? ( - onEditActionClicked(prompt)}>{prompt?.title} + render: (prompt: PromptResponse) => + prompt?.name ? ( + onEditActionClicked(prompt)}>{prompt?.name} ) : null, - sortable: ({ title }: QuickPrompt) => title, + sortable: ({ name }: PromptResponse) => name, }, { align: 'left', name: i18n.QUICK_PROMPTS_TABLE_COLUMN_CONTEXTS, - render: (prompt: QuickPrompt) => { + render: (prompt: PromptResponse) => { const selectedPromptContexts = ( basePromptContexts.filter((bpc) => prompt?.categories?.some((cat) => bpc?.category === cat) ) ?? [] ).map((bpc) => bpc?.description); return selectedPromptContexts ? ( - + ) : null; }, }, @@ -58,13 +58,13 @@ export const useQuickPromptTable = () => { align: 'center', name: i18n.QUICK_PROMPTS_TABLE_COLUMN_ACTIONS, width: '120px', - render: (prompt: QuickPrompt) => { + render: (prompt: PromptResponse) => { if (!prompt) { return null; } const isDeletable = !prompt.isDefault; return ( - + rowItem={prompt} onDelete={isDeletable ? onDeleteActionClicked : undefined} onEdit={onEditActionClicked} diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompts.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompts.test.tsx index 7fb2c9760fc7b..6e5172dc0c2ad 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompts.test.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompts.test.tsx @@ -7,10 +7,10 @@ import React from 'react'; import { fireEvent, render } from '@testing-library/react'; -import { QuickPrompts } from './quick_prompts'; import { TestProviders } from '../../mock/test_providers/test_providers'; import { MOCK_QUICK_PROMPTS } from '../../mock/quick_prompt'; import { QUICK_PROMPTS_TAB } from '../settings/const'; +import { QuickPrompts } from './quick_prompts'; const setInput = jest.fn(); const setIsSettingsModalVisible = jest.fn(); @@ -20,6 +20,7 @@ const testProps = { setIsSettingsModalVisible, trackPrompt, isFlyoutMode: false, + allPrompts: MOCK_QUICK_PROMPTS, }; const setSelectedSettingsTab = jest.fn(); const mockUseAssistantContext = { diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompts.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompts.tsx index 7d08d20f432b9..c578a58be728d 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompts.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompts.tsx @@ -17,7 +17,10 @@ import { import { useMeasure } from 'react-use'; import { css } from '@emotion/react'; -import { QuickPrompt } from '../../..'; +import { + PromptResponse, + PromptTypeEnum, +} from '@kbn/elastic-assistant-common/impl/schemas/prompts/bulk_crud_prompts_route.gen'; import * as i18n from './translations'; import { useAssistantContext } from '../../assistant_context'; import { QUICK_PROMPTS_TAB } from '../settings/const'; @@ -30,6 +33,7 @@ interface QuickPromptsProps { setIsSettingsModalVisible: React.Dispatch>; trackPrompt: (prompt: string) => void; isFlyoutMode: boolean; + allPrompts: PromptResponse[]; } /** @@ -38,11 +42,10 @@ interface QuickPromptsProps { * and localstorage for storing new and edited prompts. */ export const QuickPrompts: React.FC = React.memo( - ({ setInput, setIsSettingsModalVisible, trackPrompt, isFlyoutMode }) => { + ({ setInput, setIsSettingsModalVisible, trackPrompt, isFlyoutMode, allPrompts }) => { const [quickPromptsContainerRef, { width }] = useMeasure(); - const { allQuickPrompts, knowledgeBase, promptContexts, setSelectedSettingsTab } = - useAssistantContext(); + const { knowledgeBase, promptContexts, setSelectedSettingsTab } = useAssistantContext(); const contextFilteredQuickPrompts = useMemo(() => { const registeredPromptContextTitles = Object.values(promptContexts).map((pc) => pc.category); @@ -50,17 +53,21 @@ export const QuickPrompts: React.FC = React.memo( if (knowledgeBase.isEnabledKnowledgeBase) { registeredPromptContextTitles.push(KNOWLEDGE_BASE_CATEGORY); } - return allQuickPrompts.filter((quickPrompt) => { + return allPrompts.filter((prompt) => { + // only quick prompts + if (prompt.promptType !== PromptTypeEnum.quick) { + return false; + } // Return quick prompt as match if it has no categories, otherwise ensure category exists in registered prompt contexts - if (quickPrompt.categories == null || quickPrompt.categories.length === 0) { + if (!prompt.categories || prompt.categories.length === 0) { return true; } else { - return quickPrompt.categories.some((category) => { + return prompt.categories?.some((category) => { return registeredPromptContextTitles.includes(category); }); } }); - }, [allQuickPrompts, knowledgeBase.isEnabledKnowledgeBase, promptContexts]); + }, [allPrompts, knowledgeBase.isEnabledKnowledgeBase, promptContexts]); // Overflow state const [isOverflowPopoverOpen, setIsOverflowPopoverOpen] = useState(false); @@ -71,10 +78,10 @@ export const QuickPrompts: React.FC = React.memo( const closeOverflowPopover = useCallback(() => setIsOverflowPopoverOpen(false), []); const onClickAddQuickPrompt = useCallback( - (badge: QuickPrompt) => { - setInput(badge.prompt); + (badge: PromptResponse) => { + setInput(badge.content); if (badge.isDefault) { - trackPrompt(badge.title); + trackPrompt(badge.name); } else { trackPrompt('Custom'); } @@ -83,7 +90,7 @@ export const QuickPrompts: React.FC = React.memo( ); const onClickOverflowQuickPrompt = useCallback( - (badge: QuickPrompt) => { + (badge: PromptResponse) => { onClickAddQuickPrompt(badge); closeOverflowPopover(); }, @@ -137,9 +144,9 @@ export const QuickPrompts: React.FC = React.memo( onClickAddQuickPrompt(badge)} - onClickAriaLabel={badge.title} + onClickAriaLabel={badge.name} > - {badge.title} + {badge.name} ))} @@ -172,9 +179,9 @@ export const QuickPrompts: React.FC = React.memo( onClickOverflowQuickPrompt(badge)} - onClickAriaLabel={badge.title} + onClickAriaLabel={badge.name} > - {badge.title} + {badge.name} ))} diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/types.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/types.tsx deleted file mode 100644 index c0688f432e7dd..0000000000000 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/quick_prompts/types.tsx +++ /dev/null @@ -1,25 +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 { PromptContext } from '../../..'; - -/** - * A QuickPrompt is a badge that is displayed below the Assistant's input field. They provide - * a quick way for users to insert prompts as templates into the Assistant's input field. If no - * categories are provided they will always display with the assistant, however categories can be - * supplied to only display the QuickPrompt when the Assistant is registered with corresponding - * PromptContext's containing the same category. - * - * isDefault: If true, this QuickPrompt cannot be deleted by the user - */ -export interface QuickPrompt { - title: string; - prompt: string; - color: string; - categories?: Array; - isDefault?: boolean; -} diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings.tsx index 68a8049b825b3..d5bbefe304208 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings.tsx @@ -23,8 +23,9 @@ import { // eslint-disable-next-line @kbn/eslint/module_migration import styled from 'styled-components'; import { css } from '@emotion/react'; +import { PromptResponse } from '@kbn/elastic-assistant-common'; import { AIConnector } from '../../connectorland/connector_selector'; -import { Conversation, Prompt, QuickPrompt, useLoadConnectors } from '../../..'; +import { Conversation, useLoadConnectors } from '../../..'; import * as i18n from './translations'; import { useAssistantContext } from '../../assistant_context'; import { TEST_IDS } from '../constants'; @@ -46,6 +47,7 @@ import { QUICK_PROMPTS_TAB, SYSTEM_PROMPTS_TAB, } from './const'; +import { useFetchPrompts } from '../api/prompts/use_fetch_prompts'; const StyledEuiModal = styled(EuiModal)` width: 800px; @@ -97,6 +99,7 @@ export const AssistantSettings: React.FC = React.memo( const { data: anonymizationFields, refetch: refetchAnonymizationFieldsResults } = useFetchAnonymizationFields(); + const { data: allPrompts } = useFetchPrompts(); const { data: connectors } = useLoadConnectors({ http, @@ -112,7 +115,7 @@ export const AssistantSettings: React.FC = React.memo( setUpdatedAssistantStreamingEnabled, setUpdatedKnowledgeBaseSettings, setUpdatedQuickPromptSettings, - setUpdatedSystemPromptSettings, + promptsBulkActions, saveSettings, conversationsSettingsBulkActions, updatedAnonymizationData, @@ -120,7 +123,9 @@ export const AssistantSettings: React.FC = React.memo( anonymizationFieldsBulkActions, setAnonymizationFieldsBulkActions, setUpdatedAnonymizationData, - } = useSettingsUpdater(conversations, conversationsLoaded, anonymizationFields); + setPromptsBulkActions, + setUpdatedSystemPromptSettings, + } = useSettingsUpdater(conversations, allPrompts, conversationsLoaded, anonymizationFields); // Local state for saving previously selected items so tab switching is friendlier // Conversation Selection State @@ -137,21 +142,21 @@ export const AssistantSettings: React.FC = React.memo( ); // Quick Prompt Selection State - const [selectedQuickPrompt, setSelectedQuickPrompt] = useState(); - const onHandleSelectedQuickPromptChange = useCallback((quickPrompt?: QuickPrompt) => { + const [selectedQuickPrompt, setSelectedQuickPrompt] = useState(); + const onHandleSelectedQuickPromptChange = useCallback((quickPrompt?: PromptResponse) => { setSelectedQuickPrompt(quickPrompt); }, []); useEffect(() => { if (selectedQuickPrompt != null) { setSelectedQuickPrompt( - quickPromptSettings.find((q) => q.title === selectedQuickPrompt.title) + quickPromptSettings.find((q) => q.name === selectedQuickPrompt.name) ); } }, [quickPromptSettings, selectedQuickPrompt]); // System Prompt Selection State - const [selectedSystemPrompt, setSelectedSystemPrompt] = useState(); - const onHandleSelectedSystemPromptChange = useCallback((systemPrompt?: Prompt) => { + const [selectedSystemPrompt, setSelectedSystemPrompt] = useState(); + const onHandleSelectedSystemPromptChange = useCallback((systemPrompt?: PromptResponse) => { setSelectedSystemPrompt(systemPrompt); }, []); useEffect(() => { @@ -342,6 +347,8 @@ export const AssistantSettings: React.FC = React.memo( onSelectedQuickPromptChange={onHandleSelectedQuickPromptChange} selectedQuickPrompt={selectedQuickPrompt} setUpdatedQuickPromptSettings={setUpdatedQuickPromptSettings} + setPromptsBulkActions={setPromptsBulkActions} + promptsBulkActions={promptsBulkActions} /> )} {selectedSettingsTab === SYSTEM_PROMPTS_TAB && ( @@ -356,6 +363,8 @@ export const AssistantSettings: React.FC = React.memo( setConversationsSettingsBulkActions={setConversationsSettingsBulkActions} conversationsSettingsBulkActions={conversationsSettingsBulkActions} setUpdatedSystemPromptSettings={setUpdatedSystemPromptSettings} + setPromptsBulkActions={setPromptsBulkActions} + promptsBulkActions={promptsBulkActions} /> )} {selectedSettingsTab === ANONYMIZATION_TAB && ( diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings_button.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings_button.tsx index 432730194d1a3..30f141f219476 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings_button.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings_button.tsx @@ -8,6 +8,7 @@ import React, { useCallback } from 'react'; import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; +import { QueryObserverResult, RefetchOptions, RefetchQueryFilters } from '@tanstack/react-query'; import { AIConnector } from '../../connectorland/connector_selector'; import { Conversation } from '../../..'; import { AssistantSettings } from './assistant_settings'; @@ -26,6 +27,9 @@ interface Props { conversations: Record; conversationsLoaded: boolean; refetchConversationsState: () => Promise; + refetchPrompts?: ( + options?: RefetchOptions & RefetchQueryFilters + ) => Promise>; } /** @@ -43,6 +47,7 @@ export const AssistantSettingsButton: React.FC = React.memo( conversations, conversationsLoaded, refetchConversationsState, + refetchPrompts, }) => { const { toasts, setSelectedSettingsTab } = useAssistantContext(); @@ -59,6 +64,9 @@ export const AssistantSettingsButton: React.FC = React.memo( async (success: boolean) => { cleanupAndCloseModal(); await refetchConversationsState(); + if (refetchPrompts) { + await refetchPrompts(); + } if (success) { toasts?.addSuccess({ iconType: 'check', @@ -66,7 +74,7 @@ export const AssistantSettingsButton: React.FC = React.memo( }); } }, - [cleanupAndCloseModal, refetchConversationsState, toasts] + [cleanupAndCloseModal, refetchConversationsState, refetchPrompts, toasts] ); const handleShowConversationSettings = useCallback(() => { diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings_management.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings_management.test.tsx index 3b34b3467aa84..15fb05ca1c807 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings_management.test.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings_management.test.tsx @@ -23,6 +23,7 @@ import { QUICK_PROMPTS_TAB, SYSTEM_PROMPTS_TAB, } from './const'; +import { mockSystemPrompts } from '../../mock/system_prompt'; const mockConversations = { [alertConvo.title]: alertConvo, @@ -33,6 +34,8 @@ const saveSettings = jest.fn(); const mockValues = { conversationSettings: mockConversations, saveSettings, + systemPromptSettings: mockSystemPrompts, + quickPromptSettings: [], }; const setSelectedSettingsTab = jest.fn(); diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings_management.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings_management.tsx index 4e89bb3bba4fc..3f9be4972fe7e 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings_management.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/assistant_settings_management.tsx @@ -19,7 +19,8 @@ import { } from '@elastic/eui'; import { css } from '@emotion/react'; -import { Conversation, Prompt, QuickPrompt } from '../../..'; +import { PromptResponse, PromptTypeEnum } from '@kbn/elastic-assistant-common'; +import { Conversation } from '../../..'; import * as i18n from './translations'; import { useAssistantContext } from '../../assistant_context'; import { useSettingsUpdater } from './use_settings_updater/use_settings_updater'; @@ -42,6 +43,7 @@ import { QUICK_PROMPTS_TAB, SYSTEM_PROMPTS_TAB, } from './const'; +import { useFetchPrompts } from '../api/prompts/use_fetch_prompts'; interface Props { conversations: Record; @@ -73,6 +75,9 @@ export const AssistantSettingsManagement: React.FC = React.memo( const { data: anonymizationFields } = useFetchAnonymizationFields(); + const { data: allPrompts } = useFetchPrompts(); + + // Connector details const { data: connectors } = useLoadConnectors({ http, }); @@ -92,7 +97,7 @@ export const AssistantSettingsManagement: React.FC = React.memo( setUpdatedAssistantStreamingEnabled, setUpdatedKnowledgeBaseSettings, setUpdatedQuickPromptSettings, - setUpdatedSystemPromptSettings, + setPromptsBulkActions, saveSettings, conversationsSettingsBulkActions, updatedAnonymizationData, @@ -100,13 +105,32 @@ export const AssistantSettingsManagement: React.FC = React.memo( anonymizationFieldsBulkActions, setAnonymizationFieldsBulkActions, setUpdatedAnonymizationData, + setUpdatedSystemPromptSettings, + promptsBulkActions, resetSettings, } = useSettingsUpdater( conversations, + allPrompts, conversationsLoaded, anonymizationFields ?? { page: 0, perPage: 0, total: 0, data: [] } ); + const quickPrompts = useMemo( + () => + quickPromptSettings.length === 0 + ? allPrompts.data.filter((p) => p.promptType === PromptTypeEnum.quick) + : quickPromptSettings, + [allPrompts.data, quickPromptSettings] + ); + + const systemPrompts = useMemo( + () => + systemPromptSettings.length === 0 + ? allPrompts.data.filter((p) => p.promptType === PromptTypeEnum.system) + : systemPromptSettings, + [allPrompts.data, systemPromptSettings] + ); + // Local state for saving previously selected items so tab switching is friendlier // Conversation Selection State const [selectedConversation, setSelectedConversation] = useState( @@ -136,21 +160,21 @@ export const AssistantSettingsManagement: React.FC = React.memo( }, [selectedSettingsTab, setSelectedSettingsTab]); // Quick Prompt Selection State - const [selectedQuickPrompt, setSelectedQuickPrompt] = useState(); - const onHandleSelectedQuickPromptChange = useCallback((quickPrompt?: QuickPrompt) => { + const [selectedQuickPrompt, setSelectedQuickPrompt] = useState(); + const onHandleSelectedQuickPromptChange = useCallback((quickPrompt?: PromptResponse) => { setSelectedQuickPrompt(quickPrompt); }, []); useEffect(() => { if (selectedQuickPrompt != null) { setSelectedQuickPrompt( - quickPromptSettings.find((q) => q.title === selectedQuickPrompt.title) + quickPromptSettings.find((q) => q.name === selectedQuickPrompt.name) ); } }, [quickPromptSettings, selectedQuickPrompt]); // System Prompt Selection State - const [selectedSystemPrompt, setSelectedSystemPrompt] = useState(); - const onHandleSelectedSystemPromptChange = useCallback((systemPrompt?: Prompt) => { + const [selectedSystemPrompt, setSelectedSystemPrompt] = useState(); + const onHandleSelectedSystemPromptChange = useCallback((systemPrompt?: PromptResponse) => { setSelectedSystemPrompt(systemPrompt); }, []); useEffect(() => { @@ -303,7 +327,9 @@ export const AssistantSettingsManagement: React.FC = React.memo( setConversationSettings={setConversationSettings} setConversationsSettingsBulkActions={setConversationsSettingsBulkActions} setUpdatedSystemPromptSettings={setUpdatedSystemPromptSettings} - systemPromptSettings={systemPromptSettings} + systemPromptSettings={systemPrompts} + promptsBulkActions={promptsBulkActions} + setPromptsBulkActions={setPromptsBulkActions} /> )} {selectedSettingsTab === QUICK_PROMPTS_TAB && ( @@ -311,10 +337,12 @@ export const AssistantSettingsManagement: React.FC = React.memo( handleSave={handleSave} onCancelClick={onCancelClick} onSelectedQuickPromptChange={onHandleSelectedQuickPromptChange} - quickPromptSettings={quickPromptSettings} + quickPromptSettings={quickPrompts} resetSettings={resetSettings} selectedQuickPrompt={selectedQuickPrompt} setUpdatedQuickPromptSettings={setUpdatedQuickPromptSettings} + promptsBulkActions={promptsBulkActions} + setPromptsBulkActions={setPromptsBulkActions} /> )} {selectedSettingsTab === ANONYMIZATION_TAB && ( diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/use_settings_updater/use_settings_updater.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/use_settings_updater/use_settings_updater.test.tsx index 20e5c86ddd251..0a2c72ba80ac4 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/use_settings_updater/use_settings_updater.test.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/use_settings_updater/use_settings_updater.test.tsx @@ -9,13 +9,9 @@ import { act, renderHook } from '@testing-library/react-hooks'; import { DEFAULT_LATEST_ALERTS } from '../../../assistant_context/constants'; import { alertConvo, welcomeConvo } from '../../../mock/conversation'; import { useSettingsUpdater } from './use_settings_updater'; -import { Prompt } from '../../../..'; -import { - defaultSystemPrompt, - mockSuperheroSystemPrompt, - mockSystemPrompt, -} from '../../../mock/system_prompt'; +import { defaultQuickPrompt, mockSystemPrompt } from '../../../mock/system_prompt'; import { HttpSetup } from '@kbn/core/public'; +import { PromptResponse } from '@kbn/elastic-assistant-common'; const mockConversations = { [alertConvo.title]: alertConvo, @@ -27,8 +23,8 @@ const mockHttp = { fetch: jest.fn(), } as unknown as HttpSetup; -const mockSystemPrompts: Prompt[] = [mockSystemPrompt]; -const mockQuickPrompts: Prompt[] = [defaultSystemPrompt]; +const mockSystemPrompts: PromptResponse[] = [mockSystemPrompt]; +const mockQuickPrompts: PromptResponse[] = [defaultQuickPrompt]; const anonymizationFields = { total: 2, @@ -40,8 +36,6 @@ const anonymizationFields = { ], }; -const setAllQuickPromptsMock = jest.fn(); -const setAllSystemPromptsMock = jest.fn(); const setAssistantStreamingEnabled = jest.fn(); const setKnowledgeBaseMock = jest.fn(); const reportAssistantSettingToggled = jest.fn(); @@ -58,8 +52,6 @@ const mockValues = { latestAlerts: DEFAULT_LATEST_ALERTS, }, baseConversations: {}, - setAllQuickPrompts: setAllQuickPromptsMock, - setAllSystemPrompts: setAllSystemPromptsMock, setKnowledgeBase: setKnowledgeBaseMock, http: mockHttp, anonymizationFieldsBulkActions: {}, @@ -67,8 +59,18 @@ const mockValues = { const updatedValues = { conversations: { ...mockConversations }, - allSystemPrompts: [mockSuperheroSystemPrompt], - allQuickPrompts: [{ title: 'Prompt 2', prompt: 'Prompt 2', color: 'red' }], + allSystemPrompts: [mockSystemPrompt], + allQuickPrompts: [ + { + consumer: 'securitySolutionUI', + content: + 'You are a helpful, expert assistant who answers questions about Elastic Security. Do not answer questions unrelated to Elastic Security.\nIf you answer a question related to KQL or EQL, it should be immediately usable within an Elastic Security timeline; please always format the output correctly with back ticks. Any answer provided for Query DSL should also be usable in a security timeline. This means you should only ever include the "filter" portion of the query.\nUse the following context to answer questions:', + id: 'default-system-prompt', + name: 'Default system prompt', + promptType: 'quick', + color: 'red', + }, + ], updatedAnonymizationData: { total: 2, page: 1, @@ -101,23 +103,31 @@ describe('useSettingsUpdater', () => { it('should set all state variables to their initial values when resetSettings is called', async () => { await act(async () => { const { result, waitForNextUpdate } = renderHook(() => - useSettingsUpdater(mockConversations, conversationsLoaded, anonymizationFields) + useSettingsUpdater( + mockConversations, + { + data: [...mockSystemPrompts, ...mockQuickPrompts], + page: 1, + perPage: 100, + total: 10, + }, + conversationsLoaded, + anonymizationFields + ) ); await waitForNextUpdate(); const { setConversationSettings, setConversationsSettingsBulkActions, - setUpdatedQuickPromptSettings, - setUpdatedSystemPromptSettings, setUpdatedKnowledgeBaseSettings, setUpdatedAssistantStreamingEnabled, resetSettings, + setPromptsBulkActions, } = result.current; setConversationSettings(updatedValues.conversations); setConversationsSettingsBulkActions({}); - setUpdatedQuickPromptSettings(updatedValues.allQuickPrompts); - setUpdatedSystemPromptSettings(updatedValues.allSystemPrompts); + setPromptsBulkActions({}); setUpdatedAnonymizationData(updatedValues.updatedAnonymizationData); setUpdatedKnowledgeBaseSettings(updatedValues.knowledgeBase); setUpdatedAssistantStreamingEnabled(updatedValues.assistantStreamingEnabled); @@ -149,23 +159,31 @@ describe('useSettingsUpdater', () => { it('should update all state variables to their updated values when saveSettings is called', async () => { await act(async () => { const { result, waitForNextUpdate } = renderHook(() => - useSettingsUpdater(mockConversations, conversationsLoaded, anonymizationFields) + useSettingsUpdater( + mockConversations, + { + data: mockSystemPrompts, + page: 1, + perPage: 100, + total: 10, + }, + conversationsLoaded, + anonymizationFields + ) ); await waitForNextUpdate(); const { setConversationSettings, setConversationsSettingsBulkActions, - setUpdatedQuickPromptSettings, - setUpdatedSystemPromptSettings, setAnonymizationFieldsBulkActions, setUpdatedKnowledgeBaseSettings, + setPromptsBulkActions, } = result.current; setConversationSettings(updatedValues.conversations); setConversationsSettingsBulkActions({ delete: { ids: ['1'] } }); setAnonymizationFieldsBulkActions({ delete: { ids: ['1'] } }); - setUpdatedQuickPromptSettings(updatedValues.allQuickPrompts); - setUpdatedSystemPromptSettings(updatedValues.allSystemPrompts); + setPromptsBulkActions({}); setUpdatedAnonymizationData(updatedValues.updatedAnonymizationData); setUpdatedKnowledgeBaseSettings(updatedValues.knowledgeBase); @@ -179,8 +197,6 @@ describe('useSettingsUpdater', () => { body: '{"delete":{"ids":["1"]}}', } ); - expect(setAllQuickPromptsMock).toHaveBeenCalledWith(updatedValues.allQuickPrompts); - expect(setAllSystemPromptsMock).toHaveBeenCalledWith(updatedValues.allSystemPrompts); expect(setUpdatedAnonymizationData).toHaveBeenCalledWith( updatedValues.updatedAnonymizationData ); @@ -190,7 +206,17 @@ describe('useSettingsUpdater', () => { it('should track which toggles have been updated when saveSettings is called', async () => { await act(async () => { const { result, waitForNextUpdate } = renderHook(() => - useSettingsUpdater(mockConversations, conversationsLoaded, anonymizationFields) + useSettingsUpdater( + mockConversations, + { + data: mockSystemPrompts, + page: 1, + perPage: 100, + total: 10, + }, + conversationsLoaded, + anonymizationFields + ) ); await waitForNextUpdate(); const { setUpdatedKnowledgeBaseSettings } = result.current; @@ -207,7 +233,17 @@ describe('useSettingsUpdater', () => { it('should track only toggles that updated', async () => { await act(async () => { const { result, waitForNextUpdate } = renderHook(() => - useSettingsUpdater(mockConversations, conversationsLoaded, anonymizationFields) + useSettingsUpdater( + mockConversations, + { + data: mockSystemPrompts, + page: 1, + perPage: 100, + total: 10, + }, + conversationsLoaded, + anonymizationFields + ) ); await waitForNextUpdate(); const { setUpdatedKnowledgeBaseSettings } = result.current; @@ -225,7 +261,17 @@ describe('useSettingsUpdater', () => { it('if no toggles update, do not track anything', async () => { await act(async () => { const { result, waitForNextUpdate } = renderHook(() => - useSettingsUpdater(mockConversations, conversationsLoaded, anonymizationFields) + useSettingsUpdater( + mockConversations, + { + data: mockSystemPrompts, + page: 1, + perPage: 100, + total: 10, + }, + conversationsLoaded, + anonymizationFields + ) ); await waitForNextUpdate(); const { setUpdatedKnowledgeBaseSettings } = result.current; diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/use_settings_updater/use_settings_updater.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/use_settings_updater/use_settings_updater.tsx index c6cf81c4bf949..1ae1c9e5b1b73 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/use_settings_updater/use_settings_updater.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/use_settings_updater/use_settings_updater.tsx @@ -8,7 +8,13 @@ import React, { useCallback, useEffect, useState } from 'react'; import { FindAnonymizationFieldsResponse } from '@kbn/elastic-assistant-common/impl/schemas/anonymization_fields/find_anonymization_fields_route.gen'; import { PerformBulkActionRequestBody } from '@kbn/elastic-assistant-common/impl/schemas/anonymization_fields/bulk_crud_anonymization_fields_route.gen'; -import { Conversation, Prompt, QuickPrompt } from '../../../..'; +import { + PerformBulkActionRequestBody as PromptsPerformBulkActionRequestBody, + PromptResponse, + PromptTypeEnum, +} from '@kbn/elastic-assistant-common/impl/schemas/prompts/bulk_crud_prompts_route.gen'; +import { FindPromptsResponse } from '@kbn/elastic-assistant-common/impl/schemas/prompts/find_prompts_route.gen'; +import { Conversation } from '../../../..'; import { useAssistantContext } from '../../../assistant_context'; import type { KnowledgeBaseConfig } from '../../types'; import { @@ -16,6 +22,7 @@ import { bulkUpdateConversations, } from '../../api/conversations/bulk_update_actions_conversations'; import { bulkUpdateAnonymizationFields } from '../../api/anonymization_fields/bulk_update_anonymization_fields'; +import { bulkUpdatePrompts } from '../../api/prompts/bulk_update_prompts'; interface UseSettingsUpdater { assistantStreamingEnabled: boolean; @@ -23,9 +30,9 @@ interface UseSettingsUpdater { conversationsSettingsBulkActions: ConversationsBulkActions; updatedAnonymizationData: FindAnonymizationFieldsResponse; knowledgeBase: KnowledgeBaseConfig; - quickPromptSettings: QuickPrompt[]; + quickPromptSettings: PromptResponse[]; resetSettings: () => void; - systemPromptSettings: Prompt[]; + systemPromptSettings: PromptResponse[]; setUpdatedAnonymizationData: React.Dispatch< React.SetStateAction >; @@ -37,26 +44,25 @@ interface UseSettingsUpdater { setAnonymizationFieldsBulkActions: React.Dispatch< React.SetStateAction >; + promptsBulkActions: PromptsPerformBulkActionRequestBody; + setPromptsBulkActions: React.Dispatch>; setUpdatedKnowledgeBaseSettings: React.Dispatch>; - setUpdatedQuickPromptSettings: React.Dispatch>; - setUpdatedSystemPromptSettings: React.Dispatch>; + setUpdatedQuickPromptSettings: React.Dispatch>; + setUpdatedSystemPromptSettings: React.Dispatch>; setUpdatedAssistantStreamingEnabled: React.Dispatch>; saveSettings: () => Promise; } export const useSettingsUpdater = ( conversations: Record, + allPrompts: FindPromptsResponse, conversationsLoaded: boolean, anonymizationFields: FindAnonymizationFieldsResponse ): UseSettingsUpdater => { // Initial state from assistant context const { - allQuickPrompts, - allSystemPrompts, assistantTelemetry, knowledgeBase, - setAllQuickPrompts, - setAllSystemPrompts, assistantStreamingEnabled, setAssistantStreamingEnabled, setKnowledgeBase, @@ -73,14 +79,20 @@ export const useSettingsUpdater = ( const [conversationsSettingsBulkActions, setConversationsSettingsBulkActions] = useState({}); // Quick Prompts - const [updatedQuickPromptSettings, setUpdatedQuickPromptSettings] = - useState(allQuickPrompts); + const [quickPromptSettings, setUpdatedQuickPromptSettings] = useState( + allPrompts.data.filter((p) => p.promptType === PromptTypeEnum.quick) + ); // System Prompts - const [updatedSystemPromptSettings, setUpdatedSystemPromptSettings] = - useState(allSystemPrompts); + const [systemPromptSettings, setUpdatedSystemPromptSettings] = useState( + allPrompts.data.filter((p) => p.promptType === PromptTypeEnum.system) + ); // Anonymization const [anonymizationFieldsBulkActions, setAnonymizationFieldsBulkActions] = useState({}); + // Prompts + const [promptsBulkActions, setPromptsBulkActions] = useState( + {} + ); const [updatedAnonymizationData, setUpdatedAnonymizationData] = useState(anonymizationFields); const [updatedAssistantStreamingEnabled, setUpdatedAssistantStreamingEnabled] = @@ -95,31 +107,57 @@ export const useSettingsUpdater = ( const resetSettings = useCallback((): void => { setConversationSettings(conversations); setConversationsSettingsBulkActions({}); - setUpdatedQuickPromptSettings(allQuickPrompts); + setUpdatedQuickPromptSettings( + allPrompts.data.filter((p) => p.promptType === PromptTypeEnum.quick) + ); setUpdatedKnowledgeBaseSettings(knowledgeBase); setUpdatedAssistantStreamingEnabled(assistantStreamingEnabled); - setUpdatedSystemPromptSettings(allSystemPrompts); + setUpdatedSystemPromptSettings( + allPrompts.data.filter((p) => p.promptType === PromptTypeEnum.system) + ); setUpdatedAnonymizationData(anonymizationFields); - }, [ - allQuickPrompts, - allSystemPrompts, - anonymizationFields, - assistantStreamingEnabled, - conversations, - knowledgeBase, - ]); + }, [allPrompts, anonymizationFields, assistantStreamingEnabled, conversations, knowledgeBase]); + + const hasBulkConversations = + conversationsSettingsBulkActions.create || + conversationsSettingsBulkActions.update || + conversationsSettingsBulkActions.delete; + + const hasBulkAnonymizationFields = + anonymizationFieldsBulkActions.create || + anonymizationFieldsBulkActions.update || + anonymizationFieldsBulkActions.delete; + const hasBulkPrompts = + promptsBulkActions.create || promptsBulkActions.update || promptsBulkActions.delete; /** * Save all pending settings */ const saveSettings = useCallback(async (): Promise => { - setAllQuickPrompts(updatedQuickPromptSettings); - setAllSystemPrompts(updatedSystemPromptSettings); + const bulkPromptsResult = hasBulkPrompts + ? await bulkUpdatePrompts(http, promptsBulkActions, toasts) + : undefined; + + // replace conversation references for created + if (bulkPromptsResult) { + bulkPromptsResult.attributes.results.created.forEach((p) => { + if (conversationsSettingsBulkActions.create) { + Object.values(conversationsSettingsBulkActions.create).forEach((c) => { + if (c.apiConfig?.defaultSystemPromptId === p.name) { + c.apiConfig.defaultSystemPromptId = p.id; + } + }); + } + if (conversationsSettingsBulkActions.update) { + Object.values(conversationsSettingsBulkActions.update).forEach((c) => { + if (c.apiConfig?.defaultSystemPromptId === p.name) { + c.apiConfig.defaultSystemPromptId = p.id; + } + }); + } + }); + } - const hasBulkConversations = - conversationsSettingsBulkActions.create || - conversationsSettingsBulkActions.update || - conversationsSettingsBulkActions.delete; const bulkResult = hasBulkConversations ? await bulkUpdateConversations(http, conversationsSettingsBulkActions, toasts) : undefined; @@ -145,21 +183,20 @@ export const useSettingsUpdater = ( } setAssistantStreamingEnabled(updatedAssistantStreamingEnabled); setKnowledgeBase(updatedKnowledgeBaseSettings); - const hasBulkAnonymizationFields = - anonymizationFieldsBulkActions.create || - anonymizationFieldsBulkActions.update || - anonymizationFieldsBulkActions.delete; + const bulkAnonymizationFieldsResult = hasBulkAnonymizationFields ? await bulkUpdateAnonymizationFields(http, anonymizationFieldsBulkActions, toasts) : undefined; - return (bulkResult?.success ?? true) && (bulkAnonymizationFieldsResult?.success ?? true); + + return ( + (bulkResult?.success ?? true) && + (bulkAnonymizationFieldsResult?.success ?? true) && + (bulkPromptsResult?.success ?? true) + ); }, [ - setAllQuickPrompts, - updatedQuickPromptSettings, - setAllSystemPrompts, - updatedSystemPromptSettings, - conversationsSettingsBulkActions, + hasBulkConversations, http, + conversationsSettingsBulkActions, toasts, knowledgeBase.isEnabledKnowledgeBase, knowledgeBase.isEnabledRAGAlerts, @@ -168,7 +205,10 @@ export const useSettingsUpdater = ( updatedAssistantStreamingEnabled, setAssistantStreamingEnabled, setKnowledgeBase, + hasBulkAnonymizationFields, anonymizationFieldsBulkActions, + hasBulkPrompts, + promptsBulkActions, assistantTelemetry, ]); @@ -200,9 +240,9 @@ export const useSettingsUpdater = ( conversationsSettingsBulkActions, knowledgeBase: updatedKnowledgeBaseSettings, assistantStreamingEnabled: updatedAssistantStreamingEnabled, - quickPromptSettings: updatedQuickPromptSettings, + quickPromptSettings, resetSettings, - systemPromptSettings: updatedSystemPromptSettings, + systemPromptSettings, saveSettings, updatedAnonymizationData, setUpdatedAnonymizationData, @@ -214,5 +254,7 @@ export const useSettingsUpdater = ( setUpdatedSystemPromptSettings, setConversationSettings, setConversationsSettingsBulkActions, + promptsBulkActions, + setPromptsBulkActions, }; }; diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/types.ts b/x-pack/packages/kbn-elastic-assistant/impl/assistant/types.ts index 91ee3468a12d9..587be76910c3e 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/types.ts +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/types.ts @@ -5,19 +5,6 @@ * 2.0. */ -export type PromptType = 'system' | 'user'; - -export interface Prompt { - id: string; - content: string; - name: string; - promptType: PromptType; - isDefault?: boolean; // TODO: Should be renamed to isImmutable as this flag is used to prevent users from deleting prompts - isNewConversationDefault?: boolean; - isFlyoutMode?: boolean; - label?: string; -} - export interface KnowledgeBaseConfig { isEnabledRAGAlerts: boolean; isEnabledKnowledgeBase: boolean; diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_conversation/helpers.test.ts b/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_conversation/helpers.test.ts index c8c8ab5ff7727..3e997fef5d573 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_conversation/helpers.test.ts +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_conversation/helpers.test.ts @@ -13,7 +13,8 @@ import { getDefaultSystemPrompt, } from './helpers'; import { AIConnector } from '../../connectorland/connector_selector'; -import { Conversation, Prompt } from '../../..'; +import { Conversation } from '../../..'; +import { PromptResponse } from '@kbn/elastic-assistant-common'; const tilde = '`'; const codeDelimiter = '```'; @@ -61,28 +62,28 @@ ${codeDelimiter} This query will filter the events based on the condition that the ${tilde}user.name${tilde} field should exactly match the value \"9dcc9960-78cf-4ef6-9a2e-dbd5816daa60\".`; describe('useConversation helpers', () => { - const allSystemPrompts: Prompt[] = [ + const allSystemPrompts: PromptResponse[] = [ { id: '1', content: 'Prompt 1', name: 'Prompt 1', - promptType: 'user', + promptType: 'quick', }, { id: '2', content: 'Prompt 2', name: 'Prompt 2', - promptType: 'user', + promptType: 'quick', isNewConversationDefault: true, }, { id: '3', content: 'Prompt 3', name: 'Prompt 3', - promptType: 'user', + promptType: 'quick', }, ]; - const allSystemPromptsNoDefault: Prompt[] = allSystemPrompts.filter( + const allSystemPromptsNoDefault: PromptResponse[] = allSystemPrompts.filter( ({ isNewConversationDefault }) => isNewConversationDefault !== true ); @@ -237,25 +238,25 @@ describe('useConversation helpers', () => { }); describe('getConversationApiConfig', () => { - const allSystemPrompts: Prompt[] = [ + const allSystemPrompts: PromptResponse[] = [ { id: '1', content: 'Prompt 1', name: 'Prompt 1', - promptType: 'user', + promptType: 'quick', }, { id: '2', content: 'Prompt 2', name: 'Prompt 2', - promptType: 'user', + promptType: 'quick', isNewConversationDefault: true, }, { id: '3', content: 'Prompt 3', name: 'Prompt 3', - promptType: 'user', + promptType: 'quick', }, ]; @@ -390,7 +391,7 @@ describe('getConversationApiConfig', () => { }); test('should return the first system prompt if both conversation system prompt and default new system prompt do not exist', () => { - const allSystemPromptsNoDefault: Prompt[] = allSystemPrompts.filter( + const allSystemPromptsNoDefault: PromptResponse[] = allSystemPrompts.filter( ({ isNewConversationDefault }) => isNewConversationDefault !== true ); @@ -418,7 +419,7 @@ describe('getConversationApiConfig', () => { }); test('should return the first system prompt if conversation system prompt does not exist within all system prompts', () => { - const allSystemPromptsNoDefault: Prompt[] = allSystemPrompts.filter( + const allSystemPromptsNoDefault: PromptResponse[] = allSystemPrompts.filter( ({ isNewConversationDefault }) => isNewConversationDefault !== true ); diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_conversation/helpers.ts b/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_conversation/helpers.ts index 2d6c4075fba0e..fde1c1d3d943c 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_conversation/helpers.ts +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_conversation/helpers.ts @@ -6,7 +6,7 @@ */ import React from 'react'; -import { Prompt } from '../types'; +import { PromptResponse } from '@kbn/elastic-assistant-common'; import { Conversation } from '../../assistant_context/types'; import { AIConnector } from '../../connectorland/connector_selector'; import { getGenAiConfig } from '../../connectorland/helpers'; @@ -75,7 +75,7 @@ export const analyzeMarkdown = (markdown: string): CodeBlockDetails[] => { * * @param allSystemPrompts All available System Prompts */ -export const getDefaultNewSystemPrompt = (allSystemPrompts: Prompt[]) => +export const getDefaultNewSystemPrompt = (allSystemPrompts: PromptResponse[]) => allSystemPrompts.find((prompt) => prompt.isNewConversationDefault) ?? allSystemPrompts?.[0]; /** @@ -88,15 +88,15 @@ export const getDefaultSystemPrompt = ({ allSystemPrompts, conversation, }: { - allSystemPrompts: Prompt[]; + allSystemPrompts: PromptResponse[]; conversation: Conversation | undefined; -}): Prompt | undefined => { +}): PromptResponse | undefined => { const conversationSystemPrompt = allSystemPrompts.find( (prompt) => prompt.id === conversation?.apiConfig?.defaultSystemPromptId ); const defaultNewSystemPrompt = getDefaultNewSystemPrompt(allSystemPrompts); - return conversationSystemPrompt ?? defaultNewSystemPrompt; + return conversationSystemPrompt?.id ? conversationSystemPrompt : defaultNewSystemPrompt; }; /** @@ -109,9 +109,9 @@ export const getInitialDefaultSystemPrompt = ({ allSystemPrompts, conversation, }: { - allSystemPrompts: Prompt[]; + allSystemPrompts: PromptResponse[]; conversation: Conversation | undefined; -}): Prompt | undefined => { +}): PromptResponse | undefined => { const conversationSystemPrompt = allSystemPrompts.find( (prompt) => prompt.id === conversation?.apiConfig?.defaultSystemPromptId ); @@ -133,7 +133,7 @@ export const getConversationApiConfig = ({ connectors, defaultConnector, }: { - allSystemPrompts: Prompt[]; + allSystemPrompts: PromptResponse[]; conversation: Conversation; connectors?: AIConnector[]; defaultConnector?: AIConnector; diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_conversation/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_conversation/index.tsx index 84fa21417ae70..a276aea3ff4ab 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_conversation/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_conversation/index.tsx @@ -18,6 +18,7 @@ import { updateConversation, } from '../api/conversations'; import { WELCOME_CONVERSATION } from './sample_conversations'; +import { useFetchPrompts } from '../api/prompts/use_fetch_prompts'; export const DEFAULT_CONVERSATION_STATE: Conversation = { id: '', @@ -63,7 +64,10 @@ interface UseConversation { } export const useConversation = (): UseConversation => { - const { allSystemPrompts, http, toasts } = useAssistantContext(); + const { http, toasts } = useAssistantContext(); + const { + data: { data: allPrompts }, + } = useFetchPrompts(); const getConversation = useCallback( async (conversationId: string, silent?: boolean) => { @@ -101,7 +105,7 @@ export const useConversation = (): UseConversation => { async (conversation: Conversation) => { if (conversation.apiConfig) { const defaultSystemPromptId = getDefaultSystemPrompt({ - allSystemPrompts, + allSystemPrompts: allPrompts, conversation, })?.id; @@ -115,7 +119,7 @@ export const useConversation = (): UseConversation => { }); } }, - [allSystemPrompts, http, toasts] + [allPrompts, http, toasts] ); /** diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant_context/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant_context/index.tsx index ffafb4f704a17..78336f8a8b03d 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant_context/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant_context/index.tsx @@ -25,17 +25,13 @@ import type { Conversation } from './types'; import { DEFAULT_ASSISTANT_TITLE } from '../assistant/translations'; import { CodeBlockDetails } from '../assistant/use_conversation/helpers'; import { PromptContextTemplate } from '../assistant/prompt_context/types'; -import { QuickPrompt } from '../assistant/quick_prompts/types'; -import { KnowledgeBaseConfig, Prompt, TraceOptions } from '../assistant/types'; -import { BASE_SYSTEM_PROMPTS } from '../content/prompts/system'; +import { KnowledgeBaseConfig, TraceOptions } from '../assistant/types'; import { DEFAULT_ASSISTANT_NAMESPACE, DEFAULT_KNOWLEDGE_BASE_SETTINGS, KNOWLEDGE_BASE_LOCAL_STORAGE_KEY, LAST_CONVERSATION_ID_LOCAL_STORAGE_KEY, - QUICK_PROMPT_LOCAL_STORAGE_KEY, STREAMING_LOCAL_STORAGE_KEY, - SYSTEM_PROMPT_LOCAL_STORAGE_KEY, TRACE_OPTIONS_SESSION_STORAGE_KEY, } from './constants'; import { AssistantAvailability, AssistantTelemetry } from './types'; @@ -65,8 +61,6 @@ export interface AssistantProviderProps { ) => CodeBlockDetails[][]; basePath: string; basePromptContexts?: PromptContextTemplate[]; - baseQuickPrompts?: QuickPrompt[]; - baseSystemPrompts?: Prompt[]; docLinks: Omit; children: React.ReactNode; getComments: (commentArgs: { @@ -87,6 +81,7 @@ export interface AssistantProviderProps { navigateToApp: (appId: string, options?: NavigateToAppOptions | undefined) => Promise; title?: string; toasts?: IToasts; + currentAppId: string; } export interface UserAvatar { @@ -106,13 +101,8 @@ export interface UseAssistantContext { currentConversation: Conversation, showAnonymizedValues: boolean ) => CodeBlockDetails[][]; - allQuickPrompts: QuickPrompt[]; - allSystemPrompts: Prompt[]; docLinks: Omit; basePath: string; - basePromptContexts: PromptContextTemplate[]; - baseQuickPrompts: QuickPrompt[]; - baseSystemPrompts: Prompt[]; baseConversations: Record; getComments: (commentArgs: { abortStream: () => void; @@ -134,8 +124,6 @@ export interface UseAssistantContext { nameSpace: string; registerPromptContext: RegisterPromptContext; selectedSettingsTab: SettingsTabs | null; - setAllQuickPrompts: React.Dispatch>; - setAllSystemPrompts: React.Dispatch>; setAssistantStreamingEnabled: React.Dispatch>; setKnowledgeBase: React.Dispatch>; setLastConversationId: React.Dispatch>; @@ -150,7 +138,9 @@ export interface UseAssistantContext { title: string; toasts: IToasts | undefined; traceOptions: TraceOptions; + basePromptContexts: PromptContextTemplate[]; unRegisterPromptContext: UnRegisterPromptContext; + currentAppId: string; } const AssistantContext = React.createContext(undefined); @@ -164,8 +154,6 @@ export const AssistantProvider: React.FC = ({ docLinks, basePath, basePromptContexts = [], - baseQuickPrompts = [], - baseSystemPrompts = BASE_SYSTEM_PROMPTS, children, getComments, http, @@ -174,6 +162,7 @@ export const AssistantProvider: React.FC = ({ nameSpace = DEFAULT_ASSISTANT_NAMESPACE, title = DEFAULT_ASSISTANT_TITLE, toasts, + currentAppId, }) => { /** * Session storage for traceOptions, including APM URL and LangSmith Project/API Key @@ -189,22 +178,6 @@ export const AssistantProvider: React.FC = ({ defaultTraceOptions ); - /** - * Local storage for all quick prompts, prefixed by assistant nameSpace - */ - const [localStorageQuickPrompts, setLocalStorageQuickPrompts] = useLocalStorage( - `${nameSpace}.${QUICK_PROMPT_LOCAL_STORAGE_KEY}`, - baseQuickPrompts - ); - - /** - * Local storage for all system prompts, prefixed by assistant nameSpace - */ - const [localStorageSystemPrompts, setLocalStorageSystemPrompts] = useLocalStorage( - `${nameSpace}.${SYSTEM_PROMPT_LOCAL_STORAGE_KEY}`, - baseSystemPrompts - ); - const [localStorageLastConversationId, setLocalStorageLastConversationId] = useLocalStorage(`${nameSpace}.${LAST_CONVERSATION_ID_LOCAL_STORAGE_KEY}`); @@ -290,12 +263,8 @@ export const AssistantProvider: React.FC = ({ assistantFeatures: assistantFeatures ?? defaultAssistantFeatures, assistantTelemetry, augmentMessageCodeBlocks, - allQuickPrompts: localStorageQuickPrompts ?? [], - allSystemPrompts: localStorageSystemPrompts ?? [], basePath, basePromptContexts, - baseQuickPrompts, - baseSystemPrompts, docLinks, getComments, http, @@ -308,8 +277,6 @@ export const AssistantProvider: React.FC = ({ // can be undefined from localStorage, if not defined, default to true assistantStreamingEnabled: localStorageStreaming ?? true, setAssistantStreamingEnabled: setLocalStorageStreaming, - setAllQuickPrompts: setLocalStorageQuickPrompts, - setAllSystemPrompts: setLocalStorageSystemPrompts, setKnowledgeBase: setLocalStorageKnowledgeBase, setSelectedSettingsTab, setShowAssistantOverlay, @@ -322,6 +289,7 @@ export const AssistantProvider: React.FC = ({ getLastConversationId, setLastConversationId: setLocalStorageLastConversationId, baseConversations, + currentAppId, }), [ actionTypeRegistry, @@ -330,12 +298,8 @@ export const AssistantProvider: React.FC = ({ assistantFeatures, assistantTelemetry, augmentMessageCodeBlocks, - localStorageQuickPrompts, - localStorageSystemPrompts, basePath, basePromptContexts, - baseQuickPrompts, - baseSystemPrompts, docLinks, getComments, http, @@ -347,8 +311,6 @@ export const AssistantProvider: React.FC = ({ selectedSettingsTab, localStorageStreaming, setLocalStorageStreaming, - setLocalStorageQuickPrompts, - setLocalStorageSystemPrompts, setLocalStorageKnowledgeBase, setSessionStorageTraceOptions, showAssistantOverlay, @@ -359,6 +321,7 @@ export const AssistantProvider: React.FC = ({ getLastConversationId, setLocalStorageLastConversationId, baseConversations, + currentAppId, ] ); diff --git a/x-pack/packages/kbn-elastic-assistant/impl/content/prompts/system/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/content/prompts/system/index.tsx deleted file mode 100644 index a73fbf4854ef1..0000000000000 --- a/x-pack/packages/kbn-elastic-assistant/impl/content/prompts/system/index.tsx +++ /dev/null @@ -1,36 +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 { Prompt } from '../../../..'; -import { - DEFAULT_SYSTEM_PROMPT_LABEL, - DEFAULT_SYSTEM_PROMPT_NAME, - DEFAULT_SYSTEM_PROMPT_NON_I18N, - SUPERHERO_SYSTEM_PROMPT_LABEL, - SUPERHERO_SYSTEM_PROMPT_NAME, - SUPERHERO_SYSTEM_PROMPT_NON_I18N, -} from './translations'; - -/** - * Base System Prompts for Elastic AI Assistant (if not overridden on initialization). - */ -export const BASE_SYSTEM_PROMPTS: Prompt[] = [ - { - id: 'default-system-prompt', - content: DEFAULT_SYSTEM_PROMPT_NON_I18N, - name: DEFAULT_SYSTEM_PROMPT_NAME, - promptType: 'system', - label: DEFAULT_SYSTEM_PROMPT_LABEL, - }, - { - id: 'CB9FA555-B59F-4F71-AFF9-8A891AC5BC28', - content: SUPERHERO_SYSTEM_PROMPT_NON_I18N, - name: SUPERHERO_SYSTEM_PROMPT_NAME, - promptType: 'system', - label: SUPERHERO_SYSTEM_PROMPT_LABEL, - }, -]; diff --git a/x-pack/packages/kbn-elastic-assistant/impl/content/prompts/user/translations.ts b/x-pack/packages/kbn-elastic-assistant/impl/content/prompts/user/translations.ts deleted file mode 100644 index 28cda1f9414a8..0000000000000 --- a/x-pack/packages/kbn-elastic-assistant/impl/content/prompts/user/translations.ts +++ /dev/null @@ -1,26 +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 { i18n } from '@kbn/i18n'; - -export const THEN_SUMMARIZE_SUGGESTED_KQL_AND_EQL_QUERIES = i18n.translate( - 'xpack.elasticAssistant.assistant.content.prompts.user.thenSummarizeSuggestedKqlAndEqlQueries', - { - defaultMessage: - 'Evaluate the event from the context above and format your output neatly in markdown syntax for my Elastic Security case.', - } -); - -export const FINALLY_SUGGEST_INVESTIGATION_GUIDE_AND_FORMAT_AS_MARKDOWN = i18n.translate( - 'xpack.elasticAssistant.assistant.content.prompts.user.finallySuggestInvestigationGuideAndFormatAsMarkdown', - { - defaultMessage: `Add your description, recommended actions and bulleted triage steps. Use the MITRE ATT&CK data provided to add more context and recommendations from MITRE, and hyperlink to the relevant pages on MITRE\'s website. Be sure to include the user and host risk score data from the context. Your response should include steps that point to Elastic Security specific features, including endpoint response actions, the Elastic Agent OSQuery manager integration (with example osquery queries), timelines and entity analytics and link to all the relevant Elastic Security documentation.`, - } -); - -export const EXPLAIN_THEN_SUMMARIZE_SUGGEST_INVESTIGATION_GUIDE_NON_I18N = `${THEN_SUMMARIZE_SUGGESTED_KQL_AND_EQL_QUERIES} -${FINALLY_SUGGEST_INVESTIGATION_GUIDE_AND_FORMAT_AS_MARKDOWN}`; diff --git a/x-pack/packages/kbn-elastic-assistant/impl/mock/quick_prompt.ts b/x-pack/packages/kbn-elastic-assistant/impl/mock/quick_prompt.ts index 14318f4b1b534..31fa9bb6508b8 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/mock/quick_prompt.ts +++ b/x-pack/packages/kbn-elastic-assistant/impl/mock/quick_prompt.ts @@ -5,52 +5,69 @@ * 2.0. */ -import { QuickPrompt } from '../..'; +import { + PromptResponse, + PromptTypeEnum, +} from '@kbn/elastic-assistant-common/impl/schemas/prompts/bulk_crud_prompts_route.gen'; -export const MOCK_QUICK_PROMPTS: QuickPrompt[] = [ +export const MOCK_QUICK_PROMPTS: PromptResponse[] = [ { - title: 'ALERT_SUMMARIZATION_TITLE', - prompt: 'ALERT_SUMMARIZATION_PROMPT', + name: 'ALERT_SUMMARIZATION_TITLE', + content: 'ALERT_SUMMARIZATION_PROMPT', color: '#F68FBE', categories: ['PROMPT_CONTEXT_ALERT_CATEGORY'], isDefault: true, + id: 'ALERT_SUMMARIZATION_TITLE', + promptType: PromptTypeEnum.quick, }, { - title: 'RULE_CREATION_TITLE', - prompt: 'RULE_CREATION_PROMPT', + name: 'RULE_CREATION_TITLE', + content: 'RULE_CREATION_PROMPT', categories: ['PROMPT_CONTEXT_DETECTION_RULES_CATEGORY'], color: '#7DDED8', isDefault: true, + id: 'RULE_CREATION_TITLE', + promptType: PromptTypeEnum.quick, }, { - title: 'WORKFLOW_ANALYSIS_TITLE', - prompt: 'WORKFLOW_ANALYSIS_PROMPT', + name: 'WORKFLOW_ANALYSIS_TITLE', + content: 'WORKFLOW_ANALYSIS_PROMPT', color: '#36A2EF', isDefault: true, + id: 'WORKFLOW_ANALYSIS_TITLE', + promptType: PromptTypeEnum.quick, }, { - title: 'THREAT_INVESTIGATION_GUIDES_TITLE', - prompt: 'THREAT_INVESTIGATION_GUIDES_PROMPT', + name: 'THREAT_INVESTIGATION_GUIDES_TITLE', + content: 'THREAT_INVESTIGATION_GUIDES_PROMPT', categories: ['PROMPT_CONTEXT_EVENT_CATEGORY'], color: '#F3D371', isDefault: true, + id: 'THREAT_INVESTIGATION_GUIDES_TITLE', + promptType: PromptTypeEnum.quick, }, { - title: 'SPL_QUERY_CONVERSION_TITLE', - prompt: 'SPL_QUERY_CONVERSION_PROMPT', + name: 'SPL_QUERY_CONVERSION_TITLE', + content: 'SPL_QUERY_CONVERSION_PROMPT', color: '#BADA55', isDefault: true, + id: 'SPL_QUERY_CONVERSION_TITLE', + promptType: PromptTypeEnum.quick, }, { - title: 'AUTOMATION_TITLE', - prompt: 'AUTOMATION_PROMPT', + name: 'AUTOMATION_TITLE', + content: 'AUTOMATION_PROMPT', color: '#FFA500', isDefault: true, + id: 'AUTOMATION_TITLE', + promptType: PromptTypeEnum.quick, }, { - title: 'A_CUSTOM_OPTION', - prompt: 'quickly prompt please', + name: 'A_CUSTOM_OPTION', + content: 'quickly prompt please', color: '#D36086', categories: [], + id: 'A_CUSTOM_OPTION', + promptType: PromptTypeEnum.quick, }, ]; diff --git a/x-pack/packages/kbn-elastic-assistant/impl/mock/system_prompt/index.ts b/x-pack/packages/kbn-elastic-assistant/impl/mock/system_prompt/index.ts index de23052d15564..04b027cbfe578 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/mock/system_prompt/index.ts +++ b/x-pack/packages/kbn-elastic-assistant/impl/mock/system_prompt/index.ts @@ -5,35 +5,47 @@ * 2.0. */ -import { Prompt } from '../../assistant/types'; +import { PromptResponse } from '@kbn/elastic-assistant-common'; -export const mockSystemPrompt: Prompt = { +export const mockSystemPrompt: PromptResponse = { id: 'mock-system-prompt-1', content: 'You are a helpful, expert assistant who answers questions about Elastic Security.', name: 'Mock system prompt', + consumer: 'securitySolutionUI', promptType: 'system', - isFlyoutMode: false, }; -export const mockSuperheroSystemPrompt: Prompt = { +export const mockSuperheroSystemPrompt: PromptResponse = { id: 'mock-superhero-system-prompt-1', content: `You are a helpful, expert assistant who answers questions about Elastic Security. You have the personality of a mutant superhero who says "bub" a lot.`, name: 'Mock superhero system prompt', + consumer: 'securitySolutionUI', promptType: 'system', }; -export const defaultSystemPrompt: Prompt = { +export const defaultSystemPrompt: PromptResponse = { id: 'default-system-prompt', content: 'You are a helpful, expert assistant who answers questions about Elastic Security. Do not answer questions unrelated to Elastic Security.\nIf you answer a question related to KQL or EQL, it should be immediately usable within an Elastic Security timeline; please always format the output correctly with back ticks. Any answer provided for Query DSL should also be usable in a security timeline. This means you should only ever include the "filter" portion of the query.\nUse the following context to answer questions:', name: 'Default system prompt', promptType: 'system', + consumer: 'securitySolutionUI', isDefault: true, isNewConversationDefault: true, }; -export const mockSystemPrompts: Prompt[] = [ +export const defaultQuickPrompt: PromptResponse = { + id: 'default-system-prompt', + content: + 'You are a helpful, expert assistant who answers questions about Elastic Security. Do not answer questions unrelated to Elastic Security.\nIf you answer a question related to KQL or EQL, it should be immediately usable within an Elastic Security timeline; please always format the output correctly with back ticks. Any answer provided for Query DSL should also be usable in a security timeline. This means you should only ever include the "filter" portion of the query.\nUse the following context to answer questions:', + name: 'Default system prompt', + promptType: 'quick', + consumer: 'securitySolutionUI', + color: 'red', +}; + +export const mockSystemPrompts: PromptResponse[] = [ mockSystemPrompt, mockSuperheroSystemPrompt, defaultSystemPrompt, diff --git a/x-pack/packages/kbn-elastic-assistant/impl/mock/test_providers/test_providers.tsx b/x-pack/packages/kbn-elastic-assistant/impl/mock/test_providers/test_providers.tsx index 17e977fdbf80f..13e543a02b3b2 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/mock/test_providers/test_providers.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/mock/test_providers/test_providers.tsx @@ -81,6 +81,7 @@ export const TestProvidersComponent: React.FC = ({ baseConversations={{}} navigateToApp={mockNavigateToApp} {...providerContext} + currentAppId={'test'} > {children} diff --git a/x-pack/packages/kbn-elastic-assistant/impl/mock/user_prompt/index.ts b/x-pack/packages/kbn-elastic-assistant/impl/mock/user_prompt/index.ts index 5bc23b0d680e3..1f7c96126bc10 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/mock/user_prompt/index.ts +++ b/x-pack/packages/kbn-elastic-assistant/impl/mock/user_prompt/index.ts @@ -5,12 +5,12 @@ * 2.0. */ -import { Prompt } from '../../assistant/types'; +import { PromptResponse } from '@kbn/elastic-assistant-common'; -export const mockUserPrompt: Prompt = { +export const mockUserPrompt: PromptResponse = { id: 'mock-user-prompt-1', content: `Explain the meaning from the context above, then summarize a list of suggested Elasticsearch KQL and EQL queries. Finally, suggest an investigation guide, and format it as markdown.`, name: 'Mock user prompt', - promptType: 'user', + promptType: 'quick', }; diff --git a/x-pack/packages/kbn-elastic-assistant/index.ts b/x-pack/packages/kbn-elastic-assistant/index.ts index df0ba1e8db0f9..7cd882cd633b8 100644 --- a/x-pack/packages/kbn-elastic-assistant/index.ts +++ b/x-pack/packages/kbn-elastic-assistant/index.ts @@ -90,12 +90,6 @@ export { WELCOME_CONVERSATION_TITLE, } from './impl/assistant/use_conversation/translations'; -/** i18n translations of system prompts */ -export * as SYSTEM_PROMPTS from './impl/content/prompts/system/translations'; - -/** i18n translations of user prompts */ -export * as USER_PROMPTS from './impl/content/prompts/user/translations'; - export type { /** for rendering results in a code block */ CodeBlockDetails, @@ -114,9 +108,6 @@ export type { ClientMessage, } from './impl/assistant_context/types'; -/** Interface for defining system/user prompts */ -export type { Prompt } from './impl/assistant/types'; - /** * This interface is used to pass context to the assistant, * for the purpose of building prompts. Examples of context include: @@ -139,12 +130,6 @@ export type { PromptContext } from './impl/assistant/prompt_context/types'; */ export type { PromptContextTemplate } from './impl/assistant/prompt_context/types'; -/** - * This interface is used to pass a default or base set of Quick Prompts to the Elastic Assistant that - * can be displayed when corresponding PromptContext's are registered. - */ -export type { QuickPrompt } from './impl/assistant/quick_prompts/types'; - export { useFetchCurrentUserConversations } from './impl/assistant/api/conversations/use_fetch_current_user_conversations'; export * from './impl/assistant/api/conversations/bulk_update_actions_conversations'; export { getConversationById } from './impl/assistant/api/conversations/conversations'; @@ -152,4 +137,4 @@ export { getConversationById } from './impl/assistant/api/conversations/conversa export { mergeBaseWithPersistedConversations } from './impl/assistant/helpers'; export { UpgradeButtons } from './impl/upgrade/upgrade_buttons'; -export { getUserConversations } from './impl/assistant/api'; +export { getUserConversations, getPrompts, bulkUpdatePrompts } from './impl/assistant/api'; diff --git a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/mock/test_providers/test_providers.tsx b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/mock/test_providers/test_providers.tsx index a4e6d39e720ec..b4579dd4bd50c 100644 --- a/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/mock/test_providers/test_providers.tsx +++ b/x-pack/packages/security-solution/ecs_data_quality_dashboard/impl/data_quality/mock/test_providers/test_providers.tsx @@ -73,6 +73,7 @@ export const TestProvidersComponent: React.FC = ({ children, isILMAvailab http={mockHttp} baseConversations={{}} navigateToApp={mockNavigateToApp} + currentAppId={'securitySolutionUI'} > { id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', content: 'test content', name: 'test', - prompt_type: 'quickPrompt', - is_shared: false, + prompt_type: 'quick', + consumer: 'securitySolutionUI', + categories: [], + color: 'red', created_by: 'elastic', users: [ { @@ -62,15 +64,19 @@ export const getCreatePromptSchemaMock = (): PromptCreateProps => ({ name: 'test', content: 'test content', isNewConversationDefault: false, - isShared: true, + consumer: 'securitySolutionUI', + categories: [], + color: 'red', isDefault: false, - promptType: 'quickPrompt', + promptType: 'quick', }); export const getUpdatePromptSchemaMock = (promptId = 'prompt-1'): PromptUpdateProps => ({ content: 'test content', isNewConversationDefault: false, - isShared: true, + consumer: 'securitySolutionUI', + categories: [], + color: 'red', isDefault: false, id: promptId, }); @@ -79,7 +85,7 @@ export const getPromptMock = (params: PromptCreateProps | PromptUpdateProps): Pr id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', content: 'test content', name: 'test', - promptType: 'quickPrompt', + promptType: 'quick', isDefault: false, ...params, createdAt: '2019-12-13T16:40:33.400Z', @@ -97,19 +103,23 @@ export const getQueryPromptParams = (isUpdate?: boolean): PromptCreateProps | Pr ? { content: 'test 2', name: 'test', - promptType: 'quickPrompt', + promptType: 'quick', isDefault: false, isNewConversationDefault: true, - isShared: true, + consumer: 'securitySolutionUI', + categories: [], + color: 'red', id: '1', } : { content: 'test 2', name: 'test', - promptType: 'quickPrompt', + promptType: 'quick', isDefault: false, isNewConversationDefault: true, - isShared: true, + consumer: 'securitySolutionUI', + categories: [], + color: 'red', }; }; diff --git a/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/prompts/field_maps_configuration.ts b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/prompts/field_maps_configuration.ts index 50df573d01872..5a916793332b7 100644 --- a/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/prompts/field_maps_configuration.ts +++ b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/prompts/field_maps_configuration.ts @@ -23,11 +23,21 @@ export const assistantPromptsFieldMap: FieldMap = { array: false, required: false, }, - is_shared: { - type: 'boolean', + consumer: { + type: 'text', + array: false, + required: false, + }, + color: { + type: 'keyword', array: false, required: false, }, + categories: { + type: 'keyword', + array: true, + required: false, + }, is_new_conversation_default: { type: 'boolean', array: false, diff --git a/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/prompts/helpers.ts b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/prompts/helpers.ts index 83d7713c23f1f..a4534972c8478 100644 --- a/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/prompts/helpers.ts +++ b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/prompts/helpers.ts @@ -9,6 +9,7 @@ import { estypes } from '@elastic/elasticsearch'; import { PromptCreateProps, PromptResponse, + PromptType, PromptUpdateProps, } from '@kbn/elastic-assistant-common/impl/schemas/prompts/bulk_crud_prompts_route.gen'; import { AuthenticatedUser } from '@kbn/core-security-common'; @@ -31,8 +32,10 @@ export const transformESToPrompts = (response: EsPromptsSchema[]): PromptRespons namespace: promptSchema.namespace, id: promptSchema.id, name: promptSchema.name, - promptType: promptSchema.prompt_type, - isShared: promptSchema.is_shared, + promptType: promptSchema.prompt_type as unknown as PromptType, + color: promptSchema.color, + categories: promptSchema.categories, + consumer: promptSchema.consumer, createdBy: promptSchema.created_by, updatedBy: promptSchema.updated_by, }; @@ -65,8 +68,10 @@ export const transformESSearchToPrompts = ( // eslint-disable-next-line @typescript-eslint/no-non-null-assertion id: hit._id!, name: promptSchema.name, - promptType: promptSchema.prompt_type, - isShared: promptSchema.is_shared, + promptType: promptSchema.prompt_type as unknown as PromptType, + color: promptSchema.color, + categories: promptSchema.categories, + consumer: promptSchema.consumer, createdBy: promptSchema.created_by, updatedBy: promptSchema.updated_by, }; @@ -78,14 +83,15 @@ export const transformESSearchToPrompts = ( export const transformToUpdateScheme = ( user: AuthenticatedUser, updatedAt: string, - { content, isNewConversationDefault, isShared, id }: PromptUpdateProps + { content, isNewConversationDefault, categories, color, id }: PromptUpdateProps ): UpdatePromptSchema => { return { id, updated_at: updatedAt, content: content ?? '', is_new_conversation_default: isNewConversationDefault, - is_shared: isShared, + categories, + color, users: [ { id: user.profile_uid, @@ -98,13 +104,25 @@ export const transformToUpdateScheme = ( export const transformToCreateScheme = ( user: AuthenticatedUser, updatedAt: string, - { content, isDefault, isNewConversationDefault, isShared, name, promptType }: PromptCreateProps + { + content, + isDefault, + isNewConversationDefault, + categories, + color, + consumer, + name, + promptType, + }: PromptCreateProps ): CreatePromptSchema => { return { + '@timestamp': updatedAt, updated_at: updatedAt, content: content ?? '', is_new_conversation_default: isNewConversationDefault, - is_shared: isShared, + color, + consumer, + categories, name, is_default: isDefault, prompt_type: promptType, @@ -132,8 +150,11 @@ export const getUpdateScript = ({ if (params.assignEmpty == true || params.containsKey('is_new_conversation_default')) { ctx._source.is_new_conversation_default = params.is_new_conversation_default; } - if (params.assignEmpty == true || params.containsKey('is_shared')) { - ctx._source.is_shared = params.is_shared; + if (params.assignEmpty == true || params.containsKey('color')) { + ctx._source.color = params.color; + } + if (params.assignEmpty == true || params.containsKey('categories')) { + ctx._source.categories = params.categories; } ctx._source.updated_at = params.updated_at; `, diff --git a/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/prompts/types.ts b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/prompts/types.ts index 91f52fb3a0829..0d936cc852aca 100644 --- a/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/prompts/types.ts +++ b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/prompts/types.ts @@ -12,7 +12,9 @@ export interface EsPromptsSchema { created_by: string; content: string; is_default?: boolean; - is_shared?: boolean; + consumer?: string; + color?: string; + categories?: string[]; is_new_conversation_default?: boolean; name: string; prompt_type: string; @@ -28,7 +30,8 @@ export interface EsPromptsSchema { export interface UpdatePromptSchema { id: string; '@timestamp'?: string; - is_shared?: boolean; + color?: string; + categories?: string[]; is_new_conversation_default?: boolean; content?: string; updated_at?: string; @@ -42,7 +45,9 @@ export interface UpdatePromptSchema { export interface CreatePromptSchema { '@timestamp'?: string; - is_shared?: boolean; + consumer?: string; + color?: string; + categories?: string[]; is_new_conversation_default?: boolean; is_default?: boolean; name: string; diff --git a/x-pack/plugins/elastic_assistant/server/routes/prompts/find_route.ts b/x-pack/plugins/elastic_assistant/server/routes/prompts/find_route.ts index df2ec323bc356..8838db3c44943 100644 --- a/x-pack/plugins/elastic_assistant/server/routes/prompts/find_route.ts +++ b/x-pack/plugins/elastic_assistant/server/routes/prompts/find_route.ts @@ -59,7 +59,7 @@ export const findPromptsRoute = (router: ElasticAssistantPluginRouter, logger: L page: query.page, sortField: query.sort_field, sortOrder: query.sort_order, - filter: query.filter, + filter: query.filter ? decodeURIComponent(query.filter) : undefined, fields: query.fields, }); diff --git a/x-pack/plugins/security_solution/public/assistant/content/prompts/system/index.tsx b/x-pack/plugins/security_solution/public/assistant/content/prompts/system/index.tsx index 6b1976cd5207d..ee9d4018365c5 100644 --- a/x-pack/plugins/security_solution/public/assistant/content/prompts/system/index.tsx +++ b/x-pack/plugins/security_solution/public/assistant/content/prompts/system/index.tsx @@ -5,7 +5,11 @@ * 2.0. */ -import type { Prompt } from '@kbn/elastic-assistant'; +import { + PromptTypeEnum, + type PromptResponse, +} from '@kbn/elastic-assistant-common/impl/schemas/prompts/bulk_crud_prompts_route.gen'; +import { APP_UI_ID } from '../../../../../common'; import { DEFAULT_SYSTEM_PROMPT_NAME, DEFAULT_SYSTEM_PROMPT_NON_I18N, @@ -16,20 +20,22 @@ import { /** * Base System Prompts for Security Solution. */ -export const BASE_SECURITY_SYSTEM_PROMPTS: Prompt[] = [ +export const BASE_SECURITY_SYSTEM_PROMPTS: PromptResponse[] = [ { id: 'default-system-prompt', content: DEFAULT_SYSTEM_PROMPT_NON_I18N, name: DEFAULT_SYSTEM_PROMPT_NAME, - promptType: 'system', + promptType: PromptTypeEnum.system, isDefault: true, isNewConversationDefault: true, + consumer: APP_UI_ID, }, { id: 'CB9FA555-B59F-4F71-AFF9-8A891AC5BC28', content: SUPERHERO_SYSTEM_PROMPT_NON_I18N, name: SUPERHERO_SYSTEM_PROMPT_NAME, - promptType: 'system', + promptType: PromptTypeEnum.system, + consumer: APP_UI_ID, isDefault: true, }, ]; diff --git a/x-pack/plugins/security_solution/public/assistant/content/quick_prompts/index.tsx b/x-pack/plugins/security_solution/public/assistant/content/quick_prompts/index.tsx index 799087f202e98..adb952d661214 100644 --- a/x-pack/plugins/security_solution/public/assistant/content/quick_prompts/index.tsx +++ b/x-pack/plugins/security_solution/public/assistant/content/quick_prompts/index.tsx @@ -5,7 +5,11 @@ * 2.0. */ -import type { QuickPrompt } from '@kbn/elastic-assistant'; +import { + PromptTypeEnum, + type PromptResponse, +} from '@kbn/elastic-assistant-common/impl/schemas/prompts/bulk_crud_prompts_route.gen'; +import { APP_UI_ID } from '../../../../common'; import * as i18n from './translations'; import { KNOWLEDGE_BASE_CATEGORY, @@ -19,51 +23,72 @@ import { * Useful if wanting to see all available QuickPrompts in one place, or if needing * to reference when constructing a new chat window to include a QuickPrompt. */ -export const BASE_SECURITY_QUICK_PROMPTS: QuickPrompt[] = [ +export const BASE_SECURITY_QUICK_PROMPTS: PromptResponse[] = [ { - title: i18n.ALERT_SUMMARIZATION_TITLE, - prompt: i18n.ALERT_SUMMARIZATION_PROMPT, + name: i18n.ALERT_SUMMARIZATION_TITLE, + content: i18n.ALERT_SUMMARIZATION_PROMPT, color: '#F68FBE', categories: [PROMPT_CONTEXT_ALERT_CATEGORY], isDefault: true, + id: i18n.ALERT_SUMMARIZATION_TITLE, + promptType: PromptTypeEnum.quick, + consumer: APP_UI_ID, }, { - title: i18n.ESQL_QUERY_GENERATION_TITLE, - prompt: i18n.ESQL_QUERY_GENERATION_PROMPT, + name: i18n.ESQL_QUERY_GENERATION_TITLE, + content: i18n.ESQL_QUERY_GENERATION_PROMPT, color: '#9170B8', categories: [KNOWLEDGE_BASE_CATEGORY], isDefault: true, + id: i18n.ESQL_QUERY_GENERATION_TITLE, + promptType: PromptTypeEnum.quick, + consumer: APP_UI_ID, }, { - title: i18n.RULE_CREATION_TITLE, - prompt: i18n.RULE_CREATION_PROMPT, + name: i18n.RULE_CREATION_TITLE, + content: i18n.RULE_CREATION_PROMPT, categories: [PROMPT_CONTEXT_DETECTION_RULES_CATEGORY], color: '#7DDED8', isDefault: true, + id: i18n.RULE_CREATION_TITLE, + promptType: PromptTypeEnum.quick, + consumer: APP_UI_ID, }, { - title: i18n.WORKFLOW_ANALYSIS_TITLE, - prompt: i18n.WORKFLOW_ANALYSIS_PROMPT, + name: i18n.WORKFLOW_ANALYSIS_TITLE, + content: i18n.WORKFLOW_ANALYSIS_PROMPT, color: '#36A2EF', isDefault: true, + id: i18n.WORKFLOW_ANALYSIS_TITLE, + promptType: PromptTypeEnum.quick, + consumer: APP_UI_ID, }, { - title: i18n.THREAT_INVESTIGATION_GUIDES_TITLE, - prompt: i18n.THREAT_INVESTIGATION_GUIDES_PROMPT, + name: i18n.THREAT_INVESTIGATION_GUIDES_TITLE, + content: i18n.THREAT_INVESTIGATION_GUIDES_PROMPT, categories: [PROMPT_CONTEXT_EVENT_CATEGORY], color: '#F3D371', isDefault: true, + id: i18n.THREAT_INVESTIGATION_GUIDES_TITLE, + promptType: PromptTypeEnum.quick, + consumer: APP_UI_ID, }, { - title: i18n.SPL_QUERY_CONVERSION_TITLE, - prompt: i18n.SPL_QUERY_CONVERSION_PROMPT, + name: i18n.SPL_QUERY_CONVERSION_TITLE, + content: i18n.SPL_QUERY_CONVERSION_PROMPT, color: '#BADA55', isDefault: true, + id: i18n.SPL_QUERY_CONVERSION_TITLE, + promptType: PromptTypeEnum.quick, + consumer: APP_UI_ID, }, { - title: i18n.AUTOMATION_TITLE, - prompt: i18n.AUTOMATION_PROMPT, + name: i18n.AUTOMATION_TITLE, + content: i18n.AUTOMATION_PROMPT, color: '#FFA500', isDefault: true, + id: i18n.AUTOMATION_TITLE, + promptType: PromptTypeEnum.quick, + consumer: APP_UI_ID, }, ]; diff --git a/x-pack/plugins/security_solution/public/assistant/provider.tsx b/x-pack/plugins/security_solution/public/assistant/provider.tsx index 2b5daf73fbf4b..134bfb25c15ac 100644 --- a/x-pack/plugins/security_solution/public/assistant/provider.tsx +++ b/x-pack/plugins/security_solution/public/assistant/provider.tsx @@ -15,24 +15,28 @@ import { AssistantProvider as ElasticAssistantProvider, bulkUpdateConversations, getUserConversations, + getPrompts, + bulkUpdatePrompts, } from '@kbn/elastic-assistant'; import { once } from 'lodash/fp'; import type { HttpSetup } from '@kbn/core-http-browser'; import type { Message } from '@kbn/elastic-assistant-common'; import { loadAllActions as loadConnectors } from '@kbn/triggers-actions-ui-plugin/public/common/constants'; +import { useObservable } from 'react-use'; import { APP_ID } from '../../common'; import { useBasePath, useKibana } from '../common/lib/kibana'; import { useAssistantTelemetry } from './use_assistant_telemetry'; import { getComments } from './get_comments'; import { LOCAL_STORAGE_KEY, augmentMessageCodeBlocks } from './helpers'; -import { useBaseConversations } from './use_conversation_store'; -import { PROMPT_CONTEXTS } from './content/prompt_contexts'; import { BASE_SECURITY_QUICK_PROMPTS } from './content/quick_prompts'; import { BASE_SECURITY_SYSTEM_PROMPTS } from './content/prompts/system'; +import { useBaseConversations } from './use_conversation_store'; +import { PROMPT_CONTEXTS } from './content/prompt_contexts'; import { useAssistantAvailability } from './use_assistant_availability'; import { useAppToasts } from '../common/hooks/use_app_toasts'; import { useSignalIndex } from '../detections/containers/detection_engine/alerts/use_signal_index'; +import { licenseService } from '../common/hooks/use_license'; const ASSISTANT_TITLE = i18n.translate('xpack.securitySolution.assistant.title', { defaultMessage: 'Elastic AI Assistant', @@ -112,12 +116,28 @@ export const createConversations = async ( } }; +export const createBasePrompts = async (notifications: NotificationsStart, http: HttpSetup) => { + const promptsToCreate = [...BASE_SECURITY_QUICK_PROMPTS, ...BASE_SECURITY_SYSTEM_PROMPTS]; + + // post bulk create + const bulkResult = await bulkUpdatePrompts( + http, + { + create: promptsToCreate, + }, + notifications.toasts + ); + if (bulkResult && bulkResult.success) { + return true; + } +}; + /** * This component configures the Elastic AI Assistant context provider for the Security Solution app. */ export const AssistantProvider: FC> = ({ children }) => { const { - application: { navigateToApp }, + application: { navigateToApp, currentAppId$ }, http, notifications, storage, @@ -129,29 +149,59 @@ export const AssistantProvider: FC> = ({ children }) const baseConversations = useBaseConversations(); const assistantAvailability = useAssistantAvailability(); const assistantTelemetry = useAssistantTelemetry(); - + const currentAppId = useObservable(currentAppId$, ''); + const hasEnterpriseLicence = licenseService.isEnterprise(); useEffect(() => { const migrateConversationsFromLocalStorage = once(async () => { - const res = await getUserConversations({ - http, - }); if ( + hasEnterpriseLicence && assistantAvailability.isAssistantEnabled && - assistantAvailability.hasAssistantPrivilege && - res.total === 0 + assistantAvailability.hasAssistantPrivilege ) { - await createConversations(notifications, http, storage); + const res = await getUserConversations({ + http, + }); + if (res.total === 0) { + await createConversations(notifications, http, storage); + } } }); migrateConversationsFromLocalStorage(); }, [ assistantAvailability.hasAssistantPrivilege, assistantAvailability.isAssistantEnabled, + hasEnterpriseLicence, http, notifications, storage, ]); + useEffect(() => { + const createSecurityPrompts = once(async () => { + if ( + hasEnterpriseLicence && + assistantAvailability.isAssistantEnabled && + assistantAvailability.hasAssistantPrivilege + ) { + const res = await getPrompts({ + http, + toasts: notifications.toasts, + }); + + if (res.total === 0) { + await createBasePrompts(notifications, http); + } + } + }); + createSecurityPrompts(); + }, [ + assistantAvailability.hasAssistantPrivilege, + assistantAvailability.isAssistantEnabled, + hasEnterpriseLicence, + http, + notifications, + ]); + const { signalIndexName } = useSignalIndex(); const alertsIndexPattern = signalIndexName ?? undefined; const toasts = useAppToasts() as unknown as IToasts; // useAppToasts is the current, non-deprecated method of getting the toasts service in the Security Solution, but it doesn't return the IToasts interface (defined by core) @@ -166,14 +216,13 @@ export const AssistantProvider: FC> = ({ children }) docLinks={{ ELASTIC_WEBSITE_URL, DOC_LINK_VERSION }} basePath={basePath} basePromptContexts={Object.values(PROMPT_CONTEXTS)} - baseQuickPrompts={BASE_SECURITY_QUICK_PROMPTS} // to server and plugin start - baseSystemPrompts={BASE_SECURITY_SYSTEM_PROMPTS} // to server and plugin start baseConversations={baseConversations} getComments={getComments} http={http} navigateToApp={navigateToApp} title={ASSISTANT_TITLE} toasts={toasts} + currentAppId={currentAppId ?? 'securitySolutionUI'} > {children} diff --git a/x-pack/plugins/security_solution/public/common/mock/mock_assistant_provider.tsx b/x-pack/plugins/security_solution/public/common/mock/mock_assistant_provider.tsx index b9a83fd280b10..04860ba9c6c71 100644 --- a/x-pack/plugins/security_solution/public/common/mock/mock_assistant_provider.tsx +++ b/x-pack/plugins/security_solution/public/common/mock/mock_assistant_provider.tsx @@ -50,6 +50,7 @@ export const MockAssistantProviderComponent: React.FC = ({ http={mockHttp} navigateToApp={mockNavigateToApp} baseConversations={BASE_SECURITY_CONVERSATIONS} + currentAppId={'test'} > {children} diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/rule_status_failed_callout.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/rule_status_failed_callout.test.tsx index b5e7737be38f7..23c2d2e7b9f6b 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/rule_status_failed_callout.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/rule_status_failed_callout.test.tsx @@ -64,6 +64,7 @@ const ContextWrapper: FC> = ({ children }) => ( http={mockHttp} navigateToApp={mockNavigationToApp} baseConversations={BASE_SECURITY_CONVERSATIONS} + currentAppId={'security'} > {children} diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 04bc9871cb0af..00eb8d11923b2 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -13341,8 +13341,6 @@ "xpack.elasticAssistant.assistant.content.prompts.system.superheroPersonality": "Donnez la réponse la plus pertinente et détaillée possible, comme si vous deviez communiquer ces informations à un expert en cybersécurité.", "xpack.elasticAssistant.assistant.content.prompts.system.superheroSystemPromptName": "Invite système améliorée", "xpack.elasticAssistant.assistant.content.prompts.system.youAreAHelpfulExpertAssistant": "Vous êtes un assistant expert et serviable qui répond à des questions au sujet d’Elastic Security.", - "xpack.elasticAssistant.assistant.content.prompts.user.finallySuggestInvestigationGuideAndFormatAsMarkdown": "Ajoutez votre description, les actions que vous recommandez ainsi que les étapes de triage à puces. Utilisez les données \"MITRE ATT&CK\" fournies pour ajouter du contexte et des recommandations de MITRE ainsi que des liens hypertexte vers les pages pertinentes sur le site web de MITRE. Assurez-vous d’inclure les scores de risque de l’utilisateur et de l’hôte du contexte. Votre réponse doit inclure des étapes qui pointent vers les fonctionnalités spécifiques d’Elastic Security, y compris les actions de réponse du terminal, l’intégration OSQuery Manager d’Elastic Agent (avec des exemples de requêtes OSQuery), des analyses de timeline et d’entités, ainsi qu’un lien pour toute la documentation Elastic Security pertinente.", - "xpack.elasticAssistant.assistant.content.prompts.user.thenSummarizeSuggestedKqlAndEqlQueries": "Évaluer l’événement depuis le contexte ci-dessus et formater soigneusement la sortie en syntaxe Markdown pour mon cas Elastic Security.", "xpack.elasticAssistant.assistant.conversations.settings.connectorTitle": "Connecteur", "xpack.elasticAssistant.assistant.conversations.settings.promptHelpTextTitle": "Contexte fournit dans le cadre de chaque conversation", "xpack.elasticAssistant.assistant.conversations.settings.promptTitle": "Invite système", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 2fed66584a779..7778a86693d86 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -13320,8 +13320,6 @@ "xpack.elasticAssistant.assistant.content.prompts.system.superheroPersonality": "サイバーセキュリティの専門家に情報を伝えるつもりで、できるだけ詳細で関連性のある回答を入力してください。", "xpack.elasticAssistant.assistant.content.prompts.system.superheroSystemPromptName": "拡張システムプロンプト", "xpack.elasticAssistant.assistant.content.prompts.system.youAreAHelpfulExpertAssistant": "あなたはElasticセキュリティに関する質問に答える、親切で専門的なアシスタントです。", - "xpack.elasticAssistant.assistant.content.prompts.user.finallySuggestInvestigationGuideAndFormatAsMarkdown": "説明、推奨されるアクション、箇条書きのトリアージステップを追加します。提供された MITRE ATT&CKデータを使用して、MITREからのコンテキストや推奨事項を追加し、MITREのWebサイトの関連ページにハイパーリンクを貼ります。コンテキストのユーザーとホストのリスクスコアデータを必ず含めてください。回答には、エンドポイント対応アクション、ElasticエージェントOSQueryマネージャー統合(osqueryクエリの例を付けて)、タイムライン、エンティティ分析など、Elasticセキュリティ固有の機能を指す手順を含め、関連するElasticセキュリティのドキュメントすべてにリンクしてください。", - "xpack.elasticAssistant.assistant.content.prompts.user.thenSummarizeSuggestedKqlAndEqlQueries": "上記のコンテキストからイベントを評価し、Elasticセキュリティのケース用に、出力をマークダウン構文で正しく書式設定してください。", "xpack.elasticAssistant.assistant.conversations.settings.connectorTitle": "コネクター", "xpack.elasticAssistant.assistant.conversations.settings.promptHelpTextTitle": "すべての会話の一部として提供されたコンテキスト", "xpack.elasticAssistant.assistant.conversations.settings.promptTitle": "システムプロンプト", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 8735ee2e0257d..0257b4d2830a4 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -13346,8 +13346,6 @@ "xpack.elasticAssistant.assistant.content.prompts.system.superheroPersonality": "提供可能的最详细、最相关的答案,就好像您正将此信息转发给网络安全专家一样。", "xpack.elasticAssistant.assistant.content.prompts.system.superheroSystemPromptName": "已增强系统提示", "xpack.elasticAssistant.assistant.content.prompts.system.youAreAHelpfulExpertAssistant": "您是一位可帮助回答 Elastic Security 相关问题的专家助手。", - "xpack.elasticAssistant.assistant.content.prompts.user.finallySuggestInvestigationGuideAndFormatAsMarkdown": "添加描述、建议操作和带项目符号的分类步骤。使用提供的 MITRE ATT&CK 数据以从 MITRE 添加更多上下文和建议,以及指向 MITRE 网站上的相关页面的超链接。确保包括上下文中的用户和主机风险分数数据。您的响应应包含指向 Elastic Security 特定功能的步骤,包括终端响应操作、Elastic 代理 OSQuery 管理器集成(带示例 osquery 查询)、时间线和实体分析,以及所有相关 Elastic Security 文档的链接。", - "xpack.elasticAssistant.assistant.content.prompts.user.thenSummarizeSuggestedKqlAndEqlQueries": "评估来自上述上下文的事件,并以用于我的 Elastic Security 案例的 Markdown 语法对您的输出进行全面格式化。", "xpack.elasticAssistant.assistant.conversations.settings.connectorTitle": "连接器", "xpack.elasticAssistant.assistant.conversations.settings.promptHelpTextTitle": "已作为每个对话的一部分提供上下文", "xpack.elasticAssistant.assistant.conversations.settings.promptTitle": "系统提示", From 482f2a95031999e5b14fb4fb786e673c2178b38d Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Wed, 3 Jul 2024 10:39:37 -0700 Subject: [PATCH 04/13] [Logstash Plugin] Migrate authc.getCurrentUser usage to coreContext.security (#187180) Part of https://github.com/elastic/kibana/issues/186574 ## Summary This PR migrates the Logstash Plugin's route handler for saving a pipeline, which consumes `authc.getCurrentUser`, to use `coreContext.security`. Background: This PR serves as an example of a plugin migrating away from depending on the Security plugin, which is a high priority effort for the last release before 9.0. ### Checklist Delete any items that are not applicable to this PR. - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/logstash/server/plugin.ts | 4 +--- x-pack/plugins/logstash/server/routes/index.ts | 5 ++--- .../logstash/server/routes/pipeline/save.ts | 16 +++++----------- x-pack/plugins/logstash/tsconfig.json | 1 - 4 files changed, 8 insertions(+), 18 deletions(-) diff --git a/x-pack/plugins/logstash/server/plugin.ts b/x-pack/plugins/logstash/server/plugin.ts index 13cd3ba3fc472..24b8fa5a23b2d 100644 --- a/x-pack/plugins/logstash/server/plugin.ts +++ b/x-pack/plugins/logstash/server/plugin.ts @@ -8,12 +8,10 @@ import { CoreSetup, CoreStart, Logger, Plugin, PluginInitializerContext } from '@kbn/core/server'; import { LicensingPluginSetup } from '@kbn/licensing-plugin/server'; import { PluginSetupContract as FeaturesPluginSetup } from '@kbn/features-plugin/server'; -import { SecurityPluginSetup } from '@kbn/security-plugin/server'; import { registerRoutes } from './routes'; interface SetupDeps { licensing: LicensingPluginSetup; - security?: SecurityPluginSetup; features: FeaturesPluginSetup; } @@ -27,7 +25,7 @@ export class LogstashPlugin implements Plugin { setup(core: CoreSetup, deps: SetupDeps) { this.logger.debug('Setting up Logstash plugin'); - registerRoutes(core.http.createRouter(), deps.security); + registerRoutes(core.http.createRouter()); deps.features.registerElasticsearchFeature({ id: 'pipelines', diff --git a/x-pack/plugins/logstash/server/routes/index.ts b/x-pack/plugins/logstash/server/routes/index.ts index 63b2febd3eda7..08e8c0732edca 100644 --- a/x-pack/plugins/logstash/server/routes/index.ts +++ b/x-pack/plugins/logstash/server/routes/index.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { SecurityPluginSetup } from '@kbn/security-plugin/server'; import type { LogstashPluginRouter } from '../types'; import { registerClusterLoadRoute } from './cluster'; import { @@ -15,12 +14,12 @@ import { } from './pipeline'; import { registerPipelinesListRoute, registerPipelinesDeleteRoute } from './pipelines'; -export function registerRoutes(router: LogstashPluginRouter, security?: SecurityPluginSetup) { +export function registerRoutes(router: LogstashPluginRouter) { registerClusterLoadRoute(router); registerPipelineDeleteRoute(router); registerPipelineLoadRoute(router); - registerPipelineSaveRoute(router, security); + registerPipelineSaveRoute(router); registerPipelinesListRoute(router); registerPipelinesDeleteRoute(router); diff --git a/x-pack/plugins/logstash/server/routes/pipeline/save.ts b/x-pack/plugins/logstash/server/routes/pipeline/save.ts index 9e837bf9bd416..4d76518d7376c 100644 --- a/x-pack/plugins/logstash/server/routes/pipeline/save.ts +++ b/x-pack/plugins/logstash/server/routes/pipeline/save.ts @@ -9,15 +9,11 @@ import { schema } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; import { wrapRouteWithLicenseCheck } from '@kbn/licensing-plugin/server'; -import { SecurityPluginSetup } from '@kbn/security-plugin/server'; import { Pipeline } from '../../models/pipeline'; import { checkLicense } from '../../lib/check_license'; import type { LogstashPluginRouter } from '../../types'; -export function registerPipelineSaveRoute( - router: LogstashPluginRouter, - security?: SecurityPluginSetup -) { +export function registerPipelineSaveRoute(router: LogstashPluginRouter) { router.put( { path: '/api/logstash/pipeline/{id}', @@ -39,14 +35,12 @@ export function registerPipelineSaveRoute( wrapRouteWithLicenseCheck( checkLicense, router.handleLegacyErrors(async (context, request, response) => { + const coreContext = await context.core; try { - let username: string | undefined; - if (security) { - const user = await security.authc.getCurrentUser(request); - username = user?.username; - } + const user = coreContext.security.authc.getCurrentUser(); + const username = user?.username; - const { client } = (await context.core).elasticsearch; + const { client } = coreContext.elasticsearch; const pipeline = Pipeline.fromDownstreamJSON(request.body, request.params.id, username); await client.asCurrentUser.logstash.putPipeline({ diff --git a/x-pack/plugins/logstash/tsconfig.json b/x-pack/plugins/logstash/tsconfig.json index f6d4b28a8d896..7c5af6106c6f2 100644 --- a/x-pack/plugins/logstash/tsconfig.json +++ b/x-pack/plugins/logstash/tsconfig.json @@ -16,7 +16,6 @@ "@kbn/features-plugin", "@kbn/licensing-plugin", - "@kbn/security-plugin", "@kbn/i18n", "@kbn/i18n-react", "@kbn/test-jest-helpers", From ee80b740facfd2d5e887458dd90c43c5f2970710 Mon Sep 17 00:00:00 2001 From: "Christiane (Tina) Heiligers" Date: Wed, 3 Jul 2024 10:43:47 -0700 Subject: [PATCH 05/13] Reuse core-security-service `createMockAuthenticatedUser` mock (#187426) Follow up to https://github.com/elastic/kibana/pull/187318 Implement core `createMockAuthenticatedUser` in the security plugin mock to avoid divergence. --------- Co-authored-by: Elastic Machine --- x-pack/plugins/security/public/mocks.ts | 7 +++---- x-pack/plugins/security/server/mocks.ts | 7 +++---- x-pack/plugins/security/tsconfig.json | 3 ++- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/security/public/mocks.ts b/x-pack/plugins/security/public/mocks.ts index edfc42aae585e..d2700afc1c12d 100644 --- a/x-pack/plugins/security/public/mocks.ts +++ b/x-pack/plugins/security/public/mocks.ts @@ -7,12 +7,12 @@ import { of } from 'rxjs'; +import { securityServiceMock } from '@kbn/core-security-server-mocks'; + import { authenticationMock, authorizationMock } from './authentication/index.mock'; import { navControlServiceMock } from './nav_control/index.mock'; import { getUiApiMock } from './ui_api/index.mock'; import { licenseMock } from '../common/licensing/index.mock'; -import type { MockAuthenticatedUserProps } from '../common/model/authenticated_user.mock'; -import { mockAuthenticatedUser } from '../common/model/authenticated_user.mock'; function createSetupMock() { return { @@ -43,6 +43,5 @@ function createStartMock() { export const securityMock = { createSetup: createSetupMock, createStart: createStartMock, - createMockAuthenticatedUser: (props: MockAuthenticatedUserProps = {}) => - mockAuthenticatedUser(props), + createMockAuthenticatedUser: securityServiceMock.createMockAuthenticatedUser, }; diff --git a/x-pack/plugins/security/server/mocks.ts b/x-pack/plugins/security/server/mocks.ts index ba0dbaafeef3b..a5473176fc7e7 100644 --- a/x-pack/plugins/security/server/mocks.ts +++ b/x-pack/plugins/security/server/mocks.ts @@ -7,13 +7,13 @@ import type { TransportResult } from '@elastic/elasticsearch'; +import { securityServiceMock } from '@kbn/core-security-server-mocks'; + import { auditServiceMock } from './audit/mocks'; import { authenticationServiceMock } from './authentication/authentication_service.mock'; import { authorizationMock } from './authorization/index.mock'; import { userProfileServiceMock } from './user_profile/user_profile_service.mock'; import { licenseMock } from '../common/licensing/index.mock'; -import { mockAuthenticatedUser } from '../common/model/authenticated_user.mock'; -import type { MockAuthenticatedUserProps } from '../common/model/authenticated_user.mock'; function createSetupMock() { const mockAuthz = authorizationMock.create(); @@ -79,6 +79,5 @@ export const securityMock = { createSetup: createSetupMock, createStart: createStartMock, createApiResponse: createApiResponseMock, - createMockAuthenticatedUser: (props: MockAuthenticatedUserProps = {}) => - mockAuthenticatedUser(props), + createMockAuthenticatedUser: securityServiceMock.createMockAuthenticatedUser, }; diff --git a/x-pack/plugins/security/tsconfig.json b/x-pack/plugins/security/tsconfig.json index 728fcec8d911a..64d162839cf1e 100644 --- a/x-pack/plugins/security/tsconfig.json +++ b/x-pack/plugins/security/tsconfig.json @@ -82,7 +82,8 @@ "@kbn/core-user-profile-server", "@kbn/core-user-profile-browser", "@kbn/security-api-key-management", - "@kbn/security-form-components" + "@kbn/security-form-components", + "@kbn/core-security-server-mocks", ], "exclude": [ "target/**/*", From a7cea133009322f380727901243c96736f8cb150 Mon Sep 17 00:00:00 2001 From: Maxim Palenov Date: Wed, 3 Jul 2024 21:02:30 +0200 Subject: [PATCH 06/13] [Security Solution] Fix generation of circular types using non-circular types (#187061) **Relates to:** https://github.com/elastic/kibana/issues/186066, https://github.com/elastic/kibana/pull/186221 ## Summary This PR fixes generated TS files for circular OpenAPI schemas when non circular (internal or external) schema is used. ## Details https://github.com/elastic/kibana/pull/186221 added code generation support for circular schemas. Such schemas have input TS types generated which may depend on the other circular or non circular TS types. The problem appears when a circular schema uses a non circular schema. Generated code expects an input type for used schemas exist but it's not a case non circular schemas. Let's consider a following OpenAPI spec with a self circular schema and a field referencing `NonEmptyString` schema ```yaml ... components: x-codegen-enabled: true schemas: SelfCircular: type: object properties: circularField: $ref: '#/components/schemas/SelfCircular' stringField: $ref: '../model/primitives.schema.yaml#/components/schemas/NonEmptyString' ``` where a generated TS file looks like ```ts import type { ZodTypeDef } from 'zod'; import { z } from 'zod'; import { NonEmptyString } from '../model/primitives.gen'; export interface SelfCircular { circularField?: SelfCircular; stringField?: NonEmptyString; } export interface SelfCircularInput { circularField?: SelfCircularInput; stringField?: NonEmptyStringInput; } export const SelfCircular: z.ZodType = z.object({ circularField: z.lazy(() => SelfCircular).optional(), stringField: NonEmptyString.optional(), }); ``` You can notice the generated TS file contains usage of `NonEmptyStringInput` which doesn't exist. **After applying the fix the generated TS file looks like** ```ts import type { ZodTypeDef } from 'zod'; import { z } from 'zod'; import { NonEmptyString } from '../model/primitives.gen'; export interface SelfCircular { circularField?: SelfCircular; stringField?: NonEmptyString; } export interface SelfCircularInput { circularField?: SelfCircularInput; stringField?: NonEmptyString; } export const SelfCircular: z.ZodType = z.object({ circularField: z.lazy(() => SelfCircular).optional(), stringField: NonEmptyString.optional(), }); ``` --- .../src/template_service/templates/ts_input_type.handlebars | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/kbn-openapi-generator/src/template_service/templates/ts_input_type.handlebars b/packages/kbn-openapi-generator/src/template_service/templates/ts_input_type.handlebars index 453e4cdf452d5..5091c6f9a7019 100644 --- a/packages/kbn-openapi-generator/src/template_service/templates/ts_input_type.handlebars +++ b/packages/kbn-openapi-generator/src/template_service/templates/ts_input_type.handlebars @@ -3,7 +3,8 @@ {{~/if~}} {{~#if $ref~}} - {{referenceName}}Input + {{referenceName}} + {{~#if (isCircularRef $ref)}}Input{{/if~}} {{~#if nullable}} | null {{/if~}} {{~/if~}} From 184b6e2ad47fb207246554b2719dbbb9b917951e Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Wed, 3 Jul 2024 14:16:47 -0500 Subject: [PATCH 07/13] [Security Solution][CTI] Enable rendering of CTI indicators with flattened fields (#179395) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Our initial implementation of these components assumed a very flat, normal structure for the indicator documents we would retrieve (because we leverage the `fields` API). However, `flattened` fields do not quite fit this pattern, and there is a bug where indicator documents containing `flattened` fields with complex values would not be parsed correctly, and we attempt to render JS objects to the DOM (which React does not like, and throws an error). This issue was uncovered originally in an SDH. ### How to Review See https://github.com/elastic/kibana/issues/179483 for details on how to repro. ### Screenshots (Using the data described in https://github.com/elastic/kibana/issues/179483): Screenshot 2024-03-26 at 3 28 00 PM Screenshot 2024-03-26 at 3 28 15 PM Linked issue: https://github.com/elastic/kibana/issues/179483 --- .../indicator_with_nested_objects.ts | 146 ++++++++++++++++++ .../enrichment_accordion_group.test.tsx | 33 ++++ .../enrichment_accordion_group.tsx | 22 +-- .../cti_details/helpers.test.tsx | 131 ++++++++++++++++ .../event_details/cti_details/helpers.tsx | 29 +++- .../event_details/cti_details/translations.ts | 8 + 6 files changed, 351 insertions(+), 18 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/common/components/event_details/__mocks__/indicator_with_nested_objects.ts create mode 100644 x-pack/plugins/security_solution/public/common/components/event_details/cti_details/enrichment_accordion_group.test.tsx diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/__mocks__/indicator_with_nested_objects.ts b/x-pack/plugins/security_solution/public/common/components/event_details/__mocks__/indicator_with_nested_objects.ts new file mode 100644 index 0000000000000..189cc45da4aa8 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/event_details/__mocks__/indicator_with_nested_objects.ts @@ -0,0 +1,146 @@ +/* + * 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. + */ + +/** + * This represents an indicator document with an array of objects as field + * values. This shape of indicator was previously causing render errors in the + * CTI UI. + */ +export const indicatorWithNestedObjects = { + 'threat.indicator.type': ['ipv4-addr'], + 'elastic_agent.version': ['8.10.4'], + 'event.category': ['threat'], + 'recordedfuture.risk_string': ['7/75'], + 'threat.indicator.provider': [ + 'Mastodon', + 'Twitter', + 'Recorded Future Command & Control Reports', + 'Recorded Future Sandbox - Malware C2 Extractions', + 'GitHub', + 'Recorded Future Command & Control Validation', + 'Malware Patrol', + 'Polyswarm Sandbox Analysis - Malware C2 Extractions', + 'Recorded Future Triage Malware Analysis - Malware C2 Extractions', + ], + 'agent.type': ['filebeat'], + 'agent.name': ['win-10'], + 'elastic_agent.snapshot': [false], + 'event.agent_id_status': ['verified'], + 'event.kind': ['enrichment'], + 'threat.feed.name': ['Recorded Future'], + 'elastic_agent.id': ['e8ffaf42-7436-4e39-b895-772bb86e6585'], + 'recordedfuture.name': ['188.116.21.141'], + 'data_stream.namespace': ['default'], + 'recordedfuture.evidence_details': [ + { + SourcesCount: 2, + SightingsCount: 2, + CriticalityLabel: 'Unusual', + Rule: 'Recently Reported as a Defanged IP', + EvidenceString: + '2 sightings on 2 sources: Mastodon, Twitter. Most recent link (Feb 13, 2024): https://ioc.exchange/@SarlackLab/111926194382069197', + Sources: ['source:pupSAn', 'source:BV5'], + Timestamp: '2024-02-13T21:03:10.000Z', + Name: 'recentDefanged', + MitigationString: '', + Criticality: 1, + }, + { + SourcesCount: 2, + SightingsCount: 12, + CriticalityLabel: 'Suspicious', + Rule: 'Historically Reported C&C Server', + EvidenceString: + '12 sightings on 2 sources: Recorded Future Command & Control Reports, Recorded Future Sandbox - Malware C2 Extractions. 188.116.21.141:20213 was reported as a command and control server for RedLine Stealer on Feb 10, 2024', + Sources: ['source:qU_q-9', 'source:oWAG20'], + Timestamp: '2024-02-10T08:22:27.790Z', + Name: 'reportedCnc', + MitigationString: '', + Criticality: 2, + }, + { + SourcesCount: 1, + SightingsCount: 2, + CriticalityLabel: 'Suspicious', + Rule: 'Recently Linked to Intrusion Method', + EvidenceString: + '2 sightings on 1 source: GitHub. 6 related intrusion methods including DDOS Toolkit, njRAT, Phishing, Remote Access Trojan, Stealware. Most recent link (Feb 13, 2024): https://github.com/0xDanielLopez/TweetFeed/commit/fd64eaa71f7e948d1cca1dc8c148b6515e878df5', + Sources: ['source:MIKjae'], + Timestamp: '2024-02-13T21:57:24.894Z', + Name: 'recentLinkedIntrusion', + MitigationString: '', + Criticality: 2, + }, + { + SourcesCount: 1, + SightingsCount: 11, + CriticalityLabel: 'Suspicious', + Rule: 'Previously Validated C&C Server', + EvidenceString: + '11 sightings on 1 source: Recorded Future Command & Control Validation. Recorded Future analysis validated 188.116.21.141:20213 as a command and control server for RedLine Stealer on Feb 22, 2024', + Sources: ['source:qGriFQ'], + Timestamp: '2024-02-22T00:06:26.000Z', + Name: 'validatedCnc', + MitigationString: '', + Criticality: 2, + }, + { + SourcesCount: 1, + SightingsCount: 1, + CriticalityLabel: 'Suspicious', + Rule: 'Recent Suspected C&C Server', + EvidenceString: + '1 sighting on 1 source: Malware Patrol. Malware Patrol identified 188.116.21.141:20213 as a command and control server for RecordBreaker Stealer on February 14, 2024.', + Sources: ['source:qs_-cU'], + Timestamp: '2024-02-14T10:55:01.908Z', + Name: 'recentSuspectedCnc', + MitigationString: '', + Criticality: 2, + }, + { + SourcesCount: 4, + SightingsCount: 26, + CriticalityLabel: 'Malicious', + Rule: 'Recently Reported C&C Server', + EvidenceString: + '26 sightings on 4 sources: Polyswarm Sandbox Analysis - Malware C2 Extractions, Recorded Future Command & Control Reports, Recorded Future Triage Malware Analysis - Malware C2 Extractions, Recorded Future Sandbox - Malware C2 Extractions. 188.116.21.141:20213 was reported as a command and control server for Redline Stealer on Feb 21, 2024', + Sources: ['source:hyihHO', 'source:qU_q-9', 'source:nTcIsu', 'source:oWAG20'], + Timestamp: '2024-02-21T08:22:44.811Z', + Name: 'recentReportedCnc', + MitigationString: '', + Criticality: 3, + }, + { + SourcesCount: 1, + SightingsCount: 3, + CriticalityLabel: 'Very Malicious', + Rule: 'Validated C&C Server', + EvidenceString: + '3 sightings on 1 source: Recorded Future Command & Control Validation. Recorded Future analysis validated 188.116.21.141:20213 as a command and control server for RedLine Stealer on Feb 24, 2024', + Sources: ['source:qGriFQ'], + Timestamp: '2024-02-24T00:52:16.000Z', + Name: 'recentValidatedCnc', + MitigationString: '', + Criticality: 4, + }, + ], + 'input.type': ['httpjson'], + 'data_stream.type': ['logs'], + 'event.risk_score': [98], + tags: ['forwarded', 'recordedfuture'], + 'event.ingested': ['2024-02-24T17:32:40.000Z'], + '@timestamp': ['2024-02-24T17:32:37.813Z'], + 'agent.id': ['e8ffaf42-7436-4e39-b895-772bb86e6585'], + 'threat.indicator.ip': ['188.116.21.141'], + 'ecs.version': ['8.11.0'], + 'data_stream.dataset': ['ti_recordedfuture.threat'], + 'event.created': ['2024-02-24T17:32:37.813Z'], + 'event.type': ['indicator'], + 'agent.ephemeral_id': ['0532c813-1434-4c76-800b-6abdf7eaf62c'], + 'agent.version': ['8.10.4'], + 'event.dataset': ['ti_recordedfuture.threat'], +} as const; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/enrichment_accordion_group.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/enrichment_accordion_group.test.tsx new file mode 100644 index 0000000000000..3462069e0aa16 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/enrichment_accordion_group.test.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 { render } from '@testing-library/react'; +import { EnrichmentAccordionGroup } from './enrichment_accordion_group'; +import { TestProviders } from '../../../mock'; +import { indicatorWithNestedObjects } from '../__mocks__/indicator_with_nested_objects'; +import type { CtiEnrichment } from '../../../../../common/search_strategy'; + +describe('EnrichmentAccordionGroup', () => { + describe('with an indicator with an array of nested objects as a field value', () => { + it('renders the indicator without those fields', () => { + // @ts-expect-error this indicator intentionally does not conform to the CtiEnrichment type + const enrichments = [indicatorWithNestedObjects] as CtiEnrichment[]; + + const { getByTestId } = render( + + + + ); + + const enrichmentView = getByTestId('threat-details-view-0'); + + expect(enrichmentView).toBeInTheDocument(); + expect(enrichmentView).toHaveTextContent('ipv4-addr'); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/enrichment_accordion_group.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/enrichment_accordion_group.tsx index 71100ee3bc07d..da9b26ddc4e4a 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/enrichment_accordion_group.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/enrichment_accordion_group.tsx @@ -18,7 +18,12 @@ import { import type { CtiEnrichment } from '../../../../../common/search_strategy/security_solution/cti'; import type { ThreatDetailsRow } from './helpers'; -import { getEnrichmentIdentifiers, isInvestigationTimeEnrichment, getFirstSeen } from './helpers'; +import { + getEnrichmentIdentifiers, + isInvestigationTimeEnrichment, + getFirstSeen, + buildThreatDetailsItems, +} from './helpers'; import { EnrichmentButtonContent } from './enrichment_button_content'; import { ThreatSummaryTitle } from './threat_summary_title'; import { InspectButton } from '../../inspect'; @@ -26,8 +31,6 @@ import { QUERY_ID } from '../../../containers/cti/event_enrichment'; import * as i18n from './translations'; import { ThreatSummaryTable } from './threat_summary_table'; import { REFERENCE } from '../../../../../common/cti/constants'; -import { DEFAULT_INDICATOR_SOURCE_PATH } from '../../../../../common/constants'; -import { getFirstElement } from '../../../../../common/utils/data_retrieval'; const StyledEuiAccordion = styled(EuiAccordion)` .euiAccordion__triggerWrapper { @@ -82,19 +85,6 @@ const columns: Array> = [ }, ]; -const buildThreatDetailsItems = (enrichment: CtiEnrichment) => - Object.keys(enrichment) - .sort() - .map((field) => ({ - title: field.startsWith(DEFAULT_INDICATOR_SOURCE_PATH) - ? field.replace(`${DEFAULT_INDICATOR_SOURCE_PATH}`, 'indicator') - : field, - description: { - fieldName: field, - value: getFirstElement(enrichment[field]), - }, - })); - const EnrichmentAccordion: React.FC<{ enrichment: CtiEnrichment; index: number; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.test.tsx index 169a14fb4df70..b1573663313ee 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.test.tsx @@ -12,6 +12,7 @@ import { getEnrichmentFields, parseExistingEnrichments, getEnrichmentIdentifiers, + buildThreatDetailsItems, } from './helpers'; describe('parseExistingEnrichments', () => { @@ -492,3 +493,133 @@ describe('getEnrichmentIdentifiers', () => { }); }); }); + +describe('buildThreatDetailsItems', () => { + it('returns an empty array if given an empty enrichment', () => { + expect(buildThreatDetailsItems({})).toEqual([]); + }); + + it('returns an array of threat details items', () => { + const enrichment = { + 'matched.field': ['matched field'], + 'matched.atomic': ['matched atomic'], + 'matched.type': ['matched type'], + 'feed.name': ['feed name'], + }; + expect(buildThreatDetailsItems(enrichment)).toEqual([ + { + description: { + fieldName: 'feed.name', + value: 'feed name', + }, + title: 'feed.name', + }, + { + description: { + fieldName: 'matched.atomic', + value: 'matched atomic', + }, + title: 'matched.atomic', + }, + { + description: { + fieldName: 'matched.field', + value: 'matched field', + }, + title: 'matched.field', + }, + { + description: { + fieldName: 'matched.type', + value: 'matched type', + }, + title: 'matched.type', + }, + ]); + }); + + it('retrieves the first value of an array field', () => { + const enrichment = { + array_values: ['first value', 'second value'], + }; + + expect(buildThreatDetailsItems(enrichment)).toEqual([ + { + title: 'array_values', + description: { + fieldName: 'array_values', + value: 'first value', + }, + }, + ]); + }); + + it('shortens indicator field names if they contain the default indicator path', () => { + const enrichment = { + 'threat.indicator.ip': ['127.0.0.1'], + }; + expect(buildThreatDetailsItems(enrichment)).toEqual([ + { + title: 'indicator.ip', + description: { + fieldName: 'threat.indicator.ip', + value: '127.0.0.1', + }, + }, + ]); + }); + + it('parses an object field', () => { + const enrichment = { + 'object_field.foo': ['bar'], + }; + + expect(buildThreatDetailsItems(enrichment)).toEqual([ + { + title: 'object_field.foo', + description: { + fieldName: 'object_field.foo', + value: 'bar', + }, + }, + ]); + }); + + describe('edge cases', () => { + describe('field responses for fields of type "flattened"', () => { + it('returns a note for the value of a flattened field containing a single object', () => { + const enrichment = { + flattened_object: [{ foo: 'bar' }], + }; + + expect(buildThreatDetailsItems(enrichment)).toEqual([ + { + title: 'flattened_object', + description: { + fieldName: 'flattened_object', + value: + 'This field contains nested object values, which are not rendered here. See the full document for all fields/values', + }, + }, + ]); + }); + + it('returns a note for the value of a flattened field containing an array of objects', () => { + const enrichment = { + array_field: [{ foo: 'bar' }, { baz: 'qux' }], + }; + + expect(buildThreatDetailsItems(enrichment)).toEqual([ + { + title: 'array_field', + description: { + fieldName: 'array_field', + value: + 'This field contains nested object values, which are not rendered here. See the full document for all fields/values', + }, + }, + ]); + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.tsx index f124d59581fb3..84e2dab7e7aa1 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.tsx @@ -5,9 +5,12 @@ * 2.0. */ -import { groupBy } from 'lodash'; +import { groupBy, isObject } from 'lodash'; import { getDataFromFieldsHits } from '../../../../../common/utils/field_formatters'; -import { ENRICHMENT_DESTINATION_PATH } from '../../../../../common/constants'; +import { + DEFAULT_INDICATOR_SOURCE_PATH, + ENRICHMENT_DESTINATION_PATH, +} from '../../../../../common/constants'; import { ENRICHMENT_TYPES, FIRST_SEEN, @@ -25,6 +28,7 @@ import type { } from '../../../../../common/search_strategy/security_solution/cti'; import { isValidEventField } from '../../../../../common/search_strategy/security_solution/cti'; import { getFirstElement } from '../../../../../common/utils/data_retrieval'; +import * as i18n from './translations'; export const isInvestigationTimeEnrichment = (type: string | undefined) => type === ENRICHMENT_TYPES.InvestigationTime; @@ -134,3 +138,24 @@ export interface ThreatDetailsRow { value: string; }; } + +interface ThreatDetailItem { + title: string; + description: { fieldName: string; value: unknown }; +} + +export const buildThreatDetailsItems = (enrichment: CtiEnrichment): ThreatDetailItem[] => + Object.keys(enrichment) + .sort() + .map((field) => { + const title = field.startsWith(DEFAULT_INDICATOR_SOURCE_PATH) + ? field.replace(`${DEFAULT_INDICATOR_SOURCE_PATH}`, 'indicator') + : field; + + let value = getFirstElement(enrichment[field]); + if (isObject(value)) { + value = i18n.NESTED_OBJECT_VALUES_NOT_RENDERED; + } + + return { title, description: { fieldName: field, value } }; + }); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/translations.ts b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/translations.ts index 973b438c866c3..ecc5dec40d99e 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/translations.ts @@ -92,6 +92,14 @@ export const ENRICHED_DATA = i18n.translate( } ); +export const NESTED_OBJECT_VALUES_NOT_RENDERED = i18n.translate( + 'xpack.securitySolution.eventDetails.ctiSummary.investigationEnrichmentObjectValuesNotRendered', + { + defaultMessage: + 'This field contains nested object values, which are not rendered here. See the full document for all fields/values', + } +); + export const CURRENT_RISK_LEVEL = (riskEntity: RiskScoreEntity) => i18n.translate('xpack.securitySolution.alertDetails.overview.hostRiskLevel', { defaultMessage: 'Current {riskEntity} risk level', From adc9310845000ce00b61292540c63e052b909cd5 Mon Sep 17 00:00:00 2001 From: Luke G <11671118+lgestc@users.noreply.github.com> Date: Wed, 3 Jul 2024 22:03:02 +0200 Subject: [PATCH 08/13] [SecuritySolution] remove "fields" from the BrowserField (#187066) ## Summary This is part 1/n of a wider effort:) BrowserField used to be some kind of field dictionary (!) which is obviously wrong:). Added FieldCategory type for that as an intermediate step as I dont know if it will hold up after the changes I am doing are complete. --- .../components/event_details/columns.test.tsx | 2 +- .../cti_details/enrichment_summary.tsx | 5 ----- .../components/event_details/summary_view.test.tsx | 2 -- .../event_details/table/field_value_cell.test.tsx | 4 ---- .../event_details/table/field_value_cell.tsx | 2 +- .../event_details/table/prevalence_cell.test.tsx | 2 -- .../event_details/table/summary_value_cell.test.tsx | 3 --- .../table/use_action_cell_data_provider.ts | 2 +- .../public/common/components/event_details/types.ts | 2 +- .../components/query_bar/index.test.tsx | 3 --- .../rule_creation_ui/components/query_bar/index.tsx | 3 --- .../components/step_define_rule/index.test.tsx | 1 - .../components/step_define_rule/index.tsx | 13 ++----------- .../components/threatmatch_input/index.tsx | 4 ---- .../rule_creation_ui/pages/rule_creation/index.tsx | 4 +--- .../rule_creation_ui/pages/rule_editing/index.tsx | 4 +--- .../components/edit_data_provider/helpers.tsx | 5 +++-- .../timeline/body/renderers/formatted_field.tsx | 2 +- .../common/search_strategy/index_fields/index.ts | 9 +++++++-- 19 files changed, 19 insertions(+), 53 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/columns.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/columns.test.tsx index c2b044b35cae1..52b6493a25c21 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/columns.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/columns.test.tsx @@ -60,7 +60,7 @@ describe('getColumns', () => { describe('column actions', () => { let actionsColumn: Column; - const mockDataToUse = mockBrowserFields.agent; + const mockDataToUse = mockBrowserFields.agent.fields; const testValue = 'testValue'; const testData = { type: 'someType', diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/enrichment_summary.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/enrichment_summary.tsx index c8e8f2bfb24db..f9a0ac72b54a4 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/enrichment_summary.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/enrichment_summary.tsx @@ -21,7 +21,6 @@ import { getEnrichmentIdentifiers, isInvestigationTimeEnrichment } from './helpe import type { FieldsData } from '../types'; import type { - BrowserField, BrowserFields, TimelineEventsDetailsItem, } from '../../../../../common/search_strategy'; @@ -30,7 +29,6 @@ import { EnrichedDataRow, ThreatSummaryPanelHeader } from './threat_summary_view import { getSourcererScopeId } from '../../../../helpers'; export interface ThreatSummaryDescription { - browserField: BrowserField; data: FieldsData | undefined; eventId: string; index: number; @@ -63,7 +61,6 @@ export const StyledEuiFlexGroup = styled(EuiFlexGroup)` `; const EnrichmentDescription: React.FC = ({ - browserField, data, eventId, index, @@ -179,7 +176,6 @@ const EnrichmentSummaryComponent: React.FC<{ scopeId={scopeId} value={value} data={fieldsData} - browserField={browserField} isDraggable={isDraggable} isReadOnly={isReadOnly} /> @@ -210,7 +206,6 @@ const EnrichmentSummaryComponent: React.FC<{ scopeId={scopeId} value={value} data={fieldsData} - browserField={browserField} isDraggable={isDraggable} isReadOnly={isReadOnly} /> diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/summary_view.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/summary_view.test.tsx index 3848bb8a15295..9913c733446e3 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/summary_view.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/summary_view.test.tsx @@ -21,7 +21,6 @@ const eventId = 'TUWyf3wBFCFU0qRJTauW'; const hostIpValues = ['127.0.0.1', '::1', '10.1.2.3', '2001:0DB8:AC10:FE01::']; const hostIpFieldFromBrowserField: BrowserField = { aggregatable: true, - fields: {}, format: '', indexes: ['auditbeat-*', 'filebeat-*', 'logs-*', 'winlogbeat-*'], name: 'host.ip', @@ -33,7 +32,6 @@ const hostIpData: EventFieldsData = { ...hostIpFieldFromBrowserField, ariaRowindex: 35, field: 'host.ip', - fields: {}, format: '', isObjectArray: false, originalValue: [...hostIpValues], diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/table/field_value_cell.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/table/field_value_cell.test.tsx index 2529122140b07..95c6890360637 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/table/field_value_cell.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/table/field_value_cell.test.tsx @@ -21,7 +21,6 @@ const hostIpData: EventFieldsData = { aggregatable: true, ariaRowindex: 35, field: 'host.ip', - fields: {}, format: '', indexes: ['auditbeat-*', 'filebeat-*', 'logs-*', 'winlogbeat-*'], isObjectArray: false, @@ -87,7 +86,6 @@ describe('FieldValueCell', () => { aggregatable: false, ariaRowindex: 50, field: 'message', - fields: {}, format: '', indexes: ['auditbeat-*', 'filebeat-*', 'logs-*', 'winlogbeat-*'], isObjectArray: false, @@ -102,7 +100,6 @@ describe('FieldValueCell', () => { const messageFieldFromBrowserField: BrowserField = { aggregatable: false, - fields: {}, format: '', indexes: ['auditbeat-*', 'filebeat-*', 'logs-*', 'winlogbeat-*'], name: 'message', @@ -139,7 +136,6 @@ describe('FieldValueCell', () => { describe('when `BrowserField` metadata IS available', () => { const hostIpFieldFromBrowserField: BrowserField = { aggregatable: true, - fields: {}, format: '', indexes: ['auditbeat-*', 'filebeat-*', 'logs-*', 'winlogbeat-*'], name: 'host.ip', diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/table/field_value_cell.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/table/field_value_cell.tsx index 37f4f4559b50b..02b73651ea6ba 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/table/field_value_cell.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/table/field_value_cell.tsx @@ -18,7 +18,7 @@ export interface FieldValueCellProps { contextId: string; data: EventFieldsData | FieldsData; eventId: string; - fieldFromBrowserField?: BrowserField; + fieldFromBrowserField?: Partial; getLinkValue?: (field: string) => string | null; isDraggable?: boolean; linkValue?: string | null | undefined; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/table/prevalence_cell.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/table/prevalence_cell.test.tsx index d48d7cd0fdaaf..7facc4e30149e 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/table/prevalence_cell.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/table/prevalence_cell.test.tsx @@ -27,7 +27,6 @@ const eventId = 'TUWyf3wBFCFU0qRJTauW'; const hostIpValues = ['127.0.0.1', '::1', '10.1.2.3', '2001:0DB8:AC10:FE01::']; const hostIpFieldFromBrowserField: BrowserField = { aggregatable: true, - fields: {}, format: '', indexes: ['auditbeat-*', 'filebeat-*', 'logs-*', 'winlogbeat-*'], name: 'host.ip', @@ -39,7 +38,6 @@ const hostIpData: EventFieldsData = { ...hostIpFieldFromBrowserField, ariaRowindex: 35, field: 'host.ip', - fields: {}, format: '', isObjectArray: false, originalValue: [...hostIpValues], diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/table/summary_value_cell.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/table/summary_value_cell.test.tsx index 859d1b258c796..183e634a641c3 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/table/summary_value_cell.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/table/summary_value_cell.test.tsx @@ -24,7 +24,6 @@ const eventId = 'TUWyf3wBFCFU0qRJTauW'; const hostIpValues = ['127.0.0.1', '::1', '10.1.2.3', '2001:0DB8:AC10:FE01::']; const hostIpFieldFromBrowserField: BrowserField = { aggregatable: true, - fields: {}, format: '', indexes: ['auditbeat-*', 'filebeat-*', 'logs-*', 'winlogbeat-*'], name: 'host.ip', @@ -36,7 +35,6 @@ const hostIpData: EventFieldsData = { ...hostIpFieldFromBrowserField, ariaRowindex: 35, field: 'host.ip', - fields: {}, format: '', isObjectArray: false, originalValue: [...hostIpValues], @@ -58,7 +56,6 @@ const enrichedAgentStatusData: AlertSummaryRow['description'] = { format: '', type: '', aggregatable: false, - fields: {}, indexes: [], name: AGENT_STATUS_FIELD_NAME, searchable: false, diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/table/use_action_cell_data_provider.ts b/x-pack/plugins/security_solution/public/common/components/event_details/table/use_action_cell_data_provider.ts index c9d8162af8f0c..40611748c69c8 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/table/use_action_cell_data_provider.ts +++ b/x-pack/plugins/security_solution/public/common/components/event_details/table/use_action_cell_data_provider.ts @@ -38,7 +38,7 @@ export interface UseActionCellDataProvider { eventId?: string; field: string; fieldFormat?: string; - fieldFromBrowserField?: BrowserField; + fieldFromBrowserField?: Partial; fieldType?: string; isObjectArray?: boolean; linkValue?: string | null; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/types.ts b/x-pack/plugins/security_solution/public/common/components/event_details/types.ts index bc76ce88aa2f6..87f72da37c8b7 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/event_details/types.ts @@ -20,7 +20,7 @@ export interface FieldsData { export interface EnrichedFieldInfo { data: FieldsData | EventFieldsData; eventId: string; - fieldFromBrowserField?: BrowserField; + fieldFromBrowserField?: Partial; scopeId: string; values: string[] | null | undefined; linkValue?: string; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/query_bar/index.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/query_bar/index.test.tsx index 05fa86a1fa1df..0ba6bea89e0fc 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/query_bar/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/query_bar/index.test.tsx @@ -73,7 +73,6 @@ describe('QueryBarDefineRule', () => { { { ( export const QueryBarDefineRule = ({ defaultSavedQuery, - browserFields, dataTestSubj, field, idAria, diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.test.tsx index 3a0573f3f9b75..23ac62df27661 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.test.tsx @@ -704,7 +704,6 @@ function TestForm({ setOptionsSelected={setSelectedEqlOptions} indexPattern={indexPattern} isIndexPatternLoading={false} - browserFields={{}} isQueryBarValid={true} setIsQueryBarValid={jest.fn()} setIsThreatQueryBarValid={jest.fn()} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.tsx index df6152c7069df..5f9e877e1c2dc 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.tsx @@ -26,7 +26,6 @@ import { i18n as i18nCore } from '@kbn/i18n'; import { isEqual, isEmpty } from 'lodash'; import type { FieldSpec } from '@kbn/data-views-plugin/common'; import usePrevious from 'react-use/lib/usePrevious'; -import type { BrowserFields } from '@kbn/timelines-plugin/common'; import type { Type } from '@kbn/securitysolution-io-ts-alerting-types'; import { useQueryClient } from '@tanstack/react-query'; @@ -118,7 +117,6 @@ export interface StepDefineRuleProps extends RuleStepProps { setOptionsSelected: React.Dispatch>; indexPattern: DataViewBase; isIndexPatternLoading: boolean; - browserFields: BrowserFields; isQueryBarValid: boolean; setIsQueryBarValid: (valid: boolean) => void; setIsThreatQueryBarValid: (valid: boolean) => void; @@ -167,7 +165,6 @@ const IntendedRuleTypeEuiFormRow = styled(RuleTypeEuiFormRow)` // eslint-disable-next-line complexity const StepDefineRuleComponent: FC = ({ - browserFields, dataSourceType, defaultSavedQuery, enableThresholdSuppression, @@ -293,10 +290,8 @@ const StepDefineRuleComponent: FC = ({ [aggFields] ); - const [ - threatIndexPatternsLoading, - { browserFields: threatBrowserFields, indexPatterns: threatIndexPatterns }, - ] = useFetchIndex(threatIndex); + const [threatIndexPatternsLoading, { indexPatterns: threatIndexPatterns }] = + useFetchIndex(threatIndex); // reset form when rule type changes useEffect(() => { @@ -452,7 +447,6 @@ const StepDefineRuleComponent: FC = ({ = ({ handleResetThreatIndices, indexPattern, setIsThreatQueryBarValid, - threatBrowserFields, threatIndexModified, threatIndexPatterns, threatIndexPatternsLoading, @@ -821,7 +814,6 @@ const StepDefineRuleComponent: FC = ({ component={QueryBarDefineRule} componentProps={ { - browserFields, idAria: 'detectionEngineStepDefineRuleQueryBar', indexPattern, isDisabled: isLoading || shouldLoadQueryDynamically || timelineQueryLoading, @@ -841,7 +833,6 @@ const StepDefineRuleComponent: FC = ({ [ handleOpenTimelineSearch, shouldLoadQueryDynamically, - browserFields, indexPattern, isLoading, timelineQueryLoading, diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/threatmatch_input/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/threatmatch_input/index.tsx index 2d23dbb7578c4..c2b04b7d15daa 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/threatmatch_input/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/threatmatch_input/index.tsx @@ -10,7 +10,6 @@ import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiFormRow } from '@elastic/eui'; import type { DataViewBase } from '@kbn/es-query'; import type { ThreatMapEntries } from '../../../../common/components/threat_match/types'; import { ThreatMatchComponent } from '../../../../common/components/threat_match'; -import type { BrowserField } from '../../../../common/containers/source'; import type { FieldHook } from '../../../../shared_imports'; import { Field, @@ -28,7 +27,6 @@ const CommonUseField = getUseField({ component: Field }); interface ThreatMatchInputProps { threatMapping: FieldHook; - threatBrowserFields: Readonly>>; threatIndexPatterns: DataViewBase; indexPatterns: DataViewBase; threatIndexPatternsLoading: boolean; @@ -44,7 +42,6 @@ const ThreatMatchInputComponent: React.FC = ({ indexPatterns, threatIndexPatterns, threatIndexPatternsLoading, - threatBrowserFields, onValidityChange, }: ThreatMatchInputProps) => { const { setValue, value: threatItems } = threatMapping; @@ -101,7 +98,6 @@ const ThreatMatchInputComponent: React.FC = ({ }} component={QueryBarDefineRule} componentProps={{ - browserFields: threatBrowserFields, idAria: 'detectionEngineStepDefineThreatRuleQueryBar', indexPattern: threatIndexPatterns, isDisabled: false, diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/index.tsx index 806ea9f336bd5..6fc19c2b24116 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/index.tsx @@ -262,7 +262,7 @@ const CreateRulePageComponent: React.FC = () => { }; fetchDV(); }, [dataViews]); - const { indexPattern, isIndexPatternLoading, browserFields } = useRuleIndexPattern({ + const { indexPattern, isIndexPatternLoading } = useRuleIndexPattern({ dataSourceType: defineStepData.dataSourceType, index: memoizedIndex, dataViewId: defineStepData.dataViewId, @@ -504,7 +504,6 @@ const CreateRulePageComponent: React.FC = () => { setOptionsSelected={setEqlOptionsSelected} indexPattern={indexPattern} isIndexPatternLoading={isIndexPatternLoading} - browserFields={browserFields} isQueryBarValid={isQueryBarValid} setIsQueryBarValid={setIsQueryBarValid} setIsThreatQueryBarValid={setIsThreatQueryBarValid} @@ -530,7 +529,6 @@ const CreateRulePageComponent: React.FC = () => { ), [ activeStep, - browserFields, dataViewOptions, defineRuleNextStep, defineStepData.dataSourceType, diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_editing/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_editing/index.tsx index 47b67c8ed720a..b5b87b528d01e 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_editing/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_editing/index.tsx @@ -210,7 +210,7 @@ const EditRulePageComponent: FC<{ rule: RuleResponse }> = ({ rule }) => { }); const actionMessageParams = useMemo(() => getActionMessageParams(rule?.type), [rule?.type]); - const { indexPattern, isIndexPatternLoading, browserFields } = useRuleIndexPattern({ + const { indexPattern, isIndexPatternLoading } = useRuleIndexPattern({ dataSourceType: defineStepData.dataSourceType, index: memoizedIndex, dataViewId: defineStepData.dataViewId, @@ -245,7 +245,6 @@ const EditRulePageComponent: FC<{ rule: RuleResponse }> = ({ rule }) => { key="defineStep" indexPattern={indexPattern} isIndexPatternLoading={isIndexPatternLoading} - browserFields={browserFields} isQueryBarValid={isQueryBarValid} setIsQueryBarValid={setIsQueryBarValid} setIsThreatQueryBarValid={setIsThreatQueryBarValid} @@ -371,7 +370,6 @@ const EditRulePageComponent: FC<{ rule: RuleResponse }> = ({ rule }) => { setEqlOptionsSelected, indexPattern, isIndexPatternLoading, - browserFields, isQueryBarValid, defineStepData, aboutStepData, diff --git a/x-pack/plugins/security_solution/public/timelines/components/edit_data_provider/helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/edit_data_provider/helpers.tsx index ecd2ba46560f4..f215a790414d4 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/edit_data_provider/helpers.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/edit_data_provider/helpers.tsx @@ -8,9 +8,10 @@ import { findIndex } from 'lodash/fp'; import type { EuiComboBoxOptionOption } from '@elastic/eui'; +import type { FieldCategory } from '@kbn/timelines-plugin/common/search_strategy'; import { DataProviderType } from '../../../../common/api/timeline'; -import type { BrowserField, BrowserFields } from '../../../common/containers/source'; +import type { BrowserFields } from '../../../common/containers/source'; import { getAllFieldsByName } from '../../../common/containers/source'; import type { QueryOperator } from '../timeline/data_providers/data_provider'; import { @@ -46,7 +47,7 @@ export const operatorLabels: EuiComboBoxOptionOption[] = [ export const EMPTY_ARRAY_RESULT = []; /** Returns the names of fields in a category */ -export const getFieldNames = (category: Partial): string[] => +export const getFieldNames = (category: FieldCategory): string[] => category.fields != null && Object.keys(category.fields).length > 0 ? Object.keys(category.fields) : EMPTY_ARRAY_RESULT; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx index 36233fcc3a391..61bff652c807f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx @@ -71,7 +71,7 @@ const FormattedFieldValueComponent: React.FC<{ isObjectArray?: boolean; isUnifiedDataTable?: boolean; fieldFormat?: string; - fieldFromBrowserField?: BrowserField; + fieldFromBrowserField?: Partial; fieldName: string; fieldType?: string; isButton?: boolean; diff --git a/x-pack/plugins/timelines/common/search_strategy/index_fields/index.ts b/x-pack/plugins/timelines/common/search_strategy/index_fields/index.ts index 81b681dfd812b..ba97282c6de5f 100644 --- a/x-pack/plugins/timelines/common/search_strategy/index_fields/index.ts +++ b/x-pack/plugins/timelines/common/search_strategy/index_fields/index.ts @@ -76,7 +76,6 @@ export interface IndexFieldsStrategyResponse extends IEsSearchResponse { */ export interface BrowserField { aggregatable: boolean; - fields: Record>; // FIXME: missing in FieldSpec format: string; indexes: string[]; // FIXME: missing in FieldSpec name: string; @@ -88,6 +87,12 @@ export interface BrowserField { runtimeField?: RuntimeField; } +type FieldCategoryName = string; + +export interface FieldCategory { + fields: Record>; +} + /** * @deprecated use fields list on dataview / "indexPattern" * about to use browserFields? Reconsider! Maybe you can accomplish @@ -95,7 +100,7 @@ export interface BrowserField { * you are working with? Or perhaps you need a description for a * particular field? Consider using the EcsFlat module from `@kbn/ecs` */ -export type BrowserFields = Record>; +export type BrowserFields = Record; export const EMPTY_BROWSER_FIELDS = {}; export const EMPTY_INDEX_FIELDS: FieldSpec[] = []; From f03fa06d5e9f3028f08ca1e7974d2575ed7d671d Mon Sep 17 00:00:00 2001 From: Jen Huang Date: Wed, 3 Jul 2024 13:19:10 -0700 Subject: [PATCH 09/13] [UII] Fix unsupported input callout not showing for Cloud Defend (#187518) ## Summary Resolves #186785 This PR: - Fixes unsupported input callout in data tagging UI not showing for Cloud Defend - Simplifies the constants list for unsupported inputs - Tweaks copy and UI for empty state to match closer to [designs](https://github.com/elastic/kibana/issues/179915#issuecomment-2034365557) image ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- x-pack/plugins/fleet/common/constants/epm.ts | 38 ++++++------------- .../custom_fields/global_data_tags_table.tsx | 8 ++-- .../custom_fields/index.test.tsx | 4 +- .../custom_fields/index.tsx | 18 ++++++--- 4 files changed, 31 insertions(+), 37 deletions(-) diff --git a/x-pack/plugins/fleet/common/constants/epm.ts b/x-pack/plugins/fleet/common/constants/epm.ts index e10485affa87f..adfbcb299238b 100644 --- a/x-pack/plugins/fleet/common/constants/epm.ts +++ b/x-pack/plugins/fleet/common/constants/epm.ts @@ -25,37 +25,23 @@ export const FLEET_CLOUD_SECURITY_POSTURE_KSPM_POLICY_TEMPLATE = 'kspm'; export const FLEET_CLOUD_SECURITY_POSTURE_CSPM_POLICY_TEMPLATE = 'cspm'; export const FLEET_CLOUD_SECURITY_POSTURE_CNVM_POLICY_TEMPLATE = 'vuln_mgmt'; export const FLEET_CLOUD_DEFEND_PACKAGE = 'cloud_defend'; -export const FLEET_PF_HOST_AGENT_PACKAGE = 'pf-host-agent'; -export const FLEET_PF_ELASTIC_SYMBOLIZER_PACKAGE = 'pf-elastic-symbolizer'; -export const FLEET_PF_ELASTIC_COLLECTOR_PACKAGE = 'pf-elastic-collector'; export const FLEET_CLOUD_BEAT_PACKAGE = 'cloudbeat'; -export const FLEET_CLOUD_BEAT_CIS_K8S_PACKAGE = `${FLEET_CLOUD_BEAT_PACKAGE}/cis_k8s`; -export const FLEET_CLOUD_BEAT_CIS_EKS_PACKAGE = `${FLEET_CLOUD_BEAT_PACKAGE}/cis_eks`; -export const FLEET_CLOUD_BEAT_CIS_AWS_PACKAGE = `${FLEET_CLOUD_BEAT_PACKAGE}/cis_aws`; -export const FLEET_CLOUD_BEAT_CIS_GCP_PACKAGE = `${FLEET_CLOUD_BEAT_PACKAGE}/cis_gcp`; -export const FLEET_CLOUD_BEAT_CIS_AZURE_PACKAGE = `${FLEET_CLOUD_BEAT_PACKAGE}/cis_azure`; -export const FLEET_CLOUD_BEAT_VULN_MGMT_AWS_PACKAGE = `${FLEET_CLOUD_BEAT_PACKAGE}/vuln_mgmt_aws`; export const GLOBAL_DATA_TAG_EXCLUDED_INPUTS = new Set([ FLEET_APM_PACKAGE, - FLEET_PF_HOST_AGENT_PACKAGE, - FLEET_PF_ELASTIC_SYMBOLIZER_PACKAGE, - FLEET_PF_ELASTIC_COLLECTOR_PACKAGE, - /* The package names and input types are not the same. For example package - * name for fleet server is "fleet_server" whereas the input type is "fleet-server". - * This is the same case for cloud defend. That's why we are replacing the - * underscores with dashes for the two of them. Global data tag functionality - * relies on input types. - */ - FLEET_SERVER_PACKAGE.replace(/_/g, '-'), - FLEET_CLOUD_DEFEND_PACKAGE.replace(/_/g, '-'), + `pf-host-agent`, + `pf-elastic-symbolizer`, + `pf-elastic-collector`, + `fleet-server`, + FLEET_CLOUD_DEFEND_PACKAGE, + `${FLEET_CLOUD_DEFEND_PACKAGE}/control`, FLEET_CLOUD_BEAT_PACKAGE, - FLEET_CLOUD_BEAT_CIS_K8S_PACKAGE, - FLEET_CLOUD_BEAT_CIS_EKS_PACKAGE, - FLEET_CLOUD_BEAT_CIS_AWS_PACKAGE, - FLEET_CLOUD_BEAT_CIS_GCP_PACKAGE, - FLEET_CLOUD_BEAT_CIS_AZURE_PACKAGE, - FLEET_CLOUD_BEAT_VULN_MGMT_AWS_PACKAGE, + `${FLEET_CLOUD_BEAT_PACKAGE}/cis_k8s`, + `${FLEET_CLOUD_BEAT_PACKAGE}/cis_eks`, + `${FLEET_CLOUD_BEAT_PACKAGE}/cis_aws`, + `${FLEET_CLOUD_BEAT_PACKAGE}/cis_gcp`, + `${FLEET_CLOUD_BEAT_PACKAGE}/cis_azure`, + `${FLEET_CLOUD_BEAT_PACKAGE}/vuln_mgmt_aws`, ]); export const PACKAGE_TEMPLATE_SUFFIX = '@package'; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/custom_fields/global_data_tags_table.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/custom_fields/global_data_tags_table.tsx index 228b666af38f3..1a30d9ad14fe0 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/custom_fields/global_data_tags_table.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/custom_fields/global_data_tags_table.tsx @@ -20,6 +20,7 @@ import { EuiFieldText, EuiButtonIcon, EuiCode, + EuiSpacer, type EuiBasicTableColumn, } from '@elastic/eui'; @@ -423,15 +424,16 @@ export const GlobalDataTagsTable: React.FunctionComponent = ({ return ( <> {globalDataTags.length === 0 && !isAdding ? ( - + -

+

-
+
+ { renderComponent(mockAgentPolicy); - const unsupportedInputsWarning = renderResult.getByText('Unsupported Inputs'); + const unsupportedInputsWarning = renderResult.getByText('Unsupported inputs'); expect(unsupportedInputsWarning).toBeInTheDocument(); const strongElements = renderResult.container.querySelector('strong'); @@ -87,7 +87,7 @@ describe('CustomFields', () => { ], }); renderComponent(mockAgentPolicy); - expect(renderResult.queryByText('Unsupported Inputs')).not.toBeInTheDocument(); + expect(renderResult.queryByText('Unsupported inputs')).not.toBeInTheDocument(); }); it('should render global data tags table with initial tags', () => { diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/custom_fields/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/custom_fields/index.tsx index ccd761c53e96b..b62d5438b9c1c 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/custom_fields/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/custom_fields/index.tsx @@ -4,11 +4,10 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import React from 'react'; +import styled from 'styled-components'; import { EuiDescribedFormGroup, EuiSpacer, EuiCallOut } from '@elastic/eui'; - import { FormattedMessage } from '@kbn/i18n-react'; -import React from 'react'; import type { NewAgentPolicy, @@ -27,6 +26,13 @@ interface Props { isDisabled?: boolean; } +// Fix to align description to top during empty state w/ unsupported callout +const DescribedFormGroup = styled(EuiDescribedFormGroup)` + .euiFlexGroup { + align-items: flex-start; + } +`; + export const CustomFields: React.FunctionComponent = ({ agentPolicy, updateAgentPolicy, @@ -58,7 +64,7 @@ export const CustomFields: React.FunctionComponent = ({ const unsupportedInputs = findUnsupportedInputs(agentPolicy, GLOBAL_DATA_TAG_EXCLUDED_INPUTS); return ( - @@ -81,7 +87,7 @@ export const CustomFields: React.FunctionComponent = ({ title={ } color="warning" @@ -109,6 +115,6 @@ export const CustomFields: React.FunctionComponent = ({ updateAgentPolicy={updateAgentPolicy} globalDataTags={agentPolicy.global_data_tags ? agentPolicy.global_data_tags : []} /> - + ); }; From e4a44fd23e0d611dc84ddebe8c58ed4a3b8b622c Mon Sep 17 00:00:00 2001 From: Philippe Oberti Date: Wed, 3 Jul 2024 22:36:11 +0200 Subject: [PATCH 10/13] [Security Solution][Notes] - add telemetry (#187362) --- .../control_columns/row_action/index.tsx | 51 ++++++++++--------- .../public/common/lib/telemetry/constants.ts | 2 + .../lib/telemetry/events/notes/index.ts | 35 +++++++++++++ .../lib/telemetry/events/notes/types.ts | 31 +++++++++++ .../lib/telemetry/events/telemetry_events.ts | 6 +++ .../lib/telemetry/telemetry_client.mock.ts | 2 + .../common/lib/telemetry/telemetry_client.ts | 16 ++++++ .../public/common/lib/telemetry/types.ts | 16 +++++- .../left/components/add_note.tsx | 7 ++- .../timeline/body/events/stateful_event.tsx | 7 +++ .../components/timeline/body/index.test.tsx | 4 ++ .../components/timeline/tabs/eql/index.tsx | 10 ++++ .../components/timeline/tabs/pinned/index.tsx | 10 ++++ .../components/timeline/tabs/query/index.tsx | 10 +++- .../unified_components/data_table/index.tsx | 7 ++- 15 files changed, 186 insertions(+), 28 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/common/lib/telemetry/events/notes/index.ts create mode 100644 x-pack/plugins/security_solution/public/common/lib/telemetry/events/notes/types.ts diff --git a/x-pack/plugins/security_solution/public/common/components/control_columns/row_action/index.tsx b/x-pack/plugins/security_solution/public/common/components/control_columns/row_action/index.tsx index a86c1f181485d..416dfcae71c99 100644 --- a/x-pack/plugins/security_solution/public/common/components/control_columns/row_action/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/control_columns/row_action/index.tsx @@ -169,31 +169,36 @@ const RowActionComponent = ({ tabType, ]); - const toggleShowNotes = useCallback( - () => - openFlyout({ - right: { - id: DocumentDetailsRightPanelKey, - params: { - id: eventId, - indexName, - scopeId: tableId, - }, + const toggleShowNotes = useCallback(() => { + openFlyout({ + right: { + id: DocumentDetailsRightPanelKey, + params: { + id: eventId, + indexName, + scopeId: tableId, }, - left: { - id: DocumentDetailsLeftPanelKey, - path: { - tab: LeftPanelNotesTab, - }, - params: { - id: eventId, - indexName, - scopeId: tableId, - }, + }, + left: { + id: DocumentDetailsLeftPanelKey, + path: { + tab: LeftPanelNotesTab, }, - }), - [eventId, indexName, openFlyout, tableId] - ); + params: { + id: eventId, + indexName, + scopeId: tableId, + }, + }, + }); + telemetry.reportOpenNoteInExpandableFlyoutClicked({ + location: tableId, + }); + telemetry.reportDetailsFlyoutOpened({ + location: tableId, + panel: 'left', + }); + }, [eventId, indexName, openFlyout, tableId, telemetry]); const Action = controlColumn.rowCellRender; diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/constants.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/constants.ts index bc9004c8d99c7..f42f77f19a0f9 100644 --- a/x-pack/plugins/security_solution/public/common/lib/telemetry/constants.ts +++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/constants.ts @@ -84,6 +84,8 @@ export enum TelemetryEventTypes { ManualRuleRunCancelJob = 'Manual Rule Run Cancel Job', EventLogFilterByRunType = 'Event Log Filter By Run Type', EventLogShowSourceEventDateRange = 'Event Log -> Show Source -> Event Date Range', + OpenNoteInExpandableFlyoutClicked = 'Open Note In Expandable Flyout Clicked', + AddNoteFromExpandableFlyoutClicked = 'Add Note From Expandable Flyout Clicked', } export enum ML_JOB_TELEMETRY_STATUS { diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/events/notes/index.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/notes/index.ts new file mode 100644 index 0000000000000..c560f69730d36 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/notes/index.ts @@ -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 type { TelemetryEvent } from '../../types'; +import { TelemetryEventTypes } from '../../constants'; + +export const openNoteInExpandableFlyoutClickedEvent: TelemetryEvent = { + eventType: TelemetryEventTypes.OpenNoteInExpandableFlyoutClicked, + schema: { + location: { + type: 'text', + _meta: { + description: 'Table ID or timeline ID', + optional: false, + }, + }, + }, +}; + +export const addNoteFromExpandableFlyoutClickedEvent: TelemetryEvent = { + eventType: TelemetryEventTypes.AddNoteFromExpandableFlyoutClicked, + schema: { + isRelatedToATimeline: { + type: 'boolean', + _meta: { + description: 'If the note was added related to a saved timeline', + optional: false, + }, + }, + }, +}; diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/events/notes/types.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/notes/types.ts new file mode 100644 index 0000000000000..a785f2f8493e1 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/notes/types.ts @@ -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 type { RootSchema } from '@kbn/core/public'; +import type { TelemetryEventTypes } from '../../constants'; + +export interface OpenNoteInExpandableFlyoutClickedParams { + location: string; +} + +export interface AddNoteFromExpandableFlyoutClickedParams { + isRelatedToATimeline: boolean; +} + +export type NotesTelemetryEventParams = + | OpenNoteInExpandableFlyoutClickedParams + | AddNoteFromExpandableFlyoutClickedParams; + +export type NotesTelemetryEvents = + | { + eventType: TelemetryEventTypes.OpenNoteInExpandableFlyoutClicked; + schema: RootSchema; + } + | { + eventType: TelemetryEventTypes.AddNoteFromExpandableFlyoutClicked; + schema: RootSchema; + }; diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/events/telemetry_events.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/telemetry_events.ts index 3cf5fb9b37818..d1f9502346a04 100644 --- a/x-pack/plugins/security_solution/public/common/lib/telemetry/events/telemetry_events.ts +++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/telemetry_events.ts @@ -44,6 +44,10 @@ import { manualRuleRunOpenModalEvent, } from './manual_rule_run'; import { eventLogFilterByRunTypeEvent, eventLogShowSourceEventDateRangeEvent } from './event_log'; +import { + addNoteFromExpandableFlyoutClickedEvent, + openNoteInExpandableFlyoutClickedEvent, +} from './notes'; const mlJobUpdateEvent: TelemetryEvent = { eventType: TelemetryEventTypes.MLJobUpdate, @@ -186,4 +190,6 @@ export const telemetryEvents = [ manualRuleRunOpenModalEvent, eventLogFilterByRunTypeEvent, eventLogShowSourceEventDateRangeEvent, + openNoteInExpandableFlyoutClickedEvent, + addNoteFromExpandableFlyoutClickedEvent, ]; diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_client.mock.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_client.mock.ts index 24057982ed588..02342cb4257be 100644 --- a/x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_client.mock.ts +++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_client.mock.ts @@ -40,4 +40,6 @@ export const createTelemetryClientMock = (): jest.Mocked = reportManualRuleRunCancelJob: jest.fn(), reportManualRuleRunExecute: jest.fn(), reportManualRuleRunOpenModal: jest.fn(), + reportOpenNoteInExpandableFlyoutClicked: jest.fn(), + reportAddNoteFromExpandableFlyoutClicked: jest.fn(), }); diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_client.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_client.ts index 130bbc7817034..0023064adac69 100644 --- a/x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_client.ts +++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_client.ts @@ -6,6 +6,10 @@ */ import type { AnalyticsServiceSetup } from '@kbn/core-analytics-server'; +import type { + AddNoteFromExpandableFlyoutClickedParams, + OpenNoteInExpandableFlyoutClickedParams, +} from './events/notes/types'; import type { TelemetryClientStart, ReportAlertsGroupingChangedParams, @@ -195,4 +199,16 @@ export class TelemetryClient implements TelemetryClientStart { ): void { this.analytics.reportEvent(TelemetryEventTypes.EventLogShowSourceEventDateRange, params); } + + public reportOpenNoteInExpandableFlyoutClicked = ( + params: OpenNoteInExpandableFlyoutClickedParams + ) => { + this.analytics.reportEvent(TelemetryEventTypes.OpenNoteInExpandableFlyoutClicked, params); + }; + + public reportAddNoteFromExpandableFlyoutClicked = ( + params: AddNoteFromExpandableFlyoutClickedParams + ) => { + this.analytics.reportEvent(TelemetryEventTypes.AddNoteFromExpandableFlyoutClicked, params); + }; } diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/types.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/types.ts index 3aba8176d9f67..49c78dc50feeb 100644 --- a/x-pack/plugins/security_solution/public/common/lib/telemetry/types.ts +++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/types.ts @@ -66,6 +66,12 @@ import type { ReportEventLogShowSourceEventDateRangeParams, ReportEventLogTelemetryEventParams, } from './events/event_log/types'; +import type { + AddNoteFromExpandableFlyoutClickedParams, + NotesTelemetryEventParams, + NotesTelemetryEvents, + OpenNoteInExpandableFlyoutClickedParams, +} from './events/notes/types'; export * from './events/ai_assistant/types'; export * from './events/alerts_grouping/types'; @@ -129,7 +135,8 @@ export type TelemetryEventParams = | OnboardingHubStepFinishedParams | OnboardingHubStepLinkClickedParams | ReportManualRuleRunTelemetryEventParams - | ReportEventLogTelemetryEventParams; + | ReportEventLogTelemetryEventParams + | NotesTelemetryEventParams; export interface TelemetryClientStart { reportAlertsGroupingChanged(params: ReportAlertsGroupingChangedParams): void; @@ -183,6 +190,10 @@ export interface TelemetryClientStart { reportEventLogShowSourceEventDateRange( params: ReportEventLogShowSourceEventDateRangeParams ): void; + + // new notes + reportOpenNoteInExpandableFlyoutClicked(params: OpenNoteInExpandableFlyoutClickedParams): void; + reportAddNoteFromExpandableFlyoutClicked(params: AddNoteFromExpandableFlyoutClickedParams): void; } export type TelemetryEvent = @@ -209,4 +220,5 @@ export type TelemetryEvent = } | OnboardingHubTelemetryEvent | ManualRuleRunTelemetryEvent - | EventLogTelemetryEvent; + | EventLogTelemetryEvent + | NotesTelemetryEvents; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/add_note.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/add_note.tsx index 6d66193f30efa..6eea833420cb5 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/add_note.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/add_note.tsx @@ -20,6 +20,7 @@ import { import { css } from '@emotion/react'; import { useDispatch, useSelector } from 'react-redux'; import { i18n } from '@kbn/i18n'; +import { useKibana } from '../../../../common/lib/kibana'; import { TimelineId } from '../../../../../common/types'; import { timelineSelectors } from '../../../../timelines/store'; import { useIsTimelineFlyoutOpen } from '../../shared/hooks/use_is_timeline_flyout_open'; @@ -80,6 +81,7 @@ export interface AddNewNoteProps { * The checkbox is automatically checked if the flyout is opened from a timeline and that timeline is saved. It is disabled if the flyout is NOT opened from a timeline. */ export const AddNote = memo(({ eventId }: AddNewNoteProps) => { + const { telemetry } = useKibana().services; const dispatch = useDispatch(); const { addError: addErrorToast } = useAppToasts(); const [editorValue, setEditorValue] = useState(''); @@ -110,8 +112,11 @@ export const AddNote = memo(({ eventId }: AddNewNoteProps) => { }, }) ); + telemetry.reportAddNoteFromExpandableFlyoutClicked({ + isRelatedToATimeline: checked && activeTimeline?.savedObjectId !== null, + }); setEditorValue(''); - }, [activeTimeline?.savedObjectId, checked, dispatch, editorValue, eventId]); + }, [activeTimeline?.savedObjectId, checked, dispatch, editorValue, eventId, telemetry]); // show a toast if the create note call fails useEffect(() => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx index ce8de17c22216..000837ff83500 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx @@ -14,6 +14,7 @@ import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features'; import { DocumentDetailsRightPanelKey } from '../../../../../flyout/document_details/shared/constants/panel_keys'; import { useDeepEqualSelector } from '../../../../../common/hooks/use_selector'; +import { useKibana } from '../../../../../common/lib/kibana'; import type { ColumnHeaderOptions, CellValueElementProps, @@ -107,6 +108,7 @@ const StatefulEventComponent: React.FC = ({ trailingControlColumns, onToggleShowNotes, }) => { + const { telemetry } = useKibana().services; const trGroupRef = useRef(null); const dispatch = useDispatch(); @@ -224,6 +226,10 @@ const StatefulEventComponent: React.FC = ({ }, }, }); + telemetry.reportDetailsFlyoutOpened({ + location: timelineId, + panel: 'right', + }); } else { // opens the panel when clicking on the table row action dispatch( @@ -241,6 +247,7 @@ const StatefulEventComponent: React.FC = ({ expandableFlyoutDisabled, openFlyout, timelineId, + telemetry, dispatch, tabType, ]); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx index fe46d0b878801..98d3ea8f507bb 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx @@ -38,6 +38,7 @@ import type { DroppableStateSnapshot, } from '@hello-pangea/dnd'; import { DocumentDetailsRightPanelKey } from '../../../../flyout/document_details/shared/constants/panel_keys'; +import { createTelemetryServiceMock } from '../../../../common/lib/telemetry/telemetry_service.mock'; jest.mock('../../../../common/hooks/use_app_toasts'); jest.mock('../../../../common/components/guided_onboarding_tour/tour_step'); @@ -104,6 +105,8 @@ jest.mock('@kbn/expandable-flyout', () => { }; }); +const mockedTelemetry = createTelemetryServiceMock(); + jest.mock('../../../../common/components/link_to', () => { const originalModule = jest.requireActual('../../../../common/components/link_to'); return { @@ -255,6 +258,7 @@ describe('Body', () => { savedObjects: { client: {}, }, + telemetry: mockedTelemetry, timelines: { getLastUpdated: jest.fn(), getLoadingPanel: jest.fn(), diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/eql/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/eql/index.tsx index 6a5ccda2d1677..0d6332ffe805c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/eql/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/eql/index.tsx @@ -17,6 +17,7 @@ import type { EuiDataGridControlColumn } from '@elastic/eui'; import { DataLoadingState } from '@kbn/unified-data-table'; import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; +import { useKibana } from '../../../../../common/lib/kibana'; import { DocumentDetailsLeftPanelKey, DocumentDetailsRightPanelKey, @@ -87,6 +88,7 @@ export const EqlTabContentComponent: React.FC = ({ pinnedEventIds, eventIdToNoteIds, }) => { + const { telemetry } = useKibana().services; const dispatch = useDispatch(); const { query: eqlQuery = '', ...restEqlOption } = eqlOptions; const { portalNode: eqlEventsCountPortalNode } = useEqlEventsCountPortal(); @@ -188,6 +190,13 @@ export const EqlTabContentComponent: React.FC = ({ }, }, }); + telemetry.reportOpenNoteInExpandableFlyoutClicked({ + location: timelineId, + }); + telemetry.reportDetailsFlyoutOpened({ + location: timelineId, + panel: 'left', + }); } else { if (eventId) { setNotesEventId(eventId); @@ -200,6 +209,7 @@ export const EqlTabContentComponent: React.FC = ({ openFlyout, securitySolutionNotesEnabled, selectedPatterns, + telemetry, timelineId, setNotesEventId, showNotesFlyout, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/pinned/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/pinned/index.tsx index ba842d058d42e..76db7bcde9afe 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/pinned/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/pinned/index.tsx @@ -20,6 +20,7 @@ import { DocumentDetailsRightPanelKey, } from '../../../../../flyout/document_details/shared/constants/panel_keys'; import type { ControlColumnProps } from '../../../../../../common/types'; +import { useKibana } from '../../../../../common/lib/kibana'; import { timelineActions, timelineSelectors } from '../../../../store'; import type { Direction } from '../../../../../../common/search_strategy'; import { useTimelineEvents } from '../../../../containers'; @@ -94,6 +95,7 @@ export const PinnedTabContentComponent: React.FC = ({ expandedDetail, eventIdToNoteIds, }) => { + const { telemetry } = useKibana().services; const { browserFields, dataViewId, @@ -224,6 +226,13 @@ export const PinnedTabContentComponent: React.FC = ({ }, }, }); + telemetry.reportOpenNoteInExpandableFlyoutClicked({ + location: timelineId, + }); + telemetry.reportDetailsFlyoutOpened({ + location: timelineId, + panel: 'left', + }); } else { if (eventId) { setNotesEventId(eventId); @@ -236,6 +245,7 @@ export const PinnedTabContentComponent: React.FC = ({ openFlyout, securitySolutionNotesEnabled, selectedPatterns, + telemetry, timelineId, setNotesEventId, showNotesFlyout, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/index.tsx index 001eb31442790..017031e1ffba6 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/index.tsx @@ -116,7 +116,7 @@ export const QueryTabContentComponent: React.FC = ({ selectedPatterns, } = useSourcererDataView(SourcererScopeName.timeline); - const { uiSettings, timelineDataService } = useKibana().services; + const { uiSettings, telemetry, timelineDataService } = useKibana().services; const { query: { filterManager: timelineFilterManager }, } = timelineDataService; @@ -256,6 +256,13 @@ export const QueryTabContentComponent: React.FC = ({ }, }, }); + telemetry.reportOpenNoteInExpandableFlyoutClicked({ + location: timelineId, + }); + telemetry.reportDetailsFlyoutOpened({ + location: timelineId, + panel: 'left', + }); } else { if (eventId) { setNotesEventId(eventId); @@ -268,6 +275,7 @@ export const QueryTabContentComponent: React.FC = ({ openFlyout, securitySolutionNotesEnabled, selectedPatterns, + telemetry, timelineId, showNotesFlyout, setNotesEventId, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/data_table/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/data_table/index.tsx index d41ba9dfcc5d7..e1ecabcc06c9a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/data_table/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/data_table/index.tsx @@ -134,6 +134,7 @@ export const TimelineDataTableComponent: React.FC = memo( storage, dataViewFieldEditor, notifications: { toasts: toastsService }, + telemetry, theme, data: dataPluginContract, }, @@ -187,6 +188,10 @@ export const TimelineDataTableComponent: React.FC = memo( }, }, }); + telemetry.reportDetailsFlyoutOpened({ + location: timelineId, + panel: 'right', + }); } else { dispatch( timelineActions.toggleDetailPanel({ @@ -199,7 +204,7 @@ export const TimelineDataTableComponent: React.FC = memo( activeTimeline.toggleExpandedDetail({ ...updatedExpandedDetail }); }, - [activeTab, dispatch, refetch, timelineId, isExpandableFlyoutDisabled, openFlyout] + [refetch, isExpandableFlyoutDisabled, openFlyout, timelineId, telemetry, dispatch, activeTab] ); const onTimelineLegacyFlyoutClose = useCallback(() => { From 5e353a3a00a23e5616b389084821525184a44c29 Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Wed, 3 Jul 2024 14:46:04 -0600 Subject: [PATCH 11/13] [Embeddables Rebuild] [Controls] Fix data control editor type selector (#187390) Closes https://github.com/elastic/kibana/issues/187382 ## Summary This PR separates out the previously memoized `CompatibleControlTypesComponent` into a separate component that accepts **props** for the fields that it is dependant on rather than relying on the dependencies to the `useMemo` function. This is because, previously, we had an extra dependency in the dependency array (`controlType`) that was causing the memoized component to render too many times and it was causing a weird bug where the old "disabled" menu item wasn't getting unmounted properly. | Before | After | |--------|--------| | ![Jul-02-2024 13-21-44](https://github.com/elastic/kibana/assets/8698078/240b561e-f3b7-4519-bfe1-caf550927310) | ![Jul-02-2024 13-12-29](https://github.com/elastic/kibana/assets/8698078/4f9b4eb6-2ce3-471e-a5b8-4b92179c48bc) | By switching to a component with explicit props, unnecessary dependencies should hopefully be avoided in the future. ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --- .../data_controls/data_control_editor.tsx | 136 ++++++++++-------- 1 file changed, 77 insertions(+), 59 deletions(-) diff --git a/examples/controls_example/public/react_controls/data_controls/data_control_editor.tsx b/examples/controls_example/public/react_controls/data_controls/data_control_editor.tsx index 9f0db921b0778..c35462b66ecb2 100644 --- a/examples/controls_example/public/react_controls/data_controls/data_control_editor.tsx +++ b/examples/controls_example/public/react_controls/data_controls/data_control_editor.tsx @@ -30,7 +30,15 @@ import { EuiTitle, EuiToolTip, } from '@elastic/eui'; +import { + ControlWidth, + DEFAULT_CONTROL_GROW, + DEFAULT_CONTROL_WIDTH, +} from '@kbn/controls-plugin/common'; +import { CONTROL_WIDTH_OPTIONS } from '@kbn/controls-plugin/public'; +import { DataControlFieldRegistry } from '@kbn/controls-plugin/public/types'; import { DataViewField } from '@kbn/data-views-plugin/common'; +import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import { useBatchedPublishingSubjects } from '@kbn/presentation-publishing'; import { LazyDataViewPicker, @@ -38,13 +46,6 @@ import { withSuspense, } from '@kbn/presentation-util-plugin/public'; -import { - ControlWidth, - DEFAULT_CONTROL_GROW, - DEFAULT_CONTROL_WIDTH, -} from '@kbn/controls-plugin/common'; -import { CONTROL_WIDTH_OPTIONS } from '@kbn/controls-plugin/public'; -import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import { getAllControlTypes, getControlFactory } from '../control_factory_registry'; import { ControlGroupApi } from '../control_group/types'; import { ControlStateManager } from '../types'; @@ -69,6 +70,65 @@ export interface ControlEditorProps< const FieldPicker = withSuspense(LazyFieldPicker, null); const DataViewPicker = withSuspense(LazyDataViewPicker, null); +const CompatibleControlTypesComponent = ({ + fieldRegistry, + selectedFieldName, + selectedControlType, + setSelectedControlType, +}: { + fieldRegistry?: DataControlFieldRegistry; + selectedFieldName: string; + selectedControlType?: string; + setSelectedControlType: (type: string) => void; +}) => { + const dataControlFactories = useMemo(() => { + return getAllControlTypes() + .map((type) => getControlFactory(type)) + .filter((factory) => { + return isDataControlFactory(factory); + }); + }, []); + + return ( + + {dataControlFactories.map((factory) => { + const disabled = + fieldRegistry && selectedFieldName + ? !fieldRegistry[selectedFieldName]?.compatibleControlTypes.includes(factory.type) + : true; + const keyPadMenuItem = ( + setSelectedControlType(factory.type)} + label={factory.getDisplayName()} + > + + + ); + + return disabled ? ( + + {keyPadMenuItem} + + ) : ( + keyPadMenuItem + ); + })} + + ); +}; + export const DataControlEditor = ({ controlId, controlType, @@ -139,57 +199,6 @@ export const DataControlEditor = ({ ); }, [selectedFieldName, setControlEditorValid, selectedDataView, selectedControlType]); - const dataControlFactories = useMemo(() => { - return getAllControlTypes() - .map((type) => getControlFactory(type)) - .filter((factory) => { - return isDataControlFactory(factory); - }); - }, []); - - const CompatibleControlTypesComponent = useMemo(() => { - return ( - - {dataControlFactories.map((factory) => { - const disabled = - fieldRegistry && selectedFieldName - ? !fieldRegistry[selectedFieldName]?.compatibleControlTypes.includes(factory.type) - : true; - const keyPadMenuItem = ( - setSelectedControlType(factory.type)} - label={factory.getDisplayName()} - > - - - ); - - return disabled ? ( - - {keyPadMenuItem} - - ) : ( - keyPadMenuItem - ); - })} - - ); - }, [selectedFieldName, fieldRegistry, selectedControlType, controlType, dataControlFactories]); - const CustomSettingsComponent = useMemo(() => { if (!selectedControlType || !selectedFieldName || !fieldRegistry) return; @@ -254,6 +263,7 @@ export const DataControlEditor = ({ selectedDataViewId={selectedDataViewId} onChangeDataViewId={(newDataViewId) => { stateManager.dataViewId.next(newDataViewId); + setSelectedControlType(undefined); }} trigger={{ label: @@ -300,7 +310,15 @@ export const DataControlEditor = ({ - {CompatibleControlTypesComponent} + {/* wrapping in `div` so that focus gets passed properly to the form row */} +
+ +
Date: Wed, 3 Jul 2024 14:00:44 -0700 Subject: [PATCH 12/13] [CloudSecurity] Converting Findings DistributionBar FTR into integration test (#186938) ## Summary It closes #176700 This PR converts the DistributionBar FTR test on the Findings page into an integration test using MSW. It also closes #176700as it was once triggering an error in the past Also, it adds the following changes: - Added a `generateMultipleCspFindings` helper to help with the writing of future tests and generating batch data. - Removed DistributionBar FTR test - Removed the extra layer of sub-components on the DistributionBar component to be simpler and added an aria-label on the distribution bar buttons. ## Screenshots ![image](https://github.com/elastic/kibana/assets/19270322/ee4abc0e-1f60-46d0-afe7-48bce93bf24a) ![image](https://github.com/elastic/kibana/assets/19270322/bf443121-eb14-4ae5-b9aa-dea662410da4) --- .../use_cloud_posture_data_table.ts | 2 +- .../configurations.handlers.mock.ts | 28 ++++- .../configurations/configurations.test.tsx | 106 ++++++++++++++++++ .../layout/findings_distribution_bar.tsx | 77 +++++-------- .../pages/findings.ts | 14 --- 5 files changed, 160 insertions(+), 67 deletions(-) diff --git a/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_data_table/use_cloud_posture_data_table.ts b/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_data_table/use_cloud_posture_data_table.ts index 03517383ecc3f..ae8ddb48488c1 100644 --- a/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_data_table/use_cloud_posture_data_table.ts +++ b/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_data_table/use_cloud_posture_data_table.ts @@ -137,7 +137,7 @@ export const useCloudPostureDataTable = ({ return { setUrlQuery, sort: urlQuery.sort, - filters: urlQuery.filters, + filters: urlQuery.filters || [], query: baseEsQuery.query ? baseEsQuery.query : { diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/configurations.handlers.mock.ts b/x-pack/plugins/cloud_security_posture/public/pages/configurations/configurations.handlers.mock.ts index 38e4edf46f77a..c5fb197583dd9 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/configurations.handlers.mock.ts +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/configurations.handlers.mock.ts @@ -11,6 +11,15 @@ import { isArray } from 'lodash'; import { http, HttpResponse } from 'msw'; import { v4 as uuidV4 } from 'uuid'; +export const generateMultipleCspFindings = ( + option: { count: number; failedCount?: number } = { count: 1, failedCount: 0 } +) => { + const failedCount = option.failedCount || 0; + return Array.from({ length: option?.count }, (_, i) => { + return generateCspFinding(i.toString(), i < failedCount ? 'failed' : 'passed'); + }); +}; + export const generateCspFinding = ( id: string, evaluation: 'failed' | 'passed' = 'passed' @@ -211,25 +220,36 @@ export const bsearchFindingsHandler = (findings: CspFinding[]) => filter[0]?.bool?.should?.[0]?.term?.['rule.section']?.value !== undefined; if (hasRuleSectionQuerySearchTerm) { - const filteredFindingJson = findings.filter((finding) => { + const filteredFindings = findings.filter((finding) => { const termValue = (filter[0].bool?.should as estypes.QueryDslQueryContainer[])?.[0]?.term?.[ 'rule.section' ]?.value; return finding.rule.section === termValue; }); - return HttpResponse.json(getFindingsBsearchResponse(filteredFindingJson)); + return HttpResponse.json(getFindingsBsearchResponse(filteredFindings)); } const hasRuleSectionFilter = isArray(filter) && filter?.[0]?.match_phrase?.['rule.section'] !== undefined; if (hasRuleSectionFilter) { - const filteredFindingJson = findings.filter((finding) => { + const filteredFindings = findings.filter((finding) => { return finding.rule.section === filter?.[0]?.match_phrase?.['rule.section']; }); - return HttpResponse.json(getFindingsBsearchResponse(filteredFindingJson)); + return HttpResponse.json(getFindingsBsearchResponse(filteredFindings)); + } + + const hasResultEvaluationFilter = + isArray(filter) && filter?.[0]?.match_phrase?.['result.evaluation'] !== undefined; + + if (hasResultEvaluationFilter) { + const filteredFindings = findings.filter((finding) => { + return finding.result.evaluation === filter?.[0]?.match_phrase?.['result.evaluation']; + }); + + return HttpResponse.json(getFindingsBsearchResponse(filteredFindings)); } return HttpResponse.json(getFindingsBsearchResponse(findings)); diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/configurations.test.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/configurations.test.tsx index 324eddd1fd8fc..9ff1b40d49c70 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/configurations.test.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/configurations.test.tsx @@ -22,6 +22,7 @@ import * as statusHandlers from '../../../server/routes/status/status.handlers.m import { bsearchFindingsHandler, generateCspFinding, + generateMultipleCspFindings, rulesGetStatesHandler, } from './configurations.handlers.mock'; @@ -247,4 +248,109 @@ describe('', () => { expect(screen.getByText(finding2.resource.name)).toBeInTheDocument(); }); }); + + describe('DistributionBar', () => { + it('renders the distribution bar', async () => { + server.use(statusHandlers.indexedHandler); + server.use( + bsearchFindingsHandler( + generateMultipleCspFindings({ + count: 10, + failedCount: 3, + }) + ) + ); + + renderFindingsPage(); + + // Loading while checking the status API + expect(screen.getByText(/loading/i)).toBeInTheDocument(); + + await waitFor(() => expect(screen.getByText(/10 findings/i)).toBeInTheDocument()); + + screen.getByRole('button', { + name: /passed findings: 7/i, + }); + screen.getByRole('button', { + name: /failed findings: 3/i, + }); + + // Assert that the distribution bar has the correct percentages rendered + expect(screen.getByTestId('distribution_bar_passed')).toHaveStyle('flex: 7'); + expect(screen.getByTestId('distribution_bar_failed')).toHaveStyle('flex: 3'); + }); + + it('filters by passed findings when clicking on the passed findings button', async () => { + server.use(statusHandlers.indexedHandler); + server.use( + bsearchFindingsHandler( + generateMultipleCspFindings({ + count: 2, + failedCount: 1, + }) + ) + ); + + renderFindingsPage(); + + // Loading while checking the status API + expect(screen.getByText(/loading/i)).toBeInTheDocument(); + + await waitFor(() => expect(screen.getByText(/2 findings/i)).toBeInTheDocument()); + + const passedFindingsButton = screen.getByRole('button', { + name: /passed findings: 1/i, + }); + userEvent.click(passedFindingsButton); + + await waitFor(() => expect(screen.getByText(/1 findings/i)).toBeInTheDocument()); + + screen.getByRole('button', { + name: /passed findings: 1/i, + }); + screen.getByRole('button', { + name: /failed findings: 0/i, + }); + + // Assert that the distribution bar has the correct percentages rendered + expect(screen.getByTestId('distribution_bar_passed')).toHaveStyle('flex: 1'); + expect(screen.getByTestId('distribution_bar_failed')).toHaveStyle('flex: 0'); + }, 10000); + it('filters by failed findings when clicking on the failed findings button', async () => { + server.use(statusHandlers.indexedHandler); + server.use( + bsearchFindingsHandler( + generateMultipleCspFindings({ + count: 2, + failedCount: 1, + }) + ) + ); + + renderFindingsPage(); + + // Loading while checking the status API + expect(screen.getByText(/loading/i)).toBeInTheDocument(); + + await waitFor(() => expect(screen.getByText(/2 findings/i)).toBeInTheDocument()); + + const failedFindingsButton = screen.getByRole('button', { + name: /failed findings: 1/i, + }); + userEvent.click(failedFindingsButton); + + await waitFor(() => expect(screen.getByText(/1 findings/i)).toBeInTheDocument()); + + screen.getByRole('button', { + name: /passed findings: 0/i, + }); + screen.getByRole('button', { + name: /failed findings: 1/i, + }); + + // Assert that the distribution bar has the correct percentages rendered + expect(screen.getByTestId('distribution_bar_passed')).toHaveStyle('flex: 0'); + expect(screen.getByTestId('distribution_bar_failed')).toHaveStyle('flex: 1'); + }, 10000); + }); }); diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/layout/findings_distribution_bar.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/layout/findings_distribution_bar.tsx index 35f6fd008d4b9..56ca9687551d8 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/layout/findings_distribution_bar.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/layout/findings_distribution_bar.tsx @@ -11,7 +11,6 @@ import { EuiBadge, EuiSpacer, EuiFlexGroup, - EuiFlexItem, useEuiTheme, EuiTextColor, } from '@elastic/eui'; @@ -28,6 +27,14 @@ interface Props { distributionOnClick: (evaluation: Evaluation) => void; } +const I18N_PASSED_FINDINGS = i18n.translate('xpack.csp.findings.distributionBar.totalPassedLabel', { + defaultMessage: 'Passed Findings', +}); + +const I18N_FAILED_FINDINGS = i18n.translate('xpack.csp.findings.distributionBar.totalFailedLabel', { + defaultMessage: 'Failed Findings', +}); + export const CurrentPageOfTotal = ({ pageEnd, pageStart, @@ -60,42 +67,21 @@ export const FindingsDistributionBar = (props: Props) => (
); - -const Counters = (props: Props) => ( - - - - - - - -); - -const PassedFailedCounters = ({ passed, failed }: Pick) => { +const Counters = ({ passed, failed }: Pick) => { const { euiTheme } = useEuiTheme(); + return ( -
- - -
+ {I18N_PASSED_FINDINGS} + {getAbbreviatedNumber(passed)} + {I18N_FAILED_FINDINGS} + {getAbbreviatedNumber(failed)} + ); }; @@ -121,6 +107,7 @@ const DistributionBar: React.FC> = ({ distributionOnClick(RULE_PASSED); }} data-test-subj="distribution_bar_passed" + aria-label={`${I18N_PASSED_FINDINGS}: ${passed}`} /> > = ({ distributionOnClick(RULE_FAILED); }} data-test-subj="distribution_bar_failed" + aria-label={`${I18N_FAILED_FINDINGS}: ${failed}`} /> ); @@ -144,25 +132,18 @@ const DistributionBarPart = ({ color: string; distributionOnClick: () => void; ['data-test-subj']: string; + ['aria-label']: string; }) => (