[ML] AIOps: UI action for Change Point Detection embeddable to open in the ML app (#176694)

## Summary

Closes #161248 

Added a UI action to open change point detection in the AIOps labs.

<img width="1137" alt="image"
src="077c0e34-0ac0-4790-8cbe-c6048ee90d22">

### Checklist

- [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
- [x] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [x] Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))
- [x] This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
- [x] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)
This commit is contained in:
Dima Arnautov 2024-02-13 16:36:09 +01:00 committed by GitHub
parent 28d46a8c02
commit 550c10dc5e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 191 additions and 3 deletions

View file

@ -17,7 +17,8 @@
"presentationUtil",
"dashboard",
"fieldFormats",
"unifiedSearch"
"unifiedSearch",
"share"
],
"optionalPlugins": [
"cases",

View file

@ -13,6 +13,7 @@ import {
} from '@kbn/ml-ui-actions/src/aiops/ui_actions';
import type { CoreStart } from '@kbn/core/public';
import { createOpenChangePointInMlAppAction } from './open_change_point_ml';
import type { AiopsPluginStartDeps } from '../types';
import { createEditChangePointChartsPanelAction } from './edit_change_point_charts_panel';
import { createCategorizeFieldAction } from '../components/log_categorization';
@ -27,6 +28,8 @@ export function registerAiopsUiActions(
coreStart,
pluginStart
);
const openChangePointInMlAppAction = createOpenChangePointInMlAppAction(coreStart, pluginStart);
// // Register actions and triggers
uiActions.addTriggerAction(CONTEXT_MENU_TRIGGER, editChangePointChartPanelAction);
@ -36,4 +39,6 @@ export function registerAiopsUiActions(
CATEGORIZE_FIELD_TRIGGER,
createCategorizeFieldAction(coreStart, pluginStart)
);
uiActions.addTriggerAction(CONTEXT_MENU_TRIGGER, openChangePointInMlAppAction);
}

View file

@ -0,0 +1,58 @@
/*
* 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 { UiActionsActionDefinition } from '@kbn/ui-actions-plugin/public';
import { i18n } from '@kbn/i18n';
import type { CoreStart } from '@kbn/core/public';
import type { AiopsPluginStartDeps } from '../types';
import type { EditChangePointChartsPanelContext } from '../embeddable/types';
import { EMBEDDABLE_CHANGE_POINT_CHART_TYPE } from '../../common/constants';
export const OPEN_CHANGE_POINT_IN_ML_APP_ACTION = 'openChangePointInMlAppAction';
export function createOpenChangePointInMlAppAction(
coreStart: CoreStart,
pluginStart: AiopsPluginStartDeps
): UiActionsActionDefinition<EditChangePointChartsPanelContext> {
return {
id: 'open-change-point-in-ml-app',
type: OPEN_CHANGE_POINT_IN_ML_APP_ACTION,
getIconType(context): string {
return 'link';
},
getDisplayName: () =>
i18n.translate('xpack.aiops.actions.openChangePointInMlAppName', {
defaultMessage: 'Open in AIOps Labs',
}),
async getHref(context): Promise<string | undefined> {
const locator = pluginStart.share.url.locators.get('ML_APP_LOCATOR')!;
const { timeRange, metricField, fn, splitField, dataViewId } = context.embeddable.getInput();
return locator.getUrl({
page: 'aiops/change_point_detection',
pageState: {
index: dataViewId,
timeRange,
fieldConfigs: [{ fn, metricField, ...(splitField ? { splitField } : {}) }],
},
});
},
async execute(context) {
if (!context.embeddable) {
throw new Error('Not possible to execute an action without the embeddable context');
}
const aiopsChangePointUrl = await this.getHref!(context);
if (aiopsChangePointUrl) {
await coreStart.application.navigateToUrl(aiopsChangePointUrl!);
}
},
async isCompatible({ embeddable }) {
return embeddable.type === EMBEDDABLE_CHANGE_POINT_CHART_TYPE;
},
};
}

View file

@ -124,6 +124,7 @@ export interface ExplorerAppState {
query?: any;
mlShowCharts?: boolean;
}
export interface ExplorerGlobalState {
ml: { jobIds: JobId[] };
time?: TimeRange;
@ -279,7 +280,8 @@ export type MlLocatorState =
| MlGenericUrlState
| NotificationsUrlState
| TrainedModelsUrlState
| MemoryUsageUrlState;
| MemoryUsageUrlState
| ChangePointDetectionUrlState;
export type MlLocatorParams = MlLocatorState & SerializableRecord;
@ -303,3 +305,18 @@ export type NotificationsUrlState = MLPageState<
typeof ML_PAGES.NOTIFICATIONS,
NotificationsQueryState | undefined
>;
export interface ChangePointDetectionQueryState {
index: string;
timeRange?: TimeRange;
fieldConfigs: Array<{
fn: string;
splitField?: string;
metricField: string;
}>;
}
export type ChangePointDetectionUrlState = MLPageState<
typeof ML_PAGES.AIOPS_CHANGE_POINT_DETECTION,
ChangePointDetectionQueryState
>;

View file

@ -0,0 +1,49 @@
/*
* 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 { setStateToKbnUrl } from '@kbn/kibana-utils-plugin/public';
import { ML_PAGES } from '../../../common/constants/locator';
import type { ChangePointDetectionUrlState } from '../../../common/types/locator';
/**
* Creates URL to the Change Point Detection page
*/
export function formatChangePointDetectionUrl(
appBasePath: string,
params: ChangePointDetectionUrlState['pageState']
): string {
let url = `${appBasePath}/${ML_PAGES.AIOPS_CHANGE_POINT_DETECTION}`;
if (!params?.fieldConfigs) {
throw new Error('Field configs are required to create a change point detection URL');
}
if (!params.index) {
throw new Error('Data view is required to create a change point detection URL');
}
url = `${url}?index=${params.index}`;
const { timeRange, fieldConfigs } = params;
const appState = {
fieldConfigs,
};
const queryState = {
time: timeRange,
};
url = setStateToKbnUrl('_g', queryState, { useHash: false, storeInHashQuery: false }, url);
url = setStateToKbnUrl(
'_a',
{ changePoint: appState },
{ useHash: false, storeInHashQuery: false },
url
);
return url;
}

View file

@ -340,5 +340,56 @@ describe('ML locator', () => {
});
});
});
describe('AIOps labs', () => {
it('should throw an error for invalid Change point detection page state', async () => {
await expect(
definition.getLocation({
page: ML_PAGES.AIOPS_CHANGE_POINT_DETECTION,
pageState: {
index: '123123',
},
})
).rejects.toThrow('Field configs are required to create a change point detection URL');
await expect(
definition.getLocation({
page: ML_PAGES.AIOPS_CHANGE_POINT_DETECTION,
pageState: {
fieldConfigs: [
{
fn: 'max',
metricField: 'CPUUtilization',
splitField: 'instance',
},
],
},
})
).rejects.toThrow('Data view is required to create a change point detection URL');
});
it('should generate valid URL for the Change point detection page', async () => {
const location = await definition.getLocation({
page: ML_PAGES.AIOPS_CHANGE_POINT_DETECTION,
pageState: {
index: 'test-index',
timeRange: { from: '2019-10-28T00:00:00.000Z', to: '2019-11-11T13:31:00.000Z' },
fieldConfigs: [
{
fn: 'max',
metricField: 'CPUUtilization',
splitField: 'instance',
},
],
},
});
expect(location).toMatchObject({
app: 'ml',
path: "/aiops/change_point_detection?index=test-index&_g=(time:(from:'2019-10-28T00:00:00.000Z',to:'2019-11-11T13:31:00.000Z'))&_a=(changePoint:(fieldConfigs:!((fn:max,metricField:CPUUtilization,splitField:instance))))",
state: {},
});
});
});
});
});

View file

@ -6,11 +6,13 @@
*/
import type { LocatorDefinition, KibanaLocation } from '@kbn/share-plugin/public';
import { formatChangePointDetectionUrl } from './formatters/aiops';
import { formatNotificationsUrl } from './formatters/notifications';
import {
DataFrameAnalyticsExplorationUrlState,
MlLocatorParams,
MlLocator,
ChangePointDetectionQueryState,
} from '../../common/types/locator';
import { ML_APP_LOCATOR, ML_PAGES } from '../../common/constants/locator';
import {
@ -77,6 +79,12 @@ export class MlLocatorDefinition implements LocatorDefinition<MlLocatorParams> {
case ML_PAGES.MEMORY_USAGE:
path = formatMemoryUsageUrl('', params.pageState);
break;
case ML_PAGES.AIOPS_CHANGE_POINT_DETECTION:
path = formatChangePointDetectionUrl(
'',
params.pageState as ChangePointDetectionQueryState
);
break;
case ML_PAGES.DATA_DRIFT_INDEX_SELECT:
case ML_PAGES.DATA_DRIFT_CUSTOM:
case ML_PAGES.DATA_DRIFT:
@ -96,7 +104,6 @@ export class MlLocatorDefinition implements LocatorDefinition<MlLocatorParams> {
case ML_PAGES.AIOPS_LOG_RATE_ANALYSIS_INDEX_SELECT:
case ML_PAGES.AIOPS_LOG_CATEGORIZATION:
case ML_PAGES.AIOPS_LOG_CATEGORIZATION_INDEX_SELECT:
case ML_PAGES.AIOPS_CHANGE_POINT_DETECTION:
case ML_PAGES.AIOPS_CHANGE_POINT_DETECTION_INDEX_SELECT:
case ML_PAGES.OVERVIEW:
case ML_PAGES.SETTINGS: