mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[ML] AIOps: Add field stats for metric and split fields (#155177)
This commit is contained in:
parent
5a4dd3fccd
commit
20532c8051
7 changed files with 196 additions and 111 deletions
|
@ -5,9 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { type FC, useCallback } from 'react';
|
||||
import React, { type FC, useCallback, useMemo, useState } from 'react';
|
||||
import {
|
||||
EuiAccordion,
|
||||
EuiButton,
|
||||
EuiButtonIcon,
|
||||
EuiCallOut,
|
||||
|
@ -16,10 +15,13 @@ import {
|
|||
EuiPanel,
|
||||
EuiProgress,
|
||||
EuiSpacer,
|
||||
useGeneratedHtmlId,
|
||||
} from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { type FieldStatsServices } from '@kbn/unified-field-list-plugin/public';
|
||||
import { useTimefilter, useTimeRangeUpdates } from '@kbn/ml-date-picker';
|
||||
import { useDataSource } from '../../hooks/use_data_source';
|
||||
import { useAiopsAppContext } from '../../hooks/use_aiops_app_context';
|
||||
import { ChangePointsTable } from './change_points_table';
|
||||
import { MAX_CHANGE_POINT_CONFIGS, SPLIT_FIELD_CARDINALITY_LIMIT } from './constants';
|
||||
import { FunctionPicker } from './function_picker';
|
||||
|
@ -34,7 +36,7 @@ import {
|
|||
import { useChangePointResults } from './use_change_point_agg_request';
|
||||
import { useSplitFieldCardinality } from './use_split_field_cardinality';
|
||||
|
||||
const selectControlCss = { width: '300px' };
|
||||
const selectControlCss = { width: '350px' };
|
||||
|
||||
/**
|
||||
* Contains panels with controls and change point results.
|
||||
|
@ -140,50 +142,67 @@ const FieldPanel: FC<FieldPanelProps> = ({
|
|||
|
||||
const splitFieldCardinality = useSplitFieldCardinality(fieldConfig.splitField, combinedQuery);
|
||||
|
||||
const [isExpanded, setIsExpanded] = useState<boolean>(true);
|
||||
|
||||
const {
|
||||
results: annotations,
|
||||
isLoading: annotationsLoading,
|
||||
progress,
|
||||
} = useChangePointResults(fieldConfig, requestParams, combinedQuery, splitFieldCardinality);
|
||||
|
||||
const accordionId = useGeneratedHtmlId({ prefix: 'fieldConfig' });
|
||||
|
||||
return (
|
||||
<EuiPanel paddingSize="s" hasBorder hasShadow={false}>
|
||||
<EuiAccordion
|
||||
id={accordionId}
|
||||
initialIsOpen={true}
|
||||
buttonElement={'div'}
|
||||
buttonContent={
|
||||
<FieldsControls fieldConfig={fieldConfig} onChange={onChange}>
|
||||
<EuiFlexItem css={{ visibility: progress === null ? 'hidden' : 'visible' }} grow={true}>
|
||||
<EuiProgress
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.aiops.changePointDetection.progressBarLabel"
|
||||
defaultMessage="Fetching change points"
|
||||
/>
|
||||
}
|
||||
value={progress ?? 0}
|
||||
max={100}
|
||||
valueText
|
||||
size="m"
|
||||
<EuiFlexGroup alignItems={'center'} justifyContent={'spaceBetween'} gutterSize={'s'}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup alignItems={'center'} gutterSize={'s'}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonIcon
|
||||
iconType={isExpanded ? 'arrowDown' : 'arrowRight'}
|
||||
onClick={setIsExpanded.bind(null, (prevState) => !prevState)}
|
||||
aria-label={i18n.translate('xpack.aiops.changePointDetection.expandConfigLabel', {
|
||||
defaultMessage: 'Expand configuration',
|
||||
})}
|
||||
/>
|
||||
<EuiSpacer size="s" />
|
||||
</EuiFlexItem>
|
||||
</FieldsControls>
|
||||
}
|
||||
extraAction={
|
||||
<EuiFlexItem grow={false}>
|
||||
<FieldsControls fieldConfig={fieldConfig} onChange={onChange}>
|
||||
<EuiFlexItem
|
||||
css={{ visibility: progress === null ? 'hidden' : 'visible' }}
|
||||
grow={true}
|
||||
>
|
||||
<EuiProgress
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.aiops.changePointDetection.progressBarLabel"
|
||||
defaultMessage="Fetching change points"
|
||||
/>
|
||||
}
|
||||
value={progress ?? 0}
|
||||
max={100}
|
||||
valueText
|
||||
size="m"
|
||||
/>
|
||||
<EuiSpacer size="s" />
|
||||
</EuiFlexItem>
|
||||
</FieldsControls>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonIcon
|
||||
disabled={removeDisabled}
|
||||
aria-label="trash"
|
||||
aria-label={i18n.translate('xpack.aiops.changePointDetection.removeConfigLabel', {
|
||||
defaultMessage: 'Remove configuration',
|
||||
})}
|
||||
iconType="trash"
|
||||
color="danger"
|
||||
onClick={onRemove}
|
||||
/>
|
||||
}
|
||||
paddingSize="s"
|
||||
>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
{isExpanded ? (
|
||||
<ChangePointResults
|
||||
fieldConfig={fieldConfig}
|
||||
isLoading={annotationsLoading}
|
||||
|
@ -191,7 +210,7 @@ const FieldPanel: FC<FieldPanelProps> = ({
|
|||
splitFieldCardinality={splitFieldCardinality}
|
||||
onSelectionChange={onSelectionChange}
|
||||
/>
|
||||
</EuiAccordion>
|
||||
) : null}
|
||||
</EuiPanel>
|
||||
);
|
||||
};
|
||||
|
@ -205,7 +224,25 @@ interface FieldsControlsProps {
|
|||
* Renders controls for fields selection and emits updates on change.
|
||||
*/
|
||||
export const FieldsControls: FC<FieldsControlsProps> = ({ fieldConfig, onChange, children }) => {
|
||||
const { splitFieldsOptions } = useChangePointDetectionContext();
|
||||
const { splitFieldsOptions, combinedQuery } = useChangePointDetectionContext();
|
||||
const { dataView } = useDataSource();
|
||||
const { data, uiSettings, fieldFormats, charts, fieldStats } = useAiopsAppContext();
|
||||
const timefilter = useTimefilter();
|
||||
// required in order to trigger state updates
|
||||
useTimeRangeUpdates();
|
||||
const timefilterActiveBounds = timefilter.getActiveBounds();
|
||||
|
||||
const fieldStatsServices: FieldStatsServices = useMemo(() => {
|
||||
return {
|
||||
uiSettings,
|
||||
dataViews: data.dataViews,
|
||||
data,
|
||||
fieldFormats,
|
||||
charts,
|
||||
};
|
||||
}, [uiSettings, data, fieldFormats, charts]);
|
||||
|
||||
const FieldStatsFlyoutProvider = fieldStats!.FieldStatsFlyoutProvider;
|
||||
|
||||
const onChangeFn = useCallback(
|
||||
(field: keyof FieldConfig, value: string) => {
|
||||
|
@ -216,27 +253,41 @@ export const FieldsControls: FC<FieldsControlsProps> = ({ fieldConfig, onChange,
|
|||
);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup alignItems={'center'} responsive={true} wrap={true} gutterSize={'m'}>
|
||||
<EuiFlexItem grow={false} css={{ width: '200px' }}>
|
||||
<FunctionPicker value={fieldConfig.fn} onChange={(v) => onChangeFn('fn', v)} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={true} css={selectControlCss}>
|
||||
<MetricFieldSelector
|
||||
value={fieldConfig.metricField!}
|
||||
onChange={(v) => onChangeFn('metricField', v)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
{splitFieldsOptions.length > 0 ? (
|
||||
<EuiFlexItem grow={true} css={selectControlCss}>
|
||||
<SplitFieldSelector
|
||||
value={fieldConfig.splitField}
|
||||
onChange={(v) => onChangeFn('splitField', v!)}
|
||||
<FieldStatsFlyoutProvider
|
||||
fieldStatsServices={fieldStatsServices}
|
||||
dataView={dataView}
|
||||
dslQuery={combinedQuery}
|
||||
timeRangeMs={
|
||||
timefilterActiveBounds
|
||||
? {
|
||||
from: timefilterActiveBounds.min!.valueOf(),
|
||||
to: timefilterActiveBounds.max!.valueOf(),
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<EuiFlexGroup alignItems={'center'} responsive={true} wrap={true} gutterSize={'m'}>
|
||||
<EuiFlexItem grow={false} css={{ width: '200px' }}>
|
||||
<FunctionPicker value={fieldConfig.fn} onChange={(v) => onChangeFn('fn', v)} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false} css={selectControlCss}>
|
||||
<MetricFieldSelector
|
||||
value={fieldConfig.metricField!}
|
||||
onChange={(v) => onChangeFn('metricField', v)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
) : null}
|
||||
{splitFieldsOptions.length > 0 ? (
|
||||
<EuiFlexItem grow={false} css={selectControlCss}>
|
||||
<SplitFieldSelector
|
||||
value={fieldConfig.splitField}
|
||||
onChange={(v) => onChangeFn('splitField', v!)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
) : null}
|
||||
|
||||
{children}
|
||||
</EuiFlexGroup>
|
||||
{children}
|
||||
</EuiFlexGroup>
|
||||
</FieldStatsFlyoutProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -33,7 +33,6 @@ export const FunctionPicker: FC<FunctionPickerProps> = React.memo(({ value, onCh
|
|||
onChange={(id) => onChange(id)}
|
||||
isFullWidth
|
||||
buttonSize="compressed"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -5,10 +5,11 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { FC, useCallback, useMemo } from 'react';
|
||||
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 { useAiopsAppContext } from '../../hooks/use_aiops_app_context';
|
||||
|
||||
interface MetricFieldSelectorProps {
|
||||
value: string;
|
||||
|
@ -17,10 +18,19 @@ interface MetricFieldSelectorProps {
|
|||
|
||||
export const MetricFieldSelector: FC<MetricFieldSelectorProps> = React.memo(
|
||||
({ value, onChange }) => {
|
||||
const { fieldStats } = useAiopsAppContext();
|
||||
const { metricFieldOptions } = useChangePointDetectionContext();
|
||||
|
||||
const { renderOption, closeFlyout } = fieldStats!.useFieldStatsTrigger();
|
||||
|
||||
const options = useMemo<EuiComboBoxOptionOption[]>(() => {
|
||||
return metricFieldOptions.map((v) => ({ value: v.name, label: v.displayName }));
|
||||
return metricFieldOptions.map((v) => {
|
||||
return {
|
||||
value: v.name,
|
||||
label: v.displayName,
|
||||
field: { id: v.name, type: v.type },
|
||||
};
|
||||
});
|
||||
}, [metricFieldOptions]);
|
||||
|
||||
const selection = options.filter((v) => v.value === value);
|
||||
|
@ -29,28 +39,32 @@ export const MetricFieldSelector: FC<MetricFieldSelectorProps> = React.memo(
|
|||
(selectedOptions: EuiComboBoxOptionOption[]) => {
|
||||
const option = selectedOptions[0];
|
||||
if (typeof option !== 'undefined') {
|
||||
onChange(option.label);
|
||||
onChange(option.value as string);
|
||||
}
|
||||
closeFlyout();
|
||||
},
|
||||
[onChange]
|
||||
[onChange, closeFlyout]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiFormRow>
|
||||
<EuiComboBox
|
||||
compressed
|
||||
prepend={i18n.translate('xpack.aiops.changePointDetection.selectMetricFieldLabel', {
|
||||
defaultMessage: 'Metric field',
|
||||
})}
|
||||
singleSelection={{ asPlainText: true }}
|
||||
options={options}
|
||||
selectedOptions={selection}
|
||||
onChange={onChangeCallback}
|
||||
isClearable={false}
|
||||
data-test-subj="aiopsChangePointMetricField"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<>
|
||||
<EuiFormRow>
|
||||
<EuiComboBox
|
||||
compressed
|
||||
prepend={i18n.translate('xpack.aiops.changePointDetection.selectMetricFieldLabel', {
|
||||
defaultMessage: 'Metric field',
|
||||
})}
|
||||
singleSelection={{ asPlainText: true }}
|
||||
options={options}
|
||||
selectedOptions={selection}
|
||||
onChange={onChangeCallback}
|
||||
isClearable={false}
|
||||
data-test-subj="aiopsChangePointMetricField"
|
||||
// @ts-ignore
|
||||
renderOption={renderOption}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
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';
|
||||
|
||||
interface SplitFieldSelectorProps {
|
||||
|
@ -16,32 +17,37 @@ interface SplitFieldSelectorProps {
|
|||
}
|
||||
|
||||
export const SplitFieldSelector: FC<SplitFieldSelectorProps> = React.memo(({ value, onChange }) => {
|
||||
const { fieldStats } = useAiopsAppContext();
|
||||
const { renderOption, closeFlyout } = fieldStats!.useFieldStatsTrigger();
|
||||
|
||||
const { splitFieldsOptions } = useChangePointDetectionContext();
|
||||
|
||||
const options = useMemo<Array<EuiComboBoxOptionOption<string>>>(() => {
|
||||
const options = useMemo<EuiComboBoxOptionOption[]>(() => {
|
||||
return [
|
||||
{
|
||||
name: undefined,
|
||||
displayName: i18n.translate('xpack.aiops.changePointDetection.notSelectedSplitFieldLabel', {
|
||||
value: undefined,
|
||||
label: i18n.translate('xpack.aiops.changePointDetection.notSelectedSplitFieldLabel', {
|
||||
defaultMessage: '--- Not selected ---',
|
||||
}),
|
||||
},
|
||||
...splitFieldsOptions,
|
||||
].map((v) => ({
|
||||
value: v.name,
|
||||
label: v.displayName,
|
||||
}));
|
||||
...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: Array<EuiComboBoxOptionOption<string>>) => {
|
||||
(selectedOptions: EuiComboBoxOptionOption[]) => {
|
||||
const option = selectedOptions[0];
|
||||
const newValue = option?.value;
|
||||
const newValue = option?.value as string;
|
||||
onChange(newValue);
|
||||
closeFlyout();
|
||||
},
|
||||
[onChange]
|
||||
[onChange, closeFlyout]
|
||||
);
|
||||
|
||||
return (
|
||||
|
@ -57,7 +63,8 @@ export const SplitFieldSelector: FC<SplitFieldSelectorProps> = React.memo(({ val
|
|||
onChange={onChangeCallback}
|
||||
isClearable
|
||||
data-test-subj="aiopsChangePointSplitField"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
// @ts-ignore
|
||||
renderOption={renderOption}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { createContext, useContext } from 'react';
|
||||
import { createContext, type FC, useContext } from 'react';
|
||||
|
||||
import type { IStorageWrapper } from '@kbn/kibana-utils-plugin/public';
|
||||
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
|
||||
|
@ -14,14 +14,18 @@ import type { ChartsPluginStart } from '@kbn/charts-plugin/public';
|
|||
import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
|
||||
import type { SharePluginStart } from '@kbn/share-plugin/public';
|
||||
import type {
|
||||
CoreStart,
|
||||
CoreSetup,
|
||||
CoreStart,
|
||||
ExecutionContextStart,
|
||||
HttpStart,
|
||||
IUiSettingsClient,
|
||||
ThemeServiceStart,
|
||||
} from '@kbn/core/public';
|
||||
import type { LensPublicStart } from '@kbn/lens-plugin/public';
|
||||
import { type EuiComboBoxProps } from '@elastic/eui/src/components/combo_box/combo_box';
|
||||
import { type DataView } from '@kbn/data-views-plugin/common';
|
||||
import type { FieldStatsProps, FieldStatsServices } from '@kbn/unified-field-list-plugin/public';
|
||||
import type { TimeRange as TimeRangeMs } from '@kbn/ml-date-picker';
|
||||
|
||||
export interface AiopsAppDependencies {
|
||||
application: CoreStart['application'];
|
||||
|
@ -37,6 +41,19 @@ export interface AiopsAppDependencies {
|
|||
unifiedSearch: UnifiedSearchPublicPluginStart;
|
||||
share: SharePluginStart;
|
||||
lens: LensPublicStart;
|
||||
// deps for unified field stats
|
||||
fieldStats?: {
|
||||
useFieldStatsTrigger: () => {
|
||||
renderOption: EuiComboBoxProps<string>['renderOption'];
|
||||
closeFlyout: () => void;
|
||||
};
|
||||
FieldStatsFlyoutProvider: FC<{
|
||||
dataView: DataView;
|
||||
fieldStatsServices: FieldStatsServices;
|
||||
timeRangeMs?: TimeRangeMs;
|
||||
dslQuery?: FieldStatsProps['dslQuery'];
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
||||
export const AiopsAppContext = createContext<AiopsAppDependencies | undefined>(undefined);
|
||||
|
|
|
@ -13,6 +13,7 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
|||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { ChangePointDetection } from '@kbn/aiops-plugin/public';
|
||||
|
||||
import { useFieldStatsTrigger, FieldStatsFlyoutProvider } from '../components/field_stats_flyout';
|
||||
import { useMlContext } from '../contexts/ml';
|
||||
import { useMlKibana } from '../contexts/kibana';
|
||||
import { HelpMenu } from '../components/help_menu';
|
||||
|
@ -46,21 +47,24 @@ export const ChangePointDetectionPage: FC = () => {
|
|||
<ChangePointDetection
|
||||
dataView={dataView}
|
||||
savedSearch={savedSearch}
|
||||
appDependencies={pick(services, [
|
||||
'application',
|
||||
'data',
|
||||
'executionContext',
|
||||
'charts',
|
||||
'fieldFormats',
|
||||
'http',
|
||||
'notifications',
|
||||
'share',
|
||||
'storage',
|
||||
'uiSettings',
|
||||
'unifiedSearch',
|
||||
'theme',
|
||||
'lens',
|
||||
])}
|
||||
appDependencies={{
|
||||
...pick(services, [
|
||||
'application',
|
||||
'data',
|
||||
'executionContext',
|
||||
'charts',
|
||||
'fieldFormats',
|
||||
'http',
|
||||
'notifications',
|
||||
'share',
|
||||
'storage',
|
||||
'uiSettings',
|
||||
'unifiedSearch',
|
||||
'theme',
|
||||
'lens',
|
||||
]),
|
||||
fieldStats: { useFieldStatsTrigger, FieldStatsFlyoutProvider },
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
<HelpMenu
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import React, { useCallback, useState, type FC } from 'react';
|
||||
import type { DataView } from '@kbn/data-plugin/common';
|
||||
import type { FieldStatsServices } from '@kbn/unified-field-list-plugin/public';
|
||||
import type { TimeRange as TimeRangeMs } from '@kbn/ml-date-picker';
|
||||
|
@ -13,19 +13,12 @@ import type { FieldStatsProps } from '@kbn/unified-field-list-plugin/public';
|
|||
import { MLJobWizardFieldStatsFlyoutContext } from './use_field_stats_flytout_context';
|
||||
import { FieldStatsFlyout } from './field_stats_flyout';
|
||||
|
||||
export const FieldStatsFlyoutProvider = ({
|
||||
dataView,
|
||||
fieldStatsServices,
|
||||
timeRangeMs,
|
||||
dslQuery,
|
||||
children,
|
||||
}: {
|
||||
export const FieldStatsFlyoutProvider: FC<{
|
||||
dataView: DataView;
|
||||
fieldStatsServices: FieldStatsServices;
|
||||
timeRangeMs?: TimeRangeMs;
|
||||
dslQuery?: FieldStatsProps['dslQuery'];
|
||||
children: React.ReactElement;
|
||||
}) => {
|
||||
}> = ({ dataView, fieldStatsServices, timeRangeMs, dslQuery, children }) => {
|
||||
const [isFieldStatsFlyoutVisible, setFieldStatsIsFlyoutVisible] = useState(false);
|
||||
const [fieldName, setFieldName] = useState<string | undefined>();
|
||||
const [fieldValue, setFieldValue] = useState<string | number | undefined>();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue