Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ML] Configure sorting for partition values on Single Metric Viewer #81510

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
593c9ea
[ML] fix callout styles
darnautov Oct 21, 2020
beb067e
[ML] refactor timeseriesexplorer.js, add series_controls.tsx, storage…
darnautov Oct 21, 2020
b648467
[ML] anomalousOnly support
darnautov Oct 21, 2020
149287f
[ML] sort by control
darnautov Oct 22, 2020
71487fc
[ML] update query
darnautov Oct 22, 2020
5defba6
[ML] sort order controls
darnautov Oct 22, 2020
953e1ff
Merge remote-tracking branch 'upstream/master' into ML-69526-improve-…
darnautov Oct 27, 2020
e1a5c45
[ML] adjust query
darnautov Oct 27, 2020
4855208
[ML] merge default and local configs, add info
darnautov Oct 27, 2020
d2710c9
[ML] fix types, adjust sorting logic for model plot results
darnautov Oct 27, 2020
fa59221
[ML] fix translation keys
darnautov Oct 27, 2020
799efd0
[ML] fixed size for the icon flex item
darnautov Oct 27, 2020
faeaf2f
[ML] fix time range condition, refactor
darnautov Oct 27, 2020
79d10e7
[ML] change info messages and the icon color
darnautov Oct 28, 2020
ff0fb56
Fix model plot info message
darnautov Oct 28, 2020
b0df9a8
[ML] functional tests
darnautov Oct 29, 2020
9139d1f
[ML] rename ML_ENTITY_FIELDS_CONFIG
darnautov Oct 29, 2020
1f71210
[ML] support manual input
darnautov Oct 29, 2020
af2f0ef
[ML] show max record score color indicator
darnautov Oct 29, 2020
a8415b7
Merge remote-tracking branch 'upstream/master' into ML-69526-improve-…
darnautov Oct 29, 2020
56ce1ad
[ML] use :checked selector
darnautov Oct 29, 2020
ca7c2fc
[ML] refactor functional tests
darnautov Oct 30, 2020
9f094ea
[ML] extend config with "applyTimeRange", refactor with entity_config…
darnautov Oct 30, 2020
6026d41
[ML] info messages
darnautov Oct 30, 2020
c0b348e
[ML] remove custom message
darnautov Oct 30, 2020
044768a
[ML] adjust the endpoint
darnautov Oct 30, 2020
3ad0468
[ML] customOptionText
darnautov Oct 30, 2020
accd14a
[ML] sort by name UI tweak
darnautov Oct 30, 2020
a9a810e
[ML] change text
darnautov Oct 30, 2020
49f3d0e
[ML] remove TODO comment
darnautov Oct 30, 2020
9783cfa
[ML] fix functional test
darnautov Oct 30, 2020
8728e0f
Merge branch 'master' into ML-69526-improve-partition-sorting
kibanamachine Nov 2, 2020
5d4b138
[ML] move "Anomalous only"/"Apply time range" control to the bottom o…
darnautov Nov 2, 2020
c4203a3
[ML] update types
darnautov Nov 2, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions x-pack/plugins/ml/common/types/anomalies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,5 @@ export interface AnomalyCategorizerStatsDoc {
log_time: number;
timestamp: number;
}

export type EntityFieldType = 'partition_field' | 'over_field' | 'by_field';
38 changes: 38 additions & 0 deletions x-pack/plugins/ml/common/types/storage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { EntityFieldType } from './anomalies';

export const ML_ENTITY_FIELDS_CONFIG = 'ml.singleMetricViewer.partitionFields';

export type PartitionFieldConfig =
| {
/**
* Relevant for jobs with enabled model plot.
* If true, entity values are based on records with anomalies.
* Otherwise aggregated from the model plot results.
*/
anomalousOnly: boolean;
/**
* Relevant for jobs with disabled model plot.
* If true, entity values are filtered by the active time range.
* If false, the lists consist of the values from all existing records.
*/
applyTimeRange: boolean;
sort: {
by: 'anomaly_score' | 'name';
order: 'asc' | 'desc';
};
}
| undefined;

export type PartitionFieldsConfig =
| Partial<Record<EntityFieldType, PartitionFieldConfig>>
| undefined;

export type MlStorage = Partial<{
[ML_ENTITY_FIELDS_CONFIG]: PartitionFieldsConfig;
}> | null;
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { SecurityPluginSetup } from '../../../../../security/public';
import { LicenseManagementUIPluginSetup } from '../../../../../license_management/public';
import { SharePluginStart } from '../../../../../../../src/plugins/share/public';
import { MlServicesContext } from '../../app';
import { IStorageWrapper } from '../../../../../../../src/plugins/kibana_utils/public';

