mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[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:
parent
505582037d
commit
f6dd17c93e
5 changed files with 96 additions and 54 deletions
|
@ -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>
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
};
|
||||
|
|
|
@ -109,7 +109,7 @@
|
|||
<ml-annotation-table
|
||||
annotations="annotationsData"
|
||||
drill-down="true"
|
||||
jobs="selectedJobs"
|
||||
number-badge="false"
|
||||
/>
|
||||
|
||||
<br /><br />
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue