[ML] AIOps: Adds change point detection charts embeddable (#162796)

## Summary

Part of #161248 

- Adds Change point charts as an embeddable component 
- Allows attachment of selected partitions to a new / existing dashboard
![Aug-01-2023
14-46-49](cba12d6e-7e2b-481e-a788-18435a0a9840)
- Allows attachment of top N change points (with an ascending sorting by
`p_value`)
![Aug-01-2023
14-55-10](d99e119d-c428-49d0-bc76-7a33a3ce4ffd)
- Allows applying a persistent time range 
- Attachment to a case of a single change point / selected partitions
and applied time range
![Aug-01-2023
15-07-33](1c4cc131-0897-4fc4-8f45-e8118ab1f941)
- Exposes `EmbeddableChangePointChart` embeddable component as part of
the plugin start contract


### 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)
- [ ]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [ ] [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))
- [ ] If a plugin configuration key changed, check if it needs to be
allowlisted in the cloud and added to the [docker
list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)
- [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 2023-08-08 17:33:51 +03:00 committed by GitHub
parent bc9d601feb
commit b62747e085
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
42 changed files with 1815 additions and 214 deletions

View file

@ -29,3 +29,7 @@ export type LogRateAnalysisType =
* In future versions we might use a user specific seed or let the user customise it.
*/
export const RANDOM_SAMPLER_SEED = 3867412;
export const CASES_ATTACHMENT_CHANGE_POINT_CHART = 'aiopsChangePointChart';
export const EMBEDDABLE_CHANGE_POINT_CHART_TYPE = 'aiopsChangePointChart' as const;

View file

@ -13,11 +13,20 @@
"lens",
"licensing",
"uiActions",
"embeddable",
"presentationUtil",
"dashboard",
"fieldFormats"
],
"optionalPlugins": [
"cases"
],
"requiredBundles": [
"fieldFormats",
"kibanaReact",
"kibanaUtils"
"kibanaUtils",
"embeddable",
"cases"
],
"extraPublicDirs": [
"common"

View file

@ -0,0 +1,59 @@
/*
* 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 { memoize } from 'lodash';
import { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
import React, { FC } from 'react';
import { 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 { EmbeddableChangePointChartProps } from '../embeddable';
export const initComponent = memoize(
(fieldFormats: FieldFormatsStart, EmbeddableComponent: FC<EmbeddableChangePointChartProps>) => {
return React.memo(
(props: PersistableStateAttachmentViewProps) => {
const { persistableStateAttachmentState } = props;
const dataFormatter = fieldFormats.deserialize({
id: FIELD_FORMAT_IDS.DATE,
});
const inputProps =
persistableStateAttachmentState as unknown as EmbeddableChangePointChartProps;
const listItems = [
{
title: (
<FormattedMessage
id="xpack.aiops.changePointDetection.cases.timeRangeLabel"
defaultMessage="Time range"
/>
),
description: `${dataFormatter.convert(
inputProps.timeRange.from
)} - ${dataFormatter.convert(inputProps.timeRange.to)}`,
},
];
return (
<>
<EuiDescriptionList compressed type={'inline'} listItems={listItems} />
<EmbeddableComponent {...inputProps} />
</>
);
},
(prevProps, nextProps) =>
deepEqual(
prevProps.persistableStateAttachmentState,
nextProps.persistableStateAttachmentState
)
);
}
);

View file

@ -0,0 +1,47 @@
/*
* 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 { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import type { CasesUiSetup } from '@kbn/cases-plugin/public';
import type { CoreStart } from '@kbn/core/public';
import { CASES_ATTACHMENT_CHANGE_POINT_CHART } from '../../common/constants';
import { getEmbeddableChangePointChart } from '../embeddable/embeddable_change_point_chart_component';
import { AiopsPluginStartDeps } from '../types';
export function registerChangePointChartsAttachment(
cases: CasesUiSetup,
coreStart: CoreStart,
pluginStart: AiopsPluginStartDeps
) {
const EmbeddableComponent = getEmbeddableChangePointChart(coreStart, pluginStart);
cases.attachmentFramework.registerPersistableState({
id: CASES_ATTACHMENT_CHANGE_POINT_CHART,
icon: 'machineLearningApp',
displayName: i18n.translate('xpack.aiops.changePointDetection.cases.attachmentName', {
defaultMessage: 'Change point chart',
}),
getAttachmentViewObject: () => ({
event: (
<FormattedMessage
id="xpack.aiops.changePointDetection.cases.attachmentEvent"
defaultMessage="added change point chart"
/>
),
timelineAvatar: 'machineLearningApp',
children: React.lazy(async () => {
const { initComponent } = await import('./change_point_charts_attachment');
return {
default: initComponent(pluginStart.fieldFormats, EmbeddableComponent),
};
}),
}),
});
}

View file

@ -21,6 +21,7 @@ import { usePageUrlState } from '@kbn/ml-url-state';
import { useTimefilter, useTimeRangeUpdates } from '@kbn/ml-date-picker';
import { ES_FIELD_TYPES } from '@kbn/field-types';
import { type QueryDslQueryContainer } from '@kbn/data-views-plugin/common/types';
import { FilterQueryContextProvider } from '../../hooks/use_filters_query';
import { type ChangePointType, DEFAULT_AGG_FUNCTION } from './constants';
import {
createMergedEsQuery,
@ -254,7 +255,7 @@ export const ChangePointDetectionContextProvider: FC = ({ children }) => {
return (
<ChangePointDetectionContext.Provider value={value}>
{children}
<FilterQueryContextProvider>{children}</FilterQueryContextProvider>
</ChangePointDetectionContext.Provider>
);
};

View file

@ -21,7 +21,7 @@ import {
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import type { Query } from '@kbn/es-query';
import { ChartsGrid } from './charts_grid';
import { ChartsGridContainer } from './charts_grid';
import { FieldsConfig } from './fields_config';
import { useDataSource } from '../../hooks/use_data_source';
import { ChangePointTypeFilter } from './change_point_type_filter';
@ -163,7 +163,7 @@ export const ChangePointDetectionPage: FC = () => {
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<ChartsGrid changePoints={selectedChangePoints} />
<ChartsGridContainer changePoints={selectedChangePoints} />
</EuiFlyoutBody>
</EuiFlyout>
) : null}

View file

@ -5,9 +5,10 @@
* 2.0.
*/
import React, { FC } from 'react';
import React, { FC, useMemo } from 'react';
import { map } from 'rxjs/operators';
import { pick } from 'lodash';
import { EuiThemeProvider as StyledComponentsThemeProvider } from '@kbn/kibana-react-plugin/common';
import { EuiSpacer } from '@elastic/eui';
import { DataView } from '@kbn/data-views-plugin/common';
@ -15,10 +16,11 @@ import type { SavedSearch } from '@kbn/saved-search-plugin/public';
import { StorageContextProvider } from '@kbn/ml-local-storage';
import { UrlStateProvider } from '@kbn/ml-url-state';
import { Storage } from '@kbn/kibana-utils-plugin/public';
import { DatePickerContextProvider } from '@kbn/ml-date-picker';
import { DatePickerContextProvider, mlTimefilterRefresh$ } from '@kbn/ml-date-picker';
import { UI_SETTINGS } from '@kbn/data-plugin/common';
import { toMountPoint, wrapWithTheme } from '@kbn/kibana-react-plugin/public';
import { type Observable } from 'rxjs';
import { DataSourceContext } from '../../hooks/use_data_source';
import { AiopsAppContext, AiopsAppDependencies } from '../../hooks/use_aiops_app_context';
import { AIOPS_STORAGE_KEYS } from '../../types/storage';
@ -28,6 +30,7 @@ import { PageHeader } from '../page_header';
import { ChangePointDetectionPage } from './change_point_detection_page';
import { ChangePointDetectionContextProvider } from './change_point_detection_context';
import { timeSeriesDataViewWarning } from '../../application/utils/time_series_dataview_check';
import { ReloadContextProvider } from '../../hooks/use_reload';
const localStorage = new Storage(window.localStorage);
@ -57,25 +60,43 @@ export const ChangePointDetectionAppState: FC<ChangePointDetectionAppStateProps>
const warning = timeSeriesDataViewWarning(dataView, 'change_point_detection');
const reload$ = useMemo<Observable<number>>(() => {
return mlTimefilterRefresh$.pipe(map((v) => v.lastRefresh));
}, []);
if (warning !== null) {
return <>{warning}</>;
}
const PresentationContextProvider =
appDependencies.presentationUtil?.ContextProvider ?? React.Fragment;
const CasesContext = appDependencies.cases?.ui.getCasesContext() ?? React.Fragment;
const casesPermissions = appDependencies.cases?.helpers.canUseCases();
return (
<AiopsAppContext.Provider value={appDependencies}>
<UrlStateProvider>
<DataSourceContext.Provider value={{ dataView, savedSearch }}>
<StorageContextProvider storage={localStorage} storageKeys={AIOPS_STORAGE_KEYS}>
<DatePickerContextProvider {...datePickerDeps}>
<PageHeader />
<EuiSpacer />
<ChangePointDetectionContextProvider>
<ChangePointDetectionPage />
</ChangePointDetectionContextProvider>
</DatePickerContextProvider>
</StorageContextProvider>
</DataSourceContext.Provider>
</UrlStateProvider>
</AiopsAppContext.Provider>
<PresentationContextProvider>
<StyledComponentsThemeProvider>
<CasesContext owner={[]} permissions={casesPermissions!}>
<AiopsAppContext.Provider value={appDependencies}>
<UrlStateProvider>
<DataSourceContext.Provider value={{ dataView, savedSearch }}>
<StorageContextProvider storage={localStorage} storageKeys={AIOPS_STORAGE_KEYS}>
<DatePickerContextProvider {...datePickerDeps}>
<PageHeader />
<EuiSpacer />
<ReloadContextProvider reload$={reload$}>
<ChangePointDetectionContextProvider>
<ChangePointDetectionPage />
</ChangePointDetectionContextProvider>
</ReloadContextProvider>
</DatePickerContextProvider>
</StorageContextProvider>
</DataSourceContext.Provider>
</UrlStateProvider>
</AiopsAppContext.Provider>
</CasesContext>
</StyledComponentsThemeProvider>
</PresentationContextProvider>
);
};

