mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 18:51:07 -04:00
[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  - Allows attachment of top N change points (with an ascending sorting by `p_value`)  - Allows applying a persistent time range - Attachment to a case of a single change point / selected partitions and applied time range  - 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:
parent
bc9d601feb
commit
b62747e085
42 changed files with 1815 additions and 214 deletions
|
@ -29,3 +29,7 @@ export type LogRateAnalysisType =
|
||||||
* In future versions we might use a user specific seed or let the user customise it.
|
* In future versions we might use a user specific seed or let the user customise it.
|
||||||
*/
|
*/
|
||||||
export const RANDOM_SAMPLER_SEED = 3867412;
|
export const RANDOM_SAMPLER_SEED = 3867412;
|
||||||
|
|
||||||
|
export const CASES_ATTACHMENT_CHANGE_POINT_CHART = 'aiopsChangePointChart';
|
||||||
|
|
||||||
|
export const EMBEDDABLE_CHANGE_POINT_CHART_TYPE = 'aiopsChangePointChart' as const;
|
||||||
|
|
|
@ -13,11 +13,20 @@
|
||||||
"lens",
|
"lens",
|
||||||
"licensing",
|
"licensing",
|
||||||
"uiActions",
|
"uiActions",
|
||||||
|
"embeddable",
|
||||||
|
"presentationUtil",
|
||||||
|
"dashboard",
|
||||||
|
"fieldFormats"
|
||||||
|
],
|
||||||
|
"optionalPlugins": [
|
||||||
|
"cases"
|
||||||
],
|
],
|
||||||
"requiredBundles": [
|
"requiredBundles": [
|
||||||
"fieldFormats",
|
"fieldFormats",
|
||||||
"kibanaReact",
|
"kibanaReact",
|
||||||
"kibanaUtils"
|
"kibanaUtils",
|
||||||
|
"embeddable",
|
||||||
|
"cases"
|
||||||
],
|
],
|
||||||
"extraPublicDirs": [
|
"extraPublicDirs": [
|
||||||
"common"
|
"common"
|
||||||
|
|
|
@ -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
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
|
@ -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),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
|
@ -21,6 +21,7 @@ import { usePageUrlState } from '@kbn/ml-url-state';
|
||||||
import { useTimefilter, useTimeRangeUpdates } from '@kbn/ml-date-picker';
|
import { useTimefilter, useTimeRangeUpdates } from '@kbn/ml-date-picker';
|
||||||
import { ES_FIELD_TYPES } from '@kbn/field-types';
|
import { ES_FIELD_TYPES } from '@kbn/field-types';
|
||||||
import { type QueryDslQueryContainer } from '@kbn/data-views-plugin/common/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 { type ChangePointType, DEFAULT_AGG_FUNCTION } from './constants';
|
||||||
import {
|
import {
|
||||||
createMergedEsQuery,
|
createMergedEsQuery,
|
||||||
|
@ -254,7 +255,7 @@ export const ChangePointDetectionContextProvider: FC = ({ children }) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ChangePointDetectionContext.Provider value={value}>
|
<ChangePointDetectionContext.Provider value={value}>
|
||||||
{children}
|
<FilterQueryContextProvider>{children}</FilterQueryContextProvider>
|
||||||
</ChangePointDetectionContext.Provider>
|
</ChangePointDetectionContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -21,7 +21,7 @@ import {
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
import { FormattedMessage } from '@kbn/i18n-react';
|
import { FormattedMessage } from '@kbn/i18n-react';
|
||||||
import type { Query } from '@kbn/es-query';
|
import type { Query } from '@kbn/es-query';
|
||||||
import { ChartsGrid } from './charts_grid';
|
import { ChartsGridContainer } from './charts_grid';
|
||||||
import { FieldsConfig } from './fields_config';
|
import { FieldsConfig } from './fields_config';
|
||||||
import { useDataSource } from '../../hooks/use_data_source';
|
import { useDataSource } from '../../hooks/use_data_source';
|
||||||
import { ChangePointTypeFilter } from './change_point_type_filter';
|
import { ChangePointTypeFilter } from './change_point_type_filter';
|
||||||
|
@ -163,7 +163,7 @@ export const ChangePointDetectionPage: FC = () => {
|
||||||
</EuiTitle>
|
</EuiTitle>
|
||||||
</EuiFlyoutHeader>
|
</EuiFlyoutHeader>
|
||||||
<EuiFlyoutBody>
|
<EuiFlyoutBody>
|
||||||
<ChartsGrid changePoints={selectedChangePoints} />
|
<ChartsGridContainer changePoints={selectedChangePoints} />
|
||||||
</EuiFlyoutBody>
|
</EuiFlyoutBody>
|
||||||
</EuiFlyout>
|
</EuiFlyout>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
|
@ -5,9 +5,10 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { FC } from 'react';
|
import React, { FC, useMemo } from 'react';
|
||||||
|
import { map } from 'rxjs/operators';
|
||||||
import { pick } from 'lodash';
|
import { pick } from 'lodash';
|
||||||
|
import { EuiThemeProvider as StyledComponentsThemeProvider } from '@kbn/kibana-react-plugin/common';
|
||||||
import { EuiSpacer } from '@elastic/eui';
|
import { EuiSpacer } from '@elastic/eui';
|
||||||
|
|
||||||
import { DataView } from '@kbn/data-views-plugin/common';
|
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 { StorageContextProvider } from '@kbn/ml-local-storage';
|
||||||
import { UrlStateProvider } from '@kbn/ml-url-state';
|
import { UrlStateProvider } from '@kbn/ml-url-state';
|
||||||
import { Storage } from '@kbn/kibana-utils-plugin/public';
|
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 { UI_SETTINGS } from '@kbn/data-plugin/common';
|
||||||
import { toMountPoint, wrapWithTheme } from '@kbn/kibana-react-plugin/public';
|
import { toMountPoint, wrapWithTheme } from '@kbn/kibana-react-plugin/public';
|
||||||
|
|
||||||
|
import { type Observable } from 'rxjs';
|
||||||
import { DataSourceContext } from '../../hooks/use_data_source';
|
import { DataSourceContext } from '../../hooks/use_data_source';
|
||||||
import { AiopsAppContext, AiopsAppDependencies } from '../../hooks/use_aiops_app_context';
|
import { AiopsAppContext, AiopsAppDependencies } from '../../hooks/use_aiops_app_context';
|
||||||
import { AIOPS_STORAGE_KEYS } from '../../types/storage';
|
import { AIOPS_STORAGE_KEYS } from '../../types/storage';
|
||||||
|
@ -28,6 +30,7 @@ import { PageHeader } from '../page_header';
|
||||||
import { ChangePointDetectionPage } from './change_point_detection_page';
|
import { ChangePointDetectionPage } from './change_point_detection_page';
|
||||||
import { ChangePointDetectionContextProvider } from './change_point_detection_context';
|
import { ChangePointDetectionContextProvider } from './change_point_detection_context';
|
||||||
import { timeSeriesDataViewWarning } from '../../application/utils/time_series_dataview_check';
|
import { timeSeriesDataViewWarning } from '../../application/utils/time_series_dataview_check';
|
||||||
|
import { ReloadContextProvider } from '../../hooks/use_reload';
|
||||||
|
|
||||||
const localStorage = new Storage(window.localStorage);
|
const localStorage = new Storage(window.localStorage);
|
||||||
|
|
||||||
|
@ -57,25 +60,43 @@ export const ChangePointDetectionAppState: FC<ChangePointDetectionAppStateProps>
|
||||||
|
|
||||||
const warning = timeSeriesDataViewWarning(dataView, 'change_point_detection');
|
const warning = timeSeriesDataViewWarning(dataView, 'change_point_detection');
|
||||||
|
|
||||||
|
const reload$ = useMemo<Observable<number>>(() => {
|
||||||
|
return mlTimefilterRefresh$.pipe(map((v) => v.lastRefresh));
|
||||||
|
}, []);
|
||||||
|
|
||||||
if (warning !== null) {
|
if (warning !== null) {
|
||||||
return <>{warning}</>;
|
return <>{warning}</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const PresentationContextProvider =
|
||||||
|
appDependencies.presentationUtil?.ContextProvider ?? React.Fragment;
|
||||||
|
|
||||||
|
const CasesContext = appDependencies.cases?.ui.getCasesContext() ?? React.Fragment;
|
||||||
|
const casesPermissions = appDependencies.cases?.helpers.canUseCases();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AiopsAppContext.Provider value={appDependencies}>
|
<PresentationContextProvider>
|
||||||
<UrlStateProvider>
|
<StyledComponentsThemeProvider>
|
||||||
<DataSourceContext.Provider value={{ dataView, savedSearch }}>
|
<CasesContext owner={[]} permissions={casesPermissions!}>
|
||||||
<StorageContextProvider storage={localStorage} storageKeys={AIOPS_STORAGE_KEYS}>
|
<AiopsAppContext.Provider value={appDependencies}>
|
||||||
<DatePickerContextProvider {...datePickerDeps}>
|
<UrlStateProvider>
|
||||||
<PageHeader />
|
<DataSourceContext.Provider value={{ dataView, savedSearch }}>
|
||||||
<EuiSpacer />
|
<StorageContextProvider storage={localStorage} storageKeys={AIOPS_STORAGE_KEYS}>
|
||||||
<ChangePointDetectionContextProvider>
|
<DatePickerContextProvider {...datePickerDeps}>
|
||||||
<ChangePointDetectionPage />
|
<PageHeader />
|
||||||
</ChangePointDetectionContextProvider>
|
<EuiSpacer />
|
||||||
</DatePickerContextProvider>
|
<ReloadContextProvider reload$={reload$}>
|
||||||
</StorageContextProvider>
|
<ChangePointDetectionContextProvider>
|
||||||
</DataSourceContext.Provider>
|
<ChangePointDetectionPage />
|
||||||
</UrlStateProvider>
|
</ChangePointDetectionContextProvider>
|
||||||
</AiopsAppContext.Provider>
|
</ReloadContextProvider>
|
||||||
|
</DatePickerContextProvider>
|
||||||
|
</StorageContextProvider>
|
||||||
|
</DataSourceContext.Provider>
|
||||||
|
</UrlStateProvider>
|
||||||
|
</AiopsAppContext.Provider>
|
||||||
|
</CasesContext>
|
||||||
|
</StyledComponentsThemeProvider>
|
||||||
|
</PresentationContextProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -19,12 +19,14 @@ import { i18n } from '@kbn/i18n';
|
||||||
import { FormattedMessage } from '@kbn/i18n-react';
|
import { FormattedMessage } from '@kbn/i18n-react';
|
||||||
import { EuiTableSelectionType } from '@elastic/eui/src/components/basic_table/table_types';
|
import { EuiTableSelectionType } from '@elastic/eui/src/components/basic_table/table_types';
|
||||||
import { type Filter, FilterStateStore } from '@kbn/es-query';
|
import { type Filter, FilterStateStore } from '@kbn/es-query';
|
||||||
|
import { NoChangePointsWarning } from './no_change_points_warning';
|
||||||
import { useDataSource } from '../../hooks/use_data_source';
|
import { useDataSource } from '../../hooks/use_data_source';
|
||||||
import { useCommonChartProps } from './use_common_chart_props';
|
import { useCommonChartProps } from './use_common_chart_props';
|
||||||
import {
|
import {
|
||||||
type ChangePointAnnotation,
|
type ChangePointAnnotation,
|
||||||
FieldConfig,
|
FieldConfig,
|
||||||
SelectedChangePoint,
|
SelectedChangePoint,
|
||||||
|
useChangePointDetectionContext,
|
||||||
} from './change_point_detection_context';
|
} from './change_point_detection_context';
|
||||||
import { type ChartComponentProps } from './chart_component';
|
import { type ChartComponentProps } from './chart_component';
|
||||||
import { useAiopsAppContext } from '../../hooks/use_aiops_app_context';
|
import { useAiopsAppContext } from '../../hooks/use_aiops_app_context';
|
||||||
|
@ -92,6 +94,8 @@ export const ChangePointsTable: FC<ChangePointsTableProps> = ({
|
||||||
|
|
||||||
const hasActions = fieldConfig.splitField !== undefined;
|
const hasActions = fieldConfig.splitField !== undefined;
|
||||||
|
|
||||||
|
const { bucketInterval } = useChangePointDetectionContext();
|
||||||
|
|
||||||
const columns: Array<EuiBasicTableColumn<ChangePointAnnotation>> = [
|
const columns: Array<EuiBasicTableColumn<ChangePointAnnotation>> = [
|
||||||
{
|
{
|
||||||
id: 'timestamp',
|
id: 'timestamp',
|
||||||
|
@ -122,7 +126,13 @@ export const ChangePointsTable: FC<ChangePointsTableProps> = ({
|
||||||
['&.euiTableCellContent']: { display: 'block', padding: 0 },
|
['&.euiTableCellContent']: { display: 'block', padding: 0 },
|
||||||
},
|
},
|
||||||
render: (annotation: ChangePointAnnotation) => {
|
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
|
<NoChangePointsWarning />
|
||||||
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>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
@ -313,10 +305,13 @@ export const MiniChartPreview: FC<ChartComponentProps> = ({ fieldConfig, annotat
|
||||||
lens: { EmbeddableComponent },
|
lens: { EmbeddableComponent },
|
||||||
} = useAiopsAppContext();
|
} = useAiopsAppContext();
|
||||||
|
|
||||||
|
const { bucketInterval } = useChangePointDetectionContext();
|
||||||
|
|
||||||
const { filters, query, attributes, timeRange } = useCommonChartProps({
|
const { filters, query, attributes, timeRange } = useCommonChartProps({
|
||||||
annotation,
|
annotation,
|
||||||
fieldConfig,
|
fieldConfig,
|
||||||
previewMode: true,
|
previewMode: true,
|
||||||
|
bucketInterval: bucketInterval.expression,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
|
import type { Filter, Query, TimeRange } from '@kbn/es-query';
|
||||||
import { useCommonChartProps } from './use_common_chart_props';
|
import { useCommonChartProps } from './use_common_chart_props';
|
||||||
import { useAiopsAppContext } from '../../hooks/use_aiops_app_context';
|
import { useAiopsAppContext } from '../../hooks/use_aiops_app_context';
|
||||||
import type { ChangePointAnnotation, FieldConfig } from './change_point_detection_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 {
|
export interface ChartComponentProps {
|
||||||
fieldConfig: FieldConfig;
|
fieldConfig: FieldConfig;
|
||||||
annotation: ChangePointAnnotation;
|
annotation: ChangePointAnnotation;
|
||||||
|
|
||||||
|
interval: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ChartComponent: FC<ChartComponentProps> = React.memo(({ annotation, fieldConfig }) => {
|
export interface ChartComponentPropsAll {
|
||||||
const {
|
fn: string;
|
||||||
lens: { EmbeddableComponent },
|
metricField: string;
|
||||||
} = useAiopsAppContext();
|
splitField?: string;
|
||||||
|
maxResults: number;
|
||||||
|
timeRange: TimeRange;
|
||||||
|
filters?: Filter[];
|
||||||
|
query?: Query;
|
||||||
|
}
|
||||||
|
|
||||||
const { filters, timeRange, query, attributes } = useCommonChartProps({
|
export const ChartComponent: FC<ChartComponentProps> = React.memo(
|
||||||
fieldConfig,
|
({ annotation, fieldConfig, interval }) => {
|
||||||
annotation,
|
const {
|
||||||
});
|
lens: { EmbeddableComponent },
|
||||||
|
} = useAiopsAppContext();
|
||||||
|
|
||||||
return (
|
const { filters, timeRange, query, attributes } = useCommonChartProps({
|
||||||
<EmbeddableComponent
|
fieldConfig,
|
||||||
id={`changePointChart_${annotation.group ? annotation.group.value : annotation.label}`}
|
annotation,
|
||||||
style={{ height: 350 }}
|
bucketInterval: interval,
|
||||||
timeRange={timeRange}
|
});
|
||||||
query={query}
|
|
||||||
filters={filters}
|
return (
|
||||||
// @ts-ignore
|
<EmbeddableComponent
|
||||||
attributes={attributes}
|
id={`changePointChart_${annotation.group ? annotation.group.value : annotation.label}`}
|
||||||
renderMode={'view'}
|
style={{ height: 350 }}
|
||||||
executionContext={{
|
timeRange={timeRange}
|
||||||
type: 'aiops_change_point_detection_chart',
|
query={query}
|
||||||
name: 'Change point detection',
|
filters={filters}
|
||||||
}}
|
// @ts-ignore
|
||||||
disableTriggers
|
attributes={attributes}
|
||||||
/>
|
renderMode={'view'}
|
||||||
);
|
executionContext={{
|
||||||
});
|
type: 'aiops_change_point_detection_chart',
|
||||||
|
name: 'Change point detection',
|
||||||
|
}}
|
||||||
|
disableTriggers
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
|
@ -23,7 +23,10 @@ import { i18n } from '@kbn/i18n';
|
||||||
import { FormattedMessage } from '@kbn/i18n-react';
|
import { FormattedMessage } from '@kbn/i18n-react';
|
||||||
import { useTimefilter } from '@kbn/ml-date-picker';
|
import { useTimefilter } from '@kbn/ml-date-picker';
|
||||||
import { type RefreshInterval } from '@kbn/data-plugin/common';
|
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';
|
import { ChartComponent } from './chart_component';
|
||||||
|
|
||||||
const CHARTS_PER_PAGE = 6;
|
const CHARTS_PER_PAGE = 6;
|
||||||
|
@ -32,11 +35,115 @@ interface ChartsGridProps {
|
||||||
changePoints: Record<number, SelectedChangePoint[]>;
|
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 timefilter = useTimefilter();
|
||||||
|
|
||||||
const initialRefreshSetting = useRef<RefreshInterval>();
|
const initialRefreshSetting = useRef<RefreshInterval>();
|
||||||
|
|
||||||
|
const { bucketInterval } = useChangePointDetectionContext();
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
function pauseRefreshOnMount() {
|
function pauseRefreshOnMount() {
|
||||||
initialRefreshSetting.current = timefilter.getRefreshInterval();
|
initialRefreshSetting.current = timefilter.getRefreshInterval();
|
||||||
|
@ -76,85 +183,7 @@ export const ChartsGrid: FC<ChartsGridProps> = ({ changePoints: changePointsDict
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<EuiFlexGrid columns={resultPerPage.length >= 2 ? 2 : 1} responsive gutterSize={'m'}>
|
<ChartsGrid changePoints={resultPerPage} interval={bucketInterval.expression} />
|
||||||
{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>
|
|
||||||
|
|
||||||
{pagination.pageCount > 1 ? (
|
{pagination.pageCount > 1 ? (
|
||||||
<EuiFlexGroup justifyContent="spaceAround">
|
<EuiFlexGroup justifyContent="spaceAround">
|
||||||
|
|
|
@ -10,16 +10,35 @@ import {
|
||||||
EuiButton,
|
EuiButton,
|
||||||
EuiButtonIcon,
|
EuiButtonIcon,
|
||||||
EuiCallOut,
|
EuiCallOut,
|
||||||
|
EuiContextMenu,
|
||||||
|
EuiFieldNumber,
|
||||||
EuiFlexGroup,
|
EuiFlexGroup,
|
||||||
EuiFlexItem,
|
EuiFlexItem,
|
||||||
|
EuiForm,
|
||||||
|
EuiFormRow,
|
||||||
|
EuiIcon,
|
||||||
EuiPanel,
|
EuiPanel,
|
||||||
|
EuiPopover,
|
||||||
EuiProgress,
|
EuiProgress,
|
||||||
EuiSpacer,
|
EuiSpacer,
|
||||||
|
EuiSwitch,
|
||||||
|
EuiToolTip,
|
||||||
} from '@elastic/eui';
|
} from '@elastic/eui';
|
||||||
import { FormattedMessage } from '@kbn/i18n-react';
|
import { FormattedMessage } from '@kbn/i18n-react';
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
import type { FieldStatsServices } from '@kbn/unified-field-list/src/components/field_stats';
|
import type { FieldStatsServices } from '@kbn/unified-field-list/src/components/field_stats';
|
||||||
import { useTimefilter, useTimeRangeUpdates } from '@kbn/ml-date-picker';
|
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 { useDataSource } from '../../hooks/use_data_source';
|
||||||
import { useAiopsAppContext } from '../../hooks/use_aiops_app_context';
|
import { useAiopsAppContext } from '../../hooks/use_aiops_app_context';
|
||||||
import { ChangePointsTable } from './change_points_table';
|
import { ChangePointsTable } from './change_points_table';
|
||||||
|
@ -35,9 +54,12 @@ import {
|
||||||
} from './change_point_detection_context';
|
} from './change_point_detection_context';
|
||||||
import { useChangePointResults } from './use_change_point_agg_request';
|
import { useChangePointResults } from './use_change_point_agg_request';
|
||||||
import { useSplitFieldCardinality } from './use_split_field_cardinality';
|
import { useSplitFieldCardinality } from './use_split_field_cardinality';
|
||||||
|
import { MAX_SERIES } from '../../embeddable/const';
|
||||||
|
|
||||||
const selectControlCss = { width: '350px' };
|
const selectControlCss = { width: '350px' };
|
||||||
|
|
||||||
|
const SavedObjectSaveModalDashboard = withSuspense(LazySavedObjectSaveModalDashboard);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Contains panels with controls and change point results.
|
* Contains panels with controls and change point results.
|
||||||
*/
|
*/
|
||||||
|
@ -93,6 +115,7 @@ export const FieldsConfig: FC = () => {
|
||||||
return (
|
return (
|
||||||
<React.Fragment key={key}>
|
<React.Fragment key={key}>
|
||||||
<FieldPanel
|
<FieldPanel
|
||||||
|
panelIndex={index}
|
||||||
data-test-subj={`aiopsChangePointPanel_${index}`}
|
data-test-subj={`aiopsChangePointPanel_${index}`}
|
||||||
fieldConfig={fieldConfig}
|
fieldConfig={fieldConfig}
|
||||||
onChange={(value) => onChange(value, index)}
|
onChange={(value) => onChange(value, index)}
|
||||||
|
@ -121,6 +144,7 @@ export const FieldsConfig: FC = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface FieldPanelProps {
|
export interface FieldPanelProps {
|
||||||
|
panelIndex: number;
|
||||||
fieldConfig: FieldConfig;
|
fieldConfig: FieldConfig;
|
||||||
removeDisabled: boolean;
|
removeDisabled: boolean;
|
||||||
onChange: (update: FieldConfig) => void;
|
onChange: (update: FieldConfig) => void;
|
||||||
|
@ -138,6 +162,7 @@ export interface FieldPanelProps {
|
||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
const FieldPanel: FC<FieldPanelProps> = ({
|
const FieldPanel: FC<FieldPanelProps> = ({
|
||||||
|
panelIndex,
|
||||||
fieldConfig,
|
fieldConfig,
|
||||||
onChange,
|
onChange,
|
||||||
onRemove,
|
onRemove,
|
||||||
|
@ -145,18 +170,315 @@ const FieldPanel: FC<FieldPanelProps> = ({
|
||||||
onSelectionChange,
|
onSelectionChange,
|
||||||
'data-test-subj': dataTestSubj,
|
'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 splitFieldCardinality = useSplitFieldCardinality(fieldConfig.splitField, combinedQuery);
|
||||||
|
|
||||||
const [isExpanded, setIsExpanded] = useState<boolean>(true);
|
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 {
|
const {
|
||||||
results: annotations,
|
results: annotations,
|
||||||
isLoading: annotationsLoading,
|
isLoading: annotationsLoading,
|
||||||
progress,
|
progress,
|
||||||
} = useChangePointResults(fieldConfig, requestParams, combinedQuery, splitFieldCardinality);
|
} = 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 (
|
return (
|
||||||
<EuiPanel paddingSize="s" hasBorder hasShadow={false} data-test-subj={dataTestSubj}>
|
<EuiPanel paddingSize="s" hasBorder hasShadow={false} data-test-subj={dataTestSubj}>
|
||||||
<EuiFlexGroup alignItems={'center'} justifyContent={'spaceBetween'} gutterSize={'s'}>
|
<EuiFlexGroup alignItems={'center'} justifyContent={'spaceBetween'} gutterSize={'s'}>
|
||||||
|
@ -197,15 +519,32 @@ const FieldPanel: FC<FieldPanelProps> = ({
|
||||||
</EuiFlexItem>
|
</EuiFlexItem>
|
||||||
|
|
||||||
<EuiFlexItem grow={false}>
|
<EuiFlexItem grow={false}>
|
||||||
<EuiButtonIcon
|
<EuiFlexGroup alignItems={'center'} justifyContent={'spaceBetween'} gutterSize={'s'}>
|
||||||
disabled={removeDisabled}
|
<EuiFlexItem grow={false}>
|
||||||
aria-label={i18n.translate('xpack.aiops.changePointDetection.removeConfigLabel', {
|
<EuiPopover
|
||||||
defaultMessage: 'Remove configuration',
|
id={`panelContextMenu_${panelIndex}`}
|
||||||
})}
|
button={
|
||||||
iconType="trash"
|
<EuiButtonIcon
|
||||||
color="danger"
|
aria-label={i18n.translate(
|
||||||
onClick={onRemove}
|
'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>
|
</EuiFlexItem>
|
||||||
</EuiFlexGroup>
|
</EuiFlexGroup>
|
||||||
|
|
||||||
|
@ -218,6 +557,34 @@ const FieldPanel: FC<FieldPanelProps> = ({
|
||||||
onSelectionChange={onSelectionChange}
|
onSelectionChange={onSelectionChange}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : 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>
|
</EuiPanel>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
|
@ -8,8 +8,8 @@
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { type QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
|
import { type QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
import { useRefresh } from '@kbn/ml-date-picker';
|
|
||||||
import { isDefined } from '@kbn/ml-is-defined';
|
import { isDefined } from '@kbn/ml-is-defined';
|
||||||
|
import { useReload } from '../../hooks/use_reload';
|
||||||
import { useAiopsAppContext } from '../../hooks/use_aiops_app_context';
|
import { useAiopsAppContext } from '../../hooks/use_aiops_app_context';
|
||||||
import {
|
import {
|
||||||
ChangePointAnnotation,
|
ChangePointAnnotation,
|
||||||
|
@ -102,7 +102,7 @@ function getChangePointDetectionRequestBody(
|
||||||
index,
|
index,
|
||||||
size: 0,
|
size: 0,
|
||||||
body: {
|
body: {
|
||||||
query,
|
...(query ? { query } : {}),
|
||||||
aggregations,
|
aggregations,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -121,7 +121,7 @@ export function useChangePointResults(
|
||||||
|
|
||||||
const { dataView } = useDataSource();
|
const { dataView } = useDataSource();
|
||||||
|
|
||||||
const refresh = useRefresh();
|
const { refreshTimestamp: refresh } = useReload();
|
||||||
|
|
||||||
const [results, setResults] = useState<ChangePointAnnotation[]>([]);
|
const [results, setResults] = useState<ChangePointAnnotation[]>([]);
|
||||||
/**
|
/**
|
||||||
|
@ -164,6 +164,7 @@ export function useChangePointResults(
|
||||||
},
|
},
|
||||||
query
|
query
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = await runRequest<
|
const result = await runRequest<
|
||||||
typeof requestPayload,
|
typeof requestPayload,
|
||||||
{ rawResponse: ChangePointAggResponse }
|
{ rawResponse: ChangePointAggResponse }
|
||||||
|
|
|
@ -8,31 +8,30 @@
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { FilterStateStore, type TimeRange } from '@kbn/es-query';
|
import { FilterStateStore, type TimeRange } from '@kbn/es-query';
|
||||||
import { type TypedLensByValueInput } from '@kbn/lens-plugin/public';
|
import { type TypedLensByValueInput } from '@kbn/lens-plugin/public';
|
||||||
import { useTimeRangeUpdates } from '@kbn/ml-date-picker';
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
import { useFilerQueryUpdates } from '../../hooks/use_filters_query';
|
||||||
import { fnOperationTypeMapping } from './constants';
|
import { fnOperationTypeMapping } from './constants';
|
||||||
import { useDataSource } from '../../hooks/use_data_source';
|
import { useDataSource } from '../../hooks/use_data_source';
|
||||||
import {
|
import { ChangePointAnnotation, FieldConfig } from './change_point_detection_context';
|
||||||
ChangePointAnnotation,
|
|
||||||
FieldConfig,
|
|
||||||
useChangePointDetectionContext,
|
|
||||||
} from './change_point_detection_context';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides common props for the Lens Embeddable component
|
* Provides common props for the Lens Embeddable component
|
||||||
|
* based on the change point definition and currently applied filters and query.
|
||||||
*/
|
*/
|
||||||
export const useCommonChartProps = ({
|
export const useCommonChartProps = ({
|
||||||
annotation,
|
annotation,
|
||||||
fieldConfig,
|
fieldConfig,
|
||||||
previewMode = false,
|
previewMode = false,
|
||||||
|
bucketInterval,
|
||||||
}: {
|
}: {
|
||||||
fieldConfig: FieldConfig;
|
fieldConfig: FieldConfig;
|
||||||
annotation: ChangePointAnnotation;
|
annotation: ChangePointAnnotation;
|
||||||
previewMode?: boolean;
|
previewMode?: boolean;
|
||||||
|
bucketInterval: string;
|
||||||
}): Partial<TypedLensByValueInput> => {
|
}): Partial<TypedLensByValueInput> => {
|
||||||
const timeRange = useTimeRangeUpdates(true);
|
|
||||||
const { dataView } = useDataSource();
|
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,
|
* In order to correctly render annotations for change points at the edges,
|
||||||
|
@ -48,6 +47,7 @@ export const useCommonChartProps = ({
|
||||||
const filters = useMemo(() => {
|
const filters = useMemo(() => {
|
||||||
return [
|
return [
|
||||||
...resultFilters,
|
...resultFilters,
|
||||||
|
// Adds a filter for change point partition value
|
||||||
...(annotation.group
|
...(annotation.group
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
|
@ -161,7 +161,9 @@ export const useCommonChartProps = ({
|
||||||
outside: false,
|
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,
|
isBucketed: true,
|
||||||
scale: 'interval',
|
scale: 'interval',
|
||||||
params: {
|
params: {
|
||||||
interval: bucketInterval.expression,
|
interval: bucketInterval,
|
||||||
includeEmptyRows: true,
|
includeEmptyRows: true,
|
||||||
dropPartials: false,
|
dropPartials: false,
|
||||||
},
|
},
|
||||||
|
@ -219,7 +221,7 @@ export const useCommonChartProps = ({
|
||||||
dataView.timeFieldName,
|
dataView.timeFieldName,
|
||||||
resultQuery,
|
resultQuery,
|
||||||
filters,
|
filters,
|
||||||
bucketInterval.expression,
|
bucketInterval,
|
||||||
fieldConfig.fn,
|
fieldConfig.fn,
|
||||||
fieldConfig.metricField,
|
fieldConfig.metricField,
|
||||||
gridAndLabelsVisibility,
|
gridAndLabelsVisibility,
|
||||||
|
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
8
x-pack/plugins/aiops/public/embeddable/const.ts
Normal file
8
x-pack/plugins/aiops/public/embeddable/const.ts
Normal 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;
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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} />;
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
9
x-pack/plugins/aiops/public/embeddable/index.ts
Normal file
9
x-pack/plugins/aiops/public/embeddable/index.ts
Normal 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';
|
|
@ -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);
|
||||||
|
};
|
|
@ -29,6 +29,9 @@ import type {
|
||||||
FieldStatsServices,
|
FieldStatsServices,
|
||||||
} from '@kbn/unified-field-list/src/components/field_stats';
|
} from '@kbn/unified-field-list/src/components/field_stats';
|
||||||
import type { TimeRange as TimeRangeMs } from '@kbn/ml-date-picker';
|
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.
|
* AIOps App Dependencies to be provided via React context.
|
||||||
|
@ -104,6 +107,9 @@ export interface AiopsAppDependencies {
|
||||||
dslQuery?: FieldStatsProps['dslQuery'];
|
dslQuery?: FieldStatsProps['dslQuery'];
|
||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
|
presentationUtil?: PresentationUtilPluginStart;
|
||||||
|
embeddable?: EmbeddableStart;
|
||||||
|
cases?: CasesUiStart;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
52
x-pack/plugins/aiops/public/hooks/use_cases_modal.ts
Normal file
52
x-pack/plugins/aiops/public/hooks/use_cases_modal.ts
Normal 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]
|
||||||
|
);
|
||||||
|
};
|
|
@ -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);
|
|
||||||
}
|
|
105
x-pack/plugins/aiops/public/hooks/use_data_source.tsx
Normal file
105
x-pack/plugins/aiops/public/hooks/use_data_source.tsx
Normal 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>;
|
||||||
|
};
|
86
x-pack/plugins/aiops/public/hooks/use_filters_query.tsx
Normal file
86
x-pack/plugins/aiops/public/hooks/use_filters_query.tsx
Normal 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);
|
||||||
|
};
|
30
x-pack/plugins/aiops/public/hooks/use_reload.tsx
Normal file
30
x-pack/plugins/aiops/public/hooks/use_reload.tsx
Normal 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);
|
||||||
|
};
|
|
@ -6,23 +6,42 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { CoreStart, Plugin } from '@kbn/core/public';
|
import type { CoreStart, Plugin } from '@kbn/core/public';
|
||||||
|
import { type CoreSetup } from '@kbn/core/public';
|
||||||
import { firstValueFrom } from 'rxjs';
|
import { firstValueFrom } from 'rxjs';
|
||||||
|
import type {
|
||||||
import {
|
|
||||||
AiopsPluginSetup,
|
AiopsPluginSetup,
|
||||||
AiopsPluginSetupDeps,
|
AiopsPluginSetupDeps,
|
||||||
AiopsPluginStart,
|
AiopsPluginStart,
|
||||||
AiopsPluginStartDeps,
|
AiopsPluginStartDeps,
|
||||||
} from './types';
|
} from './types';
|
||||||
|
import { getEmbeddableChangePointChart } from './embeddable/embeddable_change_point_chart_component';
|
||||||
|
|
||||||
export class AiopsPlugin
|
export class AiopsPlugin
|
||||||
implements Plugin<AiopsPluginSetup, AiopsPluginStart, AiopsPluginSetupDeps, AiopsPluginStartDeps>
|
implements Plugin<AiopsPluginSetup, AiopsPluginStart, AiopsPluginSetupDeps, AiopsPluginStartDeps>
|
||||||
{
|
{
|
||||||
public setup() {
|
public setup(
|
||||||
return {};
|
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
|
// importing async to keep the aiops plugin size to a minimum
|
||||||
Promise.all([
|
Promise.all([
|
||||||
import('@kbn/ui-actions-plugin/public'),
|
import('@kbn/ui-actions-plugin/public'),
|
||||||
|
@ -42,7 +61,9 @@ export class AiopsPlugin
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return {};
|
return {
|
||||||
|
EmbeddableChangePointChart: getEmbeddableChangePointChart(core, plugins),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public stop() {}
|
public stop() {}
|
|
@ -15,9 +15,16 @@ import type { LicensingPluginStart } from '@kbn/licensing-plugin/public';
|
||||||
import type { SharePluginStart } from '@kbn/share-plugin/public';
|
import type { SharePluginStart } from '@kbn/share-plugin/public';
|
||||||
import type { UiActionsStart } from '@kbn/ui-actions-plugin/public';
|
import type { UiActionsStart } from '@kbn/ui-actions-plugin/public';
|
||||||
import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-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 {
|
export interface AiopsPluginStartDeps {
|
||||||
data: DataPublicPluginStart;
|
data: DataPublicPluginStart;
|
||||||
|
@ -30,12 +37,10 @@ export interface AiopsPluginStartDeps {
|
||||||
storage: IStorageWrapper;
|
storage: IStorageWrapper;
|
||||||
licensing: LicensingPluginStart;
|
licensing: LicensingPluginStart;
|
||||||
executionContext: ExecutionContextStart;
|
executionContext: ExecutionContextStart;
|
||||||
|
embeddable: EmbeddableStart;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export type AiopsPluginSetup = void;
|
||||||
* aiops plugin server setup contract
|
export interface AiopsPluginStart {
|
||||||
*/
|
EmbeddableChangePointChart: React.ComponentType<EmbeddableChangePointChartProps>;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
}
|
||||||
export interface AiopsPluginSetup {}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
|
||||||
export interface AiopsPluginStart {}
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { Subscription } from 'rxjs';
|
||||||
import { PluginInitializerContext, CoreSetup, CoreStart, Plugin, Logger } from '@kbn/core/server';
|
import { PluginInitializerContext, CoreSetup, CoreStart, Plugin, Logger } from '@kbn/core/server';
|
||||||
import type { DataRequestHandlerContext } from '@kbn/data-plugin/server';
|
import type { DataRequestHandlerContext } from '@kbn/data-plugin/server';
|
||||||
|
|
||||||
|
import { CASES_ATTACHMENT_CHANGE_POINT_CHART } from '../common/constants';
|
||||||
import { isActiveLicense } from './lib/license';
|
import { isActiveLicense } from './lib/license';
|
||||||
import {
|
import {
|
||||||
AiopsLicense,
|
AiopsLicense,
|
||||||
|
@ -54,6 +55,12 @@ export class AiopsPlugin
|
||||||
defineLogCategorizationRoutes(router, aiopsLicense);
|
defineLogCategorizationRoutes(router, aiopsLicense);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (plugins.cases) {
|
||||||
|
plugins.cases.attachmentFramework.registerPersistableState({
|
||||||
|
id: CASES_ATTACHMENT_CHANGE_POINT_CHART,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,10 +7,12 @@
|
||||||
|
|
||||||
import type { PluginSetup, PluginStart } from '@kbn/data-plugin/server';
|
import type { PluginSetup, PluginStart } from '@kbn/data-plugin/server';
|
||||||
import type { LicensingPluginStart } from '@kbn/licensing-plugin/server';
|
import type { LicensingPluginStart } from '@kbn/licensing-plugin/server';
|
||||||
|
import type { CasesSetup } from '@kbn/cases-plugin/server';
|
||||||
|
|
||||||
export interface AiopsPluginSetupDeps {
|
export interface AiopsPluginSetupDeps {
|
||||||
data: PluginSetup;
|
data: PluginSetup;
|
||||||
licensing: LicensingPluginStart;
|
licensing: LicensingPluginStart;
|
||||||
|
cases?: CasesSetup;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AiopsPluginStartDeps {
|
export interface AiopsPluginStartDeps {
|
||||||
|
|
|
@ -57,6 +57,11 @@
|
||||||
"@kbn/unified-field-list",
|
"@kbn/unified-field-list",
|
||||||
"@kbn/unified-search-plugin",
|
"@kbn/unified-search-plugin",
|
||||||
"@kbn/utility-types",
|
"@kbn/utility-types",
|
||||||
|
"@kbn/presentation-util-plugin",
|
||||||
|
"@kbn/embeddable-plugin",
|
||||||
|
"@kbn/core-theme-browser",
|
||||||
|
"@kbn/core-lifecycle-browser",
|
||||||
|
"@kbn/cases-plugin",
|
||||||
],
|
],
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"target/**/*",
|
"target/**/*",
|
||||||
|
|
|
@ -28,7 +28,8 @@
|
||||||
"unifiedSearch",
|
"unifiedSearch",
|
||||||
"savedObjectsManagement",
|
"savedObjectsManagement",
|
||||||
"savedSearch",
|
"savedSearch",
|
||||||
"contentManagement"
|
"contentManagement",
|
||||||
|
"presentationUtil"
|
||||||
],
|
],
|
||||||
"optionalPlugins": [
|
"optionalPlugins": [
|
||||||
"alerting",
|
"alerting",
|
||||||
|
|
|
@ -60,6 +60,9 @@ export const ChangePointDetectionPage: FC = () => {
|
||||||
'unifiedSearch',
|
'unifiedSearch',
|
||||||
'theme',
|
'theme',
|
||||||
'lens',
|
'lens',
|
||||||
|
'presentationUtil',
|
||||||
|
'embeddable',
|
||||||
|
'cases',
|
||||||
]),
|
]),
|
||||||
fieldStats: { useFieldStatsTrigger, FieldStatsFlyoutProvider },
|
fieldStats: { useFieldStatsTrigger, FieldStatsFlyoutProvider },
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -102,6 +102,7 @@ const App: FC<AppProps> = ({ coreStart, deps, appMountParams }) => {
|
||||||
savedObjectsManagement: deps.savedObjectsManagement,
|
savedObjectsManagement: deps.savedObjectsManagement,
|
||||||
savedSearch: deps.savedSearch,
|
savedSearch: deps.savedSearch,
|
||||||
contentManagement: deps.contentManagement,
|
contentManagement: deps.contentManagement,
|
||||||
|
presentationUtil: deps.presentationUtil,
|
||||||
...coreStart,
|
...coreStart,
|
||||||
mlServices: getMlGlobalServices(coreStart.http, deps.usageCollection),
|
mlServices: getMlGlobalServices(coreStart.http, deps.usageCollection),
|
||||||
};
|
};
|
||||||
|
|
|
@ -28,6 +28,7 @@ import type { LensPublicStart } from '@kbn/lens-plugin/public';
|
||||||
import type { SavedObjectsManagementPluginStart } from '@kbn/saved-objects-management-plugin/public';
|
import type { SavedObjectsManagementPluginStart } from '@kbn/saved-objects-management-plugin/public';
|
||||||
import type { ContentManagementPublicStart } from '@kbn/content-management-plugin/public';
|
import type { ContentManagementPublicStart } from '@kbn/content-management-plugin/public';
|
||||||
import type { SavedSearchPublicPluginStart } from '@kbn/saved-search-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';
|
import type { MlServicesContext } from '../../app';
|
||||||
|
|
||||||
interface StartPlugins {
|
interface StartPlugins {
|
||||||
|
@ -53,6 +54,7 @@ interface StartPlugins {
|
||||||
savedObjectsManagement: SavedObjectsManagementPluginStart;
|
savedObjectsManagement: SavedObjectsManagementPluginStart;
|
||||||
savedSearch: SavedSearchPublicPluginStart;
|
savedSearch: SavedSearchPublicPluginStart;
|
||||||
contentManagement: ContentManagementPublicStart;
|
contentManagement: ContentManagementPublicStart;
|
||||||
|
presentationUtil: PresentationUtilPluginStart;
|
||||||
}
|
}
|
||||||
export type StartServices = CoreStart &
|
export type StartServices = CoreStart &
|
||||||
StartPlugins & {
|
StartPlugins & {
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { i18n } from '@kbn/i18n';
|
||||||
import type { StartServicesAccessor } from '@kbn/core/public';
|
import type { StartServicesAccessor } from '@kbn/core/public';
|
||||||
|
|
||||||
import type { EmbeddableFactoryDefinition, IContainer } from '@kbn/embeddable-plugin/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 { PLUGIN_ID, PLUGIN_ICON, ML_APP_NAME } from '../../../common/constants/app';
|
||||||
import { HttpService } from '../../application/services/http_service';
|
import { HttpService } from '../../application/services/http_service';
|
||||||
import type { MlPluginStart, MlStartDependencies } from '../../plugin';
|
import type { MlPluginStart, MlStartDependencies } from '../../plugin';
|
||||||
|
@ -94,7 +95,7 @@ export class AnomalySwimlaneEmbeddableFactory
|
||||||
public async create(
|
public async create(
|
||||||
initialInput: AnomalySwimlaneEmbeddableInput,
|
initialInput: AnomalySwimlaneEmbeddableInput,
|
||||||
parent?: IContainer
|
parent?: IContainer
|
||||||
): Promise<any> {
|
): Promise<InstanceType<IAnomalySwimlaneEmbeddable>> {
|
||||||
const services = await this.getServices();
|
const services = await this.getServices();
|
||||||
const { AnomalySwimlaneEmbeddable } = await import('./anomaly_swimlane_embeddable');
|
const { AnomalySwimlaneEmbeddable } = await import('./anomaly_swimlane_embeddable');
|
||||||
return new AnomalySwimlaneEmbeddable(initialInput, services, parent);
|
return new AnomalySwimlaneEmbeddable(initialInput, services, parent);
|
||||||
|
|
|
@ -47,6 +47,7 @@ import type { DashboardSetup, DashboardStart } from '@kbn/dashboard-plugin/publi
|
||||||
import type { ChartsPluginStart } from '@kbn/charts-plugin/public';
|
import type { ChartsPluginStart } from '@kbn/charts-plugin/public';
|
||||||
import type { CasesUiSetup, CasesUiStart } from '@kbn/cases-plugin/public';
|
import type { CasesUiSetup, CasesUiStart } from '@kbn/cases-plugin/public';
|
||||||
import type { SavedSearchPublicPluginStart } from '@kbn/saved-search-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 { registerManagementSection } from './application/management';
|
||||||
import { MlLocatorDefinition, type MlLocator } from './locator';
|
import { MlLocatorDefinition, type MlLocator } from './locator';
|
||||||
import { setDependencyCache } from './application/util/dependency_cache';
|
import { setDependencyCache } from './application/util/dependency_cache';
|
||||||
|
@ -75,6 +76,7 @@ export interface MlStartDependencies {
|
||||||
savedObjectsManagement: SavedObjectsManagementPluginStart;
|
savedObjectsManagement: SavedObjectsManagementPluginStart;
|
||||||
savedSearch: SavedSearchPublicPluginStart;
|
savedSearch: SavedSearchPublicPluginStart;
|
||||||
contentManagement: ContentManagementPublicStart;
|
contentManagement: ContentManagementPublicStart;
|
||||||
|
presentationUtil: PresentationUtilPluginStart;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MlSetupDependencies {
|
export interface MlSetupDependencies {
|
||||||
|
@ -145,6 +147,7 @@ export class MlPlugin implements Plugin<MlPluginSetup, MlPluginStart> {
|
||||||
savedObjectsManagement: pluginsStart.savedObjectsManagement,
|
savedObjectsManagement: pluginsStart.savedObjectsManagement,
|
||||||
savedSearch: pluginsStart.savedSearch,
|
savedSearch: pluginsStart.savedSearch,
|
||||||
contentManagement: pluginsStart.contentManagement,
|
contentManagement: pluginsStart.contentManagement,
|
||||||
|
presentationUtil: pluginsStart.presentationUtil,
|
||||||
},
|
},
|
||||||
params
|
params
|
||||||
);
|
);
|
||||||
|
|
|
@ -102,5 +102,6 @@
|
||||||
"@kbn/core-ui-settings-browser",
|
"@kbn/core-ui-settings-browser",
|
||||||
"@kbn/content-management-plugin",
|
"@kbn/content-management-plugin",
|
||||||
"@kbn/ml-in-memory-table",
|
"@kbn/ml-in-memory-table",
|
||||||
|
"@kbn/presentation-util-plugin",
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
|
@ -291,6 +291,7 @@ export default ({ getService }: FtrProviderContext): void => {
|
||||||
expect(types).to.eql({
|
expect(types).to.eql({
|
||||||
'.lens': '78559fd806809ac3a1008942ead2a079864054f5',
|
'.lens': '78559fd806809ac3a1008942ead2a079864054f5',
|
||||||
'.test': 'ab2204830c67f5cf992c9aa2f7e3ead752cc60a1',
|
'.test': 'ab2204830c67f5cf992c9aa2f7e3ead752cc60a1',
|
||||||
|
aiopsChangePointChart: 'a1212d71947ec34487b374cecc47ab9941b5d91c',
|
||||||
ml_anomaly_charts: '23e92e824af9db6e8b8bb1d63c222e04f57d2147',
|
ml_anomaly_charts: '23e92e824af9db6e8b8bb1d63c222e04f57d2147',
|
||||||
ml_anomaly_swimlane: 'a3517f3e53fb041e9cbb150477fb6ef0f731bd5f',
|
ml_anomaly_swimlane: 'a3517f3e53fb041e9cbb150477fb6ef0f731bd5f',
|
||||||
});
|
});
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue