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 12 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';
27 changes: 27 additions & 0 deletions x-pack/plugins/ml/common/types/storage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* 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_PARTITION_FIELDS_CONFIG = 'ml.singleMetricViewer.partitionFields';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe all the variables here should be referenced as 'entity fields' to make it clearer that they relate to by and over fields, and not just the partition field? e.g.

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed in 9139d1f

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the key should be changed to be ml.singleMetricViewer.entityFields to match the name of the variable.


export type PartitionFieldConfig =
| {
anomalousOnly: boolean;
sort: {
by: string; // 'anomaly_score' | 'name';
order: string; // 'asc' | 'desc';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this type be updated with the commented code?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, updated in c4203a3

};
}
| undefined;

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

export type MlStorage = Partial<{
[ML_PARTITION_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 @@ -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
Expand Up @@ -9,27 +9,56 @@ import React, { Component } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';

import { EuiComboBox, EuiComboBoxOptionOption, EuiFlexItem, EuiFormRow } from '@elastic/eui';
import {
EuiButtonIcon,
EuiSwitch,
EuiComboBox,
EuiComboBoxOptionOption,
EuiFlexItem,
EuiFormRow,
EuiPopover,
EuiText,
EuiRadioGroup,
EuiHorizontalRule,
EuiToolTip,
EuiIcon,
EuiFlexGroup,
EuiRadioGroupOption,
} from '@elastic/eui';
import { EntityFieldType } from '../../../../../common/types/anomalies';
import { UiPartitionFieldConfig } from '../series_controls/series_controls';

export interface Entity {
fieldName: string;
fieldType: EntityFieldType;
fieldValue: any;
fieldValues: any;
fieldValues?: any;
}

interface EntityControlProps {
/**
* Configuration for entity field dropdown options
*/
export interface FieldConfig {
isAnomalousOnly: boolean;
}

export interface EntityControlProps {
entity: Entity;
entityFieldValueChanged: (entity: Entity, fieldValue: any) => void;
isLoading: boolean;
onSearchChange: (entity: Entity, queryTerm: string) => void;
config: UiPartitionFieldConfig;
onConfigChange: (fieldType: EntityFieldType, config: Partial<UiPartitionFieldConfig>) => void;
forceSelection: boolean;
options: Array<EuiComboBoxOptionOption<string>>;
isModelPlotEnabled: boolean;
}

interface EntityControlState {
selectedOptions: Array<EuiComboBoxOptionOption<string>> | undefined;
isLoading: boolean;
options: Array<EuiComboBoxOptionOption<string>> | undefined;
isEntityConfigPopoverOpen: boolean;
}

export const EMPTY_FIELD_VALUE_LABEL = i18n.translate(
Expand All @@ -46,6 +75,7 @@ export class EntityControl extends Component<EntityControlProps, EntityControlSt
selectedOptions: undefined,
options: undefined,
isLoading: false,
isEntityConfigPopoverOpen: false,
};

componentDidUpdate(prevProps: EntityControlProps) {
Expand Down Expand Up @@ -111,6 +141,39 @@ export class EntityControl extends Component<EntityControlProps, EntityControlSt
return label === EMPTY_FIELD_VALUE_LABEL ? <i>{label}</i> : label;
};

getSortOptions = (): EuiRadioGroupOption[] => {
return [
{
id: 'anomaly_score',
label: i18n.translate('xpack.ml.timeSeriesExplorer.sortByScoreLabel', {
defaultMessage: 'Anomaly score',
}),
disabled: !this.props.config?.anomalousOnly && this.props.isModelPlotEnabled,
},
{
id: 'name',
label: i18n.translate('xpack.ml.timeSeriesExplorer.sortByNameLabel', {
defaultMessage: 'Name',
}),
},
];
};

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

render() {
const { entity, forceSelection } = this.props;
const { isLoading, options, selectedOptions } = this.state;
Expand All @@ -136,6 +199,132 @@ export class EntityControl extends Component<EntityControlProps, EntityControlSt
isClearable={false}
renderOption={this.renderOption}
data-test-subj={`mlSingleMetricViewerEntitySelection ${entity.fieldName}`}
prepend={
<EuiPopover
ownFocus
button={
<EuiButtonIcon
color="text"
iconSize="xxl"
iconType="gear"
aria-label={i18n.translate('xpack.ml.timeSeriesExplorer.editControlConfiguration', {
defaultMessage: 'Edit field configuration',
})}
onClick={() => {
this.setState({
isEntityConfigPopoverOpen: !this.state.isEntityConfigPopoverOpen,
});
}}
data-test-subj={`mlSingleMetricViewerEntitySelectionConfigButton_${entity.fieldName}`}
/>
}
isOpen={this.state.isEntityConfigPopoverOpen}
closePopover={() => {
this.setState({
isEntityConfigPopoverOpen: false,
});
}}
>
<EuiText>
<EuiFlexGroup gutterSize={'xs'} alignItems={'center'}>
<EuiFlexItem grow={false}>
<EuiSwitch
label={
<FormattedMessage
id="xpack.ml.timeSeriesExplorer.anomalousOnlyLabel"
defaultMessage="Anomalous only"
/>
}
checked={!!this.props.config?.anomalousOnly}
onChange={(e) => {
const isAnomalousOnly = e.target.checked;
this.props.onConfigChange(this.props.entity.fieldType, {
anomalousOnly: isAnomalousOnly,
sort: {
order: this.props.config.sort.order,
by: this.props.config.sort.by,
},
});
}}
compressed
data-test-subj={`mlSingleMetricViewerEntitySelectionConfigAnomalousOnly_${entity.fieldName}`}
/>
</EuiFlexItem>

<EuiFlexItem grow={false} style={{ width: '16px', height: '24px' }}>
{!this.props.config?.anomalousOnly ? (
<EuiToolTip
position="top"
content={
this.props.isModelPlotEnabled ? (
<FormattedMessage
id="xpack.ml.timeSeriesExplorer.nonAnomalousResultsWithModelPlotInfo"
defaultMessage="You will be suggested to select values for all possible model plot results"
lcawl marked this conversation as resolved.
Show resolved Hide resolved
/>
) : (
<FormattedMessage
id="xpack.ml.timeSeriesExplorer.nonAnomalousResultsAllRecordsInfo"
defaultMessage="You will be suggested to select values for all records created during the job lifetime"
lcawl marked this conversation as resolved.
Show resolved Hide resolved
/>
)
}
>
<EuiIcon tabIndex={0} type="iInCircle" color={'accent'} />
</EuiToolTip>
) : null}
</EuiFlexItem>
</EuiFlexGroup>

<EuiHorizontalRule margin="s" />
<EuiFormRow
label={
<FormattedMessage
id="xpack.ml.timeSeriesExplorer.sortByLabel"
defaultMessage="Sort by"
/>
}
>
<EuiRadioGroup
options={this.getSortOptions()}
idSelected={this.props.config?.sort?.by}
onChange={(id) => {
this.props.onConfigChange(this.props.entity.fieldType, {
sort: {
order: this.props.config.sort.order,
by: id,
},
});
}}
compressed
data-test-subj={`mlSingleMetricViewerEntitySelectionConfigSortBy_${entity.fieldName}`}
/>
</EuiFormRow>
<EuiFormRow
label={
<FormattedMessage
id="xpack.ml.timeSeriesExplorer.orderLabel"
defaultMessage="Order"
/>
}
>
<EuiRadioGroup
options={this.orderOptions}
idSelected={this.props.config?.sort?.order}
onChange={(id) => {
this.props.onConfigChange(this.props.entity.fieldType, {
sort: {
by: this.props.config.sort.by,
order: id,
},
});
}}
compressed
data-test-subj={`mlSingleMetricViewerEntitySelectionConfigOrder_${entity.fieldName}`}
/>
</EuiFormRow>
</EuiText>
</EuiPopover>
}
/>
);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/*
* 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.
*/

export { SeriesControls } from './series_controls';
Loading