[ML] AIOps: Add field stats for metric and split fields (#155177)

This commit is contained in:
Dima Arnautov 2023-04-20 13:45:46 +02:00 committed by GitHub
parent 5a4dd3fccd
commit 20532c8051
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 196 additions and 111 deletions

View file

@ -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>
);
};

View file

@ -33,7 +33,6 @@ export const FunctionPicker: FC<FunctionPickerProps> = React.memo(({ value, onCh
onChange={(id) => onChange(id)}
isFullWidth
buttonSize="compressed"
onClick={(e) => e.stopPropagation()}
/>
);
});

View file

@ -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>
</>
);
}
);

View file

@ -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>
);

View file

@ -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);

View file

@ -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

View file

@ -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>();