[ML] Annotations Table in Anomaly Explorer. (#27312)

- The annotations table has been updated to support multiple jobs.If annotations from multiple jobs are shown, a column with job ID will dynamically be added to the annotation table.
- The code for openSingleMetricView() to drill down from the annotations table has been updated to work both from the jobs list and Anomaly Explorer.
- The wrapper angularjs directive for the annotations table now has support for a scope attribute to show/hide the column with A/B/C... labels.
This commit is contained in:
Walter Rafelsberger 2018-12-18 15:20:59 +01:00 committed by GitHub
parent 505582037d
commit f6dd17c93e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 96 additions and 54 deletions

View file

@ -10,6 +10,7 @@
* getting the annotations via props (used in Anomaly Explorer and Single Series Viewer).
*/
import _ from 'lodash';
import PropTypes from 'prop-types';
import rison from 'rison-node';
@ -38,8 +39,11 @@ import chrome from 'ui/chrome';
import { addItemToRecentlyAccessed } from '../../util/recently_accessed';
import { ml } from '../../services/ml_api_service';
import { mlJobService } from '../../services/job_service';
import { mlTableService } from '../../services/table_service';
import { ANNOTATIONS_TABLE_DEFAULT_QUERY_SIZE } from '../../../common/constants/search';
import { isTimeSeriesViewJob } from '../../../common/util/job_utils';
const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss';
@ -51,7 +55,10 @@ class AnnotationsTable extends Component {
super(props);
this.state = {
annotations: [],
isLoading: false
isLoading: false,
// Need to do a detailed check here because the angular wrapper could pass on something like `[undefined]`.
jobId: (Array.isArray(this.props.jobs) && this.props.jobs.length > 0 && this.props.jobs[0] !== undefined)
? this.props.jobs[0].job_id : undefined,
};
}
@ -78,7 +85,7 @@ class AnnotationsTable extends Component {
jobId: props.jobs[0].job_id
}));
}).catch((resp) => {
console.log('Error loading list of annoations for jobs list:', resp);
console.log('Error loading list of annotations for jobs list:', resp);
this.setState({
annotations: [],
errorMessage: 'Error loading the list of annotations for this job',
@ -89,6 +96,18 @@ class AnnotationsTable extends Component {
}
}
getJob(jobId) {
// check if the job was supplied via props and matches the supplied jobId
if (Array.isArray(this.props.jobs) && this.props.jobs.length > 0) {
const job = this.props.jobs[0];
if (jobId === undefined || job.job_id === jobId) {
return job;
}
}
return mlJobService.getJob(jobId);
}
componentDidMount() {
if (this.props.annotations === undefined) {
this.getAnnotations();
@ -105,16 +124,17 @@ class AnnotationsTable extends Component {
}
}
openSingleMetricView(annotation) {
openSingleMetricView = (annotation = {}) => {
// Creates the link to the Single Metric Viewer.
// Set the total time range from the start to the end of the annotation,
const dataCounts = this.props.jobs[0].data_counts;
// Set the total time range from the start to the end of the annotation.
const job = this.getJob(annotation.job_id);
const dataCounts = job.data_counts;
const from = new Date(dataCounts.earliest_record_timestamp).toISOString();
const to = new Date(dataCounts.latest_record_timestamp).toISOString();
const globalSettings = {
ml: {
jobIds: [this.props.jobs[0].job_id]
jobIds: [job.job_id]
},
refreshInterval: {
display: 'Off',
@ -138,7 +158,7 @@ class AnnotationsTable extends Component {
}
};
if (annotation !== undefined) {
if (annotation.timestamp !== undefined && annotation.end_timestamp !== undefined) {
appState.mlTimeSeriesExplorer = {
zoom: {
from: new Date(annotation.timestamp).toISOString(),
@ -159,7 +179,7 @@ class AnnotationsTable extends Component {
const _a = rison.encode(appState);
const url = `?_g=${_g}&_a=${_a}`;
addItemToRecentlyAccessed('timeseriesexplorer', this.props.jobs[0].job_id, url);
addItemToRecentlyAccessed('timeseriesexplorer', job.job_id, url);
window.open(`${chrome.getBasePath()}/app/ml#/timeseriesexplorer${url}`, '_self');
}
@ -221,10 +241,12 @@ class AnnotationsTable extends Component {
title="No annotations created for this job"
iconType="iInCircle"
>
<p>
To create an annotation,
open the <EuiLink onClick={this.openSingleMetricView}>Single Metric Viewer</EuiLink>
</p>
{this.state.jobId && isTimeSeriesViewJob(this.getJob(this.state.jobId)) &&
<p>
To create an annotation,
open the <EuiLink onClick={() => this.openSingleMetricView()}>Single Metric Viewer</EuiLink>
</p>
}
</EuiCallOut>
);
}
@ -277,6 +299,15 @@ class AnnotationsTable extends Component {
},
];
const jobIds = _.uniq(annotations.map(a => a.job_id));
if (jobIds.length > 1) {
columns.unshift({
field: 'job_id',
name: 'job ID',
sortable: true,
});
}
if (isNumberBadgeVisible) {
columns.unshift({
field: 'key',
@ -293,23 +324,30 @@ class AnnotationsTable extends Component {
}
if (isSingleMetricViewerLinkVisible) {
const openInSingleMetricViewerText = 'Open in Single Metric Viewer';
columns.push({
align: RIGHT_ALIGNMENT,
width: '60px',
name: 'View',
render: (annotation) => (
<EuiToolTip
position="bottom"
content={openInSingleMetricViewerText}
>
<EuiButtonIcon
onClick={() => this.openSingleMetricView(annotation)}
iconType="stats"
aria-label={openInSingleMetricViewerText}
/>
</EuiToolTip>
)
render: (annotation) => {
const isDrillDownAvailable = isTimeSeriesViewJob(this.getJob(annotation.job_id));
const openInSingleMetricViewerText = isDrillDownAvailable
? 'Open in Single Metric Viewer'
: 'Job configuration not supported in Single Metric Viewer';
return (
<EuiToolTip
position="bottom"
content={openInSingleMetricViewerText}
>
<EuiButtonIcon
onClick={() => this.openSingleMetricView(annotation)}
disabled={!isDrillDownAvailable}
iconType="stats"
aria-label={openInSingleMetricViewerText}
/>
</EuiToolTip>
);
}
});
}

View file

@ -27,7 +27,7 @@ module.directive('mlAnnotationTable', function () {
function link(scope, element) {
function renderReactComponent() {
if (typeof scope.jobs === 'undefined') {
if (typeof scope.jobs === 'undefined' && typeof scope.annotations === 'undefined') {
return;
}
@ -35,7 +35,7 @@ module.directive('mlAnnotationTable', function () {
annotations: scope.annotations,
jobs: scope.jobs,
isSingleMetricViewerLinkVisible: scope.drillDown,
isNumberBadgeVisible: true
isNumberBadgeVisible: scope.numberBadge
};
ReactDOM.render(
@ -69,7 +69,8 @@ module.directive('mlAnnotationTable', function () {
scope: {
annotations: '=',
drillDown: '=',
jobs: '='
jobs: '=',
numberBadge: '='
},
link: link
};

View file

@ -109,7 +109,7 @@
<ml-annotation-table
annotations="annotationsData"
drill-down="true"
jobs="selectedJobs"
number-badge="false"
/>
<br /><br />

View file

@ -55,9 +55,8 @@ import {
} from '../../common/constants/search';
// TODO Fully support Annotations in Anomaly Explorer
// import chrome from 'ui/chrome';
// const mlAnnotationsEnabled = chrome.getInjected('mlAnnotationsEnabled', false);
const mlAnnotationsEnabled = false;
import chrome from 'ui/chrome';
const mlAnnotationsEnabled = chrome.getInjected('mlAnnotationsEnabled', false);
uiRoutes
.when('/explorer/?', {
@ -960,7 +959,6 @@ module.controller('MlExplorerController', function (
cellData.lanes : $scope.getSelectedJobIds();
const timeRange = getSelectionTimeRange(cellData);
if (mlAnnotationsEnabled) {
const resp = await ml.annotations.getAnnotations({
jobIds,
@ -970,11 +968,13 @@ module.controller('MlExplorerController', function (
});
$scope.$evalAsync(() => {
const annotationsData = resp.annotations[jobIds[0]];
if (annotationsData === undefined) {
return;
}
const annotationsData = [];
jobIds.forEach((jobId) => {
const jobAnnotations = resp.annotations[jobId];
if (jobAnnotations !== undefined) {
annotationsData.push(...jobAnnotations);
}
});
$scope.annotationsData = annotationsData
.sort((a, b) => {

View file

@ -29,6 +29,7 @@ import { FORECAST_REQUEST_STATE } from 'plugins/ml/../common/constants/states';
import { addItemToRecentlyAccessed } from 'plugins/ml/util/recently_accessed';
import { mlForecastService } from 'plugins/ml/services/forecast_service';
import { FormattedMessage, injectI18n } from '@kbn/i18n/react';
import { isTimeSeriesViewJob } from '../../../../../../common/util/job_utils';
const MAX_FORECASTS = 500;
const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss';
@ -160,22 +161,24 @@ class ForecastsTableUI extends Component {
/>)}
iconType="iInCircle"
>
<p>
<FormattedMessage
id="xpack.ml.jobsList.jobDetails.forecastsTable.noForecastsDescription"
defaultMessage="To run a forecast, open the {singleMetricViewerLink}"
values={{
singleMetricViewerLink: (
<EuiLink onClick={() => this.openSingleMetricView()}>
<FormattedMessage
id="xpack.ml.jobsList.jobDetails.forecastsTable.noForecastsDescription.linkText"
defaultMessage="Single Metric Viewer"
/>
</EuiLink>
)
}}
/>
</p>
{isTimeSeriesViewJob(this.props.job) &&
<p>
<FormattedMessage
id="xpack.ml.jobsList.jobDetails.forecastsTable.noForecastsDescription"
defaultMessage="To run a forecast, open the {singleMetricViewerLink}"
values={{
singleMetricViewerLink: (
<EuiLink onClick={() => this.openSingleMetricView()}>
<FormattedMessage
id="xpack.ml.jobsList.jobDetails.forecastsTable.noForecastsDescription.linkText"
defaultMessage="Single Metric Viewer"
/>
</EuiLink>
)
}}
/>
</p>
}
</EuiCallOut>
);
}