interface StartPlugins {
data: DataPublicPluginStart;
Expand All @@ -22,6 +23,10 @@ interface StartPlugins {
share: SharePluginStart;
}
export type StartServices = CoreStart &
StartPlugins & { appName: string; kibanaVersion: string } & MlServicesContext;
StartPlugins & {
appName: string;
kibanaVersion: string;
storage: IStorageWrapper;
} & MlServicesContext;
export const useMlKibana = () => useKibana<StartServices>();
export type MlKibanaReactContextValue = KibanaReactContextValue<StartServices>;
32 changes: 32 additions & 0 deletions x-pack/plugins/ml/public/application/contexts/ml/use_storage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { useCallback, useState } from 'react';
import { useMlKibana } from '../kibana';

/**
* Hook for accessing and changing a value in the storage.
* @param key - Storage key
* @param initValue
*/
export function useStorage<T>(key: string, initValue?: T): [T, (value: T) => void] {
const {
services: { storage },
} = useMlKibana();

const [val, setVal] = useState<T>(storage.get(key) ?? initValue);

const setStorage = useCallback((value: T): void => {
try {
storage.set(key, value);
setVal(value);
} catch (e) {
throw new Error('Unable to update storage with provided value');
peteharverson marked this conversation as resolved.
Show resolved Hide resolved
}
}, []);

return [val, setStorage];
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { basePath } from './index';
import { JobId } from '../../../../common/types/anomaly_detection_jobs';
import { JOB_ID, PARTITION_FIELD_VALUE } from '../../../../common/constants/anomalies';
import { PartitionFieldsDefinition } from '../results_service/result_service_rx';
import { PartitionFieldsConfig } from '../../../../common/types/storage';

export const resultsApiProvider = (httpService: HttpService) => ({
getAnomaliesTableData(
Expand Down Expand Up @@ -87,9 +88,17 @@ export const resultsApiProvider = (httpService: HttpService) => ({
searchTerm: Record<string, string>,
criteriaFields: Array<{ fieldName: string; fieldValue: any }>,
earliestMs: number,
latestMs: number
latestMs: number,
fieldsConfig?: PartitionFieldsConfig
) {
const body = JSON.stringify({ jobId, searchTerm, criteriaFields, earliestMs, latestMs });
const body = JSON.stringify({
jobId,
searchTerm,
criteriaFields,
earliestMs,
latestMs,
fieldsConfig,
});
return httpService.http$<PartitionFieldsDefinition>({
path: `${basePath()}/results/partition_fields_values`,
method: 'POST',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,13 @@ export interface MetricData extends ResultResponse {

export interface FieldDefinition {
/**
* Partition field name.
* Field name.
*/
name: string | number;
/**
* Partitions field distinct values.
* Field distinct values.
*/
values: any[];
values: Array<{ value: any; maxRecordScore?: number }>;
}

type FieldTypes = 'partition_field' | 'over_field' | 'by_field';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,25 +37,6 @@
}
}

.series-controls {
div.entity-controls {
display: inline-block;
padding-left: $euiSize;

input.entity-input-blank {
border-color: $euiColorDanger;
}

.entity-input {
width: 300px;
}
}

button {
margin-left: $euiSizeXS;
}
}

.forecast-controls {
float: right;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import React, { FC, useMemo, useState } from 'react';
import {
EuiButtonIcon,
EuiFlexGroup,
EuiFlexItem,
EuiFormRow,
EuiHorizontalRule,
EuiIcon,
EuiPopover,
EuiRadioGroup,
EuiRadioGroupOption,
EuiSwitch,
EuiToolTip,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { Entity } from './entity_control';
import { UiPartitionFieldConfig } from '../series_controls/series_controls';
import { EntityFieldType } from '../../../../../common/types/anomalies';

interface EntityConfigProps {
entity: Entity;
isModelPlotEnabled: boolean;
config: UiPartitionFieldConfig;
onConfigChange: (fieldType: EntityFieldType, config: Partial<UiPartitionFieldConfig>) => void;
}

export const EntityConfig: FC<EntityConfigProps> = ({
entity,
isModelPlotEnabled,
config,
onConfigChange,
}) => {
const [isEntityConfigPopoverOpen, setIsEntityConfigPopoverOpen] = useState(false);

const forceSortByName = isModelPlotEnabled && !config?.anomalousOnly;

const sortOptions: EuiRadioGroupOption[] = useMemo(() => {
return [
{
id: 'anomaly_score',
label: i18n.translate('xpack.ml.timeSeriesExplorer.sortByScoreLabel', {
defaultMessage: 'Anomaly score',
}),
disabled: forceSortByName,
},
{
id: 'name',
label: i18n.translate('xpack.ml.timeSeriesExplorer.sortByNameLabel', {
defaultMessage: 'Name',
}),
},
];
}, [isModelPlotEnabled, config]);

const orderOptions: EuiRadioGroupOption[] = useMemo(() => {
return [
{
id: 'asc',
label: i18n.translate('xpack.ml.timeSeriesExplorer.ascOptionsOrderLabel', {
defaultMessage: 'asc',
}),
},
{
id: 'desc',
label: i18n.translate('xpack.ml.timeSeriesExplorer.descOptionsOrderLabel', {
defaultMessage: 'desc',
}),
},
];
}, []);

return (
<EuiPopover
ownFocus
style={{ height: '40px' }}
button={
<EuiButtonIcon
color="text"
iconSize="m"
iconType="gear"
aria-label={i18n.translate('xpack.ml.timeSeriesExplorer.editControlConfiguration', {
defaultMessage: 'Edit field configuration',
})}
onClick={() => {
setIsEntityConfigPopoverOpen(!isEntityConfigPopoverOpen);
}}
data-test-subj={`mlSingleMetricViewerEntitySelectionConfigButton_${entity.fieldName}`}
/>
}
isOpen={isEntityConfigPopoverOpen}
closePopover={() => {
setIsEntityConfigPopoverOpen(false);
}}
>
<div data-test-subj={`mlSingleMetricViewerEntitySelectionConfigPopover_${entity.fieldName}`}>
<EuiFormRow
label={
<FormattedMessage
id="xpack.ml.timeSeriesExplorer.sortByLabel"
defaultMessage="Sort by"
/>
}
>
<EuiRadioGroup
options={sortOptions}
idSelected={forceSortByName ? 'name' : config?.sort?.by}
onChange={(id) => {
onConfigChange(entity.fieldType, {
sort: {
order: config.sort.order,
by: id as UiPartitionFieldConfig['sort']['by'],
},
});
}}
compressed
data-test-subj={`mlSingleMetricViewerEntitySelectionConfigSortBy_${entity.fieldName}`}
/>
</EuiFormRow>
<EuiFormRow
label={
<FormattedMessage id="xpack.ml.timeSeriesExplorer.orderLabel" defaultMessage="Order" />
}
>
<EuiRadioGroup
options={orderOptions}
idSelected={config?.sort?.order}
onChange={(id) => {
onConfigChange(entity.fieldType, {
sort: {
by: config.sort.by,
order: id as UiPartitionFieldConfig['sort']['order'],
},
});
}}
compressed
data-test-subj={`mlSingleMetricViewerEntitySelectionConfigOrder_${entity.fieldName}`}
/>
</EuiFormRow>

<EuiHorizontalRule margin="s" />

<EuiFlexGroup gutterSize={'xs'} alignItems={'center'}>
<EuiFlexItem grow={false}>
{isModelPlotEnabled ? (
<EuiSwitch
label={
<FormattedMessage
id="xpack.ml.timeSeriesExplorer.anomalousOnlyLabel"
defaultMessage="Anomalous only"
/>
}
checked={config.anomalousOnly}
onChange={(e) => {
const isAnomalousOnly = e.target.checked;
onConfigChange(entity.fieldType, {
anomalousOnly: isAnomalousOnly,
sort: {
order: config.sort.order,
by: config.sort.by,
},
});
}}
compressed
data-test-subj={`mlSingleMetricViewerEntitySelectionConfigAnomalousOnly_${entity.fieldName}`}
/>
) : (
<EuiSwitch
label={
<FormattedMessage
id="xpack.ml.timeSeriesExplorer.applyTimeRangeLabel"
defaultMessage="Apply time range"
/>
}
checked={config.applyTimeRange}
onChange={(e) => {
const applyTimeRange = e.target.checked;
onConfigChange(entity.fieldType, {
applyTimeRange,
});
}}
compressed
data-test-subj={`mlSingleMetricViewerEntitySelectionConfigAnomalousOnly_${entity.fieldName}`}
/>
)}
</EuiFlexItem>

<EuiFlexItem grow={false} style={{ width: '16px' }}>
{isModelPlotEnabled && !config?.anomalousOnly ? (
<EuiToolTip
position="top"
content={
<FormattedMessage
id="xpack.ml.timeSeriesExplorer.nonAnomalousResultsWithModelPlotInfo"
defaultMessage="The list contains values from the model plot results."
/>
}
>
<EuiIcon tabIndex={0} type="iInCircle" color={'subdued'} />
</EuiToolTip>
) : null}

{!isModelPlotEnabled && !config?.applyTimeRange ? (
<EuiToolTip
position="top"
content={
<FormattedMessage
id="xpack.ml.timeSeriesExplorer.ignoreTimeRangeInfo"
defaultMessage="The list contains values from all anomalies created during the lifetime of the job."
/>
}
>
<EuiIcon tabIndex={0} type="iInCircle" color={'subdued'} />
</EuiToolTip>
) : null}
</EuiFlexItem>
</EuiFlexGroup>
</div>
</EuiPopover>
);
};
Loading