diff --git a/x-pack/platform/packages/shared/kbn-streams-schema/src/tasks/types.ts b/x-pack/platform/packages/shared/kbn-streams-schema/src/tasks/types.ts index ebff24042ba48..f584381d21d5b 100644 --- a/x-pack/platform/packages/shared/kbn-streams-schema/src/tasks/types.ts +++ b/x-pack/platform/packages/shared/kbn-streams-schema/src/tasks/types.ts @@ -30,4 +30,5 @@ export type TaskResult = | TaskStatus.Canceled; } | { status: TaskStatus.Failed; error: string } - | ({ status: TaskStatus.Completed | TaskStatus.Acknowledged } & TPayload); + | ({ status: TaskStatus.Completed } & TPayload) + | ({ status: TaskStatus.Acknowledged } & TPayload); diff --git a/x-pack/platform/plugins/shared/streams/server/lib/tasks/task_definitions/onboarding.ts b/x-pack/platform/plugins/shared/streams/server/lib/tasks/task_definitions/onboarding.ts index fdd8f6485661b..c9f9afde3f5c2 100644 --- a/x-pack/platform/plugins/shared/streams/server/lib/tasks/task_definitions/onboarding.ts +++ b/x-pack/platform/plugins/shared/streams/server/lib/tasks/task_definitions/onboarding.ts @@ -54,30 +54,6 @@ export function getOnboardingTaskId(streamName: string, saveQueries: boolean = t return saveQueries ? base : `${base}_no_save_queries`; } -const FEATURES_IDENTIFICATION_RECENCY_MS = 12 * 60 * 60 * 1000; // 12 hours -async function areFeaturesUpToDate({ - taskClient, - featuresTaskId, -}: { - taskClient: TaskClient; - featuresTaskId: string; -}) { - const featuresTask = await taskClient.get< - FeaturesIdentificationTaskParams, - IdentifyFeaturesResult - >(featuresTaskId); - - if (featuresTask.status !== TaskStatus.Completed) { - return false; - } - - return ( - featuresTask.last_completed_at && - Date.now() - new Date(featuresTask.last_completed_at).getTime() < - FEATURES_IDENTIFICATION_RECENCY_MS - ); -} - export function createStreamsOnboardingTask(taskContext: TaskContext) { return { [STREAMS_ONBOARDING_TASK_TYPE]: { @@ -115,32 +91,20 @@ export function createStreamsOnboardingTask(taskContext: TaskContext) { case OnboardingStep.FeaturesIdentification: { const featuresTaskId = getFeaturesIdentificationTaskId(streamName); - if ( - await areFeaturesUpToDate({ - taskClient, - featuresTaskId, - }) - ) { - featuresTaskResult = await taskClient.getStatus< - FeaturesIdentificationTaskParams, - IdentifyFeaturesResult - >(featuresTaskId); - } else { - await scheduleFeaturesIdentificationTask( - { - start: from, - end: to, - streamName, - }, - taskClient, - runContext.fakeRequest - ); - - featuresTaskResult = await waitForSubtask< - FeaturesIdentificationTaskParams, - IdentifyFeaturesResult - >(featuresTaskId, runContext.taskInstance.id, taskClient); - } + await scheduleFeaturesIdentificationTask( + { + start: from, + end: to, + streamName, + }, + taskClient, + runContext.fakeRequest + ); + + featuresTaskResult = await waitForSubtask< + FeaturesIdentificationTaskParams, + IdentifyFeaturesResult + >(featuresTaskId, runContext.taskInstance.id, taskClient); if (featuresTaskResult.status !== TaskStatus.Completed) { return; diff --git a/x-pack/platform/plugins/shared/streams/server/routes/internal/streams/onboarding/route.ts b/x-pack/platform/plugins/shared/streams/server/routes/internal/streams/onboarding/route.ts index 23d926a9ddd4d..f3decfed4279f 100644 --- a/x-pack/platform/plugins/shared/streams/server/routes/internal/streams/onboarding/route.ts +++ b/x-pack/platform/plugins/shared/streams/server/routes/internal/streams/onboarding/route.ts @@ -10,10 +10,10 @@ import { BooleanFromString } from '@kbn/zod-helpers/v4'; import type { OnboardingResult, TaskResult } from '@kbn/streams-schema'; import { OnboardingStep } from '@kbn/streams-schema'; import { STREAMS_API_PRIVILEGES } from '../../../../../common/constants'; -import type { OnboardingTaskParams } from '../../../../lib/tasks/task_definitions/onboarding'; import { getOnboardingTaskId, STREAMS_ONBOARDING_TASK_TYPE, + type OnboardingTaskParams, } from '../../../../lib/tasks/task_definitions/onboarding'; import { createServerRoute } from '../../../create_server_route'; import { assertSignificantEventsAccess } from '../../../utils/assert_significant_events_access'; diff --git a/x-pack/platform/plugins/shared/streams_app/moon.yml b/x-pack/platform/plugins/shared/streams_app/moon.yml index acc3c296c17af..4e3b80c0bc9a6 100644 --- a/x-pack/platform/plugins/shared/streams_app/moon.yml +++ b/x-pack/platform/plugins/shared/streams_app/moon.yml @@ -107,6 +107,7 @@ dependsOn: - '@kbn/inference-endpoint-ui-common' - '@kbn/stack-connectors-plugin' - '@kbn/std' + - '@kbn/streams-ai' - '@kbn/cps' tags: - plugin diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/queries_table/query_details_flyout.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/queries_table/query_details_flyout.tsx index b17edf0837dd8..e0c6712450039 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/queries_table/query_details_flyout.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/queries_table/query_details_flyout.tsx @@ -10,7 +10,6 @@ import { EuiButton, EuiButtonEmpty, EuiButtonIcon, - EuiCodeBlock, EuiConfirmModal, EuiContextMenuItem, EuiContextMenuPanel, @@ -30,33 +29,19 @@ import { EuiText, EuiTextArea, EuiTitle, - EuiToolTip, useEuiTheme, useGeneratedHtmlId, } from '@elastic/eui'; import { css } from '@emotion/react'; import { i18n } from '@kbn/i18n'; -import React, { useEffect, useMemo, useState } from 'react'; -import { StreamsESQLEditor } from '../../../../esql_query_editor'; +import React, { useEffect, useState } from 'react'; import type { SignificantEventItem } from '../../../../../hooks/sig_events/use_fetch_significant_events'; +import { StreamsESQLEditor } from '../../../../esql_query_editor'; import { InfoPanel } from '../../../../info_panel'; import { SparkPlot } from '../../../../spark_plot'; -import { SeveritySelector } from '../../../stream_detail_significant_events_view/add_significant_event_flyout/common/severity_selector'; +import { SeveritySelector } from '../severity_selector'; import { SeverityBadge } from '../severity_badge/severity_badge'; -import { - BACKED_STATUS_COLUMN, - IMPACT_COLUMN, - LAST_OCCURRED_COLUMN, - NOT_PROMOTED_BADGE_LABEL, - NOT_PROMOTED_TOOLTIP_CONTENT, - OCCURRENCES_COLUMN, - OCCURRENCES_TOOLTIP_NAME, - PROMOTED_BADGE_LABEL, - PROMOTED_TOOLTIP_CONTENT, - STREAM_COLUMN, -} from './translations'; -import { formatLastOccurredAt } from './utils'; -import { AssetImage } from '../../../../asset_image'; +import { OCCURRENCES_COLUMN, OCCURRENCES_TOOLTIP_NAME } from './translations'; interface QueryDetailsFlyoutProps { item: SignificantEventItem; @@ -99,14 +84,6 @@ export function QueryDetailsFlyout({ setSeverityScore(item.query.severity_score); }, [item]); - const lastOccurredAt = useMemo( - () => formatLastOccurredAt(item.occurrences, DEFAULT_QUERY_PLACEHOLDER), - [item.occurrences] - ); - const hasDetectedOccurrences = useMemo( - () => item.occurrences.some((occurrence) => occurrence.y > 0), - [item.occurrences] - ); const isSaveDisabled = !title.trim() || !query.trim() || isSaving; const handleCancelEdit = () => { @@ -131,12 +108,14 @@ export function QueryDetailsFlyout({ }; const infoListItems = [ + { + title: TYPE_LABEL, + description: {QUERY_TYPE_BADGE_LABEL}, + }, { title: QUERY_LABEL, description: ( - - {getDisplayQueryValue(item)} - + {getQueryInputValue(item) || DEFAULT_QUERY_PLACEHOLDER} ), }, { @@ -146,30 +125,9 @@ export function QueryDetailsFlyout({ ), }, { - title: IMPACT_COLUMN, + title: SEVERITY_DETAILS_LABEL, description: , }, - { - title: LAST_OCCURRED_COLUMN, - description: {lastOccurredAt}, - }, - { - title: STREAM_COLUMN, - description: {item.stream_name}, - }, - { - title: BACKED_STATUS_COLUMN, - description: ( - - - {item.rule_backed && {PROMOTED_BADGE_LABEL}} - {!item.rule_backed && {NOT_PROMOTED_BADGE_LABEL}} - - - ), - }, ]; return ( @@ -193,6 +151,7 @@ export function QueryDetailsFlyout({ - {hasDetectedOccurrences ? ( - - ) : ( - - - - {NO_OCCURRENCES_DESCRIPTION} - - - )} + @@ -397,16 +341,21 @@ function getQueryInputValue(item: SignificantEventItem) { return item.query.esql?.query ?? ''; } -function getDisplayQueryValue(item: SignificantEventItem) { - const queryText = getQueryInputValue(item); - return queryText || DEFAULT_QUERY_PLACEHOLDER; -} - const QUERY_INFORMATION_TITLE = i18n.translate( 'xpack.streams.significantEventsDiscovery.queryDetailsFlyout.queryInformationTitle', { defaultMessage: 'Query information' } ); +const TYPE_LABEL = i18n.translate( + 'xpack.streams.significantEventsDiscovery.queryDetailsFlyout.typeLabel', + { defaultMessage: 'Type' } +); + +const QUERY_TYPE_BADGE_LABEL = i18n.translate( + 'xpack.streams.significantEventsDiscovery.queryDetailsFlyout.queryTypeBadgeLabel', + { defaultMessage: 'Query' } +); + const EDIT_QUERY_TITLE = i18n.translate( 'xpack.streams.significantEventsDiscovery.queryDetailsFlyout.editQueryTitle', { defaultMessage: 'Edit query' } @@ -437,6 +386,11 @@ const SEVERITY_LABEL = i18n.translate( { defaultMessage: 'Severity' } ); +const SEVERITY_DETAILS_LABEL = i18n.translate( + 'xpack.streams.significantEventsDiscovery.queryDetailsFlyout.severityDetailsLabel', + { defaultMessage: 'Severity' } +); + const ACTIONS_BUTTON_ARIA_LABEL = i18n.translate( 'xpack.streams.significantEventsDiscovery.queryDetailsFlyout.actionsButtonAriaLabel', { defaultMessage: 'Actions' } @@ -483,11 +437,3 @@ const DELETE_CONFIRM_BUTTON_LABEL = i18n.translate( 'xpack.streams.significantEventsDiscovery.queryDetailsFlyout.deleteConfirmButtonLabel', { defaultMessage: 'Delete query' } ); - -const NO_OCCURRENCES_DESCRIPTION = i18n.translate( - 'xpack.streams.significantEventsDiscovery.queryDetailsFlyout.noOccurrencesDescription', - { - defaultMessage: - "We currently don't detect any events. You can leave it, as it might happen later or modify the query.", - } -); diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/add_significant_event_flyout/types.ts b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/severity_selector/index.ts similarity index 52% rename from x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/add_significant_event_flyout/types.ts rename to x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/severity_selector/index.ts index 3af89f85a1954..523c51d0612d1 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/add_significant_event_flyout/types.ts +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/severity_selector/index.ts @@ -5,10 +5,4 @@ * 2.0. */ -import type { StreamQuery } from '@kbn/streams-schema'; - -export type Flow = 'manual' | 'ai'; - -export type SaveData = - | { type: 'single'; query: StreamQuery; isUpdating?: boolean } - | { type: 'multiple'; queries: StreamQuery[] }; +export { SeveritySelector } from './severity_selector'; diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/add_significant_event_flyout/common/severity_selector.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/severity_selector/severity_selector.tsx similarity index 85% rename from x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/add_significant_event_flyout/common/severity_selector.tsx rename to x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/severity_selector/severity_selector.tsx index 90301aa42fa2c..680fd259b7d3a 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/add_significant_event_flyout/common/severity_selector.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/severity_selector/severity_selector.tsx @@ -12,7 +12,7 @@ import { SeverityBadge, SIGNIFICANT_EVENT_SEVERITY, scoreSeverity, -} from '../../../significant_events_discovery/components/severity_badge/severity_badge'; +} from '../severity_badge/severity_badge'; export function SeveritySelector({ severityScore, @@ -36,6 +36,7 @@ export function SeveritySelector({ return ( ); } + +const SEVERITY_SELECTOR_ARIA_LABEL = i18n.translate( + 'xpack.streams.significantEvents.severitySelector.ariaLabel', + { + defaultMessage: 'Select severity', + } +); diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/add_significant_event_flyout/add_significant_event_flyout.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/add_significant_event_flyout/add_significant_event_flyout.tsx deleted file mode 100644 index dd33ac3ef4edf..0000000000000 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/add_significant_event_flyout/add_significant_event_flyout.tsx +++ /dev/null @@ -1,403 +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 { - EuiButton, - EuiButtonEmpty, - EuiFlexGroup, - EuiFlexItem, - EuiFlyout, - EuiFlyoutBody, - EuiFlyoutHeader, - EuiPanel, - EuiSpacer, - EuiText, - EuiTitle, - useEuiTheme, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import type { OnboardingResult, TaskResult } from '@kbn/streams-schema'; -import { TaskStatus, type StreamQuery, type Streams } from '@kbn/streams-schema'; -import { streamQuerySchema } from '@kbn/streams-schema'; -import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { css } from '@emotion/css'; -import { v4 } from 'uuid'; -import useAsyncFn from 'react-use/lib/useAsyncFn'; -import { useBoolean } from '@kbn/react-hooks'; -import { useKibana } from '../../../../hooks/use_kibana'; -import { useOnboardingApi } from '../../../../hooks/use_onboarding_api'; -import type { AIFeatures } from '../../../../hooks/use_ai_features'; -import { GeneratedFlowForm } from './generated_flow_form/generated_flow_form'; -import { ManualFlowForm } from './manual_flow_form/manual_flow_form'; -import type { Flow, SaveData } from './types'; -import { defaultQuery } from './utils/default_query'; -import { StreamsAppSearchBar } from '../../../streams_app_search_bar'; -import { validateEsqlQuery } from './common/validate_query'; -import { useStreamsAppFetch } from '../../../../hooks/use_streams_app_fetch'; -import { useTaskPolling } from '../../../../hooks/use_task_polling'; -import { SignificantEventsGenerationPanel } from '../generation_panel'; - -const defaultTask: TaskResult = { - status: TaskStatus.NotStarted, -}; -interface Props { - onClose: () => void; - definition: Streams.all.GetResponse; - onSave: (data: SaveData) => Promise; - query?: StreamQuery; - initialFlow?: Flow; - generateOnMount: boolean; - aiFeatures: AIFeatures | null; -} - -export function AddSignificantEventFlyout({ - generateOnMount, - query, - onClose, - definition, - onSave, - initialFlow = undefined, - aiFeatures, -}: Props) { - const { euiTheme } = useEuiTheme(); - const { - dependencies: { - start: { data }, - }, - } = useKibana(); - - const dataViewsFetch = useStreamsAppFetch(() => { - return data.dataViews.create({ title: definition.stream.name }).then((value) => { - return [value]; - }); - }, [data.dataViews, definition.stream.name]); - - const { scheduleOnboardingTask, getOnboardingTaskStatus, cancelOnboardingTask } = - useOnboardingApi({ - saveQueries: false, - }); - - const streamName = definition.stream.name; - - const isEditMode = !!query?.id; - const [selectedFlow, setSelectedFlow] = useState( - isEditMode ? 'manual' : initialFlow - ); - const flowRef = useRef(selectedFlow); - const [queries, setQueries] = useState([{ ...defaultQuery(), ...query }]); - const [canSave, setCanSave] = useState(false); - const [isSubmitting, setIsSubmitting] = useState(false); - - const [generatedQueries, setGeneratedQueries] = useState([]); - - const [task, setTask] = useState>(defaultTask); - const [isGettingTaskStatus, { on: gettingTaskStatus, off: stoppedGettingTaskStatus }] = - useBoolean(false); - - const [{ loading: isSchedulingGenerationTask }, doScheduleOnboardingTask] = useAsyncFn( - scheduleOnboardingTask, - [scheduleOnboardingTask] - ); - - const scheduleTask = () => { - setTask(defaultTask); - doScheduleOnboardingTask(streamName).then(setTask); - }; - - const getTaskStatus = useCallback(() => { - gettingTaskStatus(); - getOnboardingTaskStatus(streamName).then(setTask).finally(stoppedGettingTaskStatus); - }, [stoppedGettingTaskStatus, gettingTaskStatus, streamName, getOnboardingTaskStatus]); - - useEffect(() => { - // Skip initial status fetch when we are about to schedule a new generation on mount, - // to avoid a race where the previous completed task resolves after the reset and - // surfaces stale queries alongside the new loading indicator. - if (generateOnMount && initialFlow === 'ai') { - return; - } - - getTaskStatus(); - }, [generateOnMount, getTaskStatus, initialFlow]); - - const pollTask = useCallback( - () => getOnboardingTaskStatus(streamName), - [getOnboardingTaskStatus, streamName] - ); - - const cancelOnboarding = useCallback( - () => cancelOnboardingTask(streamName), - [cancelOnboardingTask, streamName] - ); - - const { cancelTask, isCancellingTask } = useTaskPolling({ - task, - onPoll: pollTask, - onRefresh: getTaskStatus, - onCancel: cancelOnboarding, - }); - - const isGenerating = - task?.status === 'in_progress' || - task?.status === 'being_canceled' || - isGettingTaskStatus || - isSchedulingGenerationTask; - - const prevTaskStatusRef = useRef(undefined); - - useEffect(() => { - const prevStatus = prevTaskStatusRef.current; - - // Process completed when: - // - Transitioning from any non-completed state to completed - const isNewlyCompleted = - task?.status === TaskStatus.Completed && prevStatus !== TaskStatus.Completed; - if (isNewlyCompleted) { - const queriesResult = task.queriesTaskResult; - const completedQueries = - queriesResult?.status === TaskStatus.Completed ? queriesResult.queries : []; - - setGeneratedQueries( - completedQueries - .filter((nextQuery) => validateEsqlQuery(nextQuery.esql.query).isInvalid === false) - .map((nextQuery) => ({ - id: v4(), - esql: nextQuery.esql, - title: nextQuery.title, - description: nextQuery.description, - severity_score: nextQuery.severity_score, - evidence: nextQuery.evidence, - })) - ); - } - - prevTaskStatusRef.current = task?.status; - }, [task]); - - const parsedQueries = useMemo(() => { - return streamQuerySchema.array().safeParse(queries); - }, [queries]); - - useEffect(() => { - if (flowRef.current !== selectedFlow) { - flowRef.current = selectedFlow; - setIsSubmitting(false); - setCanSave(false); - setQueries([defaultQuery()]); - } - }, [selectedFlow]); - - const generateQueries = () => { - setSelectedFlow('ai'); - setGeneratedQueries([]); - scheduleTask(); - }; - - useEffect(() => { - if (initialFlow === 'ai' && generateOnMount) { - generateQueries(); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [aiFeatures?.enabled]); - - return ( - onClose()} - size={isEditMode ? 'm' : 'l'} - type={isEditMode ? 'push' : 'overlay'} - > - - -

- {isEditMode - ? i18n.translate( - 'xpack.streams.streamDetailView.addSignificantEventFlyout.editTitle', - { defaultMessage: 'Edit significant events' } - ) - : i18n.translate( - 'xpack.streams.streamDetailView.addSignificantEventFlyout.createTitle', - { defaultMessage: 'Add significant events' } - )} -

-
-
- - - {!isEditMode && ( - - - setSelectedFlow('manual')} - onGenerateSuggestionsClick={generateQueries} - isGeneratingQueries={isGenerating} - isSavingManualEntry={isSubmitting} - selectedFlow={selectedFlow} - /> - - - )} - - - - - {flowRef.current === 'manual' && ( - <> - - - -

- {i18n.translate( - 'xpack.streams.streamDetailView.addSignificantEventFlyout.previewSignificantEventsLabel', - { defaultMessage: 'Preview significant events' } - )} -

-
-
- - - -
- - setQueries([next])} - query={queries[0]} - setCanSave={setCanSave} - definition={definition.stream} - /> - - )} - - {flowRef.current === 'ai' && ( - { - setGeneratedQueries((prev) => - prev.map((q) => (q.id === editedQuery.id ? editedQuery : q)) - ); - }} - stopGeneration={cancelTask} - definition={definition.stream} - setQueries={(next: StreamQuery[]) => { - setQueries(next); - }} - setCanSave={(next: boolean) => { - setCanSave(next); - }} - dataViews={dataViewsFetch.value ?? []} - taskStatus={task?.status} - taskError={task?.status === 'failed' ? task.error : undefined} - /> - )} -
-
- - - - onClose()} - disabled={isSubmitting} - data-test-subj={ - selectedFlow === 'manual' - ? 'significant_events_manual_entry_cancel_button' - : 'significant_events_ai_generate_cancel_button' - } - > - {i18n.translate( - 'xpack.streams.streamDetailView.addSignificantEventFlyout.cancelButtonLabel', - { defaultMessage: 'Cancel' } - )} - - { - setIsSubmitting(true); - - switch (selectedFlow) { - case 'manual': - onSave({ - type: 'single', - query: queries[0], - isUpdating: isEditMode, - }).finally(() => setIsSubmitting(false)); - break; - case 'ai': - onSave({ - type: 'multiple', - queries, - }).finally(() => setIsSubmitting(false)); - break; - } - }} - data-test-subj={ - isEditMode - ? 'significant_events_edit_save_button' - : selectedFlow === 'manual' - ? 'significant_events_manual_entry_save_button' - : 'significant_events_ai_generate_save_button' - } - > - {selectedFlow === 'manual' - ? isEditMode - ? i18n.translate( - 'xpack.streams.streamDetailView.addSignificantEventFlyout.updateButtonLabel', - { defaultMessage: 'Update event' } - ) - : i18n.translate( - 'xpack.streams.streamDetailView.addSignificantEventFlyout.addButtonLabel', - { defaultMessage: 'Add event' } - ) - : i18n.translate( - 'xpack.streams.streamDetailView.addSignificantEventFlyout.addSelectedButtonLabel', - { defaultMessage: 'Add selected' } - )} - - - -
-
-
-
-
- ); -} diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/add_significant_event_flyout/common/get_valid_prefixes.test.ts b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/add_significant_event_flyout/common/get_valid_prefixes.test.ts deleted file mode 100644 index 1761d48aae493..0000000000000 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/add_significant_event_flyout/common/get_valid_prefixes.test.ts +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { Streams } from '@kbn/streams-schema'; -import { getValidPrefixes } from './get_valid_prefixes'; - -const makeClassicDefinition = (name: string): Streams.ClassicStream.Definition => ({ - type: 'classic', - name, - description: '', - updated_at: new Date().toISOString(), - ingest: { - lifecycle: { inherit: {} }, - processing: { steps: [], updated_at: new Date().toISOString() }, - settings: {}, - classic: {}, - failure_store: { inherit: {} }, - }, -}); - -const makeWiredDefinition = (name: string): Streams.WiredStream.Definition => ({ - type: 'wired', - name, - description: '', - updated_at: new Date().toISOString(), - ingest: { - lifecycle: { inherit: {} }, - processing: { steps: [], updated_at: new Date().toISOString() }, - settings: {}, - wired: { fields: {}, routing: [] }, - failure_store: { inherit: {} }, - }, -}); - -describe('getValidPrefixes', () => { - describe('wired stream definitions', () => { - it('returns only the primary prefix', () => { - const definition = makeWiredDefinition('logs'); - const result = getValidPrefixes(definition); - expect(result.primary).toBe('FROM logs,logs.* METADATA _id, _source'); - expect(result.alsoAllowed).toBeUndefined(); - }); - - it('returns only the primary prefix even when initialEsql uses wired-style pattern', () => { - const definition = makeWiredDefinition('logs'); - const result = getValidPrefixes( - definition, - 'FROM logs,logs.* METADATA _id, _source | WHERE severity = "critical"' - ); - expect(result.primary).toBe('FROM logs,logs.* METADATA _id, _source'); - expect(result.alsoAllowed).toBeUndefined(); - }); - }); - - describe('classic stream definitions', () => { - it('returns only the primary prefix when no initialEsql is provided', () => { - const definition = makeClassicDefinition('logs'); - const result = getValidPrefixes(definition); - expect(result.primary).toBe('FROM logs METADATA _id, _source'); - expect(result.alsoAllowed).toBeUndefined(); - }); - - it('returns only the primary prefix when initialEsql uses the primary pattern', () => { - const definition = makeClassicDefinition('logs'); - const result = getValidPrefixes( - definition, - 'FROM logs METADATA _id, _source | WHERE severity = "critical"' - ); - expect(result.primary).toBe('FROM logs METADATA _id, _source'); - expect(result.alsoAllowed).toBeUndefined(); - }); - - it('includes wired-style as alternative when initialEsql uses the wired-style pattern', () => { - const definition = makeClassicDefinition('logs'); - const result = getValidPrefixes( - definition, - 'FROM logs,logs.* METADATA _id, _source | WHERE severity = "critical"' - ); - expect(result.primary).toBe('FROM logs METADATA _id, _source'); - expect(result.alsoAllowed).toEqual(['FROM logs,logs.* METADATA _id, _source']); - }); - }); -}); diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/add_significant_event_flyout/common/get_valid_prefixes.ts b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/add_significant_event_flyout/common/get_valid_prefixes.ts deleted file mode 100644 index bf95e5208bce9..0000000000000 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/add_significant_event_flyout/common/get_valid_prefixes.ts +++ /dev/null @@ -1,60 +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 { BasicPrettyPrinter, Builder } from '@elastic/esql'; -import { - Streams, - buildMetadataOption, - getIndexPatternsForStream, - normalizeEsqlQuery, -} from '@kbn/streams-schema'; -import type { ValidPrefixes } from '../../../../esql_query_editor'; - -const getDefaultQueryFrom = (definition: Streams.all.Definition) => - BasicPrettyPrinter.print( - Builder.expression.query([ - Builder.command({ - name: 'from', - args: [ - Builder.expression.source.index(getIndexPatternsForStream(definition).join(',')), - buildMetadataOption(), - ], - }), - ]) - ); - -export const getValidPrefixes = ( - definition: Streams.all.Definition, - initialEsql?: string -): ValidPrefixes => { - const primary = getDefaultQueryFrom(definition); - - // Classic streams use "FROM " as the primary prefix, but older significant events - // may have been saved with the wired-style pattern "FROM ,.*". - // When we detect that the initial query uses the wired-style pattern, we accept both - // so existing queries remain valid without forcing users to re-edit them. - if (Streams.ClassicStream.Definition.is(definition) && initialEsql) { - const wiredStylePrefix = BasicPrettyPrinter.print( - Builder.expression.query([ - Builder.command({ - name: 'from', - args: [ - Builder.expression.source.index(`${definition.name},${definition.name}.*`), - buildMetadataOption(), - ], - }), - ]) - ); - const normalizedInitial = normalizeEsqlQuery(initialEsql); - const normalizedWired = normalizeEsqlQuery(wiredStylePrefix); - if (normalizedInitial.startsWith(normalizedWired)) { - return { primary, alsoAllowed: [wiredStylePrefix] }; - } - } - - return { primary }; -}; diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/add_significant_event_flyout/common/preview_data_spark_plot.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/add_significant_event_flyout/common/preview_data_spark_plot.tsx deleted file mode 100644 index 7a9cedd66ec46..0000000000000 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/add_significant_event_flyout/common/preview_data_spark_plot.tsx +++ /dev/null @@ -1,234 +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 { niceTimeFormatter } from '@elastic/charts'; -import { - EuiButtonEmpty, - EuiFlexGroup, - EuiFlexItem, - EuiIcon, - EuiLoadingSpinner, - EuiPanel, - EuiText, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import type { StreamQuery, Streams } from '@kbn/streams-schema'; -import React, { useMemo } from 'react'; -import { useEuiTheme } from '@elastic/eui'; -import { DISCOVER_APP_LOCATOR, type DiscoverAppLocatorParams } from '@kbn/discover-plugin/common'; -import type { AbsoluteTimeRange } from '@kbn/es-query'; -import { useKibana } from '../../../../../hooks/use_kibana'; -import { useTimefilter } from '../../../../../hooks/use_timefilter'; -import { SparkPlot } from '../../../../spark_plot'; -import { useSignificantEventPreviewFetch } from '../manual_flow_form/use_significant_event_preview_fetch'; -import { useSparkplotDataFromSigEvents } from '../manual_flow_form/use_spark_plot_data_from_sig_events'; -import { AssetImage } from '../../../../asset_image'; - -export function PreviewDataSparkPlot({ - query, - definition, - isQueryValid, - showTitle = true, - compressed = false, - hideAxis = false, - height, - noOfBuckets, - timeRange, -}: { - definition: Streams.all.Definition; - query: StreamQuery; - isQueryValid: boolean; - showTitle?: boolean; - compressed?: boolean; - hideAxis?: boolean; - height?: number; - noOfBuckets?: number; - timeRange?: AbsoluteTimeRange; -}) { - const { timeState } = useTimefilter(); - const { euiTheme } = useEuiTheme(); - const effectiveTimeRange = timeRange ?? timeState.asAbsoluteTimeRange; - - const previewFetch = useSignificantEventPreviewFetch({ - name: definition.name, - esqlQuery: query.esql.query, - timeRange: timeRange ?? timeState.asAbsoluteTimeRange, - isQueryValid, - noOfBuckets, - }); - - const xFormatter = useMemo(() => { - return niceTimeFormatter([ - new Date(effectiveTimeRange.from).getTime(), - new Date(effectiveTimeRange.to).getTime(), - ]); - }, [effectiveTimeRange.from, effectiveTimeRange.to]); - - const sparkPlotData = useSparkplotDataFromSigEvents({ - previewFetch, - query, - xFormatter, - }); - - const noOccurrencesFound = sparkPlotData.timeseries.every((point) => point.y === 0); - - const { - dependencies: { - start: { share }, - }, - } = useKibana(); - const useUrl = share.url.locators.useUrl; - - const discoverEsqlQuery = query.esql.query; - - const discoverLink = useUrl( - () => ({ - id: DISCOVER_APP_LOCATOR, - params: { - query: { - esql: discoverEsqlQuery, - }, - }, - }), - [discoverEsqlQuery] - ); - - function renderContent() { - if (isQueryValid === false) { - return ( - <> - - - {i18n.translate( - 'xpack.streams.addSignificantEventFlyout.manualFlow.previewChartPrompt', - { - defaultMessage: 'Preview will appear here', - } - )} - - - ); - } - - if (previewFetch.loading) { - return ; - } - - if (previewFetch.error) { - if (compressed) { - return ; - } - - return ( - <> - - - {i18n.translate( - 'xpack.streams.addSignificantEventFlyout.manualFlow.previewChartError', - { - defaultMessage: 'Failed to load preview data', - } - )} - - - ); - } - - if (noOccurrencesFound) { - if (compressed) { - return ( - - ); - } - - return ( - <> - - - {i18n.translate( - 'xpack.streams.addSignificantEventFlyout.manualFlow.previewChartNoData', - { - defaultMessage: 'No events found, make sure to review your query', - } - )} - - - ); - } - - const openInDiscoverLabel = i18n.translate( - 'xpack.streams.addSignificantEventFlyout.manualFlow.previewChartOpenInDiscover', - { defaultMessage: 'Open in Discover' } - ); - - return ( - <> - {showTitle && ( - - - - {i18n.translate( - 'xpack.streams.addSignificantEventFlyout.manualFlow.previewChartDetectedOccurrences', - { - defaultMessage: 'Detected event occurrences ({count})', - values: { - count: sparkPlotData.timeseries.reduce((acc, point) => acc + point.y, 0), - }, - } - )} - - - - - {openInDiscoverLabel} - - - - )} - - - ); - } - - return ( - - - {renderContent()} - - - ); -} diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/add_significant_event_flyout/common/validate_query.ts b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/add_significant_event_flyout/common/validate_query.ts deleted file mode 100644 index c42a9276ec769..0000000000000 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/add_significant_event_flyout/common/validate_query.ts +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { StreamQuery } from '@kbn/streams-schema'; -import { i18n } from '@kbn/i18n'; -import { Parser } from '@elastic/esql'; - -export interface FieldValidation { - isInvalid: boolean; - error?: string; -} - -const REQUIRED_MESSAGE = i18n.translate( - 'xpack.streams.significantEventFlyout.formFieldRequiredError', - { defaultMessage: 'Required' } -); - -const INVALID_SYNTAX_MESSAGE = i18n.translate( - 'xpack.streams.significantEventFlyout.formFieldQuerySyntaxError', - { defaultMessage: 'Invalid syntax' } -); - -export function validateTitle(title: string): FieldValidation { - const isEmpty = title.length === 0; - return { - isInvalid: isEmpty, - error: isEmpty ? REQUIRED_MESSAGE : undefined, - }; -} - -export function validateEsqlQuery(esqlQuery: string): FieldValidation { - if (!esqlQuery.trim()) { - return { isInvalid: true, error: REQUIRED_MESSAGE }; - } - - let hasSyntaxError = false; - try { - const { errors } = Parser.parse(esqlQuery); - hasSyntaxError = errors.length > 0; - } catch { - hasSyntaxError = true; - } - - return { - isInvalid: hasSyntaxError, - error: hasSyntaxError ? INVALID_SYNTAX_MESSAGE : undefined, - }; -} - -export function validateQuery(query: Pick): { - title: FieldValidation; - esql: FieldValidation; -} { - return { - title: validateTitle(query.title ?? ''), - esql: validateEsqlQuery(query.esql?.query ?? ''), - }; -} diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/add_significant_event_flyout/edit_significant_event_flyout.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/add_significant_event_flyout/edit_significant_event_flyout.tsx deleted file mode 100644 index ff3f00ef09fcf..0000000000000 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/add_significant_event_flyout/edit_significant_event_flyout.tsx +++ /dev/null @@ -1,143 +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'; -import React from 'react'; -import type { StreamQuery, Streams } from '@kbn/streams-schema'; -import { useSignificantEventsApi } from '../../../../hooks/sig_events/use_significant_events_api'; -import { useOnboardingApi } from '../../../../hooks/use_onboarding_api'; -import { useKibana } from '../../../../hooks/use_kibana'; -import type { AIFeatures } from '../../../../hooks/use_ai_features'; -import { AddSignificantEventFlyout } from './add_significant_event_flyout'; -import type { Flow, SaveData } from './types'; -import { getStreamTypeFromDefinition } from '../../../../util/get_stream_type_from_definition'; - -export const EditSignificantEventFlyout = ({ - queryToEdit, - definition, - isEditFlyoutOpen, - setIsEditFlyoutOpen, - initialFlow, - setQueryToEdit, - refresh, - generateOnMount, - aiFeatures, -}: { - refresh: () => void; - setQueryToEdit: React.Dispatch>; - initialFlow?: Flow; - queryToEdit?: StreamQuery; - definition: Streams.all.GetResponse; - isEditFlyoutOpen: boolean; - setIsEditFlyoutOpen: React.Dispatch>; - generateOnMount: boolean; - aiFeatures: AIFeatures | null; -}) => { - const { - core: { notifications }, - services: { telemetryClient }, - } = useKibana(); - - const { upsertQuery, bulk } = useSignificantEventsApi({ - name: definition.stream.name, - }); - - const { acknowledgeOnboardingTask } = useOnboardingApi({ - saveQueries: false, - }); - - const onCloseFlyout = () => { - setIsEditFlyoutOpen(false); - setQueryToEdit(undefined); - }; - - return isEditFlyoutOpen ? ( - { - const streamType = getStreamTypeFromDefinition(definition.stream); - - switch (data.type) { - case 'single': - await upsertQuery(data.query).then( - () => { - notifications.toasts.addSuccess({ - title: i18n.translate( - 'xpack.streams.significantEvents.savedSingle.successfullyToastTitle', - { defaultMessage: `Saved significant event query successfully` } - ), - }); - - onCloseFlyout(); - refresh(); - - telemetryClient.trackSignificantEventsCreated({ - count: 1, - stream_name: definition.stream.name, - stream_type: streamType, - }); - }, - (error) => { - notifications.showErrorDialog({ - title: i18n.translate( - 'xpack.streams.significantEvents.savedSingle.errorToastTitle', - { defaultMessage: `Could not save significant event query` } - ), - error, - }); - } - ); - break; - case 'multiple': - await bulk( - data.queries.map((bulkQuery) => ({ - index: bulkQuery, - })) - ).then( - async () => { - // Acknowledge the task after successful save - await acknowledgeOnboardingTask(definition.stream.name).catch(() => { - // Ignore errors - task acknowledgment is not critical - }); - - notifications.toasts.addSuccess({ - title: i18n.translate( - 'xpack.streams.significantEvents.savedMultiple.successfullyToastTitle', - { defaultMessage: `Saved significant events queries successfully` } - ), - }); - - onCloseFlyout(); - refresh(); - - telemetryClient.trackSignificantEventsCreated({ - count: data.queries.length, - stream_name: definition.stream.name, - stream_type: streamType, - }); - }, - (error) => { - notifications.showErrorDialog({ - title: i18n.translate( - 'xpack.streams.significantEvents.savedMultiple.errorToastTitle', - { defaultMessage: 'Could not save significant events queries' } - ), - error, - }); - } - ); - break; - } - }} - onClose={onCloseFlyout} - initialFlow={initialFlow} - /> - ) : null; -}; diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/add_significant_event_flyout/generated_flow_form/ai_features_disabled_callout.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/add_significant_event_flyout/generated_flow_form/ai_features_disabled_callout.tsx deleted file mode 100644 index 44d75db15e4db..0000000000000 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/add_significant_event_flyout/generated_flow_form/ai_features_disabled_callout.tsx +++ /dev/null @@ -1,58 +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 { EuiFlexGroup, EuiFlexItem, EuiLink, EuiText, EuiToolTip } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React from 'react'; -import { useKibana } from '../../../../../hooks/use_kibana'; - -export function AIFeaturesDisabledCallout({ couldBeEnabled }: { couldBeEnabled: boolean }) { - const { - core: { http }, - } = useKibana(); - - return ( - - - - {couldBeEnabled ? ( - - {i18n.translate( - 'xpack.streams.addSignificantEventFlyout.aiFlow.aiAssistantNotEnabled', - { - defaultMessage: 'Enable AI Assistant features', - } - )} - - ) : ( - -

- {i18n.translate( - 'xpack.streams.addSignificantEventFlyout.aiFlow.aiAssistantNotEnabledAskAdmin', - { defaultMessage: 'Ask your administrator to enable AI Assistant features' } - )} -

-
- )} -
-
-
- ); -} diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/add_significant_event_flyout/generated_flow_form/empty_state.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/add_significant_event_flyout/generated_flow_form/empty_state.tsx deleted file mode 100644 index e601c2b43125a..0000000000000 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/add_significant_event_flyout/generated_flow_form/empty_state.tsx +++ /dev/null @@ -1,38 +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 { EuiFlexGroup, EuiSpacer, EuiTitle, EuiText } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React from 'react'; -import { AssetImage } from '../../../../asset_image'; - -export function AiFlowEmptyState() { - return ( - - - - -

- {i18n.translate('xpack.streams.addSignificantEventFlyout.aiFlow.emptyState.title', { - defaultMessage: 'Your preview will appear here', - })} -

-
- - {i18n.translate('xpack.streams.addSignificantEventFlyout.aiFlow.emptyState.description', { - defaultMessage: - 'You can generate events with AI, by giving context through features selection. Manual entry is also available.', - })} - -
- ); -} diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/add_significant_event_flyout/generated_flow_form/generated_event_preview.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/add_significant_event_flyout/generated_flow_form/generated_event_preview.tsx deleted file mode 100644 index f7aab71e0f467..0000000000000 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/add_significant_event_flyout/generated_flow_form/generated_event_preview.tsx +++ /dev/null @@ -1,238 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import type { StreamQuery, Streams } from '@kbn/streams-schema'; -import React, { useMemo, useState } from 'react'; -import { - EuiButton, - EuiButtonEmpty, - EuiFieldText, - EuiFlexGroup, - EuiFlexItem, - EuiForm, - EuiFormLabel, - EuiFormRow, - EuiHorizontalRule, - EuiSpacer, - EuiTextArea, - useEuiTheme, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { css } from '@emotion/css'; -import { PreviewDataSparkPlot } from '../common/preview_data_spark_plot'; -import { StreamsESQLEditor, validatePrefix } from '../../../../esql_query_editor'; -import { validateQuery } from '../common/validate_query'; -import { SeveritySelector } from '../common/severity_selector'; -import { getValidPrefixes } from '../common/get_valid_prefixes'; - -interface GeneratedEventPreviewProps { - definition: Streams.all.Definition; - query: StreamQuery; - onSave: (query: StreamQuery) => void; - isEditing: boolean; - setIsEditing: (isEditing: boolean) => void; -} - -export function GeneratedEventPreview({ - definition, - query: initialQuery, - isEditing, - setIsEditing, - onSave, -}: GeneratedEventPreviewProps) { - const { euiTheme } = useEuiTheme(); - - const [query, setQuery] = useState(initialQuery); - - const [touched, setTouched] = useState({ title: false, esql: false }); - const validation = validateQuery(query); - const prefix = useMemo(() => getValidPrefixes(definition), [definition]); - const prefixValidation = useMemo( - () => validatePrefix(query.esql.query, prefix), - [query.esql.query, prefix] - ); - - return ( -
- - - {i18n.translate( - 'xpack.streams.addSignificantEventFlyout.generatedEventPreview.formFieldTitleLabel', - { defaultMessage: 'Title' } - )} - - } - labelAppend={ - isEditing ? ( - <> - - - { - setIsEditing(false); - setQuery(initialQuery); - setTouched({ - title: false, - esql: false, - }); - }} - data-test-subj="significant_events_generated_event_cancel_button" - > - {i18n.translate( - 'xpack.streams.addSignificantEventFlyout.generatedEventPreview.cancelButtonLabel', - { defaultMessage: 'Cancel' } - )} - - - - { - setIsEditing(false); - onSave(query); - setTouched({ - title: false, - esql: false, - }); - }} - data-test-subj="significant_events_generated_event_save_button" - > - {i18n.translate( - 'xpack.streams.addSignificantEventFlyout.generatedEventPreview.saveButtonLabel', - { defaultMessage: 'Save' } - )} - - - - - ) : ( - { - setIsEditing(true); - }} - data-test-subj="significant_events_generated_event_edit_button" - > - {i18n.translate( - 'xpack.streams.addSignificantEventFlyout.generatedEventPreview.editButtonLabel', - { defaultMessage: 'Edit' } - )} - - ) - } - {...(touched.title && { ...validation.title })} - > - <> - - { - setQuery({ ...query, title: event.currentTarget.value }); - setTouched((prev) => ({ ...prev, title: true })); - }} - onBlur={() => { - setTouched((prev) => ({ ...prev, title: true })); - }} - /> - - - - - {i18n.translate( - 'xpack.streams.addSignificantEventFlyout.generatedEventPreview.formFieldDescriptionLabel', - { defaultMessage: 'Description' } - )} - - } - > - { - setQuery({ ...query, description: event.currentTarget.value }); - }} - placeholder={i18n.translate( - 'xpack.streams.addSignificantEventFlyout.generatedEventPreview.descriptionPlaceholder', - { - defaultMessage: 'Describe what this query detects and why it matters', - } - )} - /> - - - - {i18n.translate( - 'xpack.streams.addSignificantEventFlyout.generatedEventPreview.formFieldSeverityLabel', - { defaultMessage: 'Severity' } - )} - - } - > - { - setQuery({ ...query, severity_score: score }); - setTouched((prev) => ({ ...prev, severity: true })); - }} - /> - - - { - setTouched((prev) => ({ ...prev, esql: true })); - setQuery({ ...query, esql: { query: newQuery.esql } }); - }} - onTextLangQuerySubmit={async (newQuery) => { - setTouched((prev) => ({ ...prev, esql: true })); - setQuery({ ...query, esql: { query: newQuery?.esql ?? '' } }); - }} - prefix={prefix} - /> - - - - - -
- ); -} diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/add_significant_event_flyout/generated_flow_form/generated_flow_form.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/add_significant_event_flyout/generated_flow_form/generated_flow_form.tsx deleted file mode 100644 index 26cc61be14e7a..0000000000000 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/add_significant_event_flyout/generated_flow_form/generated_flow_form.tsx +++ /dev/null @@ -1,124 +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 { EuiCallOut } from '@elastic/eui'; -import type { StreamQuery } from '@kbn/streams-schema'; -import type { Streams } from '@kbn/streams-schema'; -import React, { useEffect, useState } from 'react'; -import type { DataView } from '@kbn/data-views-plugin/public'; -import { i18n } from '@kbn/i18n'; -import { SignificantEventsGeneratedTable } from './significant_events_generated_table'; -import { AiFlowEmptyState } from './empty_state'; -import { AiFlowWaitingForGeneration } from './waiting_for_generation'; - -interface Props { - isGenerating: boolean; - isBeingCanceled: boolean; - isSchedulingGenerationTask: boolean; - generatedQueries: StreamQuery[]; - onEditQuery: (query: StreamQuery) => void; - stopGeneration: () => void; - definition: Streams.all.Definition; - isSubmitting: boolean; - setQueries: (queries: StreamQuery[]) => void; - setCanSave: (canSave: boolean) => void; - dataViews: DataView[]; - taskStatus?: string; - taskError?: string; -} - -export function GeneratedFlowForm({ - isGenerating, - isBeingCanceled, - isSchedulingGenerationTask, - generatedQueries, - onEditQuery, - stopGeneration, - setQueries, - definition, - setCanSave, - isSubmitting, - dataViews, - taskStatus, - taskError, -}: Props) { - const [selectedQueries, setSelectedQueries] = useState([]); - const [isEditingQueries, setIsEditingQueries] = useState(false); - - const onSelectionChange = (selectedItems: StreamQuery[]) => { - setSelectedQueries(selectedItems); - setQueries(selectedItems); - }; - - useEffect(() => { - setCanSave(!isEditingQueries && selectedQueries.length > 0); - }, [selectedQueries, isEditingQueries, setCanSave]); - - if (!isGenerating && (taskStatus === 'failed' || taskStatus === 'stale')) { - const isFailed = taskStatus === 'failed'; - return ( - - {isFailed - ? taskError - : i18n.translate( - 'xpack.streams.streamDetailView.addSignificantEventFlyout.generationStaleDescription', - { defaultMessage: 'The generation task took too long and was marked as stale.' } - )} - - ); - } - - if (generatedQueries.length === 0) { - return isGenerating ? ( - - ) : ( - - ); - } - - return ( - <> - - {isGenerating && ( - - )} - - ); -} diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/add_significant_event_flyout/generated_flow_form/significant_events_generated_table.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/add_significant_event_flyout/generated_flow_form/significant_events_generated_table.tsx deleted file mode 100644 index 655710b7cdc7c..0000000000000 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/add_significant_event_flyout/generated_flow_form/significant_events_generated_table.tsx +++ /dev/null @@ -1,215 +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 { - EuiBasicTable, - EuiButtonIcon, - EuiCodeBlock, - EuiScreenReaderOnly, - EuiText, - type EuiBasicTableColumn, - type EuiTableSelectionType, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import type { StreamQuery, Streams } from '@kbn/streams-schema'; -import React, { useCallback, useEffect, useState, type ReactNode } from 'react'; -import type { DataView } from '@kbn/data-views-plugin/public'; -import { PreviewDataSparkPlot } from '../common/preview_data_spark_plot'; -import { validateEsqlQuery } from '../common/validate_query'; -import { GeneratedEventPreview } from './generated_event_preview'; -import { SeverityBadge } from '../../../significant_events_discovery/components/severity_badge/severity_badge'; - -interface Props { - definition: Streams.all.Definition; - generatedQueries: StreamQuery[]; - setIsEditingQueries: (isEditingQueries: boolean) => void; - onEditQuery: (query: StreamQuery) => void; - selectedQueries: StreamQuery[]; - isSubmitting: boolean; - onSelectionChange: (selectedItems: StreamQuery[]) => void; - dataViews: DataView[]; -} - -export function SignificantEventsGeneratedTable({ - generatedQueries, - onEditQuery, - setIsEditingQueries, - selectedQueries, - onSelectionChange, - definition, - isSubmitting, - dataViews, -}: Props) { - const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState>( - {} - ); - const [eventsInEditMode, setEventsInEditMode] = useState([]); - - const setIsEditing = useCallback( - (isEditing: boolean, query: StreamQuery) => { - const nextEventsInEditMode = isEditing - ? [...eventsInEditMode, query.id] - : eventsInEditMode.filter((id) => id !== query.id); - setEventsInEditMode(nextEventsInEditMode); - setIsEditingQueries(nextEventsInEditMode.length > 0); - }, - [eventsInEditMode, setIsEditingQueries] - ); - - const toggleDetails = (query: StreamQuery) => { - const itemIdToExpandedRowMapValues = { ...itemIdToExpandedRowMap }; - - if (itemIdToExpandedRowMapValues[query.id]) { - delete itemIdToExpandedRowMapValues[query.id]; - } else { - itemIdToExpandedRowMapValues[query.id] = ( - setIsEditing(nextIsEditing, query)} - onSave={onEditQuery} - /> - ); - } - - setItemIdToExpandedRowMap(itemIdToExpandedRowMapValues); - }; - - useEffect(() => { - const copy = { ...itemIdToExpandedRowMap }; - for (const queryId of Object.keys(copy)) { - const query = generatedQueries.find((q) => q.id === queryId)!; - copy[queryId] = ( - setIsEditing(nextIsEditing, query)} - onSave={onEditQuery} - /> - ); - } - setItemIdToExpandedRowMap(copy); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [eventsInEditMode, definition, onEditQuery, dataViews, generatedQueries]); - - const columns: Array> = [ - { - align: 'right', - width: '40px', - isExpander: true, - name: ( - - - {i18n.translate('xpack.streams.addSignificantEventFlyout.aiFlow.previewToggleColumn', { - defaultMessage: 'Preview toggle', - })} - - - ), - mobileOptions: { header: false }, - render: (query: StreamQuery) => { - const itemIdToExpandedRowMapValues = { ...itemIdToExpandedRowMap }; - - return ( - toggleDetails(query)} - aria-label={itemIdToExpandedRowMapValues[query.id] ? 'Collapse' : 'Expand'} - iconType={ - itemIdToExpandedRowMapValues[query.id] ? 'chevronSingleDown' : 'chevronSingleRight' - } - /> - ); - }, - }, - { - field: 'title', - width: '30%', - name: i18n.translate('xpack.streams.addSignificantEventFlyout.aiFlow.titleColumn', { - defaultMessage: 'Title', - }), - render: (title: StreamQuery['title']) => {title}, - }, - { - width: '35%', - field: 'esql', - name: i18n.translate('xpack.streams.addSignificantEventFlyout.aiFlow.queryColumn', { - defaultMessage: 'Query', - }), - render: (esql: StreamQuery['esql']) => { - return ( - - {esql.query} - - ); - }, - }, - { - width: '10%', - field: 'severity_score', - name: i18n.translate('xpack.streams.addSignificantEventFlyout.aiFlow.severityScoreColumn', { - defaultMessage: 'Severity', - }), - render: (score: StreamQuery['severity_score']) => { - return ; - }, - }, - { - name: ( - - - {i18n.translate('xpack.streams.addSignificantEventFlyout.aiFlow.dataColumn', { - defaultMessage: 'Data preview', - })} - - - ), - width: '25%', - render: (query: StreamQuery) => { - const validation = validateEsqlQuery(query.esql.query); - return ( - - ); - }, - }, - ]; - - const selection: EuiTableSelectionType = { - onSelectionChange, - selected: selectedQueries, - selectable: () => !isSubmitting, - }; - - return ( - - ); -} diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/add_significant_event_flyout/generated_flow_form/waiting_for_generation.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/add_significant_event_flyout/generated_flow_form/waiting_for_generation.tsx deleted file mode 100644 index 75b73835a636b..0000000000000 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/add_significant_event_flyout/generated_flow_form/waiting_for_generation.tsx +++ /dev/null @@ -1,59 +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 { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiLoadingElastic } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React from 'react'; -import { useWaitingForAiMessage } from '../../../../../hooks/sig_events/use_waiting_for_ai_message'; - -export function AiFlowWaitingForGeneration({ - stopGeneration, - hasInitialResults = false, - isBeingCanceled = false, - isSchedulingGenerationTask = false, -}: { - stopGeneration: () => void; - hasInitialResults?: boolean; - isBeingCanceled?: boolean; - isSchedulingGenerationTask?: boolean; -}) { - const label = useWaitingForAiMessage(hasInitialResults); - - return ( - - - - - - {isBeingCanceled - ? i18n.translate('xpack.streams.aiFlowWaitingForGeneration.cancelingGenerationLabel', { - defaultMessage: 'Canceling generation...', - }) - : label} - - {!isBeingCanceled && !isSchedulingGenerationTask && ( - - - {i18n.translate( - 'xpack.streams.aiFlowWaitingForGeneration.button.stopGenerationButtonLabel', - { defaultMessage: 'Stop' } - )} - - - )} - - ); -} diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/add_significant_event_flyout/manual_flow_form/i18n.ts b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/add_significant_event_flyout/manual_flow_form/i18n.ts deleted file mode 100644 index a0c2dcbd3beb4..0000000000000 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/add_significant_event_flyout/manual_flow_form/i18n.ts +++ /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 { i18n } from '@kbn/i18n'; -import type { StreamQuery } from '@kbn/streams-schema'; - -export function getSigEventFlyoutTitle(query?: StreamQuery): string { - if (!query) { - return i18n.translate('xpack.streams.significantEventFlyout.addNewQueryFlyoutTitle', { - defaultMessage: 'Add significant event', - }); - } - - return i18n.translate('xpack.streams.significantEventFlyout.editQueryFlyoutTitle', { - defaultMessage: 'Edit {title}', - values: { - title: query.title, - }, - }); -} - -export function getSigEventSubmitTitle(query?: StreamQuery): string { - if (!query) { - return i18n.translate('xpack.streams.significantEventFlyout.addButtonLabel', { - defaultMessage: 'Add', - }); - } - - return i18n.translate('xpack.streams.significantEventFlyout.editButtonLabel', { - defaultMessage: 'Save changes', - }); -} diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/add_significant_event_flyout/manual_flow_form/manual_flow_form.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/add_significant_event_flyout/manual_flow_form/manual_flow_form.tsx deleted file mode 100644 index db4e105b3e7e1..0000000000000 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/add_significant_event_flyout/manual_flow_form/manual_flow_form.tsx +++ /dev/null @@ -1,218 +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 { - EuiFieldText, - EuiFlexGroup, - EuiForm, - EuiFormLabel, - EuiFormRow, - EuiPanel, - EuiTextArea, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import type { StreamQuery, Streams } from '@kbn/streams-schema'; -import { useDebouncedValue } from '@kbn/react-hooks'; -import React, { useEffect, useMemo, useRef } from 'react'; -import { Controller, useForm } from 'react-hook-form'; -import { StreamsESQLEditor, validatePrefix } from '../../../../esql_query_editor'; -import { PreviewDataSparkPlot } from '../common/preview_data_spark_plot'; -import { SeveritySelector } from '../common/severity_selector'; -import { validateTitle, validateEsqlQuery } from '../common/validate_query'; -import { getValidPrefixes } from '../common/get_valid_prefixes'; - -interface ManualFlowFormProps { - definition: Streams.all.Definition; - query: StreamQuery; - isSubmitting: boolean; - setQuery: (query: StreamQuery) => void; - setCanSave: (canSave: boolean) => void; -} - -const DEBOUNCE_DELAY_MS = 500; - -export function ManualFlowForm({ - definition, - query, - setQuery, - setCanSave, - isSubmitting, -}: ManualFlowFormProps) { - // Captured once at mount so that re-renders with new query props don't shift the allowed prefixes. - const initialEsqlRef = useRef(query.esql.query); - const validPrefixes = useMemo( - () => getValidPrefixes(definition, initialEsqlRef.current), - [definition] - ); - - const defaultEsql = query.esql.query || validPrefixes.primary; - - const { - control, - watch, - formState: { isDirty, isValid }, - } = useForm>({ - defaultValues: { - esql: { query: defaultEsql }, - title: query.title, - description: query.description ?? '', - severity_score: query.severity_score, - }, - mode: 'onChange', - }); - - const { esql } = watch(); - - useEffect(() => { - const { unsubscribe } = watch((values) => setQuery({ ...query, ...values } as StreamQuery)); - return () => unsubscribe(); - }, [query, setQuery, watch]); - - useEffect(() => { - setCanSave(isValid && isDirty); - }, [isValid, isDirty, setCanSave]); - - const debouncedEsqlQuery = useDebouncedValue(esql.query, DEBOUNCE_DELAY_MS); - - const isPreviewQueryValid = useMemo(() => { - const syntaxCheck = validateEsqlQuery(debouncedEsqlQuery); - if (syntaxCheck.isInvalid) return false; - const prefixCheck = validatePrefix(debouncedEsqlQuery, validPrefixes); - return prefixCheck.isValid; - }, [debouncedEsqlQuery, validPrefixes]); - - return ( - - - - validateTitle(v).error ?? true }} - render={({ field, fieldState }) => { - const isInvalid = fieldState.isTouched && !!fieldState.error; - return ( - - {i18n.translate( - 'xpack.streams.addSignificantEventFlyout.manualFlow.formFieldTitleLabel', - { defaultMessage: 'Title' } - )} - - } - > - field.onChange(e.target.value)} - placeholder={i18n.translate( - 'xpack.streams.addSignificantEventFlyout.manualFlow.titlePlaceholder', - { defaultMessage: 'Add title' } - )} - /> - - ); - }} - /> - - ( - - {i18n.translate( - 'xpack.streams.addSignificantEventFlyout.manualFlow.formFieldDescriptionLabel', - { defaultMessage: 'Description' } - )} - - } - > - field.onChange(e.target.value)} - rows={2} - resize="vertical" - placeholder={i18n.translate( - 'xpack.streams.addSignificantEventFlyout.manualFlow.descriptionPlaceholder', - { defaultMessage: 'Describe what this query detects and why it matters' } - )} - /> - - )} - /> - - ( - - {i18n.translate( - 'xpack.streams.addSignificantEventFlyout.manualFlow.formFieldSeverityLabel', - { defaultMessage: 'Severity' } - )} - - } - > - field.onChange(score)} - /> - - )} - /> - - { - const syntaxError = validateEsqlQuery(value); - if (syntaxError.isInvalid) { - return syntaxError.error; - } - const prefixError = validatePrefix(value, validPrefixes); - if (!prefixError.isValid) { - return prefixError.error.message; - } - return true; - }, - }} - render={({ field }) => ( - { - if (newQuery) field.onChange(newQuery.esql); - }} - onTextLangQueryChange={(newQuery) => field.onChange(newQuery.esql)} - prefix={validPrefixes} - /> - )} - /> - - - - - - ); -} diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/add_significant_event_flyout/manual_flow_form/use_significant_event_preview_fetch.ts b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/add_significant_event_flyout/manual_flow_form/use_significant_event_preview_fetch.ts deleted file mode 100644 index 8f78cca55d5f8..0000000000000 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/add_significant_event_flyout/manual_flow_form/use_significant_event_preview_fetch.ts +++ /dev/null @@ -1,79 +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 { calculateAuto } from '@kbn/calculate-auto'; -import type { AbsoluteTimeRange } from '@kbn/es-query'; -import type { AbortableAsyncState } from '@kbn/react-hooks'; -import type { SignificantEventsPreviewResponse } from '@kbn/streams-schema'; -import moment from 'moment'; -import { useKibana } from '../../../../../hooks/use_kibana'; -import { useStreamsAppFetch } from '../../../../../hooks/use_streams_app_fetch'; - -export function useSignificantEventPreviewFetch({ - name, - esqlQuery, - timeRange, - isQueryValid, - noOfBuckets = 10, -}: { - noOfBuckets?: number; - name: string; - esqlQuery: string; - timeRange: AbsoluteTimeRange; - isQueryValid: boolean; -}): AbortableAsyncState> { - const { - dependencies: { - start: { streams }, - }, - } = useKibana(); - - const previewFetch = useStreamsAppFetch( - ({ signal }) => { - if (!isQueryValid) { - return Promise.resolve(undefined); - } - - const bucketSize = calculateAuto - .near(noOfBuckets, moment.duration(moment(timeRange.to).diff(timeRange.from))) - ?.asSeconds()!; - - return streams.streamsRepositoryClient.fetch( - `POST /api/streams/{name}/significant_events/_preview 2023-10-31`, - { - signal, - params: { - path: { - name, - }, - query: { - bucketSize: `${bucketSize}s`, - from: timeRange.from, - to: timeRange.to, - }, - body: { - query: { - esql: { query: esqlQuery }, - }, - }, - }, - } - ); - }, - [ - isQueryValid, - timeRange.from, - timeRange.to, - noOfBuckets, - streams.streamsRepositoryClient, - name, - esqlQuery, - ] - ); - - return previewFetch; -} diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/add_significant_event_flyout/manual_flow_form/use_spark_plot_data_from_sig_events.ts b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/add_significant_event_flyout/manual_flow_form/use_spark_plot_data_from_sig_events.ts deleted file mode 100644 index 46e27aa4944d0..0000000000000 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/add_significant_event_flyout/manual_flow_form/use_spark_plot_data_from_sig_events.ts +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { AbortableAsyncState } from '@kbn/react-hooks'; -import type { SignificantEventsPreviewResponse, StreamQuery } from '@kbn/streams-schema'; -import { useEuiTheme } from '@elastic/eui'; -import type { TickFormatter } from '@elastic/charts'; -import { useMemo } from 'react'; -import { formatChangePoint } from '../../utils/change_point'; -import { getAnnotationFromFormattedChangePoint } from '../../utils/get_annotation_from_formatted_change_point'; - -export function useSparkplotDataFromSigEvents({ - previewFetch, - query, - xFormatter, -}: { - previewFetch: AbortableAsyncState>; - query: StreamQuery; - xFormatter: TickFormatter; -}) { - const theme = useEuiTheme().euiTheme; - - return useMemo(() => { - const changePoints = previewFetch.value?.change_points; - const occurrences = previewFetch.value?.occurrences; - - const timeseries = - occurrences?.map(({ date, count }) => { - return { - x: new Date(date).getTime(), - y: count, - }; - }) ?? []; - - const change = - changePoints && occurrences - ? formatChangePoint({ - query, - change_points: changePoints, - occurrences: timeseries, - }) - : undefined; - - return { - timeseries, - annotations: change - ? [ - getAnnotationFromFormattedChangePoint({ - time: change.time, - changes: [change], - theme, - xFormatter, - }), - ] - : [], - }; - }, [previewFetch, xFormatter, query, theme]); -} diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/delete_table_items_modal/delete_table_items_modal.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/delete_table_items_modal/delete_table_items_modal.tsx new file mode 100644 index 0000000000000..dcefd5ff1105f --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/delete_table_items_modal/delete_table_items_modal.tsx @@ -0,0 +1,141 @@ +/* + * 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 { + EuiBadge, + EuiButton, + EuiButtonEmpty, + EuiCallOut, + EuiDescriptionList, + EuiFlexGroup, + EuiFlexItem, + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, + EuiSpacer, + EuiText, +} from '@elastic/eui'; +import { css } from '@emotion/react'; +import { i18n } from '@kbn/i18n'; +import type { KnowledgeIndicator } from '@kbn/streams-ai'; +import { upperFirst } from 'lodash'; +import React, { useMemo } from 'react'; +import { SeverityBadge } from '../../significant_events_discovery/components/severity_badge/severity_badge'; + +interface DeleteTableItemsModalProps { + title: string; + items: KnowledgeIndicator[]; + onConfirm: () => void; + onCancel: () => void; + isLoading?: boolean; +} + +export function DeleteTableItemsModal({ + title, + items, + onConfirm, + onCancel, + isLoading = false, +}: DeleteTableItemsModalProps) { + const listItems = useMemo( + () => + items.map((item) => { + const titleValue = + item.kind === 'feature' + ? item.feature.title ?? item.feature.id + : item.query.title ?? item.query.id; + + return { + title: titleValue, + description: ( + + {item.kind === 'feature' ? ( + + + {upperFirst(item.feature.type)} + + + ) : null} + {item.kind === 'query' ? ( + + + + ) : null} + + ), + }; + }), + [items] + ); + + return ( + + + {title} + + + {CONSEQUENCE_MESSAGE} + + + +
+ +
+
+ + + {CANCEL_BUTTON_LABEL} + + + {i18n.translate('xpack.streams.deleteTableItemsModal.confirmButtonLabel', { + defaultMessage: 'Delete', + })} + + +
+ ); +} + +const MODAL_ARIA_LABEL = i18n.translate( + 'xpack.streams.deleteTableItemsModal.euiModal.deleteTableItemsModalLabel', + { + defaultMessage: 'Delete items modal', + } +); + +const CONSEQUENCE_MESSAGE = i18n.translate( + 'xpack.streams.deleteTableItemsModal.consequenceMessage', + { + defaultMessage: 'This will permanently delete the selected items.', + } +); + +const WARNING_MESSAGE = i18n.translate('xpack.streams.deleteTableItemsModal.warningMessage', { + defaultMessage: 'This action cannot be undone.', +}); + +const TABLE_CONTENT_ARIA_LABEL = i18n.translate( + 'xpack.streams.deleteTableItemsModal.tableContentAriaLabel', + { + defaultMessage: 'List of items to delete', + } +); + +const CANCEL_BUTTON_LABEL = i18n.translate('xpack.streams.deleteTableItemsModal.cancelButton', { + defaultMessage: 'Cancel', +}); + +const MODAL_TABLE_CSS = css` + max-height: 300px; + overflow: auto; +`; + +const TYPE_BADGE_CSS = css` + text-transform: capitalize; +`; diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/add_significant_event_flyout/utils/default_query.ts b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/delete_table_items_modal/index.ts similarity index 52% rename from x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/add_significant_event_flyout/utils/default_query.ts rename to x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/delete_table_items_modal/index.ts index 15d6cfcbd1011..81b350e9807a3 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/add_significant_event_flyout/utils/default_query.ts +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/delete_table_items_modal/index.ts @@ -5,16 +5,4 @@ * 2.0. */ -import type { StreamQuery } from '@kbn/streams-schema'; -import { v4 } from 'uuid'; - -export function defaultQuery(): StreamQuery { - return { - id: v4(), - title: '', - description: '', - esql: { - query: '', - }, - }; -} +export { DeleteTableItemsModal } from './delete_table_items_modal'; diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/empty_state/index.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/empty_state/index.tsx index 22a31094d3275..706d7f00067e5 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/empty_state/index.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/empty_state/index.tsx @@ -5,49 +5,181 @@ * 2.0. */ -import { EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; +import { + EuiButton, + EuiButtonIcon, + EuiCallOut, + EuiEmptyPrompt, + EuiFlexGroup, + EuiFlexItem, + EuiImage, + EuiLink, + EuiText, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { GEN_AI_SETTINGS_DEFAULT_AI_CONNECTOR } from '@kbn/management-settings-ids'; import React from 'react'; -import { SignificantEventsGenerationPanel } from '../generation_panel'; +import { useKibana } from '../../../../hooks/use_kibana'; +import noSigEventsImage from './no_sig_events.svg'; + +const ML_MODEL_SETTINGS_PATH = '/ml/model_settings'; +const NO_DEFAULT_CONNECTOR = 'NO_DEFAULT_CONNECTOR'; export function EmptyState({ - onManualEntryClick, + isGenerating, + isCanceling, + isGenerateDisabled, + onCancelGenerationClick, onGenerateSuggestionsClick, }: { - onManualEntryClick: () => void; + isGenerating: boolean; + isCanceling: boolean; + isGenerateDisabled: boolean; + onCancelGenerationClick: () => void; onGenerateSuggestionsClick: () => void; }) { + const { core } = useKibana(); + const defaultConnector = core.uiSettings.get(GEN_AI_SETTINGS_DEFAULT_AI_CONNECTOR); + + const isDefaultAiConnectorMissing = + !defaultConnector || defaultConnector === NO_DEFAULT_CONNECTOR; + const genAiSettingsUrl = core.application.getUrlForApp('management', { + path: ML_MODEL_SETTINGS_PATH, + }); + return ( {i18n.translate('xpack.streams.significantEvents.emptyState.title', { - defaultMessage: 'Significant events', + defaultMessage: 'No Significant events yet', })} } + icon={ + + } body={ {i18n.translate('xpack.streams.significantEvents.emptyState.description', { defaultMessage: - "Single, 'interesting' log event identified by an automated rule as being important for understanding a system's behaviour.", + 'Significant events runs on generated content which we use for context to create meaningful insights. Enable it for this stream.', })} - - - - + {isDefaultAiConnectorMissing ? ( + + +

+ {NO_DEFAULT_CONNECTOR_CALLOUT_DESCRIPTION}{' '} + + {NO_DEFAULT_CONNECTOR_CALLOUT_LINK_LABEL} + +

+
+
+ ) : ( + + + {isGenerating ? ( + + + + ) : null} + + + {isGenerating + ? isCanceling + ? CANCELING_BUTTON_LABEL + : GENERATING_BUTTON_LABEL + : GENERATE_BUTTON_LABEL} + + + + + )}
} /> ); } + +const NO_SIGNIFICANT_EVENTS_IMAGE_ALT = i18n.translate( + 'xpack.streams.significantEvents.emptyState.imageAlt', + { + defaultMessage: 'No significant events illustration', + } +); + +const GENERATE_BUTTON_LABEL = i18n.translate( + 'xpack.streams.significantEvents.emptyState.generateButtonLabel', + { + defaultMessage: 'Generate', + } +); + +const GENERATING_BUTTON_LABEL = i18n.translate( + 'xpack.streams.significantEvents.emptyState.generatingButtonLabel', + { + defaultMessage: 'Generating', + } +); + +const CANCELING_BUTTON_LABEL = i18n.translate( + 'xpack.streams.significantEvents.emptyState.cancelingButtonLabel', + { + defaultMessage: 'Canceling', + } +); + +const CANCEL_GENERATION_BUTTON_ARIA_LABEL = i18n.translate( + 'xpack.streams.significantEvents.emptyState.cancelGenerationButtonAriaLabel', + { + defaultMessage: 'Cancel generation', + } +); + +const NO_DEFAULT_CONNECTOR_CALLOUT_TITLE = i18n.translate( + 'xpack.streams.significantEvents.emptyState.noDefaultConnectorCalloutTitle', + { + defaultMessage: 'No default connector configured', + } +); + +const NO_DEFAULT_CONNECTOR_CALLOUT_DESCRIPTION = i18n.translate( + 'xpack.streams.significantEvents.emptyState.noDefaultConnectorCalloutDescription', + { + defaultMessage: + 'Generating significant events requires a default AI connector. Open Model Settings to configure one.', + } +); + +const NO_DEFAULT_CONNECTOR_CALLOUT_LINK_LABEL = i18n.translate( + 'xpack.streams.significantEvents.emptyState.noDefaultConnectorCalloutLinkLabel', + { + defaultMessage: 'Open Model Settings', + } +); diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/empty_state/no_sig_events.svg b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/empty_state/no_sig_events.svg new file mode 100644 index 0000000000000..481b517fc4315 --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/empty_state/no_sig_events.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/generation_panel.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/generation_panel.tsx deleted file mode 100644 index 5d45885d40486..0000000000000 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/generation_panel.tsx +++ /dev/null @@ -1,144 +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 React from 'react'; -import { - EuiButton, - EuiFlexGroup, - EuiFlexItem, - EuiHorizontalRule, - EuiPanel, - EuiSpacer, - EuiText, - EuiTitle, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { AssetImage } from '../../asset_image'; -import type { Flow } from './add_significant_event_flyout/types'; - -export function SignificantEventsGenerationPanel({ - onGenerateSuggestionsClick, - onManualEntryClick, - isGeneratingQueries, - isSavingManualEntry, - selectedFlow, -}: { - onManualEntryClick: () => void; - onGenerateSuggestionsClick: () => void; - isGeneratingQueries: boolean; - isSavingManualEntry: boolean; - selectedFlow?: Flow; -}) { - return ( - - - - - - - - {i18n.translate( - 'xpack.streams.significantEvents.significantEventsGenerationPanel.generationContextTitle', - { - defaultMessage: 'Generation context', - } - )} - - - - - - - {i18n.translate( - 'xpack.streams.significantEvents.significantEventsGenerationPanel.description', - { - defaultMessage: - 'Use AI to generate significant events from the data in this stream. Generation uses the last 24 hours of data.', - } - )} - - - - - - - - - - - - - {i18n.translate( - 'xpack.streams.significantEvents.significantEventsGenerationPanel.generateSuggestionsButtonLabel', - { - defaultMessage: 'Generate suggestions', - } - )} - - - - - - - - - - - - - - {i18n.translate( - 'xpack.streams.significantEvents.significantEventsGenerationPanel.orStartWithLabel', - { - defaultMessage: 'or create significant events with', - } - )} - - - - - - - - - - - - - - {i18n.translate( - 'xpack.streams.significantEvents.significantEventsGenerationPanel.manualEntryButtonLabel', - { - defaultMessage: 'Manual entry', - } - )} - - - - - - ); -} diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/hooks/use_knowledge_indicators_bulk_delete.ts b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/hooks/use_knowledge_indicators_bulk_delete.ts new file mode 100644 index 0000000000000..2385d1bce7118 --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/hooks/use_knowledge_indicators_bulk_delete.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 type { Streams } from '@kbn/streams-schema'; +import type { KnowledgeIndicator } from '@kbn/streams-ai'; +import { i18n } from '@kbn/i18n'; +import { useMutation, useQueryClient } from '@kbn/react-query'; +import { DISCOVERY_QUERIES_QUERY_KEY } from '../../../../hooks/sig_events/use_fetch_discovery_queries'; +import { useKibana } from '../../../../hooks/use_kibana'; +import { useQueriesApi } from '../../../../hooks/sig_events/use_queries_api'; +import { useStreamFeaturesApi } from '../../../../hooks/sig_events/use_stream_features_api'; + +interface UseKnowledgeIndicatorsBulkDeleteParams { + definition: Streams.all.Definition; + onSuccess?: () => void; +} + +export function useKnowledgeIndicatorsBulkDelete({ + definition, + onSuccess, +}: UseKnowledgeIndicatorsBulkDeleteParams) { + const { + core: { + notifications: { toasts }, + }, + } = useKibana(); + const queryClient = useQueryClient(); + const { deleteFeaturesInBulk } = useStreamFeaturesApi(definition); + const { deleteQueriesInBulk } = useQueriesApi(); + + const mutation = useMutation({ + mutationFn: async (knowledgeIndicators) => { + const featureUuids = knowledgeIndicators + .filter((knowledgeIndicator) => knowledgeIndicator.kind === 'feature') + .map((knowledgeIndicator) => knowledgeIndicator.feature.uuid); + + const queryIds = knowledgeIndicators + .filter((knowledgeIndicator) => knowledgeIndicator.kind === 'query') + .map((knowledgeIndicator) => knowledgeIndicator.query.id); + + const requests: Array> = []; + + if (featureUuids.length > 0) { + requests.push(deleteFeaturesInBulk(featureUuids)); + } + + if (queryIds.length > 0) { + requests.push(deleteQueriesInBulk({ queryIds, streamName: definition.name })); + } + + await Promise.all(requests); + }, + onSuccess: async () => { + onSuccess?.(); + + toasts.addSuccess({ + title: BULK_DELETE_SUCCESS_TOAST_TITLE, + }); + }, + onError: (error) => { + toasts.addError(error, { + title: BULK_DELETE_ERROR_TOAST_TITLE, + }); + }, + onSettled: async () => { + await Promise.all([ + queryClient.invalidateQueries({ queryKey: DISCOVERY_QUERIES_QUERY_KEY }), + queryClient.invalidateQueries({ queryKey: ['features', definition.name] }), + ]); + }, + }); + + return { + deleteKnowledgeIndicatorsInBulk: async (knowledgeIndicators: KnowledgeIndicator[]) => { + if (knowledgeIndicators.length === 0) { + return; + } + + await mutation.mutateAsync(knowledgeIndicators); + }, + isDeleting: mutation.isLoading, + }; +} + +const BULK_DELETE_ERROR_TOAST_TITLE = i18n.translate( + 'xpack.streams.significantEventsTable.bulkDeleteErrorToastTitle', + { + defaultMessage: 'Failed to delete selected knowledge indicators', + } +); + +const BULK_DELETE_SUCCESS_TOAST_TITLE = i18n.translate( + 'xpack.streams.significantEventsTable.bulkDeleteSuccessToastTitle', + { + defaultMessage: 'Knowledge indicators deleted', + } +); diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/hooks/use_knowledge_indicators_data.ts b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/hooks/use_knowledge_indicators_data.ts new file mode 100644 index 0000000000000..8332a18c3c7ea --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/hooks/use_knowledge_indicators_data.ts @@ -0,0 +1,88 @@ +/* + * 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 { Streams } from '@kbn/streams-schema'; +import type { KnowledgeIndicator } from '@kbn/streams-ai'; +import { useCallback, useMemo } from 'react'; +import { useFetchDiscoveryQueries } from '../../../../hooks/sig_events/use_fetch_discovery_queries'; +import { useStreamFeatures } from '../../../../hooks/sig_events/use_stream_features'; + +interface UseKnowledgeIndicatorsDataParams { + definition: Streams.all.GetResponse; +} + +export function useFetchKnowledgeIndicators( + { definition }: UseKnowledgeIndicatorsDataParams, + deps: unknown[] = [] +) { + const queriesFetchState = useFetchDiscoveryQueries( + { + name: definition.stream.name, + query: '', + page: 1, + perPage: 1000, + status: ['active', 'draft'], + }, + deps + ); + + const { features, excludedFeatures, featuresLoading, refreshFeatures } = useStreamFeatures( + definition.stream, + deps + ); + + const knowledgeIndicators = useMemo(() => { + const queryKnowledgeIndicators = (queriesFetchState.data?.queries ?? []).map((queryRow) => ({ + kind: 'query' as const, + query: queryRow.query, + rule: { + backed: queryRow.rule_backed, + id: queryRow.query.id, + }, + stream_name: queryRow.stream_name, + })); + + return [ + ...features.map((feature) => ({ kind: 'feature' as const, feature })), + ...excludedFeatures.map((feature) => ({ kind: 'feature' as const, feature })), + ...queryKnowledgeIndicators, + ]; + }, [excludedFeatures, features, queriesFetchState.data?.queries]); + + const isLoading = queriesFetchState.isLoading || featuresLoading; + + const isEmpty = + !queriesFetchState.isLoading && + !featuresLoading && + features.length === 0 && + excludedFeatures.length === 0 && + (queriesFetchState.data?.queries.length ?? 0) === 0; + + const occurrencesByQueryId = useMemo( + () => + Object.fromEntries( + (queriesFetchState.data?.queries ?? []).map((queryRow) => [ + queryRow.query.id, + queryRow.occurrences, + ]) + ), + [queriesFetchState.data?.queries] + ); + + const refetch = useCallback(() => { + queriesFetchState.refetch(); + refreshFeatures(); + }, [queriesFetchState, refreshFeatures]); + + return { + knowledgeIndicators, + occurrencesByQueryId, + isLoading, + isEmpty, + refetch, + }; +} diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/hooks/use_knowledge_indicators_task.ts b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/hooks/use_knowledge_indicators_task.ts new file mode 100644 index 0000000000000..243a2b314d89d --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/hooks/use_knowledge_indicators_task.ts @@ -0,0 +1,155 @@ +/* + * 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 { useMutation, useQuery } from '@kbn/react-query'; +import { i18n } from '@kbn/i18n'; +import { useCallback, useEffect, useRef, useState } from 'react'; +import { TaskStatus, type OnboardingResult, type TaskResult } from '@kbn/streams-schema'; +import { useOnboardingApi } from '../../../../hooks/use_onboarding_api'; +import { getFormattedError } from '../../../../util/errors'; +import { useKibana } from '../../../../hooks/use_kibana'; + +interface Props { + streamName: string; + onComplete: ( + completedTaskState: Extract, { status: TaskStatus.Completed }> + ) => void; + onError: ( + failedTaskState: Extract, { status: TaskStatus.Failed }> + ) => void; +} + +export function useKnowledgeIndicatorsTask({ streamName, onComplete, onError }: Props) { + const previousTaskStatusRef = useRef(null); + const [knowledgeIndicatorsTaskState, setKnowledgeIndicatorsTaskState] = + useState | null>(null); + + const { + core: { + notifications: { toasts }, + }, + } = useKibana(); + const { getOnboardingTaskStatus, scheduleOnboardingTask, cancelOnboardingTask } = + useOnboardingApi({ saveQueries: true }); + + const scheduleTaskMutation = useMutation({ + mutationFn: scheduleOnboardingTask, + onError: (error: Error) => { + toasts.addError(getFormattedError(error), { + title: KNOWLEDGE_INDICATORS_TASK_SCHEDULING_FAILURE_TITLE, + }); + }, + }); + + const cancelTaskMutation = useMutation({ + mutationFn: cancelOnboardingTask, + onError: (error: Error) => { + toasts.addError(getFormattedError(error), { + title: INSIGHTS_DISCOVERY_CANCELLATION_FAILURE_TITLE, + }); + }, + }); + + useEffect(() => { + getOnboardingTaskStatus(streamName) + .then((taskState) => { + setKnowledgeIndicatorsTaskState(taskState); + previousTaskStatusRef.current = taskState.status; + }) + .catch(() => { + setKnowledgeIndicatorsTaskState({ status: TaskStatus.NotStarted }); + previousTaskStatusRef.current = TaskStatus.NotStarted; + }); + /** + * Explicitly running this hook only once to get the initial + * task state + */ + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const scheduleKnowledgeIndicatorsTask = useCallback(() => { + setKnowledgeIndicatorsTaskState({ + status: TaskStatus.InProgress, + }); + + scheduleTaskMutation.mutate(streamName); + }, [scheduleTaskMutation, streamName]); + + const cancelKnowledgeIndicatorsTask = useCallback(() => { + setKnowledgeIndicatorsTaskState({ + status: TaskStatus.BeingCanceled, + }); + + cancelTaskMutation.mutate(streamName); + }, [cancelTaskMutation, streamName]); + + const isPending = + knowledgeIndicatorsTaskState !== null && + [TaskStatus.InProgress, TaskStatus.BeingCanceled].includes(knowledgeIndicatorsTaskState.status); + + const fetchStatus = async () => { + const taskState = await getOnboardingTaskStatus(streamName); + + setKnowledgeIndicatorsTaskState(taskState); + + /** + * Firing an explicit callback when tasks **changes** to + * Completed state, so components can react by reloading + * task-related data. This handles the case when there was + * a successful task ran in the past and the task state + * reports Completed status, callback won't be fired in that + * case. + */ + if ( + previousTaskStatusRef.current !== null && + previousTaskStatusRef.current !== TaskStatus.Completed && + taskState.status === TaskStatus.Completed + ) { + onComplete(taskState); + } + + if ( + previousTaskStatusRef.current !== null && + previousTaskStatusRef.current !== TaskStatus.Failed && + taskState.status === TaskStatus.Failed + ) { + onError(taskState); + } + + previousTaskStatusRef.current = taskState.status; + + return taskState; + }; + + useQuery, Error>({ + queryKey: ['knowledgeIndicatorsTaskStatus', streamName], + queryFn: fetchStatus, + enabled: isPending && !scheduleTaskMutation.isLoading && !cancelTaskMutation.isLoading, + refetchInterval: 2000, + }); + + return { + isPending, + knowledgeIndicatorsTaskState, + scheduleKnowledgeIndicatorsTask, + cancelKnowledgeIndicatorsTask, + }; +} + +const KNOWLEDGE_INDICATORS_TASK_SCHEDULING_FAILURE_TITLE = i18n.translate( + 'xpack.streams.significantEventsDiscovery.knowledgeIndicatorsTaskSchedulingFailureTitle', + { + defaultMessage: 'Failed to schedule KIs generation', + } +); + +const INSIGHTS_DISCOVERY_CANCELLATION_FAILURE_TITLE = i18n.translate( + 'xpack.streams.significantEventsDiscovery.knowledgeIndicatorsTaskCancellationFailureTitle', + { + defaultMessage: 'Failed to cancel KIs generation', + } +); diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/hooks/use_queries_bulk_delete.ts b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/hooks/use_queries_bulk_delete.ts new file mode 100644 index 0000000000000..a5f4f9e82639c --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/hooks/use_queries_bulk_delete.ts @@ -0,0 +1,73 @@ +/* + * 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 { Streams } from '@kbn/streams-schema'; +import { i18n } from '@kbn/i18n'; +import { useMutation, useQueryClient } from '@kbn/react-query'; +import { DISCOVERY_QUERIES_QUERY_KEY } from '../../../../hooks/sig_events/use_fetch_discovery_queries'; +import { useKibana } from '../../../../hooks/use_kibana'; +import { useQueriesApi } from '../../../../hooks/sig_events/use_queries_api'; + +interface UseQueriesBulkDeleteParams { + definition: Streams.all.Definition; + onSuccess?: () => void; +} + +export function useQueriesBulkDelete({ definition, onSuccess }: UseQueriesBulkDeleteParams) { + const { + core: { + notifications: { toasts }, + }, + } = useKibana(); + const queryClient = useQueryClient(); + const { deleteQueriesInBulk } = useQueriesApi(); + + const mutation = useMutation({ + mutationFn: async (queryIds) => { + await deleteQueriesInBulk({ queryIds, streamName: definition.name }); + }, + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: DISCOVERY_QUERIES_QUERY_KEY }); + + onSuccess?.(); + + toasts.addSuccess({ + title: BULK_DELETE_SUCCESS_TOAST_TITLE, + }); + }, + onError: (error) => { + toasts.addError(error, { + title: BULK_DELETE_ERROR_TOAST_TITLE, + }); + }, + }); + + return { + deleteRulesInBulk: async (queryIds: string[]) => { + if (queryIds.length === 0) { + return; + } + + await mutation.mutateAsync(queryIds); + }, + isDeleting: mutation.isLoading, + }; +} + +const BULK_DELETE_ERROR_TOAST_TITLE = i18n.translate( + 'xpack.streams.rulesTable.bulkDeleteErrorToastTitle', + { + defaultMessage: 'Failed to delete selected rules', + } +); + +const BULK_DELETE_SUCCESS_TOAST_TITLE = i18n.translate( + 'xpack.streams.rulesTable.bulkDeleteSuccessToastTitle', + { + defaultMessage: 'Rules deleted', + } +); diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/index.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/index.tsx index 8be9a11a2e69d..9b978fdfdade0 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/index.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/index.tsx @@ -4,101 +4,169 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { niceTimeFormatter } from '@elastic/charts'; -import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiPanel, EuiText, useEuiTheme } from '@elastic/eui'; -import type { TimeRange } from '@kbn/es-query'; +import type { EuiSelectableOption } from '@elastic/eui'; +import { + EuiButton, + EuiButtonIcon, + EuiFieldSearch, + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiSpacer, +} from '@elastic/eui'; +import { css } from '@emotion/react'; import { i18n } from '@kbn/i18n'; -import type { StreamQuery, Streams } from '@kbn/streams-schema'; -import { compact, isEqual } from 'lodash'; -import React, { useMemo, useState } from 'react'; -import { useAIFeatures } from '../../../hooks/use_ai_features'; -import { useFetchSignificantEvents } from '../../../hooks/sig_events/use_fetch_significant_events'; +import { useDebouncedValue } from '@kbn/react-hooks'; +import { useQueryClient } from '@kbn/react-query'; +import { + TaskStatus, + type OnboardingResult, + type Streams, + type TaskResult, +} from '@kbn/streams-schema'; +import type { KnowledgeIndicator } from '@kbn/streams-ai'; +import React, { useCallback, useMemo, useState } from 'react'; +import useInterval from 'react-use/lib/useInterval'; +import { DISCOVERY_QUERIES_QUERY_KEY } from '../../../hooks/sig_events/use_fetch_discovery_queries'; import { useKibana } from '../../../hooks/use_kibana'; -import { useSignificantEventsApi } from '../../../hooks/sig_events/use_significant_events_api'; -import { useTimeRange } from '../../../hooks/use_time_range'; -import { useTimeRangeUpdate } from '../../../hooks/use_time_range_update'; -import { useTimefilter } from '../../../hooks/use_timefilter'; -import { LoadingPanel } from '../../loading_panel'; -import { EditSignificantEventFlyout } from './add_significant_event_flyout/edit_significant_event_flyout'; -import type { Flow } from './add_significant_event_flyout/types'; import { EmptyState } from './empty_state'; -import { SignificantEventsHistogramChart } from './significant_events_histogram'; -import { SignificantEventsTable } from './significant_events_table'; -import { formatChangePoint } from './utils/change_point'; +import { useFetchKnowledgeIndicators } from './hooks/use_knowledge_indicators_data'; +import { KnowledgeIndicatorsTable } from './knowledge_indicators_table'; +import { KnowledgeIndicatorDetailsFlyout } from './knowledge_indicator_details_flyout'; +import { useKnowledgeIndicatorsTask } from './hooks/use_knowledge_indicators_task'; +import { KnowledgeIndicatorRulesSelector } from './knowledge_indicator_rules_selector'; +import { KnowledgeIndicatorsStatusFilter } from './knowledge_indicators_status_filter'; +import { KnowledgeIndicatorsTypeFilter } from './knowledge_indicators_type_filter'; +import { RulesTable } from './rules_table'; +import { LoadingPanel } from '../../loading_panel'; + +const SEARCH_DEBOUNCE_MS = 300; interface Props { definition: Streams.all.GetResponse; } export function StreamDetailSignificantEventsView({ definition }: Props) { - const { rangeFrom, rangeTo, startMs, endMs } = useTimeRange(); - const { updateTimeRange } = useTimeRangeUpdate(); - const { refresh } = useTimefilter(); const { - dependencies: { - start: { unifiedSearch }, + core: { + notifications: { toasts }, }, } = useKibana(); - const { euiTheme } = useEuiTheme(); - const aiFeatures = useAIFeatures(); + const queryClient = useQueryClient(); + const [tableSearchValue, setTableSearchValue] = useState(''); + const debouncedTableSearchValue = useDebouncedValue(tableSearchValue, SEARCH_DEBOUNCE_MS); + const [knowledgeIndicatorStatusFilter, setKnowledgeIndicatorStatusFilter] = useState< + 'active' | 'excluded' + >('active'); + const [selectedKnowledgeIndicator, setSelectedKnowledgeIndicator] = + useState(null); + const [selectedKnowledgeIndicatorTypes, setSelectedKnowledgeIndicatorTypes] = useState( + [] + ); + const [typeFilterOptions, setTypeFilterOptions] = useState([ + { + key: 'knowledge_indicator', + checked: 'on', + label: KNOWLEDGE_INDICATORS_FILTER_LABEL, + }, + { + key: 'rule', + label: RULES_FILTER_LABEL, + }, + ]); + const { + knowledgeIndicators, + occurrencesByQueryId, + isLoading: isKnowledgeIndicatorsLoading, + isEmpty, + refetch, + } = useFetchKnowledgeIndicators({ definition }); + const onKnowledgeIndicatorsTaskComplete = useCallback( + ( + completedTaskState: Extract, { status: TaskStatus.Completed }> + ) => { + const queriesTaskResult = completedTaskState.queriesTaskResult; + const generatedKnowledgeIndicatorsCount = + queriesTaskResult?.status === TaskStatus.Completed ? queriesTaskResult.queries.length : 0; - const xFormatter = useMemo(() => { - return niceTimeFormatter([startMs, endMs]); - }, [startMs, endMs]); + toasts.addSuccess({ + title: i18n.translate( + 'xpack.streams.significantEventsTable.generateMoreSuccessToastTitle', + { + defaultMessage: + '{count, plural, one {Generated # knowledge indicator} other {Generated # knowledge indicators}}', + values: { + count: generatedKnowledgeIndicatorsCount, + }, + } + ), + }); - const [query, setQuery] = useState(''); - const significantEventsFetchState = useFetchSignificantEvents({ - name: definition.stream.name, - query, - }); + void Promise.all([ + queryClient.invalidateQueries({ queryKey: DISCOVERY_QUERIES_QUERY_KEY }), + queryClient.invalidateQueries({ queryKey: ['features', definition.stream.name] }), + ]); + }, + [definition.stream.name, queryClient, toasts] + ); + + const onKnowledgeIndicatorsTaskError = useCallback( + (failedTaskState: Extract, { status: TaskStatus.Failed }>) => { + toasts.addDanger({ + title: KNOWLEDGE_INDICATORS_TASK_FAILED_TOAST_TITLE, + text: failedTaskState.error, + }); + }, + [toasts] + ); - const { removeQuery } = useSignificantEventsApi({ name: definition.stream.name }); - const [isEditFlyoutOpen, setIsEditFlyoutOpen] = useState(false); - const [initialFlow, setInitialFlow] = useState('ai'); + const { + isPending: isKnowledgeIndicatorsGenerationPending, + knowledgeIndicatorsTaskState, + scheduleKnowledgeIndicatorsTask, + cancelKnowledgeIndicatorsTask, + } = useKnowledgeIndicatorsTask({ + streamName: definition.stream.name, + onComplete: onKnowledgeIndicatorsTaskComplete, + onError: onKnowledgeIndicatorsTaskError, + }); - const [queryToEdit, setQueryToEdit] = useState(); - const [dateRange, setDateRange] = useState({ from: rangeFrom, to: rangeTo }); + useInterval( + refetch, + knowledgeIndicatorsTaskState?.status === TaskStatus.InProgress ? 5000 : null + ); - if (!significantEventsFetchState.data && significantEventsFetchState.isLoading) { - return ; - } + const ruleKnowledgeIndicators = useMemo( + () => + knowledgeIndicators.filter( + (knowledgeIndicator) => + knowledgeIndicator.kind === 'query' && knowledgeIndicator.rule.backed + ), + [knowledgeIndicators] + ); - const editFlyout = (generateOnMount: boolean) => ( - + const isRulesSelected = useMemo( + () => typeFilterOptions.some((option) => option.key === 'rule' && option.checked === 'on'), + [typeFilterOptions] ); + const isKnowledgeIndicatorsGenerationCanceling = + knowledgeIndicatorsTaskState?.status === TaskStatus.BeingCanceled; + const isGenerateButtonDisabled = + knowledgeIndicatorsTaskState === null || isKnowledgeIndicatorsGenerationPending; - const noSignificantEvents = - !query && - !significantEventsFetchState.isLoading && - significantEventsFetchState.data && - significantEventsFetchState.data.significant_events.length === 0; + if (isKnowledgeIndicatorsLoading) { + return ; + } - if (noSignificantEvents) { + if (isEmpty) { return ( - <> - { - setQueryToEdit(undefined); - setInitialFlow('manual'); - setIsEditFlyoutOpen(true); - }} - onGenerateSuggestionsClick={() => { - setInitialFlow('ai'); - setIsEditFlyoutOpen(true); - }} - /> - {editFlyout(true)} - + ); } @@ -106,110 +174,200 @@ export function StreamDetailSignificantEventsView({ definition }: Props) { <> - - - { - setQuery(String(queryN.query?.query ?? '')); - - if (isEqual(queryN.dateRange, dateRange)) { - refresh(); - } else if (queryN.dateRange) { - updateTimeRange(queryN.dateRange); - setDateRange(queryN.dateRange); - } - }} - query={{ - query, - language: 'text', - }} - isLoading={significantEventsFetchState.isLoading} + + + + + + + setTableSearchValue(event.target.value)} + placeholder={SIGNIFICANT_EVENTS_SEARCH_PLACEHOLDER} + aria-label={SIGNIFICANT_EVENTS_SEARCH_ARIA_LABEL} + /> + + {!isRulesSelected ? ( + + + + ) : null} + {!isRulesSelected ? ( + + + + ) : null} + {!isRulesSelected ? ( + + + + ) : null} + + + {isRulesSelected ? ( + - - - { - setIsEditFlyoutOpen(true); - setQueryToEdit(undefined); - }} - iconType="plus" - data-test-subj="significant_events_existing_queries_open_flyout_button" - > - {i18n.translate('xpack.streams.significantEvents.addSignificantEventButton', { - defaultMessage: 'Significant events', - })} - - - - - - - - - - {i18n.translate( - 'xpack.streams.addSignificantEventFlyout.manualFlow.previewChartDetectedOccurrences', - { - defaultMessage: 'Detected event occurrences ({count})', - values: { - count: ( - significantEventsFetchState.data?.aggregated_occurrences ?? [] - ).reduce((acc, point) => acc + point.y, 0), - }, - } - )} - - - - - - formatChangePoint({ - query: item.query, - change_points: item.change_points, - occurrences: item.occurrences, - }) - ) - )} - xFormatter={xFormatter} - compressed={false} + ) : ( + - - - + )} + + + + {selectedKnowledgeIndicator ? ( + setSelectedKnowledgeIndicator(null)} + /> + ) : null} + + ); +} +function KnowledgeIndicatorsGenerationControls({ + isGenerating, + isCanceling, + isGenerateDisabled, + onGenerateSuggestionsClick, + onCancelGenerationClick, +}: { + isGenerating: boolean; + isCanceling: boolean; + isGenerateDisabled: boolean; + onGenerateSuggestionsClick: () => void; + onCancelGenerationClick: () => void; +}) { + return ( + + {isGenerating ? ( - { - setIsEditFlyoutOpen(true); - setQueryToEdit({ ...item.query }); - }} - onDeleteClick={async (item) => { - await removeQuery?.(item.query.id).then(() => { - significantEventsFetchState.refetch(); - }); - }} - xFormatter={xFormatter} + - - {editFlyout(false)} - + ) : null} + + + {isGenerating + ? isCanceling + ? CANCELING_BUTTON_LABEL + : GENERATING_BUTTON_LABEL + : GENERATE_MORE_BUTTON_LABEL} + + + ); } + +const KNOWLEDGE_INDICATORS_FILTER_LABEL = i18n.translate( + 'xpack.streams.significantEventsTable.typeFilter.knowledgeIndicatorsLabel', + { + defaultMessage: 'Knowledge Indicators', + } +); + +const RULES_FILTER_LABEL = i18n.translate( + 'xpack.streams.significantEventsTable.typeFilter.rulesLabel', + { + defaultMessage: 'Rules', + } +); + +const SIGNIFICANT_EVENTS_SEARCH_PLACEHOLDER = i18n.translate( + 'xpack.streams.significantEventsTable.searchPlaceholder', + { + defaultMessage: 'Search significant events', + } +); + +const SIGNIFICANT_EVENTS_SEARCH_ARIA_LABEL = i18n.translate( + 'xpack.streams.significantEventsTable.searchAriaLabel', + { + defaultMessage: 'Search significant events', + } +); + +const GENERATE_MORE_BUTTON_LABEL = i18n.translate( + 'xpack.streams.significantEventsTable.generateMoreButtonLabel', + { + defaultMessage: 'Generate more', + } +); + +const GENERATING_BUTTON_LABEL = i18n.translate( + 'xpack.streams.significantEventsTable.generatingButtonLabel', + { + defaultMessage: 'Generating', + } +); + +const CANCELING_BUTTON_LABEL = i18n.translate( + 'xpack.streams.significantEventsTable.cancelingButtonLabel', + { + defaultMessage: 'Canceling', + } +); + +const CANCEL_GENERATION_BUTTON_ARIA_LABEL = i18n.translate( + 'xpack.streams.significantEventsTable.cancelGenerationButtonAriaLabel', + { + defaultMessage: 'Cancel generation', + } +); + +const KNOWLEDGE_INDICATORS_TASK_FAILED_TOAST_TITLE = i18n.translate( + 'xpack.streams.significantEventsTable.knowledgeIndicatorsTaskFailedToastTitle', + { + defaultMessage: 'Failed to generate knowledge indicators', + } +); diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/knowledge_indicator_actions_cell/index.ts b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/knowledge_indicator_actions_cell/index.ts new file mode 100644 index 0000000000000..bd6f71b93db7f --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/knowledge_indicator_actions_cell/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 { KnowledgeIndicatorActionsCell } from './knowledge_indicator_actions_cell'; diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/knowledge_indicator_actions_cell/knowledge_indicator_actions_cell.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/knowledge_indicator_actions_cell/knowledge_indicator_actions_cell.tsx new file mode 100644 index 0000000000000..8ed8f98c88fbe --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/knowledge_indicator_actions_cell/knowledge_indicator_actions_cell.tsx @@ -0,0 +1,296 @@ +/* + * 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 { EuiButtonIcon, EuiContextMenuItem, EuiContextMenuPanel, EuiPopover } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { useMutation, useQueryClient } from '@kbn/react-query'; +import type { Streams } from '@kbn/streams-schema'; +import type { KnowledgeIndicator } from '@kbn/streams-ai'; +import React, { useCallback, useMemo, useState } from 'react'; +import { DISCOVERY_QUERIES_QUERY_KEY } from '../../../../hooks/sig_events/use_fetch_discovery_queries'; +import { useKibana } from '../../../../hooks/use_kibana'; +import { useQueriesApi } from '../../../../hooks/sig_events/use_queries_api'; +import { useStreamFeaturesApi } from '../../../../hooks/sig_events/use_stream_features_api'; + +interface Props { + definition: Streams.all.Definition; + knowledgeIndicator: KnowledgeIndicator; + onDeleteRequest: (knowledgeIndicator: KnowledgeIndicator) => void; +} + +export function KnowledgeIndicatorActionsCell({ + definition, + knowledgeIndicator, + onDeleteRequest, +}: Props) { + const [isActionsMenuOpen, setIsActionsMenuOpen] = useState(false); + const [isActionInProgress, setIsActionInProgress] = useState(false); + + const { + core: { + notifications: { toasts }, + }, + } = useKibana(); + const queryClient = useQueryClient(); + const { excludeFeaturesInBulk, restoreFeaturesInBulk } = useStreamFeaturesApi(definition); + const { promote } = useQueriesApi(); + + const invalidateKnowledgeIndicatorsData = useCallback(async () => { + await Promise.all([ + queryClient.invalidateQueries({ queryKey: DISCOVERY_QUERIES_QUERY_KEY }), + queryClient.invalidateQueries({ queryKey: ['features', definition.name] }), + ]); + }, [definition.name, queryClient]); + + const excludeAction = useMutation({ + mutationFn: async (featureUuid) => { + await excludeFeaturesInBulk([featureUuid]); + }, + onSuccess: async () => { + await invalidateKnowledgeIndicatorsData(); + toasts.addSuccess({ title: KI_EXCLUDE_ACTION_SUCCESS_TOAST_TITLE }); + }, + onError: (error) => { + toasts.addError(error, { title: KI_EXCLUDE_ACTION_ERROR_TOAST_TITLE }); + }, + }); + + const restoreAction = useMutation({ + mutationFn: async (featureUuid) => { + await restoreFeaturesInBulk([featureUuid]); + }, + onSuccess: async () => { + await invalidateKnowledgeIndicatorsData(); + toasts.addSuccess({ title: KI_RESTORE_ACTION_SUCCESS_TOAST_TITLE }); + }, + onError: (error) => { + toasts.addError(error, { title: KI_RESTORE_ACTION_ERROR_TOAST_TITLE }); + }, + }); + + const promoteAction = useMutation({ + mutationFn: async (queryId) => { + await promote({ queryIds: [queryId] }); + }, + onSuccess: async () => { + await invalidateKnowledgeIndicatorsData(); + toasts.addSuccess({ title: KI_PROMOTE_ACTION_SUCCESS_TOAST_TITLE }); + }, + onError: (error) => { + toasts.addError(error, { title: KI_PROMOTE_ACTION_ERROR_TOAST_TITLE }); + }, + }); + + const withActionLoading = useCallback((run: () => void) => { + setIsActionInProgress(true); + setIsActionsMenuOpen(false); + run(); + }, []); + + const featureActionItems = useMemo( + () => + knowledgeIndicator.kind === 'feature' + ? [ + { + setIsActionsMenuOpen(false); + onDeleteRequest(knowledgeIndicator); + }} + > + {KI_ACTION_DELETE_LABEL} + , + knowledgeIndicator.feature.excluded_at ? ( + + withActionLoading(() => + restoreAction.mutate(knowledgeIndicator.feature.uuid, { + onSettled: () => { + setIsActionInProgress(false); + }, + }) + ) + } + > + {KI_ACTION_RESTORE_LABEL} + + ) : ( + + withActionLoading(() => + excludeAction.mutate(knowledgeIndicator.feature.uuid, { + onSettled: () => { + setIsActionInProgress(false); + }, + }) + ) + } + > + {KI_ACTION_EXCLUDE_LABEL} + + ), + ] + : [], + [ + excludeAction, + isActionInProgress, + knowledgeIndicator, + onDeleteRequest, + restoreAction, + withActionLoading, + ] + ); + + const queryActionItems = useMemo( + () => + knowledgeIndicator.kind === 'query' + ? [ + { + setIsActionsMenuOpen(false); + onDeleteRequest(knowledgeIndicator); + }} + > + {KI_ACTION_DELETE_LABEL} + , + + withActionLoading(() => + promoteAction.mutate(knowledgeIndicator.query.id, { + onSettled: () => { + setIsActionInProgress(false); + }, + }) + ) + } + > + {KI_ACTION_PROMOTE_LABEL} + , + ] + : [], + [isActionInProgress, knowledgeIndicator, onDeleteRequest, promoteAction, withActionLoading] + ); + + return ( + setIsActionsMenuOpen((current) => !current)} + /> + } + isOpen={isActionsMenuOpen} + closePopover={() => setIsActionsMenuOpen(false)} + panelPaddingSize="none" + anchorPosition="downRight" + > + + + ); +} + +const KI_ACTIONS_MENU_BUTTON_ARIA_LABEL = i18n.translate( + 'xpack.streams.significantEventsTable.actionsMenuButtonAriaLabel', + { + defaultMessage: 'Knowledge indicator actions', + } +); + +const KI_ACTIONS_MENU_POPOVER_ARIA_LABEL = i18n.translate( + 'xpack.streams.significantEventsTable.actionsMenuPopoverAriaLabel', + { + defaultMessage: 'Knowledge indicator actions menu', + } +); + +const KI_ACTION_DELETE_LABEL = i18n.translate( + 'xpack.streams.significantEventsTable.action.delete', + { + defaultMessage: 'Delete', + } +); + +const KI_ACTION_EXCLUDE_LABEL = i18n.translate( + 'xpack.streams.significantEventsTable.action.exclude', + { + defaultMessage: 'Exclude', + } +); + +const KI_ACTION_RESTORE_LABEL = i18n.translate( + 'xpack.streams.significantEventsTable.action.restore', + { + defaultMessage: 'Restore', + } +); + +const KI_ACTION_PROMOTE_LABEL = i18n.translate( + 'xpack.streams.significantEventsTable.action.promote', + { + defaultMessage: 'Promote', + } +); + +const KI_EXCLUDE_ACTION_SUCCESS_TOAST_TITLE = i18n.translate( + 'xpack.streams.significantEventsTable.excludeActionSuccessToastTitle', + { + defaultMessage: 'Knowledge indicator excluded', + } +); + +const KI_EXCLUDE_ACTION_ERROR_TOAST_TITLE = i18n.translate( + 'xpack.streams.significantEventsTable.excludeActionErrorToastTitle', + { + defaultMessage: 'Failed to exclude knowledge indicator', + } +); + +const KI_RESTORE_ACTION_SUCCESS_TOAST_TITLE = i18n.translate( + 'xpack.streams.significantEventsTable.restoreActionSuccessToastTitle', + { + defaultMessage: 'Knowledge indicator restored', + } +); + +const KI_RESTORE_ACTION_ERROR_TOAST_TITLE = i18n.translate( + 'xpack.streams.significantEventsTable.restoreActionErrorToastTitle', + { + defaultMessage: 'Failed to restore knowledge indicator', + } +); + +const KI_PROMOTE_ACTION_SUCCESS_TOAST_TITLE = i18n.translate( + 'xpack.streams.significantEventsTable.promoteActionSuccessToastTitle', + { + defaultMessage: 'Knowledge indicator promoted', + } +); + +const KI_PROMOTE_ACTION_ERROR_TOAST_TITLE = i18n.translate( + 'xpack.streams.significantEventsTable.promoteActionErrorToastTitle', + { + defaultMessage: 'Failed to promote knowledge indicator', + } +); diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/knowledge_indicator_details_flyout/index.ts b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/knowledge_indicator_details_flyout/index.ts new file mode 100644 index 0000000000000..6f580932d08f0 --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/knowledge_indicator_details_flyout/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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { KnowledgeIndicatorFeatureDetailsContent } from './knowledge_indicator_feature_details_content'; +export { KnowledgeIndicatorQueryDetailsContent } from './knowledge_indicator_query_details_content'; +export { KnowledgeIndicatorDetailsFlyout } from './knowledge_indicator_details_flyout'; diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/knowledge_indicator_details_flyout/knowledge_indicator_details_flyout.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/knowledge_indicator_details_flyout/knowledge_indicator_details_flyout.tsx new file mode 100644 index 0000000000000..877d09c5713a4 --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/knowledge_indicator_details_flyout/knowledge_indicator_details_flyout.tsx @@ -0,0 +1,85 @@ +/* + * 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 { + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutHeader, + EuiTitle, + useGeneratedHtmlId, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import type { KnowledgeIndicator } from '@kbn/streams-ai'; +import React from 'react'; +import { KnowledgeIndicatorFeatureDetailsContent } from './knowledge_indicator_feature_details_content'; +import { KnowledgeIndicatorQueryDetailsContent } from './knowledge_indicator_query_details_content'; + +interface Props { + knowledgeIndicator: KnowledgeIndicator; + occurrencesByQueryId: Record>; + onClose: () => void; +} + +export function KnowledgeIndicatorDetailsFlyout({ + knowledgeIndicator, + occurrencesByQueryId, + onClose, +}: Props) { + const flyoutTitleId = useGeneratedHtmlId({ prefix: 'knowledgeIndicatorDetailsFlyoutTitle' }); + const title = + knowledgeIndicator.kind === 'feature' + ? knowledgeIndicator.feature.title ?? knowledgeIndicator.feature.id + : knowledgeIndicator.query.title ?? knowledgeIndicator.query.id; + + return ( + + + + + +

{title}

+
+
+ + + +
+
+ + {knowledgeIndicator.kind === 'feature' ? ( + + ) : ( + + )} + +
+ ); +} + +const CLOSE_BUTTON_ARIA_LABEL = i18n.translate( + 'xpack.streams.knowledgeIndicatorDetailsFlyout.closeButtonAriaLabel', + { + defaultMessage: 'Close', + } +); diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/knowledge_indicator_details_flyout/knowledge_indicator_feature_details_content.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/knowledge_indicator_details_flyout/knowledge_indicator_feature_details_content.tsx new file mode 100644 index 0000000000000..56e7ac9b4ccb9 --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/knowledge_indicator_details_flyout/knowledge_indicator_feature_details_content.tsx @@ -0,0 +1,248 @@ +/* + * 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 { + EuiBadge, + EuiCodeBlock, + EuiDescriptionList, + EuiFlexGroup, + EuiFlexItem, + EuiHealth, + EuiHorizontalRule, + EuiText, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import type { Feature } from '@kbn/streams-schema'; +import { upperFirst } from 'lodash'; +import React, { useMemo } from 'react'; +import { InfoPanel } from '../../../info_panel'; +import { getConfidenceColor } from '../../stream_detail_systems/stream_features/use_stream_features_table'; + +interface Props { + feature: Feature; +} + +export function KnowledgeIndicatorFeatureDetailsContent({ feature }: Props) { + const listItems = useMemo(() => { + const tags = feature.tags?.length ? feature.tags : []; + + return [ + { + title: DETAILS_ID_LABEL, + description: ( + + {feature.id} + + ), + }, + { + title: DETAILS_TYPE_LABEL, + description: {upperFirst(feature.type)}, + }, + { + title: DETAILS_SUBTYPE_LABEL, + description: {feature.subtype ?? EMPTY_VALUE}, + }, + { + title: DETAILS_PROPERTIES_LABEL, + description: ( + + {Object.entries(feature.properties) + .filter(([, value]) => typeof value === 'string') + .map(([key, value]) => ( + + {key} {value as string} + + ))} + + ), + }, + { + title: DETAILS_CONFIDENCE_LABEL, + description: ( + {feature.confidence} + ), + }, + { + title: DETAILS_TAGS_LABEL, + description: + tags.length > 0 ? ( + + {tags.map((tag) => ( + + {tag} + + ))} + + ) : ( + {EMPTY_VALUE} + ), + }, + { + title: DETAILS_LAST_SEEN_LABEL, + description: {feature.last_seen || EMPTY_VALUE}, + }, + { + title: DETAILS_EXPIRES_AT_LABEL, + description: {feature.expires_at ?? EMPTY_VALUE}, + }, + ]; + }, [feature]); + + const evidence = feature.evidence?.length ? feature.evidence : []; + const hasMeta = Object.keys(feature.meta ?? {}).length > 0; + + return ( + + + + {listItems.map((item, index) => ( + + + {index < listItems.length - 1 && } + + ))} + + + + + {feature.description || NO_DESCRIPTION_AVAILABLE} + + + + + {evidence.length > 0 ? ( + evidence.map((item, index) => ( + + + + + + + {item} + + + {index < evidence.length - 1 && } + + )) + ) : ( + {NO_EVIDENCE_AVAILABLE} + )} + + + + + {hasMeta ? ( + + {JSON.stringify(feature.meta ?? {}, null, 2)} + + ) : ( + {NO_META_AVAILABLE} + )} + + + + + + {JSON.stringify(feature, null, 2)} + + + + + ); +} + +const GENERAL_INFORMATION_LABEL = i18n.translate( + 'xpack.streams.featureDetailsFlyout.generalInformationLabel', + { + defaultMessage: 'General information', + } +); + +const DETAILS_TYPE_LABEL = i18n.translate('xpack.streams.featureDetailsFlyout.typeLabel', { + defaultMessage: 'Type', +}); + +const DETAILS_ID_LABEL = i18n.translate('xpack.streams.featureDetailsFlyout.idLabel', { + defaultMessage: 'ID', +}); + +const DETAILS_SUBTYPE_LABEL = i18n.translate('xpack.streams.featureDetailsFlyout.subtypeLabel', { + defaultMessage: 'Subtype', +}); + +const DETAILS_PROPERTIES_LABEL = i18n.translate( + 'xpack.streams.featureDetailsFlyout.propertiesLabel', + { + defaultMessage: 'Properties', + } +); + +const DETAILS_CONFIDENCE_LABEL = i18n.translate( + 'xpack.streams.featureDetailsFlyout.confidenceLabel', + { + defaultMessage: 'Confidence', + } +); + +const DETAILS_TAGS_LABEL = i18n.translate('xpack.streams.featureDetailsFlyout.tagsLabel', { + defaultMessage: 'Tags', +}); + +const DETAILS_LAST_SEEN_LABEL = i18n.translate('xpack.streams.featureDetailsFlyout.lastSeenLabel', { + defaultMessage: 'Last seen', +}); + +const DETAILS_EXPIRES_AT_LABEL = i18n.translate( + 'xpack.streams.featureDetailsFlyout.expiresAtLabel', + { + defaultMessage: 'Expires at', + } +); + +const DESCRIPTION_LABEL = i18n.translate('xpack.streams.featureDetailsFlyout.descriptionLabel', { + defaultMessage: 'Description', +}); + +const NO_DESCRIPTION_AVAILABLE = i18n.translate( + 'xpack.streams.featureDetailsFlyout.noDescriptionAvailable', + { + defaultMessage: 'No description available', + } +); + +const EVIDENCE_LABEL = i18n.translate('xpack.streams.featureDetailsFlyout.evidenceLabel', { + defaultMessage: 'Evidence', +}); + +const NO_EVIDENCE_AVAILABLE = i18n.translate( + 'xpack.streams.featureDetailsFlyout.noEvidenceAvailable', + { + defaultMessage: 'No evidence available', + } +); + +const META_LABEL = i18n.translate('xpack.streams.featureDetailsFlyout.metaLabel', { + defaultMessage: 'Meta', +}); + +const NO_META_AVAILABLE = i18n.translate('xpack.streams.featureDetailsFlyout.noMetaAvailable', { + defaultMessage: 'No meta information', +}); + +const RAW_DOCUMENT_LABEL = i18n.translate('xpack.streams.featureDetailsFlyout.rawDocumentLabel', { + defaultMessage: 'Raw document', +}); + +const EMPTY_VALUE = i18n.translate('xpack.streams.featureDetailsFlyout.emptyValue', { + defaultMessage: '-', +}); diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/knowledge_indicator_details_flyout/knowledge_indicator_query_details_content.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/knowledge_indicator_details_flyout/knowledge_indicator_query_details_content.tsx new file mode 100644 index 0000000000000..d4e6e9ebd83e1 --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/knowledge_indicator_details_flyout/knowledge_indicator_query_details_content.tsx @@ -0,0 +1,135 @@ +/* + * 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 { + EuiBadge, + EuiCodeBlock, + EuiDescriptionList, + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiSpacer, + EuiText, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import type { StreamQuery } from '@kbn/streams-schema'; +import React from 'react'; +import { SeverityBadge } from '../../significant_events_discovery/components/severity_badge/severity_badge'; +import { InfoPanel } from '../../../info_panel'; +import { SparkPlot } from '../../../spark_plot'; + +interface Props { + query: StreamQuery; + occurrences?: Array<{ x: number; y: number }>; +} + +export function KnowledgeIndicatorQueryDetailsContent({ query, occurrences }: Props) { + const listItems = [ + { + title: DETAILS_TYPE_LABEL, + description: {QUERY_BADGE_LABEL}, + }, + { + title: DETAILS_QUERY_LABEL, + description: ( + + {query.esql?.query ?? EMPTY_VALUE} + + ), + }, + { + title: DETAILS_DESCRIPTION_LABEL, + description: {query.description || EMPTY_VALUE}, + }, + { + title: DETAILS_SEVERITY_LABEL, + description: , + }, + ]; + + return ( + + + + {listItems.map((item, index) => ( + + + {index < listItems.length - 1 && } + + ))} + + + {occurrences ? ( + + + + + + + ) : null} + + ); +} + +const GENERAL_INFORMATION_LABEL = i18n.translate( + 'xpack.streams.knowledgeIndicatorDetails.generalInformationLabel', + { + defaultMessage: 'General information', + } +); + +const DETAILS_TYPE_LABEL = i18n.translate('xpack.streams.knowledgeIndicatorDetails.typeLabel', { + defaultMessage: 'Type', +}); + +const DETAILS_QUERY_LABEL = i18n.translate('xpack.streams.knowledgeIndicatorDetails.queryLabel', { + defaultMessage: 'Query', +}); + +const DETAILS_SEVERITY_LABEL = i18n.translate( + 'xpack.streams.knowledgeIndicatorDetails.severityLabel', + { + defaultMessage: 'Severity', + } +); + +const DETAILS_DESCRIPTION_LABEL = i18n.translate( + 'xpack.streams.knowledgeIndicatorDetails.descriptionLabel', + { + defaultMessage: 'Description', + } +); + +const OCCURRENCES_LABEL = i18n.translate( + 'xpack.streams.knowledgeIndicatorDetails.occurrencesLabel', + { + defaultMessage: 'Occurrences', + } +); + +const QUERY_BADGE_LABEL = i18n.translate( + 'xpack.streams.knowledgeIndicatorDetails.queryBadgeLabel', + { + defaultMessage: 'Query', + } +); + +const EMPTY_VALUE = i18n.translate('xpack.streams.knowledgeIndicatorDetails.emptyValue', { + defaultMessage: '-', +}); diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/knowledge_indicator_rules_selector/index.ts b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/knowledge_indicator_rules_selector/index.ts new file mode 100644 index 0000000000000..8e994ba0343b5 --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/knowledge_indicator_rules_selector/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 { KnowledgeIndicatorRulesSelector } from './knowledge_indicator_rules_selector'; diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/knowledge_indicator_rules_selector/knowledge_indicator_rules_selector.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/knowledge_indicator_rules_selector/knowledge_indicator_rules_selector.tsx new file mode 100644 index 0000000000000..5b36f1d41340d --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/knowledge_indicator_rules_selector/knowledge_indicator_rules_selector.tsx @@ -0,0 +1,88 @@ +/* + * 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 { EuiSelectableOption } from '@elastic/eui'; +import { EuiButton, EuiPopover, EuiSelectable, useGeneratedHtmlId } from '@elastic/eui'; +import { css } from '@emotion/react'; +import { i18n } from '@kbn/i18n'; +import React, { useMemo, useState } from 'react'; + +interface KnowledgeIndicatorRulesSelectorProps { + options: EuiSelectableOption[]; + onChange: (options: EuiSelectableOption[]) => void; +} + +export function KnowledgeIndicatorRulesSelector({ + options, + onChange, +}: KnowledgeIndicatorRulesSelectorProps) { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const popoverId = useGeneratedHtmlId({ prefix: 'significantEventsTypeFilterPopover' }); + + const selectedLabel = useMemo( + () => options.find((option) => option.checked === 'on')?.label, + [options] + ); + + return ( + setIsPopoverOpen((isOpen) => !isOpen)} + > + {selectedLabel ?? TYPE_FILTER_LABEL} + + } + isOpen={isPopoverOpen} + closePopover={() => setIsPopoverOpen(false)} + panelPaddingSize="none" + > + { + onChange(nextOptions); + setIsPopoverOpen(false); + }} + > + {(list) => ( +
+ {list} +
+ )} +
+
+ ); +} + +const TYPE_FILTER_POPOVER_ARIA_LABEL = i18n.translate( + 'xpack.streams.significantEventsTable.typeFilterPopoverLabel', + { + defaultMessage: 'Type filter', + } +); + +const TYPE_FILTER_LABEL = i18n.translate('xpack.streams.significantEventsTable.typeFilterLabel', { + defaultMessage: 'Knowledge Indicators', +}); + +const TYPE_FILTER_SELECTABLE_ARIA_LABEL = i18n.translate( + 'xpack.streams.significantEventsTable.typeFilterSelectableAriaLabel', + { + defaultMessage: 'Filter by type', + } +); diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/knowledge_indicators_status_filter/index.ts b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/knowledge_indicators_status_filter/index.ts new file mode 100644 index 0000000000000..61edd7f63a3f0 --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/knowledge_indicators_status_filter/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 { KnowledgeIndicatorsStatusFilter } from './knowledge_indicators_status_filter'; diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/knowledge_indicators_status_filter/knowledge_indicators_status_filter.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/knowledge_indicators_status_filter/knowledge_indicators_status_filter.tsx new file mode 100644 index 0000000000000..3259acb8086ff --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/knowledge_indicators_status_filter/knowledge_indicators_status_filter.tsx @@ -0,0 +1,98 @@ +/* + * 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 { EuiFilterButton, EuiFilterGroup } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import type { KnowledgeIndicator } from '@kbn/streams-ai'; +import React, { useMemo } from 'react'; + +interface KnowledgeIndicatorStatusFilterProps { + knowledgeIndicators: KnowledgeIndicator[]; + searchTerm: string; + selectedTypes: string[]; + statusFilter: 'active' | 'excluded'; + onStatusFilterChange: (filter: 'active' | 'excluded') => void; +} + +export function KnowledgeIndicatorsStatusFilter({ + knowledgeIndicators, + searchTerm, + selectedTypes, + statusFilter, + onStatusFilterChange, +}: KnowledgeIndicatorStatusFilterProps) { + const statusFilterCounts = useMemo(() => { + const normalizedSearchTerm = searchTerm.trim().toLowerCase(); + + return knowledgeIndicators.reduce( + (accumulator, knowledgeIndicator) => { + const type = + knowledgeIndicator.kind === 'feature' ? knowledgeIndicator.feature.type : 'query'; + const matchesType = selectedTypes.length === 0 || selectedTypes.includes(type); + + if (!matchesType) { + return accumulator; + } + + const title = + knowledgeIndicator.kind === 'feature' + ? (knowledgeIndicator.feature.title ?? '').toLowerCase() + : (knowledgeIndicator.query.title ?? '').toLowerCase(); + + if (normalizedSearchTerm && !title.includes(normalizedSearchTerm)) { + return accumulator; + } + + if (knowledgeIndicator.kind === 'feature' && knowledgeIndicator.feature.excluded_at) { + accumulator.excluded += 1; + } else { + accumulator.active += 1; + } + + return accumulator; + }, + { active: 0, excluded: 0 } + ); + }, [knowledgeIndicators, searchTerm, selectedTypes]); + + return ( + + onStatusFilterChange('active')} + > + {KNOWLEDGE_INDICATOR_STATUS_FILTER_ACTIVE_LABEL} + + onStatusFilterChange('excluded')} + > + {KNOWLEDGE_INDICATOR_STATUS_FILTER_EXCLUDED_LABEL} + + + ); +} + +const KNOWLEDGE_INDICATOR_STATUS_FILTER_ACTIVE_LABEL = i18n.translate( + 'xpack.streams.significantEventsTable.knowledgeIndicatorStatusFilterActiveLabel', + { + defaultMessage: 'Active', + } +); + +const KNOWLEDGE_INDICATOR_STATUS_FILTER_EXCLUDED_LABEL = i18n.translate( + 'xpack.streams.significantEventsTable.knowledgeIndicatorStatusFilterExcludedLabel', + { + defaultMessage: 'Excluded', + } +); diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/knowledge_indicators_table/index.ts b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/knowledge_indicators_table/index.ts new file mode 100644 index 0000000000000..adade775c0472 --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/knowledge_indicators_table/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 { KnowledgeIndicatorsTable } from './knowledge_indicators_table'; diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/knowledge_indicators_table/knowledge_indicators_table.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/knowledge_indicators_table/knowledge_indicators_table.tsx new file mode 100644 index 0000000000000..fe73378f8ea9d --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/knowledge_indicators_table/knowledge_indicators_table.tsx @@ -0,0 +1,395 @@ +/* + * 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 { EuiBasicTableColumn } from '@elastic/eui'; +import { + type CriteriaWithPagination, + EuiBadge, + EuiButtonIcon, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiInMemoryTable, + EuiSpacer, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import type { Streams } from '@kbn/streams-schema'; +import type { KnowledgeIndicator } from '@kbn/streams-ai'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { css } from '@emotion/react'; +import { useKnowledgeIndicatorsBulkDelete } from '../hooks/use_knowledge_indicators_bulk_delete'; +import { KnowledgeIndicatorActionsCell } from '../knowledge_indicator_actions_cell'; +import { DeleteTableItemsModal } from '../delete_table_items_modal'; +import { SparkPlot } from '../../../spark_plot'; +import { TableTitle } from '../../stream_detail_systems/table_title'; + +interface KnowledgeIndicatorsTableProps { + definition: Streams.all.Definition; + knowledgeIndicators: KnowledgeIndicator[]; + occurrencesByQueryId: Record>; + searchTerm: string; + selectedTypes: string[]; + statusFilter: 'active' | 'excluded'; + onViewDetails: (knowledgeIndicator: KnowledgeIndicator) => void; +} + +const getKnowledgeIndicatorItemId = (knowledgeIndicator: KnowledgeIndicator) => { + if (knowledgeIndicator.kind === 'feature') { + return `feature:${knowledgeIndicator.feature.uuid}`; + } + + return `query:${knowledgeIndicator.query.id}`; +}; + +export function KnowledgeIndicatorsTable({ + definition, + knowledgeIndicators, + occurrencesByQueryId, + searchTerm, + selectedTypes, + statusFilter, + onViewDetails, +}: KnowledgeIndicatorsTableProps) { + const [selectedKnowledgeIndicators, setSelectedKnowledgeIndicators] = useState< + KnowledgeIndicator[] + >([]); + const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 25 }); + const [knowledgeIndicatorsToDelete, setKnowledgeIndicatorsToDelete] = useState< + KnowledgeIndicator[] + >([]); + const { deleteKnowledgeIndicatorsInBulk, isDeleting } = useKnowledgeIndicatorsBulkDelete({ + definition, + onSuccess: () => { + setSelectedKnowledgeIndicators([]); + setKnowledgeIndicatorsToDelete([]); + }, + }); + + const filteredKnowledgeIndicators = useMemo(() => { + const normalizedSearchTerm = searchTerm.trim().toLowerCase(); + + return knowledgeIndicators.filter((knowledgeIndicator) => { + const matchesStatusFilter = + statusFilter === 'active' + ? knowledgeIndicator.kind === 'query' || !knowledgeIndicator.feature.excluded_at + : knowledgeIndicator.kind === 'feature' && + Boolean(knowledgeIndicator.feature.excluded_at); + + if (!matchesStatusFilter) { + return false; + } + + const type = + knowledgeIndicator.kind === 'feature' ? knowledgeIndicator.feature.type : 'query'; + const matchesType = selectedTypes.length === 0 || selectedTypes.includes(type); + + if (!matchesType) { + return false; + } + + if (!normalizedSearchTerm) { + return true; + } + + if (knowledgeIndicator.kind === 'feature') { + return (knowledgeIndicator.feature.title ?? '') + .toLowerCase() + .includes(normalizedSearchTerm); + } + + return (knowledgeIndicator.query.title ?? '').toLowerCase().includes(normalizedSearchTerm); + }); + }, [knowledgeIndicators, searchTerm, selectedTypes, statusFilter]); + + useEffect(() => { + setPagination((currentPagination) => { + if (currentPagination.pageIndex === 0) { + return currentPagination; + } + + return { + ...currentPagination, + pageIndex: 0, + }; + }); + }, [searchTerm, selectedTypes, statusFilter]); + + const isSelectionActionsDisabled = selectedKnowledgeIndicators.length === 0; + + const handleTableChange = useCallback(({ page }: CriteriaWithPagination) => { + if (!page) { + return; + } + + setPagination({ + pageIndex: page.index, + pageSize: page.size, + }); + }, []); + + const columns = useMemo>>( + () => [ + { + name: SIGNIFICANT_EVENTS_TABLE_TITLE_COLUMN_LABEL, + render: (knowledgeIndicator: KnowledgeIndicator) => { + const title = + knowledgeIndicator.kind === 'feature' + ? knowledgeIndicator.feature.title ?? knowledgeIndicator.feature.id + : knowledgeIndicator.query.title ?? knowledgeIndicator.query.id; + + if (knowledgeIndicator.kind === 'feature') { + return ( + + + onViewDetails(knowledgeIndicator)} + /> + + + {title} + + + ); + } + + return ( + + + onViewDetails(knowledgeIndicator)} + /> + + + {title} + + + ); + }, + }, + { + name: SIGNIFICANT_EVENTS_TABLE_EVENTS_COLUMN_LABEL, + width: '160px', + render: (knowledgeIndicator: KnowledgeIndicator) => { + if (knowledgeIndicator.kind !== 'query' || !knowledgeIndicator.rule.backed) { + return null; + } + + const occurrences = occurrencesByQueryId[knowledgeIndicator.query.id]; + + if (!occurrences) { + return null; + } + + return ( + + ); + }, + }, + { + name: SIGNIFICANT_EVENTS_TABLE_TYPE_COLUMN_LABEL, + width: '200px', + render: (knowledgeIndicator: KnowledgeIndicator) => { + if (knowledgeIndicator.kind === 'feature') { + return ( + + {knowledgeIndicator.feature.type} + + ); + } + + return {SIGNIFICANT_EVENTS_TABLE_QUERY_TYPE_LABEL}; + }, + }, + { + name: SIGNIFICANT_EVENTS_TABLE_ACTIONS_COLUMN_LABEL, + width: '80px', + align: 'right', + render: (knowledgeIndicator: KnowledgeIndicator) => ( + setKnowledgeIndicatorsToDelete([item])} + /> + ), + }, + ], + [definition, occurrencesByQueryId, onViewDetails] + ); + + return ( + <> + + + + + + setSelectedKnowledgeIndicators([])} + > + {SIGNIFICANT_EVENTS_TABLE_CLEAR_SELECTION_LABEL} + + + + { + setKnowledgeIndicatorsToDelete(selectedKnowledgeIndicators); + }} + > + {SIGNIFICANT_EVENTS_TABLE_DELETE_BULK_ACTION_LABEL} + + + + + + + items={filteredKnowledgeIndicators} + itemId={getKnowledgeIndicatorItemId} + columns={columns} + selection={{ + selected: selectedKnowledgeIndicators, + onSelectionChange: setSelectedKnowledgeIndicators, + }} + pagination={{ + pageIndex: pagination.pageIndex, + pageSize: pagination.pageSize, + pageSizeOptions: [25, 50, 100], + }} + onTableChange={handleTableChange} + tableCaption={SIGNIFICANT_EVENTS_TABLE_CAPTION} + /> + {knowledgeIndicatorsToDelete.length > 0 ? ( + setKnowledgeIndicatorsToDelete([])} + onConfirm={() => { + void deleteKnowledgeIndicatorsInBulk(knowledgeIndicatorsToDelete); + }} + isLoading={isDeleting} + /> + ) : null} + + ); +} + +const SIGNIFICANT_EVENTS_TABLE_TITLE_COLUMN_LABEL = i18n.translate( + 'xpack.streams.significantEventsTable.columns.titleLabel', + { + defaultMessage: 'Knowledge Indicator', + } +); + +const SIGNIFICANT_EVENTS_TABLE_TYPE_COLUMN_LABEL = i18n.translate( + 'xpack.streams.significantEventsTable.columns.typeLabel', + { + defaultMessage: 'Type', + } +); + +const SIGNIFICANT_EVENTS_TABLE_QUERY_TYPE_LABEL = i18n.translate( + 'xpack.streams.significantEventsTable.columns.queryTypeLabel', + { + defaultMessage: 'Query', + } +); + +const SIGNIFICANT_EVENTS_TABLE_ACTIONS_COLUMN_LABEL = i18n.translate( + 'xpack.streams.significantEventsTable.columns.actionsLabel', + { + defaultMessage: 'Actions', + } +); + +const SIGNIFICANT_EVENTS_TABLE_EVENTS_COLUMN_LABEL = i18n.translate( + 'xpack.streams.significantEventsTable.columns.eventsLabel', + { + defaultMessage: 'Events', + } +); + +const SIGNIFICANT_EVENTS_TABLE_CAPTION = i18n.translate( + 'xpack.streams.significantEventsTable.tableCaption', + { + defaultMessage: 'Significant events', + } +); + +const SIGNIFICANT_EVENTS_TABLE_OCCURRENCES_TOOLTIP_NAME = i18n.translate( + 'xpack.streams.significantEventsTable.occurrencesTooltipName', + { + defaultMessage: 'Detected event occurrences', + } +); + +const SIGNIFICANT_EVENTS_TABLE_CLEAR_SELECTION_LABEL = i18n.translate( + 'xpack.streams.significantEventsTable.clearSelectionLabel', + { + defaultMessage: 'Clear selection', + } +); + +const SIGNIFICANT_EVENTS_TABLE_DELETE_BULK_ACTION_LABEL = i18n.translate( + 'xpack.streams.significantEventsTable.deleteBulkActionLabel', + { + defaultMessage: 'Delete selected', + } +); + +const SIGNIFICANT_EVENTS_TABLE_LABEL = i18n.translate( + 'xpack.streams.significantEventsTable.label', + { + defaultMessage: 'Knowledge indicators', + } +); + +const VIEW_DETAILS_ARIA_LABEL = i18n.translate( + 'xpack.streams.knowledgeIndicatorsTable.viewDetailsAriaLabel', + { + defaultMessage: 'View details', + } +); + +const DELETE_KNOWLEDGE_INDICATORS_MODAL_TITLE = (count: number) => + i18n.translate('xpack.streams.deleteKnowledgeIndicatorsModal.title', { + defaultMessage: + 'Are you sure you want to delete {count, plural, one {this knowledge indicator} other {these knowledge indicators}}?', + values: { count }, + }); diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/knowledge_indicators_type_filter/index.ts b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/knowledge_indicators_type_filter/index.ts new file mode 100644 index 0000000000000..6a4a04884f0ae --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/knowledge_indicators_type_filter/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 { KnowledgeIndicatorsTypeFilter } from './knowledge_indicators_type_filter'; diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/knowledge_indicators_type_filter/knowledge_indicators_type_filter.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/knowledge_indicators_type_filter/knowledge_indicators_type_filter.tsx new file mode 100644 index 0000000000000..87e0eb29ceab6 --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/knowledge_indicators_type_filter/knowledge_indicators_type_filter.tsx @@ -0,0 +1,176 @@ +/* + * 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 { EuiSelectableOption } from '@elastic/eui'; +import { EuiBadge, EuiButton, EuiPopover, EuiSelectable, useGeneratedHtmlId } from '@elastic/eui'; +import { css } from '@emotion/react'; +import { i18n } from '@kbn/i18n'; +import type { KnowledgeIndicator } from '@kbn/streams-ai'; +import { upperFirst } from 'lodash'; +import React, { useMemo, useState } from 'react'; + +interface KnowledgeIndicatorTypeFilterProps { + knowledgeIndicators: KnowledgeIndicator[]; + searchTerm: string; + statusFilter: 'active' | 'excluded'; + selectedTypes: string[]; + onSelectedTypesChange: (selectedTypes: string[]) => void; +} + +export function KnowledgeIndicatorsTypeFilter({ + knowledgeIndicators, + searchTerm, + statusFilter, + selectedTypes, + onSelectedTypesChange, +}: KnowledgeIndicatorTypeFilterProps) { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const popoverId = useGeneratedHtmlId({ + prefix: 'knowledgeIndicatorTypeFilterPopover', + }); + + const hasActiveFilters = selectedTypes.length > 0; + + const availableTypes = useMemo(() => { + const types = new Set(); + + knowledgeIndicators.forEach((knowledgeIndicator) => { + if (knowledgeIndicator.kind === 'feature') { + types.add(knowledgeIndicator.feature.type); + } else { + types.add('query'); + } + }); + + return Array.from(types).sort((left, right) => left.localeCompare(right)); + }, [knowledgeIndicators]); + + const typeCounts = useMemo(() => { + const normalizedSearchTerm = searchTerm.trim().toLowerCase(); + const counts: Record = {}; + + knowledgeIndicators.forEach((knowledgeIndicator) => { + const matchesStatusFilter = + statusFilter === 'active' + ? knowledgeIndicator.kind === 'query' || !knowledgeIndicator.feature.excluded_at + : knowledgeIndicator.kind === 'feature' && + Boolean(knowledgeIndicator.feature.excluded_at); + + if (!matchesStatusFilter) { + return; + } + + const type = + knowledgeIndicator.kind === 'feature' ? knowledgeIndicator.feature.type : 'query'; + + const title = + knowledgeIndicator.kind === 'feature' + ? (knowledgeIndicator.feature.title ?? '').toLowerCase() + : (knowledgeIndicator.query.title ?? '').toLowerCase(); + + if (!normalizedSearchTerm || title.includes(normalizedSearchTerm)) { + counts[type] = (counts[type] ?? 0) + 1; + } + }); + + return counts; + }, [knowledgeIndicators, searchTerm, statusFilter]); + + const options = useMemo( + () => [ + { + label: KNOWLEDGE_INDICATOR_TYPE_FILTER_GROUP_LABEL, + isGroupLabel: true, + }, + ...availableTypes.map((type) => ({ + key: type, + checked: selectedTypes.includes(type) ? ('on' as const) : undefined, + label: type === 'query' ? KNOWLEDGE_INDICATOR_QUERY_TYPE_LABEL : upperFirst(type), + append: {typeCounts[type] ?? 0}, + })), + ], + [availableTypes, selectedTypes, typeCounts] + ); + + return ( + setIsPopoverOpen((isOpen) => !isOpen)} + > + {KNOWLEDGE_INDICATOR_TYPE_FILTER_LABEL} + + } + isOpen={isPopoverOpen} + closePopover={() => setIsPopoverOpen(false)} + panelPaddingSize="none" + > + { + onSelectedTypesChange( + nextOptions + .filter((option) => option.checked === 'on') + .map((option) => String(option.key ?? option.label)) + ); + }} + > + {(list) => ( +
+ {list} +
+ )} +
+
+ ); +} + +const KNOWLEDGE_INDICATOR_TYPE_FILTER_GROUP_LABEL = i18n.translate( + 'xpack.streams.significantEventsTable.knowledgeIndicatorTypeFilterGroupLabel', + { + defaultMessage: 'Filter by field type', + } +); + +const KNOWLEDGE_INDICATOR_QUERY_TYPE_LABEL = i18n.translate( + 'xpack.streams.significantEventsTable.knowledgeIndicatorType.query', + { + defaultMessage: 'Query', + } +); + +const KNOWLEDGE_INDICATOR_TYPE_FILTER_POPOVER_ARIA_LABEL = i18n.translate( + 'xpack.streams.significantEventsTable.knowledgeIndicatorTypeFilterPopoverLabel', + { + defaultMessage: 'Knowledge indicator type filter', + } +); + +const KNOWLEDGE_INDICATOR_TYPE_FILTER_LABEL = i18n.translate( + 'xpack.streams.significantEventsTable.knowledgeIndicatorTypeFilterLabel', + { + defaultMessage: 'Type', + } +); + +const KNOWLEDGE_INDICATOR_TYPE_FILTER_SELECTABLE_ARIA_LABEL = i18n.translate( + 'xpack.streams.significantEventsTable.knowledgeIndicatorTypeFilterSelectableAriaLabel', + { + defaultMessage: 'Filter knowledge indicators by type', + } +); diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/rules_table/index.ts b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/rules_table/index.ts new file mode 100644 index 0000000000000..e2845846b1d57 --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/rules_table/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 { RulesTable } from './rules_table'; diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/rules_table/rule_actions_cell/index.ts b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/rules_table/rule_actions_cell/index.ts new file mode 100644 index 0000000000000..772d04759879b --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/rules_table/rule_actions_cell/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 { RuleActionsCell } from './rule_actions_cell'; diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/rules_table/rule_actions_cell/rule_actions_cell.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/rules_table/rule_actions_cell/rule_actions_cell.tsx new file mode 100644 index 0000000000000..2357cda2f530e --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/rules_table/rule_actions_cell/rule_actions_cell.tsx @@ -0,0 +1,78 @@ +/* + * 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 { EuiButtonIcon, EuiContextMenuItem, EuiContextMenuPanel, EuiPopover } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React, { useState } from 'react'; +import type { KnowledgeIndicator } from '@kbn/streams-ai'; + +interface RuleActionsCellProps { + rule: KnowledgeIndicator; + onDeleteRequest: (rule: KnowledgeIndicator) => void; + isDisabled?: boolean; +} + +export function RuleActionsCell({ + rule, + onDeleteRequest, + isDisabled = false, +}: RuleActionsCellProps) { + const [isActionsMenuOpen, setIsActionsMenuOpen] = useState(false); + + return ( + setIsActionsMenuOpen((current) => !current)} + /> + } + isOpen={isActionsMenuOpen} + closePopover={() => setIsActionsMenuOpen(false)} + panelPaddingSize="none" + anchorPosition="downRight" + > + { + setIsActionsMenuOpen(false); + onDeleteRequest(rule); + }} + > + {RULE_ACTION_DELETE_LABEL} + , + ]} + /> + + ); +} + +const RULE_ACTIONS_MENU_BUTTON_ARIA_LABEL = i18n.translate( + 'xpack.streams.rulesTable.actionsMenuButtonAriaLabel', + { + defaultMessage: 'Rule actions', + } +); + +const RULE_ACTIONS_MENU_POPOVER_ARIA_LABEL = i18n.translate( + 'xpack.streams.rulesTable.actionsMenuPopoverAriaLabel', + { + defaultMessage: 'Rule actions menu', + } +); + +const RULE_ACTION_DELETE_LABEL = i18n.translate('xpack.streams.rulesTable.action.delete', { + defaultMessage: 'Delete', +}); diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/rules_table/rules_table.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/rules_table/rules_table.tsx new file mode 100644 index 0000000000000..515637da1309b --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/rules_table/rules_table.tsx @@ -0,0 +1,342 @@ +/* + * 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 { EuiBasicTableColumn } from '@elastic/eui'; +import { + type CriteriaWithPagination, + EuiButtonIcon, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiInMemoryTable, + EuiSpacer, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import type { Streams } from '@kbn/streams-schema'; +import type { KnowledgeIndicator } from '@kbn/streams-ai'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { useQueriesBulkDelete } from '../hooks/use_queries_bulk_delete'; +import { RuleActionsCell } from './rule_actions_cell'; +import { DeleteTableItemsModal } from '../delete_table_items_modal'; +import { SeverityBadge } from '../../significant_events_discovery/components/severity_badge/severity_badge'; +import { SparkPlot } from '../../../spark_plot'; +import { formatLastOccurredAt } from '../../significant_events_discovery/components/queries_table/utils'; +import { TableTitle } from '../../stream_detail_systems/table_title'; + +interface RulesTableProps { + definition: Streams.all.Definition; + rules: KnowledgeIndicator[]; + occurrencesByQueryId: Record>; + searchTerm: string; + onViewDetails: (knowledgeIndicator: KnowledgeIndicator) => void; +} + +export function RulesTable({ + definition, + rules, + occurrencesByQueryId, + searchTerm, + onViewDetails, +}: RulesTableProps) { + const [selectedRules, setSelectedRules] = useState([]); + const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 10 }); + const [rulesToDelete, setRulesToDelete] = useState([]); + const { deleteRulesInBulk, isDeleting } = useQueriesBulkDelete({ + definition, + onSuccess: () => { + setSelectedRules([]); + setRulesToDelete([]); + }, + }); + + const filteredRules = useMemo(() => { + const normalizedSearchTerm = searchTerm.trim().toLowerCase(); + + if (!normalizedSearchTerm) { + return rules; + } + + return rules.filter((rule) => { + if (rule.kind !== 'query') { + return false; + } + + const title = (rule.query.title ?? '').toLowerCase(); + return title.includes(normalizedSearchTerm); + }); + }, [rules, searchTerm]); + + useEffect(() => { + setPagination((currentPagination) => { + if (currentPagination.pageIndex === 0) { + return currentPagination; + } + + return { + ...currentPagination, + pageIndex: 0, + }; + }); + }, [searchTerm]); + + const isSelectionActionsDisabled = selectedRules.length === 0; + + const handleTableChange = useCallback(({ page }: CriteriaWithPagination) => { + if (!page) { + return; + } + + setPagination({ + pageIndex: page.index, + pageSize: page.size, + }); + }, []); + + const columns = useMemo>>( + () => [ + { + name: RULES_TABLE_RULES_COLUMN_LABEL, + render: (item: KnowledgeIndicator) => { + if (item.kind !== 'query') { + return null; + } + + return ( + + + onViewDetails(item)} + /> + + + {item.query.title || item.query.id} + + + ); + }, + }, + { + name: RULES_TABLE_SEVERITY_COLUMN_LABEL, + align: 'left', + width: '120px', + render: (item: KnowledgeIndicator) => + item.kind === 'query' ? : null, + }, + { + name: RULES_TABLE_LAST_OCCURRED_COLUMN_LABEL, + align: 'left', + width: '220px', + render: (item: KnowledgeIndicator) => { + if (item.kind !== 'query') { + return null; + } + + const occurrences = occurrencesByQueryId[item.query.id]; + if (!occurrences) { + return null; + } + + return formatLastOccurredAt(occurrences); + }, + }, + { + name: RULES_TABLE_EVENTS_COLUMN_LABEL, + width: '160px', + align: 'center', + render: (item: KnowledgeIndicator) => { + if (item.kind !== 'query') { + return null; + } + + const occurrences = occurrencesByQueryId[item.query.id]; + if (!occurrences) { + return null; + } + + return ( + + ); + }, + }, + { + name: RULES_TABLE_ACTIONS_COLUMN_LABEL, + width: '80px', + align: 'right', + render: (item: KnowledgeIndicator) => ( + setRulesToDelete([rule])} + /> + ), + }, + ], + [isDeleting, occurrencesByQueryId, onViewDetails] + ); + + return ( + <> + + + + + + setSelectedRules([])} + > + {RULES_TABLE_CLEAR_SELECTION_LABEL} + + + + { + setRulesToDelete(selectedRules); + }} + > + {RULES_TABLE_DELETE_BULK_ACTION_LABEL} + + + + + + + items={filteredRules} + itemId={(item) => (item.kind === 'query' ? item.query.id : item.feature.uuid)} + columns={columns} + selection={{ + selected: selectedRules, + onSelectionChange: setSelectedRules, + }} + pagination={{ + pageIndex: pagination.pageIndex, + pageSize: pagination.pageSize, + pageSizeOptions: [10, 25, 50], + }} + onTableChange={handleTableChange} + tableCaption={i18n.translate('xpack.streams.rulesTable.tableCaption', { + defaultMessage: 'Rules', + })} + /> + {rulesToDelete.length > 0 ? ( + setRulesToDelete([])} + onConfirm={() => { + void deleteRulesInBulk( + rulesToDelete + .filter( + (item): item is Extract => + item.kind === 'query' + ) + .map((item) => item.query.id) + ); + }} + isLoading={isDeleting} + /> + ) : null} + + ); +} + +const RULES_TABLE_RULES_COLUMN_LABEL = i18n.translate( + 'xpack.streams.rulesTable.columns.rulesLabel', + { + defaultMessage: 'Rules', + } +); + +const RULES_TABLE_SEVERITY_COLUMN_LABEL = i18n.translate( + 'xpack.streams.rulesTable.columns.severityLabel', + { + defaultMessage: 'Severity', + } +); + +const RULES_TABLE_LAST_OCCURRED_COLUMN_LABEL = i18n.translate( + 'xpack.streams.rulesTable.columns.lastOccurredLabel', + { + defaultMessage: 'Last occurred', + } +); + +const RULES_TABLE_EVENTS_COLUMN_LABEL = i18n.translate( + 'xpack.streams.rulesTable.columns.eventsLabel', + { + defaultMessage: 'Events', + } +); + +const RULES_TABLE_ACTIONS_COLUMN_LABEL = i18n.translate( + 'xpack.streams.rulesTable.columns.actionsLabel', + { + defaultMessage: 'Actions', + } +); + +const RULES_TABLE_OCCURRENCES_TOOLTIP_NAME = i18n.translate( + 'xpack.streams.rulesTable.occurrencesTooltipName', + { + defaultMessage: 'Detected event occurrences', + } +); + +const RULES_TABLE_CLEAR_SELECTION_LABEL = i18n.translate( + 'xpack.streams.rulesTable.clearSelectionLabel', + { + defaultMessage: 'Clear selection', + } +); + +const RULES_TABLE_DELETE_BULK_ACTION_LABEL = i18n.translate( + 'xpack.streams.rulesTable.deleteBulkActionLabel', + { + defaultMessage: 'Delete selected', + } +); + +const RULES_TABLE_LABEL = i18n.translate('xpack.streams.rulesTable.label', { + defaultMessage: 'Rules', +}); + +const DELETE_RULES_MODAL_TITLE = (count: number) => + i18n.translate('xpack.streams.deleteRulesModal.title', { + defaultMessage: + 'Are you sure you want to delete {count, plural, one {this rule} other {these rules}}?', + values: { count }, + }); + +const VIEW_DETAILS_ARIA_LABEL = i18n.translate('xpack.streams.rulesTable.viewDetailsAriaLabel', { + defaultMessage: 'View details', +}); diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/severity_badge/severity_badge.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/severity_badge/severity_badge.tsx new file mode 100644 index 0000000000000..e3aa4adaf5e68 --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/severity_badge/severity_badge.tsx @@ -0,0 +1,75 @@ +/* + * 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 { EuiHealth, type EuiBadgeProps } from '@elastic/eui'; +import React from 'react'; +import { i18n } from '@kbn/i18n'; + +type Severity = 'low' | 'medium' | 'high' | 'critical'; + +export const SIGNIFICANT_EVENT_SEVERITY: Record< + Severity, + { color: EuiBadgeProps['color']; label: string; defaultValue: number } +> = { + low: { + color: '#5a6d8c', + label: i18n.translate('xpack.streams.significantEventsTable.severityBadge.lowLabel', { + defaultMessage: 'Low', + }), + defaultValue: 20, + }, + medium: { + color: '#facb3d', + label: i18n.translate('xpack.streams.significantEventsTable.severityBadge.mediumLabel', { + defaultMessage: 'Medium', + }), + defaultValue: 50, + }, + high: { + color: '#ed6723', + label: i18n.translate('xpack.streams.significantEventsTable.severityBadge.highLabel', { + defaultMessage: 'High', + }), + defaultValue: 70, + }, + critical: { + color: 'danger', + label: i18n.translate('xpack.streams.significantEventsTable.severityBadge.criticalLabel', { + defaultMessage: 'Critical', + }), + defaultValue: 90, + }, +}; + +export const scoreSeverity = (score: number): Severity => { + if (score < 40) { + return 'low'; + } else if (score < 60) { + return 'medium'; + } else if (score < 80) { + return 'high'; + } + return 'critical'; +}; + +export function SeverityBadge({ score }: { score?: number }) { + if (!score) { + return ( + + {i18n.translate('xpack.streams.significantEventsTable.severityBadge.noSeverity', { + defaultMessage: 'None', + })} + + ); + } + const { color, label } = SIGNIFICANT_EVENT_SEVERITY[scoreSeverity(score)]; + return ( + + {label} + + ); +} diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/severity_selector/index.ts b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/severity_selector/index.ts new file mode 100644 index 0000000000000..523c51d0612d1 --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/severity_selector/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 { SeveritySelector } from './severity_selector'; diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/severity_selector/severity_selector.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/severity_selector/severity_selector.tsx new file mode 100644 index 0000000000000..680fd259b7d3a --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/severity_selector/severity_selector.tsx @@ -0,0 +1,63 @@ +/* + * 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 { EuiSuperSelect } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { + SeverityBadge, + SIGNIFICANT_EVENT_SEVERITY, + scoreSeverity, +} from '../severity_badge/severity_badge'; + +export function SeveritySelector({ + severityScore, + onChange, + disabled, +}: { + severityScore: number | undefined; + onChange: (score: number | undefined) => void; + disabled?: boolean; +}) { + const severityOptions = [ + { + value: -1, + inputDisplay: , + }, + ...Object.values(SIGNIFICANT_EVENT_SEVERITY).map((severity) => ({ + value: severity.defaultValue, + inputDisplay: , + })), + ].reverse(); + + return ( + onChange(value === -1 ? undefined : value)} + placeholder={i18n.translate( + 'xpack.streams.addSignificantEventFlyout.manualFlow.severityPlaceholder', + { defaultMessage: 'Select severity' } + )} + fullWidth + /> + ); +} + +const SEVERITY_SELECTOR_ARIA_LABEL = i18n.translate( + 'xpack.streams.significantEvents.severitySelector.ariaLabel', + { + defaultMessage: 'Select severity', + } +); diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/significant_events_table.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/significant_events_table.tsx deleted file mode 100644 index 96f118584101f..0000000000000 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_significant_events_view/significant_events_table.tsx +++ /dev/null @@ -1,246 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import type { EuiBasicTableColumn } from '@elastic/eui'; -import { EuiLink, EuiConfirmModal } from '@elastic/eui'; -import { EuiCodeBlock } from '@elastic/eui'; -import { EuiBasicTable } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React, { useMemo, useState } from 'react'; -import type { TickFormatter } from '@elastic/charts'; -import type { StreamQuery, Streams } from '@kbn/streams-schema'; -import { DISCOVER_APP_LOCATOR } from '@kbn/deeplinks-analytics/constants'; -import type { DiscoverAppLocatorParams } from '@kbn/discover-plugin/common'; -import type { SignificantEventItem } from '../../../hooks/sig_events/use_fetch_significant_events'; -import { useKibana } from '../../../hooks/use_kibana'; -import { formatChangePoint } from './utils/change_point'; -import { SignificantEventsHistogramChart } from './significant_events_histogram'; -import { buildDiscoverParams } from '../significant_events_discovery/utils/discover_helpers'; -import { useTimefilter } from '../../../hooks/use_timefilter'; -import { SeverityBadge } from '../significant_events_discovery/components/severity_badge/severity_badge'; - -export function SignificantEventsTable({ - definition, - items, - onDeleteClick, - onEditClick, - xFormatter, - loading, -}: { - loading?: boolean; - definition: Streams.all.Definition; - items: SignificantEventItem[]; - onDeleteClick?: (query: SignificantEventItem) => Promise; - onEditClick?: (query: SignificantEventItem) => void; - xFormatter: TickFormatter; -}) { - const { share } = useKibana().dependencies.start; - const { timeState } = useTimefilter(); - - const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false); - const [selectedDeleteItem, setSelectedDeleteItem] = useState(); - const [isDeleteLoading, setIsDeleteLoading] = useState(false); - - const discoverLocator = share.url.locators.get(DISCOVER_APP_LOCATOR); - const maxYValue = useMemo( - () => items.reduce((max, item) => Math.max(max, ...item.occurrences.map(({ y }) => y)), 0), - [items] - ); - - const columns: Array> = [ - { - field: 'title', - name: i18n.translate('xpack.streams.significantEventsTable.titleColumnTitle', { - defaultMessage: 'Title', - }), - render: (_, record) => - discoverLocator ? ( - - {record.query.title} - - ) : ( - record.query.title - ), - }, - { - field: 'query', - name: i18n.translate('xpack.streams.significantEventsTable.queryText', { - defaultMessage: 'Query', - }), - render: (query: StreamQuery) => { - if (!query.esql.query) { - return '--'; - } - - return ( - - {query.esql.query} - - ); - }, - }, - { - field: 'query', - name: i18n.translate('xpack.streams.significantEventsTable.severityColumnTitle', { - defaultMessage: 'Severity', - }), - render: (query: StreamQuery) => { - return ; - }, - }, - { - field: 'occurrences', - name: i18n.translate('xpack.streams.significantEventsTable.occurrencesColumnTitle', { - defaultMessage: 'Occurrences', - }), - render: (_, item) => { - const change = formatChangePoint(item); - - return ( - - ); - }, - }, - { - name: i18n.translate('xpack.streams.significantEventsTable.actionsColumnTitle', { - defaultMessage: 'Actions', - }), - actions: [ - { - name: i18n.translate('xpack.streams.significantEventsTable.openInDiscoverActionTitle', { - defaultMessage: 'Open in Discover', - }), - type: 'icon', - icon: 'discoverApp', - description: i18n.translate( - 'xpack.streams.significantEventsTable.openInDiscoverActionDescription', - { - defaultMessage: 'Open query in Discover', - } - ), - enabled: () => discoverLocator !== undefined, - onClick: (item) => { - discoverLocator?.navigate(buildDiscoverParams(item.query, definition, timeState)); - }, - isPrimary: true, - 'data-test-subj': 'significant_events_table_open_in_discover_action', - }, - { - icon: 'pencil', - type: 'icon', - name: i18n.translate('xpack.streams.significantEventsTable.editQueryActionTitle', { - defaultMessage: 'Edit', - }), - description: i18n.translate( - 'xpack.streams.significantEventsTable.editQueryActionDescription', - { - defaultMessage: 'Edit query', - } - ), - isPrimary: true, - onClick: (item) => { - onEditClick?.(item); - }, - 'data-test-subj': 'significant_events_table_edit_query_action', - }, - { - icon: 'trash', - type: 'icon', - color: 'danger', - name: i18n.translate('xpack.streams.significantEventsTable.removeQueryActionTitle', { - defaultMessage: 'Delete', - }), - description: i18n.translate( - 'xpack.streams.significantEventsTable.removeQueryActionDescription', - { - defaultMessage: 'Remove query from stream', - } - ), - onClick: (item) => { - setIsDeleteModalVisible(true); - setSelectedDeleteItem(item); - }, - }, - ], - }, - ]; - - return ( - <> - - {isDeleteModalVisible && selectedDeleteItem && ( - setIsDeleteModalVisible(false)} - onConfirm={() => { - setIsDeleteLoading(true); - onDeleteClick?.(selectedDeleteItem).finally(() => { - setIsDeleteModalVisible(false); - setSelectedDeleteItem(undefined); - setIsDeleteLoading(false); - }); - }} - cancelButtonText={i18n.translate( - 'xpack.streams.significantEventsTable.euiConfirmModal.cancelButtonLabel', - { defaultMessage: 'Cancel' } - )} - confirmButtonText={i18n.translate( - 'xpack.streams.significantEventsTable.euiConfirmModal.deleteSignificantEventButtonLabel', - { defaultMessage: 'Delete significant event' } - )} - isLoading={isDeleteLoading} - buttonColor="danger" - defaultFocusedButton="confirm" - > -

- {i18n.translate( - 'xpack.streams.significantEventsTable.euiConfirmModal.deleteSignificantEventMessage', - { - defaultMessage: - 'Are you sure you want to delete the selected significant event? This action cannot be undone.', - } - )} -

-
- )} - - ); -} diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_systems/stream_discovery_configuration.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_systems/stream_discovery_configuration.tsx deleted file mode 100644 index 9badeb8f7e593..0000000000000 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_systems/stream_discovery_configuration.tsx +++ /dev/null @@ -1,114 +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 React, { useState, useCallback } from 'react'; -import { i18n } from '@kbn/i18n'; -import type { Feature, Streams } from '@kbn/streams-schema'; -import { EuiPanel, EuiText, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; -import { useStreamFeatures } from '../../../hooks/sig_events/use_stream_features'; -import { StreamFeaturesAccordion } from './stream_features/stream_features_accordion'; -import { Row } from '../../stream_management/data_management/stream_detail_management/advanced_view/row'; -import { FeatureIdentificationControl } from '../stream_detail_significant_events_view/feature_identification_control'; -import type { AIFeatures } from '../../../hooks/use_ai_features'; - -interface StreamDiscoveryConfigurationProps { - definition: Streams.all.Definition; - aiFeatures: AIFeatures | null; -} - -export function StreamDiscoveryConfiguration({ - definition, - aiFeatures, -}: StreamDiscoveryConfigurationProps) { - const { - features: existingFeatures, - excludedFeatures, - refreshFeatures, - featuresLoading, - } = useStreamFeatures(definition); - - const [isIdentifyingFeatures, setIsIdentifyingFeatures] = useState(false); - const [selectedFeature, setSelectedFeature] = useState(null); - - const onSelectFeature = useCallback((feature: Feature | null) => { - setSelectedFeature(feature); - }, []); - - const handleFeatureTaskStart = useCallback(() => { - setIsIdentifyingFeatures(true); - setSelectedFeature(null); - }, []); - - const handleFeatureTaskEnd = useCallback(() => { - setIsIdentifyingFeatures(false); - }, []); - - return ( - - - -

- {i18n.translate('xpack.streams.streamDetailView.streamDiscoveryTitle', { - defaultMessage: 'Stream discovery', - })} -

-
-
- - - -

- {i18n.translate('xpack.streams.streamDetailView.featuresTitle', { - defaultMessage: 'Features', - })} -

-
- - - - {i18n.translate('xpack.streams.streamDetailView.featureConfigurationDescription', { - defaultMessage: - 'A stable fact about your system (such as infrastructure, technology, or service dependency) that is automatically extracted and validated from your log data. Extraction uses the last 24 hours of data.', - })} - - } - right={ - - - - - - } - /> - {(existingFeatures.length > 0 || excludedFeatures.length > 0) && ( - <> - - - - )} - -
-
- ); -} diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_systems/stream_features/feature_details_flyout.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_systems/stream_features/feature_details_flyout.tsx index 0d94d815ff541..ecdf06acc1b37 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_systems/stream_features/feature_details_flyout.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_systems/stream_features/feature_details_flyout.tsx @@ -5,35 +5,27 @@ * 2.0. */ import { - EuiBadge, EuiButtonIcon, - EuiCodeBlock, EuiContextMenuItem, EuiContextMenuPanel, - EuiDescriptionList, EuiFlexGroup, EuiFlexItem, EuiFlyout, EuiFlyoutBody, EuiFlyoutHeader, - EuiHealth, - EuiHorizontalRule, EuiIcon, EuiPopover, - EuiText, EuiTitle, useEuiTheme, useGeneratedHtmlId, } from '@elastic/eui'; import { css } from '@emotion/react'; import { i18n } from '@kbn/i18n'; -import { upperFirst } from 'lodash'; import type { Feature } from '@kbn/streams-schema'; import { useBoolean } from '@kbn/react-hooks'; import React from 'react'; -import { InfoPanel } from '../../../info_panel'; +import { KnowledgeIndicatorFeatureDetailsContent } from '../../stream_detail_significant_events_view/knowledge_indicator_details_flyout'; import { DeleteFeatureModal } from './delete_feature_modal'; -import { getConfidenceColor } from './use_stream_features_table'; interface FeatureDetailsFlyoutProps { feature: Feature; @@ -46,8 +38,6 @@ interface FeatureDetailsFlyoutProps { isRestoring?: boolean; } -const noDataPlaceholder = '-'; - export function FeatureDetailsFlyout({ feature, onClose, @@ -82,71 +72,6 @@ export function FeatureDetailsFlyout({ }; const displayTitle = feature.title ?? feature.id; - const evidence = feature.evidence?.length ? feature.evidence : []; - const tags = feature.tags?.length && feature.tags.length > 0 ? feature.tags : []; - - const generalInfoItems = [ - { - title: ID_LABEL, - description: ( - - {feature.id} - - ), - }, - { - title: TYPE_LABEL, - description: {upperFirst(feature.type)}, - }, - { - title: SUBTYPE_LABEL, - description: {feature.subtype ?? noDataPlaceholder}, - }, - { - title: PROPERTIES_LABEL, - description: ( - - {Object.entries(feature.properties) - .filter(([, value]) => typeof value === 'string') - .map(([key, value]) => ( - - {key} {value as string} - - ))} - - ), - }, - { - title: CONFIDENCE_LABEL, - description: ( - {feature.confidence} - ), - }, - { - title: TAGS_LABEL, - description: - tags.length > 0 ? ( - - {tags.map((tag: string) => ( - - {tag} - - ))} - - ) : ( - {noDataPlaceholder} - ), - }, - { - title: LAST_SEEN_LABEL, - description: {feature.last_seen || noDataPlaceholder}, - }, - { - title: EXPIRES_AT_LABEL, - description: {feature.expires_at ?? noDataPlaceholder}, - }, - ]; - return ( - - - - {generalInfoItems.map((item, index) => ( - - - {index < generalInfoItems.length - 1 && } - - ))} - - - - - {feature.description || NO_DESCRIPTION_AVAILABLE} - - - - - {evidence.length > 0 ? ( - evidence.map((item: string, index: number) => ( - - - - - - - {item} - - - {index < evidence.length - 1 && } - - )) - ) : ( - {NO_EVIDENCE_AVAILABLE} - )} - - - - - {Object.keys(feature.meta ?? {}).length === 0 ? ( - {NO_META_AVAILABLE} - ) : ( - - {JSON.stringify(feature.meta ?? {}, null, 2)} - - )} - - - - - - {JSON.stringify(feature, null, 2)} - - - - + {isDeleteModalVisible && onDelete && ( css` - &:hover, - &:focus { - text-decoration: ${textDecoration}; - } -`; - -const underlineOnHoverStyle = getUnderlineOnHoverStyle('underline'); -const noUnderlineOnHoverStyle = getUnderlineOnHoverStyle('none'); - -type TabId = 'active' | 'excluded'; - -interface StreamFeaturesAccordionProps { - definition: Streams.all.Definition; - features: Feature[]; - excludedFeatures: Feature[]; - isLoadingFeatures: boolean; - refreshFeatures: () => void; - isIdentifyingFeatures: boolean; - selectedFeature: Feature | null; - onSelectFeature: (feature: Feature | null) => void; -} - -export const StreamFeaturesAccordion = ({ - definition, - features, - excludedFeatures, - isLoadingFeatures, - refreshFeatures, - isIdentifyingFeatures, - selectedFeature, - onSelectFeature, -}: StreamFeaturesAccordionProps) => { - const [selectedTab, setSelectedTab] = useState('active'); - const totalCount = features.length + excludedFeatures.length; - - const handleTabChange = useCallback( - (tab: TabId) => { - onSelectFeature(null); - setSelectedTab(tab); - }, - [onSelectFeature] - ); - - return ( - - - {BUTTON_LABEL} - - - {totalCount} - - - } - buttonProps={{ css: noUnderlineOnHoverStyle }} - > - - - handleTabChange('active')} - append={{features.length}} - > - {ACTIVE_TAB_LABEL} - - handleTabChange('excluded')} - append={{excludedFeatures.length}} - > - {EXCLUDED_TAB_LABEL} - - - - - ); -}; - -const BUTTON_LABEL = i18n.translate('xpack.streams.streamFeaturesAccordion.buttonLabel', { - defaultMessage: 'Stream features', -}); - -const ACTIVE_TAB_LABEL = i18n.translate('xpack.streams.streamFeaturesAccordion.activeTab', { - defaultMessage: 'Active', -}); - -const EXCLUDED_TAB_LABEL = i18n.translate('xpack.streams.streamFeaturesAccordion.excludedTab', { - defaultMessage: 'Excluded', -}); diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_systems/stream_features/stream_features_table.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_systems/stream_features/stream_features_table.tsx deleted file mode 100644 index 1f146fda22844..0000000000000 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/stream_detail_systems/stream_features/stream_features_table.tsx +++ /dev/null @@ -1,165 +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 React, { useCallback } from 'react'; -import { - EuiHorizontalRule, - EuiSpacer, - EuiFlexGroup, - EuiFlexItem, - EuiInMemoryTable, - EuiButtonEmpty, - EuiPanel, -} from '@elastic/eui'; -import type { Feature, Streams } from '@kbn/streams-schema'; -import { FeatureDetailsFlyout } from './feature_details_flyout'; -import { DeleteFeatureModal } from './delete_feature_modal'; -import { - useStreamFeaturesTable, - TABLE_CAPTION_LABEL, - CLEAR_SELECTION, - type FeaturesTableMode, -} from './use_stream_features_table'; -import { TableTitle } from '../table_title'; - -interface StreamFeaturesTableProps { - definition: Streams.all.Definition; - isLoadingFeatures: boolean; - features: Feature[]; - refreshFeatures: () => void; - isIdentifyingFeatures: boolean; - selectedFeature: Feature | null; - onSelectFeature: (feature: Feature | null) => void; - mode: FeaturesTableMode; -} - -export function StreamFeaturesTable({ - definition, - isLoadingFeatures, - features, - refreshFeatures, - isIdentifyingFeatures, - selectedFeature, - onSelectFeature, - mode, -}: StreamFeaturesTableProps) { - const { - pagination, - selectedFeatures, - setSelectedFeatures, - isBulkDeleteModalVisible, - isIdentifyingFeatures: isTableDisabled, - hideBulkDeleteModal, - handleBulkDelete, - isBulkDeleting, - clearSelection, - handleTableChange, - columns, - noItemsMessage, - bulkActions, - flyoutActions, - label, - items, - } = useStreamFeaturesTable({ - definition, - features, - refreshFeatures, - isIdentifyingFeatures, - selectedFeature, - onSelectFeature, - mode, - }); - - const handleCloseFlyout = useCallback(() => { - onSelectFeature(null); - }, [onSelectFeature]); - - const isSelectionActionsDisabled = - selectedFeatures.length === 0 || isLoadingFeatures || isTableDisabled; - - return ( - - - - - - - - {CLEAR_SELECTION} - - - {bulkActions.map((action) => ( - - - {action.label} - - - ))} - - - - !isTableDisabled, - }} - /> - {selectedFeature && ( - - )} - {isBulkDeleteModalVisible && selectedFeatures.length > 0 && ( - - )} - - ); -} diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/stream_management/data_management/stream_detail_management/advanced_view/classic_advanced_view.test.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/stream_management/data_management/stream_detail_management/advanced_view/classic_advanced_view.test.tsx index 57ae1e44acb6d..15950ec9f2a39 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/stream_management/data_management/stream_detail_management/advanced_view/classic_advanced_view.test.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/stream_management/data_management/stream_detail_management/advanced_view/classic_advanced_view.test.tsx @@ -215,26 +215,7 @@ describe('ClassicAdvancedView', () => { expect(screen.getByText('Stream description')).toBeInTheDocument(); }); - it('should render Stream discovery panel when significantEvents feature is enabled and available', () => { - mockUseStreamsPrivileges.mockReturnValue({ - features: { - significantEvents: { enabled: true, available: true }, - }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any); - - renderWithProviders( - - ); - - // Check the Stream discovery panel title is rendered - expect(screen.getByText('Stream discovery')).toBeInTheDocument(); - }); - - it('should NOT render Stream description or Stream discovery when significantEvents is disabled', () => { + it('should NOT render Stream description when significantEvents is disabled', () => { mockUseStreamsPrivileges.mockReturnValue({ features: { significantEvents: { enabled: false, available: true }, @@ -250,10 +231,9 @@ describe('ClassicAdvancedView', () => { ); expect(screen.queryByText('Stream description')).not.toBeInTheDocument(); - expect(screen.queryByText('Stream discovery')).not.toBeInTheDocument(); }); - it('should NOT render Stream description or Stream discovery when significantEvents is enabled but not available (basic license)', () => { + it('should NOT render Stream description when significantEvents is enabled but not available (basic license)', () => { mockUseStreamsPrivileges.mockReturnValue({ features: { significantEvents: { enabled: true, available: false }, @@ -270,10 +250,9 @@ describe('ClassicAdvancedView', () => { // These components require enterprise license and should NOT render with basic license expect(screen.queryByText('Stream description')).not.toBeInTheDocument(); - expect(screen.queryByText('Stream discovery')).not.toBeInTheDocument(); }); - it('should NOT render Stream description or Stream discovery when significantEvents is undefined', () => { + it('should NOT render Stream description when significantEvents is undefined', () => { mockUseStreamsPrivileges.mockReturnValue({ features: { significantEvents: undefined, @@ -289,10 +268,9 @@ describe('ClassicAdvancedView', () => { ); expect(screen.queryByText('Stream description')).not.toBeInTheDocument(); - expect(screen.queryByText('Stream discovery')).not.toBeInTheDocument(); }); - it('should NOT render Stream description or Stream discovery when significantEvents available is undefined', () => { + it('should NOT render Stream description when significantEvents available is undefined', () => { mockUseStreamsPrivileges.mockReturnValue({ features: { significantEvents: { enabled: true, available: undefined }, @@ -308,7 +286,6 @@ describe('ClassicAdvancedView', () => { ); expect(screen.queryByText('Stream description')).not.toBeInTheDocument(); - expect(screen.queryByText('Stream discovery')).not.toBeInTheDocument(); }); }); @@ -478,8 +455,6 @@ describe('ClassicAdvancedView', () => { // Stream description expect(screen.getByText('Stream description')).toBeInTheDocument(); - // Stream discovery (contains Features and Systems) - expect(screen.getByText('Stream discovery')).toBeInTheDocument(); // Index Configuration expect(screen.getByText('Index Configuration')).toBeInTheDocument(); // Elasticsearch assets diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/stream_management/data_management/stream_detail_management/advanced_view/classic_advanced_view.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/stream_management/data_management/stream_detail_management/advanced_view/classic_advanced_view.tsx index 40f6eb4c521d1..ce29f874066ab 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/stream_management/data_management/stream_detail_management/advanced_view/classic_advanced_view.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/stream_management/data_management/stream_detail_management/advanced_view/classic_advanced_view.tsx @@ -12,7 +12,6 @@ import { StreamDescription } from '../../../../sig_events/stream_detail_systems/ import { DeleteStreamPanel } from './delete_stream'; import { useStreamsPrivileges } from '../../../../../hooks/use_streams_privileges'; import { UnmanagedElasticsearchAssets } from './unmanaged_elasticsearch_assets'; -import { StreamDiscoveryConfiguration } from '../../../../sig_events/stream_detail_systems/stream_discovery_configuration'; import { useAIFeatures } from '../../../../../hooks/use_ai_features'; export function ClassicAdvancedView({ @@ -39,8 +38,6 @@ export function ClassicAdvancedView({ aiFeatures={aiFeatures} /> - - ) : null} {!isReplicated && ( diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/stream_management/data_management/stream_detail_management/advanced_view/wired_advanced_view.test.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/stream_management/data_management/stream_detail_management/advanced_view/wired_advanced_view.test.tsx index ca23051416a73..18c4dfc975d31 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/stream_management/data_management/stream_detail_management/advanced_view/wired_advanced_view.test.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/stream_management/data_management/stream_detail_management/advanced_view/wired_advanced_view.test.tsx @@ -226,27 +226,7 @@ describe('WiredAdvancedView', () => { expect(screen.getByText('Stream description')).toBeInTheDocument(); }); - it('should render Stream discovery panel when significantEvents feature is enabled and available', () => { - mockUseStreamsPrivileges.mockReturnValue({ - features: { - contentPacks: { enabled: false }, - significantEvents: { enabled: true, available: true }, - }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any); - - renderWithProviders( - - ); - - // Check the Stream discovery panel title is rendered - expect(screen.getByText('Stream discovery')).toBeInTheDocument(); - }); - - it('should NOT render Stream description or Stream discovery when significantEvents is disabled', () => { + it('should NOT render Stream description when significantEvents is disabled', () => { mockUseStreamsPrivileges.mockReturnValue({ features: { contentPacks: { enabled: false }, @@ -263,10 +243,9 @@ describe('WiredAdvancedView', () => { ); expect(screen.queryByText('Stream description')).not.toBeInTheDocument(); - expect(screen.queryByText('Stream discovery')).not.toBeInTheDocument(); }); - it('should NOT render Stream description or Stream discovery when significantEvents is enabled but not available (basic license)', () => { + it('should NOT render Stream description when significantEvents is enabled but not available (basic license)', () => { mockUseStreamsPrivileges.mockReturnValue({ features: { contentPacks: { enabled: false }, @@ -284,10 +263,9 @@ describe('WiredAdvancedView', () => { // These components require enterprise license and should NOT render with basic license expect(screen.queryByText('Stream description')).not.toBeInTheDocument(); - expect(screen.queryByText('Stream discovery')).not.toBeInTheDocument(); }); - it('should NOT render Stream description or Stream discovery when significantEvents available is undefined', () => { + it('should NOT render Stream description when significantEvents available is undefined', () => { mockUseStreamsPrivileges.mockReturnValue({ features: { contentPacks: { enabled: false }, @@ -304,7 +282,6 @@ describe('WiredAdvancedView', () => { ); expect(screen.queryByText('Stream description')).not.toBeInTheDocument(); - expect(screen.queryByText('Stream discovery')).not.toBeInTheDocument(); }); }); @@ -447,8 +424,6 @@ describe('WiredAdvancedView', () => { expect(screen.getByText('Import & export')).toBeInTheDocument(); // Stream description expect(screen.getByText('Stream description')).toBeInTheDocument(); - // Stream discovery (contains Features and Systems) - expect(screen.getByText('Stream discovery')).toBeInTheDocument(); // Index Configuration expect(screen.getByText('Index Configuration')).toBeInTheDocument(); // Delete stream (non-root) diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/stream_management/data_management/stream_detail_management/advanced_view/wired_advanced_view.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/stream_management/data_management/stream_detail_management/advanced_view/wired_advanced_view.tsx index 758ea345091f8..7030a722674db 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/stream_management/data_management/stream_detail_management/advanced_view/wired_advanced_view.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/stream_management/data_management/stream_detail_management/advanced_view/wired_advanced_view.tsx @@ -12,7 +12,6 @@ import type { Streams } from '@kbn/streams-schema'; import { isRoot, LOGS_ROOT_STREAM_NAME } from '@kbn/streams-schema'; import { EuiCallOut, EuiSpacer } from '@elastic/eui'; import { getStreamTypeFromDefinition } from '../../../../../util/get_stream_type_from_definition'; -import { StreamDiscoveryConfiguration } from '../../../../sig_events/stream_detail_systems/stream_discovery_configuration'; import { StreamDescription } from '../../../../sig_events/stream_detail_systems/stream_description'; import { IndexConfiguration } from './index_configuration'; import { DeleteStreamPanel } from './delete_stream'; @@ -62,8 +61,6 @@ export function WiredAdvancedView({ aiFeatures={aiFeatures} /> - - )} diff --git a/x-pack/platform/plugins/shared/streams_app/public/hooks/sig_events/use_fetch_discovery_queries.ts b/x-pack/platform/plugins/shared/streams_app/public/hooks/sig_events/use_fetch_discovery_queries.ts index 3a0d013cd984a..c155968146864 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/hooks/sig_events/use_fetch_discovery_queries.ts +++ b/x-pack/platform/plugins/shared/streams_app/public/hooks/sig_events/use_fetch_discovery_queries.ts @@ -130,7 +130,7 @@ export const useFetchDiscoveryQueries = ( query, page, perPage, - status, + status?.join(','), ...deps, ], queryFn: fetchDiscoveryQueries, diff --git a/x-pack/platform/plugins/shared/streams_app/public/hooks/sig_events/use_queries_api.ts b/x-pack/platform/plugins/shared/streams_app/public/hooks/sig_events/use_queries_api.ts index fdbca298c82b1..e789de6754d38 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/hooks/sig_events/use_queries_api.ts +++ b/x-pack/platform/plugins/shared/streams_app/public/hooks/sig_events/use_queries_api.ts @@ -15,6 +15,13 @@ interface QueriesApi { promoteAll: () => Promise<{ promoted: number }>; upsertQuery: ({ query, streamName }: { query: StreamQuery; streamName: string }) => Promise; removeQuery: ({ queryId, streamName }: { queryId: string; streamName: string }) => Promise; + deleteQueriesInBulk: ({ + queryIds, + streamName, + }: { + queryIds: string[]; + streamName: string; + }) => Promise; getUnbackedQueriesCount: (signal?: AbortSignal | null) => Promise<{ count: number }>; abort: () => void; } @@ -69,6 +76,25 @@ export function useQueriesApi(): QueriesApi { } ); }, + deleteQueriesInBulk: async ({ + queryIds, + streamName, + }: { + queryIds: string[]; + streamName: string; + }) => { + await streamsRepositoryClient.fetch('POST /api/streams/{name}/queries/_bulk 2023-10-31', { + signal, + params: { + path: { + name: streamName, + }, + body: { + operations: queryIds.map((id) => ({ delete: { id } })), + }, + }, + }); + }, promoteAll: async () => { return streamsRepositoryClient.fetch('POST /internal/streams/queries/_promote', { params: { body: {} }, diff --git a/x-pack/platform/plugins/shared/streams_app/tsconfig.json b/x-pack/platform/plugins/shared/streams_app/tsconfig.json index fa9d71ca0647a..a6852bd646d59 100644 --- a/x-pack/platform/plugins/shared/streams_app/tsconfig.json +++ b/x-pack/platform/plugins/shared/streams_app/tsconfig.json @@ -108,6 +108,7 @@ "@kbn/inference-endpoint-ui-common", "@kbn/stack-connectors-plugin", "@kbn/std", + "@kbn/streams-ai", "@kbn/cps", ] }