View file

@ -19,12 +19,14 @@ import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiTableSelectionType } from '@elastic/eui/src/components/basic_table/table_types';
import { type Filter, FilterStateStore } from '@kbn/es-query';
import { NoChangePointsWarning } from './no_change_points_warning';
import { useDataSource } from '../../hooks/use_data_source';
import { useCommonChartProps } from './use_common_chart_props';
import {
type ChangePointAnnotation,
FieldConfig,
SelectedChangePoint,
useChangePointDetectionContext,
} from './change_point_detection_context';
import { type ChartComponentProps } from './chart_component';
import { useAiopsAppContext } from '../../hooks/use_aiops_app_context';
@ -92,6 +94,8 @@ export const ChangePointsTable: FC<ChangePointsTableProps> = ({
const hasActions = fieldConfig.splitField !== undefined;
const { bucketInterval } = useChangePointDetectionContext();
const columns: Array<EuiBasicTableColumn<ChangePointAnnotation>> = [
{
id: 'timestamp',
@ -122,7 +126,13 @@ export const ChangePointsTable: FC<ChangePointsTableProps> = ({
['&.euiTableCellContent']: { display: 'block', padding: 0 },
},
render: (annotation: ChangePointAnnotation) => {
return <MiniChartPreview annotation={annotation} fieldConfig={fieldConfig} />;
return (
<MiniChartPreview
annotation={annotation}
fieldConfig={fieldConfig}
interval={bucketInterval.expression}
/>
);
},
},
{
@ -283,25 +293,7 @@ export const ChangePointsTable: FC<ChangePointsTableProps> = ({
}
/>
) : (
<EuiEmptyPrompt
iconType="search"
title={
<h2>
<FormattedMessage
id="xpack.aiops.changePointDetection.noChangePointsFoundTitle"
defaultMessage="No change points found"
/>
</h2>
}
body={
<p>
<FormattedMessage
id="xpack.aiops.changePointDetection.noChangePointsFoundMessage"
defaultMessage="Detect statistically significant change points such as dips, spikes, and distribution changes in a metric. Select a metric and set a time range to start detecting change points in your data."
/>
</p>
}
/>
<NoChangePointsWarning />
)
}
/>
@ -313,10 +305,13 @@ export const MiniChartPreview: FC<ChartComponentProps> = ({ fieldConfig, annotat
lens: { EmbeddableComponent },
} = useAiopsAppContext();
const { bucketInterval } = useChangePointDetectionContext();
const { filters, query, attributes, timeRange } = useCommonChartProps({
annotation,
fieldConfig,
previewMode: true,
bucketInterval: bucketInterval.expression,
});
return (

View file

@ -6,6 +6,7 @@
*/
import React, { FC } from 'react';
import type { Filter, Query, TimeRange } from '@kbn/es-query';
import { useCommonChartProps } from './use_common_chart_props';
import { useAiopsAppContext } from '../../hooks/use_aiops_app_context';
import type { ChangePointAnnotation, FieldConfig } from './change_point_detection_context';
@ -13,33 +14,48 @@ import type { ChangePointAnnotation, FieldConfig } from './change_point_detectio
export interface ChartComponentProps {
fieldConfig: FieldConfig;
annotation: ChangePointAnnotation;
interval: string;
}
export const ChartComponent: FC<ChartComponentProps> = React.memo(({ annotation, fieldConfig }) => {
const {
lens: { EmbeddableComponent },
} = useAiopsAppContext();
export interface ChartComponentPropsAll {
fn: string;
metricField: string;
splitField?: string;
maxResults: number;
timeRange: TimeRange;
filters?: Filter[];
query?: Query;
}
const { filters, timeRange, query, attributes } = useCommonChartProps({
fieldConfig,
annotation,
});
export const ChartComponent: FC<ChartComponentProps> = React.memo(
({ annotation, fieldConfig, interval }) => {
const {
lens: { EmbeddableComponent },
} = useAiopsAppContext();
return (
<EmbeddableComponent
id={`changePointChart_${annotation.group ? annotation.group.value : annotation.label}`}
style={{ height: 350 }}
timeRange={timeRange}
query={query}
filters={filters}
// @ts-ignore
attributes={attributes}
renderMode={'view'}
executionContext={{
type: 'aiops_change_point_detection_chart',
name: 'Change point detection',
}}
disableTriggers
/>
);
});
const { filters, timeRange, query, attributes } = useCommonChartProps({
fieldConfig,
annotation,
bucketInterval: interval,
});
return (
<EmbeddableComponent
id={`changePointChart_${annotation.group ? annotation.group.value : annotation.label}`}
style={{ height: 350 }}
timeRange={timeRange}
query={query}
filters={filters}
// @ts-ignore
attributes={attributes}
renderMode={'view'}
executionContext={{
type: 'aiops_change_point_detection_chart',
name: 'Change point detection',
}}
disableTriggers
/>
);
}
);

View file

@ -23,7 +23,10 @@ import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { useTimefilter } from '@kbn/ml-date-picker';
import { type RefreshInterval } from '@kbn/data-plugin/common';
import { type SelectedChangePoint } from './change_point_detection_context';
import {
type SelectedChangePoint,
useChangePointDetectionContext,
} from './change_point_detection_context';
import { ChartComponent } from './chart_component';
const CHARTS_PER_PAGE = 6;
@ -32,11 +35,115 @@ interface ChartsGridProps {
changePoints: Record<number, SelectedChangePoint[]>;
}
export const ChartsGrid: FC<ChartsGridProps> = ({ changePoints: changePointsDict }) => {
/**
* Shared component for change point charts grid.
* Used both in AIOps UI and inside embeddable.
*
* @param changePoints
* @constructor
*/
export const ChartsGrid: FC<{ changePoints: SelectedChangePoint[]; interval: string }> = ({
changePoints,
interval,
}) => {
return (
<EuiFlexGrid
columns={changePoints.length >= 2 ? 2 : 1}
responsive
gutterSize={'m'}
css={{ width: '100%' }}
>
{changePoints.map((v, index) => {
const key = `${index}_${v.group?.value ?? 'single_metric'}_${v.fn}_${v.metricField}_${
v.timestamp
}_${v.p_value}`;
return (
<EuiFlexItem key={key}>
<EuiPanel paddingSize="s" hasBorder hasShadow={false}>
<EuiFlexGroup alignItems={'center'} justifyContent={'spaceBetween'} gutterSize={'s'}>
<EuiFlexItem grow={false}>
{v.group ? (
<EuiDescriptionList
type="inline"
listItems={[{ title: v.group.name, description: v.group.value }]}
/>
) : null}
{v.reason ? (
<EuiToolTip position="top" content={v.reason}>
<EuiIcon
tabIndex={0}
color={'warning'}
type="warning"
title={i18n.translate(
'xpack.aiops.changePointDetection.notResultsWarning',
{
defaultMessage: 'No change point agg results warning',
}
)}
/>
</EuiToolTip>
) : null}
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText color={'subdued'} size={'s'}>
{v.fn}({v.metricField})
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
<EuiHorizontalRule margin="xs" />
<EuiFlexGroup justifyContent={'spaceBetween'} alignItems={'center'}>
{v.p_value !== undefined ? (
<EuiFlexItem grow={false}>
<EuiDescriptionList
type="inline"
listItems={[
{
title: (
<FormattedMessage
id="xpack.aiops.changePointDetection.pValueLabel"
defaultMessage="p-value"
/>
),
description: v.p_value.toPrecision(3),
},
]}
/>
</EuiFlexItem>
) : null}
<EuiFlexItem grow={false}>
<EuiBadge color="hollow">{v.type}</EuiBadge>
</EuiFlexItem>
</EuiFlexGroup>
<ChartComponent
fieldConfig={{ splitField: v.splitField, fn: v.fn, metricField: v.metricField }}
annotation={v}
interval={interval}
/>
</EuiPanel>
</EuiFlexItem>
);
})}
</EuiFlexGrid>
);
};
/**
* Wrapper component for change point charts grid.
*
* @param changePointsDict
* @constructor
*/
export const ChartsGridContainer: FC<ChartsGridProps> = ({ changePoints: changePointsDict }) => {
const timefilter = useTimefilter();
const initialRefreshSetting = useRef<RefreshInterval>();
const { bucketInterval } = useChangePointDetectionContext();
useEffect(
function pauseRefreshOnMount() {
initialRefreshSetting.current = timefilter.getRefreshInterval();
@ -76,85 +183,7 @@ export const ChartsGrid: FC<ChartsGridProps> = ({ changePoints: changePointsDict
return (
<>
<EuiFlexGrid columns={resultPerPage.length >= 2 ? 2 : 1} responsive gutterSize={'m'}>
{resultPerPage.map((v, index) => {
const key = `${index}_${v.group?.value ?? 'single_metric'}_${v.fn}_${v.metricField}_${
v.timestamp
}_${v.p_value}`;
return (
<EuiFlexItem key={key}>
<EuiPanel paddingSize="s" hasBorder hasShadow={false}>
<EuiFlexGroup
alignItems={'center'}
justifyContent={'spaceBetween'}
gutterSize={'s'}
>
<EuiFlexItem grow={false}>
{v.group ? (
<EuiDescriptionList
type="inline"
listItems={[{ title: v.group.name, description: v.group.value }]}
/>
) : null}
{v.reason ? (
<EuiToolTip position="top" content={v.reason}>
<EuiIcon
tabIndex={0}
color={'warning'}
type="warning"
title={i18n.translate(
'xpack.aiops.changePointDetection.notResultsWarning',
{
defaultMessage: 'No change point agg results warning',
}
)}
/>
</EuiToolTip>
) : null}
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText color={'subdued'} size={'s'}>
{v.fn}({v.metricField})
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
<EuiHorizontalRule margin="xs" />
<EuiFlexGroup justifyContent={'spaceBetween'} alignItems={'center'}>
{v.p_value !== undefined ? (
<EuiFlexItem grow={false}>
<EuiDescriptionList
type="inline"
listItems={[
{
title: (
<FormattedMessage
id="xpack.aiops.changePointDetection.pValueLabel"
defaultMessage="p-value"
/>
),
description: v.p_value.toPrecision(3),
},
]}
/>
</EuiFlexItem>
) : null}
<EuiFlexItem grow={false}>
<EuiBadge color="hollow">{v.type}</EuiBadge>
</EuiFlexItem>
</EuiFlexGroup>
<ChartComponent
fieldConfig={{ splitField: v.splitField, fn: v.fn, metricField: v.metricField }}
annotation={v}
/>
</EuiPanel>
</EuiFlexItem>
);
})}
</EuiFlexGrid>
<ChartsGrid changePoints={resultPerPage} interval={bucketInterval.expression} />
{pagination.pageCount > 1 ? (
<EuiFlexGroup justifyContent="spaceAround">

View file

@ -10,16 +10,35 @@ import {
EuiButton,
EuiButtonIcon,
EuiCallOut,
EuiContextMenu,
EuiFieldNumber,
EuiFlexGroup,
EuiFlexItem,
EuiForm,
EuiFormRow,
EuiIcon,
EuiPanel,
EuiPopover,
EuiProgress,
EuiSpacer,
EuiSwitch,
EuiToolTip,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import type { FieldStatsServices } from '@kbn/unified-field-list/src/components/field_stats';
import { useTimefilter, useTimeRangeUpdates } from '@kbn/ml-date-picker';
import {
LazySavedObjectSaveModalDashboard,
SaveModalDashboardProps,
withSuspense,
} from '@kbn/presentation-util-plugin/public';
import { EuiContextMenuProps } from '@elastic/eui/src/components/context_menu/context_menu';
import { isDefined } from '@kbn/ml-is-defined';
import { numberValidator } from '@kbn/ml-agg-utils';
import { EMBEDDABLE_CHANGE_POINT_CHART_TYPE } from '../../../common/constants';
import { useCasesModal } from '../../hooks/use_cases_modal';
import { type EmbeddableChangePointChartInput } from '../../embeddable/embeddable_change_point_chart';
import { useDataSource } from '../../hooks/use_data_source';
import { useAiopsAppContext } from '../../hooks/use_aiops_app_context';
import { ChangePointsTable } from './change_points_table';
@ -35,9 +54,12 @@ import {
} from './change_point_detection_context';
import { useChangePointResults } from './use_change_point_agg_request';
import { useSplitFieldCardinality } from './use_split_field_cardinality';
import { MAX_SERIES } from '../../embeddable/const';
const selectControlCss = { width: '350px' };
const SavedObjectSaveModalDashboard = withSuspense(LazySavedObjectSaveModalDashboard);
/**
* Contains panels with controls and change point results.
*/
@ -93,6 +115,7 @@ export const FieldsConfig: FC = () => {
return (
<React.Fragment key={key}>
<FieldPanel
panelIndex={index}
data-test-subj={`aiopsChangePointPanel_${index}`}
fieldConfig={fieldConfig}
onChange={(value) => onChange(value, index)}
@ -121,6 +144,7 @@ export const FieldsConfig: FC = () => {
};
export interface FieldPanelProps {
panelIndex: number;
fieldConfig: FieldConfig;
removeDisabled: boolean;
onChange: (update: FieldConfig) => void;
@ -138,6 +162,7 @@ export interface FieldPanelProps {
* @constructor
*/
const FieldPanel: FC<FieldPanelProps> = ({
panelIndex,
fieldConfig,
onChange,
onRemove,
@ -145,18 +170,315 @@ const FieldPanel: FC<FieldPanelProps> = ({
onSelectionChange,
'data-test-subj': dataTestSubj,
}) => {
const { combinedQuery, requestParams } = useChangePointDetectionContext();
const {
embeddable,
application: { capabilities },
cases,
} = useAiopsAppContext();
const { dataView } = useDataSource();
const { combinedQuery, requestParams, selectedChangePoints } = useChangePointDetectionContext();
const splitFieldCardinality = useSplitFieldCardinality(fieldConfig.splitField, combinedQuery);
const [isExpanded, setIsExpanded] = useState<boolean>(true);
const [isActionMenuOpen, setIsActionMenuOpen] = useState(false);
const canEditDashboards = capabilities.dashboard?.createNew ?? false;
const { create: canCreateCase, update: canUpdateCase } = cases?.helpers?.canUseCases() ?? {
create: false,
update: false,
};
const [dashboardAttachment, setDashboardAttachment] = useState<{
applyTimeRange: boolean;
maxSeriesToPlot: number;
}>({
applyTimeRange: false,
maxSeriesToPlot: 6,
});
const [dashboardAttachmentReady, setDashboardAttachmentReady] = useState<boolean>(false);
const {
results: annotations,
isLoading: annotationsLoading,
progress,
} = useChangePointResults(fieldConfig, requestParams, combinedQuery, splitFieldCardinality);
const openCasesModalCallback = useCasesModal(EMBEDDABLE_CHANGE_POINT_CHART_TYPE);
const selectedPartitions = useMemo(() => {
return (selectedChangePoints[panelIndex] ?? []).map((v) => v.group?.value as string);
}, [selectedChangePoints, panelIndex]);
const caseAttachmentButtonDisabled =
isDefined(fieldConfig.splitField) && selectedPartitions.length === 0;
const timeRange = useTimeRangeUpdates();
const maxSeriesValidator = useMemo(
() => numberValidator({ min: 1, max: MAX_SERIES, integerOnly: true }),
[]
);
const maxSeriesInvalid = maxSeriesValidator(dashboardAttachment.maxSeriesToPlot) !== null;
const panels = useMemo<EuiContextMenuProps['panels']>(() => {
return [
{
id: 'panelActions',
size: 's',
items: [
...(canEditDashboards || canUpdateCase || canCreateCase
? [
{
name:
selectedPartitions.length > 0
? i18n.translate(
'xpack.aiops.changePointDetection.attachSelectedChartsLabel',
{
defaultMessage: 'Attach selected charts',
}
)
: i18n.translate('xpack.aiops.changePointDetection.attachChartsLabel', {
defaultMessage: 'Attach charts',
}),
icon: 'plusInCircle',
panel: 'attachMainPanel',
},
]
: []),
{
name: i18n.translate('xpack.aiops.changePointDetection.removeConfigLabel', {
defaultMessage: 'Remove configuration',
}),
icon: 'trash',
onClick: onRemove,
disabled: removeDisabled,
},
],
},
{
id: 'attachMainPanel',
size: 's',
initialFocusedItemIndex: 0,
title:
selectedPartitions.length > 0
? i18n.translate('xpack.aiops.changePointDetection.attachSelectedChartsLabel', {
defaultMessage: 'Attach selected charts',
})
: i18n.translate('xpack.aiops.changePointDetection.attachChartsLabel', {
defaultMessage: 'Attach charts',
}),
items: [
...(canEditDashboards
? [
{
name: i18n.translate('xpack.aiops.changePointDetection.attachToDashboardLabel', {
defaultMessage: 'To dashboard',
}),
panel: 'attachToDashboardPanel',
},
]
: []),
...(canUpdateCase || canCreateCase
? [
{
name: i18n.translate('xpack.aiops.changePointDetection.attachToCaseLabel', {
defaultMessage: 'To case',
}),
disabled: caseAttachmentButtonDisabled,
...(caseAttachmentButtonDisabled
? {
toolTipPosition: 'left' as const,
toolTipContent: i18n.translate(
'xpack.aiops.changePointDetection.attachToCaseTooltipContent',
{
defaultMessage: 'Select change points to attach',
}
),
}
: {}),
onClick: () => {
openCasesModalCallback({
timeRange,
fn: fieldConfig.fn,
metricField: fieldConfig.metricField,
dataViewId: dataView.id,
...(fieldConfig.splitField
? {
splitField: fieldConfig.splitField,
partitions: selectedPartitions,
}
: {}),
});
},
},
]
: []),
],
},
{
id: 'attachToDashboardPanel',
title: i18n.translate('xpack.aiops.changePointDetection.attachToDashboardTitle', {
defaultMessage: 'Attach to dashboard',
}),
size: 's',
content: (
<EuiPanel paddingSize={'s'}>
<EuiSpacer size={'s'} />
<EuiForm>
<EuiFormRow fullWidth>
<EuiSwitch
label={i18n.translate('xpack.aiops.changePointDetection.applyTimeRangeLabel', {
defaultMessage: 'Apply time range',
})}
checked={dashboardAttachment.applyTimeRange}
onChange={(e) =>
setDashboardAttachment((prevState) => {
return {
...prevState,
applyTimeRange: e.target.checked,
};
})
}
compressed
/>
</EuiFormRow>
{isDefined(fieldConfig.splitField) && selectedPartitions.length === 0 ? (
<EuiFormRow
fullWidth
isInvalid={maxSeriesInvalid}
error={
<FormattedMessage
id="xpack.aiops.changePointDetection.maxSeriesToPlotError"
defaultMessage="Max series value must be between {minValue} and {maxValue}"
values={{ minValue: 1, maxValue: MAX_SERIES }}
/>
}
label={
<EuiFlexGroup gutterSize={'xs'} alignItems={'center'}>
<EuiFlexItem grow={false}>
<FormattedMessage
id="xpack.aiops.changePointDetection.maxSeriesToPlotLabel"
defaultMessage="Max series"
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiToolTip
content={i18n.translate(
'xpack.aiops.changePointDetection.maxSeriesToPlotDescription',
{
defaultMessage: 'The maximum number of change points to visualize.',
}
)}
>
<EuiIcon type={'questionInCircle'} />
</EuiToolTip>
</EuiFlexItem>
</EuiFlexGroup>
}
>
<EuiFieldNumber
isInvalid={maxSeriesInvalid}
value={dashboardAttachment.maxSeriesToPlot}
onChange={(e) =>
setDashboardAttachment((prevState) => {
return {
...prevState,
maxSeriesToPlot: Number(e.target.value),
};
})
}
min={1}
max={MAX_SERIES}
/>
</EuiFormRow>
) : null}
<EuiSpacer size={'m'} />
<EuiButton
fill
type={'submit'}
fullWidth
onClick={setDashboardAttachmentReady.bind(null, true)}
disabled={maxSeriesInvalid}
>
<FormattedMessage
id="xpack.aiops.changePointDetection.submitDashboardAttachButtonLabel"
defaultMessage="Attach"
/>
</EuiButton>
</EuiForm>
</EuiPanel>
),
},
];
}, [
canCreateCase,
canEditDashboards,
canUpdateCase,
caseAttachmentButtonDisabled,
dashboardAttachment.applyTimeRange,
dashboardAttachment.maxSeriesToPlot,
dataView.id,
fieldConfig.fn,
fieldConfig.metricField,
fieldConfig.splitField,
onRemove,
openCasesModalCallback,
removeDisabled,
selectedPartitions,
timeRange,
maxSeriesInvalid,
]);
const onSaveCallback: SaveModalDashboardProps['onSave'] = useCallback(
({ dashboardId, newTitle, newDescription }) => {
const stateTransfer = embeddable!.getStateTransfer();
const embeddableInput: Partial<EmbeddableChangePointChartInput> = {
title: newTitle,
description: newDescription,
dataViewId: dataView.id,
metricField: fieldConfig.metricField,
splitField: fieldConfig.splitField,
fn: fieldConfig.fn,
...(dashboardAttachment.applyTimeRange ? { timeRange } : {}),
maxSeriesToPlot: dashboardAttachment.maxSeriesToPlot,
...(selectedChangePoints[panelIndex]?.length ? { partitions: selectedPartitions } : {}),
};
const state = {
input: embeddableInput,
type: EMBEDDABLE_CHANGE_POINT_CHART_TYPE,
};
const path = dashboardId === 'new' ? '#/create' : `#/view/${dashboardId}`;
stateTransfer.navigateToWithEmbeddablePackage('dashboards', {
state,
path,
});
},
[
embeddable,
dataView.id,
fieldConfig.metricField,
fieldConfig.splitField,
fieldConfig.fn,
dashboardAttachment.applyTimeRange,
dashboardAttachment.maxSeriesToPlot,
timeRange,
selectedChangePoints,
panelIndex,
selectedPartitions,
]
);
return (
<EuiPanel paddingSize="s" hasBorder hasShadow={false} data-test-subj={dataTestSubj}>
<EuiFlexGroup alignItems={'center'} justifyContent={'spaceBetween'} gutterSize={'s'}>
@ -197,15 +519,32 @@ const FieldPanel: FC<FieldPanelProps> = ({
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonIcon
disabled={removeDisabled}
aria-label={i18n.translate('xpack.aiops.changePointDetection.removeConfigLabel', {
defaultMessage: 'Remove configuration',
})}
iconType="trash"
color="danger"
onClick={onRemove}
/>
<EuiFlexGroup alignItems={'center'} justifyContent={'spaceBetween'} gutterSize={'s'}>
<EuiFlexItem grow={false}>
<EuiPopover
id={`panelContextMenu_${panelIndex}`}
button={
<EuiButtonIcon
aria-label={i18n.translate(
'xpack.aiops.changePointDetection.configActionsLabel',
{
defaultMessage: 'Context menu',
}
)}
iconType="boxesHorizontal"
color="text"
onClick={setIsActionMenuOpen.bind(null, true)}
/>
}
isOpen={isActionMenuOpen}
closePopover={setIsActionMenuOpen.bind(null, false)}
panelPaddingSize="none"
anchorPosition="downLeft"
>
<EuiContextMenu panels={panels} initialPanelId={'panelActions'} />
</EuiPopover>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
@ -218,6 +557,34 @@ const FieldPanel: FC<FieldPanelProps> = ({
onSelectionChange={onSelectionChange}
/>
) : null}
{dashboardAttachmentReady ? (
<SavedObjectSaveModalDashboard
canSaveByReference={false}
objectType={i18n.translate('xpack.aiops.changePointDetection.objectTypeLabel', {
defaultMessage: 'Change point chart',
})}
documentInfo={{
title: i18n.translate('xpack.aiops.changePointDetection.attachmentTitle', {
defaultMessage: 'Change point: {function}({metric}){splitBy}',
values: {
function: fieldConfig.fn,
metric: fieldConfig.metricField,
splitBy: fieldConfig.splitField
? i18n.translate('xpack.aiops.changePointDetection.splitByTitle', {
defaultMessage: ' split by "{splitField}"',
values: { splitField: fieldConfig.splitField },
})
: '',
},
}),
}}
onClose={() => {
setDashboardAttachmentReady(false);
}}
onSave={onSaveCallback}
/>
) : null}
</EuiPanel>
);
};

View file

@ -0,0 +1,34 @@
/*
* 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, { type FC } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiEmptyPrompt } from '@elastic/eui';
export const NoChangePointsWarning: FC = () => {
return (
<EuiEmptyPrompt
iconType="search"
title={
<h2>
<FormattedMessage
id="xpack.aiops.changePointDetection.noChangePointsFoundTitle"
defaultMessage="No change points found"
/>
</h2>
}
body={
<p>
<FormattedMessage
id="xpack.aiops.changePointDetection.noChangePointsFoundMessage"
defaultMessage="Detect statistically significant change points such as dips, spikes, and distribution changes in a metric. Select a metric and set a time range to start detecting change points in your data."
/>
</p>
}
/>
);
};

View file

@ -8,8 +8,8 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { type QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
import { i18n } from '@kbn/i18n';
import { useRefresh } from '@kbn/ml-date-picker';
import { isDefined } from '@kbn/ml-is-defined';
import { useReload } from '../../hooks/use_reload';
import { useAiopsAppContext } from '../../hooks/use_aiops_app_context';
import {
ChangePointAnnotation,
@ -102,7 +102,7 @@ function getChangePointDetectionRequestBody(
index,
size: 0,
body: {
query,
...(query ? { query } : {}),
aggregations,
},
},
@ -121,7 +121,7 @@ export function useChangePointResults(
const { dataView } = useDataSource();
const refresh = useRefresh();
const { refreshTimestamp: refresh } = useReload();
const [results, setResults] = useState<ChangePointAnnotation[]>([]);
/**
@ -164,6 +164,7 @@ export function useChangePointResults(
},
query
);
const result = await runRequest<
typeof requestPayload,
{ rawResponse: ChangePointAggResponse }

View file

@ -8,31 +8,30 @@
import moment from 'moment';
import { FilterStateStore, type TimeRange } from '@kbn/es-query';
import { type TypedLensByValueInput } from '@kbn/lens-plugin/public';
import { useTimeRangeUpdates } from '@kbn/ml-date-picker';
import { useMemo } from 'react';
import { useFilerQueryUpdates } from '../../hooks/use_filters_query';
import { fnOperationTypeMapping } from './constants';
import { useDataSource } from '../../hooks/use_data_source';
import {
ChangePointAnnotation,
FieldConfig,
useChangePointDetectionContext,
} from './change_point_detection_context';
import { ChangePointAnnotation, FieldConfig } from './change_point_detection_context';
/**
* Provides common props for the Lens Embeddable component
* based on the change point definition and currently applied filters and query.
*/
export const useCommonChartProps = ({
annotation,
fieldConfig,
previewMode = false,
bucketInterval,
}: {
fieldConfig: FieldConfig;
annotation: ChangePointAnnotation;
previewMode?: boolean;
bucketInterval: string;
}): Partial<TypedLensByValueInput> => {
const timeRange = useTimeRangeUpdates(true);
const { dataView } = useDataSource();
const { bucketInterval, resultQuery, resultFilters } = useChangePointDetectionContext();
const { filters: resultFilters, query: resultQuery, timeRange } = useFilerQueryUpdates();
/**
* In order to correctly render annotations for change points at the edges,
@ -48,6 +47,7 @@ export const useCommonChartProps = ({
const filters = useMemo(() => {
return [
...resultFilters,
// Adds a filter for change point partition value
...(annotation.group
? [
{
@ -161,7 +161,9 @@ export const useCommonChartProps = ({
outside: false,
},
],
ignoreGlobalFilters: true,
// TODO check if we need to set filter from
// the filterManager
ignoreGlobalFilters: false,
},
],
},
@ -180,7 +182,7 @@ export const useCommonChartProps = ({
isBucketed: true,
scale: 'interval',
params: {
interval: bucketInterval.expression,
interval: bucketInterval,
includeEmptyRows: true,
dropPartials: false,
},
@ -219,7 +221,7 @@ export const useCommonChartProps = ({
dataView.timeFieldName,
resultQuery,
filters,
bucketInterval.expression,
bucketInterval,
fieldConfig.fn,
fieldConfig.metricField,
gridAndLabelsVisibility,

View file

@ -0,0 +1,157 @@
/*
* 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, { FC, useState } from 'react';
import {
EuiButton,
EuiButtonEmpty,
EuiForm,
EuiFormRow,
EuiModalBody,
EuiModalFooter,
EuiModalHeader,
EuiModalHeaderTitle,
EuiFieldNumber,
EuiFieldText,
EuiModal,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { EmbeddableChangePointChartInput } from './embeddable_change_point_chart';
import { FunctionPicker } from '../components/change_point_detection/function_picker';
export const MAX_ANOMALY_CHARTS_ALLOWED = 50;
export const DEFAULT_MAX_SERIES_TO_PLOT = 6;
export interface AnomalyChartsInitializerProps {
defaultTitle: string;
initialInput?: Partial<EmbeddableChangePointChartInput>;
onCreate: (props: { panelTitle: string; maxSeriesToPlot?: number }) => void;
onCancel: () => void;
}
export const ChangePointChartInitializer: FC<AnomalyChartsInitializerProps> = ({
defaultTitle,
initialInput,
onCreate,
onCancel,
}) => {
const [panelTitle, setPanelTitle] = useState(defaultTitle);
const [maxSeriesToPlot, setMaxSeriesToPlot] = useState(
initialInput?.maxSeriesToPlot ?? DEFAULT_MAX_SERIES_TO_PLOT
);
const [fn, setFn] = useState<string>(initialInput?.fn ?? 'avg');
const isPanelTitleValid = panelTitle.length > 0;
const isMaxSeriesToPlotValid =
maxSeriesToPlot >= 1 && maxSeriesToPlot <= MAX_ANOMALY_CHARTS_ALLOWED;
const isFormValid = isPanelTitleValid && isMaxSeriesToPlotValid;
return (
<EuiModal
initialFocus="[name=panelTitle]"
onClose={onCancel}
data-test-subj={'aiopsChangePointChartEmbeddableInitializer'}
>
<EuiModalHeader>
<EuiModalHeaderTitle>
<FormattedMessage
id="xpack.aiops.embeddableChangePointChart.modalTitle"
defaultMessage="Change point charts configuration"
/>
</EuiModalHeaderTitle>
</EuiModalHeader>
<EuiModalBody>
<EuiForm>
<EuiFormRow
label={
<FormattedMessage
id="xpack.aiops.embeddableChangePointChart.panelTitleLabel"
defaultMessage="Panel title"
/>
}
isInvalid={!isPanelTitleValid}
>
<EuiFieldText
data-test-subj="panelTitleInput"
id="panelTitle"
name="panelTitle"
value={panelTitle}
onChange={(e) => setPanelTitle(e.target.value)}
isInvalid={!isPanelTitleValid}
/>
</EuiFormRow>
<EuiFormRow
label={
<FormattedMessage
id="xpack.aiops.embeddableChangePointChart.functionLabel"
defaultMessage="Function"
/>
}
>
<FunctionPicker value={fn} onChange={setFn} />
</EuiFormRow>
<EuiFormRow
isInvalid={!isMaxSeriesToPlotValid}
error={
!isMaxSeriesToPlotValid ? (
<FormattedMessage
id="xpack.aiops.embeddableChangePointChart.maxSeriesToPlotError"
defaultMessage="Maximum number of series to plot must be between 1 and 50."
/>
) : undefined
}
label={
<FormattedMessage
id="xpack.aiops.embeddableChangePointChart.maxSeriesToPlotLabel"
defaultMessage="Maximum number of series to plot"
/>
}
>
<EuiFieldNumber
data-test-subj="mlAnomalyChartsInitializerMaxSeries"
id="selectMaxSeriesToPlot"
name="selectMaxSeriesToPlot"
value={maxSeriesToPlot}
onChange={(e) => setMaxSeriesToPlot(parseInt(e.target.value, 10))}
min={1}
max={MAX_ANOMALY_CHARTS_ALLOWED}
/>
</EuiFormRow>
</EuiForm>
</EuiModalBody>
<EuiModalFooter>
<EuiButtonEmpty onClick={onCancel} data-test-subj="mlAnomalyChartsInitializerCancelButton">
<FormattedMessage
id="xpack.aiops.embeddableChangePointChart.setupModal.cancelButtonLabel"
defaultMessage="Cancel"
/>
</EuiButtonEmpty>
<EuiButton
data-test-subj="mlAnomalyChartsInitializerConfirmButton"
isDisabled={!isFormValid}
onClick={onCreate.bind(null, {
panelTitle,
maxSeriesToPlot,
})}
fill
>
<FormattedMessage
id="xpack.aiops.embeddableChangePointChart.setupModal.confirmButtonLabel"
defaultMessage="Confirm configurations"
/>
</EuiButton>
</EuiModalFooter>
</EuiModal>
);
};

View file

@ -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 const MAX_SERIES = 50;

View file

@ -0,0 +1,132 @@
/*
* 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, { Suspense } from 'react';
import ReactDOM from 'react-dom';
import {
Embeddable as AbstractEmbeddable,
EmbeddableInput,
EmbeddableOutput,
IContainer,
} from '@kbn/embeddable-plugin/public';
import { KibanaThemeProvider, toMountPoint, wrapWithTheme } from '@kbn/kibana-react-plugin/public';
import { ThemeServiceStart } from '@kbn/core-theme-browser';
import { DataPublicPluginStart, UI_SETTINGS } from '@kbn/data-plugin/public';
import { type CoreStart, IUiSettingsClient } from '@kbn/core/public';
import { DatePickerContextProvider } from '@kbn/ml-date-picker';
import { pick } from 'lodash';
import { LensPublicStart } from '@kbn/lens-plugin/public';
import { Subject } from 'rxjs';
import { EmbeddableInputTracker } from './embeddable_chart_component_wrapper';
import { EMBEDDABLE_CHANGE_POINT_CHART_TYPE } from '../../common/constants';
import { AiopsAppContext, type AiopsAppDependencies } from '../hooks/use_aiops_app_context';
import { EmbeddableChangePointChartProps } from './embeddable_change_point_chart_component';
export type EmbeddableChangePointChartInput = EmbeddableInput & EmbeddableChangePointChartProps;
export type EmbeddableChangePointChartOutput = EmbeddableOutput;
export interface EmbeddableChangePointChartDeps {
theme: ThemeServiceStart;
data: DataPublicPluginStart;
uiSettings: IUiSettingsClient;
http: CoreStart['http'];
notifications: CoreStart['notifications'];
i18n: CoreStart['i18n'];
lens: LensPublicStart;
}
export type IEmbeddableChangePointChart = typeof EmbeddableChangePointChart;
export class EmbeddableChangePointChart extends AbstractEmbeddable<
EmbeddableChangePointChartInput,
EmbeddableChangePointChartOutput
> {
public readonly type = EMBEDDABLE_CHANGE_POINT_CHART_TYPE;
private reload$ = new Subject<number>();
public reload(): void {
this.reload$.next(Date.now());
}
private node?: HTMLElement;
constructor(
private readonly deps: EmbeddableChangePointChartDeps,
initialInput: EmbeddableChangePointChartInput,
parent?: IContainer
) {
super(initialInput, { defaultTitle: initialInput.title }, parent);
}
public reportsEmbeddableLoad() {
return true;
}
public onLoading() {
this.renderComplete.dispatchInProgress();
this.updateOutput({ loading: true, error: undefined });
}
public onError(error: Error) {
this.renderComplete.dispatchError();
this.updateOutput({ loading: false, error: { name: error.name, message: error.message } });
}
public onRenderComplete() {
this.renderComplete.dispatchComplete();
this.updateOutput({ loading: false, error: undefined });
}
render(el: HTMLElement): void {
super.render(el);
this.node = el;
// required for the export feature to work
this.node.setAttribute('data-shared-item', '');
const I18nContext = this.deps.i18n.Context;
const datePickerDeps = {
...pick(this.deps, ['data', 'http', 'notifications', 'theme', 'uiSettings']),
toMountPoint,
wrapWithTheme,
uiSettingsKeys: UI_SETTINGS,
};
const input = this.getInput();
const input$ = this.getInput$();
ReactDOM.render(
<I18nContext>
<KibanaThemeProvider theme$={this.deps.theme.theme$}>
<AiopsAppContext.Provider value={this.deps as unknown as AiopsAppDependencies}>
<DatePickerContextProvider {...datePickerDeps}>
<Suspense fallback={null}>
<EmbeddableInputTracker
input$={input$}
initialInput={input}
reload$={this.reload$}
/>
</Suspense>
</DatePickerContextProvider>
</AiopsAppContext.Provider>
</KibanaThemeProvider>
</I18nContext>,
el
);
}
public destroy() {
super.destroy();
if (this.node) {
ReactDOM.unmountComponentAtNode(this.node);
}
}
}

View file

@ -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 { CoreStart } from '@kbn/core/public';
import type { TimeRange } from '@kbn/es-query';
import React from 'react';
import {
EmbeddableFactory,
EmbeddableOutput,
EmbeddableRoot,
useEmbeddableFactory,
} from '@kbn/embeddable-plugin/public';
import { EuiLoadingChart } from '@elastic/eui';
import { EMBEDDABLE_CHANGE_POINT_CHART_TYPE } from '../../common/constants';
import type { AiopsPluginStartDeps } from '../types';
import type { EmbeddableChangePointChartInput } from './embeddable_change_point_chart';
export interface EmbeddableChangePointChartProps {
dataViewId: string;
timeRange: TimeRange;
fn: 'avg' | 'sum' | 'min' | 'max' | string;
metricField: string;
splitField?: string;
partitions?: string[];
maxSeriesToPlot?: number;
}
export function getEmbeddableChangePointChart(core: CoreStart, plugins: AiopsPluginStartDeps) {
const { embeddable: embeddableStart } = plugins;
const factory = embeddableStart.getEmbeddableFactory<EmbeddableChangePointChartInput>(
EMBEDDABLE_CHANGE_POINT_CHART_TYPE
)!;
return (props: EmbeddableChangePointChartProps) => {
const input = { ...props };
return (
<EmbeddableRootWrapper factory={factory} input={input as EmbeddableChangePointChartInput} />
);
};
}
function EmbeddableRootWrapper({
factory,
input,
}: {
factory: EmbeddableFactory<EmbeddableChangePointChartInput, EmbeddableOutput>;
input: EmbeddableChangePointChartInput;
}) {
const [embeddable, loading, error] = useEmbeddableFactory<EmbeddableChangePointChartInput>({
factory,
input,
});
if (loading) {
return <EuiLoadingChart />;
}
return <EmbeddableRoot embeddable={embeddable} loading={loading} error={error} input={input} />;
}

View file

@ -0,0 +1,100 @@
/*
* 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 {
EmbeddableFactoryDefinition,
ErrorEmbeddable,
IContainer,
} from '@kbn/embeddable-plugin/public';
import { i18n } from '@kbn/i18n';
import { type DataPublicPluginStart } from '@kbn/data-plugin/public';
import { StartServicesAccessor } from '@kbn/core-lifecycle-browser';
import { EMBEDDABLE_CHANGE_POINT_CHART_TYPE } from '../../common/constants';
import type { AiopsPluginStart, AiopsPluginStartDeps } from '../types';
import {
EmbeddableChangePointChart,
EmbeddableChangePointChartInput,
} from './embeddable_change_point_chart';
export interface EmbeddableChangePointChartStartServices {
data: DataPublicPluginStart;
}
export type EmbeddableChangePointChartType = typeof EMBEDDABLE_CHANGE_POINT_CHART_TYPE;
export class EmbeddableChangePointChartFactory implements EmbeddableFactoryDefinition {
public readonly type = EMBEDDABLE_CHANGE_POINT_CHART_TYPE;
public readonly grouping = [
{
id: 'ml',
getDisplayName: () =>
i18n.translate('xpack.aiops.navMenu.mlAppNameText', {
defaultMessage: 'Machine Learning',
}),
getIconType: () => 'machineLearningApp',
},
];
constructor(
private readonly getStartServices: StartServicesAccessor<AiopsPluginStartDeps, AiopsPluginStart>
) {}
/**
* TODO
*/
public isEditable = async () => {
return false;
};
getDisplayName() {
return i18n.translate('xpack.aiops.embeddableChangePointChartDisplayName', {
defaultMessage: 'Change point detection',
});
}
/**
* TODO
*/
canCreateNew() {
return false;
}
public async getExplicitInput(): Promise<Partial<EmbeddableChangePointChartInput>> {
const [coreStart] = await this.getStartServices();
try {
const { resolveEmbeddableChangePointUserInput } = await import('./handle_explicit_input');
return await resolveEmbeddableChangePointUserInput(coreStart);
} catch (e) {
return Promise.reject();
}
}
async create(input: EmbeddableChangePointChartInput, parent?: IContainer) {
try {
const [{ i18n: i18nService, theme, http, uiSettings, notifications }, { lens, data }] =
await this.getStartServices();
return new EmbeddableChangePointChart(
{
theme,
http,
i18n: i18nService,
uiSettings,
data,
notifications,
lens,
},
input,
parent
);
} catch (e) {
return new ErrorEmbeddable(e, input, parent);
}
}
}

View file

@ -0,0 +1,172 @@
/*
* 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 Observable } from 'rxjs';
import React, { FC, useMemo } from 'react';
import { useTimefilter } from '@kbn/ml-date-picker';
import { css } from '@emotion/react';
import useObservable from 'react-use/lib/useObservable';
import { ReloadContextProvider } from '../hooks/use_reload';
import type {
ChangePointAnnotation,
ChangePointDetectionRequestParams,
} from '../components/change_point_detection/change_point_detection_context';
import { EmbeddableChangePointChartInput } from './embeddable_change_point_chart';
import { EmbeddableChangePointChartProps } from './embeddable_change_point_chart_component';
import { FilterQueryContextProvider, useFilerQueryUpdates } from '../hooks/use_filters_query';
import { DataSourceContextProvider, useDataSource } from '../hooks/use_data_source';
import { useAiopsAppContext } from '../hooks/use_aiops_app_context';
import { useTimeBuckets } from '../hooks/use_time_buckets';
import { createMergedEsQuery } from '../application/utils/search_utils';
import { useChangePointResults } from '../components/change_point_detection/use_change_point_agg_request';
import { ChartsGrid } from '../components/change_point_detection/charts_grid';
import { NoChangePointsWarning } from '../components/change_point_detection/no_change_points_warning';
const defaultSort = {
field: 'p_value' as keyof ChangePointAnnotation,
direction: 'asc',
};
export const EmbeddableInputTracker: FC<{
input$: Observable<EmbeddableChangePointChartInput>;
initialInput: EmbeddableChangePointChartInput;
reload$: Observable<number>;
}> = ({ input$, initialInput, reload$ }) => {
const input = useObservable(input$, initialInput);
return (
<ReloadContextProvider reload$={reload$}>
<DataSourceContextProvider dataViewId={input.dataViewId}>
<FilterQueryContextProvider timeRange={input.timeRange}>
<ChartGridEmbeddableWrapper
timeRange={input.timeRange}
fn={input.fn}
metricField={input.metricField}
splitField={input.splitField}
maxSeriesToPlot={input.maxSeriesToPlot}
dataViewId={input.dataViewId}
partitions={input.partitions}
/>
</FilterQueryContextProvider>
</DataSourceContextProvider>
</ReloadContextProvider>
);
};
/**
* Grid component wrapper for embeddable.
*
* @param timeRange
* @param fn
* @param metricField
* @param maxSeriesToPlot
* @param splitField
* @param partitions
* @constructor
*/
export const ChartGridEmbeddableWrapper: FC<EmbeddableChangePointChartProps> = ({
fn,
metricField,
maxSeriesToPlot,
splitField,
partitions,
}) => {
const { filters, query, timeRange } = useFilerQueryUpdates();
const fieldConfig = useMemo(() => {
return { fn, metricField, splitField };
}, [fn, metricField, splitField]);
const { dataView } = useDataSource();
const { uiSettings } = useAiopsAppContext();
const timeBuckets = useTimeBuckets();
const timefilter = useTimefilter();
const interval = useMemo(() => {
timeBuckets.setInterval('auto');
timeBuckets.setBounds(timefilter.calculateBounds(timeRange));
return timeBuckets.getInterval().expression;
}, [timeRange, timeBuckets, timefilter]);
const combinedQuery = useMemo(() => {
const mergedQuery = createMergedEsQuery(query, filters, dataView, uiSettings);
if (!Array.isArray(mergedQuery.bool?.filter)) {
if (!mergedQuery.bool) {
mergedQuery.bool = {};
}
mergedQuery.bool.filter = [];
}
mergedQuery.bool!.filter.push({
range: {
[dataView.timeFieldName!]: {
from: timeRange.from,
to: timeRange.to,
},
},
});
if (partitions && fieldConfig.splitField) {
mergedQuery.bool?.filter.push({
terms: {
[fieldConfig.splitField]: partitions,
},
});
}
return mergedQuery;
}, [
dataView,
fieldConfig.splitField,
filters,
partitions,
query,
timeRange.from,
timeRange.to,
uiSettings,
]);
const requestParams = useMemo<ChangePointDetectionRequestParams>(() => {
return { interval } as ChangePointDetectionRequestParams;
}, [interval]);
const { results } = useChangePointResults(fieldConfig, requestParams, combinedQuery, 10000);
const changePoints = useMemo<ChangePointAnnotation[]>(() => {
let resultChangePoints: ChangePointAnnotation[] = results.sort((a, b) => {
if (defaultSort.direction === 'asc') {
return (a[defaultSort.field] as number) - (b[defaultSort.field] as number);
} else {
return (b[defaultSort.field] as number) - (a[defaultSort.field] as number);
}
});
if (maxSeriesToPlot) {
resultChangePoints = resultChangePoints.slice(0, maxSeriesToPlot);
}
return resultChangePoints;
}, [results, maxSeriesToPlot]);
return (
<div
css={css`
overflow: auto;
width: 100%;
`}
>
{changePoints.length > 0 ? (
<ChartsGrid
changePoints={changePoints.map((r) => ({ ...r, ...fieldConfig }))}
interval={requestParams.interval}
/>
) : (
<NoChangePointsWarning />
)}
</div>
);
};

View file

@ -0,0 +1,50 @@
/*
* 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 { CoreStart } from '@kbn/core/public';
import { toMountPoint, wrapWithTheme } from '@kbn/kibana-react-plugin/public';
import React from 'react';
import { ChangePointChartInitializer } from './change_point_chart_initializer';
import { EmbeddableChangePointChartInput } from './embeddable_change_point_chart';
export async function resolveEmbeddableChangePointUserInput(
coreStart: CoreStart,
input?: EmbeddableChangePointChartInput
): Promise<Partial<EmbeddableChangePointChartInput>> {
const { overlays } = coreStart;
return new Promise(async (resolve, reject) => {
try {
const title = input?.title;
const { theme$ } = coreStart.theme;
const modalSession = overlays.openModal(
toMountPoint(
wrapWithTheme(
<ChangePointChartInitializer
defaultTitle={title ?? ''}
initialInput={input}
onCreate={({ panelTitle, maxSeriesToPlot }) => {
modalSession.close();
resolve({
title: panelTitle,
maxSeriesToPlot,
});
}}
onCancel={() => {
modalSession.close();
reject();
}}
/>,
theme$
)
)
);
} catch (error) {
reject(error);
}
});
}

View file

@ -0,0 +1,9 @@
/*
* 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 { EmbeddableChangePointChartFactory } from './embeddable_change_point_chart_factory';
export { type EmbeddableChangePointChartProps } from './embeddable_change_point_chart_component';

View file

@ -0,0 +1,19 @@
/*
* 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 { CoreSetup } from '@kbn/core-lifecycle-browser';
import type { EmbeddableSetup } from '@kbn/embeddable-plugin/public';
import type { AiopsPluginStart, AiopsPluginStartDeps } from '../types';
import { EmbeddableChangePointChartFactory } from './embeddable_change_point_chart_factory';
export const registerEmbeddable = (
core: CoreSetup<AiopsPluginStartDeps, AiopsPluginStart>,
embeddable: EmbeddableSetup
) => {
const factory = new EmbeddableChangePointChartFactory(core.getStartServices);
embeddable.registerEmbeddableFactory(factory.type, factory);
};

View file

@ -29,6 +29,9 @@ import type {
FieldStatsServices,
} from '@kbn/unified-field-list/src/components/field_stats';
import type { TimeRange as TimeRangeMs } from '@kbn/ml-date-picker';
import type { PresentationUtilPluginStart } from '@kbn/presentation-util-plugin/public';
import type { EmbeddableStart } from '@kbn/embeddable-plugin/public';
import type { CasesUiStart } from '@kbn/cases-plugin/public';
/**
* AIOps App Dependencies to be provided via React context.
@ -104,6 +107,9 @@ export interface AiopsAppDependencies {
dslQuery?: FieldStatsProps['dslQuery'];
}>;
};
presentationUtil?: PresentationUtilPluginStart;
embeddable?: EmbeddableStart;
cases?: CasesUiStart;
}
/**

View file

@ -0,0 +1,52 @@
/*
* 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 { useCallback } from 'react';
import { stringHash } from '@kbn/ml-string-hash';
import { AttachmentType } from '@kbn/cases-plugin/common';
import { EmbeddableChangePointChartInput } from '../embeddable/embeddable_change_point_chart';
import { EmbeddableChangePointChartType } from '../embeddable/embeddable_change_point_chart_factory';
import { useAiopsAppContext } from './use_aiops_app_context';
/**
* Returns a callback for opening the cases modal with provided attachment state.
*/
export const useCasesModal = <EmbeddableType extends EmbeddableChangePointChartType>(
embeddableType: EmbeddableType
) => {
const { cases } = useAiopsAppContext();
const selectCaseModal = cases?.hooks.useCasesAddToExistingCaseModal();
return useCallback(
(persistableState: Partial<Omit<EmbeddableChangePointChartInput, 'id'>>) => {
const persistableStateAttachmentState = {
...persistableState,
// Creates unique id based on the input
id: stringHash(JSON.stringify(persistableState)).toString(),
};
if (!selectCaseModal) {
throw new Error('Cases modal is not available');
}
selectCaseModal.open({
getAttachments: () => [
{
type: AttachmentType.persistableState,
persistableStateAttachmentTypeId: embeddableType,
persistableStateAttachmentState: JSON.parse(
JSON.stringify(persistableStateAttachmentState)
),
},
],
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[embeddableType]
);
};

View file

@ -1,24 +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 { createContext, useContext } from 'react';
import { DataView } from '@kbn/data-views-plugin/common';
import type { SavedSearch } from '@kbn/saved-search-plugin/public';
export const DataSourceContext = createContext<{
dataView: DataView | never;
savedSearch: SavedSearch | null;
}>({
get dataView(): never {
throw new Error('DataSourceContext is not implemented');
},
savedSearch: null,
});
export function useDataSource() {
return useContext(DataSourceContext);
}

View file

@ -0,0 +1,105 @@
/*
* 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, { createContext, FC, useCallback, useContext, useEffect, useState } from 'react';
import { DataView } from '@kbn/data-views-plugin/common';
import type { SavedSearch } from '@kbn/saved-search-plugin/public';
import { EuiEmptyPrompt } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { useAiopsAppContext } from './use_aiops_app_context';
export const DataSourceContext = createContext<DataViewAndSavedSearch>({
get dataView(): never {
throw new Error('DataSourceContext is not implemented');
},
savedSearch: null,
});
export function useDataSource() {
return useContext(DataSourceContext);
}
export interface DataViewAndSavedSearch {
savedSearch: SavedSearch | null;
dataView: DataView;
}
/**
* Context provider that resolves current data view and the saved search
*
* @param children
* @constructor
*/
export const DataSourceContextProvider: FC<{ dataViewId?: string; savedSearchId?: string }> = ({
dataViewId,
savedSearchId,
children,
}) => {
const [value, setValue] = useState<DataViewAndSavedSearch>();
const [error, setError] = useState<Error>();
const {
data: { dataViews },
// uiSettings,
// savedSearch: savedSearchService,
} = useAiopsAppContext();
/**
* Resolve data view or saved search if exists.
*/
const resolveDataSource = useCallback(async (): Promise<DataViewAndSavedSearch> => {
const dataViewAndSavedSearch: DataViewAndSavedSearch = {
savedSearch: null,
// @ts-ignore
dataView: null,
};
// support only data views for now
if (dataViewId !== undefined) {
dataViewAndSavedSearch.dataView = await dataViews.get(dataViewId);
}
const { savedSearch, dataView } = dataViewAndSavedSearch;
return {
dataView,
savedSearch,
};
}, [dataViewId, dataViews]);
useEffect(() => {
resolveDataSource()
.then((result) => {
setValue(result);
})
.catch((e) => {
setError(e);
});
}, [resolveDataSource]);
if (!value && !error) return null;
if (error) {
return (
<EuiEmptyPrompt
iconType="error"
color="danger"
title={
<h2>
<FormattedMessage
id="xpack.aiops.dataSourceContext.errorTitle"
defaultMessage="Unable to fetch data view or saved search"
/>
</h2>
}
body={<p>{error.message}</p>}
/>
);
}
return <DataSourceContext.Provider value={value!}>{children}</DataSourceContext.Provider>;
};

View file

@ -0,0 +1,86 @@
/*
* 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, { type FC, createContext, useEffect, useState, useContext } from 'react';
import type { Filter, Query, TimeRange } from '@kbn/es-query';
import { useTimeRangeUpdates } from '@kbn/ml-date-picker';
import { type AggregateQuery } from '@kbn/es-query';
import { useAiopsAppContext } from './use_aiops_app_context';
export const FilterQueryContext = createContext<{
filters: Filter[];
query: Query;
timeRange: TimeRange;
}>({
get filters(): Filter[] {
throw new Error('FilterQueryContext is not initialized');
},
get query(): Query {
throw new Error('FilterQueryContext is not initialized');
},
get timeRange(): TimeRange {
throw new Error('FilterQueryContext is not initialized');
},
});
/**
* Helper context to provide the latest filter, query and time range values
* from the data plugin.
* Also merges custom filters and queries provided with an input.
*
* @param children
* @constructor
*/
export const FilterQueryContextProvider: FC<{ timeRange?: TimeRange }> = ({
children,
timeRange,
}) => {
const {
data: {
query: { filterManager, queryString },
},
} = useAiopsAppContext();
const [resultFilters, setResultFilter] = useState<Filter[]>(filterManager.getFilters());
const [resultQuery, setResultQuery] = useState<Query | AggregateQuery>(queryString.getQuery());
const timeRangeUpdates = useTimeRangeUpdates(true);
useEffect(() => {
const sub = filterManager.getUpdates$().subscribe(() => {
setResultFilter(filterManager.getFilters());
});
return () => {
sub.unsubscribe();
};
}, [filterManager]);
useEffect(() => {
const sub = queryString.getUpdates$().subscribe(() => {
setResultQuery(queryString.getQuery());
});
return () => {
sub.unsubscribe();
};
}, [queryString]);
return (
<FilterQueryContext.Provider
value={{
filters: resultFilters,
query: resultQuery as Query,
timeRange: timeRange ?? timeRangeUpdates,
}}
>
{children}
</FilterQueryContext.Provider>
);
};
export const useFilerQueryUpdates = () => {
return useContext(FilterQueryContext);
};

View file

@ -0,0 +1,30 @@
/*
* 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, { useContext } from 'react';
import { type Observable } from 'rxjs';
import useObservable from 'react-use/lib/useObservable';
export interface ReloadContextValue {
refreshTimestamp: number;
}
export const ReloadContext = React.createContext<ReloadContextValue>({
refreshTimestamp: Date.now(),
});
export const ReloadContextProvider: React.FC<{ reload$: Observable<number> }> = ({
reload$,
children,
}) => {
const refreshTimestamp = useObservable(reload$, Date.now());
return <ReloadContext.Provider value={{ refreshTimestamp }}>{children}</ReloadContext.Provider>;
};
export const useReload = () => {
return useContext(ReloadContext);
};

View file

@ -6,23 +6,42 @@
*/
import type { CoreStart, Plugin } from '@kbn/core/public';
import { type CoreSetup } from '@kbn/core/public';
import { firstValueFrom } from 'rxjs';
import {
import type {
AiopsPluginSetup,
AiopsPluginSetupDeps,
AiopsPluginStart,
AiopsPluginStartDeps,
} from './types';
import { getEmbeddableChangePointChart } from './embeddable/embeddable_change_point_chart_component';
export class AiopsPlugin
implements Plugin<AiopsPluginSetup, AiopsPluginStart, AiopsPluginSetupDeps, AiopsPluginStartDeps>
{
public setup() {
return {};
public setup(
core: CoreSetup<AiopsPluginStartDeps, AiopsPluginStart>,
{ embeddable, cases, licensing }: AiopsPluginSetupDeps
) {
firstValueFrom(licensing.license$).then(async (license) => {
if (license.hasAtLeast('platinum')) {
if (embeddable) {
const { registerEmbeddable } = await import('./embeddable/register_embeddable');
registerEmbeddable(core, embeddable);
}
if (cases) {
const [coreStart, pluginStart] = await core.getStartServices();
const { registerChangePointChartsAttachment } = await import(
'./cases/register_change_point_charts_attachment'
);
registerChangePointChartsAttachment(cases, coreStart, pluginStart);
}
}
});
}
public start(core: CoreStart, plugins: AiopsPluginStartDeps) {
public start(core: CoreStart, plugins: AiopsPluginStartDeps): AiopsPluginStart {
// importing async to keep the aiops plugin size to a minimum
Promise.all([
import('@kbn/ui-actions-plugin/public'),
@ -42,7 +61,9 @@ export class AiopsPlugin
}
});
return {};
return {
EmbeddableChangePointChart: getEmbeddableChangePointChart(core, plugins),
};
}
public stop() {}

View file

@ -15,9 +15,16 @@ import type { LicensingPluginStart } from '@kbn/licensing-plugin/public';
import type { SharePluginStart } from '@kbn/share-plugin/public';
import type { UiActionsStart } from '@kbn/ui-actions-plugin/public';
import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public';
import type { EmbeddableSetup, EmbeddableStart } from '@kbn/embeddable-plugin/public';
import type { CasesUiSetup } from '@kbn/cases-plugin/public';
import { LicensingPluginSetup } from '@kbn/licensing-plugin/public';
import type { EmbeddableChangePointChartProps } from './embeddable';
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface AiopsPluginSetupDeps {}
export interface AiopsPluginSetupDeps {
embeddable: EmbeddableSetup;
cases: CasesUiSetup;
licensing: LicensingPluginSetup;
}
export interface AiopsPluginStartDeps {
data: DataPublicPluginStart;
@ -30,12 +37,10 @@ export interface AiopsPluginStartDeps {
storage: IStorageWrapper;
licensing: LicensingPluginStart;
executionContext: ExecutionContextStart;
embeddable: EmbeddableStart;
}
/**
* aiops plugin server setup contract
*/
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface AiopsPluginSetup {}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface AiopsPluginStart {}
export type AiopsPluginSetup = void;
export interface AiopsPluginStart {
EmbeddableChangePointChart: React.ComponentType<EmbeddableChangePointChartProps>;
}

View file

@ -10,6 +10,7 @@ import { Subscription } from 'rxjs';
import { PluginInitializerContext, CoreSetup, CoreStart, Plugin, Logger } from '@kbn/core/server';
import type { DataRequestHandlerContext } from '@kbn/data-plugin/server';
import { CASES_ATTACHMENT_CHANGE_POINT_CHART } from '../common/constants';
import { isActiveLicense } from './lib/license';
import {
AiopsLicense,
@ -54,6 +55,12 @@ export class AiopsPlugin
defineLogCategorizationRoutes(router, aiopsLicense);
});
if (plugins.cases) {
plugins.cases.attachmentFramework.registerPersistableState({
id: CASES_ATTACHMENT_CHANGE_POINT_CHART,
});
}
return {};
}

View file

@ -7,10 +7,12 @@
import type { PluginSetup, PluginStart } from '@kbn/data-plugin/server';
import type { LicensingPluginStart } from '@kbn/licensing-plugin/server';
import type { CasesSetup } from '@kbn/cases-plugin/server';
export interface AiopsPluginSetupDeps {
data: PluginSetup;
licensing: LicensingPluginStart;
cases?: CasesSetup;
}
export interface AiopsPluginStartDeps {

View file

@ -57,6 +57,11 @@
"@kbn/unified-field-list",
"@kbn/unified-search-plugin",
"@kbn/utility-types",
"@kbn/presentation-util-plugin",
"@kbn/embeddable-plugin",
"@kbn/core-theme-browser",
"@kbn/core-lifecycle-browser",
"@kbn/cases-plugin",
],
"exclude": [
"target/**/*",

View file

@ -28,7 +28,8 @@
"unifiedSearch",
"savedObjectsManagement",
"savedSearch",
"contentManagement"
"contentManagement",
"presentationUtil"
],
"optionalPlugins": [
"alerting",

View file

@ -60,6 +60,9 @@ export const ChangePointDetectionPage: FC = () => {
'unifiedSearch',
'theme',
'lens',
'presentationUtil',
'embeddable',
'cases',
]),
fieldStats: { useFieldStatsTrigger, FieldStatsFlyoutProvider },
}}

View file

@ -102,6 +102,7 @@ const App: FC<AppProps> = ({ coreStart, deps, appMountParams }) => {
savedObjectsManagement: deps.savedObjectsManagement,
savedSearch: deps.savedSearch,
contentManagement: deps.contentManagement,
presentationUtil: deps.presentationUtil,
...coreStart,
mlServices: getMlGlobalServices(coreStart.http, deps.usageCollection),
};

View file

@ -28,6 +28,7 @@ import type { LensPublicStart } from '@kbn/lens-plugin/public';
import type { SavedObjectsManagementPluginStart } from '@kbn/saved-objects-management-plugin/public';
import type { ContentManagementPublicStart } from '@kbn/content-management-plugin/public';
import type { SavedSearchPublicPluginStart } from '@kbn/saved-search-plugin/public';
import type { PresentationUtilPluginStart } from '@kbn/presentation-util-plugin/public';
import type { MlServicesContext } from '../../app';
interface StartPlugins {
@ -53,6 +54,7 @@ interface StartPlugins {
savedObjectsManagement: SavedObjectsManagementPluginStart;
savedSearch: SavedSearchPublicPluginStart;
contentManagement: ContentManagementPublicStart;
presentationUtil: PresentationUtilPluginStart;
}
export type StartServices = CoreStart &
StartPlugins & {

View file

@ -10,6 +10,7 @@ import { i18n } from '@kbn/i18n';
import type { StartServicesAccessor } from '@kbn/core/public';
import type { EmbeddableFactoryDefinition, IContainer } from '@kbn/embeddable-plugin/public';
import type { IAnomalySwimlaneEmbeddable } from './anomaly_swimlane_embeddable';
import { PLUGIN_ID, PLUGIN_ICON, ML_APP_NAME } from '../../../common/constants/app';
import { HttpService } from '../../application/services/http_service';
import type { MlPluginStart, MlStartDependencies } from '../../plugin';
@ -94,7 +95,7 @@ export class AnomalySwimlaneEmbeddableFactory
public async create(
initialInput: AnomalySwimlaneEmbeddableInput,
parent?: IContainer
): Promise<any> {
): Promise<InstanceType<IAnomalySwimlaneEmbeddable>> {
const services = await this.getServices();
const { AnomalySwimlaneEmbeddable } = await import('./anomaly_swimlane_embeddable');
return new AnomalySwimlaneEmbeddable(initialInput, services, parent);

View file

@ -47,6 +47,7 @@ import type { DashboardSetup, DashboardStart } from '@kbn/dashboard-plugin/publi
import type { ChartsPluginStart } from '@kbn/charts-plugin/public';
import type { CasesUiSetup, CasesUiStart } from '@kbn/cases-plugin/public';
import type { SavedSearchPublicPluginStart } from '@kbn/saved-search-plugin/public';
import type { PresentationUtilPluginStart } from '@kbn/presentation-util-plugin/public';
import { registerManagementSection } from './application/management';
import { MlLocatorDefinition, type MlLocator } from './locator';
import { setDependencyCache } from './application/util/dependency_cache';
@ -75,6 +76,7 @@ export interface MlStartDependencies {
savedObjectsManagement: SavedObjectsManagementPluginStart;
savedSearch: SavedSearchPublicPluginStart;
contentManagement: ContentManagementPublicStart;
presentationUtil: PresentationUtilPluginStart;
}
export interface MlSetupDependencies {
@ -145,6 +147,7 @@ export class MlPlugin implements Plugin<MlPluginSetup, MlPluginStart> {
savedObjectsManagement: pluginsStart.savedObjectsManagement,
savedSearch: pluginsStart.savedSearch,
contentManagement: pluginsStart.contentManagement,
presentationUtil: pluginsStart.presentationUtil,
},
params
);

View file

@ -102,5 +102,6 @@
"@kbn/core-ui-settings-browser",
"@kbn/content-management-plugin",
"@kbn/ml-in-memory-table",
"@kbn/presentation-util-plugin",
],
}

View file

@ -291,6 +291,7 @@ export default ({ getService }: FtrProviderContext): void => {
expect(types).to.eql({
'.lens': '78559fd806809ac3a1008942ead2a079864054f5',
'.test': 'ab2204830c67f5cf992c9aa2f7e3ead752cc60a1',
aiopsChangePointChart: 'a1212d71947ec34487b374cecc47ab9941b5d91c',
ml_anomaly_charts: '23e92e824af9db6e8b8bb1d63c222e04f57d2147',
ml_anomaly_swimlane: 'a3517f3e53fb041e9cbb150477fb6ef0f731bd5f',
});