diff --git a/examples/controls_example/public/react_controls/control_group/components/control_group.tsx b/examples/controls_example/public/react_controls/control_group/components/control_group.tsx index c8eb00b52ed9d..9608d8b082f40 100644 --- a/examples/controls_example/public/react_controls/control_group/components/control_group.tsx +++ b/examples/controls_example/public/react_controls/control_group/components/control_group.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { BehaviorSubject } from 'rxjs'; import { DndContext, @@ -98,64 +98,84 @@ export function ControlGroup({ }; }, [controlGroupApi]); + const ApplyButtonComponent = useMemo(() => { + return ( + + ); + }, [hasUnappliedSelections, applySelections]); + return ( - - + + {!isInitialized && } - setDraggingId(`${active.id}`)} - onDragEnd={onDragEnd} - onDragCancel={() => setDraggingId(null)} - sensors={sensors} - measuring={{ - droppable: { - strategy: MeasuringStrategy.BeforeDragging, - }, - }} - > - - {controlsInOrder.map(({ id, type }) => ( - controlGroupApi} - onApiAvailable={(controlApi) => { - controlsManager.setControlApi(id, controlApi); - }} - isControlGroupInitialized={isInitialized} - /> - ))} - - - {draggingId ? ( - - ) : null} - - - {!autoApplySelections && ( - - - - + + setDraggingId(`${active.id}`)} + onDragEnd={onDragEnd} + onDragCancel={() => setDraggingId(null)} + sensors={sensors} + measuring={{ + droppable: { + strategy: MeasuringStrategy.BeforeDragging, + }, + }} + > + + + {controlsInOrder.map(({ id, type }) => ( + controlGroupApi} + onApiAvailable={(controlApi) => { + controlsManager.setControlApi(id, controlApi); + }} + isControlGroupInitialized={isInitialized} + /> + ))} + + + + {draggingId ? ( + + ) : null} + + + + {isInitialized && !autoApplySelections && ( + + {hasUnappliedSelections ? ( + ApplyButtonComponent + ) : ( + + {ApplyButtonComponent} + + )} )} diff --git a/examples/controls_example/public/react_controls/timeslider_control/components/index.scss b/examples/controls_example/public/react_controls/timeslider_control/components/index.scss index 9fb6510b1a934..de3677656848a 100644 --- a/examples/controls_example/public/react_controls/timeslider_control/components/index.scss +++ b/examples/controls_example/public/react_controls/timeslider_control/components/index.scss @@ -1,5 +1,8 @@ -.timeSlider-playToggle:enabled { - background-color: $euiColorPrimary !important; +.timeSlider-playToggle { + height: 100%; + &:enabled { + background-color: $euiColorPrimary !important; + } } .timeSlider-prependButton { diff --git a/examples/controls_example/public/react_controls/timeslider_control/components/play_button.tsx b/examples/controls_example/public/react_controls/timeslider_control/components/play_button.tsx index c30bff5c2926d..8fe19bca8a054 100644 --- a/examples/controls_example/public/react_controls/timeslider_control/components/play_button.tsx +++ b/examples/controls_example/public/react_controls/timeslider_control/components/play_button.tsx @@ -41,7 +41,11 @@ export function PlayButton(props: Props) { /> ); return props.disablePlayButton ? ( - + {Button} ) : ( diff --git a/examples/response_stream/common/api/reducer_stream/index.ts b/examples/response_stream/common/api/reducer_stream/index.ts index cc5255761fbd6..13f3c0274f771 100644 --- a/examples/response_stream/common/api/reducer_stream/index.ts +++ b/examples/response_stream/common/api/reducer_stream/index.ts @@ -7,5 +7,3 @@ */ export { reducerStreamReducer } from './reducer'; -export { reducerStreamRequestBodySchema } from './request_body_schema'; -export type { ReducerStreamRequestBodySchema } from './request_body_schema'; diff --git a/examples/response_stream/server/routes/reducer_stream.ts b/examples/response_stream/server/routes/reducer_stream.ts index abdc90f28a23c..9a84d3009b6c3 100644 --- a/examples/response_stream/server/routes/reducer_stream.ts +++ b/examples/response_stream/server/routes/reducer_stream.ts @@ -16,7 +16,7 @@ import { deleteEntityAction, ReducerStreamApiAction, } from '../../common/api/reducer_stream/reducer_actions'; -import { reducerStreamRequestBodySchema } from '../../common/api/reducer_stream'; +import { reducerStreamRequestBodySchema } from './schemas/reducer_stream'; import { RESPONSE_STREAM_API_ENDPOINT } from '../../common/api'; import { entities, getActions } from './shared'; diff --git a/examples/response_stream/server/routes/redux_stream.ts b/examples/response_stream/server/routes/redux_stream.ts index bd694c531907b..700e1ff3d06c4 100644 --- a/examples/response_stream/server/routes/redux_stream.ts +++ b/examples/response_stream/server/routes/redux_stream.ts @@ -16,7 +16,7 @@ import { error, type ReduxStreamApiAction, } from '../../common/api/redux_stream/data_slice'; -import { reducerStreamRequestBodySchema } from '../../common/api/reducer_stream'; +import { reducerStreamRequestBodySchema } from './schemas/reducer_stream'; import { RESPONSE_STREAM_API_ENDPOINT } from '../../common/api'; import { entities, getActions } from './shared'; diff --git a/examples/response_stream/server/routes/schemas/reducer_stream/index.ts b/examples/response_stream/server/routes/schemas/reducer_stream/index.ts new file mode 100644 index 0000000000000..62247bb3f3045 --- /dev/null +++ b/examples/response_stream/server/routes/schemas/reducer_stream/index.ts @@ -0,0 +1,10 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { reducerStreamRequestBodySchema } from './request_body_schema'; +export type { ReducerStreamRequestBodySchema } from './request_body_schema'; diff --git a/examples/response_stream/common/api/reducer_stream/request_body_schema.ts b/examples/response_stream/server/routes/schemas/reducer_stream/request_body_schema.ts similarity index 100% rename from examples/response_stream/common/api/reducer_stream/request_body_schema.ts rename to examples/response_stream/server/routes/schemas/reducer_stream/request_body_schema.ts diff --git a/examples/response_stream/common/api/simple_string_stream/index.ts b/examples/response_stream/server/routes/schemas/simple_string_stream/index.ts similarity index 100% rename from examples/response_stream/common/api/simple_string_stream/index.ts rename to examples/response_stream/server/routes/schemas/simple_string_stream/index.ts diff --git a/examples/response_stream/common/api/simple_string_stream/request_body_schema.ts b/examples/response_stream/server/routes/schemas/simple_string_stream/request_body_schema.ts similarity index 100% rename from examples/response_stream/common/api/simple_string_stream/request_body_schema.ts rename to examples/response_stream/server/routes/schemas/simple_string_stream/request_body_schema.ts diff --git a/examples/response_stream/server/routes/single_string_stream.ts b/examples/response_stream/server/routes/single_string_stream.ts index d9cb65686b71e..daf0ae682a14e 100644 --- a/examples/response_stream/server/routes/single_string_stream.ts +++ b/examples/response_stream/server/routes/single_string_stream.ts @@ -9,7 +9,7 @@ import type { IRouter, Logger } from '@kbn/core/server'; import { streamFactory } from '@kbn/ml-response-stream/server'; -import { simpleStringStreamRequestBodySchema } from '../../common/api/simple_string_stream'; +import { simpleStringStreamRequestBodySchema } from './schemas/simple_string_stream'; import { RESPONSE_STREAM_API_ENDPOINT } from '../../common/api'; function timeout(ms: number) { diff --git a/fleet_packages.json b/fleet_packages.json index 76dd27e8c27f0..d0ee7ad089924 100644 --- a/fleet_packages.json +++ b/fleet_packages.json @@ -30,11 +30,11 @@ }, { "name": "elastic_agent", - "version": "1.20.0" + "version": "2.0.1" }, { "name": "endpoint", - "version": "8.14.0" + "version": "8.15.0" }, { "name": "fleet_server", @@ -52,10 +52,10 @@ }, { "name": "synthetics", - "version": "1.2.1" + "version": "1.2.2" }, { "name": "security_detection_engine", - "version": "8.14.3" + "version": "8.15.1" } ] \ No newline at end of file diff --git a/packages/kbn-cli-dev-mode/kibana.jsonc b/packages/kbn-cli-dev-mode/kibana.jsonc index 3c55d047b0efc..fcacb342273f5 100644 --- a/packages/kbn-cli-dev-mode/kibana.jsonc +++ b/packages/kbn-cli-dev-mode/kibana.jsonc @@ -1,5 +1,5 @@ { - "type": "shared-common", + "type": "shared-server", "id": "@kbn/cli-dev-mode", "devOnly": true, "owner": "@elastic/kibana-operations" diff --git a/src/plugins/console/public/styles/_app.scss b/src/plugins/console/public/styles/_app.scss index f2353821ec93f..161fd913c32ae 100644 --- a/src/plugins/console/public/styles/_app.scss +++ b/src/plugins/console/public/styles/_app.scss @@ -131,7 +131,7 @@ * The highlighting for the selected requests in the monaco editor */ .console__monaco_editor__selectedRequests { - background: transparentize($euiColorLightShade, .3); + background: transparentize($euiColorPrimary, .9); } /* * The z-index for the autocomplete suggestions popup 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 85b6360b409be..af6ac01fb18a4 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 @@ -13,6 +13,7 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { OpenAiProviderType } from '@kbn/stack-connectors-plugin/public/common'; import { noop } from 'lodash/fp'; import { PromptResponse } from '@kbn/elastic-assistant-common'; +import { QueryObserverResult } from '@tanstack/react-query'; import { Conversation } from '../../../..'; import * as i18n from './translations'; import * as i18nModel from '../../../connectorland/models/model_selector/translations'; @@ -37,6 +38,7 @@ export interface ConversationSettingsEditorProps { React.SetStateAction >; onSelectedConversationChange: (conversation?: Conversation) => void; + refetchConversations?: () => Promise, unknown>>; } /** @@ -53,6 +55,7 @@ export const ConversationSettingsEditor: React.FC { const { data: connectors, isSuccess: areConnectorsFetched } = useLoadConnectors({ http, @@ -276,6 +279,7 @@ export const ConversationSettingsEditor: React.FC = ({ conversationsSettingsBulkActions={conversationsSettingsBulkActions} http={http} isDisabled={isDisabled} + refetchConversations={refetchConversations} selectedConversation={selectedConversation} setConversationSettings={setConversationSettings} setConversationsSettingsBulkActions={setConversationsSettingsBulkActions} 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 e4457a8f38d4d..ff2ead2aeb386 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/index.tsx @@ -256,6 +256,13 @@ const AssistantComponent: React.FC = ({ conversations[WELCOME_CONVERSATION_TITLE] ?? getDefaultConversation({ cTitle: WELCOME_CONVERSATION_TITLE }); + // updated selected system prompt + setEditingSystemPromptId( + getDefaultSystemPrompt({ + allSystemPrompts, + conversation: conversationToReturn, + })?.id + ); if ( prev && prev.id === conversationToReturn.id && @@ -273,6 +280,7 @@ const AssistantComponent: React.FC = ({ }); } }, [ + allSystemPrompts, areConnectorsFetched, conversationTitle, conversations, @@ -647,6 +655,7 @@ const AssistantComponent: React.FC = ({ actionTypeId: (defaultConnector?.actionTypeId as string) ?? '.gen-ai', provider: apiConfig?.apiProvider, model: apiConfig?.defaultModel, + defaultSystemPromptId: allSystemPrompts.find((sp) => sp.isNewConversationDefault)?.id, }, }); }, @@ -665,14 +674,14 @@ const AssistantComponent: React.FC = ({ useEffect(() => { (async () => { - if (areConnectorsFetched && currentConversation?.id === '') { + if (areConnectorsFetched && currentConversation?.id === '' && !isLoadingPrompts) { const conversation = await mutateAsync(currentConversation); if (currentConversation.id === '' && conversation) { setCurrentConversationId(conversation.id); } } })(); - }, [areConnectorsFetched, currentConversation, mutateAsync]); + }, [areConnectorsFetched, currentConversation, isLoadingPrompts, mutateAsync]); const handleCreateConversation = useCallback(async () => { const newChatExists = find(conversations, ['title', NEW_CHAT]); @@ -791,6 +800,7 @@ const AssistantComponent: React.FC = ({ isSettingsModalVisible={isSettingsModalVisible} setIsSettingsModalVisible={setIsSettingsModalVisible} allSystemPrompts={allSystemPrompts} + refetchConversations={refetchResults} /> @@ -823,6 +833,7 @@ const AssistantComponent: React.FC = ({ handleOnSystemPromptSelectionChange, isSettingsModalVisible, isWelcomeSetup, + refetchResults, ]); return ( 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 f13441a3102f9..9feb3e53aa8bc 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, isCleared: false }); expect(option.value).toBe(mockSystemPrompt.id); }); it('returns an EuiSuperSelectOption with the correct inputDisplay', () => { - const option = getOptionFromPrompt({ ...mockSystemPrompt }); + const option = getOptionFromPrompt({ ...mockSystemPrompt, isCleared: 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, isCleared: false }); 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, isCleared: false }); render({option.dropdownDisplay}); @@ -51,7 +51,7 @@ describe('helpers', () => { const prompts = [mockSystemPrompt, mockSuperheroSystemPrompt]; const promptIds = prompts.map(({ id }) => id); - const options = getOptions({ prompts }); + const options = getOptions({ prompts, isCleared: false }); const optionValues = options.map(({ value }) => value); expect(optionValues).toEqual(promptIds); 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 92814927f980a..b5efd08b28f9c 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 @@ -12,19 +12,38 @@ import styled from '@emotion/styled'; import { isEmpty } from 'lodash/fp'; import { euiThemeVars } from '@kbn/ui-theme'; import { PromptResponse } from '@kbn/elastic-assistant-common'; +import { css } from '@emotion/react'; import { EMPTY_PROMPT } from './translations'; const Strong = styled.strong` margin-right: ${euiThemeVars.euiSizeS}; `; +interface GetOptionFromPromptProps extends PromptResponse { + content: string; + id: string; + name: string; + isCleared: boolean; +} + export const getOptionFromPrompt = ({ content, id, + isCleared, name, -}: PromptResponse): EuiSuperSelectOption => ({ +}: GetOptionFromPromptProps): EuiSuperSelectOption => ({ value: id, - inputDisplay: {name}, + inputDisplay: ( + + {name} + + ), dropdownDisplay: ( <> {name} @@ -41,6 +60,10 @@ export const getOptionFromPrompt = ({ interface GetOptionsProps { prompts: PromptResponse[] | undefined; + isCleared: boolean; } -export const getOptions = ({ prompts }: GetOptionsProps): Array> => - prompts?.map(getOptionFromPrompt) ?? []; +export const getOptions = ({ + prompts, + isCleared, +}: GetOptionsProps): Array> => + prompts?.map((p) => getOptionFromPrompt({ ...p, isCleared })) ?? []; 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 01fe334eb1f7d..592d91fe98326 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 @@ -5,8 +5,9 @@ * 2.0. */ -import React, { useCallback, useMemo } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import { PromptResponse } from '@kbn/elastic-assistant-common'; +import { QueryObserverResult } from '@tanstack/react-query'; import { Conversation } from '../../../..'; import { SelectSystemPrompt } from './select_system_prompt'; @@ -17,6 +18,7 @@ interface Props { onSystemPromptSelectionChange: (systemPromptId: string | undefined) => void; setIsSettingsModalVisible: React.Dispatch>; allSystemPrompts: PromptResponse[]; + refetchConversations?: () => Promise, unknown>>; } const SystemPromptComponent: React.FC = ({ @@ -26,9 +28,12 @@ const SystemPromptComponent: React.FC = ({ onSystemPromptSelectionChange, setIsSettingsModalVisible, allSystemPrompts, + refetchConversations, }) => { + const [isCleared, setIsCleared] = useState(false); const selectedPrompt = useMemo(() => { if (editingSystemPromptId !== undefined) { + setIsCleared(false); return allSystemPrompts.find((p) => p.id === editingSystemPromptId); } else { return allSystemPrompts.find((p) => p.id === conversation?.apiConfig?.defaultSystemPromptId); @@ -36,10 +41,21 @@ const SystemPromptComponent: React.FC = ({ }, [allSystemPrompts, conversation?.apiConfig?.defaultSystemPromptId, editingSystemPromptId]); const handleClearSystemPrompt = useCallback(() => { - if (conversation) { + if (editingSystemPromptId === undefined) { + setIsCleared(false); + onSystemPromptSelectionChange( + allSystemPrompts.find((p) => p.id === conversation?.apiConfig?.defaultSystemPromptId)?.id + ); + } else { + setIsCleared(true); onSystemPromptSelectionChange(undefined); } - }, [conversation, onSystemPromptSelectionChange]); + }, [ + allSystemPrompts, + conversation?.apiConfig?.defaultSystemPromptId, + editingSystemPromptId, + onSystemPromptSelectionChange, + ]); return ( = ({ conversation={conversation} data-test-subj="systemPrompt" isClearable={true} + isCleared={isCleared} + refetchConversations={refetchConversations} isSettingsModalVisible={isSettingsModalVisible} onSystemPromptSelectionChange={onSystemPromptSelectionChange} selectedPrompt={selectedPrompt} 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 fbe0f40320c4c..08a6888b30626 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 @@ -22,6 +22,7 @@ import { PromptResponse, PromptTypeEnum, } from '@kbn/elastic-assistant-common/impl/schemas/prompts/bulk_crud_prompts_route.gen'; +import { QueryObserverResult } from '@tanstack/react-query'; import { Conversation } from '../../../../..'; import { getOptions } from '../helpers'; import * as i18n from '../translations'; @@ -38,6 +39,7 @@ export interface Props { selectedPrompt: PromptResponse | undefined; clearSelectedSystemPrompt?: () => void; isClearable?: boolean; + isCleared?: boolean; isDisabled?: boolean; isOpen?: boolean; isSettingsModalVisible: boolean; @@ -46,6 +48,7 @@ export interface Props { onSelectedConversationChange?: (result: Conversation) => void; setConversationSettings?: React.Dispatch>>; setConversationsSettingsBulkActions?: React.Dispatch>; + refetchConversations?: () => Promise, unknown>>; } const ADD_NEW_SYSTEM_PROMPT = 'ADD_NEW_SYSTEM_PROMPT'; @@ -57,8 +60,10 @@ const SelectSystemPromptComponent: React.FC = ({ selectedPrompt, clearSelectedSystemPrompt, isClearable = false, + isCleared = false, isDisabled = false, isOpen = false, + refetchConversations, isSettingsModalVisible, onSystemPromptSelectionChange, setIsSettingsModalVisible, @@ -89,10 +94,11 @@ const SelectSystemPromptComponent: React.FC = ({ defaultSystemPromptId: promptId, }, }); + await refetchConversations?.(); return result; } }, - [conversation, setApiConfig] + [conversation, refetchConversations, setApiConfig] ); const addNewSystemPrompt = useMemo(() => { @@ -116,7 +122,10 @@ const SelectSystemPromptComponent: React.FC = ({ }, []); // SuperSelect State/Actions - const options = useMemo(() => getOptions({ prompts: allSystemPrompts }), [allSystemPrompts]); + const options = useMemo( + () => getOptions({ prompts: allSystemPrompts, isCleared }), + [allSystemPrompts, isCleared] + ); const onChange = useCallback( async (selectedSystemPromptId) => { @@ -160,9 +169,8 @@ const SelectSystemPromptComponent: React.FC = ({ ); const clearSystemPrompt = useCallback(() => { - setSelectedSystemPrompt(undefined); clearSelectedSystemPrompt?.(); - }, [clearSelectedSystemPrompt, setSelectedSystemPrompt]); + }, [clearSelectedSystemPrompt]); return ( = ({ inline-size: 16px; block-size: 16px; border-radius: 16px; - background: ${euiThemeVars.euiColorMediumShade}; + background: ${isCleared + ? euiThemeVars.euiColorLightShade + : euiThemeVars.euiColorMediumShade}; :hover:not(:disabled) { - background: ${euiThemeVars.euiColorMediumShade}; + background: ${isCleared + ? euiThemeVars.euiColorLightShade + : euiThemeVars.euiColorMediumShade}; transform: none; } diff --git a/x-pack/plugins/alerting/common/routes/rule/apis/unmute_alert/index.ts b/x-pack/plugins/alerting/common/routes/rule/apis/unmute_alert/index.ts new file mode 100644 index 0000000000000..202402bdda607 --- /dev/null +++ b/x-pack/plugins/alerting/common/routes/rule/apis/unmute_alert/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { unmuteAlertParamsSchema } from './schemas/latest'; +export { unmuteAlertParamsSchema as unmuteAlertParamsSchemaV1 } from './schemas/v1'; + +export type { UnmuteAlertRequestParams } from './types/latest'; +export type { UnmuteAlertRequestParams as UnmuteAlertRequestParamsV1 } from './types/v1'; diff --git a/x-pack/plugins/alerting/common/routes/rule/apis/unmute_alert/schemas/latest.ts b/x-pack/plugins/alerting/common/routes/rule/apis/unmute_alert/schemas/latest.ts new file mode 100644 index 0000000000000..e560bd87e0491 --- /dev/null +++ b/x-pack/plugins/alerting/common/routes/rule/apis/unmute_alert/schemas/latest.ts @@ -0,0 +1,7 @@ +/* + * 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 { unmuteAlertParamsSchema } from './v1'; diff --git a/x-pack/plugins/alerting/common/routes/rule/apis/unmute_alert/schemas/v1.ts b/x-pack/plugins/alerting/common/routes/rule/apis/unmute_alert/schemas/v1.ts new file mode 100644 index 0000000000000..4ae0dccb96978 --- /dev/null +++ b/x-pack/plugins/alerting/common/routes/rule/apis/unmute_alert/schemas/v1.ts @@ -0,0 +1,21 @@ +/* + * 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 { schema } from '@kbn/config-schema'; + +export const unmuteAlertParamsSchema = schema.object({ + rule_id: schema.string({ + meta: { + description: 'The identifier for the rule.', + }, + }), + alert_id: schema.string({ + meta: { + description: 'The identifier for the alert.', + }, + }), +}); diff --git a/x-pack/plugins/alerting/common/routes/rule/apis/unmute_alert/types/latest.ts b/x-pack/plugins/alerting/common/routes/rule/apis/unmute_alert/types/latest.ts new file mode 100644 index 0000000000000..cab31be4e070e --- /dev/null +++ b/x-pack/plugins/alerting/common/routes/rule/apis/unmute_alert/types/latest.ts @@ -0,0 +1,8 @@ +/* + * 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 type { UnmuteAlertRequestParams } from './v1'; diff --git a/x-pack/plugins/alerting/common/routes/rule/apis/unmute_alert/types/v1.ts b/x-pack/plugins/alerting/common/routes/rule/apis/unmute_alert/types/v1.ts new file mode 100644 index 0000000000000..0de4e0e767ceb --- /dev/null +++ b/x-pack/plugins/alerting/common/routes/rule/apis/unmute_alert/types/v1.ts @@ -0,0 +1,10 @@ +/* + * 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 { TypeOf } from '@kbn/config-schema'; +import { unmuteAlertParamsSchemaV1 } from '..'; + +export type UnmuteAlertRequestParams = TypeOf; diff --git a/x-pack/plugins/alerting/server/application/rule/methods/unmute_alert/schemas/index.ts b/x-pack/plugins/alerting/server/application/rule/methods/unmute_alert/schemas/index.ts new file mode 100644 index 0000000000000..7fc0a21218fcb --- /dev/null +++ b/x-pack/plugins/alerting/server/application/rule/methods/unmute_alert/schemas/index.ts @@ -0,0 +1,7 @@ +/* + * 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 { unmuteAlertParamsSchema } from './unmute_alert_params_schema'; diff --git a/x-pack/plugins/alerting/server/application/rule/methods/unmute_alert/schemas/unmute_alert_params_schema.ts b/x-pack/plugins/alerting/server/application/rule/methods/unmute_alert/schemas/unmute_alert_params_schema.ts new file mode 100644 index 0000000000000..edc85497ded29 --- /dev/null +++ b/x-pack/plugins/alerting/server/application/rule/methods/unmute_alert/schemas/unmute_alert_params_schema.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { schema } from '@kbn/config-schema'; + +export const unmuteAlertParamsSchema = schema.object({ + alertId: schema.string(), + alertInstanceId: schema.string(), +}); diff --git a/x-pack/plugins/alerting/server/application/rule/methods/unmute_alert/types/index.ts b/x-pack/plugins/alerting/server/application/rule/methods/unmute_alert/types/index.ts new file mode 100644 index 0000000000000..8d97bd968467c --- /dev/null +++ b/x-pack/plugins/alerting/server/application/rule/methods/unmute_alert/types/index.ts @@ -0,0 +1,8 @@ +/* + * 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 type { UnmuteAlertParams } from './unmute_alert_params'; diff --git a/x-pack/plugins/alerting/server/application/rule/methods/unmute_alert/types/unmute_alert_params.ts b/x-pack/plugins/alerting/server/application/rule/methods/unmute_alert/types/unmute_alert_params.ts new file mode 100644 index 0000000000000..ae83c5b6d4b7e --- /dev/null +++ b/x-pack/plugins/alerting/server/application/rule/methods/unmute_alert/types/unmute_alert_params.ts @@ -0,0 +1,11 @@ +/* + * 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 { TypeOf } from '@kbn/config-schema'; +import { unmuteAlertParamsSchema } from '../schemas'; + +export type UnmuteAlertParams = TypeOf; diff --git a/x-pack/plugins/alerting/server/rules_client/tests/unmute_instance.test.ts b/x-pack/plugins/alerting/server/application/rule/methods/unmute_alert/unmute_instance.test.ts similarity index 87% rename from x-pack/plugins/alerting/server/rules_client/tests/unmute_instance.test.ts rename to x-pack/plugins/alerting/server/application/rule/methods/unmute_alert/unmute_instance.test.ts index 948b9f8622002..f88b650c322ac 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/unmute_instance.test.ts +++ b/x-pack/plugins/alerting/server/application/rule/methods/unmute_alert/unmute_instance.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { RulesClient, ConstructorOptions } from '../rules_client'; +import { RulesClient, ConstructorOptions } from '../../../../rules_client/rules_client'; import { savedObjectsClientMock, loggingSystemMock, @@ -13,17 +13,17 @@ import { uiSettingsServiceMock, } from '@kbn/core/server/mocks'; import { taskManagerMock } from '@kbn/task-manager-plugin/server/mocks'; -import { ruleTypeRegistryMock } from '../../rule_type_registry.mock'; -import { alertingAuthorizationMock } from '../../authorization/alerting_authorization.mock'; +import { ruleTypeRegistryMock } from '../../../../rule_type_registry.mock'; +import { alertingAuthorizationMock } from '../../../../authorization/alerting_authorization.mock'; import { encryptedSavedObjectsMock } from '@kbn/encrypted-saved-objects-plugin/server/mocks'; import { actionsAuthorizationMock } from '@kbn/actions-plugin/server/mocks'; -import { AlertingAuthorization } from '../../authorization/alerting_authorization'; +import { AlertingAuthorization } from '../../../../authorization/alerting_authorization'; import { ActionsAuthorization } from '@kbn/actions-plugin/server'; import { auditLoggerMock } from '@kbn/security-plugin/server/audit/mocks'; -import { getBeforeSetup, setGlobalDate } from './lib'; -import { ConnectorAdapterRegistry } from '../../connector_adapters/connector_adapter_registry'; -import { RULE_SAVED_OBJECT_TYPE } from '../../saved_objects'; -import { backfillClientMock } from '../../backfill_client/backfill_client.mock'; +import { getBeforeSetup, setGlobalDate } from '../../../../rules_client/tests/lib'; +import { ConnectorAdapterRegistry } from '../../../../connector_adapters/connector_adapter_registry'; +import { RULE_SAVED_OBJECT_TYPE } from '../../../../saved_objects'; +import { backfillClientMock } from '../../../../backfill_client/backfill_client.mock'; const taskManager = taskManagerMock.createStart(); const ruleTypeRegistry = ruleTypeRegistryMock.create(); @@ -203,6 +203,17 @@ describe('unmuteInstance()', () => { ruleTypeId: 'myType', }); }); + + test('throws an error if API params do not match the schema', async () => { + const rulesClient = new RulesClient(rulesClientParams); + await expect( + // @ts-expect-error: Wrong params for testing purposes + rulesClient.unmuteInstance({ alertId: 1 }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Failed to validate params: [alertId]: expected value of type [string] but got [number]"` + ); + expect(unsecuredSavedObjectsClient.update).not.toHaveBeenCalled(); + }); }); describe('auditLogger', () => { diff --git a/x-pack/plugins/alerting/server/rules_client/methods/unmute_instance.ts b/x-pack/plugins/alerting/server/application/rule/methods/unmute_alert/unmute_instance.ts similarity index 56% rename from x-pack/plugins/alerting/server/rules_client/methods/unmute_instance.ts rename to x-pack/plugins/alerting/server/application/rule/methods/unmute_alert/unmute_instance.ts index 0b8e422f1a946..220a1b14e728c 100644 --- a/x-pack/plugins/alerting/server/rules_client/methods/unmute_instance.ts +++ b/x-pack/plugins/alerting/server/application/rule/methods/unmute_alert/unmute_instance.ts @@ -5,39 +5,43 @@ * 2.0. */ -import { Rule, RawRule } from '../../types'; -import { WriteOperations, AlertingAuthorizationEntity } from '../../authorization'; -import { retryIfConflicts } from '../../lib/retry_if_conflicts'; -import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events'; -import { MuteOptions } from '../types'; -import { RulesClientContext } from '../types'; -import { updateMeta } from '../lib'; -import { RULE_SAVED_OBJECT_TYPE } from '../../saved_objects'; +import Boom from '@hapi/boom'; +import type { Rule } from '../../../../types'; +import type { RulesClientContext } from '../../../../rules_client/types'; +import type { UnmuteAlertParams } from './types'; +import { retryIfConflicts } from '../../../../lib/retry_if_conflicts'; +import { RULE_SAVED_OBJECT_TYPE } from '../../../../saved_objects'; +import { ruleAuditEvent, RuleAuditAction } from '../../../../rules_client/common/audit_events'; +import { unmuteAlertParamsSchema } from './schemas'; +import { updateMeta } from '../../../../rules_client/lib'; +import { updateRuleSo } from '../../../../data/rule'; +import { WriteOperations, AlertingAuthorizationEntity } from '../../../../authorization'; export async function unmuteInstance( context: RulesClientContext, - { alertId, alertInstanceId }: MuteOptions + params: UnmuteAlertParams ): Promise { + const ruleId = params.alertId; + try { + unmuteAlertParamsSchema.validate(params); + } catch (error) { + throw Boom.badRequest(`Failed to validate params: ${error.message}`); + } + return await retryIfConflicts( context.logger, - `rulesClient.unmuteInstance('${alertId}')`, - async () => await unmuteInstanceWithOCC(context, { alertId, alertInstanceId }) + `rulesClient.unmuteInstance('${ruleId}')`, + async () => await unmuteInstanceWithOCC(context, params) ); } async function unmuteInstanceWithOCC( context: RulesClientContext, - { - alertId, - alertInstanceId, - }: { - alertId: string; - alertInstanceId: string; - } + { alertId: ruleId, alertInstanceId }: UnmuteAlertParams ) { const { attributes, version } = await context.unsecuredSavedObjectsClient.get( RULE_SAVED_OBJECT_TYPE, - alertId + ruleId ); try { @@ -54,7 +58,7 @@ async function unmuteInstanceWithOCC( context.auditLogger?.log( ruleAuditEvent({ action: RuleAuditAction.UNMUTE_ALERT, - savedObject: { type: RULE_SAVED_OBJECT_TYPE, id: alertId }, + savedObject: { type: RULE_SAVED_OBJECT_TYPE, id: ruleId }, error, }) ); @@ -65,7 +69,7 @@ async function unmuteInstanceWithOCC( ruleAuditEvent({ action: RuleAuditAction.UNMUTE_ALERT, outcome: 'unknown', - savedObject: { type: RULE_SAVED_OBJECT_TYPE, id: alertId }, + savedObject: { type: RULE_SAVED_OBJECT_TYPE, id: ruleId }, }) ); @@ -73,15 +77,15 @@ async function unmuteInstanceWithOCC( const mutedInstanceIds = attributes.mutedInstanceIds || []; if (!attributes.muteAll && mutedInstanceIds.includes(alertInstanceId)) { - await context.unsecuredSavedObjectsClient.update( - RULE_SAVED_OBJECT_TYPE, - alertId, - updateMeta(context, { + await updateRuleSo({ + savedObjectsClient: context.unsecuredSavedObjectsClient, + savedObjectsUpdateOptions: { version }, + id: ruleId, + updateRuleAttributes: updateMeta(context, { + mutedInstanceIds: mutedInstanceIds.filter((id: string) => id !== alertInstanceId), updatedBy: await context.getUserName(), updatedAt: new Date().toISOString(), - mutedInstanceIds: mutedInstanceIds.filter((id: string) => id !== alertInstanceId), }), - { version } - ); + }); } } diff --git a/x-pack/plugins/alerting/server/routes/index.ts b/x-pack/plugins/alerting/server/routes/index.ts index 648d661d1d612..c1fd477922fb9 100644 --- a/x-pack/plugins/alerting/server/routes/index.ts +++ b/x-pack/plugins/alerting/server/routes/index.ts @@ -34,7 +34,7 @@ import { ruleTypesRoute } from './rule_types'; import { muteAllRuleRoute } from './mute_all_rule'; import { muteAlertRoute } from './rule/apis/mute_alert/mute_alert'; import { unmuteAllRuleRoute } from './unmute_all_rule'; -import { unmuteAlertRoute } from './unmute_alert'; +import { unmuteAlertRoute } from './rule/apis/unmute_alert/unmute_alert_route'; import { updateRuleApiKeyRoute } from './rule/apis/update_api_key/update_rule_api_key_route'; import { bulkEditInternalRulesRoute } from './rule/apis/bulk_edit/bulk_edit_rules_route'; import { snoozeRuleRoute } from './rule/apis/snooze'; diff --git a/x-pack/plugins/alerting/server/routes/rule/apis/unmute_alert/transforms/index.ts b/x-pack/plugins/alerting/server/routes/rule/apis/unmute_alert/transforms/index.ts new file mode 100644 index 0000000000000..21a7250aed4e2 --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/rule/apis/unmute_alert/transforms/index.ts @@ -0,0 +1,8 @@ +/* + * 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 { transformRequestParamsToApplication } from './transform_request_params_to_application/latest'; +export { transformRequestParamsToApplication as transformRequestParamsToApplicationV1 } from './transform_request_params_to_application/v1'; diff --git a/x-pack/plugins/alerting/server/routes/rule/apis/unmute_alert/transforms/transform_request_params_to_application/latest.ts b/x-pack/plugins/alerting/server/routes/rule/apis/unmute_alert/transforms/transform_request_params_to_application/latest.ts new file mode 100644 index 0000000000000..5983069f0d8fd --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/rule/apis/unmute_alert/transforms/transform_request_params_to_application/latest.ts @@ -0,0 +1,8 @@ +/* + * 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 { transformRequestParamsToApplication } from './v1'; diff --git a/x-pack/plugins/alerting/server/routes/rule/apis/unmute_alert/transforms/transform_request_params_to_application/v1.test.ts b/x-pack/plugins/alerting/server/routes/rule/apis/unmute_alert/transforms/transform_request_params_to_application/v1.test.ts new file mode 100644 index 0000000000000..620d1ec4a746b --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/rule/apis/unmute_alert/transforms/transform_request_params_to_application/v1.test.ts @@ -0,0 +1,18 @@ +/* + * 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 { transformRequestParamsToApplication } from '..'; + +describe('transformRequestParamsToApplication', () => { + it('changes the parameters case', () => { + const transformed = transformRequestParamsToApplication({ + rule_id: 'test-rule-id', + alert_id: 'test-alert-id', + }); + expect(transformed).toEqual({ alertId: 'test-rule-id', alertInstanceId: 'test-alert-id' }); + }); +}); diff --git a/x-pack/plugins/alerting/server/routes/rule/apis/unmute_alert/transforms/transform_request_params_to_application/v1.ts b/x-pack/plugins/alerting/server/routes/rule/apis/unmute_alert/transforms/transform_request_params_to_application/v1.ts new file mode 100644 index 0000000000000..227b57ba67717 --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/rule/apis/unmute_alert/transforms/transform_request_params_to_application/v1.ts @@ -0,0 +1,18 @@ +/* + * 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 { UnmuteAlertParams } from '../../../../../../application/rule/methods/unmute_alert/types'; +import { RewriteRequestCase } from '../../../../../lib'; +import { UnmuteAlertRequestParamsV1 } from '../../../../../../../common/routes/rule/apis/unmute_alert'; + +export const transformRequestParamsToApplication: RewriteRequestCase = ({ + rule_id: alertId, + alert_id: alertInstanceId, +}: UnmuteAlertRequestParamsV1) => ({ + alertId, + alertInstanceId, +}); diff --git a/x-pack/plugins/alerting/server/routes/unmute_alert.test.ts b/x-pack/plugins/alerting/server/routes/rule/apis/unmute_alert/unmute_alert_route.test.ts similarity index 84% rename from x-pack/plugins/alerting/server/routes/unmute_alert.test.ts rename to x-pack/plugins/alerting/server/routes/rule/apis/unmute_alert/unmute_alert_route.test.ts index 6f9c553831e77..cc58bd3d93d5c 100644 --- a/x-pack/plugins/alerting/server/routes/unmute_alert.test.ts +++ b/x-pack/plugins/alerting/server/routes/rule/apis/unmute_alert/unmute_alert_route.test.ts @@ -5,15 +5,15 @@ * 2.0. */ -import { unmuteAlertRoute } from './unmute_alert'; +import { unmuteAlertRoute } from './unmute_alert_route'; import { httpServiceMock } from '@kbn/core/server/mocks'; -import { licenseStateMock } from '../lib/license_state.mock'; -import { mockHandlerArguments } from './_mock_handler_arguments'; -import { rulesClientMock } from '../rules_client.mock'; -import { RuleTypeDisabledError } from '../lib/errors/rule_type_disabled'; +import { licenseStateMock } from '../../../../lib/license_state.mock'; +import { mockHandlerArguments } from '../../../_mock_handler_arguments'; +import { rulesClientMock } from '../../../../rules_client.mock'; +import { RuleTypeDisabledError } from '../../../../lib/errors/rule_type_disabled'; const rulesClient = rulesClientMock.create(); -jest.mock('../lib/license_api_access', () => ({ +jest.mock('../../../../lib/license_api_access', () => ({ verifyApiAccess: jest.fn(), })); diff --git a/x-pack/plugins/alerting/server/routes/unmute_alert.ts b/x-pack/plugins/alerting/server/routes/rule/apis/unmute_alert/unmute_alert_route.ts similarity index 58% rename from x-pack/plugins/alerting/server/routes/unmute_alert.ts rename to x-pack/plugins/alerting/server/routes/rule/apis/unmute_alert/unmute_alert_route.ts index ba3c639a88e52..fa608405343b0 100644 --- a/x-pack/plugins/alerting/server/routes/unmute_alert.ts +++ b/x-pack/plugins/alerting/server/routes/rule/apis/unmute_alert/unmute_alert_route.ts @@ -6,32 +6,14 @@ */ import { IRouter } from '@kbn/core/server'; -import { schema } from '@kbn/config-schema'; -import { ILicenseState, RuleTypeDisabledError } from '../lib'; -import { MuteOptions } from '../rules_client'; -import { RewriteRequestCase, verifyAccessAndContext } from './lib'; -import { AlertingRequestHandlerContext, BASE_ALERTING_API_PATH } from '../types'; - -const paramSchema = schema.object({ - rule_id: schema.string({ - meta: { - description: 'The identifier for the rule.', - }, - }), - alert_id: schema.string({ - meta: { - description: 'The identifier for the alert.', - }, - }), -}); - -const rewriteParamsReq: RewriteRequestCase = ({ - rule_id: alertId, - alert_id: alertInstanceId, -}) => ({ - alertId, - alertInstanceId, -}); +import { ILicenseState, RuleTypeDisabledError } from '../../../../lib'; +import { verifyAccessAndContext } from '../../../lib'; +import { AlertingRequestHandlerContext, BASE_ALERTING_API_PATH } from '../../../../types'; +import { + unmuteAlertParamsSchemaV1, + UnmuteAlertRequestParamsV1, +} from '../../../../../common/routes/rule/apis/unmute_alert'; +import { transformRequestParamsToApplicationV1 } from './transforms'; export const unmuteAlertRoute = ( router: IRouter, @@ -45,15 +27,22 @@ export const unmuteAlertRoute = ( summary: `Unmute an alert`, }, validate: { - params: paramSchema, + request: { + params: unmuteAlertParamsSchemaV1, + }, + response: { + 204: { + description: 'Indicates a successful call.', + }, + }, }, }, router.handleLegacyErrors( verifyAccessAndContext(licenseState, async function (context, req, res) { const rulesClient = (await context.alerting).getRulesClient(); - const params = rewriteParamsReq(req.params); + const params: UnmuteAlertRequestParamsV1 = req.params; try { - await rulesClient.unmuteInstance(params); + await rulesClient.unmuteInstance(transformRequestParamsToApplicationV1(params)); return res.noContent(); } catch (e) { if (e instanceof RuleTypeDisabledError) { diff --git a/x-pack/plugins/alerting/server/rules_client/rules_client.ts b/x-pack/plugins/alerting/server/rules_client/rules_client.ts index d03bd3f59486b..343fbec059940 100644 --- a/x-pack/plugins/alerting/server/rules_client/rules_client.ts +++ b/x-pack/plugins/alerting/server/rules_client/rules_client.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { UnmuteAlertParams } from '../application/rule/methods/unmute_alert/types'; import { getRuleTags, RuleTagsParams } from '../application/rule/methods/tags'; import { MuteAlertParams } from '../application/rule/methods/mute_alert/types'; import { SanitizedRule, RuleTypeParams } from '../types'; @@ -60,7 +61,7 @@ import { clearExpiredSnoozes } from './methods/clear_expired_snoozes'; import { muteInstance } from '../application/rule/methods/mute_alert/mute_instance'; import { muteAll } from './methods/mute_all'; import { unmuteAll } from './methods/unmute_all'; -import { unmuteInstance } from './methods/unmute_instance'; +import { unmuteInstance } from '../application/rule/methods/unmute_alert/unmute_instance'; import { runSoon } from './methods/run_soon'; import { listRuleTypes } from './methods/list_rule_types'; import { getAlertFromRaw, GetAlertFromRawParams } from './lib/get_alert_from_raw'; @@ -181,7 +182,7 @@ export class RulesClient { public muteAll = (options: { id: string }) => muteAll(this.context, options); public unmuteAll = (options: { id: string }) => unmuteAll(this.context, options); public muteInstance = (options: MuteAlertParams) => muteInstance(this.context, options); - public unmuteInstance = (options: MuteAlertParams) => unmuteInstance(this.context, options); + public unmuteInstance = (options: UnmuteAlertParams) => unmuteInstance(this.context, options); public bulkUntrackAlerts = (options: BulkUntrackBody) => bulkUntrackAlerts(this.context, options); diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/utils.ts b/x-pack/plugins/observability_solution/entity_manager/server/lib/utils.ts index d948ac28bda31..d1d76e147efb0 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/lib/utils.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/utils.ts @@ -5,37 +5,21 @@ * 2.0. */ -import moment from 'moment'; +import { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; +import { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server'; +import { getFakeKibanaRequest } from '@kbn/security-plugin/server/authentication/api_keys/fake_kibana_request'; +import { EntityManagerServerSetup } from '../types'; +import { EntityDiscoveryAPIKey } from './auth/api_key/api_key'; -export function toArray(maybeArray: T | T[] | undefined): T[] { - if (!maybeArray) { - return []; - } - if (Array.isArray(maybeArray)) { - return maybeArray; - } - return [maybeArray]; -} - -export const isValidRange = (from: string, to: string): boolean => { - if (moment(from).isAfter(to)) { - return false; - } - return true; +export const getClientsFromAPIKey = ({ + apiKey, + server, +}: { + apiKey: EntityDiscoveryAPIKey; + server: EntityManagerServerSetup; +}): { esClient: ElasticsearchClient; soClient: SavedObjectsClientContract } => { + const fakeRequest = getFakeKibanaRequest({ id: apiKey.id, api_key: apiKey.apiKey }); + const esClient = server.core.elasticsearch.client.asScoped(fakeRequest).asCurrentUser; + const soClient = server.core.savedObjects.getScopedClient(fakeRequest); + return { esClient, soClient }; }; - -export function isStringOrNonEmptyArray( - value: string | string[] | undefined -): value is string | string[] { - if (typeof value === 'undefined') { - return false; - } - if (Array.isArray(value) && value.length === 0) { - return false; - } - return true; -} - -export function extractFieldValue(maybeArray: T | T[] | undefined): T { - return toArray(maybeArray)[0]; -} diff --git a/x-pack/plugins/observability_solution/entity_manager/server/routes/enablement/check.ts b/x-pack/plugins/observability_solution/entity_manager/server/routes/enablement/check.ts index 8ee8de3751ab2..23343026e3332 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/routes/enablement/check.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/routes/enablement/check.ts @@ -16,6 +16,7 @@ import { } from '../../../common/errors'; import { findEntityDefinitions } from '../../lib/entities/find_entity_definition'; import { builtInDefinitions } from '../../lib/entities/built_in'; +import { getClientsFromAPIKey } from '../../lib/utils'; export function checkEntityDiscoveryEnabledRoute({ router, @@ -43,8 +44,7 @@ export function checkEntityDiscoveryEnabledRoute { diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/ui_actions/compatibility_check.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/ui_actions/compatibility_check.ts new file mode 100644 index 0000000000000..ff282745c8fc2 --- /dev/null +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/ui_actions/compatibility_check.ts @@ -0,0 +1,15 @@ +/* + * 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 { apiIsPresentationContainer, PresentationContainer } from '@kbn/presentation-containers'; +import { EmbeddableApiContext } from '@kbn/presentation-publishing'; + +export const compatibilityCheck = ( + api: EmbeddableApiContext['embeddable'] +): api is PresentationContainer => { + return apiIsPresentationContainer(api); +}; diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/ui_actions/create_overview_panel_action.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/ui_actions/create_overview_panel_action.tsx index 202a09e1f3576..79c6a6c1195a9 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/ui_actions/create_overview_panel_action.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/ui_actions/create_overview_panel_action.tsx @@ -5,7 +5,6 @@ * 2.0. */ import { i18n } from '@kbn/i18n'; -import { apiIsPresentationContainer } from '@kbn/presentation-containers'; import { IncompatibleActionError, type UiActionsActionDefinition, @@ -34,10 +33,12 @@ export function createStatusOverviewPanelAction(): UiActionsActionDefinition 'online', isCompatible: async ({ embeddable }) => { - return apiIsPresentationContainer(embeddable); + const { compatibilityCheck } = await import('./compatibility_check'); + return compatibilityCheck(embeddable); }, execute: async ({ embeddable }) => { - if (!apiIsPresentationContainer(embeddable)) throw new IncompatibleActionError(); + const { compatibilityCheck } = await import('./compatibility_check'); + if (!compatibilityCheck(embeddable)) throw new IncompatibleActionError(); try { embeddable.addNewPanel({ panelType: SYNTHETICS_OVERVIEW_EMBEDDABLE, diff --git a/x-pack/plugins/search_playground/public/components/chat.tsx b/x-pack/plugins/search_playground/public/components/chat.tsx index cc4c0b1ccdff2..b27955c326d23 100644 --- a/x-pack/plugins/search_playground/public/components/chat.tsx +++ b/x-pack/plugins/search_playground/public/components/chat.tsx @@ -56,7 +56,7 @@ export const Chat = () => { handleSubmit, getValues, } = useFormContext(); - const { messages, append, stop: stopRequest, setMessages, reload, error } = useChat(); + const { messages, append, stop: stopRequest, setMessages, reload } = useChat(); const messagesRef = useAutoBottomScroll(); const [isRegenerating, setIsRegenerating] = useState(false); const usageTracker = useUsageTracker(); @@ -88,8 +88,8 @@ export const Chat = () => { ); const isToolBarActionsDisabled = useMemo( - () => chatMessages.length <= 1 || !!error || isRegenerating || isSubmitting, - [chatMessages, error, isSubmitting, isRegenerating] + () => chatMessages.length <= 1 || isRegenerating || isSubmitting, + [chatMessages, isSubmitting, isRegenerating] ); const regenerateMessages = async () => { diff --git a/x-pack/plugins/search_playground/server/lib/conversational_chain.test.ts b/x-pack/plugins/search_playground/server/lib/conversational_chain.test.ts index 88a6052a0bbf7..13959e4455c29 100644 --- a/x-pack/plugins/search_playground/server/lib/conversational_chain.test.ts +++ b/x-pack/plugins/search_playground/server/lib/conversational_chain.test.ts @@ -305,6 +305,45 @@ describe('conversational chain', () => { }); }, 10000); + it('should omit the system messages in chat', async () => { + await createTestChain({ + responses: ['the final answer'], + chat: [ + { + id: '1', + role: MessageRole.user, + content: 'what is the work from home policy?', + }, + { + id: '2', + role: MessageRole.system, + content: 'Error occurred. Please try again.', + }, + ], + expectedFinalAnswer: 'the final answer', + expectedDocs: [ + { + documents: [ + { metadata: { _id: '1', _index: 'index' }, pageContent: 'value' }, + { metadata: { _id: '1', _index: 'website' }, pageContent: 'value2' }, + ], + type: 'retrieved_docs', + }, + ], + expectedTokens: [ + { type: 'context_token_count', count: 15 }, + { type: 'prompt_token_count', count: 28 }, + ], + expectedSearchRequest: [ + { + method: 'POST', + path: '/index,website/_search', + body: { query: { match: { field: 'what is the work from home policy?' } }, size: 3 }, + }, + ], + }); + }, 10000); + it('should cope with quotes in the query', async () => { await createTestChain({ responses: ['rewrite "the" question', 'the final answer'], diff --git a/x-pack/plugins/search_playground/server/lib/conversational_chain.ts b/x-pack/plugins/search_playground/server/lib/conversational_chain.ts index f7b1634dd27b1..c63481e93c98f 100644 --- a/x-pack/plugins/search_playground/server/lib/conversational_chain.ts +++ b/x-pack/plugins/search_playground/server/lib/conversational_chain.ts @@ -18,7 +18,7 @@ import { createStreamDataTransformer, experimental_StreamData } from 'ai'; import { BaseLanguageModel } from '@langchain/core/language_models/base'; import { BaseMessage } from '@langchain/core/messages'; import { HumanMessage, AIMessage } from '@langchain/core/messages'; -import { ChatMessage, MessageRole } from '../types'; +import { ChatMessage } from '../types'; import { ElasticsearchRetriever } from './elasticsearch_retriever'; import { renderTemplate } from '../utils/render_template'; @@ -49,25 +49,28 @@ interface ContextInputs { question: string; } -const getSerialisedMessages = (chatHistory: ChatMessage[]) => { +const getSerialisedMessages = (chatHistory: BaseMessage[]) => { const formattedDialogueTurns = chatHistory.map((message) => { - if (message.role === MessageRole.user) { + if (message instanceof HumanMessage) { return `Human: ${message.content}`; - } else if (message.role === MessageRole.assistant) { + } else if (message instanceof AIMessage) { return `Assistant: ${message.content}`; } }); return formattedDialogueTurns.join('\n'); }; -const getMessages = (chatHistory: ChatMessage[]) => { - return chatHistory.map((message) => { - if (message.role === 'human') { - return new HumanMessage(message.content); - } else { - return new AIMessage(message.content); - } - }); +export const getMessages = (chatHistory: ChatMessage[]) => { + return chatHistory + .map((message) => { + if (message.role === 'human') { + return new HumanMessage(message.content); + } else if (message.role === 'assistant') { + return new AIMessage(message.content); + } + return null; + }) + .filter((message): message is BaseMessage => message !== null); }; const buildContext = (docs: Document[]) => { @@ -141,8 +144,9 @@ class ConversationalChainFn { const data = new experimental_StreamData(); const messages = msgs ?? []; - const previousMessages = messages.slice(0, -1); - const question = messages[messages.length - 1]!.content; + const lcMessages = getMessages(messages); + const previousMessages = lcMessages.slice(0, -1); + const question = lcMessages[lcMessages.length - 1]!.content; const retrievedDocs: Document[] = []; let retrievalChain: Runnable = RunnableLambda.from(() => ''); @@ -165,7 +169,7 @@ class ConversationalChainFn { return input.question; }); - if (previousMessages.length > 0) { + if (lcMessages.length > 1) { const questionRewritePromptTemplate = PromptTemplate.fromTemplate( this.options.questionRewritePrompt ); @@ -184,7 +188,6 @@ class ConversationalChainFn { }); } - const lcMessages = getMessages(messages); const prompt = ChatPromptTemplate.fromMessages([ SystemMessagePromptTemplate.fromTemplate(this.options.prompt), ...lcMessages, diff --git a/x-pack/plugins/search_playground/server/routes.ts b/x-pack/plugins/search_playground/server/routes.ts index de26776816f33..a71e650a8dae8 100644 --- a/x-pack/plugins/search_playground/server/routes.ts +++ b/x-pack/plugins/search_playground/server/routes.ts @@ -31,7 +31,7 @@ export function createRetriever(esQuery: string) { const query = JSON.parse(replacedQuery); return query; } catch (e) { - throw Error(e); + throw Error("Failed to parse the Elasticsearch Query. Check Query to make sure it's valid."); } }; } diff --git a/x-pack/plugins/security_solution/public/assistant/provider.test.tsx b/x-pack/plugins/security_solution/public/assistant/provider.test.tsx index 3667e077a50c1..0534df76aaf6e 100644 --- a/x-pack/plugins/security_solution/public/assistant/provider.test.tsx +++ b/x-pack/plugins/security_solution/public/assistant/provider.test.tsx @@ -5,13 +5,38 @@ * 2.0. */ +import React from 'react'; import { act, renderHook } from '@testing-library/react-hooks'; import { httpServiceMock, type HttpSetupMock } from '@kbn/core-http-browser-mocks'; import type { Storage } from '@kbn/kibana-utils-plugin/public'; -import { createConversations } from './provider'; +import { AssistantProvider, createConversations } from './provider'; import { coreMock } from '@kbn/core/public/mocks'; +import { useKibana as mockUseKibana } from '../common/lib/kibana/__mocks__'; import { loadAllActions as loadConnectors } from '@kbn/triggers-actions-ui-plugin/public/common/constants'; +import { useKibana } from '../common/lib/kibana'; +import { render, waitFor } from '@testing-library/react'; +import { TestProviders } from '../common/mock'; +import { useAssistantAvailability } from './use_assistant_availability'; +import { + bulkUpdatePrompts, + getPrompts, + getUserConversations, +} from '@kbn/elastic-assistant/impl/assistant/api'; +import { BASE_SECURITY_SYSTEM_PROMPTS } from './content/prompts/system'; +const mockedUseKibana = mockUseKibana(); +jest.mock('./use_assistant_availability'); +jest.mock('../common/lib/kibana'); + +jest.mock('@kbn/elastic-assistant/impl/assistant/api'); +jest.mock('../common/hooks/use_license', () => ({ + useLicense: () => ({ + isEnterprise: () => true, + }), + licenseService: { + isEnterprise: () => true, + }, +})); jest.mock('@kbn/triggers-actions-ui-plugin/public/common/constants'); let http: HttpSetupMock = coreMock.createSetup().http; export const mockConnectors = [ @@ -199,3 +224,85 @@ describe('createConversations', () => { }); }); }); +describe('AssistantProvider', () => { + beforeEach(() => { + jest.clearAllMocks(); + (useKibana as jest.Mock).mockReturnValue({ + ...mockedUseKibana, + services: { + ...mockedUseKibana.services, + }, + }); + jest.mocked(useAssistantAvailability).mockReturnValue({ + hasAssistantPrivilege: true, + hasConnectorsAllPrivilege: true, + hasConnectorsReadPrivilege: true, + hasUpdateAIAssistantAnonymization: true, + isAssistantEnabled: true, + }); + + (getUserConversations as jest.Mock).mockResolvedValue({ + page: 1, + perPage: 5, + total: 5, + data: [], + }); + (getPrompts as jest.Mock).mockResolvedValue({ + page: 1, + perPage: 5, + total: 0, + data: [], + }); + }); + it('should not render the assistant when no prompts have been returned', async () => { + const { queryByTestId } = render( + + + , + { + wrapper: TestProviders, + } + ); + expect(queryByTestId('ourAssistant')).toBeNull(); + }); + it('should render the assistant when prompts are returned', async () => { + (getPrompts as jest.Mock).mockResolvedValue({ + page: 1, + perPage: 5, + total: 2, + data: BASE_SECURITY_SYSTEM_PROMPTS, + }); + const { getByTestId } = render( + + + , + { + wrapper: TestProviders, + } + ); + await waitFor(() => { + expect(getByTestId('ourAssistant')).not.toBeNull(); + }); + }); + it('should render the assistant once prompts have been created', async () => { + (bulkUpdatePrompts as jest.Mock).mockResolvedValue({ + success: true, + attributes: { + results: { + created: BASE_SECURITY_SYSTEM_PROMPTS, + }, + }, + }); + const { getByTestId } = render( + + + , + { + wrapper: TestProviders, + } + ); + await waitFor(() => { + expect(getByTestId('ourAssistant')).not.toBeNull(); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/assistant/provider.tsx b/x-pack/plugins/security_solution/public/assistant/provider.tsx index 134bfb25c15ac..dbfbb026ab2d3 100644 --- a/x-pack/plugins/security_solution/public/assistant/provider.tsx +++ b/x-pack/plugins/security_solution/public/assistant/provider.tsx @@ -5,7 +5,7 @@ * 2.0. */ import type { FC, PropsWithChildren } from 'react'; -import React, { useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; import { parse } from '@kbn/datemath'; import type { Storage } from '@kbn/kibana-utils-plugin/public'; import { i18n } from '@kbn/i18n'; @@ -128,7 +128,7 @@ export const createBasePrompts = async (notifications: NotificationsStart, http: notifications.toasts ); if (bulkResult && bulkResult.success) { - return true; + return bulkResult.attributes.results.created; } }; @@ -176,6 +176,8 @@ export const AssistantProvider: FC> = ({ children }) storage, ]); + const [basePromptsLoaded, setBasePromptsLoaded] = useState(false); + useEffect(() => { const createSecurityPrompts = once(async () => { if ( @@ -183,15 +185,20 @@ export const AssistantProvider: FC> = ({ children }) assistantAvailability.isAssistantEnabled && assistantAvailability.hasAssistantPrivilege ) { - const res = await getPrompts({ - http, - toasts: notifications.toasts, - }); + try { + const res = await getPrompts({ + http, + toasts: notifications.toasts, + }); - if (res.total === 0) { - await createBasePrompts(notifications, http); - } + if (res.total === 0) { + await createBasePrompts(notifications, http); + } + // eslint-disable-next-line no-empty + } catch (e) {} } + + setBasePromptsLoaded(true); }); createSecurityPrompts(); }, [ @@ -205,6 +212,9 @@ export const AssistantProvider: FC> = ({ children }) 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) + // Because our conversations need an assigned system prompt at create time, + // we want to make sure the prompts are there before creating the first conversation + // however if there is an error fetching the prompts, we don't want to block the app return ( > = ({ children }) toasts={toasts} currentAppId={currentAppId ?? 'securitySolutionUI'} > - {children} + {basePromptsLoaded ? children : null} ); }; diff --git a/x-pack/plugins/security_solution/public/explore/components/authentication/__snapshots__/authentications_host_table.test.tsx.snap b/x-pack/plugins/security_solution/public/explore/components/authentication/__snapshots__/authentications_host_table.test.tsx.snap index 22a13d29278da..a14cc0ab81115 100644 --- a/x-pack/plugins/security_solution/public/explore/components/authentication/__snapshots__/authentications_host_table.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/explore/components/authentication/__snapshots__/authentications_host_table.test.tsx.snap @@ -352,10 +352,10 @@ exports[`Authentication Host Table Component rendering it renders the host authe
- - + + = ({ onChange={onChange} sorting={tableSorting} /> - - + + {itemsPerRow && itemsPerRow.length > 0 && totalCount >= itemsPerRow[0].numberOfRow && (