[ML] AIOps: Add/edit change point charts embeddable from the Dashboard app (#163694)

This commit is contained in:
Dima Arnautov 2023-08-15 19:10:07 +02:00 committed by GitHub
parent 39f2a2567a
commit fb6ac2e445
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 862 additions and 257 deletions

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import { memoize } from 'lodash';
import { isPopulatedObject } from '@kbn/ml-is-populated-object';
export interface NumberValidationResult {
@ -29,7 +30,7 @@ export function numberValidator(conditions?: {
throw new Error('Invalid validator conditions');
}
return (value: number): NumberValidationResult | null => {
return memoize((value: number): NumberValidationResult | null => {
const result = {} as NumberValidationResult;
if (conditions?.min !== undefined && value < conditions.min) {
result.min = true;
@ -44,5 +45,5 @@ export function numberValidator(conditions?: {
return result;
}
return null;
};
});
}

View file

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

View file

@ -94,6 +94,46 @@ export interface ChangePointAnnotation {
export type SelectedChangePoint = FieldConfig & ChangePointAnnotation;
export const ChangePointDetectionControlsContext = createContext<{
metricFieldOptions: DataViewField[];
splitFieldsOptions: DataViewField[];
}>({
splitFieldsOptions: [],
metricFieldOptions: [],
});
export const useChangePointDetectionControlsContext = () => {
return useContext(ChangePointDetectionControlsContext);
};
export const ChangePointDetectionControlsContextProvider: FC = ({ children }) => {
const { dataView } = useDataSource();
const metricFieldOptions = useMemo<DataViewField[]>(() => {
return dataView.fields.filter(({ aggregatable, type }) => aggregatable && type === 'number');
}, [dataView]);
const splitFieldsOptions = useMemo<DataViewField[]>(() => {
return dataView.fields.filter(
({ aggregatable, esTypes, displayName }) =>
aggregatable &&
esTypes &&
esTypes.some((el) =>
[ES_FIELD_TYPES.KEYWORD, ES_FIELD_TYPES.IP].includes(el as ES_FIELD_TYPES)
) &&
!['_id', '_index'].includes(displayName)
);
}, [dataView]);
const value = { metricFieldOptions, splitFieldsOptions };
return (
<ChangePointDetectionControlsContext.Provider value={value}>
{children}
</ChangePointDetectionControlsContext.Provider>
);
};
export const ChangePointDetectionContextProvider: FC = ({ children }) => {
const { dataView, savedSearch } = useDataSource();
const {

View file

@ -28,7 +28,10 @@ import { AIOPS_STORAGE_KEYS } from '../../types/storage';
import { PageHeader } from '../page_header';
import { ChangePointDetectionPage } from './change_point_detection_page';
import { ChangePointDetectionContextProvider } from './change_point_detection_context';
import {
ChangePointDetectionContextProvider,
ChangePointDetectionControlsContextProvider,
} from './change_point_detection_context';
import { timeSeriesDataViewWarning } from '../../application/utils/time_series_dataview_check';
import { ReloadContextProvider } from '../../hooks/use_reload';
@ -87,7 +90,9 @@ export const ChangePointDetectionAppState: FC<ChangePointDetectionAppStateProps>
<EuiSpacer />
<ReloadContextProvider reload$={reload$}>
<ChangePointDetectionContextProvider>
<ChangePointDetectionPage />
<ChangePointDetectionControlsContextProvider>
<ChangePointDetectionPage />
</ChangePointDetectionControlsContextProvider>
</ChangePointDetectionContextProvider>
</ReloadContextProvider>
</DatePickerContextProvider>

View file

@ -16,6 +16,8 @@ export interface ChartComponentProps {
annotation: ChangePointAnnotation;
interval: string;
onLoading?: (isLoading: boolean) => void;
}
export interface ChartComponentPropsAll {
@ -29,7 +31,7 @@ export interface ChartComponentPropsAll {
}
export const ChartComponent: FC<ChartComponentProps> = React.memo(
({ annotation, fieldConfig, interval }) => {
({ annotation, fieldConfig, interval, onLoading }) => {
const {
lens: { EmbeddableComponent },
} = useAiopsAppContext();
@ -55,6 +57,7 @@ export const ChartComponent: FC<ChartComponentProps> = React.memo(
name: 'Change point detection',
}}
disableTriggers
onLoad={onLoading}
/>
);
}

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React, { type FC, useMemo, useState, useEffect, useRef } from 'react';
import React, { type FC, useMemo, useState, useEffect, useRef, useCallback } from 'react';
import {
EuiBadge,
EuiDescriptionList,
@ -42,10 +42,28 @@ interface ChartsGridProps {
* @param changePoints
* @constructor
*/
export const ChartsGrid: FC<{ changePoints: SelectedChangePoint[]; interval: string }> = ({
changePoints,
interval,
}) => {
export const ChartsGrid: FC<{
changePoints: SelectedChangePoint[];
interval: string;
onRenderComplete?: () => void;
}> = ({ changePoints, interval, onRenderComplete }) => {
// Render is complete when all chart components in the grid are ready
const loadCounter = useRef<Record<number, boolean>>(
Object.fromEntries(changePoints.map((v, i) => [i, true]))
);
const onLoadCallback = useCallback(
(chartId: number, isLoading: boolean) => {
if (!onRenderComplete) return;
loadCounter.current[chartId] = isLoading;
const isLoadComplete = Object.values(loadCounter.current).every((v) => !v);
if (isLoadComplete) {
onRenderComplete();
}
},
[onRenderComplete]
);
return (
<EuiFlexGrid
columns={changePoints.length >= 2 ? 2 : 1}
@ -122,6 +140,9 @@ export const ChartsGrid: FC<{ changePoints: SelectedChangePoint[]; interval: str
fieldConfig={{ splitField: v.splitField, fn: v.fn, metricField: v.metricField }}
annotation={v}
interval={interval}
onLoading={(isLoading) => {
onLoadCallback(index, isLoading);
}}
/>
</EuiPanel>
</EuiFlexItem>

View file

@ -11,18 +11,15 @@ import {
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';
@ -35,7 +32,7 @@ import {
} 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 { MaxSeriesControl } from './max_series_control';
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';
@ -54,7 +51,6 @@ 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' };
@ -183,8 +179,8 @@ const FieldPanel: FC<FieldPanelProps> = ({
const splitFieldCardinality = useSplitFieldCardinality(fieldConfig.splitField, combinedQuery);
const [isExpanded, setIsExpanded] = useState<boolean>(true);
const [isActionMenuOpen, setIsActionMenuOpen] = useState(false);
const [isDashboardFormValid, setIsDashboardFormValid] = useState(true);
const canEditDashboards = capabilities.dashboard?.createNew ?? false;
const { create: canCreateCase, update: canUpdateCase } = cases?.helpers?.canUseCases() ?? {
@ -218,13 +214,6 @@ const FieldPanel: FC<FieldPanelProps> = ({
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 [
{
@ -348,54 +337,20 @@ const FieldPanel: FC<FieldPanelProps> = ({
/>
</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>
<MaxSeriesControl
value={dashboardAttachment.maxSeriesToPlot}
onChange={(v) => {
setDashboardAttachment((prevState) => {
return {
...prevState,
maxSeriesToPlot: v,
};
});
}}
onValidationChange={(result) => {
setIsDashboardFormValid(result === null);
}}
/>
) : null}
<EuiSpacer size={'m'} />
@ -405,7 +360,7 @@ const FieldPanel: FC<FieldPanelProps> = ({
type={'submit'}
fullWidth
onClick={setDashboardAttachmentReady.bind(null, true)}
disabled={maxSeriesInvalid}
disabled={!isDashboardFormValid}
>
<FormattedMessage
id="xpack.aiops.changePointDetection.submitDashboardAttachButtonLabel"
@ -428,12 +383,12 @@ const FieldPanel: FC<FieldPanelProps> = ({
fieldConfig.fn,
fieldConfig.metricField,
fieldConfig.splitField,
isDashboardFormValid,
onRemove,
openCasesModalCallback,
removeDisabled,
selectedPartitions,
timeRange,
maxSeriesInvalid,
]);
const onSaveCallback: SaveModalDashboardProps['onSave'] = useCallback(

View file

@ -0,0 +1,72 @@
/*
* 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 { EuiFieldNumber, EuiFormRow, EuiIcon, EuiToolTip } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { type NumberValidationResult, numberValidator } from '@kbn/ml-agg-utils';
import { MAX_SERIES } from '../../embeddable/const';
const maxSeriesValidator = numberValidator({ min: 1, max: MAX_SERIES, integerOnly: true });
export const MaxSeriesControl: FC<{
disabled?: boolean;
value: number;
onChange: (update: number) => void;
onValidationChange?: (result: NumberValidationResult | null) => void;
inline?: boolean;
}> = ({ value, onChange, onValidationChange, disabled, inline = true }) => {
const maxSeriesValidationResult = maxSeriesValidator(value);
const maxSeriesInvalid = maxSeriesValidationResult !== null;
const label = i18n.translate('xpack.aiops.changePointDetection.maxSeriesToPlotLabel', {
defaultMessage: 'Max series',
});
return (
<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={inline ? undefined : label}
>
<EuiFieldNumber
disabled={disabled}
prepend={inline ? label : undefined}
append={
<EuiToolTip
content={i18n.translate('xpack.aiops.changePointDetection.maxSeriesToPlotDescription', {
defaultMessage: 'The maximum number of change points to visualize.',
})}
>
<EuiIcon type={'questionInCircle'} />
</EuiToolTip>
}
compressed
fullWidth
isInvalid={maxSeriesInvalid}
value={value}
onChange={(e) => {
const newValue = Number(e.target.value);
onChange(newValue);
if (onValidationChange) {
onValidationChange(maxSeriesValidator(newValue));
}
}}
min={1}
max={MAX_SERIES}
/>
</EuiFormRow>
);
};

View file

@ -8,22 +8,23 @@
import React, { type FC, useCallback, useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiComboBox, type EuiComboBoxOptionOption, EuiFormRow } from '@elastic/eui';
import { useChangePointDetectionContext } from './change_point_detection_context';
import { useChangePointDetectionControlsContext } from './change_point_detection_context';
import { useAiopsAppContext } from '../../hooks/use_aiops_app_context';
interface MetricFieldSelectorProps {
value: string;
onChange: (value: string) => void;
inline?: boolean;
}
export const MetricFieldSelector: FC<MetricFieldSelectorProps> = React.memo(
({ value, onChange }) => {
({ value, onChange, inline = true }) => {
const { fieldStats } = useAiopsAppContext();
const { metricFieldOptions } = useChangePointDetectionContext();
const { metricFieldOptions } = useChangePointDetectionControlsContext();
const { renderOption, closeFlyout } = fieldStats!.useFieldStatsTrigger();
const { renderOption, closeFlyout } = fieldStats?.useFieldStatsTrigger() ?? {};
const options = useMemo<EuiComboBoxOptionOption[]>(() => {
const options = useMemo<Array<EuiComboBoxOptionOption<string>>>(() => {
return metricFieldOptions.map((v) => {
return {
value: v.name,
@ -41,26 +42,30 @@ export const MetricFieldSelector: FC<MetricFieldSelectorProps> = React.memo(
if (typeof option !== 'undefined') {
onChange(option.value as string);
}
closeFlyout();
if (closeFlyout) {
closeFlyout();
}
},
[onChange, closeFlyout]
);
const label = i18n.translate('xpack.aiops.changePointDetection.selectMetricFieldLabel', {
defaultMessage: 'Metric field',
});
return (
<>
<EuiFormRow>
<EuiFormRow fullWidth label={inline ? undefined : label}>
<EuiComboBox
fullWidth
compressed
prepend={i18n.translate('xpack.aiops.changePointDetection.selectMetricFieldLabel', {
defaultMessage: 'Metric field',
})}
prepend={inline ? label : undefined}
singleSelection={{ asPlainText: true }}
options={options}
selectedOptions={selection}
onChange={onChangeCallback}
isClearable={false}
data-test-subj="aiopsChangePointMetricField"
// @ts-ignore
renderOption={renderOption}
/>
</EuiFormRow>

View file

@ -0,0 +1,192 @@
/*
* 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, useState, useCallback, useMemo, useEffect } from 'react';
import { EuiComboBox, EuiFormRow } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { type SearchRequest } from '@elastic/elasticsearch/lib/api/types';
import { EuiComboBoxOptionOption } from '@elastic/eui/src/components/combo_box/types';
import { debounce } from 'lodash';
import usePrevious from 'react-use/lib/usePrevious';
import { useAiopsAppContext } from '../../hooks/use_aiops_app_context';
import { useCancellableSearch } from '../../hooks/use_cancellable_search';
import { useDataSource } from '../../hooks/use_data_source';
export interface PartitionsSelectorProps {
splitField: string;
value: string[];
onChange: (update: string[]) => void;
enableSearch?: boolean;
}
function getQueryPayload(
indexPattern: string,
fieldName: string,
queryString: string = '',
selectedPartitions?: string[]
) {
return {
params: {
index: indexPattern,
size: 0,
...(selectedPartitions?.length
? {
query: {
bool: {
must_not: [
{
terms: {
[fieldName]: selectedPartitions,
},
},
],
},
},
}
: {}),
aggs: {
aggResults: {
filter: {
bool: {
must: {
wildcard: {
[fieldName]: {
value: `*${queryString}*`,
},
},
},
},
},
aggs: {
partitionValues: {
terms: {
field: fieldName,
},
},
},
},
},
} as SearchRequest,
};
}
interface PartitionsResponse {
aggregations: {
aggResults: {
partitionValues: { buckets: Array<{ key: string }> };
};
};
}
export const PartitionsSelector: FC<PartitionsSelectorProps> = ({
value,
onChange,
splitField,
enableSearch = true,
}) => {
const { dataView } = useDataSource();
const {
notifications: { toasts },
} = useAiopsAppContext();
const prevSplitField = usePrevious(splitField);
const [options, setOptions] = useState<Array<EuiComboBoxOptionOption<string>>>([]);
const [isLoading, setIsLoading] = useState(enableSearch);
const { runRequest, cancelRequest } = useCancellableSearch();
const fetchResults = useCallback(
async (searchValue: string) => {
if (!enableSearch) return;
cancelRequest();
setIsLoading(true);
try {
const requestPayload = getQueryPayload(
dataView.getIndexPattern(),
splitField,
searchValue,
value
);
const result = await runRequest<typeof requestPayload, { rawResponse: PartitionsResponse }>(
requestPayload
);
if (result === null) {
setOptions([]);
return;
}
setOptions(
result.rawResponse.aggregations.aggResults.partitionValues.buckets.map((v) => ({
value: v.key,
label: v.key,
}))
);
} catch (e) {
toasts.addError(e, {
title: i18n.translate('xpack.aiops.changePointDetection.fetchPartitionsErrorTitle', {
defaultMessage: 'Failed to fetch partitions',
}),
});
}
setIsLoading(false);
},
[enableSearch, cancelRequest, dataView, splitField, value, runRequest, toasts]
);
useEffect(
function onSplitFieldChange() {
if (splitField !== prevSplitField) {
fetchResults('');
onChange([]);
}
},
[splitField, prevSplitField, fetchResults, onChange]
);
const selectedOptions = useMemo<Array<EuiComboBoxOptionOption<string>>>(() => {
return value.map((v) => ({ value: v, label: v }));
}, [value]);
const onChangeCallback = useCallback(
(udpate: EuiComboBoxOptionOption[]) => {
onChange(udpate.map((v) => v.value as string));
},
[onChange]
);
const onSearchChange = useMemo(() => debounce(fetchResults, 500), [fetchResults]);
const onCreateOption = useCallback(
(v: string) => {
onChange([...value, v]);
},
[onChange, value]
);
return (
<EuiFormRow
fullWidth
label={i18n.translate('xpack.aiops.changePointDetection.partitionsLabel', {
defaultMessage: 'Partitions',
})}
>
<EuiComboBox<string>
isLoading={isLoading}
fullWidth
compressed
options={options}
selectedOptions={selectedOptions}
onChange={onChangeCallback}
onSearchChange={enableSearch ? onSearchChange : undefined}
onCreateOption={!enableSearch ? onCreateOption : undefined}
isClearable
data-test-subj="aiopsChangePointPartitions"
/>
</EuiFormRow>
);
};

View file

@ -9,63 +9,70 @@ import React, { FC, useMemo, useCallback } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiComboBox, type EuiComboBoxOptionOption, EuiFormRow } from '@elastic/eui';
import { useAiopsAppContext } from '../../hooks/use_aiops_app_context';
import { useChangePointDetectionContext } from './change_point_detection_context';
import { useChangePointDetectionControlsContext } from './change_point_detection_context';
interface SplitFieldSelectorProps {
value: string | undefined;
onChange: (value: string | undefined) => void;
inline?: boolean;
}
export const SplitFieldSelector: FC<SplitFieldSelectorProps> = React.memo(({ value, onChange }) => {
const { fieldStats } = useAiopsAppContext();
const { renderOption, closeFlyout } = fieldStats!.useFieldStatsTrigger();
export const SplitFieldSelector: FC<SplitFieldSelectorProps> = React.memo(
({ value, onChange, inline = true }) => {
const { fieldStats } = useAiopsAppContext();
const { renderOption, closeFlyout } = fieldStats?.useFieldStatsTrigger() ?? {};
const { splitFieldsOptions } = useChangePointDetectionContext();
const { splitFieldsOptions } = useChangePointDetectionControlsContext();
const options = useMemo<EuiComboBoxOptionOption[]>(() => {
return [
{
value: undefined,
label: i18n.translate('xpack.aiops.changePointDetection.notSelectedSplitFieldLabel', {
defaultMessage: '--- Not selected ---',
}),
const options = useMemo<Array<EuiComboBoxOptionOption<string>>>(() => {
return [
{
value: undefined,
label: i18n.translate('xpack.aiops.changePointDetection.notSelectedSplitFieldLabel', {
defaultMessage: '--- Not selected ---',
}),
},
...splitFieldsOptions.map((v) => ({
value: v.name,
label: v.displayName,
...(v.name ? { field: { id: v.name, type: v?.type } } : {}),
})),
];
}, [splitFieldsOptions]);
const selection = options.filter((v) => v.value === value);
const onChangeCallback = useCallback(
(selectedOptions: EuiComboBoxOptionOption[]) => {
const option = selectedOptions[0];
const newValue = option?.value as string;
onChange(newValue);
if (closeFlyout) {
closeFlyout();
}
},
...splitFieldsOptions.map((v) => ({
value: v.name,
label: v.displayName,
...(v.name ? { field: { id: v.name, type: v?.type } } : {}),
})),
];
}, [splitFieldsOptions]);
[onChange, closeFlyout]
);
const selection = options.filter((v) => v.value === value);
const label = i18n.translate('xpack.aiops.changePointDetection.selectSpitFieldLabel', {
defaultMessage: 'Split field',
});
const onChangeCallback = useCallback(
(selectedOptions: EuiComboBoxOptionOption[]) => {
const option = selectedOptions[0];
const newValue = option?.value as string;
onChange(newValue);
closeFlyout();
},
[onChange, closeFlyout]
);
return (
<EuiFormRow>
<EuiComboBox
compressed
prepend={i18n.translate('xpack.aiops.changePointDetection.selectSpitFieldLabel', {
defaultMessage: 'Split field',
})}
singleSelection={{ asPlainText: true }}
options={options}
selectedOptions={selection}
onChange={onChangeCallback}
isClearable
data-test-subj="aiopsChangePointSplitField"
// @ts-ignore
renderOption={renderOption}
/>
</EuiFormRow>
);
});
return (
<EuiFormRow fullWidth label={inline ? undefined : label}>
<EuiComboBox
fullWidth
compressed
prepend={inline ? label : undefined}
singleSelection={{ asPlainText: true }}
options={options}
selectedOptions={selection}
onChange={onChangeCallback}
isClearable
data-test-subj="aiopsChangePointSplitField"
renderOption={renderOption}
/>
</EuiFormRow>
);
}
);

View file

@ -5,59 +5,97 @@
* 2.0.
*/
import React, { FC, useState } from 'react';
import React, { FC, useCallback, useEffect, useMemo, useState } from 'react';
import {
EuiButton,
EuiButtonEmpty,
EuiForm,
EuiFormRow,
EuiHorizontalRule,
EuiModal,
EuiModalBody,
EuiModalFooter,
EuiModalHeader,
EuiModalHeaderTitle,
EuiFieldNumber,
EuiFieldText,
EuiModal,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import usePrevious from 'react-use/lib/usePrevious';
import { pick } from 'lodash';
import { isPopulatedObject } from '@kbn/ml-is-populated-object';
import { ES_FIELD_TYPES } from '@kbn/field-types';
import { PartitionsSelector } from '../components/change_point_detection/partitions_selector';
import { DEFAULT_SERIES } from './const';
import { EmbeddableChangePointChartProps } from './embeddable_change_point_chart_component';
import { type EmbeddableChangePointChartExplicitInput } from './types';
import { MaxSeriesControl } from '../components/change_point_detection/max_series_control';
import { SplitFieldSelector } from '../components/change_point_detection/split_field_selector';
import { MetricFieldSelector } from '../components/change_point_detection/metric_field_selector';
import {
ChangePointDetectionControlsContextProvider,
useChangePointDetectionControlsContext,
} from '../components/change_point_detection/change_point_detection_context';
import { useAiopsAppContext } from '../hooks/use_aiops_app_context';
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;
import { DataSourceContextProvider } from '../hooks/use_data_source';
import { DEFAULT_AGG_FUNCTION } from '../components/change_point_detection/constants';
export interface AnomalyChartsInitializerProps {
defaultTitle: string;
initialInput?: Partial<EmbeddableChangePointChartInput>;
onCreate: (props: { panelTitle: string; maxSeriesToPlot?: number }) => void;
onCreate: (props: EmbeddableChangePointChartExplicitInput) => 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 {
unifiedSearch: {
ui: { IndexPatternSelect },
},
} = useAiopsAppContext();
const [dataViewId, setDataViewId] = useState(initialInput?.dataViewId ?? '');
const [formInput, setFormInput] = useState<FormControlsProps>(
pick(initialInput ?? {}, [
'fn',
'metricField',
'splitField',
'maxSeriesToPlot',
'partitions',
]) as FormControlsProps
);
const [fn, setFn] = useState<string>(initialInput?.fn ?? 'avg');
const [isFormValid, setIsFormValid] = useState(true);
const isPanelTitleValid = panelTitle.length > 0;
const isMaxSeriesToPlotValid =
maxSeriesToPlot >= 1 && maxSeriesToPlot <= MAX_ANOMALY_CHARTS_ALLOWED;
const isFormValid = isPanelTitleValid && isMaxSeriesToPlotValid;
const updatedProps = useMemo(() => {
return {
...formInput,
title: isPopulatedObject(formInput)
? i18n.translate('xpack.aiops.changePointDetection.attachmentTitle', {
defaultMessage: 'Change point: {function}({metric}){splitBy}',
values: {
function: formInput.fn,
metric: formInput?.metricField,
splitBy: formInput?.splitField
? i18n.translate('xpack.aiops.changePointDetection.splitByTitle', {
defaultMessage: ' split by "{splitField}"',
values: { splitField: formInput.splitField },
})
: '',
},
})
: '',
dataViewId,
};
}, [formInput, dataViewId]);
return (
<EuiModal
initialFocus="[name=panelTitle]"
onClose={onCancel}
data-test-subj={'aiopsChangePointChartEmbeddableInitializer'}
>
<EuiModal onClose={onCancel} data-test-subj={'aiopsChangePointChartEmbeddableInitializer'}>
<EuiModalHeader>
<EuiModalHeaderTitle>
<FormattedMessage
@ -70,67 +108,46 @@ export const ChangePointChartInitializer: FC<AnomalyChartsInitializerProps> = ({
<EuiModalBody>
<EuiForm>
<EuiFormRow
label={
<FormattedMessage
id="xpack.aiops.embeddableChangePointChart.panelTitleLabel"
defaultMessage="Panel title"
/>
}
isInvalid={!isPanelTitleValid}
fullWidth
label={i18n.translate('xpack.aiops.embeddableChangePointChart.dataViewLabel', {
defaultMessage: 'Data view',
})}
>
<EuiFieldText
data-test-subj="panelTitleInput"
id="panelTitle"
name="panelTitle"
value={panelTitle}
onChange={(e) => setPanelTitle(e.target.value)}
isInvalid={!isPanelTitleValid}
<IndexPatternSelect
autoFocus={!dataViewId}
fullWidth
compressed
indexPatternId={dataViewId}
placeholder={i18n.translate(
'xpack.aiops.embeddableChangePointChart.dataViewSelectorPlaceholder',
{
defaultMessage: 'Select data view',
}
)}
onChange={(newId) => {
setDataViewId(newId ?? '');
}}
/>
</EuiFormRow>
<EuiFormRow
label={
<FormattedMessage
id="xpack.aiops.embeddableChangePointChart.functionLabel"
defaultMessage="Function"
<DataSourceContextProvider dataViewId={dataViewId}>
<EuiHorizontalRule margin={'s'} />
<ChangePointDetectionControlsContextProvider>
<FormControls
formInput={formInput}
onChange={setFormInput}
onValidationChange={setIsFormValid}
/>
}
>
<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>
</ChangePointDetectionControlsContextProvider>
</DataSourceContextProvider>
</EuiForm>
</EuiModalBody>
<EuiModalFooter>
<EuiButtonEmpty onClick={onCancel} data-test-subj="mlAnomalyChartsInitializerCancelButton">
<EuiButtonEmpty
onClick={onCancel}
data-test-subj="aiopsChangePointChartsInitializerCancelButton"
>
<FormattedMessage
id="xpack.aiops.embeddableChangePointChart.setupModal.cancelButtonLabel"
defaultMessage="Cancel"
@ -138,12 +155,9 @@ export const ChangePointChartInitializer: FC<AnomalyChartsInitializerProps> = ({
</EuiButtonEmpty>
<EuiButton
data-test-subj="mlAnomalyChartsInitializerConfirmButton"
isDisabled={!isFormValid}
onClick={onCreate.bind(null, {
panelTitle,
maxSeriesToPlot,
})}
data-test-subj="aiopsChangePointChartsInitializerConfirmButton"
isDisabled={!isFormValid || !dataViewId}
onClick={onCreate.bind(null, updatedProps)}
fill
>
<FormattedMessage
@ -155,3 +169,109 @@ export const ChangePointChartInitializer: FC<AnomalyChartsInitializerProps> = ({
</EuiModal>
);
};
export type FormControlsProps = Pick<
EmbeddableChangePointChartProps,
'metricField' | 'splitField' | 'fn' | 'maxSeriesToPlot' | 'partitions'
>;
export const FormControls: FC<{
formInput?: FormControlsProps;
onChange: (update: FormControlsProps) => void;
onValidationChange: (isValid: boolean) => void;
}> = ({ formInput, onChange, onValidationChange }) => {
const { metricFieldOptions, splitFieldsOptions } = useChangePointDetectionControlsContext();
const prevMetricFieldOptions = usePrevious(metricFieldOptions);
const enableSearch = useMemo<boolean>(() => {
const field = splitFieldsOptions.find((v) => v.name === formInput?.splitField);
if (field && field.esTypes) {
return field.esTypes?.some((t) => t === ES_FIELD_TYPES.KEYWORD);
} else {
return false;
}
}, [splitFieldsOptions, formInput?.splitField]);
useEffect(
function setDefaultOnDataViewChange() {
if (!isPopulatedObject(formInput)) {
onChange({
fn: DEFAULT_AGG_FUNCTION,
metricField: metricFieldOptions[0]?.name,
splitField: undefined,
partitions: undefined,
maxSeriesToPlot: DEFAULT_SERIES,
});
return;
}
if (metricFieldOptions === prevMetricFieldOptions) return;
onChange({
fn: formInput.fn,
metricField: metricFieldOptions[0]?.name,
splitField: undefined,
partitions: undefined,
maxSeriesToPlot: formInput.maxSeriesToPlot,
});
},
[metricFieldOptions, prevMetricFieldOptions, formInput, onChange]
);
const updateCallback = useCallback(
(update: Partial<FormControlsProps>) => {
onChange({
...formInput,
...update,
} as FormControlsProps);
},
[formInput, onChange]
);
if (!isPopulatedObject(formInput)) return null;
return (
<>
<EuiFormRow
fullWidth
label={
<FormattedMessage
id="xpack.aiops.embeddableChangePointChart.functionLabel"
defaultMessage="Function"
/>
}
>
<FunctionPicker value={formInput.fn} onChange={(v) => updateCallback({ fn: v })} />
</EuiFormRow>
<MetricFieldSelector
inline={false}
value={formInput.metricField}
onChange={(v) => updateCallback({ metricField: v })}
/>
<SplitFieldSelector
inline={false}
value={formInput.splitField}
onChange={(v) => updateCallback({ splitField: v })}
/>
{formInput.splitField ? (
<PartitionsSelector
value={formInput.partitions ?? []}
onChange={(v) => updateCallback({ partitions: v })}
splitField={formInput.splitField}
enableSearch={enableSearch}
/>
) : null}
<MaxSeriesControl
inline={false}
disabled={!!formInput?.partitions?.length}
value={formInput.maxSeriesToPlot!}
onChange={(v) => updateCallback({ maxSeriesToPlot: v })}
onValidationChange={(result) => onValidationChange(result === null)}
/>
</>
);
};

View file

@ -6,3 +6,5 @@
*/
export const MAX_SERIES = 50;
export const DEFAULT_SERIES = 6;

View file

@ -21,6 +21,7 @@ import { DatePickerContextProvider } from '@kbn/ml-date-picker';
import { pick } from 'lodash';
import { LensPublicStart } from '@kbn/lens-plugin/public';
import { Subject } from 'rxjs';
import type { DataView } from '@kbn/data-views-plugin/common';
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';
@ -29,7 +30,7 @@ import { EmbeddableChangePointChartProps } from './embeddable_change_point_chart
export type EmbeddableChangePointChartInput = EmbeddableInput & EmbeddableChangePointChartProps;
export type EmbeddableChangePointChartOutput = EmbeddableOutput;
export type EmbeddableChangePointChartOutput = EmbeddableOutput & { indexPatterns?: DataView[] };
export interface EmbeddableChangePointChartDeps {
theme: ThemeServiceStart;
@ -113,6 +114,10 @@ export class EmbeddableChangePointChart extends AbstractEmbeddable<
input$={input$}
initialInput={input}
reload$={this.reload$}
onOutputChange={this.updateOutput.bind(this)}
onRenderComplete={this.onRenderComplete.bind(this)}
onLoading={this.onLoading.bind(this)}
onError={this.onError.bind(this)}
/>
</Suspense>
</DatePickerContextProvider>

View file

@ -44,11 +44,8 @@ export class EmbeddableChangePointChartFactory implements EmbeddableFactoryDefin
private readonly getStartServices: StartServicesAccessor<AiopsPluginStartDeps, AiopsPluginStart>
) {}
/**
* TODO
*/
public isEditable = async () => {
return false;
return true;
};
getDisplayName() {
@ -57,19 +54,16 @@ export class EmbeddableChangePointChartFactory implements EmbeddableFactoryDefin
});
}
/**
* TODO
*/
canCreateNew() {
return false;
return true;
}
public async getExplicitInput(): Promise<Partial<EmbeddableChangePointChartInput>> {
const [coreStart] = await this.getStartServices();
const [coreStart, pluginStart] = await this.getStartServices();
try {
const { resolveEmbeddableChangePointUserInput } = await import('./handle_explicit_input');
return await resolveEmbeddableChangePointUserInput(coreStart);
return await resolveEmbeddableChangePointUserInput(coreStart, pluginStart);
} catch (e) {
return Promise.reject();
}

View file

@ -6,7 +6,7 @@
*/
import { type Observable } from 'rxjs';
import React, { FC, useMemo } from 'react';
import React, { FC, useCallback, useEffect, useMemo } from 'react';
import { useTimefilter } from '@kbn/ml-date-picker';
import { css } from '@emotion/react';
import useObservable from 'react-use/lib/useObservable';
@ -15,10 +15,17 @@ import type {
ChangePointAnnotation,
ChangePointDetectionRequestParams,
} from '../components/change_point_detection/change_point_detection_context';
import { EmbeddableChangePointChartInput } from './embeddable_change_point_chart';
import type {
EmbeddableChangePointChartInput,
EmbeddableChangePointChartOutput,
} 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 {
DataSourceContextProvider,
type DataSourceContextProviderProps,
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';
@ -31,16 +38,37 @@ const defaultSort = {
direction: 'asc',
};
export const EmbeddableInputTracker: FC<{
export interface EmbeddableInputTrackerProps {
input$: Observable<EmbeddableChangePointChartInput>;
initialInput: EmbeddableChangePointChartInput;
reload$: Observable<number>;
}> = ({ input$, initialInput, reload$ }) => {
onOutputChange: (output: Partial<EmbeddableChangePointChartOutput>) => void;
onRenderComplete: () => void;
onLoading: () => void;
onError: (error: Error) => void;
}
export const EmbeddableInputTracker: FC<EmbeddableInputTrackerProps> = ({
input$,
initialInput,
reload$,
onOutputChange,
onRenderComplete,
onLoading,
onError,
}) => {
const input = useObservable(input$, initialInput);
const onChange = useCallback<Exclude<DataSourceContextProviderProps['onChange'], undefined>>(
({ dataViews }) => {
onOutputChange({ indexPatterns: dataViews });
},
[onOutputChange]
);
return (
<ReloadContextProvider reload$={reload$}>
<DataSourceContextProvider dataViewId={input.dataViewId}>
<DataSourceContextProvider dataViewId={input.dataViewId} onChange={onChange}>
<FilterQueryContextProvider timeRange={input.timeRange}>
<ChartGridEmbeddableWrapper
timeRange={input.timeRange}
@ -50,6 +78,9 @@ export const EmbeddableInputTracker: FC<{
maxSeriesToPlot={input.maxSeriesToPlot}
dataViewId={input.dataViewId}
partitions={input.partitions}
onLoading={onLoading}
onRenderComplete={onRenderComplete}
onError={onError}
/>
</FilterQueryContextProvider>
</DataSourceContextProvider>
@ -68,12 +99,21 @@ export const EmbeddableInputTracker: FC<{
* @param partitions
* @constructor
*/
export const ChartGridEmbeddableWrapper: FC<EmbeddableChangePointChartProps> = ({
export const ChartGridEmbeddableWrapper: FC<
EmbeddableChangePointChartProps & {
onRenderComplete: () => void;
onLoading: () => void;
onError: (error: Error) => void;
}
> = ({
fn,
metricField,
maxSeriesToPlot,
splitField,
partitions,
onError,
onLoading,
onRenderComplete,
}) => {
const { filters, query, timeRange } = useFilerQueryUpdates();
@ -134,7 +174,18 @@ export const ChartGridEmbeddableWrapper: FC<EmbeddableChangePointChartProps> = (
return { interval } as ChangePointDetectionRequestParams;
}, [interval]);
const { results } = useChangePointResults(fieldConfig, requestParams, combinedQuery, 10000);
const { results, isLoading } = useChangePointResults(
fieldConfig,
requestParams,
combinedQuery,
10000
);
useEffect(() => {
if (isLoading) {
onLoading();
}
}, [onLoading, isLoading]);
const changePoints = useMemo<ChangePointAnnotation[]>(() => {
let resultChangePoints: ChangePointAnnotation[] = results.sort((a, b) => {
@ -163,6 +214,7 @@ export const ChartGridEmbeddableWrapper: FC<EmbeddableChangePointChartProps> = (
<ChartsGrid
changePoints={changePoints.map((r) => ({ ...r, ...fieldConfig }))}
interval={requestParams.interval}
onRenderComplete={onRenderComplete}
/>
) : (
<NoChangePointsWarning />

View file

@ -6,41 +6,47 @@
*/
import type { CoreStart } from '@kbn/core/public';
import { toMountPoint, wrapWithTheme } from '@kbn/kibana-react-plugin/public';
import { toMountPoint } from '@kbn/react-kibana-mount';
import React from 'react';
import { EmbeddableChangePointChartExplicitInput } from './types';
import { AiopsAppDependencies } from '..';
import { AiopsAppContext } from '../hooks/use_aiops_app_context';
import type { AiopsPluginStartDeps } from '../types';
import { ChangePointChartInitializer } from './change_point_chart_initializer';
import { EmbeddableChangePointChartInput } from './embeddable_change_point_chart';
import type { EmbeddableChangePointChartInput } from './embeddable_change_point_chart';
export async function resolveEmbeddableChangePointUserInput(
coreStart: CoreStart,
pluginStart: AiopsPluginStartDeps,
input?: EmbeddableChangePointChartInput
): Promise<Partial<EmbeddableChangePointChartInput>> {
): Promise<EmbeddableChangePointChartExplicitInput> {
const { overlays } = coreStart;
return new Promise(async (resolve, reject) => {
try {
const title = input?.title;
const { theme$ } = coreStart.theme;
const modalSession = overlays.openModal(
toMountPoint(
wrapWithTheme(
<AiopsAppContext.Provider
value={
{
...coreStart,
...pluginStart,
} as unknown as AiopsAppDependencies
}
>
<ChangePointChartInitializer
defaultTitle={title ?? ''}
initialInput={input}
onCreate={({ panelTitle, maxSeriesToPlot }) => {
onCreate={(update: EmbeddableChangePointChartExplicitInput) => {
modalSession.close();
resolve({
title: panelTitle,
maxSeriesToPlot,
});
resolve(update);
}}
onCancel={() => {
modalSession.close();
reject();
}}
/>,
theme$
)
/>
</AiopsAppContext.Provider>,
{ theme: coreStart.theme, i18n: coreStart.i18n }
)
);
} catch (error) {

View file

@ -0,0 +1,21 @@
/*
* 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 { IEmbeddable } from '@kbn/embeddable-plugin/public';
import {
EmbeddableChangePointChartInput,
EmbeddableChangePointChartOutput,
} from './embeddable_change_point_chart';
import { EmbeddableChangePointChartProps } from './embeddable_change_point_chart_component';
export type EmbeddableChangePointChartExplicitInput = {
title: string;
} & Omit<EmbeddableChangePointChartProps, 'timeRange'>;
export interface EditChangePointChartsPanelContext {
embeddable: IEmbeddable<EmbeddableChangePointChartInput, EmbeddableChangePointChartOutput>;
}

View file

@ -28,16 +28,24 @@ export interface DataViewAndSavedSearch {
dataView: DataView;
}
export interface DataSourceContextProviderProps {
dataViewId?: string;
savedSearchId?: string;
/** Output resolves data view objects */
onChange?: (update: { dataViews: DataView[] }) => void;
}
/**
* Context provider that resolves current data view and the saved search
*
* @param children
* @constructor
*/
export const DataSourceContextProvider: FC<{ dataViewId?: string; savedSearchId?: string }> = ({
export const DataSourceContextProvider: FC<DataSourceContextProviderProps> = ({
dataViewId,
savedSearchId,
children,
onChange,
}) => {
const [value, setValue] = useState<DataViewAndSavedSearch>();
const [error, setError] = useState<Error>();
@ -59,7 +67,7 @@ export const DataSourceContextProvider: FC<{ dataViewId?: string; savedSearchId?
};
// support only data views for now
if (dataViewId !== undefined) {
if (dataViewId) {
dataViewAndSavedSearch.dataView = await dataViews.get(dataViewId);
}
@ -74,14 +82,18 @@ export const DataSourceContextProvider: FC<{ dataViewId?: string; savedSearchId?
useEffect(() => {
resolveDataSource()
.then((result) => {
setError(undefined);
setValue(result);
if (onChange) {
onChange({ dataViews: [result.dataView] });
}
})
.catch((e) => {
setError(e);
});
}, [resolveDataSource]);
}, [resolveDataSource, onChange, dataViewId]);
if (!value && !error) return null;
if ((!value || !value?.dataView) && !error) return null;
if (error) {
return (

View file

@ -16,12 +16,14 @@ import type {
} from './types';
import { getEmbeddableChangePointChart } from './embeddable/embeddable_change_point_chart_component';
export type AiopsCoreSetup = CoreSetup<AiopsPluginStartDeps, AiopsPluginStart>;
export class AiopsPlugin
implements Plugin<AiopsPluginSetup, AiopsPluginStart, AiopsPluginSetupDeps, AiopsPluginStartDeps>
{
public setup(
core: CoreSetup<AiopsPluginStartDeps, AiopsPluginStart>,
{ embeddable, cases, licensing }: AiopsPluginSetupDeps
core: AiopsCoreSetup,
{ embeddable, cases, licensing, uiActions }: AiopsPluginSetupDeps
) {
firstValueFrom(licensing.license$).then(async (license) => {
if (license.hasAtLeast('platinum')) {
@ -30,6 +32,11 @@ export class AiopsPlugin
registerEmbeddable(core, embeddable);
}
if (uiActions) {
const { registerAiopsUiActions } = await import('./ui_actions');
registerAiopsUiActions(uiActions, core);
}
if (cases) {
const [coreStart, pluginStart] = await core.getStartServices();
const { registerChangePointChartsAttachment } = await import(

View file

@ -13,7 +13,7 @@ import type { IStorageWrapper } from '@kbn/kibana-utils-plugin/public';
import type { LensPublicStart } from '@kbn/lens-plugin/public';
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 { UiActionsStart, UiActionsSetup } 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';
@ -24,6 +24,8 @@ export interface AiopsPluginSetupDeps {
embeddable: EmbeddableSetup;
cases: CasesUiSetup;
licensing: LicensingPluginSetup;
uiActions: UiActionsSetup;
}
export interface AiopsPluginStartDeps {

View file

@ -0,0 +1,59 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { UiActionsActionDefinition } from '@kbn/ui-actions-plugin/public';
import { i18n } from '@kbn/i18n';
import { ViewMode } from '@kbn/embeddable-plugin/common';
import { EMBEDDABLE_CHANGE_POINT_CHART_TYPE } from '../../common/constants';
import type { EditChangePointChartsPanelContext } from '../embeddable/types';
import type { AiopsCoreSetup } from '../plugin';
export const EDIT_CHANGE_POINT_CHARTS_ACTION = 'editChangePointChartsPanelAction';
export function createEditChangePointChartsPanelAction(
getStartServices: AiopsCoreSetup['getStartServices']
): UiActionsActionDefinition<EditChangePointChartsPanelContext> {
return {
id: 'edit-change-point-charts',
type: EDIT_CHANGE_POINT_CHARTS_ACTION,
getIconType(context): string {
return 'pencil';
},
getDisplayName: () =>
i18n.translate('xpack.aiops.actions.editChangePointChartsName', {
defaultMessage: 'Edit change point charts',
}),
async execute({ embeddable }) {
if (!embeddable) {
throw new Error('Not possible to execute an action without the embeddable context');
}
const [coreStart, pluginStart] = await getStartServices();
try {
const { resolveEmbeddableChangePointUserInput } = await import(
'../embeddable/handle_explicit_input'
);
const result = await resolveEmbeddableChangePointUserInput(
coreStart,
pluginStart,
embeddable.getInput()
);
embeddable.updateInput(result);
} catch (e) {
return Promise.reject();
}
},
async isCompatible({ embeddable }) {
return (
embeddable.type === EMBEDDABLE_CHANGE_POINT_CHART_TYPE &&
embeddable.getInput().viewMode === ViewMode.EDIT
);
},
};
}

View file

@ -0,0 +1,22 @@
/*
* 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 { UiActionsSetup } from '@kbn/ui-actions-plugin/public';
import { CONTEXT_MENU_TRIGGER } from '@kbn/embeddable-plugin/public';
import { createEditChangePointChartsPanelAction } from './edit_change_point_charts_panel';
import type { AiopsCoreSetup } from '../plugin';
export function registerAiopsUiActions(uiActions: UiActionsSetup, core: AiopsCoreSetup) {
// Initialize actions
const editChangePointChartPanelAction = createEditChangePointChartsPanelAction(
core.getStartServices
);
// Register actions
uiActions.registerAction(editChangePointChartPanelAction);
// Assign and register triggers
uiActions.attachAction(CONTEXT_MENU_TRIGGER, editChangePointChartPanelAction.id);
}

View file

@ -62,6 +62,7 @@
"@kbn/core-theme-browser",
"@kbn/core-lifecycle-browser",
"@kbn/cases-plugin",
"@kbn/react-kibana-mount",
],
"exclude": [
"target/**/*",