diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_features/stream_description.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_features/stream_description.tsx index 4245d68daea1e..8c33edab3a1bb 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_features/stream_description.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_features/stream_description.tsx @@ -10,6 +10,7 @@ import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, + EuiLoadingSpinner, EuiMarkdownEditor, EuiPanel, EuiText, @@ -57,6 +58,13 @@ const GENERATE_DESCRIPTION_BUTTON_LABEL = i18n.translate( } ); +const STOP_GENERATION_BUTTON_LABEL = i18n.translate( + 'xpack.streams.streamDetailView.streamDescription.stopGenerationButtonLabel', + { + defaultMessage: 'Stop', + } +); + const SAVE_DESCRIPTION_BUTTON_LABEL = i18n.translate( 'xpack.streams.streamDetailView.streamDescription.saveDescriptionButtonLabel', { @@ -85,6 +93,50 @@ const CANCEL_LABEL = i18n.translate( } ); +const GenerateDescriptionButton: React.FC<{ + size: 's' | 'm'; + isGenerating: boolean; + isDisabled: boolean; + onGenerate: () => void; + onStop: () => void; + aiFeatures: AIFeatures | null; +}> = ({ size, isGenerating, isDisabled, onGenerate, onStop, aiFeatures }) => { + return ( + + + + + {STOP_GENERATION_BUTTON_LABEL} + + ), + onClick: onStop, + 'data-test-subj': 'stream_description_stop_button', + } + : { + size, + iconType: 'sparkles', + children: GENERATE_DESCRIPTION_BUTTON_LABEL, + onClick: onGenerate, + isDisabled, + 'data-test-subj': 'stream_description_generate_button', + } + } + aiFeatures={aiFeatures} + /> + ); +}; + export const StreamDescription: React.FC = ({ definition, refreshDefinition, @@ -101,6 +153,7 @@ export const StreamDescription: React.FC = ({ onSaveDescription, onStartEditing, areButtonsDisabled, + abort, } = useStreamDescriptionApi({ definition, refreshDefinition, aiFeatures }); return ( @@ -145,16 +198,12 @@ export const StreamDescription: React.FC = ({ )} - @@ -209,16 +258,12 @@ export const StreamDescription: React.FC = ({ - diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_features/stream_description/use_stream_description_api.ts b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_features/stream_description/use_stream_description_api.ts index e5c4032179e90..e64a1ba96770d 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_features/stream_description/use_stream_description_api.ts +++ b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_features/stream_description/use_stream_description_api.ts @@ -22,12 +22,19 @@ export const useStreamDescriptionApi = ({ definition, refreshDefinition, aiFeatures, + silent = false, }: { definition: Streams.all.GetResponse; refreshDefinition: () => void; aiFeatures: AIFeatures | null; + silent?: boolean; }) => { - const { signal } = useAbortController(); + const { signal, abort: abortController, refresh } = useAbortController(); + + const abort = useCallback(() => { + abortController(); + refresh(); + }, [abortController, refresh]); const updateStream = useUpdateStreams(definition.stream.name); @@ -47,7 +54,7 @@ export const useStreamDescriptionApi = ({ const [isUpdating, setIsUpdating] = useState(false); const [isEditing, setIsEditing] = useState(false); - // Save the updated description; show success and error toasts + // Save the updated description; show success and error toasts unless silent const save = useCallback( async (nextDescription: string) => { setIsUpdating(true); @@ -79,25 +86,29 @@ export const useStreamDescriptionApi = ({ }) ) .then(() => { - notifications.toasts.addSuccess({ - title: i18n.translate( - 'xpack.streams.streamDetailView.streamDescription.saveSuccessTitle', - { - defaultMessage: 'Description saved', - } - ), - }); + if (!silent) { + notifications.toasts.addSuccess({ + title: i18n.translate( + 'xpack.streams.streamDetailView.streamDescription.saveSuccessTitle', + { + defaultMessage: 'Description saved', + } + ), + }); + } }) .catch((error) => { - notifications.toasts.addError(error, { - title: i18n.translate( - 'xpack.streams.streamDetailView.streamDescription.saveErrorTitle', - { - defaultMessage: 'Failed to save description', - } - ), - toastMessage: getFormattedError(error).message, - }); + if (!silent) { + notifications.toasts.addError(error, { + title: i18n.translate( + 'xpack.streams.streamDetailView.streamDescription.saveErrorTitle', + { + defaultMessage: 'Failed to save description', + } + ), + toastMessage: getFormattedError(error).message, + }); + } }) .finally(() => { setIsUpdating(false); @@ -105,6 +116,7 @@ export const useStreamDescriptionApi = ({ }); }, [ + silent, updateStream, definition.dashboards, definition.queries, @@ -154,17 +166,20 @@ export const useStreamDescriptionApi = ({ if (error.name === 'AbortError') { return; } - notifications.toasts.addError(error, { - title: i18n.translate( - 'xpack.streams.streamDetailView.streamDescription.generateErrorTitle', - { defaultMessage: 'Failed to generate description' } - ), - toastMessage: getFormattedError(error).message, - }); + if (!silent) { + notifications.toasts.addError(error, { + title: i18n.translate( + 'xpack.streams.streamDetailView.streamDescription.generateErrorTitle', + { defaultMessage: 'Failed to generate description' } + ), + toastMessage: getFormattedError(error).message, + }); + } } finally { setIsGenerating(false); } }, [ + silent, aiFeatures?.genAiConnectors.selectedConnector, streams.streamsRepositoryClient, signal, @@ -182,7 +197,9 @@ export const useStreamDescriptionApi = ({ const onGenerateDescription = useCallback(async () => { const result = await generate(); - setIsEditing(true); + if (result) { + setIsEditing(true); + } return result; }, [generate, setIsEditing]); @@ -214,5 +231,6 @@ export const useStreamDescriptionApi = ({ onGenerateDescription, onStartEditing, onSaveDescription, + abort, }; }; diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/add_significant_event_flyout/add_significant_event_flyout.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/add_significant_event_flyout/add_significant_event_flyout.tsx index 3dcecfc43fbed..61526f3d956d8 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/add_significant_event_flyout/add_significant_event_flyout.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_significant_events_view/add_significant_event_flyout/add_significant_event_flyout.tsx @@ -94,8 +94,11 @@ export function AddSignificantEventFlyout({ }); }, [data.dataViews, definition.stream.name]); - const { onGenerateDescription: generateDescription, onSaveDescription: saveDescription } = - useStreamDescriptionApi({ definition, refreshDefinition, aiFeatures }); + const { + onGenerateDescription: generateDescription, + onSaveDescription: saveDescription, + abort: abortDescription, + } = useStreamDescriptionApi({ definition, refreshDefinition, aiFeatures, silent: true }); const { generate, abort } = useSignificantEventsApi({ name: definition.stream.name, start, end }); @@ -116,7 +119,8 @@ export function AddSignificantEventFlyout({ const stopGeneration = useCallback(() => { setIsGenerating(false); abort(); - }, [abort]); + abortDescription(); + }, [abort, abortDescription]); const parsedQueries = useMemo(() => { return streamQuerySchema.array().safeParse(queries);