[ML] Anomaly Detection: Visualize delayed - data Part 2 (#102270)
* add link in datafeed tab.remove interval * add annotation overlay to chart * adds annotations checkbox * ensure annotation with same start/end time show up in chart * update annotations time format * move time format to client * adds info tooltip to modal title * adds model snapshots to datafeed chart
This commit is contained in:
parent
dec77cfafb
commit
b161bf03be
9 changed files with 308 additions and 201 deletions
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import { estypes } from '@elastic/elasticsearch';
|
||||
import { LineAnnotationDatum, RectAnnotationDatum } from '@elastic/charts';
|
||||
|
||||
export interface GetStoppedPartitionResult {
|
||||
jobs: string[] | Record<string, string[]>;
|
||||
|
@ -13,6 +14,9 @@ export interface GetStoppedPartitionResult {
|
|||
export interface GetDatafeedResultsChartDataResult {
|
||||
bucketResults: number[][];
|
||||
datafeedResults: number[][];
|
||||
annotationResultsRect: RectAnnotationDatum[];
|
||||
annotationResultsLine: LineAnnotationDatum[];
|
||||
modelSnapshotResultsLine: LineAnnotationDatum[];
|
||||
}
|
||||
|
||||
export interface DatafeedResultsChartDataParams {
|
||||
|
|
|
@ -494,13 +494,13 @@ class AnnotationsTableUI extends Component {
|
|||
render: (annotation) => {
|
||||
const viewDataFeedText = (
|
||||
<FormattedMessage
|
||||
id="xpack.ml.annotationsTable.viewDatafeedTooltip"
|
||||
defaultMessage="View datafeed"
|
||||
id="xpack.ml.annotationsTable.datafeedChartTooltip"
|
||||
defaultMessage="Datafeed chart"
|
||||
/>
|
||||
);
|
||||
const viewDataFeedTooltipAriaLabelText = i18n.translate(
|
||||
'xpack.ml.annotationsTable.viewDatafeedTooltipAriaLabel',
|
||||
{ defaultMessage: 'View datafeed' }
|
||||
'xpack.ml.annotationsTable.datafeedChartTooltipAriaLabel',
|
||||
{ defaultMessage: 'Datafeed chart' }
|
||||
);
|
||||
return (
|
||||
<EuiButtonEmpty
|
||||
|
@ -735,9 +735,7 @@ class AnnotationsTableUI extends Component {
|
|||
});
|
||||
}}
|
||||
end={this.state.datafeedEnd}
|
||||
timefield={this.props.jobs[0].data_description.time_field}
|
||||
jobId={this.state.jobId}
|
||||
bucketSpan={this.props.jobs[0].analysis_config.bucket_span}
|
||||
/>
|
||||
) : null}
|
||||
</Fragment>
|
||||
|
|
|
@ -15,7 +15,7 @@ export const CHART_DIRECTION = {
|
|||
export type ChartDirectionType = typeof CHART_DIRECTION[keyof typeof CHART_DIRECTION];
|
||||
|
||||
// [width, height]
|
||||
export const CHART_SIZE: ChartSizeArray = ['100%', 300];
|
||||
export const CHART_SIZE: ChartSizeArray = ['100%', 380];
|
||||
|
||||
export const TAB_IDS = {
|
||||
CHART: 'chart',
|
||||
|
|
|
@ -11,25 +11,35 @@ import { i18n } from '@kbn/i18n';
|
|||
import moment from 'moment';
|
||||
import {
|
||||
EuiButtonEmpty,
|
||||
EuiCheckbox,
|
||||
EuiDatePicker,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiIcon,
|
||||
EuiIconTip,
|
||||
EuiLoadingChart,
|
||||
EuiModal,
|
||||
EuiModalHeader,
|
||||
EuiModalBody,
|
||||
EuiSelect,
|
||||
EuiSpacer,
|
||||
EuiTabs,
|
||||
EuiTab,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
EuiToolTip,
|
||||
htmlIdGenerator,
|
||||
} from '@elastic/eui';
|
||||
import {
|
||||
AnnotationDomainType,
|
||||
Axis,
|
||||
Chart,
|
||||
CurveType,
|
||||
LineAnnotation,
|
||||
LineSeries,
|
||||
LineAnnotationDatum,
|
||||
Position,
|
||||
RectAnnotation,
|
||||
RectAnnotationDatum,
|
||||
ScaleType,
|
||||
Settings,
|
||||
timeFormatter,
|
||||
|
@ -42,7 +52,6 @@ import { useMlApiContext } from '../../../../contexts/kibana';
|
|||
import { useCurrentEuiTheme } from '../../../../components/color_range_legend';
|
||||
import { JobMessagesPane } from '../job_details/job_messages_pane';
|
||||
import { EditQueryDelay } from './edit_query_delay';
|
||||
import { getIntervalOptions } from './get_interval_options';
|
||||
import {
|
||||
CHART_DIRECTION,
|
||||
ChartDirectionType,
|
||||
|
@ -53,12 +62,18 @@ import {
|
|||
} from './constants';
|
||||
import { loadFullJob } from '../utils';
|
||||
|
||||
const dateFormatter = timeFormatter('MM-DD HH:mm');
|
||||
const dateFormatter = timeFormatter('MM-DD HH:mm:ss');
|
||||
const MAX_CHART_POINTS = 480;
|
||||
|
||||
interface DatafeedModalProps {
|
||||
jobId: string;
|
||||
end: number;
|
||||
onClose: (deletionApproved?: boolean) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
function setLineAnnotationHeader(lineDatum: LineAnnotationDatum) {
|
||||
lineDatum.header = dateFormatter(lineDatum.dataValue);
|
||||
return lineDatum;
|
||||
}
|
||||
|
||||
export const DatafeedModal: FC<DatafeedModalProps> = ({ jobId, end, onClose }) => {
|
||||
|
@ -68,11 +83,17 @@ export const DatafeedModal: FC<DatafeedModalProps> = ({ jobId, end, onClose }) =
|
|||
isInitialized: boolean;
|
||||
}>({ datafeedConfig: undefined, bucketSpan: undefined, isInitialized: false });
|
||||
const [endDate, setEndDate] = useState<any>(moment(end));
|
||||
const [interval, setInterval] = useState<string | undefined>();
|
||||
const [selectedTabId, setSelectedTabId] = useState<TabIdsType>(TAB_IDS.CHART);
|
||||
const [isLoadingChartData, setIsLoadingChartData] = useState<boolean>(false);
|
||||
const [bucketData, setBucketData] = useState<number[][]>([]);
|
||||
const [annotationData, setAnnotationData] = useState<{
|
||||
rect: RectAnnotationDatum[];
|
||||
line: LineAnnotationDatum[];
|
||||
}>({ rect: [], line: [] });
|
||||
const [modelSnapshotData, setModelSnapshotData] = useState<LineAnnotationDatum[]>([]);
|
||||
const [sourceData, setSourceData] = useState<number[][]>([]);
|
||||
const [showAnnotations, setShowAnnotations] = useState<boolean>(true);
|
||||
const [showModelSnapshots, setShowModelSnapshots] = useState<boolean>(true);
|
||||
|
||||
const {
|
||||
results: { getDatafeedResultChartData },
|
||||
|
@ -102,25 +123,30 @@ export const DatafeedModal: FC<DatafeedModalProps> = ({ jobId, end, onClose }) =
|
|||
const handleChange = (date: moment.Moment) => setEndDate(date);
|
||||
|
||||
const handleEndDateChange = (direction: ChartDirectionType) => {
|
||||
if (interval === undefined) return;
|
||||
if (data.bucketSpan === undefined) return;
|
||||
|
||||
const newEndDate = endDate.clone();
|
||||
const [count, type] = interval.split(' ');
|
||||
const unitMatch = data.bucketSpan.match(/[d | h| m | s]/g)!;
|
||||
const unit = unitMatch[0];
|
||||
const count = Number(data.bucketSpan.replace(/[^0-9]/g, ''));
|
||||
|
||||
if (direction === CHART_DIRECTION.FORWARD) {
|
||||
newEndDate.add(Number(count), type);
|
||||
newEndDate.add(MAX_CHART_POINTS * count, unit);
|
||||
} else {
|
||||
newEndDate.subtract(Number(count), type);
|
||||
newEndDate.subtract(MAX_CHART_POINTS * count, unit);
|
||||
}
|
||||
setEndDate(newEndDate);
|
||||
};
|
||||
|
||||
const getChartData = useCallback(async () => {
|
||||
if (interval === undefined) return;
|
||||
if (data.bucketSpan === undefined) return;
|
||||
|
||||
const endTimestamp = moment(endDate).valueOf();
|
||||
const [count, type] = interval.split(' ');
|
||||
const startMoment = endDate.clone().subtract(Number(count), type);
|
||||
const unitMatch = data.bucketSpan.match(/[d | h| m | s]/g)!;
|
||||
const unit = unitMatch[0];
|
||||
const count = Number(data.bucketSpan.replace(/[^0-9]/g, ''));
|
||||
// STARTTIME = ENDTIME - (BucketSpan * MAX_CHART_POINTS)
|
||||
const startMoment = endDate.clone().subtract(MAX_CHART_POINTS * count, unit);
|
||||
const startTimestamp = moment(startMoment).valueOf();
|
||||
|
||||
try {
|
||||
|
@ -128,6 +154,11 @@ export const DatafeedModal: FC<DatafeedModalProps> = ({ jobId, end, onClose }) =
|
|||
|
||||
setSourceData(chartData.datafeedResults);
|
||||
setBucketData(chartData.bucketResults);
|
||||
setAnnotationData({
|
||||
rect: chartData.annotationResultsRect,
|
||||
line: chartData.annotationResultsLine.map(setLineAnnotationHeader),
|
||||
});
|
||||
setModelSnapshotData(chartData.modelSnapshotResultsLine.map(setLineAnnotationHeader));
|
||||
} catch (error) {
|
||||
const title = i18n.translate('xpack.ml.jobsList.datafeedModal.errorToastTitle', {
|
||||
defaultMessage: 'Error fetching data',
|
||||
|
@ -135,7 +166,7 @@ export const DatafeedModal: FC<DatafeedModalProps> = ({ jobId, end, onClose }) =
|
|||
displayErrorToast(error, title);
|
||||
}
|
||||
setIsLoadingChartData(false);
|
||||
}, [endDate, interval]);
|
||||
}, [endDate, data.bucketSpan]);
|
||||
|
||||
const getJobData = async () => {
|
||||
try {
|
||||
|
@ -145,11 +176,6 @@ export const DatafeedModal: FC<DatafeedModalProps> = ({ jobId, end, onClose }) =
|
|||
bucketSpan: job.analysis_config.bucket_span,
|
||||
isInitialized: true,
|
||||
});
|
||||
const intervalOptions = getIntervalOptions(job.analysis_config.bucket_span);
|
||||
const initialInterval = intervalOptions.length
|
||||
? intervalOptions[intervalOptions.length - 1]
|
||||
: undefined;
|
||||
setInterval(initialInterval?.value || '72 hours');
|
||||
} catch (error) {
|
||||
displayErrorToast(error);
|
||||
}
|
||||
|
@ -161,20 +187,17 @@ export const DatafeedModal: FC<DatafeedModalProps> = ({ jobId, end, onClose }) =
|
|||
|
||||
useEffect(
|
||||
function loadChartData() {
|
||||
if (interval !== undefined) {
|
||||
if (data.bucketSpan !== undefined) {
|
||||
setIsLoadingChartData(true);
|
||||
getChartData();
|
||||
}
|
||||
},
|
||||
[endDate, interval]
|
||||
[endDate, data.bucketSpan]
|
||||
);
|
||||
|
||||
const { datafeedConfig, bucketSpan, isInitialized } = data;
|
||||
|
||||
const intervalOptions = useMemo(() => {
|
||||
if (bucketSpan === undefined) return [];
|
||||
return getIntervalOptions(bucketSpan);
|
||||
}, [bucketSpan]);
|
||||
const checkboxIdAnnotation = useMemo(() => htmlIdGenerator()(), []);
|
||||
const checkboxIdModelSnapshot = useMemo(() => htmlIdGenerator()(), []);
|
||||
|
||||
return (
|
||||
<EuiModal
|
||||
|
@ -185,13 +208,33 @@ export const DatafeedModal: FC<DatafeedModalProps> = ({ jobId, end, onClose }) =
|
|||
<EuiModalHeader>
|
||||
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween" gutterSize="xl">
|
||||
<EuiFlexItem grow={false}>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.jobsList.datafeedModal.header"
|
||||
defaultMessage="{jobId}"
|
||||
values={{
|
||||
jobId,
|
||||
}}
|
||||
/>
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIconTip
|
||||
color="primary"
|
||||
type="help"
|
||||
content={
|
||||
<FormattedMessage
|
||||
id="xpack.ml.jobsList.datafeedModal.headerTooltipContent"
|
||||
defaultMessage="Charts the event counts of the job and the source data to identify where missing data has occurred."
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle size="xs">
|
||||
<h4>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.jobsList.datafeedModal.header"
|
||||
defaultMessage="Datafeed chart for {jobId}"
|
||||
values={{
|
||||
jobId,
|
||||
}}
|
||||
/>
|
||||
</h4>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiDatePicker
|
||||
|
@ -219,19 +262,6 @@ export const DatafeedModal: FC<DatafeedModalProps> = ({ jobId, end, onClose }) =
|
|||
<EuiFlexGroup direction="column">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiSelect
|
||||
options={intervalOptions}
|
||||
value={interval}
|
||||
onChange={(e) => setInterval(e.target.value)}
|
||||
aria-label={i18n.translate(
|
||||
'xpack.ml.jobsList.datafeedModal.intervalSelection',
|
||||
{
|
||||
defaultMessage: 'Datafeed modal chart interval selection',
|
||||
}
|
||||
)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EditQueryDelay
|
||||
datafeedId={datafeedConfig.datafeed_id}
|
||||
|
@ -239,6 +269,40 @@ export const DatafeedModal: FC<DatafeedModalProps> = ({ jobId, end, onClose }) =
|
|||
isEnabled={datafeedConfig.state === DATAFEED_STATE.STOPPED}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiCheckbox
|
||||
id={checkboxIdAnnotation}
|
||||
label={
|
||||
<EuiText size={'xs'}>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.jobsList.datafeedModal.showAnnotationsCheckboxLabel"
|
||||
defaultMessage="Show annotations"
|
||||
/>
|
||||
</EuiText>
|
||||
}
|
||||
checked={showAnnotations}
|
||||
onChange={() => setShowAnnotations(!showAnnotations)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiCheckbox
|
||||
id={checkboxIdModelSnapshot}
|
||||
label={
|
||||
<EuiText size={'xs'}>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.jobsList.datafeedModal.showModelSnapshotsCheckboxLabel"
|
||||
defaultMessage="Show model snapshots"
|
||||
/>
|
||||
</EuiText>
|
||||
}
|
||||
checked={showModelSnapshots}
|
||||
onChange={() => setShowModelSnapshots(!showModelSnapshots)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
|
@ -298,7 +362,65 @@ export const DatafeedModal: FC<DatafeedModalProps> = ({ jobId, end, onClose }) =
|
|||
})}
|
||||
position={Position.Left}
|
||||
/>
|
||||
{showModelSnapshots ? (
|
||||
<LineAnnotation
|
||||
id={i18n.translate(
|
||||
'xpack.ml.jobsList.datafeedModal.modelSnapshotsLineSeriesId',
|
||||
{
|
||||
defaultMessage: 'Model snapshots',
|
||||
}
|
||||
)}
|
||||
key="model-snapshots-results-line"
|
||||
domainType={AnnotationDomainType.XDomain}
|
||||
dataValues={modelSnapshotData}
|
||||
marker={<EuiIcon type="asterisk" />}
|
||||
markerPosition={Position.Top}
|
||||
style={{
|
||||
line: {
|
||||
strokeWidth: 3,
|
||||
stroke: euiTheme.euiColorVis1,
|
||||
opacity: 0.5,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{showAnnotations ? (
|
||||
<>
|
||||
<LineAnnotation
|
||||
id={i18n.translate(
|
||||
'xpack.ml.jobsList.datafeedModal.annotationLineSeriesId',
|
||||
{
|
||||
defaultMessage: 'Annotations line result',
|
||||
}
|
||||
)}
|
||||
key="annotation-results-line"
|
||||
domainType={AnnotationDomainType.XDomain}
|
||||
dataValues={annotationData.line}
|
||||
marker={<EuiIcon type="annotation" />}
|
||||
markerPosition={Position.Top}
|
||||
style={{
|
||||
line: {
|
||||
strokeWidth: 3,
|
||||
stroke: euiTheme.euiColorDangerText,
|
||||
opacity: 0.5,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<RectAnnotation
|
||||
key="annotation-results-rect"
|
||||
dataValues={annotationData.rect}
|
||||
id={i18n.translate(
|
||||
'xpack.ml.jobsList.datafeedModal.annotationRectSeriesId',
|
||||
{
|
||||
defaultMessage: 'Annotations rectangle result',
|
||||
}
|
||||
)}
|
||||
style={{ fill: euiTheme.euiColorDangerText }}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
<LineSeries
|
||||
key={'source-results'}
|
||||
color={euiTheme.euiColorPrimary}
|
||||
id={i18n.translate('xpack.ml.jobsList.datafeedModal.sourceSeriesId', {
|
||||
defaultMessage: 'Source indices',
|
||||
|
@ -311,6 +433,7 @@ export const DatafeedModal: FC<DatafeedModalProps> = ({ jobId, end, onClose }) =
|
|||
curve={CurveType.LINEAR}
|
||||
/>
|
||||
<LineSeries
|
||||
key={'job-results'}
|
||||
color={euiTheme.euiColorAccentText}
|
||||
id={i18n.translate('xpack.ml.jobsList.datafeedModal.bucketSeriesId', {
|
||||
defaultMessage: 'Job results',
|
||||
|
|
|
@ -1,118 +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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const getIntervalOptions = (bucketSpan: string) => {
|
||||
const unitMatch = bucketSpan.match(/[d | h| m | s]/g)!;
|
||||
const unit = unitMatch[0];
|
||||
const count = Number(bucketSpan.replace(/[^0-9]/g, ''));
|
||||
|
||||
const intervalOptions = [];
|
||||
|
||||
if (['s', 'ms', 'micros', 'nanos'].includes(unit)) {
|
||||
intervalOptions.push(
|
||||
{
|
||||
value: '1 hour',
|
||||
text: i18n.translate('xpack.ml.jobsList.datafeedModal.1hourOption', {
|
||||
defaultMessage: '{count} hour',
|
||||
values: { count: 1 },
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: '2 hours',
|
||||
text: i18n.translate('xpack.ml.jobsList.datafeedModal.2hourOption', {
|
||||
defaultMessage: '{count} hours',
|
||||
values: { count: 2 },
|
||||
}),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if ((unit === 'm' && count <= 4) || unit === 'h') {
|
||||
intervalOptions.push(
|
||||
{
|
||||
value: '3 hours',
|
||||
text: i18n.translate('xpack.ml.jobsList.datafeedModal.3hourOption', {
|
||||
defaultMessage: '{count} hours',
|
||||
values: { count: 3 },
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: '8 hours',
|
||||
text: i18n.translate('xpack.ml.jobsList.datafeedModal.8hourOption', {
|
||||
defaultMessage: '{count} hours',
|
||||
values: { count: 8 },
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: '12 hours',
|
||||
text: i18n.translate('xpack.ml.jobsList.datafeedModal.12hourOption', {
|
||||
defaultMessage: '{count} hours',
|
||||
values: { count: 12 },
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: '24 hours',
|
||||
text: i18n.translate('xpack.ml.jobsList.datafeedModal.24hourOption', {
|
||||
defaultMessage: '{count} hours',
|
||||
values: { count: 24 },
|
||||
}),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if ((unit === 'm' && count >= 5 && count <= 15) || unit === 'h') {
|
||||
intervalOptions.push(
|
||||
{
|
||||
value: '48 hours',
|
||||
text: i18n.translate('xpack.ml.jobsList.datafeedModal.48hourOption', {
|
||||
defaultMessage: '{count} hours',
|
||||
values: { count: 48 },
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: '72 hours',
|
||||
text: i18n.translate('xpack.ml.jobsList.datafeedModal.72hourOption', {
|
||||
defaultMessage: '{count} hours',
|
||||
values: { count: 72 },
|
||||
}),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if ((unit === 'm' && count >= 10 && count <= 15) || unit === 'h' || unit === 'd') {
|
||||
intervalOptions.push(
|
||||
{
|
||||
value: '5 days',
|
||||
text: i18n.translate('xpack.ml.jobsList.datafeedModal.5daysOption', {
|
||||
defaultMessage: '{count} days',
|
||||
values: { count: 5 },
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: '7 days',
|
||||
text: i18n.translate('xpack.ml.jobsList.datafeedModal.7daysOption', {
|
||||
defaultMessage: '{count} days',
|
||||
values: { count: 7 },
|
||||
}),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (unit === 'h' || unit === 'd') {
|
||||
intervalOptions.push({
|
||||
value: '14 days',
|
||||
text: i18n.translate('xpack.ml.jobsList.datafeedModal.14DaysOption', {
|
||||
defaultMessage: '{count} days',
|
||||
values: { count: 14 },
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
return intervalOptions;
|
||||
};
|
|
@ -7,26 +7,29 @@
|
|||
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component, Fragment } from 'react';
|
||||
|
||||
import { EuiTabbedContent, EuiLoadingSpinner } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiButtonIcon, EuiTabbedContent, EuiLoadingSpinner, EuiToolTip } from '@elastic/eui';
|
||||
|
||||
import { extractJobDetails } from './extract_job_details';
|
||||
import { JsonPane } from './json_tab';
|
||||
import { DatafeedPreviewPane } from './datafeed_preview_tab';
|
||||
import { AnnotationsTable } from '../../../../components/annotations/annotations_table';
|
||||
import { DatafeedModal } from '../datafeed_modal';
|
||||
import { AnnotationFlyout } from '../../../../components/annotations/annotation_flyout';
|
||||
import { ModelSnapshotTable } from '../../../../components/model_snapshots';
|
||||
import { ForecastsTable } from './forecasts_table';
|
||||
import { JobDetailsPane } from './job_details_pane';
|
||||
import { JobMessagesPane } from './job_messages_pane';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { withKibana } from '../../../../../../../../../src/plugins/kibana_react/public';
|
||||
|
||||
export class JobDetailsUI extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {};
|
||||
this.state = {
|
||||
datafeedModalVisible: false,
|
||||
};
|
||||
if (this.props.addYourself) {
|
||||
this.props.addYourself(props.jobId, (j) => this.updateJob(j));
|
||||
}
|
||||
|
@ -77,6 +80,30 @@ export class JobDetailsUI extends Component {
|
|||
alertRules,
|
||||
} = extractJobDetails(job, basePath, refreshJobList);
|
||||
|
||||
datafeed.titleAction = (
|
||||
<EuiToolTip
|
||||
content={
|
||||
<FormattedMessage
|
||||
id="xpack.ml.jobDetails.datafeedChartTooltipText"
|
||||
defaultMessage="Datafeed chart"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiButtonIcon
|
||||
size="xs"
|
||||
aria-label={i18n.translate('xpack.ml.jobDetails.datafeedChartAriaLabel', {
|
||||
defaultMessage: 'Datafeed chart',
|
||||
})}
|
||||
iconType="visAreaStacked"
|
||||
onClick={() =>
|
||||
this.setState({
|
||||
datafeedModalVisible: true,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
);
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
id: 'job-settings',
|
||||
|
@ -105,6 +132,32 @@ export class JobDetailsUI extends Component {
|
|||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'datafeed',
|
||||
'data-test-subj': 'mlJobListTab-datafeed',
|
||||
name: i18n.translate('xpack.ml.jobsList.jobDetails.tabs.datafeedLabel', {
|
||||
defaultMessage: 'Datafeed',
|
||||
}),
|
||||
content: (
|
||||
<>
|
||||
<JobDetailsPane
|
||||
data-test-subj="mlJobDetails-datafeed"
|
||||
sections={[datafeed, datafeedTimingStats]}
|
||||
/>
|
||||
{this.props.jobId && this.state.datafeedModalVisible ? (
|
||||
<DatafeedModal
|
||||
onClose={() => {
|
||||
this.setState({
|
||||
datafeedModalVisible: false,
|
||||
});
|
||||
}}
|
||||
end={job.data_counts.latest_bucket_timestamp}
|
||||
jobId={this.props.jobId}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'counts',
|
||||
'data-test-subj': 'mlJobListTab-counts',
|
||||
|
@ -137,21 +190,6 @@ export class JobDetailsUI extends Component {
|
|||
];
|
||||
|
||||
if (showFullDetails && datafeed.items.length) {
|
||||
// Datafeed should be at index 2 in tabs array for full details
|
||||
tabs.splice(2, 0, {
|
||||
id: 'datafeed',
|
||||
'data-test-subj': 'mlJobListTab-datafeed',
|
||||
name: i18n.translate('xpack.ml.jobsList.jobDetails.tabs.datafeedLabel', {
|
||||
defaultMessage: 'Datafeed',
|
||||
}),
|
||||
content: (
|
||||
<JobDetailsPane
|
||||
data-test-subj="mlJobDetails-datafeed"
|
||||
sections={[datafeed, datafeedTimingStats]}
|
||||
/>
|
||||
),
|
||||
});
|
||||
|
||||
tabs.push(
|
||||
{
|
||||
id: 'datafeed-preview',
|
||||
|
|
|
@ -9,6 +9,8 @@ import PropTypes from 'prop-types';
|
|||
import React, { Component } from 'react';
|
||||
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiTitle,
|
||||
EuiTable,
|
||||
EuiTableBody,
|
||||
|
@ -42,9 +44,14 @@ function Section({ section }) {
|
|||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<EuiTitle size="xs">
|
||||
<h4>{section.title}</h4>
|
||||
</EuiTitle>
|
||||
<EuiFlexGroup gutterSize="xs">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle size="xs">
|
||||
<h4>{section.title}</h4>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>{section.titleAction}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<div className="job-section" data-test-subj={`mlJobRowDetailsSection-${section.id}`}>
|
||||
<EuiTable compressed={true}>
|
||||
<EuiTableBody>
|
||||
|
|
|
@ -6,7 +6,10 @@
|
|||
*/
|
||||
|
||||
// Service for obtaining data for the ML Results dashboards.
|
||||
import { GetStoppedPartitionResult } from '../../../../common/types/results';
|
||||
import {
|
||||
GetStoppedPartitionResult,
|
||||
GetDatafeedResultsChartDataResult,
|
||||
} from '../../../../common/types/results';
|
||||
import { HttpService } from '../http_service';
|
||||
import { basePath } from './index';
|
||||
import { JobId } from '../../../../common/types/anomaly_detection_jobs';
|
||||
|
@ -148,7 +151,7 @@ export const resultsApiProvider = (httpService: HttpService) => ({
|
|||
start,
|
||||
end,
|
||||
});
|
||||
return httpService.http<any>({
|
||||
return httpService.http<GetDatafeedResultsChartDataResult>({
|
||||
path: `${basePath()}/results/datafeed_results_chart`,
|
||||
method: 'POST',
|
||||
body,
|
||||
|
|
|
@ -27,6 +27,7 @@ import {
|
|||
import { MlJobsResponse } from '../../../common/types/job_service';
|
||||
import type { MlClient } from '../../lib/ml_client';
|
||||
import { datafeedsProvider } from '../job_service/datafeeds';
|
||||
import { annotationServiceProvider } from '../annotation_service';
|
||||
|
||||
// Service for carrying out Elasticsearch queries to obtain data for the
|
||||
// ML Results dashboards.
|
||||
|
@ -620,13 +621,19 @@ export function resultsServiceProvider(mlClient: MlClient, client?: IScopedClust
|
|||
const finalResults: GetDatafeedResultsChartDataResult = {
|
||||
bucketResults: [],
|
||||
datafeedResults: [],
|
||||
annotationResultsRect: [],
|
||||
annotationResultsLine: [],
|
||||
modelSnapshotResultsLine: [],
|
||||
};
|
||||
|
||||
const { getDatafeedByJobId } = datafeedsProvider(client!, mlClient);
|
||||
const datafeedConfig = await getDatafeedByJobId(jobId);
|
||||
|
||||
const { body: jobsResponse } = await mlClient.getJobs({ job_id: jobId });
|
||||
if (jobsResponse.count === 0 || jobsResponse.jobs === undefined) {
|
||||
const [datafeedConfig, { body: jobsResponse }] = await Promise.all([
|
||||
getDatafeedByJobId(jobId),
|
||||
mlClient.getJobs({ job_id: jobId }),
|
||||
]);
|
||||
|
||||
if (jobsResponse && (jobsResponse.count === 0 || jobsResponse.jobs === undefined)) {
|
||||
throw Boom.notFound(`Job with the id "${jobId}" not found`);
|
||||
}
|
||||
|
||||
|
@ -696,10 +703,25 @@ export function resultsServiceProvider(mlClient: MlClient, client?: IScopedClust
|
|||
]) || [];
|
||||
}
|
||||
|
||||
const bucketResp = await mlClient.getBuckets({
|
||||
job_id: jobId,
|
||||
body: { desc: true, start: String(start), end: String(end), page: { from: 0, size: 1000 } },
|
||||
});
|
||||
const { getAnnotations } = annotationServiceProvider(client!);
|
||||
|
||||
const [bucketResp, annotationResp, { body: modelSnapshotsResp }] = await Promise.all([
|
||||
mlClient.getBuckets({
|
||||
job_id: jobId,
|
||||
body: { desc: true, start: String(start), end: String(end), page: { from: 0, size: 1000 } },
|
||||
}),
|
||||
getAnnotations({
|
||||
jobIds: [jobId],
|
||||
earliestMs: start,
|
||||
latestMs: end,
|
||||
maxAnnotations: 1000,
|
||||
}),
|
||||
mlClient.getModelSnapshots({
|
||||
job_id: jobId,
|
||||
start: String(start),
|
||||
end: String(end),
|
||||
}),
|
||||
]);
|
||||
|
||||
const bucketResults = bucketResp?.body?.buckets ?? [];
|
||||
bucketResults.forEach((dataForTime) => {
|
||||
|
@ -708,6 +730,36 @@ export function resultsServiceProvider(mlClient: MlClient, client?: IScopedClust
|
|||
finalResults.bucketResults.push([timestamp, eventCount]);
|
||||
});
|
||||
|
||||
const annotationResults = annotationResp.annotations[jobId] || [];
|
||||
annotationResults.forEach((annotation) => {
|
||||
const timestamp = Number(annotation?.timestamp);
|
||||
const endTimestamp = Number(annotation?.end_timestamp);
|
||||
if (timestamp === endTimestamp) {
|
||||
finalResults.annotationResultsLine.push({
|
||||
dataValue: timestamp,
|
||||
details: annotation.annotation,
|
||||
});
|
||||
} else {
|
||||
finalResults.annotationResultsRect.push({
|
||||
coordinates: {
|
||||
x0: timestamp,
|
||||
x1: endTimestamp,
|
||||
},
|
||||
details: annotation.annotation,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const modelSnapshots = modelSnapshotsResp?.model_snapshots ?? [];
|
||||
modelSnapshots.forEach((modelSnapshot) => {
|
||||
const timestamp = Number(modelSnapshot?.timestamp);
|
||||
|
||||
finalResults.modelSnapshotResultsLine.push({
|
||||
dataValue: timestamp,
|
||||
details: modelSnapshot.description,
|
||||
});
|
||||
});
|
||||
|
||||
return finalResults;
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue