[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:
Melissa Alvarez 2021-06-22 16:58:18 -04:00 committed by GitHub
parent dec77cfafb
commit b161bf03be
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 308 additions and 201 deletions

View file

@ -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 {

View file

@ -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>

View file

@ -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',

View file

@ -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',

View file

@ -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;
};

View file

@ -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',

View file

@ -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>

View file

@ -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,

View file

@ -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;
}