) => {
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');
+ },
};
}