mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -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.
|
||||
*/
|
||||
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",
|
||||
"licensing",
|
||||
"uiActions",
|
||||
"embeddable",
|
||||
"presentationUtil",
|
||||
"dashboard",
|
||||
"fieldFormats"
|
||||
],
|
||||
"optionalPlugins": [
|
||||
"cases"
|
||||
],
|
||||
"requiredBundles": [
|
||||
"fieldFormats",
|
||||
"kibanaReact",
|
||||
"kibanaUtils"
|
||||
"kibanaUtils",
|
||||
"embeddable",
|
||||
"cases"
|
||||
],
|
||||
"extraPublicDirs": [
|
||||
"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 { ES_FIELD_TYPES } from '@kbn/field-types';
|
||||
import { type QueryDslQueryContainer } from '@kbn/data-views-plugin/common/types';
|
||||
import { FilterQueryContextProvider } from '../../hooks/use_filters_query';
|
||||
import { type ChangePointType, DEFAULT_AGG_FUNCTION } from './constants';
|
||||
import {
|
||||
createMergedEsQuery,
|
||||
|
@ -254,7 +255,7 @@ export const ChangePointDetectionContextProvider: FC = ({ children }) => {
|
|||
|
||||
return (
|
||||
<ChangePointDetectionContext.Provider value={value}>
|
||||
{children}
|
||||
<FilterQueryContextProvider>{children}</FilterQueryContextProvider>
|
||||
</ChangePointDetectionContext.Provider>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -21,7 +21,7 @@ import {
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import type { Query } from '@kbn/es-query';
|
||||
import { ChartsGrid } from './charts_grid';
|
||||
import { ChartsGridContainer } from './charts_grid';
|
||||
import { FieldsConfig } from './fields_config';
|
||||
import { useDataSource } from '../../hooks/use_data_source';
|
||||
import { ChangePointTypeFilter } from './change_point_type_filter';
|
||||
|
@ -163,7 +163,7 @@ export const ChangePointDetectionPage: FC = () => {
|
|||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
<ChartsGrid changePoints={selectedChangePoints} />
|
||||
<ChartsGridContainer changePoints={selectedChangePoints} />
|
||||
</EuiFlyoutBody>
|
||||
</EuiFlyout>
|
||||
) : null}
|
||||
|
|
|
@ -5,9 +5,10 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { FC } from 'react';
|
||||
import React, { FC, useMemo } from 'react';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { pick } from 'lodash';
|
||||
|
||||
import { EuiThemeProvider as StyledComponentsThemeProvider } from '@kbn/kibana-react-plugin/common';
|
||||
import { EuiSpacer } from '@elastic/eui';
|
||||
|
||||
import { DataView } from '@kbn/data-views-plugin/common';
|
||||
|
@ -15,10 +16,11 @@ import type { SavedSearch } from '@kbn/saved-search-plugin/public';
|
|||
import { StorageContextProvider } from '@kbn/ml-local-storage';
|
||||
import { UrlStateProvider } from '@kbn/ml-url-state';
|
||||
import { Storage } from '@kbn/kibana-utils-plugin/public';
|
||||
import { DatePickerContextProvider } from '@kbn/ml-date-picker';
|
||||
import { DatePickerContextProvider, mlTimefilterRefresh$ } from '@kbn/ml-date-picker';
|
||||
import { UI_SETTINGS } from '@kbn/data-plugin/common';
|
||||
import { toMountPoint, wrapWithTheme } from '@kbn/kibana-react-plugin/public';
|
||||
|
||||
import { type Observable } from 'rxjs';
|
||||
import { DataSourceContext } from '../../hooks/use_data_source';
|
||||
import { AiopsAppContext, AiopsAppDependencies } from '../../hooks/use_aiops_app_context';
|
||||
import { AIOPS_STORAGE_KEYS } from '../../types/storage';
|
||||
|
@ -28,6 +30,7 @@ import { PageHeader } from '../page_header';
|
|||
import { ChangePointDetectionPage } from './change_point_detection_page';
|
||||
import { ChangePointDetectionContextProvider } from './change_point_detection_context';
|
||||
import { timeSeriesDataViewWarning } from '../../application/utils/time_series_dataview_check';
|
||||
import { ReloadContextProvider } from '../../hooks/use_reload';
|
||||
|
||||
const localStorage = new Storage(window.localStorage);
|
||||
|
||||
|
@ -57,25 +60,43 @@ export const ChangePointDetectionAppState: FC<ChangePointDetectionAppStateProps>
|
|||
|
||||
const warning = timeSeriesDataViewWarning(dataView, 'change_point_detection');
|
||||
|
||||
const reload$ = useMemo<Observable<number>>(() => {
|
||||
return mlTimefilterRefresh$.pipe(map((v) => v.lastRefresh));
|
||||
}, []);
|
||||
|
||||
if (warning !== null) {
|
||||
return <>{warning}</>;
|
||||
}
|
||||
|
||||
const PresentationContextProvider =
|
||||
appDependencies.presentationUtil?.ContextProvider ?? React.Fragment;
|
||||
|
||||
const CasesContext = appDependencies.cases?.ui.getCasesContext() ?? React.Fragment;
|
||||
const casesPermissions = appDependencies.cases?.helpers.canUseCases();
|
||||
|
||||
return (
|
||||
<AiopsAppContext.Provider value={appDependencies}>
|
||||
<UrlStateProvider>
|
||||
<DataSourceContext.Provider value={{ dataView, savedSearch }}>
|
||||
<StorageContextProvider storage={localStorage} storageKeys={AIOPS_STORAGE_KEYS}>
|
||||
<DatePickerContextProvider {...datePickerDeps}>
|
||||
<PageHeader />
|
||||
<EuiSpacer />
|
||||
<ChangePointDetectionContextProvider>
|
||||
<ChangePointDetectionPage />
|
||||
</ChangePointDetectionContextProvider>
|
||||
</DatePickerContextProvider>
|
||||
</StorageContextProvider>
|
||||
</DataSourceContext.Provider>
|
||||
</UrlStateProvider>
|
||||
</AiopsAppContext.Provider>
|
||||
<PresentationContextProvider>
|
||||
<StyledComponentsThemeProvider>
|
||||
<CasesContext owner={[]} permissions={casesPermissions!}>
|
||||
<AiopsAppContext.Provider value={appDependencies}>
|
||||
<UrlStateProvider>
|
||||
<DataSourceContext.Provider value={{ dataView, savedSearch }}>
|
||||
<StorageContextProvider storage={localStorage} storageKeys={AIOPS_STORAGE_KEYS}>
|
||||
<DatePickerContextProvider {...datePickerDeps}>
|
||||
<PageHeader />
|
||||
<EuiSpacer />
|
||||
<ReloadContextProvider reload$={reload$}>
|
||||
<ChangePointDetectionContextProvider>
|
||||
<ChangePointDetectionPage />
|
||||
</ChangePointDetectionContextProvider>
|
||||
</ReloadContextProvider>
|
||||
</DatePickerContextProvider>
|
||||
</StorageContextProvider>
|
||||
</DataSourceContext.Provider>
|
||||
</UrlStateProvider>
|
||||
</AiopsAppContext.Provider>
|
||||
</CasesContext>
|
||||
</StyledComponentsThemeProvider>
|
||||
</PresentationContextProvider>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -19,12 +19,14 @@ import { i18n } from '@kbn/i18n';
|
|||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { EuiTableSelectionType } from '@elastic/eui/src/components/basic_table/table_types';
|
||||
import { type Filter, FilterStateStore } from '@kbn/es-query';
|
||||
import { NoChangePointsWarning } from './no_change_points_warning';
|
||||
import { useDataSource } from '../../hooks/use_data_source';
|
||||
import { useCommonChartProps } from './use_common_chart_props';
|
||||
import {
|
||||
type ChangePointAnnotation,
|
||||
FieldConfig,
|
||||
SelectedChangePoint,
|
||||
useChangePointDetectionContext,
|
||||
} from './change_point_detection_context';
|
||||
import { type ChartComponentProps } from './chart_component';
|
||||
import { useAiopsAppContext } from '../../hooks/use_aiops_app_context';
|
||||
|
@ -92,6 +94,8 @@ export const ChangePointsTable: FC<ChangePointsTableProps> = ({
|
|||
|
||||
const hasActions = fieldConfig.splitField !== undefined;
|
||||
|
||||
const { bucketInterval } = useChangePointDetectionContext();
|
||||
|
||||
const columns: Array<EuiBasicTableColumn<ChangePointAnnotation>> = [
|
||||
{
|
||||
id: 'timestamp',
|
||||
|
@ -122,7 +126,13 @@ export const ChangePointsTable: FC<ChangePointsTableProps> = ({
|
|||
['&.euiTableCellContent']: { display: 'block', padding: 0 },
|
||||
},
|
||||
render: (annotation: ChangePointAnnotation) => {
|
||||
return <MiniChartPreview annotation={annotation} fieldConfig={fieldConfig} />;
|
||||
return (
|
||||
<MiniChartPreview
|
||||
annotation={annotation}
|
||||
fieldConfig={fieldConfig}
|
||||
interval={bucketInterval.expression}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -283,25 +293,7 @@ export const ChangePointsTable: FC<ChangePointsTableProps> = ({
|
|||
}
|
||||
/>
|
||||
) : (
|
||||
<EuiEmptyPrompt
|
||||
iconType="search"
|
||||
title={
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="xpack.aiops.changePointDetection.noChangePointsFoundTitle"
|
||||
defaultMessage="No change points found"
|
||||
/>
|
||||
</h2>
|
||||
}
|
||||
body={
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.aiops.changePointDetection.noChangePointsFoundMessage"
|
||||
defaultMessage="Detect statistically significant change points such as dips, spikes, and distribution changes in a metric. Select a metric and set a time range to start detecting change points in your data."
|
||||
/>
|
||||
</p>
|
||||
}
|
||||
/>
|
||||
<NoChangePointsWarning />
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
@ -313,10 +305,13 @@ export const MiniChartPreview: FC<ChartComponentProps> = ({ fieldConfig, annotat
|
|||
lens: { EmbeddableComponent },
|
||||
} = useAiopsAppContext();
|
||||
|
||||
const { bucketInterval } = useChangePointDetectionContext();
|
||||
|
||||
const { filters, query, attributes, timeRange } = useCommonChartProps({
|
||||
annotation,
|
||||
fieldConfig,
|
||||
previewMode: true,
|
||||
bucketInterval: bucketInterval.expression,
|
||||
});
|
||||
|
||||
return (
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import React, { FC } from 'react';
|
||||
import type { Filter, Query, TimeRange } from '@kbn/es-query';
|
||||
import { useCommonChartProps } from './use_common_chart_props';
|
||||
import { useAiopsAppContext } from '../../hooks/use_aiops_app_context';
|
||||
import type { ChangePointAnnotation, FieldConfig } from './change_point_detection_context';
|
||||
|
@ -13,33 +14,48 @@ import type { ChangePointAnnotation, FieldConfig } from './change_point_detectio
|
|||
export interface ChartComponentProps {
|
||||
fieldConfig: FieldConfig;
|
||||
annotation: ChangePointAnnotation;
|
||||
|
||||
interval: string;
|
||||
}
|
||||
|
||||
export const ChartComponent: FC<ChartComponentProps> = React.memo(({ annotation, fieldConfig }) => {
|
||||
const {
|
||||
lens: { EmbeddableComponent },
|
||||
} = useAiopsAppContext();
|
||||
export interface ChartComponentPropsAll {
|
||||
fn: string;
|
||||
metricField: string;
|
||||
splitField?: string;
|
||||
maxResults: number;
|
||||
timeRange: TimeRange;
|
||||
filters?: Filter[];
|
||||
query?: Query;
|
||||
}
|
||||
|
||||
const { filters, timeRange, query, attributes } = useCommonChartProps({
|
||||
fieldConfig,
|
||||
annotation,
|
||||
});
|
||||
export const ChartComponent: FC<ChartComponentProps> = React.memo(
|
||||
({ annotation, fieldConfig, interval }) => {
|
||||
const {
|
||||
lens: { EmbeddableComponent },
|
||||
} = useAiopsAppContext();
|
||||
|
||||
return (
|
||||
<EmbeddableComponent
|
||||
id={`changePointChart_${annotation.group ? annotation.group.value : annotation.label}`}
|
||||
style={{ height: 350 }}
|
||||
timeRange={timeRange}
|
||||
query={query}
|
||||
filters={filters}
|
||||
// @ts-ignore
|
||||
attributes={attributes}
|
||||
renderMode={'view'}
|
||||
executionContext={{
|
||||
type: 'aiops_change_point_detection_chart',
|
||||
name: 'Change point detection',
|
||||
}}
|
||||
disableTriggers
|
||||
/>
|
||||
);
|
||||
});
|
||||
const { filters, timeRange, query, attributes } = useCommonChartProps({
|
||||
fieldConfig,
|
||||
annotation,
|
||||
bucketInterval: interval,
|
||||
});
|
||||
|
||||
return (
|
||||
<EmbeddableComponent
|
||||
id={`changePointChart_${annotation.group ? annotation.group.value : annotation.label}`}
|
||||
style={{ height: 350 }}
|
||||
timeRange={timeRange}
|
||||
query={query}
|
||||
filters={filters}
|
||||
// @ts-ignore
|
||||
attributes={attributes}
|
||||
renderMode={'view'}
|
||||
executionContext={{
|
||||
type: 'aiops_change_point_detection_chart',
|
||||
name: 'Change point detection',
|
||||
}}
|
||||
disableTriggers
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
|
@ -23,7 +23,10 @@ import { i18n } from '@kbn/i18n';
|
|||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { useTimefilter } from '@kbn/ml-date-picker';
|
||||
import { type RefreshInterval } from '@kbn/data-plugin/common';
|
||||
import { type SelectedChangePoint } from './change_point_detection_context';
|
||||
import {
|
||||
type SelectedChangePoint,
|
||||
useChangePointDetectionContext,
|
||||
} from './change_point_detection_context';
|
||||
import { ChartComponent } from './chart_component';
|
||||
|
||||
const CHARTS_PER_PAGE = 6;
|
||||
|
@ -32,11 +35,115 @@ interface ChartsGridProps {
|
|||
changePoints: Record<number, SelectedChangePoint[]>;
|
||||
}
|
||||
|
||||
export const ChartsGrid: FC<ChartsGridProps> = ({ changePoints: changePointsDict }) => {
|
||||
/**
|
||||
* Shared component for change point charts grid.
|
||||
* Used both in AIOps UI and inside embeddable.
|
||||
*
|
||||
* @param changePoints
|
||||
* @constructor
|
||||
*/
|
||||
export const ChartsGrid: FC<{ changePoints: SelectedChangePoint[]; interval: string }> = ({
|
||||
changePoints,
|
||||
interval,
|
||||
}) => {
|
||||
return (
|
||||
<EuiFlexGrid
|
||||
columns={changePoints.length >= 2 ? 2 : 1}
|
||||
responsive
|
||||
gutterSize={'m'}
|
||||
css={{ width: '100%' }}
|
||||
>
|
||||
{changePoints.map((v, index) => {
|
||||
const key = `${index}_${v.group?.value ?? 'single_metric'}_${v.fn}_${v.metricField}_${
|
||||
v.timestamp
|
||||
}_${v.p_value}`;
|
||||
return (
|
||||
<EuiFlexItem key={key}>
|
||||
<EuiPanel paddingSize="s" hasBorder hasShadow={false}>
|
||||
<EuiFlexGroup alignItems={'center'} justifyContent={'spaceBetween'} gutterSize={'s'}>
|
||||
<EuiFlexItem grow={false}>
|
||||
{v.group ? (
|
||||
<EuiDescriptionList
|
||||
type="inline"
|
||||
listItems={[{ title: v.group.name, description: v.group.value }]}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{v.reason ? (
|
||||
<EuiToolTip position="top" content={v.reason}>
|
||||
<EuiIcon
|
||||
tabIndex={0}
|
||||
color={'warning'}
|
||||
type="warning"
|
||||
title={i18n.translate(
|
||||
'xpack.aiops.changePointDetection.notResultsWarning',
|
||||
{
|
||||
defaultMessage: 'No change point agg results warning',
|
||||
}
|
||||
)}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
) : null}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText color={'subdued'} size={'s'}>
|
||||
{v.fn}({v.metricField})
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiHorizontalRule margin="xs" />
|
||||
|
||||
<EuiFlexGroup justifyContent={'spaceBetween'} alignItems={'center'}>
|
||||
{v.p_value !== undefined ? (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiDescriptionList
|
||||
type="inline"
|
||||
listItems={[
|
||||
{
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.aiops.changePointDetection.pValueLabel"
|
||||
defaultMessage="p-value"
|
||||
/>
|
||||
),
|
||||
description: v.p_value.toPrecision(3),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
) : null}
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiBadge color="hollow">{v.type}</EuiBadge>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<ChartComponent
|
||||
fieldConfig={{ splitField: v.splitField, fn: v.fn, metricField: v.metricField }}
|
||||
annotation={v}
|
||||
interval={interval}
|
||||
/>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
})}
|
||||
</EuiFlexGrid>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Wrapper component for change point charts grid.
|
||||
*
|
||||
* @param changePointsDict
|
||||
* @constructor
|
||||
*/
|
||||
export const ChartsGridContainer: FC<ChartsGridProps> = ({ changePoints: changePointsDict }) => {
|
||||
const timefilter = useTimefilter();
|
||||
|
||||
const initialRefreshSetting = useRef<RefreshInterval>();
|
||||
|
||||
const { bucketInterval } = useChangePointDetectionContext();
|
||||
|
||||
useEffect(
|
||||
function pauseRefreshOnMount() {
|
||||
initialRefreshSetting.current = timefilter.getRefreshInterval();
|
||||
|
@ -76,85 +183,7 @@ export const ChartsGrid: FC<ChartsGridProps> = ({ changePoints: changePointsDict
|
|||
|
||||
return (
|
||||
<>
|
||||
<EuiFlexGrid columns={resultPerPage.length >= 2 ? 2 : 1} responsive gutterSize={'m'}>
|
||||
{resultPerPage.map((v, index) => {
|
||||
const key = `${index}_${v.group?.value ?? 'single_metric'}_${v.fn}_${v.metricField}_${
|
||||
v.timestamp
|
||||
}_${v.p_value}`;
|
||||
return (
|
||||
<EuiFlexItem key={key}>
|
||||
<EuiPanel paddingSize="s" hasBorder hasShadow={false}>
|
||||
<EuiFlexGroup
|
||||
alignItems={'center'}
|
||||
justifyContent={'spaceBetween'}
|
||||
gutterSize={'s'}
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
{v.group ? (
|
||||
<EuiDescriptionList
|
||||
type="inline"
|
||||
listItems={[{ title: v.group.name, description: v.group.value }]}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{v.reason ? (
|
||||
<EuiToolTip position="top" content={v.reason}>
|
||||
<EuiIcon
|
||||
tabIndex={0}
|
||||
color={'warning'}
|
||||
type="warning"
|
||||
title={i18n.translate(
|
||||
'xpack.aiops.changePointDetection.notResultsWarning',
|
||||
{
|
||||
defaultMessage: 'No change point agg results warning',
|
||||
}
|
||||
)}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
) : null}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText color={'subdued'} size={'s'}>
|
||||
{v.fn}({v.metricField})
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiHorizontalRule margin="xs" />
|
||||
|
||||
<EuiFlexGroup justifyContent={'spaceBetween'} alignItems={'center'}>
|
||||
{v.p_value !== undefined ? (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiDescriptionList
|
||||
type="inline"
|
||||
listItems={[
|
||||
{
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.aiops.changePointDetection.pValueLabel"
|
||||
defaultMessage="p-value"
|
||||
/>
|
||||
),
|
||||
description: v.p_value.toPrecision(3),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
) : null}
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiBadge color="hollow">{v.type}</EuiBadge>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<ChartComponent
|
||||
fieldConfig={{ splitField: v.splitField, fn: v.fn, metricField: v.metricField }}
|
||||
annotation={v}
|
||||
/>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
})}
|
||||
</EuiFlexGrid>
|
||||
<ChartsGrid changePoints={resultPerPage} interval={bucketInterval.expression} />
|
||||
|
||||
{pagination.pageCount > 1 ? (
|
||||
<EuiFlexGroup justifyContent="spaceAround">
|
||||
|
|
|
@ -10,16 +10,35 @@ import {
|
|||
EuiButton,
|
||||
EuiButtonIcon,
|
||||
EuiCallOut,
|
||||
EuiContextMenu,
|
||||
EuiFieldNumber,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiForm,
|
||||
EuiFormRow,
|
||||
EuiIcon,
|
||||
EuiPanel,
|
||||
EuiPopover,
|
||||
EuiProgress,
|
||||
EuiSpacer,
|
||||
EuiSwitch,
|
||||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { FieldStatsServices } from '@kbn/unified-field-list/src/components/field_stats';
|
||||
import { useTimefilter, useTimeRangeUpdates } from '@kbn/ml-date-picker';
|
||||
import {
|
||||
LazySavedObjectSaveModalDashboard,
|
||||
SaveModalDashboardProps,
|
||||
withSuspense,
|
||||
} from '@kbn/presentation-util-plugin/public';
|
||||
import { EuiContextMenuProps } from '@elastic/eui/src/components/context_menu/context_menu';
|
||||
import { isDefined } from '@kbn/ml-is-defined';
|
||||
import { numberValidator } from '@kbn/ml-agg-utils';
|
||||
import { EMBEDDABLE_CHANGE_POINT_CHART_TYPE } from '../../../common/constants';
|
||||
import { useCasesModal } from '../../hooks/use_cases_modal';
|
||||
import { type EmbeddableChangePointChartInput } from '../../embeddable/embeddable_change_point_chart';
|
||||
import { useDataSource } from '../../hooks/use_data_source';
|
||||
import { useAiopsAppContext } from '../../hooks/use_aiops_app_context';
|
||||
import { ChangePointsTable } from './change_points_table';
|
||||
|
@ -35,9 +54,12 @@ import {
|
|||
} from './change_point_detection_context';
|
||||
import { useChangePointResults } from './use_change_point_agg_request';
|
||||
import { useSplitFieldCardinality } from './use_split_field_cardinality';
|
||||
import { MAX_SERIES } from '../../embeddable/const';
|
||||
|
||||
const selectControlCss = { width: '350px' };
|
||||
|
||||
const SavedObjectSaveModalDashboard = withSuspense(LazySavedObjectSaveModalDashboard);
|
||||
|
||||
/**
|
||||
* Contains panels with controls and change point results.
|
||||
*/
|
||||
|
@ -93,6 +115,7 @@ export const FieldsConfig: FC = () => {
|
|||
return (
|
||||
<React.Fragment key={key}>
|
||||
<FieldPanel
|
||||
panelIndex={index}
|
||||
data-test-subj={`aiopsChangePointPanel_${index}`}
|
||||
fieldConfig={fieldConfig}
|
||||
onChange={(value) => onChange(value, index)}
|
||||
|
@ -121,6 +144,7 @@ export const FieldsConfig: FC = () => {
|
|||
};
|
||||
|
||||
export interface FieldPanelProps {
|
||||
panelIndex: number;
|
||||
fieldConfig: FieldConfig;
|
||||
removeDisabled: boolean;
|
||||
onChange: (update: FieldConfig) => void;
|
||||
|
@ -138,6 +162,7 @@ export interface FieldPanelProps {
|
|||
* @constructor
|
||||
*/
|
||||
const FieldPanel: FC<FieldPanelProps> = ({
|
||||
panelIndex,
|
||||
fieldConfig,
|
||||
onChange,
|
||||
onRemove,
|
||||
|
@ -145,18 +170,315 @@ const FieldPanel: FC<FieldPanelProps> = ({
|
|||
onSelectionChange,
|
||||
'data-test-subj': dataTestSubj,
|
||||
}) => {
|
||||
const { combinedQuery, requestParams } = useChangePointDetectionContext();
|
||||
const {
|
||||
embeddable,
|
||||
application: { capabilities },
|
||||
cases,
|
||||
} = useAiopsAppContext();
|
||||
|
||||
const { dataView } = useDataSource();
|
||||
|
||||
const { combinedQuery, requestParams, selectedChangePoints } = useChangePointDetectionContext();
|
||||
|
||||
const splitFieldCardinality = useSplitFieldCardinality(fieldConfig.splitField, combinedQuery);
|
||||
|
||||
const [isExpanded, setIsExpanded] = useState<boolean>(true);
|
||||
|
||||
const [isActionMenuOpen, setIsActionMenuOpen] = useState(false);
|
||||
|
||||
const canEditDashboards = capabilities.dashboard?.createNew ?? false;
|
||||
const { create: canCreateCase, update: canUpdateCase } = cases?.helpers?.canUseCases() ?? {
|
||||
create: false,
|
||||
update: false,
|
||||
};
|
||||
|
||||
const [dashboardAttachment, setDashboardAttachment] = useState<{
|
||||
applyTimeRange: boolean;
|
||||
maxSeriesToPlot: number;
|
||||
}>({
|
||||
applyTimeRange: false,
|
||||
maxSeriesToPlot: 6,
|
||||
});
|
||||
const [dashboardAttachmentReady, setDashboardAttachmentReady] = useState<boolean>(false);
|
||||
|
||||
const {
|
||||
results: annotations,
|
||||
isLoading: annotationsLoading,
|
||||
progress,
|
||||
} = useChangePointResults(fieldConfig, requestParams, combinedQuery, splitFieldCardinality);
|
||||
|
||||
const openCasesModalCallback = useCasesModal(EMBEDDABLE_CHANGE_POINT_CHART_TYPE);
|
||||
|
||||
const selectedPartitions = useMemo(() => {
|
||||
return (selectedChangePoints[panelIndex] ?? []).map((v) => v.group?.value as string);
|
||||
}, [selectedChangePoints, panelIndex]);
|
||||
|
||||
const caseAttachmentButtonDisabled =
|
||||
isDefined(fieldConfig.splitField) && selectedPartitions.length === 0;
|
||||
|
||||
const timeRange = useTimeRangeUpdates();
|
||||
|
||||
const maxSeriesValidator = useMemo(
|
||||
() => numberValidator({ min: 1, max: MAX_SERIES, integerOnly: true }),
|
||||
[]
|
||||
);
|
||||
|
||||
const maxSeriesInvalid = maxSeriesValidator(dashboardAttachment.maxSeriesToPlot) !== null;
|
||||
|
||||
const panels = useMemo<EuiContextMenuProps['panels']>(() => {
|
||||
return [
|
||||
{
|
||||
id: 'panelActions',
|
||||
size: 's',
|
||||
items: [
|
||||
...(canEditDashboards || canUpdateCase || canCreateCase
|
||||
? [
|
||||
{
|
||||
name:
|
||||
selectedPartitions.length > 0
|
||||
? i18n.translate(
|
||||
'xpack.aiops.changePointDetection.attachSelectedChartsLabel',
|
||||
{
|
||||
defaultMessage: 'Attach selected charts',
|
||||
}
|
||||
)
|
||||
: i18n.translate('xpack.aiops.changePointDetection.attachChartsLabel', {
|
||||
defaultMessage: 'Attach charts',
|
||||
}),
|
||||
icon: 'plusInCircle',
|
||||
panel: 'attachMainPanel',
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
name: i18n.translate('xpack.aiops.changePointDetection.removeConfigLabel', {
|
||||
defaultMessage: 'Remove configuration',
|
||||
}),
|
||||
icon: 'trash',
|
||||
onClick: onRemove,
|
||||
disabled: removeDisabled,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'attachMainPanel',
|
||||
size: 's',
|
||||
initialFocusedItemIndex: 0,
|
||||
title:
|
||||
selectedPartitions.length > 0
|
||||
? i18n.translate('xpack.aiops.changePointDetection.attachSelectedChartsLabel', {
|
||||
defaultMessage: 'Attach selected charts',
|
||||
})
|
||||
: i18n.translate('xpack.aiops.changePointDetection.attachChartsLabel', {
|
||||
defaultMessage: 'Attach charts',
|
||||
}),
|
||||
items: [
|
||||
...(canEditDashboards
|
||||
? [
|
||||
{
|
||||
name: i18n.translate('xpack.aiops.changePointDetection.attachToDashboardLabel', {
|
||||
defaultMessage: 'To dashboard',
|
||||
}),
|
||||
panel: 'attachToDashboardPanel',
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(canUpdateCase || canCreateCase
|
||||
? [
|
||||
{
|
||||
name: i18n.translate('xpack.aiops.changePointDetection.attachToCaseLabel', {
|
||||
defaultMessage: 'To case',
|
||||
}),
|
||||
disabled: caseAttachmentButtonDisabled,
|
||||
...(caseAttachmentButtonDisabled
|
||||
? {
|
||||
toolTipPosition: 'left' as const,
|
||||
toolTipContent: i18n.translate(
|
||||
'xpack.aiops.changePointDetection.attachToCaseTooltipContent',
|
||||
{
|
||||
defaultMessage: 'Select change points to attach',
|
||||
}
|
||||
),
|
||||
}
|
||||
: {}),
|
||||
onClick: () => {
|
||||
openCasesModalCallback({
|
||||
timeRange,
|
||||
fn: fieldConfig.fn,
|
||||
metricField: fieldConfig.metricField,
|
||||
dataViewId: dataView.id,
|
||||
...(fieldConfig.splitField
|
||||
? {
|
||||
splitField: fieldConfig.splitField,
|
||||
partitions: selectedPartitions,
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'attachToDashboardPanel',
|
||||
title: i18n.translate('xpack.aiops.changePointDetection.attachToDashboardTitle', {
|
||||
defaultMessage: 'Attach to dashboard',
|
||||
}),
|
||||
size: 's',
|
||||
content: (
|
||||
<EuiPanel paddingSize={'s'}>
|
||||
<EuiSpacer size={'s'} />
|
||||
<EuiForm>
|
||||
<EuiFormRow fullWidth>
|
||||
<EuiSwitch
|
||||
label={i18n.translate('xpack.aiops.changePointDetection.applyTimeRangeLabel', {
|
||||
defaultMessage: 'Apply time range',
|
||||
})}
|
||||
checked={dashboardAttachment.applyTimeRange}
|
||||
onChange={(e) =>
|
||||
setDashboardAttachment((prevState) => {
|
||||
return {
|
||||
...prevState,
|
||||
applyTimeRange: e.target.checked,
|
||||
};
|
||||
})
|
||||
}
|
||||
compressed
|
||||
/>
|
||||
</EuiFormRow>
|
||||
{isDefined(fieldConfig.splitField) && selectedPartitions.length === 0 ? (
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
isInvalid={maxSeriesInvalid}
|
||||
error={
|
||||
<FormattedMessage
|
||||
id="xpack.aiops.changePointDetection.maxSeriesToPlotError"
|
||||
defaultMessage="Max series value must be between {minValue} and {maxValue}"
|
||||
values={{ minValue: 1, maxValue: MAX_SERIES }}
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<EuiFlexGroup gutterSize={'xs'} alignItems={'center'}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<FormattedMessage
|
||||
id="xpack.aiops.changePointDetection.maxSeriesToPlotLabel"
|
||||
defaultMessage="Max series"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiToolTip
|
||||
content={i18n.translate(
|
||||
'xpack.aiops.changePointDetection.maxSeriesToPlotDescription',
|
||||
{
|
||||
defaultMessage: 'The maximum number of change points to visualize.',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<EuiIcon type={'questionInCircle'} />
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
}
|
||||
>
|
||||
<EuiFieldNumber
|
||||
isInvalid={maxSeriesInvalid}
|
||||
value={dashboardAttachment.maxSeriesToPlot}
|
||||
onChange={(e) =>
|
||||
setDashboardAttachment((prevState) => {
|
||||
return {
|
||||
...prevState,
|
||||
maxSeriesToPlot: Number(e.target.value),
|
||||
};
|
||||
})
|
||||
}
|
||||
min={1}
|
||||
max={MAX_SERIES}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
) : null}
|
||||
|
||||
<EuiSpacer size={'m'} />
|
||||
|
||||
<EuiButton
|
||||
fill
|
||||
type={'submit'}
|
||||
fullWidth
|
||||
onClick={setDashboardAttachmentReady.bind(null, true)}
|
||||
disabled={maxSeriesInvalid}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.aiops.changePointDetection.submitDashboardAttachButtonLabel"
|
||||
defaultMessage="Attach"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiForm>
|
||||
</EuiPanel>
|
||||
),
|
||||
},
|
||||
];
|
||||
}, [
|
||||
canCreateCase,
|
||||
canEditDashboards,
|
||||
canUpdateCase,
|
||||
caseAttachmentButtonDisabled,
|
||||
dashboardAttachment.applyTimeRange,
|
||||
dashboardAttachment.maxSeriesToPlot,
|
||||
dataView.id,
|
||||
fieldConfig.fn,
|
||||
fieldConfig.metricField,
|
||||
fieldConfig.splitField,
|
||||
onRemove,
|
||||
openCasesModalCallback,
|
||||
removeDisabled,
|
||||
selectedPartitions,
|
||||
timeRange,
|
||||
maxSeriesInvalid,
|
||||
]);
|
||||
|
||||
const onSaveCallback: SaveModalDashboardProps['onSave'] = useCallback(
|
||||
({ dashboardId, newTitle, newDescription }) => {
|
||||
const stateTransfer = embeddable!.getStateTransfer();
|
||||
|
||||
const embeddableInput: Partial<EmbeddableChangePointChartInput> = {
|
||||
title: newTitle,
|
||||
description: newDescription,
|
||||
dataViewId: dataView.id,
|
||||
metricField: fieldConfig.metricField,
|
||||
splitField: fieldConfig.splitField,
|
||||
fn: fieldConfig.fn,
|
||||
...(dashboardAttachment.applyTimeRange ? { timeRange } : {}),
|
||||
maxSeriesToPlot: dashboardAttachment.maxSeriesToPlot,
|
||||
...(selectedChangePoints[panelIndex]?.length ? { partitions: selectedPartitions } : {}),
|
||||
};
|
||||
|
||||
const state = {
|
||||
input: embeddableInput,
|
||||
type: EMBEDDABLE_CHANGE_POINT_CHART_TYPE,
|
||||
};
|
||||
|
||||
const path = dashboardId === 'new' ? '#/create' : `#/view/${dashboardId}`;
|
||||
|
||||
stateTransfer.navigateToWithEmbeddablePackage('dashboards', {
|
||||
state,
|
||||
path,
|
||||
});
|
||||
},
|
||||
[
|
||||
embeddable,
|
||||
dataView.id,
|
||||
fieldConfig.metricField,
|
||||
fieldConfig.splitField,
|
||||
fieldConfig.fn,
|
||||
dashboardAttachment.applyTimeRange,
|
||||
dashboardAttachment.maxSeriesToPlot,
|
||||
timeRange,
|
||||
selectedChangePoints,
|
||||
panelIndex,
|
||||
selectedPartitions,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiPanel paddingSize="s" hasBorder hasShadow={false} data-test-subj={dataTestSubj}>
|
||||
<EuiFlexGroup alignItems={'center'} justifyContent={'spaceBetween'} gutterSize={'s'}>
|
||||
|
@ -197,15 +519,32 @@ const FieldPanel: FC<FieldPanelProps> = ({
|
|||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonIcon
|
||||
disabled={removeDisabled}
|
||||
aria-label={i18n.translate('xpack.aiops.changePointDetection.removeConfigLabel', {
|
||||
defaultMessage: 'Remove configuration',
|
||||
})}
|
||||
iconType="trash"
|
||||
color="danger"
|
||||
onClick={onRemove}
|
||||
/>
|
||||
<EuiFlexGroup alignItems={'center'} justifyContent={'spaceBetween'} gutterSize={'s'}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiPopover
|
||||
id={`panelContextMenu_${panelIndex}`}
|
||||
button={
|
||||
<EuiButtonIcon
|
||||
aria-label={i18n.translate(
|
||||
'xpack.aiops.changePointDetection.configActionsLabel',
|
||||
{
|
||||
defaultMessage: 'Context menu',
|
||||
}
|
||||
)}
|
||||
iconType="boxesHorizontal"
|
||||
color="text"
|
||||
onClick={setIsActionMenuOpen.bind(null, true)}
|
||||
/>
|
||||
}
|
||||
isOpen={isActionMenuOpen}
|
||||
closePopover={setIsActionMenuOpen.bind(null, false)}
|
||||
panelPaddingSize="none"
|
||||
anchorPosition="downLeft"
|
||||
>
|
||||
<EuiContextMenu panels={panels} initialPanelId={'panelActions'} />
|
||||
</EuiPopover>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
|
@ -218,6 +557,34 @@ const FieldPanel: FC<FieldPanelProps> = ({
|
|||
onSelectionChange={onSelectionChange}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{dashboardAttachmentReady ? (
|
||||
<SavedObjectSaveModalDashboard
|
||||
canSaveByReference={false}
|
||||
objectType={i18n.translate('xpack.aiops.changePointDetection.objectTypeLabel', {
|
||||
defaultMessage: 'Change point chart',
|
||||
})}
|
||||
documentInfo={{
|
||||
title: i18n.translate('xpack.aiops.changePointDetection.attachmentTitle', {
|
||||
defaultMessage: 'Change point: {function}({metric}){splitBy}',
|
||||
values: {
|
||||
function: fieldConfig.fn,
|
||||
metric: fieldConfig.metricField,
|
||||
splitBy: fieldConfig.splitField
|
||||
? i18n.translate('xpack.aiops.changePointDetection.splitByTitle', {
|
||||
defaultMessage: ' split by "{splitField}"',
|
||||
values: { splitField: fieldConfig.splitField },
|
||||
})
|
||||
: '',
|
||||
},
|
||||
}),
|
||||
}}
|
||||
onClose={() => {
|
||||
setDashboardAttachmentReady(false);
|
||||
}}
|
||||
onSave={onSaveCallback}
|
||||
/>
|
||||
) : null}
|
||||
</EuiPanel>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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 { type QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useRefresh } from '@kbn/ml-date-picker';
|
||||
import { isDefined } from '@kbn/ml-is-defined';
|
||||
import { useReload } from '../../hooks/use_reload';
|
||||
import { useAiopsAppContext } from '../../hooks/use_aiops_app_context';
|
||||
import {
|
||||
ChangePointAnnotation,
|
||||
|
@ -102,7 +102,7 @@ function getChangePointDetectionRequestBody(
|
|||
index,
|
||||
size: 0,
|
||||
body: {
|
||||
query,
|
||||
...(query ? { query } : {}),
|
||||
aggregations,
|
||||
},
|
||||
},
|
||||
|
@ -121,7 +121,7 @@ export function useChangePointResults(
|
|||
|
||||
const { dataView } = useDataSource();
|
||||
|
||||
const refresh = useRefresh();
|
||||
const { refreshTimestamp: refresh } = useReload();
|
||||
|
||||
const [results, setResults] = useState<ChangePointAnnotation[]>([]);
|
||||
/**
|
||||
|
@ -164,6 +164,7 @@ export function useChangePointResults(
|
|||
},
|
||||
query
|
||||
);
|
||||
|
||||
const result = await runRequest<
|
||||
typeof requestPayload,
|
||||
{ rawResponse: ChangePointAggResponse }
|
||||
|
|
|
@ -8,31 +8,30 @@
|
|||
import moment from 'moment';
|
||||
import { FilterStateStore, type TimeRange } from '@kbn/es-query';
|
||||
import { type TypedLensByValueInput } from '@kbn/lens-plugin/public';
|
||||
import { useTimeRangeUpdates } from '@kbn/ml-date-picker';
|
||||
import { useMemo } from 'react';
|
||||
import { useFilerQueryUpdates } from '../../hooks/use_filters_query';
|
||||
import { fnOperationTypeMapping } from './constants';
|
||||
import { useDataSource } from '../../hooks/use_data_source';
|
||||
import {
|
||||
ChangePointAnnotation,
|
||||
FieldConfig,
|
||||
useChangePointDetectionContext,
|
||||
} from './change_point_detection_context';
|
||||
import { ChangePointAnnotation, FieldConfig } from './change_point_detection_context';
|
||||
|
||||
/**
|
||||
* Provides common props for the Lens Embeddable component
|
||||
* based on the change point definition and currently applied filters and query.
|
||||
*/
|
||||
export const useCommonChartProps = ({
|
||||
annotation,
|
||||
fieldConfig,
|
||||
previewMode = false,
|
||||
bucketInterval,
|
||||
}: {
|
||||
fieldConfig: FieldConfig;
|
||||
annotation: ChangePointAnnotation;
|
||||
previewMode?: boolean;
|
||||
bucketInterval: string;
|
||||
}): Partial<TypedLensByValueInput> => {
|
||||
const timeRange = useTimeRangeUpdates(true);
|
||||
const { dataView } = useDataSource();
|
||||
const { bucketInterval, resultQuery, resultFilters } = useChangePointDetectionContext();
|
||||
|
||||
const { filters: resultFilters, query: resultQuery, timeRange } = useFilerQueryUpdates();
|
||||
|
||||
/**
|
||||
* In order to correctly render annotations for change points at the edges,
|
||||
|
@ -48,6 +47,7 @@ export const useCommonChartProps = ({
|
|||
const filters = useMemo(() => {
|
||||
return [
|
||||
...resultFilters,
|
||||
// Adds a filter for change point partition value
|
||||
...(annotation.group
|
||||
? [
|
||||
{
|
||||
|
@ -161,7 +161,9 @@ export const useCommonChartProps = ({
|
|||
outside: false,
|
||||
},
|
||||
],
|
||||
ignoreGlobalFilters: true,
|
||||
// TODO check if we need to set filter from
|
||||
// the filterManager
|
||||
ignoreGlobalFilters: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -180,7 +182,7 @@ export const useCommonChartProps = ({
|
|||
isBucketed: true,
|
||||
scale: 'interval',
|
||||
params: {
|
||||
interval: bucketInterval.expression,
|
||||
interval: bucketInterval,
|
||||
includeEmptyRows: true,
|
||||
dropPartials: false,
|
||||
},
|
||||
|
@ -219,7 +221,7 @@ export const useCommonChartProps = ({
|
|||
dataView.timeFieldName,
|
||||
resultQuery,
|
||||
filters,
|
||||
bucketInterval.expression,
|
||||
bucketInterval,
|
||||
fieldConfig.fn,
|
||||
fieldConfig.metricField,
|
||||
gridAndLabelsVisibility,
|
||||
|
|
|
@ -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,
|
||||
} from '@kbn/unified-field-list/src/components/field_stats';
|
||||
import type { TimeRange as TimeRangeMs } from '@kbn/ml-date-picker';
|
||||
import type { PresentationUtilPluginStart } from '@kbn/presentation-util-plugin/public';
|
||||
import type { EmbeddableStart } from '@kbn/embeddable-plugin/public';
|
||||
import type { CasesUiStart } from '@kbn/cases-plugin/public';
|
||||
|
||||
/**
|
||||
* AIOps App Dependencies to be provided via React context.
|
||||
|
@ -104,6 +107,9 @@ export interface AiopsAppDependencies {
|
|||
dslQuery?: FieldStatsProps['dslQuery'];
|
||||
}>;
|
||||
};
|
||||
presentationUtil?: PresentationUtilPluginStart;
|
||||
embeddable?: EmbeddableStart;
|
||||
cases?: CasesUiStart;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
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 CoreSetup } from '@kbn/core/public';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
|
||||
import {
|
||||
import type {
|
||||
AiopsPluginSetup,
|
||||
AiopsPluginSetupDeps,
|
||||
AiopsPluginStart,
|
||||
AiopsPluginStartDeps,
|
||||
} from './types';
|
||||
import { getEmbeddableChangePointChart } from './embeddable/embeddable_change_point_chart_component';
|
||||
|
||||
export class AiopsPlugin
|
||||
implements Plugin<AiopsPluginSetup, AiopsPluginStart, AiopsPluginSetupDeps, AiopsPluginStartDeps>
|
||||
{
|
||||
public setup() {
|
||||
return {};
|
||||
public setup(
|
||||
core: CoreSetup<AiopsPluginStartDeps, AiopsPluginStart>,
|
||||
{ embeddable, cases, licensing }: AiopsPluginSetupDeps
|
||||
) {
|
||||
firstValueFrom(licensing.license$).then(async (license) => {
|
||||
if (license.hasAtLeast('platinum')) {
|
||||
if (embeddable) {
|
||||
const { registerEmbeddable } = await import('./embeddable/register_embeddable');
|
||||
registerEmbeddable(core, embeddable);
|
||||
}
|
||||
|
||||
if (cases) {
|
||||
const [coreStart, pluginStart] = await core.getStartServices();
|
||||
const { registerChangePointChartsAttachment } = await import(
|
||||
'./cases/register_change_point_charts_attachment'
|
||||
);
|
||||
registerChangePointChartsAttachment(cases, coreStart, pluginStart);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public start(core: CoreStart, plugins: AiopsPluginStartDeps) {
|
||||
public start(core: CoreStart, plugins: AiopsPluginStartDeps): AiopsPluginStart {
|
||||
// importing async to keep the aiops plugin size to a minimum
|
||||
Promise.all([
|
||||
import('@kbn/ui-actions-plugin/public'),
|
||||
|
@ -42,7 +61,9 @@ export class AiopsPlugin
|
|||
}
|
||||
});
|
||||
|
||||
return {};
|
||||
return {
|
||||
EmbeddableChangePointChart: getEmbeddableChangePointChart(core, plugins),
|
||||
};
|
||||
}
|
||||
|
||||
public stop() {}
|
|
@ -15,9 +15,16 @@ import type { LicensingPluginStart } from '@kbn/licensing-plugin/public';
|
|||
import type { SharePluginStart } from '@kbn/share-plugin/public';
|
||||
import type { UiActionsStart } from '@kbn/ui-actions-plugin/public';
|
||||
import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public';
|
||||
import type { EmbeddableSetup, EmbeddableStart } from '@kbn/embeddable-plugin/public';
|
||||
import type { CasesUiSetup } from '@kbn/cases-plugin/public';
|
||||
import { LicensingPluginSetup } from '@kbn/licensing-plugin/public';
|
||||
import type { EmbeddableChangePointChartProps } from './embeddable';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface AiopsPluginSetupDeps {}
|
||||
export interface AiopsPluginSetupDeps {
|
||||
embeddable: EmbeddableSetup;
|
||||
cases: CasesUiSetup;
|
||||
licensing: LicensingPluginSetup;
|
||||
}
|
||||
|
||||
export interface AiopsPluginStartDeps {
|
||||
data: DataPublicPluginStart;
|
||||
|
@ -30,12 +37,10 @@ export interface AiopsPluginStartDeps {
|
|||
storage: IStorageWrapper;
|
||||
licensing: LicensingPluginStart;
|
||||
executionContext: ExecutionContextStart;
|
||||
embeddable: EmbeddableStart;
|
||||
}
|
||||
|
||||
/**
|
||||
* aiops plugin server setup contract
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface AiopsPluginSetup {}
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface AiopsPluginStart {}
|
||||
export type AiopsPluginSetup = void;
|
||||
export interface AiopsPluginStart {
|
||||
EmbeddableChangePointChart: React.ComponentType<EmbeddableChangePointChartProps>;
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import { Subscription } from 'rxjs';
|
|||
import { PluginInitializerContext, CoreSetup, CoreStart, Plugin, Logger } from '@kbn/core/server';
|
||||
import type { DataRequestHandlerContext } from '@kbn/data-plugin/server';
|
||||
|
||||
import { CASES_ATTACHMENT_CHANGE_POINT_CHART } from '../common/constants';
|
||||
import { isActiveLicense } from './lib/license';
|
||||
import {
|
||||
AiopsLicense,
|
||||
|
@ -54,6 +55,12 @@ export class AiopsPlugin
|
|||
defineLogCategorizationRoutes(router, aiopsLicense);
|
||||
});
|
||||
|
||||
if (plugins.cases) {
|
||||
plugins.cases.attachmentFramework.registerPersistableState({
|
||||
id: CASES_ATTACHMENT_CHANGE_POINT_CHART,
|
||||
});
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
|
|
|
@ -7,10 +7,12 @@
|
|||
|
||||
import type { PluginSetup, PluginStart } from '@kbn/data-plugin/server';
|
||||
import type { LicensingPluginStart } from '@kbn/licensing-plugin/server';
|
||||
import type { CasesSetup } from '@kbn/cases-plugin/server';
|
||||
|
||||
export interface AiopsPluginSetupDeps {
|
||||
data: PluginSetup;
|
||||
licensing: LicensingPluginStart;
|
||||
cases?: CasesSetup;
|
||||
}
|
||||
|
||||
export interface AiopsPluginStartDeps {
|
||||
|
|
|
@ -57,6 +57,11 @@
|
|||
"@kbn/unified-field-list",
|
||||
"@kbn/unified-search-plugin",
|
||||
"@kbn/utility-types",
|
||||
"@kbn/presentation-util-plugin",
|
||||
"@kbn/embeddable-plugin",
|
||||
"@kbn/core-theme-browser",
|
||||
"@kbn/core-lifecycle-browser",
|
||||
"@kbn/cases-plugin",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -28,7 +28,8 @@
|
|||
"unifiedSearch",
|
||||
"savedObjectsManagement",
|
||||
"savedSearch",
|
||||
"contentManagement"
|
||||
"contentManagement",
|
||||
"presentationUtil"
|
||||
],
|
||||
"optionalPlugins": [
|
||||
"alerting",
|
||||
|
|
|
@ -60,6 +60,9 @@ export const ChangePointDetectionPage: FC = () => {
|
|||
'unifiedSearch',
|
||||
'theme',
|
||||
'lens',
|
||||
'presentationUtil',
|
||||
'embeddable',
|
||||
'cases',
|
||||
]),
|
||||
fieldStats: { useFieldStatsTrigger, FieldStatsFlyoutProvider },
|
||||
}}
|
||||
|
|
|
@ -102,6 +102,7 @@ const App: FC<AppProps> = ({ coreStart, deps, appMountParams }) => {
|
|||
savedObjectsManagement: deps.savedObjectsManagement,
|
||||
savedSearch: deps.savedSearch,
|
||||
contentManagement: deps.contentManagement,
|
||||
presentationUtil: deps.presentationUtil,
|
||||
...coreStart,
|
||||
mlServices: getMlGlobalServices(coreStart.http, deps.usageCollection),
|
||||
};
|
||||
|
|
|
@ -28,6 +28,7 @@ import type { LensPublicStart } from '@kbn/lens-plugin/public';
|
|||
import type { SavedObjectsManagementPluginStart } from '@kbn/saved-objects-management-plugin/public';
|
||||
import type { ContentManagementPublicStart } from '@kbn/content-management-plugin/public';
|
||||
import type { SavedSearchPublicPluginStart } from '@kbn/saved-search-plugin/public';
|
||||
import type { PresentationUtilPluginStart } from '@kbn/presentation-util-plugin/public';
|
||||
import type { MlServicesContext } from '../../app';
|
||||
|
||||
interface StartPlugins {
|
||||
|
@ -53,6 +54,7 @@ interface StartPlugins {
|
|||
savedObjectsManagement: SavedObjectsManagementPluginStart;
|
||||
savedSearch: SavedSearchPublicPluginStart;
|
||||
contentManagement: ContentManagementPublicStart;
|
||||
presentationUtil: PresentationUtilPluginStart;
|
||||
}
|
||||
export type StartServices = CoreStart &
|
||||
StartPlugins & {
|
||||
|
|
|
@ -10,6 +10,7 @@ import { i18n } from '@kbn/i18n';
|
|||
import type { StartServicesAccessor } from '@kbn/core/public';
|
||||
|
||||
import type { EmbeddableFactoryDefinition, IContainer } from '@kbn/embeddable-plugin/public';
|
||||
import type { IAnomalySwimlaneEmbeddable } from './anomaly_swimlane_embeddable';
|
||||
import { PLUGIN_ID, PLUGIN_ICON, ML_APP_NAME } from '../../../common/constants/app';
|
||||
import { HttpService } from '../../application/services/http_service';
|
||||
import type { MlPluginStart, MlStartDependencies } from '../../plugin';
|
||||
|
@ -94,7 +95,7 @@ export class AnomalySwimlaneEmbeddableFactory
|
|||
public async create(
|
||||
initialInput: AnomalySwimlaneEmbeddableInput,
|
||||
parent?: IContainer
|
||||
): Promise<any> {
|
||||
): Promise<InstanceType<IAnomalySwimlaneEmbeddable>> {
|
||||
const services = await this.getServices();
|
||||
const { AnomalySwimlaneEmbeddable } = await import('./anomaly_swimlane_embeddable');
|
||||
return new AnomalySwimlaneEmbeddable(initialInput, services, parent);
|
||||
|
|
|
@ -47,6 +47,7 @@ import type { DashboardSetup, DashboardStart } from '@kbn/dashboard-plugin/publi
|
|||
import type { ChartsPluginStart } from '@kbn/charts-plugin/public';
|
||||
import type { CasesUiSetup, CasesUiStart } from '@kbn/cases-plugin/public';
|
||||
import type { SavedSearchPublicPluginStart } from '@kbn/saved-search-plugin/public';
|
||||
import type { PresentationUtilPluginStart } from '@kbn/presentation-util-plugin/public';
|
||||
import { registerManagementSection } from './application/management';
|
||||
import { MlLocatorDefinition, type MlLocator } from './locator';
|
||||
import { setDependencyCache } from './application/util/dependency_cache';
|
||||
|
@ -75,6 +76,7 @@ export interface MlStartDependencies {
|
|||
savedObjectsManagement: SavedObjectsManagementPluginStart;
|
||||
savedSearch: SavedSearchPublicPluginStart;
|
||||
contentManagement: ContentManagementPublicStart;
|
||||
presentationUtil: PresentationUtilPluginStart;
|
||||
}
|
||||
|
||||
export interface MlSetupDependencies {
|
||||
|
@ -145,6 +147,7 @@ export class MlPlugin implements Plugin<MlPluginSetup, MlPluginStart> {
|
|||
savedObjectsManagement: pluginsStart.savedObjectsManagement,
|
||||
savedSearch: pluginsStart.savedSearch,
|
||||
contentManagement: pluginsStart.contentManagement,
|
||||
presentationUtil: pluginsStart.presentationUtil,
|
||||
},
|
||||
params
|
||||
);
|
||||
|
|
|
@ -102,5 +102,6 @@
|
|||
"@kbn/core-ui-settings-browser",
|
||||
"@kbn/content-management-plugin",
|
||||
"@kbn/ml-in-memory-table",
|
||||
"@kbn/presentation-util-plugin",
|
||||
],
|
||||
}
|
||||
|
|
|
@ -291,6 +291,7 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
expect(types).to.eql({
|
||||
'.lens': '78559fd806809ac3a1008942ead2a079864054f5',
|
||||
'.test': 'ab2204830c67f5cf992c9aa2f7e3ead752cc60a1',
|
||||
aiopsChangePointChart: 'a1212d71947ec34487b374cecc47ab9941b5d91c',
|
||||
ml_anomaly_charts: '23e92e824af9db6e8b8bb1d63c222e04f57d2147',
|
||||
ml_anomaly_swimlane: 'a3517f3e53fb041e9cbb150477fb6ef0f731bd5f',
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue