[ML] Severity control for Anomaly timeline (#99489)
This commit is contained in:
parent
4c48993bb0
commit
d50a3db2b1
|
@ -112,6 +112,10 @@ export interface ExplorerAppState {
|
|||
viewByFieldName?: string;
|
||||
viewByPerPage?: number;
|
||||
viewByFromPage?: number;
|
||||
/**
|
||||
* Indicated severity threshold for both swim lanes
|
||||
*/
|
||||
severity?: number;
|
||||
};
|
||||
mlExplorerFilter: {
|
||||
influencersFilterQuery?: InfluencersFilterQuery;
|
||||
|
|
|
@ -13,7 +13,7 @@ import { JobSelectorControl } from './job_selector';
|
|||
import { useMlKibana } from '../application/contexts/kibana';
|
||||
import { jobsApiProvider } from '../application/services/ml_api_service/jobs';
|
||||
import { HttpService } from '../application/services/http_service';
|
||||
import { SeverityControl } from './severity_control';
|
||||
import { SeverityControl } from '../application/components/severity_control';
|
||||
import { ResultTypeSelector } from './result_type_selector';
|
||||
import { alertingApiProvider } from '../application/services/ml_api_service/alerting';
|
||||
import { PreviewAlertCondition } from './preview_alert_condition';
|
||||
|
|
|
@ -1,84 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { FC } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { EuiFormRow, EuiRange, EuiRangeProps } from '@elastic/eui';
|
||||
import { SEVERITY_OPTIONS } from '../../application/components/controls/select_severity/select_severity';
|
||||
import { ANOMALY_THRESHOLD } from '../../../common';
|
||||
import './styles.scss';
|
||||
|
||||
export interface SeveritySelectorProps {
|
||||
value: number | undefined;
|
||||
onChange: (value: number) => void;
|
||||
}
|
||||
|
||||
const MAX_ANOMALY_SCORE = 100;
|
||||
|
||||
export const SeverityControl: FC<SeveritySelectorProps> = React.memo(({ value, onChange }) => {
|
||||
const levels: EuiRangeProps['levels'] = [
|
||||
{
|
||||
min: ANOMALY_THRESHOLD.LOW,
|
||||
max: ANOMALY_THRESHOLD.MINOR - 1,
|
||||
color: 'success',
|
||||
},
|
||||
{
|
||||
min: ANOMALY_THRESHOLD.MINOR,
|
||||
max: ANOMALY_THRESHOLD.MAJOR - 1,
|
||||
color: 'primary',
|
||||
},
|
||||
{
|
||||
min: ANOMALY_THRESHOLD.MAJOR,
|
||||
max: ANOMALY_THRESHOLD.CRITICAL,
|
||||
color: 'warning',
|
||||
},
|
||||
{
|
||||
min: ANOMALY_THRESHOLD.CRITICAL,
|
||||
max: MAX_ANOMALY_SCORE,
|
||||
color: 'danger',
|
||||
},
|
||||
];
|
||||
|
||||
const toggleButtons = SEVERITY_OPTIONS.map((v) => ({
|
||||
value: v.val,
|
||||
label: v.display,
|
||||
}));
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.ml.severitySelector.formControlLabel"
|
||||
defaultMessage="Select severity threshold"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiRange
|
||||
className={'mlSeverityControl'}
|
||||
fullWidth
|
||||
min={ANOMALY_THRESHOLD.LOW}
|
||||
max={MAX_ANOMALY_SCORE}
|
||||
value={value ?? ANOMALY_THRESHOLD.LOW}
|
||||
onChange={(e) => {
|
||||
// @ts-ignore Property 'value' does not exist on type 'EventTarget' | (EventTarget & HTMLInputElement)
|
||||
onChange(Number(e.target.value));
|
||||
}}
|
||||
showLabels
|
||||
showValue
|
||||
aria-label={i18n.translate('xpack.ml.severitySelector.formControlLabel', {
|
||||
defaultMessage: 'Select severity threshold',
|
||||
})}
|
||||
showTicks
|
||||
ticks={toggleButtons}
|
||||
levels={levels}
|
||||
data-test-subj={'mlAnomalyAlertScoreSelection'}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
});
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import React, { FC } from 'react';
|
||||
import { EuiSelect } from '@elastic/eui';
|
||||
import { EuiIcon, EuiSelect, EuiToolTip } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { usePageUrlState } from '../../../util/url_state';
|
||||
|
||||
|
@ -78,8 +78,22 @@ export const SelectIntervalUI: FC<SelectIntervalUIProps> = ({ interval, onChange
|
|||
|
||||
return (
|
||||
<EuiSelect
|
||||
prepend={i18n.translate('xpack.ml.explorer.intervalLabel', {
|
||||
defaultMessage: 'Interval',
|
||||
})}
|
||||
append={
|
||||
<EuiToolTip
|
||||
content={i18n.translate('xpack.ml.explorer.intervalTooltip', {
|
||||
defaultMessage:
|
||||
'Show only the highest severity anomaly for each interval (such as hour or day) or show all anomalies in the selected time period.',
|
||||
})}
|
||||
>
|
||||
<EuiIcon type="questionInCircle" color="subdued" />
|
||||
</EuiToolTip>
|
||||
}
|
||||
compressed
|
||||
id="selectInterval"
|
||||
options={OPTIONS}
|
||||
className="ml-select-interval"
|
||||
value={interval.val}
|
||||
onChange={handleOnChange}
|
||||
/>
|
||||
|
|
|
@ -8,11 +8,11 @@
|
|||
/*
|
||||
* React component for rendering a select element with threshold levels.
|
||||
*/
|
||||
import React, { Fragment, FC } from 'react';
|
||||
import React, { Fragment, FC, useMemo } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
import { EuiHealth, EuiSpacer, EuiSuperSelect, EuiText } from '@elastic/eui';
|
||||
import { EuiHealth, EuiSpacer, EuiSuperSelect, EuiText, EuiSuperSelectProps } from '@elastic/eui';
|
||||
|
||||
import { getSeverityColor } from '../../../../../common/util/anomaly_utils';
|
||||
import { usePageUrlState } from '../../../util/url_state';
|
||||
|
@ -124,23 +124,34 @@ export const SelectSeverity: FC<Props> = ({ classNames } = { classNames: '' }) =
|
|||
return <SelectSeverityUI severity={severity} onChange={setSeverity} />;
|
||||
};
|
||||
|
||||
export const SelectSeverityUI: FC<{
|
||||
classNames?: string;
|
||||
severity: TableSeverity;
|
||||
onChange: (s: TableSeverity) => void;
|
||||
}> = ({ classNames = '', severity, onChange }) => {
|
||||
export const SelectSeverityUI: FC<
|
||||
Omit<EuiSuperSelectProps<string>, 'onChange' | 'options'> & {
|
||||
classNames?: string;
|
||||
severity: TableSeverity;
|
||||
onChange: (s: TableSeverity) => void;
|
||||
}
|
||||
> = ({ classNames = '', severity, onChange, compressed }) => {
|
||||
const handleOnChange = (valueDisplay: string) => {
|
||||
onChange(optionValueToThreshold(optionsMap[valueDisplay]));
|
||||
};
|
||||
|
||||
const options = useMemo(() => {
|
||||
return getSeverityOptions();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<EuiSuperSelect
|
||||
prepend={i18n.translate('xpack.ml.explorer.severityThresholdLabel', {
|
||||
defaultMessage: 'Severity',
|
||||
})}
|
||||
id="severityThreshold"
|
||||
data-test-subj={'mlAnomalySeverityThresholdControls'}
|
||||
className={classNames}
|
||||
hasDividers
|
||||
options={getSeverityOptions()}
|
||||
options={options}
|
||||
valueOfSelected={severity.display}
|
||||
onChange={handleOnChange}
|
||||
compressed
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,106 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { FC } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
EuiFieldNumber,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFormRow,
|
||||
EuiRange,
|
||||
EuiRangeProps,
|
||||
} from '@elastic/eui';
|
||||
import { ANOMALY_THRESHOLD } from '../../../../common';
|
||||
import './styles.scss';
|
||||
|
||||
export interface SeveritySelectorProps {
|
||||
value: number | undefined;
|
||||
onChange: (value: number) => void;
|
||||
}
|
||||
|
||||
const MAX_ANOMALY_SCORE = 100;
|
||||
|
||||
export const SeverityControl: FC<SeveritySelectorProps> = React.memo(({ value, onChange }) => {
|
||||
const levels: EuiRangeProps['levels'] = [
|
||||
{
|
||||
min: ANOMALY_THRESHOLD.LOW,
|
||||
max: ANOMALY_THRESHOLD.MINOR - 1,
|
||||
color: 'success',
|
||||
},
|
||||
{
|
||||
min: ANOMALY_THRESHOLD.MINOR,
|
||||
max: ANOMALY_THRESHOLD.MAJOR - 1,
|
||||
color: 'primary',
|
||||
},
|
||||
{
|
||||
min: ANOMALY_THRESHOLD.MAJOR,
|
||||
max: ANOMALY_THRESHOLD.CRITICAL,
|
||||
color: 'warning',
|
||||
},
|
||||
{
|
||||
min: ANOMALY_THRESHOLD.CRITICAL,
|
||||
max: MAX_ANOMALY_SCORE,
|
||||
color: 'danger',
|
||||
},
|
||||
];
|
||||
|
||||
const label = i18n.translate('xpack.ml.severitySelector.formControlLabel', {
|
||||
defaultMessage: 'Severity',
|
||||
});
|
||||
|
||||
const resultValue = value ?? ANOMALY_THRESHOLD.LOW;
|
||||
|
||||
const onChangeCallback = (
|
||||
e: React.ChangeEvent<HTMLInputElement> | React.MouseEvent<HTMLButtonElement>
|
||||
) => {
|
||||
// @ts-ignore Property 'value' does not exist on type 'EventTarget' | (EventTarget & HTMLInputElement)
|
||||
onChange(Number(e.target.value));
|
||||
};
|
||||
|
||||
const ticks = new Array(5).fill(null).map((x, i) => {
|
||||
const v = i * 25;
|
||||
return { value: v, label: v };
|
||||
});
|
||||
|
||||
return (
|
||||
<EuiFormRow fullWidth>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFieldNumber
|
||||
id="severityControl"
|
||||
style={{ width: '70px' }}
|
||||
compressed
|
||||
prepend={label}
|
||||
value={resultValue}
|
||||
onChange={onChangeCallback}
|
||||
min={ANOMALY_THRESHOLD.LOW}
|
||||
max={MAX_ANOMALY_SCORE}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={true}>
|
||||
<EuiRange
|
||||
className={'mlSeverityControl'}
|
||||
fullWidth
|
||||
min={ANOMALY_THRESHOLD.LOW}
|
||||
max={MAX_ANOMALY_SCORE}
|
||||
value={resultValue}
|
||||
onChange={onChangeCallback}
|
||||
aria-label={i18n.translate('xpack.ml.severitySelector.formControlAriaLabel', {
|
||||
defaultMessage: 'Select severity threshold',
|
||||
})}
|
||||
showTicks
|
||||
ticks={ticks}
|
||||
showRange={false}
|
||||
levels={levels}
|
||||
data-test-subj={'mlAnomalyAlertScoreSelection'}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFormRow>
|
||||
);
|
||||
});
|
|
@ -40,14 +40,6 @@ $borderRadius: $euiBorderRadius / 2;
|
|||
font-size: $euiFontSizeXS;
|
||||
}
|
||||
}
|
||||
|
||||
.ml-anomalies-controls {
|
||||
padding-top: $euiSizeXS;
|
||||
|
||||
#show_charts_checkbox_control {
|
||||
padding-top: $euiSizeL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mlSwimLaneContainer {
|
||||
|
|
|
@ -83,6 +83,7 @@ export interface LoadExplorerDataConfig {
|
|||
viewByFromPage: number;
|
||||
viewByPerPage: number;
|
||||
swimlaneContainerWidth: number;
|
||||
swimLaneSeverity: number;
|
||||
}
|
||||
|
||||
export const isLoadExplorerDataConfig = (arg: any): arg is LoadExplorerDataConfig => {
|
||||
|
@ -135,6 +136,7 @@ const loadExplorerDataProvider = (
|
|||
swimlaneContainerWidth,
|
||||
viewByFromPage,
|
||||
viewByPerPage,
|
||||
swimLaneSeverity,
|
||||
} = config;
|
||||
|
||||
const combinedJobRecords: Record<string, CombinedJob> = selectedJobs.reduce((acc, job) => {
|
||||
|
@ -192,7 +194,13 @@ const loadExplorerDataProvider = (
|
|||
influencersFilterQuery
|
||||
)
|
||||
: Promise.resolve({}),
|
||||
overallState: memoizedLoadOverallData(lastRefresh, selectedJobs, swimlaneContainerWidth),
|
||||
overallState: memoizedLoadOverallData(
|
||||
lastRefresh,
|
||||
selectedJobs,
|
||||
swimlaneContainerWidth,
|
||||
undefined,
|
||||
swimLaneSeverity
|
||||
),
|
||||
tableData: memoizedLoadAnomaliesTableData(
|
||||
lastRefresh,
|
||||
selectedCells,
|
||||
|
@ -278,7 +286,9 @@ const loadExplorerDataProvider = (
|
|||
viewByPerPage,
|
||||
viewByFromPage,
|
||||
swimlaneContainerWidth,
|
||||
influencersFilterQuery
|
||||
influencersFilterQuery,
|
||||
undefined,
|
||||
swimLaneSeverity
|
||||
),
|
||||
}).pipe(
|
||||
map(({ viewBySwimlaneState, filteredTopInfluencers }) => {
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { FC, useMemo, useState } from 'react';
|
||||
import React, { FC, useCallback, useMemo, useState } from 'react';
|
||||
import { isEqual } from 'lodash';
|
||||
import {
|
||||
EuiPanel,
|
||||
|
@ -14,7 +14,6 @@ import {
|
|||
EuiButtonIcon,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFormRow,
|
||||
EuiSelect,
|
||||
EuiTitle,
|
||||
EuiSpacer,
|
||||
|
@ -35,7 +34,9 @@ import { ExplorerNoInfluencersFound } from './components/explorer_no_influencers
|
|||
import { SwimlaneContainer } from './swimlane_container';
|
||||
import { AppStateSelectedCells, OverallSwimlaneData, ViewBySwimLaneData } from './explorer_utils';
|
||||
import { NoOverallData } from './components/no_overall_data';
|
||||
import { SeverityControl } from '../components/severity_control';
|
||||
import { AnomalyTimelineHelpPopover } from './anomaly_timeline_help_popover';
|
||||
import { isDefined } from '../../../common/types/guards';
|
||||
|
||||
function mapSwimlaneOptionsToEuiOptions(options: string[]) {
|
||||
return options.map((option) => ({
|
||||
|
@ -76,10 +77,8 @@ export const AnomalyTimeline: FC<AnomalyTimelineProps> = React.memo(
|
|||
filterActive,
|
||||
filteredFields,
|
||||
maskAll,
|
||||
overallSwimlaneData,
|
||||
selectedCells,
|
||||
viewByLoadedForTimeFormatted,
|
||||
viewBySwimlaneData,
|
||||
viewBySwimlaneDataLoading,
|
||||
viewBySwimlaneFieldName,
|
||||
viewBySwimlaneOptions,
|
||||
|
@ -89,6 +88,9 @@ export const AnomalyTimeline: FC<AnomalyTimelineProps> = React.memo(
|
|||
swimlaneLimit,
|
||||
loading,
|
||||
overallAnnotations,
|
||||
swimLaneSeverity,
|
||||
overallSwimlaneData,
|
||||
viewBySwimlaneData,
|
||||
} = explorerState;
|
||||
|
||||
const annotations = useMemo(() => overallAnnotations.annotationsData, [overallAnnotations]);
|
||||
|
@ -128,7 +130,7 @@ export const AnomalyTimeline: FC<AnomalyTimelineProps> = React.memo(
|
|||
return (
|
||||
<>
|
||||
<EuiPanel paddingSize="m">
|
||||
<EuiFlexGroup direction="row" gutterSize="m" responsive={false} alignItems="center">
|
||||
<EuiFlexGroup direction="row" gutterSize="xs" responsive={false} alignItems="baseline">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle className="panel-title">
|
||||
<h2>
|
||||
|
@ -139,68 +141,10 @@ export const AnomalyTimeline: FC<AnomalyTimelineProps> = React.memo(
|
|||
</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
{viewBySwimlaneOptions.length > 0 && (
|
||||
<>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFormRow
|
||||
label={
|
||||
<span className="eui-textNoWrap">
|
||||
<FormattedMessage
|
||||
id="xpack.ml.explorer.viewByLabel"
|
||||
defaultMessage="View by"
|
||||
/>
|
||||
</span>
|
||||
}
|
||||
display={'columnCompressed'}
|
||||
>
|
||||
<EuiSelect
|
||||
compressed
|
||||
id="selectViewBy"
|
||||
options={mapSwimlaneOptionsToEuiOptions(viewBySwimlaneOptions)}
|
||||
value={viewBySwimlaneFieldName}
|
||||
onChange={(e) => explorerService.setViewBySwimlaneFieldName(e.target.value)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
{selectedCells ? (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
size="xs"
|
||||
onClick={setSelectedCells.bind(null, undefined)}
|
||||
data-test-subj="mlAnomalyTimelineClearSelection"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.explorer.clearSelectionLabel"
|
||||
defaultMessage="Clear selection"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
) : null}
|
||||
<EuiFlexItem grow={false} style={{ alignSelf: 'center' }}>
|
||||
<div className="panel-sub-title">
|
||||
{viewByLoadedForTimeFormatted && (
|
||||
<FormattedMessage
|
||||
id="xpack.ml.explorer.sortedByMaxAnomalyScoreForTimeFormattedLabel"
|
||||
defaultMessage="(Sorted by max anomaly score for {viewByLoadedForTimeFormatted})"
|
||||
values={{ viewByLoadedForTimeFormatted }}
|
||||
/>
|
||||
)}
|
||||
{viewByLoadedForTimeFormatted === undefined && (
|
||||
<FormattedMessage
|
||||
id="xpack.ml.explorer.sortedByMaxAnomalyScoreLabel"
|
||||
defaultMessage="(Sorted by max anomaly score)"
|
||||
/>
|
||||
)}
|
||||
{filterActive === true && viewBySwimlaneFieldName === VIEW_BY_JOB_LABEL && (
|
||||
<FormattedMessage
|
||||
id="xpack.ml.explorer.jobScoreAcrossAllInfluencersLabel"
|
||||
defaultMessage="(Job score across all influencers)"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</EuiFlexItem>
|
||||
</>
|
||||
)}
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<AnomalyTimelineHelpPopover />
|
||||
</EuiFlexItem>
|
||||
|
||||
{menuItems.length > 0 && (
|
||||
<EuiFlexItem grow={false} style={{ marginLeft: 'auto', alignSelf: 'baseline' }}>
|
||||
|
@ -226,14 +170,83 @@ export const AnomalyTimeline: FC<AnomalyTimelineProps> = React.memo(
|
|||
</EuiPopover>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<AnomalyTimelineHelpPopover />
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
<EuiFlexGroup direction="row" gutterSize="m" responsive={false} alignItems="baseline">
|
||||
{viewBySwimlaneOptions.length > 0 && (
|
||||
<>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiSelect
|
||||
prepend={i18n.translate('xpack.ml.explorer.viewByLabel', {
|
||||
defaultMessage: 'View by',
|
||||
})}
|
||||
compressed
|
||||
id="selectViewBy"
|
||||
options={mapSwimlaneOptionsToEuiOptions(viewBySwimlaneOptions)}
|
||||
value={viewBySwimlaneFieldName}
|
||||
onChange={(e) => explorerService.setViewBySwimlaneFieldName(e.target.value)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</>
|
||||
)}
|
||||
|
||||
<EuiFlexItem grow={true}>
|
||||
<SeverityControl
|
||||
value={swimLaneSeverity ?? 0}
|
||||
onChange={useCallback((update) => {
|
||||
explorerService.setSwimLaneSeverity(update);
|
||||
}, [])}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
<EuiFlexGroup direction="row" gutterSize="m" responsive={false} alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<div className="panel-sub-title">
|
||||
{viewByLoadedForTimeFormatted && (
|
||||
<FormattedMessage
|
||||
id="xpack.ml.explorer.sortedByMaxAnomalyScoreForTimeFormattedLabel"
|
||||
defaultMessage="(Sorted by max anomaly score for {viewByLoadedForTimeFormatted})"
|
||||
values={{ viewByLoadedForTimeFormatted }}
|
||||
/>
|
||||
)}
|
||||
{isDefined(viewByLoadedForTimeFormatted) ? null : (
|
||||
<FormattedMessage
|
||||
id="xpack.ml.explorer.sortedByMaxAnomalyScoreLabel"
|
||||
defaultMessage="(Sorted by max anomaly score)"
|
||||
/>
|
||||
)}
|
||||
{filterActive === true && viewBySwimlaneFieldName === VIEW_BY_JOB_LABEL && (
|
||||
<FormattedMessage
|
||||
id="xpack.ml.explorer.jobScoreAcrossAllInfluencersLabel"
|
||||
defaultMessage="(Job score across all influencers)"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</EuiFlexItem>
|
||||
|
||||
{selectedCells ? (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
size="xs"
|
||||
onClick={setSelectedCells.bind(null, undefined)}
|
||||
data-test-subj="mlAnomalyTimelineClearSelection"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.explorer.clearSelectionLabel"
|
||||
defaultMessage="Clear selection"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
) : null}
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
<SwimlaneContainer
|
||||
id="overall"
|
||||
data-test-subj="mlAnomalyExplorerSwimlaneOverall"
|
||||
|
@ -249,6 +262,7 @@ export const AnomalyTimeline: FC<AnomalyTimelineProps> = React.memo(
|
|||
noDataWarning={<NoOverallData />}
|
||||
showTimeline={false}
|
||||
annotationsData={annotations}
|
||||
showLegend={false}
|
||||
/>
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
|
@ -266,7 +280,7 @@ export const AnomalyTimeline: FC<AnomalyTimelineProps> = React.memo(
|
|||
})
|
||||
}
|
||||
timeBuckets={timeBuckets}
|
||||
showLegend={true}
|
||||
showLegend={false}
|
||||
swimlaneData={viewBySwimlaneData as ViewBySwimLaneData}
|
||||
swimlaneType={SWIMLANE_TYPE.VIEW_BY}
|
||||
selection={selectedCells}
|
||||
|
|
|
@ -19,9 +19,7 @@ import {
|
|||
EuiCallOut,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFormRow,
|
||||
EuiHorizontalRule,
|
||||
EuiIcon,
|
||||
EuiIconTip,
|
||||
EuiPage,
|
||||
EuiPageBody,
|
||||
|
@ -29,7 +27,6 @@ import {
|
|||
EuiPageHeaderSection,
|
||||
EuiSpacer,
|
||||
EuiTitle,
|
||||
EuiToolTip,
|
||||
EuiLoadingContent,
|
||||
EuiPanel,
|
||||
EuiAccordion,
|
||||
|
@ -78,6 +75,7 @@ import { ANOMALY_DETECTION_DEFAULT_TIME_RANGE } from '../../../common/constants/
|
|||
import { withKibana } from '../../../../../../src/plugins/kibana_react/public';
|
||||
import { ML_APP_URL_GENERATOR } from '../../../common/constants/ml_url_generator';
|
||||
import { AnomalyContextMenu } from './anomaly_context_menu';
|
||||
import { isDefined } from '../../../common/types/guards';
|
||||
|
||||
const ExplorerPage = ({
|
||||
children,
|
||||
|
@ -263,6 +261,7 @@ export class ExplorerUI extends React.Component {
|
|||
selectedCells,
|
||||
selectedJobs,
|
||||
tableData,
|
||||
swimLaneSeverity,
|
||||
} = this.props.explorerState;
|
||||
const { annotationsData, aggregations, error: annotationsError } = annotations;
|
||||
|
||||
|
@ -276,6 +275,8 @@ export class ExplorerUI extends React.Component {
|
|||
(hasResults && overallSwimlaneData.points.some((v) => v.value > 0)) ||
|
||||
tableData.anomalies?.length > 0;
|
||||
|
||||
const hasActiveFilter = isDefined(swimLaneSeverity);
|
||||
|
||||
if (noJobsFound && !loading) {
|
||||
return (
|
||||
<ExplorerPage jobSelectorProps={jobSelectorProps}>
|
||||
|
@ -284,7 +285,7 @@ export class ExplorerUI extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
if (hasResultsWithAnomalies === false && !loading) {
|
||||
if (!hasResultsWithAnomalies && !loading && !hasActiveFilter) {
|
||||
return (
|
||||
<ExplorerPage jobSelectorProps={jobSelectorProps}>
|
||||
<ExplorerNoResultsFound
|
||||
|
@ -374,7 +375,9 @@ export class ExplorerUI extends React.Component {
|
|||
explorerState={this.props.explorerState}
|
||||
setSelectedCells={this.props.setSelectedCells}
|
||||
/>
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
{annotationsError !== undefined && (
|
||||
<>
|
||||
<EuiTitle
|
||||
|
@ -402,9 +405,9 @@ export class ExplorerUI extends React.Component {
|
|||
<EuiSpacer size="m" />
|
||||
</>
|
||||
)}
|
||||
{loading === false && tableData.anomalies?.length && (
|
||||
{loading === false && tableData.anomalies?.length ? (
|
||||
<AnomaliesMap anomalies={tableData.anomalies} jobIds={selectedJobIds} />
|
||||
)}
|
||||
) : null}
|
||||
{annotationsData.length > 0 && (
|
||||
<>
|
||||
<EuiPanel data-test-subj="mlAnomalyExplorerAnnotationsPanel loaded">
|
||||
|
@ -476,47 +479,16 @@ export class ExplorerUI extends React.Component {
|
|||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiFlexGroup
|
||||
direction="row"
|
||||
gutterSize="l"
|
||||
responsive={true}
|
||||
className="ml-anomalies-controls"
|
||||
>
|
||||
<EuiFlexItem grow={false} style={{ width: '170px' }}>
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.ml.explorer.severityThresholdLabel', {
|
||||
defaultMessage: 'Severity threshold',
|
||||
})}
|
||||
>
|
||||
<SelectSeverity />
|
||||
</EuiFormRow>
|
||||
<EuiFlexGroup direction="row" gutterSize="l" responsive={true} alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<SelectSeverity />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false} style={{ width: '170px' }}>
|
||||
<EuiFormRow
|
||||
label={
|
||||
<EuiToolTip
|
||||
content={i18n.translate('xpack.ml.explorer.intervalTooltip', {
|
||||
defaultMessage:
|
||||
'Show only the highest severity anomaly for each interval (such as hour or day) or show all anomalies in the selected time period.',
|
||||
})}
|
||||
>
|
||||
<span>
|
||||
{i18n.translate('xpack.ml.explorer.intervalLabel', {
|
||||
defaultMessage: 'Interval',
|
||||
})}
|
||||
<EuiIcon type="questionInCircle" color="subdued" />
|
||||
</span>
|
||||
</EuiToolTip>
|
||||
}
|
||||
>
|
||||
<SelectInterval />
|
||||
</EuiFormRow>
|
||||
<EuiFlexItem grow={false}>
|
||||
<SelectInterval />
|
||||
</EuiFlexItem>
|
||||
{chartsData.seriesToPlot.length > 0 && selectedCells !== undefined && (
|
||||
<EuiFlexItem grow={false} style={{ alignSelf: 'center' }}>
|
||||
<EuiFormRow label="​">
|
||||
<CheckboxShowCharts />
|
||||
</EuiFormRow>
|
||||
<EuiFlexItem grow={false}>
|
||||
<CheckboxShowCharts />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
|
@ -524,7 +496,7 @@ export class ExplorerUI extends React.Component {
|
|||
<EuiSpacer size="m" />
|
||||
|
||||
<div className="euiText explorer-charts">
|
||||
{showCharts && (
|
||||
{showCharts ? (
|
||||
<ExplorerChartsContainer
|
||||
{...{
|
||||
...chartsData,
|
||||
|
@ -535,7 +507,7 @@ export class ExplorerUI extends React.Component {
|
|||
onSelectEntity: this.applyFilter,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<AnomaliesTable
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSpacer, EuiText } from '@elastic/eui';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { FC } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
@ -56,21 +56,9 @@ export const ExplorerAnomaliesContainer: FC<ExplorerAnomaliesContainerProps> = (
|
|||
}) => {
|
||||
return (
|
||||
<>
|
||||
<EuiFlexGroup
|
||||
id={id}
|
||||
direction="row"
|
||||
gutterSize="l"
|
||||
responsive={true}
|
||||
className="ml-anomalies-controls"
|
||||
>
|
||||
<EuiFlexItem grow={false} style={{ width: '170px' }}>
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.ml.explorer.severityThresholdLabel', {
|
||||
defaultMessage: 'Severity threshold',
|
||||
})}
|
||||
>
|
||||
<SelectSeverityUI severity={severity} onChange={setSeverity} />
|
||||
</EuiFormRow>
|
||||
<EuiFlexGroup id={id} direction="row" gutterSize="l" responsive={true}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<SelectSeverityUI severity={severity} onChange={setSeverity} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
|
|
|
@ -33,6 +33,7 @@ export const EXPLORER_ACTION = {
|
|||
SET_VIEW_BY_SWIMLANE_LOADING: 'setViewBySwimlaneLoading',
|
||||
SET_VIEW_BY_PER_PAGE: 'setViewByPerPage',
|
||||
SET_VIEW_BY_FROM_PAGE: 'setViewByFromPage',
|
||||
SET_SWIM_LANE_SEVERITY: 'setSwimLaneSeverity',
|
||||
};
|
||||
|
||||
export const FILTER_ACTION = {
|
||||
|
|
|
@ -79,6 +79,10 @@ const explorerAppState$: Observable<ExplorerAppState> = explorerState$.pipe(
|
|||
appState.mlExplorerSwimlane.viewByPerPage = state.viewByPerPage;
|
||||
}
|
||||
|
||||
if (state.swimLaneSeverity !== undefined) {
|
||||
appState.mlExplorerSwimlane.severity = state.swimLaneSeverity;
|
||||
}
|
||||
|
||||
if (state.filterActive) {
|
||||
appState.mlExplorerFilter.influencersFilterQuery = state.influencersFilterQuery;
|
||||
appState.mlExplorerFilter.filterActive = state.filterActive;
|
||||
|
@ -161,6 +165,9 @@ export const explorerService = {
|
|||
setViewByPerPage: (payload: number) => {
|
||||
explorerAction$.next({ type: EXPLORER_ACTION.SET_VIEW_BY_PER_PAGE, payload });
|
||||
},
|
||||
setSwimLaneSeverity: (payload: number) => {
|
||||
explorerAction$.next({ type: EXPLORER_ACTION.SET_SWIM_LANE_SEVERITY, payload });
|
||||
},
|
||||
};
|
||||
|
||||
export type ExplorerService = typeof explorerService;
|
||||
|
|
|
@ -149,6 +149,15 @@ export const explorerReducer = (state: ExplorerState, nextAction: Action): Explo
|
|||
};
|
||||
break;
|
||||
|
||||
case EXPLORER_ACTION.SET_SWIM_LANE_SEVERITY:
|
||||
nextState = {
|
||||
...state,
|
||||
// reset current page on the page size change
|
||||
viewByFromPage: 1,
|
||||
swimLaneSeverity: payload,
|
||||
};
|
||||
break;
|
||||
|
||||
default:
|
||||
nextState = state;
|
||||
}
|
||||
|
@ -181,7 +190,9 @@ export const explorerReducer = (state: ExplorerState, nextAction: Action): Explo
|
|||
...nextState,
|
||||
swimlaneBucketInterval,
|
||||
viewByLoadedForTimeFormatted: timeRange
|
||||
? formatHumanReadableDateTime(timeRange.earliestMs)
|
||||
? `${formatHumanReadableDateTime(timeRange.earliestMs)} - ${formatHumanReadableDateTime(
|
||||
timeRange.latestMs
|
||||
)}`
|
||||
: null,
|
||||
viewBySwimlaneFieldName,
|
||||
viewBySwimlaneOptions,
|
||||
|
|
|
@ -58,6 +58,7 @@ export interface ExplorerState {
|
|||
viewByFromPage: number;
|
||||
viewBySwimlaneOptions: string[];
|
||||
swimlaneLimit?: number;
|
||||
swimLaneSeverity?: number;
|
||||
}
|
||||
|
||||
function getDefaultIndexPattern() {
|
||||
|
|
|
@ -68,6 +68,10 @@ declare global {
|
|||
const RESIZE_THROTTLE_TIME_MS = 500;
|
||||
const CELL_HEIGHT = 30;
|
||||
const LEGEND_HEIGHT = 34;
|
||||
/**
|
||||
* Minimum container height to make sure "No data" message is displayed without overflow.
|
||||
*/
|
||||
const MIN_CONTAINER_HEIGHT = 40;
|
||||
|
||||
const Y_AXIS_HEIGHT = 24;
|
||||
|
||||
|
@ -245,7 +249,10 @@ export const SwimlaneContainer: FC<SwimlaneProps> = ({
|
|||
return isLoading
|
||||
? containerHeightRef.current
|
||||
: // TODO update when elastic charts X label will be fixed
|
||||
rowsCount * CELL_HEIGHT + LEGEND_HEIGHT + (true ? Y_AXIS_HEIGHT : 0);
|
||||
Math.max(
|
||||
rowsCount * CELL_HEIGHT + (showLegend ? LEGEND_HEIGHT : 0) + (true ? Y_AXIS_HEIGHT : 0),
|
||||
MIN_CONTAINER_HEIGHT
|
||||
);
|
||||
}, [isLoading, rowsCount, showTimeline]);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -331,7 +338,7 @@ export const SwimlaneContainer: FC<SwimlaneProps> = ({
|
|||
brushArea: {
|
||||
stroke: isDarkTheme ? 'rgb(255, 255, 255)' : 'rgb(105, 112, 125)',
|
||||
},
|
||||
maxLegendHeight: LEGEND_HEIGHT,
|
||||
...(showLegend ? { maxLegendHeight: LEGEND_HEIGHT } : {}),
|
||||
timeZone: 'UTC',
|
||||
};
|
||||
}, [
|
||||
|
@ -463,7 +470,7 @@ export const SwimlaneContainer: FC<SwimlaneProps> = ({
|
|||
)}
|
||||
{!isLoading && !showSwimlane && (
|
||||
<EuiEmptyPrompt
|
||||
titleSize="xs"
|
||||
titleSize="xxs"
|
||||
style={{ padding: 0 }}
|
||||
title={<h2>{noDataWarning}</h2>}
|
||||
/>
|
||||
|
|
|
@ -177,7 +177,7 @@ const ExplorerUrlStateManager: FC<ExplorerUrlStateManagerProps> = ({ jobsWithTim
|
|||
explorerService.setFilterData(filterData);
|
||||
}
|
||||
|
||||
const { viewByFieldName, viewByFromPage, viewByPerPage } =
|
||||
const { viewByFieldName, viewByFromPage, viewByPerPage, severity } =
|
||||
explorerUrlState?.mlExplorerSwimlane ?? {};
|
||||
|
||||
if (viewByFieldName !== undefined) {
|
||||
|
@ -191,6 +191,10 @@ const ExplorerUrlStateManager: FC<ExplorerUrlStateManagerProps> = ({ jobsWithTim
|
|||
if (viewByFromPage !== undefined) {
|
||||
explorerService.setViewByFromPage(viewByFromPage);
|
||||
}
|
||||
|
||||
if (severity !== undefined) {
|
||||
explorerService.setSwimLaneSeverity(severity);
|
||||
}
|
||||
}, []);
|
||||
|
||||
/** Sync URL state with {@link explorerService} state */
|
||||
|
@ -238,6 +242,7 @@ const ExplorerUrlStateManager: FC<ExplorerUrlStateManagerProps> = ({ jobsWithTim
|
|||
swimlaneContainerWidth: explorerState.swimlaneContainerWidth,
|
||||
viewByPerPage: explorerState.viewByPerPage,
|
||||
viewByFromPage: explorerState.viewByFromPage,
|
||||
swimLaneSeverity: explorerState.swimLaneSeverity,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
|
|
|
@ -98,7 +98,8 @@ export class AnomalyTimelineService {
|
|||
public async loadOverallData(
|
||||
selectedJobs: ExplorerJob[],
|
||||
chartWidth?: number,
|
||||
bucketInterval?: TimeBucketsInterval
|
||||
bucketInterval?: TimeBucketsInterval,
|
||||
overallScore?: number
|
||||
): Promise<OverallSwimlaneData> {
|
||||
const interval = bucketInterval ?? this.getSwimlaneBucketInterval(selectedJobs, chartWidth!);
|
||||
|
||||
|
@ -127,7 +128,8 @@ export class AnomalyTimelineService {
|
|||
1,
|
||||
overallBucketsBounds.min.valueOf(),
|
||||
overallBucketsBounds.max.valueOf(),
|
||||
interval.asSeconds() + 's'
|
||||
interval.asSeconds() + 's',
|
||||
overallScore
|
||||
);
|
||||
const overallSwimlaneData = this.processOverallResults(
|
||||
resp.results,
|
||||
|
@ -161,7 +163,8 @@ export class AnomalyTimelineService {
|
|||
fromPage: number,
|
||||
swimlaneContainerWidth?: number,
|
||||
influencersFilterQuery?: any,
|
||||
bucketInterval?: TimeBucketsInterval
|
||||
bucketInterval?: TimeBucketsInterval,
|
||||
swimLaneSeverity?: number
|
||||
): Promise<SwimlaneData | undefined> {
|
||||
const timefilterBounds = this.getTimeBounds();
|
||||
|
||||
|
@ -195,7 +198,8 @@ export class AnomalyTimelineService {
|
|||
searchBounds.max.valueOf(),
|
||||
intervalMs,
|
||||
perPage,
|
||||
fromPage
|
||||
fromPage,
|
||||
swimLaneSeverity
|
||||
);
|
||||
} else {
|
||||
response = await this.mlResultsService.getInfluencerValueMaxScoreByTime(
|
||||
|
@ -208,7 +212,8 @@ export class AnomalyTimelineService {
|
|||
swimlaneLimit,
|
||||
perPage,
|
||||
fromPage,
|
||||
influencersFilterQuery
|
||||
influencersFilterQuery,
|
||||
swimLaneSeverity
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -323,14 +323,22 @@ export function mlApiServicesProvider(httpService: HttpService) {
|
|||
bucketSpan,
|
||||
start,
|
||||
end,
|
||||
overallScore,
|
||||
}: {
|
||||
jobId: string;
|
||||
topN: string;
|
||||
bucketSpan: string;
|
||||
start: number;
|
||||
end: number;
|
||||
overallScore?: number;
|
||||
}) {
|
||||
const body = JSON.stringify({ topN, bucketSpan, start, end });
|
||||
const body = JSON.stringify({
|
||||
topN,
|
||||
bucketSpan,
|
||||
start,
|
||||
end,
|
||||
...(overallScore ? { overall_score: overallScore } : {}),
|
||||
});
|
||||
return httpService.http<any>({
|
||||
path: `${basePath()}/anomaly_detectors/${jobId}/results/overall_buckets`,
|
||||
method: 'POST',
|
||||
|
|
|
@ -22,7 +22,8 @@ export function resultsServiceProvider(
|
|||
latestMs: number,
|
||||
intervalMs: number,
|
||||
perPage?: number,
|
||||
fromPage?: number
|
||||
fromPage?: number,
|
||||
swimLaneSeverity?: number
|
||||
): Promise<any>;
|
||||
getTopInfluencers(
|
||||
selectedJobIds: string[],
|
||||
|
@ -40,7 +41,8 @@ export function resultsServiceProvider(
|
|||
topN: any,
|
||||
earliestMs: any,
|
||||
latestMs: any,
|
||||
interval?: any
|
||||
interval?: any,
|
||||
overallScore?: number
|
||||
): Promise<any>;
|
||||
getInfluencerValueMaxScoreByTime(
|
||||
jobIds: string[],
|
||||
|
@ -52,7 +54,8 @@ export function resultsServiceProvider(
|
|||
maxResults: number,
|
||||
perPage: number,
|
||||
fromPage: number,
|
||||
influencersFilterQuery: InfluencersFilterQuery
|
||||
influencersFilterQuery: InfluencersFilterQuery,
|
||||
swimLaneSeverity?: number
|
||||
): Promise<any>;
|
||||
getRecordInfluencers(): Promise<any>;
|
||||
getRecordsForDetector(): Promise<any>;
|
||||
|
|
|
@ -30,7 +30,15 @@ export function resultsServiceProvider(mlApiServices) {
|
|||
// Pass an empty array or ['*'] to search over all job IDs.
|
||||
// Returned response contains a results property, with a key for job
|
||||
// which has results for the specified time range.
|
||||
getScoresByBucket(jobIds, earliestMs, latestMs, intervalMs, perPage = 10, fromPage = 1) {
|
||||
getScoresByBucket(
|
||||
jobIds,
|
||||
earliestMs,
|
||||
latestMs,
|
||||
intervalMs,
|
||||
perPage = 10,
|
||||
fromPage = 1,
|
||||
swimLaneSeverity = 0
|
||||
) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const obj = {
|
||||
success: true,
|
||||
|
@ -49,6 +57,13 @@ export function resultsServiceProvider(mlApiServices) {
|
|||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
range: {
|
||||
anomaly_score: {
|
||||
gt: swimLaneSeverity,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
if (jobIds && jobIds.length > 0 && !(jobIds.length === 1 && jobIds[0] === '*')) {
|
||||
|
@ -463,7 +478,7 @@ export function resultsServiceProvider(mlApiServices) {
|
|||
// Obtains the overall bucket scores for the specified job ID(s).
|
||||
// Pass ['*'] to search over all job IDs.
|
||||
// Returned response contains a results property as an object of max score by time.
|
||||
getOverallBucketScores(jobIds, topN, earliestMs, latestMs, interval) {
|
||||
getOverallBucketScores(jobIds, topN, earliestMs, latestMs, interval, overallScore) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const obj = { success: true, results: {} };
|
||||
|
||||
|
@ -474,6 +489,7 @@ export function resultsServiceProvider(mlApiServices) {
|
|||
bucketSpan: interval,
|
||||
start: earliestMs,
|
||||
end: latestMs,
|
||||
overallScore,
|
||||
})
|
||||
.then((resp) => {
|
||||
const dataByTime = get(resp, ['overall_buckets'], []);
|
||||
|
@ -507,7 +523,8 @@ export function resultsServiceProvider(mlApiServices) {
|
|||
maxResults = ANOMALY_SWIM_LANE_HARD_LIMIT,
|
||||
perPage = SWIM_LANE_DEFAULT_PAGE_SIZE,
|
||||
fromPage = 1,
|
||||
influencersFilterQuery
|
||||
influencersFilterQuery,
|
||||
swimLaneSeverity
|
||||
) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const obj = { success: true, results: {} };
|
||||
|
@ -527,7 +544,7 @@ export function resultsServiceProvider(mlApiServices) {
|
|||
{
|
||||
range: {
|
||||
influencer_score: {
|
||||
gt: 0,
|
||||
gt: swimLaneSeverity !== undefined ? swimLaneSeverity : 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -19,10 +19,6 @@
|
|||
float: right;
|
||||
}
|
||||
|
||||
.ml-anomalies-controls {
|
||||
padding-top: $euiSizeXS;
|
||||
}
|
||||
|
||||
.ml-timeseries-chart {
|
||||
svg {
|
||||
font-size: $euiFontSizeXS;
|
||||
|
|
|
@ -26,11 +26,9 @@ import {
|
|||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFormRow,
|
||||
EuiIcon,
|
||||
EuiSpacer,
|
||||
EuiPanel,
|
||||
EuiTitle,
|
||||
EuiToolTip,
|
||||
EuiAccordion,
|
||||
EuiBadge,
|
||||
} from '@elastic/eui';
|
||||
|
@ -1273,41 +1271,12 @@ export class TimeSeriesExplorer extends React.Component {
|
|||
/>
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
<EuiFlexGroup
|
||||
direction="row"
|
||||
gutterSize="l"
|
||||
responsive={true}
|
||||
className="ml-anomalies-controls"
|
||||
>
|
||||
<EuiFlexItem grow={false} style={{ width: '170px' }}>
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.ml.timeSeriesExplorer.severityThresholdLabel', {
|
||||
defaultMessage: 'Severity threshold',
|
||||
})}
|
||||
>
|
||||
<SelectSeverity />
|
||||
</EuiFormRow>
|
||||
<EuiFlexGroup direction="row" gutterSize="l" responsive={true}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<SelectSeverity />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false} style={{ width: '170px' }}>
|
||||
<EuiFormRow
|
||||
label={
|
||||
<EuiToolTip
|
||||
content={i18n.translate('xpack.ml.timeSeriesExplorer.intervalTooltip', {
|
||||
defaultMessage:
|
||||
'Show only the highest severity anomaly for each interval (such as hour or day) or show all anomalies in the selected time period.',
|
||||
})}
|
||||
>
|
||||
<span>
|
||||
{i18n.translate('xpack.ml.timeSeriesExplorer.intervalLabel', {
|
||||
defaultMessage: 'Interval',
|
||||
})}
|
||||
<EuiIcon type="questionInCircle" color="subdued" />
|
||||
</span>
|
||||
</EuiToolTip>
|
||||
}
|
||||
>
|
||||
<SelectInterval />
|
||||
</EuiFormRow>
|
||||
<EuiFlexItem grow={false}>
|
||||
<SelectInterval />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="m" />
|
||||
|
|
|
@ -522,6 +522,7 @@ export function jobRoutes({ router, routeGuard }: RouteInitialization) {
|
|||
bucket_span: request.body.bucketSpan,
|
||||
start: request.body.start !== undefined ? String(request.body.start) : undefined,
|
||||
end: request.body.end !== undefined ? String(request.body.end) : undefined,
|
||||
overall_score: request.body.overall_score ?? 0,
|
||||
},
|
||||
});
|
||||
return response.ok({
|
||||
|
|
|
@ -186,6 +186,7 @@ export const getOverallBucketsSchema = schema.object({
|
|||
bucketSpan: schema.string(),
|
||||
start: schema.number(),
|
||||
end: schema.number(),
|
||||
overall_score: schema.maybe(schema.number()),
|
||||
});
|
||||
|
||||
export const getCategoriesSchema = schema.object({
|
||||
|
|
|
@ -15967,7 +15967,6 @@
|
|||
"xpack.ml.timeSeriesExplorer.forecastsList.viewForecastAriaLabel": "{createdDate} に作成された予測を表示",
|
||||
"xpack.ml.timeSeriesExplorer.highestAnomalyScoreErrorToastTitle": "最高異常値スコアのレコードの取得中にエラーが発生しました",
|
||||
"xpack.ml.timeSeriesExplorer.ignoreTimeRangeInfo": "リストには、ジョブのライフタイム中に作成されたすべての異常値の値が含まれます。",
|
||||
"xpack.ml.timeSeriesExplorer.intervalLabel": "間隔",
|
||||
"xpack.ml.timeSeriesExplorer.invalidTimeRangeInUrlCallout": "無効なデフォルト時間フィルターのため、このジョブの時間フィルターが全範囲に変更されました。{field}の詳細設定を確認してください。",
|
||||
"xpack.ml.timeSeriesExplorer.loadingLabel": "読み込み中",
|
||||
"xpack.ml.timeSeriesExplorer.metricPlotByOption": "関数",
|
||||
|
@ -15990,7 +15989,6 @@
|
|||
"xpack.ml.timeSeriesExplorer.runControls.runNewForecastTitle": "新規予測の実行",
|
||||
"xpack.ml.timeSeriesExplorer.selectFieldMessage": "{fieldName}を選択してください",
|
||||
"xpack.ml.timeSeriesExplorer.setManualInputHelperText": "一致する値がありません",
|
||||
"xpack.ml.timeSeriesExplorer.severityThresholdLabel": "深刻度のしきい値",
|
||||
"xpack.ml.timeSeriesExplorer.showForecastLabel": "予測を表示",
|
||||
"xpack.ml.timeSeriesExplorer.showModelBoundsLabel": "モデルバウンドを表示",
|
||||
"xpack.ml.timeSeriesExplorer.singleTimeSeriesAnalysisTitle": "{functionLabel} の単独時系列分析",
|
||||
|
|
|
@ -16195,7 +16195,6 @@
|
|||
"xpack.ml.timeSeriesExplorer.forecastsList.viewForecastAriaLabel": "查看在 {createdDate} 创建的预测",
|
||||
"xpack.ml.timeSeriesExplorer.highestAnomalyScoreErrorToastTitle": "在获取异常分数最高的记录时出错",
|
||||
"xpack.ml.timeSeriesExplorer.ignoreTimeRangeInfo": "该列表包含在作业生命周期内创建的所有异常的值。",
|
||||
"xpack.ml.timeSeriesExplorer.intervalLabel": "时间间隔",
|
||||
"xpack.ml.timeSeriesExplorer.invalidTimeRangeInUrlCallout": "由于默认时间筛选无效,时间筛选已更改为此作业的完整范围。检查 {field} 的高级设置。",
|
||||
"xpack.ml.timeSeriesExplorer.loadingLabel": "正在加载",
|
||||
"xpack.ml.timeSeriesExplorer.metricPlotByOption": "函数",
|
||||
|
@ -16218,7 +16217,6 @@
|
|||
"xpack.ml.timeSeriesExplorer.runControls.runNewForecastTitle": "运行新的预测",
|
||||
"xpack.ml.timeSeriesExplorer.selectFieldMessage": "选择 {fieldName}",
|
||||
"xpack.ml.timeSeriesExplorer.setManualInputHelperText": "无匹配值",
|
||||
"xpack.ml.timeSeriesExplorer.severityThresholdLabel": "严重性阈值",
|
||||
"xpack.ml.timeSeriesExplorer.showForecastLabel": "显示预测",
|
||||
"xpack.ml.timeSeriesExplorer.showModelBoundsLabel": "显示模型边界",
|
||||
"xpack.ml.timeSeriesExplorer.singleMetricRequiredMessage": "要查看单个指标,请选择 {missingValuesCount, plural, one {{fieldName1} 的值} other {{fieldName1} 和 {fieldName2} 的值}}。",
|
||||
|
|
Loading…
Reference in a new issue