Skip to content

Commit

Permalink
[ML] AIOps: Add field stats for metric and split fields (elastic#155177)
Browse files Browse the repository at this point in the history
  • Loading branch information
darnautov authored and nikitaindik committed Apr 25, 2023
1 parent e41625c commit 5d4ba95
Show file tree
Hide file tree
Showing 7 changed files with 196 additions and 111 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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';
Expand All @@ -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.
Expand Down Expand Up @@ -140,58 +142,75 @@ 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}
annotations={annotations}
splitFieldCardinality={splitFieldCardinality}
onSelectionChange={onSelectionChange}
/>
</EuiAccordion>
) : null}
</EuiPanel>
);
};
Expand All @@ -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) => {
Expand All @@ -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>
);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ export const FunctionPicker: FC<FunctionPickerProps> = React.memo(({ value, onCh
onChange={(id) => onChange(id)}
isFullWidth
buttonSize="compressed"
onClick={(e) => e.stopPropagation()}
/>
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
Expand All @@ -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>
</>
);
}
);
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 (
Expand All @@ -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>
);
Expand Down
Loading

0 comments on commit 5d4ba95

Please sign in to comment.