From b527589ea07ef44742729809632635b71649becd Mon Sep 17 00:00:00 2001 From: Robert Jaszczurek <92210485+rbrtj@users.noreply.github.com> Date: Wed, 13 Nov 2024 15:56:56 +0100 Subject: [PATCH] [ML] AiOps: Action for adding Log Pattern embeddable to a dashboard and case (#199478) ## Summary Part of [#197247](https://github.com/elastic/kibana/issues/197247) - Added the ability to add a Log Pattern Embeddable to a dashboard and case. - Fixed the Change Point Detection embeddable in cases and added a functional test to cover this scenario. https://github.com/user-attachments/assets/d09eccc1-6738-4c8b-9a54-7c78d9ac9017 ### Checklist Delete any items that are not applicable to this PR. - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> (cherry picked from commit 4ad25cf88df179334db72a593d05ff5bc0e3eb3e) --- packages/kbn-optimizer/limits.yml | 2 +- .../aiops_log_pattern_analysis/constants.ts | 2 + .../public/cases/log_pattern_attachment.tsx | 61 +++++ ...arts_attachment.tsx => register_cases.tsx} | 32 ++- .../change_point_detection/fields_config.tsx | 2 +- .../log_categorization/attachments_menu.tsx | 243 ++++++++++++++++++ .../loading_categorization.tsx | 5 +- .../log_categorization_app_state.tsx | 23 +- .../log_categorization_for_embeddable.tsx | 13 +- .../log_categorization_page.tsx | 19 +- .../embeddable_chart_component_wrapper.tsx | 12 +- .../embeddable_pattern_analysis_factory.tsx | 2 + .../aiops/public/hooks/use_cases_modal.ts | 15 +- x-pack/plugins/aiops/public/plugin.tsx | 4 +- x-pack/plugins/aiops/server/register_cases.ts | 8 +- .../application/aiops/log_categorization.tsx | 2 + .../registered_persistable_state_trial.ts | 1 + .../apps/aiops/change_point_detection.ts | 24 ++ .../apps/aiops/log_pattern_analysis.ts | 42 +++ .../aiops/change_point_detection_page.ts | 23 ++ .../aiops/log_pattern_analysis_page.ts | 61 +++++ x-pack/test/functional/services/ml/cases.ts | 13 + 22 files changed, 575 insertions(+), 34 deletions(-) create mode 100644 x-pack/plugins/aiops/public/cases/log_pattern_attachment.tsx rename x-pack/plugins/aiops/public/cases/{register_change_point_charts_attachment.tsx => register_cases.tsx} (59%) create mode 100644 x-pack/plugins/aiops/public/components/log_categorization/attachments_menu.tsx diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index aca3a1a0c3c99..7e6d18a75dd4d 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -2,7 +2,7 @@ pageLoadAssetSize: actions: 20000 advancedSettings: 27596 aiAssistantManagementSelection: 19146 - aiops: 16000 + aiops: 16526 alerting: 106936 apm: 64385 banners: 17946 diff --git a/x-pack/packages/ml/aiops_log_pattern_analysis/constants.ts b/x-pack/packages/ml/aiops_log_pattern_analysis/constants.ts index e88944a83b8bb..ff068307425bc 100644 --- a/x-pack/packages/ml/aiops_log_pattern_analysis/constants.ts +++ b/x-pack/packages/ml/aiops_log_pattern_analysis/constants.ts @@ -5,6 +5,8 @@ * 2.0. */ +export const CASES_ATTACHMENT_LOG_PATTERN = 'aiopsPatternAnalysisEmbeddable'; + export const EMBEDDABLE_PATTERN_ANALYSIS_TYPE = 'aiopsPatternAnalysisEmbeddable' as const; export const PATTERN_ANALYSIS_DATA_VIEW_REF_NAME = 'aiopsPatternAnalysisEmbeddableDataViewId'; diff --git a/x-pack/plugins/aiops/public/cases/log_pattern_attachment.tsx b/x-pack/plugins/aiops/public/cases/log_pattern_attachment.tsx new file mode 100644 index 0000000000000..33a64d26d38ff --- /dev/null +++ b/x-pack/plugins/aiops/public/cases/log_pattern_attachment.tsx @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; +import { memoize } from 'lodash'; +import React from 'react'; +import type { PersistableStateAttachmentViewProps } from '@kbn/cases-plugin/public/client/attachment_framework/types'; +import { FIELD_FORMAT_IDS } from '@kbn/field-formats-plugin/common'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { EuiDescriptionList } from '@elastic/eui'; +import deepEqual from 'fast-deep-equal'; +import type { + PatternAnalysisProps, + PatternAnalysisSharedComponent, +} from '../shared_components/pattern_analysis'; + +export const initComponent = memoize( + (fieldFormats: FieldFormatsStart, PatternAnalysisComponent: PatternAnalysisSharedComponent) => { + return React.memo( + (props: PersistableStateAttachmentViewProps) => { + const { persistableStateAttachmentState } = props; + + const dataFormatter = fieldFormats.deserialize({ + id: FIELD_FORMAT_IDS.DATE, + }); + + const inputProps = persistableStateAttachmentState as unknown as PatternAnalysisProps; + + const listItems = [ + { + title: ( + + ), + description: `${dataFormatter.convert( + inputProps.timeRange.from + )} - ${dataFormatter.convert(inputProps.timeRange.to)}`, + }, + ]; + + return ( + <> + + + + ); + }, + (prevProps, nextProps) => + deepEqual( + prevProps.persistableStateAttachmentState, + nextProps.persistableStateAttachmentState + ) + ); + } +); diff --git a/x-pack/plugins/aiops/public/cases/register_change_point_charts_attachment.tsx b/x-pack/plugins/aiops/public/cases/register_cases.tsx similarity index 59% rename from x-pack/plugins/aiops/public/cases/register_change_point_charts_attachment.tsx rename to x-pack/plugins/aiops/public/cases/register_cases.tsx index 4dc364836d185..b3b6efaf16d28 100644 --- a/x-pack/plugins/aiops/public/cases/register_change_point_charts_attachment.tsx +++ b/x-pack/plugins/aiops/public/cases/register_cases.tsx @@ -11,10 +11,14 @@ import { FormattedMessage } from '@kbn/i18n-react'; import type { CasesPublicSetup } from '@kbn/cases-plugin/public'; import type { CoreStart } from '@kbn/core/public'; import { CASES_ATTACHMENT_CHANGE_POINT_CHART } from '@kbn/aiops-change-point-detection/constants'; -import { getChangePointDetectionComponent } from '../shared_components'; +import { CASES_ATTACHMENT_LOG_PATTERN } from '@kbn/aiops-log-pattern-analysis/constants'; +import { + getChangePointDetectionComponent, + getPatternAnalysisComponent, +} from '../shared_components'; import type { AiopsPluginStartDeps } from '../types'; -export function registerChangePointChartsAttachment( +export function registerCases( cases: CasesPublicSetup, coreStart: CoreStart, pluginStart: AiopsPluginStartDeps @@ -44,4 +48,28 @@ export function registerChangePointChartsAttachment( }), }), }); + + const LogPatternAttachmentComponent = getPatternAnalysisComponent(coreStart, pluginStart); + + cases.attachmentFramework.registerPersistableState({ + id: CASES_ATTACHMENT_LOG_PATTERN, + icon: 'machineLearningApp', + displayName: i18n.translate('xpack.aiops.logPatternAnalysis.cases.attachmentName', { + defaultMessage: 'Log pattern analysis', + }), + getAttachmentViewObject: () => ({ + event: ( + + ), + timelineAvatar: 'machineLearningApp', + children: React.lazy(async () => { + const { initComponent } = await import('./log_pattern_attachment'); + + return { default: initComponent(pluginStart.fieldFormats, LogPatternAttachmentComponent) }; + }), + }), + }); } diff --git a/x-pack/plugins/aiops/public/components/change_point_detection/fields_config.tsx b/x-pack/plugins/aiops/public/components/change_point_detection/fields_config.tsx index f967fffd45647..90d356809acf5 100644 --- a/x-pack/plugins/aiops/public/components/change_point_detection/fields_config.tsx +++ b/x-pack/plugins/aiops/public/components/change_point_detection/fields_config.tsx @@ -400,7 +400,7 @@ const FieldPanel: FC = ({ content: ( - + { diff --git a/x-pack/plugins/aiops/public/components/log_categorization/attachments_menu.tsx b/x-pack/plugins/aiops/public/components/log_categorization/attachments_menu.tsx new file mode 100644 index 0000000000000..37d0a828aa607 --- /dev/null +++ b/x-pack/plugins/aiops/public/components/log_categorization/attachments_menu.tsx @@ -0,0 +1,243 @@ +/* + * 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 { EuiContextMenuProps } from '@elastic/eui'; +import { + EuiButton, + EuiButtonIcon, + EuiContextMenu, + EuiFlexItem, + EuiForm, + EuiFormRow, + EuiPanel, + EuiPopover, + EuiSpacer, + EuiSwitch, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import type { SaveModalDashboardProps } from '@kbn/presentation-util-plugin/public'; +import { + LazySavedObjectSaveModalDashboard, + withSuspense, +} from '@kbn/presentation-util-plugin/public'; +import React, { useCallback, useState } from 'react'; +import { useMemo } from 'react'; +import type { DataView } from '@kbn/data-views-plugin/common'; +import { EMBEDDABLE_PATTERN_ANALYSIS_TYPE } from '@kbn/aiops-log-pattern-analysis/constants'; +import { useTimeRangeUpdates } from '@kbn/ml-date-picker'; +import type { PatternAnalysisEmbeddableState } from '../../embeddables/pattern_analysis/types'; +import type { RandomSamplerOption, RandomSamplerProbability } from './sampling_menu/random_sampler'; +import { useCasesModal } from '../../hooks/use_cases_modal'; +import { useAiopsAppContext } from '../../hooks/use_aiops_app_context'; + +const SavedObjectSaveModalDashboard = withSuspense(LazySavedObjectSaveModalDashboard); + +interface AttachmentsMenuProps { + randomSamplerMode: RandomSamplerOption; + randomSamplerProbability: RandomSamplerProbability; + dataView: DataView; + selectedField?: string; +} + +export const AttachmentsMenu = ({ + randomSamplerMode, + randomSamplerProbability, + dataView, + selectedField, +}: AttachmentsMenuProps) => { + const { + application: { capabilities }, + cases, + embeddable, + } = useAiopsAppContext(); + + const [applyTimeRange, setApplyTimeRange] = useState(false); + + const [dashboardAttachmentReady, setDashboardAttachmentReady] = useState(false); + const [isActionMenuOpen, setIsActionMenuOpen] = useState(false); + + const { create: canCreateCase, update: canUpdateCase } = cases?.helpers?.canUseCases() ?? { + create: false, + update: false, + }; + + const openCasesModalCallback = useCasesModal(EMBEDDABLE_PATTERN_ANALYSIS_TYPE); + + const timeRange = useTimeRangeUpdates(); + + const canEditDashboards = capabilities.dashboard.createNew; + + const onSave: SaveModalDashboardProps['onSave'] = useCallback( + ({ dashboardId, newTitle, newDescription }) => { + const stateTransfer = embeddable!.getStateTransfer(); + + const embeddableInput: Partial = { + title: newTitle, + description: newDescription, + dataViewId: dataView.id, + fieldName: selectedField, + randomSamplerMode, + randomSamplerProbability, + minimumTimeRangeOption: 'No minimum', + ...(applyTimeRange && { timeRange }), + }; + + const state = { + input: embeddableInput, + type: EMBEDDABLE_PATTERN_ANALYSIS_TYPE, + }; + + const path = dashboardId === 'new' ? '#/create' : `#/view/${dashboardId}`; + + stateTransfer.navigateToWithEmbeddablePackage('dashboards', { + state, + path, + }); + }, + [ + embeddable, + dataView.id, + selectedField, + randomSamplerMode, + randomSamplerProbability, + applyTimeRange, + timeRange, + ] + ); + + const panels = useMemo(() => { + return [ + { + id: 'attachMainPanel', + size: 's', + items: [ + ...(canEditDashboards + ? [ + { + name: i18n.translate('xpack.aiops.logCategorization.addToDashboardTitle', { + defaultMessage: 'Add to dashboard', + }), + panel: 'attachToDashboardPanel', + 'data-test-subj': 'aiopsLogPatternAnalysisAttachToDashboardButton', + }, + ] + : []), + ...(canUpdateCase || canCreateCase + ? [ + { + name: i18n.translate('xpack.aiops.logCategorization.attachToCaseLabel', { + defaultMessage: 'Add to case', + }), + 'data-test-subj': 'aiopsLogPatternAnalysisAttachToCaseButton', + onClick: () => { + setIsActionMenuOpen(false); + openCasesModalCallback({ + dataViewId: dataView.id, + fieldName: selectedField, + minimumTimeRangeOption: 'No minimum', + randomSamplerMode, + randomSamplerProbability, + timeRange, + }); + }, + }, + ] + : []), + ], + }, + { + id: 'attachToDashboardPanel', + size: 's', + title: i18n.translate('xpack.aiops.logCategorization.addToDashboardTitle', { + defaultMessage: 'Add to dashboard', + }), + content: ( + + + + + setApplyTimeRange(e.target.checked)} + /> + + + { + setIsActionMenuOpen(false); + setDashboardAttachmentReady(true); + }} + > + + + + + ), + }, + ]; + }, [ + canEditDashboards, + canUpdateCase, + canCreateCase, + applyTimeRange, + openCasesModalCallback, + dataView.id, + selectedField, + randomSamplerMode, + randomSamplerProbability, + timeRange, + ]); + + return ( + + setIsActionMenuOpen(!isActionMenuOpen)} + /> + } + isOpen={isActionMenuOpen} + closePopover={() => setIsActionMenuOpen(false)} + panelPaddingSize="none" + anchorPosition="downRight" + > + + + {dashboardAttachmentReady ? ( + setDashboardAttachmentReady(false)} + onSave={onSave} + /> + ) : null} + + ); +}; diff --git a/x-pack/plugins/aiops/public/components/log_categorization/loading_categorization.tsx b/x-pack/plugins/aiops/public/components/log_categorization/loading_categorization.tsx index 1d98325f2d987..17eac431dfba2 100644 --- a/x-pack/plugins/aiops/public/components/log_categorization/loading_categorization.tsx +++ b/x-pack/plugins/aiops/public/components/log_categorization/loading_categorization.tsx @@ -19,7 +19,7 @@ import { import { FormattedMessage } from '@kbn/i18n-react'; interface Props { - onCancel: () => void; + onCancel?: () => void; } export const LoadingCategorization: FC = ({ onCancel }) => ( @@ -46,7 +46,8 @@ export const LoadingCategorization: FC = ({ onCancel }) => ( onCancel()} + onClick={onCancel} + disabled={!onCancel} > Cancel diff --git a/x-pack/plugins/aiops/public/components/log_categorization/log_categorization_app_state.tsx b/x-pack/plugins/aiops/public/components/log_categorization/log_categorization_app_state.tsx index 85e81ec0f2996..073316455cb53 100644 --- a/x-pack/plugins/aiops/public/components/log_categorization/log_categorization_app_state.tsx +++ b/x-pack/plugins/aiops/public/components/log_categorization/log_categorization_app_state.tsx @@ -60,17 +60,22 @@ export const LogCategorizationAppState: FC = ({ showFrozenDataTierChoice, }; + const CasesContext = appContextValue.cases?.ui.getCasesContext() ?? React.Fragment; + const casesPermissions = appContextValue.cases?.helpers.canUseCases(); + return ( - - - - - - - - - + + + + + + + + + + + ); }; diff --git a/x-pack/plugins/aiops/public/components/log_categorization/log_categorization_for_embeddable/log_categorization_for_embeddable.tsx b/x-pack/plugins/aiops/public/components/log_categorization/log_categorization_for_embeddable/log_categorization_for_embeddable.tsx index 5ca3cd947f7fe..fde191c42aa3e 100644 --- a/x-pack/plugins/aiops/public/components/log_categorization/log_categorization_for_embeddable/log_categorization_for_embeddable.tsx +++ b/x-pack/plugins/aiops/public/components/log_categorization/log_categorization_for_embeddable/log_categorization_for_embeddable.tsx @@ -18,7 +18,7 @@ import type { Category } from '@kbn/aiops-log-pattern-analysis/types'; import type { CategorizationAdditionalFilter } from '@kbn/aiops-log-pattern-analysis/create_category_request'; import type { EmbeddablePatternAnalysisInput } from '@kbn/aiops-log-pattern-analysis/embeddable'; import { useTableState } from '@kbn/ml-in-memory-table/hooks/use_table_state'; -import { AIOPS_ANALYSIS_RUN_ORIGIN } from '@kbn/aiops-common/constants'; +import { AIOPS_ANALYSIS_RUN_ORIGIN, AIOPS_EMBEDDABLE_ORIGIN } from '@kbn/aiops-common/constants'; import datemath from '@elastic/datemath'; import useMountedState from 'react-use/lib/useMountedState'; import { getEsQueryConfig } from '@kbn/data-service'; @@ -354,12 +354,19 @@ export const LogCategorizationEmbeddable: FC = [lastReloadRequestTime] ); - const actions = [...getActions(false), ...getActions(true)]; + const isCasesEmbeddable = embeddingOrigin === AIOPS_EMBEDDABLE_ORIGIN.CASES; + + // When in cases, we can only show the "Filter for pattern in Discover" actions as Cases does not have full filter management. + const actions = isCasesEmbeddable + ? getActions(true) + : [...getActions(false), ...getActions(true)]; return ( <> - {(loading ?? true) === true ? : null} + {(loading ?? true) === true ? ( + + ) : null} { const actions = getActions(true); + const attachmentsMenuProps = { + dataView, + selectedField, + randomSamplerMode: randomSampler.getMode(), + randomSamplerProbability: randomSampler.getProbability(), + }; + return ( @@ -390,9 +398,14 @@ export const LogCategorizationPage: FC = () => { )} - - loadCategories()} /> - + + + loadCategories()} /> + + + + + {eventRate.length ? ( diff --git a/x-pack/plugins/aiops/public/embeddables/change_point_chart/embeddable_chart_component_wrapper.tsx b/x-pack/plugins/aiops/public/embeddables/change_point_chart/embeddable_chart_component_wrapper.tsx index 69da4be087a14..15159d7adb60c 100644 --- a/x-pack/plugins/aiops/public/embeddables/change_point_chart/embeddable_chart_component_wrapper.tsx +++ b/x-pack/plugins/aiops/public/embeddables/change_point_chart/embeddable_chart_component_wrapper.tsx @@ -6,11 +6,12 @@ */ import type { FC } from 'react'; -import React, { useEffect, useMemo } from 'react'; +import React, { useMemo } from 'react'; import { css } from '@emotion/react'; import { CHANGE_POINT_DETECTION_VIEW_TYPE } from '@kbn/aiops-change-point-detection/constants'; import { getEsQueryConfig } from '@kbn/data-service'; import { buildEsQuery } from '@kbn/es-query'; +import { EuiLoadingSpinner } from '@elastic/eui'; import type { ChangePointDetectionProps } from '../../shared_components/change_point_detection'; import { ChangePointsTable } from '../../components/change_point_detection/change_points_table'; import { @@ -48,7 +49,6 @@ export const ChartGridEmbeddableWrapper: FC = ({ splitField, partitions, onError, - onLoading, onRenderComplete, onChange, emptyState, @@ -101,10 +101,6 @@ export const ChartGridEmbeddableWrapper: FC = ({ 10000 ); - useEffect(() => { - onLoading(isLoading); - }, [onLoading, isLoading]); - const changePoints = useMemo(() => { let resultChangePoints: ChangePointAnnotation[] = results.sort((a, b) => { if (defaultSort.direction === 'asc') { @@ -125,6 +121,10 @@ export const ChartGridEmbeddableWrapper: FC = ({ return resultChangePoints; }, [results, maxSeriesToPlot, onChange]); + if (isLoading) { + return ; + } + return (
) => { diff --git a/x-pack/plugins/aiops/public/hooks/use_cases_modal.ts b/x-pack/plugins/aiops/public/hooks/use_cases_modal.ts index a59fb8983b794..8ec73a21f9bbd 100644 --- a/x-pack/plugins/aiops/public/hooks/use_cases_modal.ts +++ b/x-pack/plugins/aiops/public/hooks/use_cases_modal.ts @@ -11,11 +11,22 @@ import { AttachmentType } from '@kbn/cases-plugin/common'; import type { ChangePointEmbeddableRuntimeState } from '../embeddables/change_point_chart/types'; import type { EmbeddableChangePointChartType } from '../embeddables/change_point_chart/embeddable_change_point_chart_factory'; import { useAiopsAppContext } from './use_aiops_app_context'; +import type { EmbeddablePatternAnalysisType } from '../embeddables/pattern_analysis/embeddable_pattern_analysis_factory'; +import type { PatternAnalysisEmbeddableRuntimeState } from '../embeddables/pattern_analysis/types'; + +type SupportedEmbeddableTypes = EmbeddableChangePointChartType | EmbeddablePatternAnalysisType; + +type EmbeddableRuntimeState = + T extends EmbeddableChangePointChartType + ? ChangePointEmbeddableRuntimeState + : T extends EmbeddablePatternAnalysisType + ? PatternAnalysisEmbeddableRuntimeState + : never; /** * Returns a callback for opening the cases modal with provided attachment state. */ -export const useCasesModal = ( +export const useCasesModal = ( embeddableType: EmbeddableType ) => { const { cases } = useAiopsAppContext(); @@ -23,7 +34,7 @@ export const useCasesModal = >) => { + (persistableState: Partial, 'id'>>) => { const persistableStateAttachmentState = { ...persistableState, // Creates unique id based on the input diff --git a/x-pack/plugins/aiops/public/plugin.tsx b/x-pack/plugins/aiops/public/plugin.tsx index 5863ea03b3072..d8c3dfd4c3636 100755 --- a/x-pack/plugins/aiops/public/plugin.tsx +++ b/x-pack/plugins/aiops/public/plugin.tsx @@ -19,7 +19,7 @@ import type { } from './types'; import { registerEmbeddables } from './embeddables'; import { registerAiopsUiActions } from './ui_actions'; -import { registerChangePointChartsAttachment } from './cases/register_change_point_charts_attachment'; +import { registerCases } from './cases/register_cases'; export type AiopsCoreSetup = CoreSetup; @@ -44,7 +44,7 @@ export class AiopsPlugin } if (cases) { - registerChangePointChartsAttachment(cases, coreStart, pluginStart); + registerCases(cases, coreStart, pluginStart); } } } diff --git a/x-pack/plugins/aiops/server/register_cases.ts b/x-pack/plugins/aiops/server/register_cases.ts index 8877c2ef9b5ee..5649c88ca6327 100644 --- a/x-pack/plugins/aiops/server/register_cases.ts +++ b/x-pack/plugins/aiops/server/register_cases.ts @@ -8,6 +8,7 @@ import type { Logger } from '@kbn/core/server'; import type { CasesServerSetup } from '@kbn/cases-plugin/server'; import { CASES_ATTACHMENT_CHANGE_POINT_CHART } from '@kbn/aiops-change-point-detection/constants'; +import { CASES_ATTACHMENT_LOG_PATTERN } from '@kbn/aiops-log-pattern-analysis/constants'; export function registerCasesPersistableState(cases: CasesServerSetup | undefined, logger: Logger) { if (cases) { @@ -15,10 +16,11 @@ export function registerCasesPersistableState(cases: CasesServerSetup | undefine cases.attachmentFramework.registerPersistableState({ id: CASES_ATTACHMENT_CHANGE_POINT_CHART, }); + cases.attachmentFramework.registerPersistableState({ + id: CASES_ATTACHMENT_LOG_PATTERN, + }); } catch (error) { - logger.warn( - `AIOPs failed to register cases persistable state for ${CASES_ATTACHMENT_CHANGE_POINT_CHART}` - ); + logger.warn(`AIOPs failed to register cases persistable state`); } } } diff --git a/x-pack/plugins/ml/public/application/aiops/log_categorization.tsx b/x-pack/plugins/ml/public/application/aiops/log_categorization.tsx index fea9b0d7e8810..2dc34ba80a080 100644 --- a/x-pack/plugins/ml/public/application/aiops/log_categorization.tsx +++ b/x-pack/plugins/ml/public/application/aiops/log_categorization.tsx @@ -63,6 +63,8 @@ export const LogCategorizationPage: FC = () => { 'uiActions', 'uiSettings', 'unifiedSearch', + 'embeddable', + 'cases', ]), }} /> diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/attachments_framework/registered_persistable_state_trial.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/attachments_framework/registered_persistable_state_trial.ts index c5e5a23032f66..0969a9261df26 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/attachments_framework/registered_persistable_state_trial.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/attachments_framework/registered_persistable_state_trial.ts @@ -39,6 +39,7 @@ export default ({ getService }: FtrProviderContext): void => { ml_anomaly_charts: '23e92e824af9db6e8b8bb1d63c222e04f57d2147', ml_anomaly_swimlane: 'a3517f3e53fb041e9cbb150477fb6ef0f731bd5f', ml_single_metric_viewer: '8b9532b0a40dfdfa282e262949b82cc1a643147c', + aiopsPatternAnalysisEmbeddable: '6c2809a0c51e668d11794de0815b293fdb3a9060', }); }); }); diff --git a/x-pack/test/functional/apps/aiops/change_point_detection.ts b/x-pack/test/functional/apps/aiops/change_point_detection.ts index 22177a0a9166d..c0ac744e687b5 100644 --- a/x-pack/test/functional/apps/aiops/change_point_detection.ts +++ b/x-pack/test/functional/apps/aiops/change_point_detection.ts @@ -7,11 +7,13 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; +import { USER } from '../../services/ml/security_common'; export default function ({ getPageObjects, getService }: FtrProviderContext) { const elasticChart = getService('elasticChart'); const esArchiver = getService('esArchiver'); const aiops = getService('aiops'); + const cases = getService('cases'); // aiops lives in the ML UI so we need some related services. const ml = getService('ml'); @@ -26,6 +28,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { after(async () => { await ml.testResources.deleteDataViewByTitle('ft_ecommerce'); + await cases.api.deleteAllCases(); }); it(`loads the change point detection page`, async () => { @@ -108,5 +111,26 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { maxSeries: 1, }); }); + + it('attaches change point charts to a case', async () => { + await ml.navigation.navigateToMl(); + await elasticChart.setNewChartUiDebugFlag(true); + await aiops.changePointDetectionPage.navigateToDataViewSelection(); + await ml.jobSourceSelection.selectSourceForChangePointDetection('ft_ecommerce'); + await aiops.changePointDetectionPage.assertChangePointDetectionPageExists(); + + await aiops.changePointDetectionPage.clickUseFullDataButton(); + await aiops.changePointDetectionPage.selectMetricField(0, 'products.discount_amount'); + + const caseParams = { + title: 'ML Change Point Detection case', + description: 'Case with a change point detection attachment', + tag: 'ml_change_point_detection', + reporter: USER.ML_POWERUSER, + }; + + await aiops.changePointDetectionPage.attachChartsToCases(0, caseParams); + await ml.cases.assertCaseWithChangePointDetectionChartsAttachment(caseParams); + }); }); } diff --git a/x-pack/test/functional/apps/aiops/log_pattern_analysis.ts b/x-pack/test/functional/apps/aiops/log_pattern_analysis.ts index 4cfca6d4d82b5..b056b3d6ec8fb 100644 --- a/x-pack/test/functional/apps/aiops/log_pattern_analysis.ts +++ b/x-pack/test/functional/apps/aiops/log_pattern_analysis.ts @@ -6,6 +6,7 @@ */ import { FtrProviderContext } from '../../ftr_provider_context'; +import { USER } from '../../services/ml/security_common'; export default function ({ getPageObjects, getService }: FtrProviderContext) { const elasticChart = getService('elasticChart'); @@ -16,6 +17,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const ml = getService('ml'); const selectedField = '@message'; const totalDocCount = 14005; + const cases = getService('cases'); async function retrySwitchTab(tabIndex: number, seconds: number) { await retry.tryForTime(seconds * 1000, async () => { @@ -43,6 +45,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { after(async () => { await ml.testResources.deleteDataViewByTitle('logstash-*'); + await cases.api.deleteAllCases(); }); it(`loads the log pattern analysis page and filters in patterns in discover`, async () => { @@ -97,5 +100,44 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { // ensure the discover doc count is greater than 0 await aiops.logPatternAnalysisPage.assertDiscoverDocCountGreaterThan(0); }); + + it('attaches log pattern analysis table to a dashboard', async () => { + // Start navigation from the base of the ML app. + await ml.navigation.navigateToMl(); + await elasticChart.setNewChartUiDebugFlag(true); + await aiops.logPatternAnalysisPage.navigateToDataViewSelection(); + await ml.jobSourceSelection.selectSourceForLogPatternAnalysisDetection('logstash-*'); + await aiops.logPatternAnalysisPage.assertLogPatternAnalysisPageExists(); + + await aiops.logPatternAnalysisPage.clickUseFullDataButton(totalDocCount); + await aiops.logPatternAnalysisPage.selectCategoryField(selectedField); + await aiops.logPatternAnalysisPage.clickRunButton(); + + await aiops.logPatternAnalysisPage.attachToDashboard(); + }); + + it('attaches log pattern analysis table to a case', async () => { + // Start navigation from the base of the ML app. + await ml.navigation.navigateToMl(); + await elasticChart.setNewChartUiDebugFlag(true); + await aiops.logPatternAnalysisPage.navigateToDataViewSelection(); + await ml.jobSourceSelection.selectSourceForLogPatternAnalysisDetection('logstash-*'); + await aiops.logPatternAnalysisPage.assertLogPatternAnalysisPageExists(); + + await aiops.logPatternAnalysisPage.clickUseFullDataButton(totalDocCount); + await aiops.logPatternAnalysisPage.selectCategoryField(selectedField); + await aiops.logPatternAnalysisPage.clickRunButton(); + + const caseParams = { + title: 'ML Log pattern analysis case', + description: 'Case with a log pattern analysis attachment', + tag: 'ml_log_pattern_analysis', + reporter: USER.ML_POWERUSER, + }; + + await aiops.logPatternAnalysisPage.attachToCase(caseParams); + + await ml.cases.assertCaseWithLogPatternAnalysisAttachment(caseParams); + }); }); } diff --git a/x-pack/test/functional/services/aiops/change_point_detection_page.ts b/x-pack/test/functional/services/aiops/change_point_detection_page.ts index 79bc4c378fb1a..e4eceb5539856 100644 --- a/x-pack/test/functional/services/aiops/change_point_detection_page.ts +++ b/x-pack/test/functional/services/aiops/change_point_detection_page.ts @@ -8,6 +8,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; import { MlTableService } from '../ml/common_table_service'; +import { CreateCaseParams } from '../cases/create'; export interface DashboardAttachmentOptions { applyTimeRange: boolean; @@ -24,6 +25,7 @@ export function ChangePointDetectionPageProvider( const browser = getService('browser'); const elasticChart = getService('elasticChart'); const dashboardPage = getPageObject('dashboard'); + const cases = getService('cases'); return { async navigateToDataViewSelection() { @@ -160,6 +162,17 @@ export function ChangePointDetectionPageProvider( }); }, + async clickAttachCasesButton() { + await testSubjects.click('aiopsChangePointDetectionAttachToCaseButton'); + await retry.tryForTime(30 * 1000, async () => { + await testSubjects.existOrFail('aiopsChangePointDetectionCaseAttachmentForm'); + }); + }, + + async clickSubmitCaseAttachButton() { + await testSubjects.click('aiopsChangePointDetectionSubmitCaseAttachButton'); + }, + async assertApplyTimeRangeControl(expectedValue: boolean) { const isChecked = await testSubjects.isEuiSwitchChecked( `aiopsChangePointDetectionAttachToDashboardApplyTimeRangeSwitch` @@ -281,5 +294,15 @@ export function ChangePointDetectionPageProvider( `aiopsChangePointPanel_${index}` ); }, + + async attachChartsToCases(panelIndex: number, params: CreateCaseParams) { + await this.assertPanelExist(panelIndex); + await this.openPanelContextMenu(panelIndex); + await this.clickAttachChartsButton(); + await this.clickAttachCasesButton(); + await this.clickSubmitCaseAttachButton(); + + await cases.create.createCaseFromModal(params); + }, }; } diff --git a/x-pack/test/functional/services/aiops/log_pattern_analysis_page.ts b/x-pack/test/functional/services/aiops/log_pattern_analysis_page.ts index 558cfb0af9f0b..f1c62cf63ae5e 100644 --- a/x-pack/test/functional/services/aiops/log_pattern_analysis_page.ts +++ b/x-pack/test/functional/services/aiops/log_pattern_analysis_page.ts @@ -8,11 +8,14 @@ import expect from '@kbn/expect'; import type { FtrProviderContext } from '../../ftr_provider_context'; +import { CreateCaseParams } from '../cases/create'; export function LogPatternAnalysisPageProvider({ getService, getPageObject }: FtrProviderContext) { const retry = getService('retry'); const testSubjects = getService('testSubjects'); const comboBox = getService('comboBox'); + const dashboardPage = getPageObject('dashboard'); + const cases = getService('cases'); type RandomSamplerOption = | 'aiopsRandomSamplerOptionOnAutomatic' @@ -249,5 +252,63 @@ export function LogPatternAnalysisPageProvider({ getService, getPageObject }: Ft await testSubjects.missingOrFail('aiopsRandomSamplerOptionsFormRow', { timeout: 1000 }); }); }, + + async openAttachmentsMenu() { + await testSubjects.click('aiopsLogPatternAnalysisAttachmentsMenuButton'); + }, + + async clickAttachToDashboard() { + await testSubjects.click('aiopsLogPatternAnalysisAttachToDashboardButton'); + }, + + async clickAttachToCase() { + await testSubjects.click('aiopsLogPatternAnalysisAttachToCaseButton'); + }, + + async confirmAttachToDashboard() { + await testSubjects.click('aiopsLogPatternAnalysisAttachToDashboardSubmitButton'); + }, + + async completeSaveToDashboardForm(createNew?: boolean) { + const dashboardSelector = await testSubjects.find('add-to-dashboard-options'); + if (createNew) { + const label = await dashboardSelector.findByCssSelector( + `label[for="new-dashboard-option"]` + ); + await label.click(); + } + + await testSubjects.click('confirmSaveSavedObjectButton'); + await retry.waitForWithTimeout('Save modal to disappear', 1000, () => + testSubjects + .missingOrFail('confirmSaveSavedObjectButton') + .then(() => true) + .catch(() => false) + ); + + // make sure the dashboard page actually loaded + const dashboardItemCount = await dashboardPage.getSharedItemsCount(); + expect(dashboardItemCount).to.not.eql(undefined); + + const embeddable = await testSubjects.find('aiopsEmbeddablePatternAnalysis', 30 * 1000); + expect(await embeddable.isDisplayed()).to.eql( + true, + 'Log pattern analysis chart should be displayed in dashboard' + ); + }, + + async attachToDashboard() { + await this.openAttachmentsMenu(); + await this.clickAttachToDashboard(); + await this.confirmAttachToDashboard(); + await this.completeSaveToDashboardForm(true); + }, + + async attachToCase(params: CreateCaseParams) { + await this.openAttachmentsMenu(); + await this.clickAttachToCase(); + + await cases.create.createCaseFromModal(params); + }, }; } diff --git a/x-pack/test/functional/services/ml/cases.ts b/x-pack/test/functional/services/ml/cases.ts index 6c481df8b99a8..2245410985592 100644 --- a/x-pack/test/functional/services/ml/cases.ts +++ b/x-pack/test/functional/services/ml/cases.ts @@ -81,5 +81,18 @@ export function MachineLearningCasesProvider( expectedChartsCount ); }, + + async assertCaseWithLogPatternAnalysisAttachment(params: CaseParams) { + await this.assertBasicCaseProps(params); + await testSubjects.existOrFail('comment-persistableState-aiopsPatternAnalysisEmbeddable'); + await testSubjects.existOrFail('aiopsEmbeddablePatternAnalysis'); + await testSubjects.existOrFail('aiopsLogPatternsTable'); + }, + + async assertCaseWithChangePointDetectionChartsAttachment(params: CaseParams) { + await this.assertBasicCaseProps(params); + await testSubjects.existOrFail('comment-persistableState-aiopsChangePointChart'); + await testSubjects.existOrFail('aiopsEmbeddableChangePointChart'); + }, }; }