mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 01:13:23 -04:00
[i18n] Translate ML - time series explorer (#28066)
* Translate timeseries explorer * Fix issues * Fix tslint errors * Fix tslint error * Fix test * Update tests * Fix tests * Update snapshot * Resolve issues from review comments * Resolve issues from review comments
This commit is contained in:
parent
b03e63b5de
commit
9b3a216f6f
18 changed files with 601 additions and 152 deletions
|
@ -36,7 +36,7 @@ describe('ML - <ml-timeseries-chart>', () => {
|
|||
document.getElementsByTagName('body')[0].append(mockClassedElement);
|
||||
|
||||
// spy the TimeseriesChart component's unmount method to be able to test if it was called
|
||||
const componentWillUnmountSpy = sinon.spy(TimeseriesChart.prototype, 'componentWillUnmount');
|
||||
const componentWillUnmountSpy = sinon.spy(TimeseriesChart.WrappedComponent.prototype, 'componentWillUnmount');
|
||||
|
||||
$element = $compile('<ml-timeseries-chart show-forecast="true" />')($scope);
|
||||
const scope = $element.isolateScope();
|
||||
|
|
|
@ -6,9 +6,9 @@
|
|||
|
||||
import mockAnnotations from '../../../components/annotations_table/__mocks__/mock_annotations.json';
|
||||
|
||||
import { shallow } from 'enzyme';
|
||||
import moment from 'moment-timezone';
|
||||
import React from 'react';
|
||||
import { shallowWithIntl } from 'test_utils/enzyme_helpers';
|
||||
|
||||
import { AnnotationDescriptionList } from './index';
|
||||
|
||||
|
@ -21,7 +21,12 @@ describe('AnnotationDescriptionList', () => {
|
|||
});
|
||||
|
||||
test('Initialization with annotation.', () => {
|
||||
const wrapper = shallow(<AnnotationDescriptionList annotation={mockAnnotations[0]} />);
|
||||
const wrapper = shallowWithIntl(
|
||||
<AnnotationDescriptionList.WrappedComponent
|
||||
annotation={mockAnnotations[0]}
|
||||
intl={null as any}
|
||||
/>
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -13,47 +13,70 @@ import React from 'react';
|
|||
|
||||
import { EuiDescriptionList } from '@elastic/eui';
|
||||
|
||||
import { InjectedIntl, injectI18n } from '@kbn/i18n/react';
|
||||
import { Annotation } from '../../../../common/types/annotations';
|
||||
import { formatHumanReadableDateTimeSeconds } from '../../../util/date_utils';
|
||||
|
||||
interface Props {
|
||||
annotation: Annotation;
|
||||
intl: InjectedIntl;
|
||||
}
|
||||
|
||||
export const AnnotationDescriptionList: React.SFC<Props> = ({ annotation }) => {
|
||||
export const AnnotationDescriptionList = injectI18n(({ annotation, intl }: Props) => {
|
||||
const listItems = [
|
||||
{
|
||||
title: 'Job ID',
|
||||
title: intl.formatMessage({
|
||||
id: 'xpack.ml.timeSeriesExplorer.annotationDescriptionList.jobIdTitle',
|
||||
defaultMessage: 'Job ID',
|
||||
}),
|
||||
description: annotation.job_id,
|
||||
},
|
||||
{
|
||||
title: 'Start',
|
||||
title: intl.formatMessage({
|
||||
id: 'xpack.ml.timeSeriesExplorer.annotationDescriptionList.startTitle',
|
||||
defaultMessage: 'Start',
|
||||
}),
|
||||
description: formatHumanReadableDateTimeSeconds(annotation.timestamp),
|
||||
},
|
||||
];
|
||||
|
||||
if (annotation.end_timestamp !== undefined) {
|
||||
listItems.push({
|
||||
title: 'End',
|
||||
title: intl.formatMessage({
|
||||
id: 'xpack.ml.timeSeriesExplorer.annotationDescriptionList.endTitle',
|
||||
defaultMessage: 'End',
|
||||
}),
|
||||
description: formatHumanReadableDateTimeSeconds(annotation.end_timestamp),
|
||||
});
|
||||
}
|
||||
|
||||
if (annotation.create_time !== undefined && annotation.modified_time !== undefined) {
|
||||
listItems.push({
|
||||
title: 'Created',
|
||||
title: intl.formatMessage({
|
||||
id: 'xpack.ml.timeSeriesExplorer.annotationDescriptionList.createdTitle',
|
||||
defaultMessage: 'Created',
|
||||
}),
|
||||
description: formatHumanReadableDateTimeSeconds(annotation.create_time),
|
||||
});
|
||||
listItems.push({
|
||||
title: 'Created by',
|
||||
title: intl.formatMessage({
|
||||
id: 'xpack.ml.timeSeriesExplorer.annotationDescriptionList.createdByTitle',
|
||||
defaultMessage: 'Created by',
|
||||
}),
|
||||
description: annotation.create_username,
|
||||
});
|
||||
listItems.push({
|
||||
title: 'Last modified',
|
||||
title: intl.formatMessage({
|
||||
id: 'xpack.ml.timeSeriesExplorer.annotationDescriptionList.lastModifiedTitle',
|
||||
defaultMessage: 'Last modified',
|
||||
}),
|
||||
description: formatHumanReadableDateTimeSeconds(annotation.modified_time),
|
||||
});
|
||||
listItems.push({
|
||||
title: 'Modified by',
|
||||
title: intl.formatMessage({
|
||||
id: 'xpack.ml.timeSeriesExplorer.annotationDescriptionList.modifiedByTitle',
|
||||
defaultMessage: 'Modified by',
|
||||
}),
|
||||
description: annotation.modified_username,
|
||||
});
|
||||
}
|
||||
|
@ -65,4 +88,4 @@ export const AnnotationDescriptionList: React.SFC<Props> = ({ annotation }) => {
|
|||
listItems={listItems}
|
||||
/>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
|
|
@ -20,13 +20,16 @@ exports[`AnnotationFlyout Initialization. 1`] = `
|
|||
<h2
|
||||
id="mlAnnotationFlyoutTitle"
|
||||
>
|
||||
Edit
|
||||
annotation
|
||||
<FormattedMessage
|
||||
defaultMessage="Edit annotation"
|
||||
id="xpack.ml.timeSeriesExplorer.annotationFlyout.editAnnotationTitle"
|
||||
values={Object {}}
|
||||
/>
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
<Component
|
||||
<InjectIntl(Component)
|
||||
annotation={
|
||||
Object {
|
||||
"_id": "KCCkDWgB_ZdQ1MFDSYPi",
|
||||
|
@ -49,7 +52,13 @@ exports[`AnnotationFlyout Initialization. 1`] = `
|
|||
describedByIds={Array []}
|
||||
fullWidth={true}
|
||||
hasEmptyLabelSpace={false}
|
||||
label="Annotation text"
|
||||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Annotation text"
|
||||
id="xpack.ml.timeSeriesExplorer.annotationFlyout.annotationTextLabel"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiTextArea
|
||||
fullWidth={true}
|
||||
|
@ -83,7 +92,11 @@ exports[`AnnotationFlyout Initialization. 1`] = `
|
|||
onClick={[MockFunction]}
|
||||
type="button"
|
||||
>
|
||||
Cancel
|
||||
<FormattedMessage
|
||||
defaultMessage="Cancel"
|
||||
id="xpack.ml.timeSeriesExplorer.annotationFlyout.cancelButtonLabel"
|
||||
values={Object {}}
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
|
@ -96,7 +109,11 @@ exports[`AnnotationFlyout Initialization. 1`] = `
|
|||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Delete
|
||||
<FormattedMessage
|
||||
defaultMessage="Delete"
|
||||
id="xpack.ml.timeSeriesExplorer.annotationFlyout.deleteButtonLabel"
|
||||
values={Object {}}
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
|
@ -111,7 +128,11 @@ exports[`AnnotationFlyout Initialization. 1`] = `
|
|||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Update
|
||||
<FormattedMessage
|
||||
defaultMessage="Update"
|
||||
id="xpack.ml.timeSeriesExplorer.annotationFlyout.updateButtonLabel"
|
||||
values={Object {}}
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -6,8 +6,8 @@
|
|||
|
||||
import mockAnnotations from '../../../components/annotations_table/__mocks__/mock_annotations.json';
|
||||
|
||||
import { shallow } from 'enzyme';
|
||||
import React from 'react';
|
||||
import { shallowWithIntl } from 'test_utils/enzyme_helpers';
|
||||
|
||||
import { AnnotationFlyout } from './index';
|
||||
|
||||
|
@ -21,7 +21,7 @@ describe('AnnotationFlyout', () => {
|
|||
saveAction: jest.fn(),
|
||||
};
|
||||
|
||||
const wrapper = shallow(<AnnotationFlyout {...props} />);
|
||||
const wrapper = shallowWithIntl(<AnnotationFlyout {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -25,6 +25,8 @@ import { AnnotationDescriptionList } from '../annotation_description_list';
|
|||
|
||||
import { Annotation } from '../../../../common/types/annotations';
|
||||
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
interface Props {
|
||||
annotation: Annotation;
|
||||
cancelAction: () => {};
|
||||
|
@ -43,19 +45,37 @@ export const AnnotationFlyout: React.SFC<Props> = ({
|
|||
const saveActionWrapper = () => saveAction(annotation);
|
||||
const deleteActionWrapper = () => deleteAction(annotation);
|
||||
const isExistingAnnotation = typeof annotation._id !== 'undefined';
|
||||
const titlePrefix = isExistingAnnotation ? 'Edit' : 'Add';
|
||||
|
||||
return (
|
||||
<EuiFlyout onClose={cancelAction} size="s" aria-labelledby="Add annotation">
|
||||
<EuiFlyoutHeader hasBorder>
|
||||
<EuiTitle size="s">
|
||||
<h2 id="mlAnnotationFlyoutTitle">{titlePrefix} annotation</h2>
|
||||
<h2 id="mlAnnotationFlyoutTitle">
|
||||
{isExistingAnnotation ? (
|
||||
<FormattedMessage
|
||||
id="xpack.ml.timeSeriesExplorer.annotationFlyout.editAnnotationTitle"
|
||||
defaultMessage="Edit annotation"
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="xpack.ml.timeSeriesExplorer.annotationFlyout.addAnnotationTitle"
|
||||
defaultMessage="Add annotation"
|
||||
/>
|
||||
)}
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
<AnnotationDescriptionList annotation={annotation} />
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFormRow label="Annotation text" fullWidth>
|
||||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.ml.timeSeriesExplorer.annotationFlyout.annotationTextLabel"
|
||||
defaultMessage="Annotation text"
|
||||
/>
|
||||
}
|
||||
fullWidth
|
||||
>
|
||||
<EuiTextArea
|
||||
fullWidth
|
||||
isInvalid={annotation.annotation === ''}
|
||||
|
@ -69,19 +89,35 @@ export const AnnotationFlyout: React.SFC<Props> = ({
|
|||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty iconType="cross" onClick={cancelAction} flush="left">
|
||||
Cancel
|
||||
<FormattedMessage
|
||||
id="xpack.ml.timeSeriesExplorer.annotationFlyout.cancelButtonLabel"
|
||||
defaultMessage="Cancel"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
{isExistingAnnotation && (
|
||||
<EuiButtonEmpty color="danger" onClick={deleteActionWrapper}>
|
||||
Delete
|
||||
<FormattedMessage
|
||||
id="xpack.ml.timeSeriesExplorer.annotationFlyout.deleteButtonLabel"
|
||||
defaultMessage="Delete"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton fill isDisabled={annotation.annotation === ''} onClick={saveActionWrapper}>
|
||||
{isExistingAnnotation ? 'Update' : 'Create'}
|
||||
{isExistingAnnotation ? (
|
||||
<FormattedMessage
|
||||
id="xpack.ml.timeSeriesExplorer.annotationFlyout.updateButtonLabel"
|
||||
defaultMessage="Update"
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="xpack.ml.timeSeriesExplorer.annotationFlyout.createButtonLabel"
|
||||
defaultMessage="Create"
|
||||
/>
|
||||
)}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -14,6 +14,8 @@ import {
|
|||
EUI_MODAL_CONFIRM_BUTTON,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
export function DeleteAnnotationModal({
|
||||
cancelAction,
|
||||
deleteAction,
|
||||
|
@ -24,11 +26,20 @@ export function DeleteAnnotationModal({
|
|||
{isVisible === true &&
|
||||
<EuiOverlayMask>
|
||||
<EuiConfirmModal
|
||||
title="Delete this annotation?"
|
||||
title={<FormattedMessage
|
||||
id="xpack.ml.timeSeriesExplorer.deleteAnnotationModal.deleteAnnotationTitle"
|
||||
defaultMessage="Delete this annotation?"
|
||||
/>}
|
||||
onCancel={cancelAction}
|
||||
onConfirm={deleteAction}
|
||||
cancelButtonText="Cancel"
|
||||
confirmButtonText="Delete"
|
||||
cancelButtonText={<FormattedMessage
|
||||
id="xpack.ml.timeSeriesExplorer.deleteAnnotationModal.cancelButtonLabel"
|
||||
defaultMessage="Cancel"
|
||||
/>}
|
||||
confirmButtonText={<FormattedMessage
|
||||
id="xpack.ml.timeSeriesExplorer.deleteAnnotationModal.deleteButtonLabel"
|
||||
defaultMessage="Delete"
|
||||
/>}
|
||||
buttonColor="danger"
|
||||
defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON}
|
||||
className="eui-textBreakWord"
|
||||
|
|
|
@ -25,6 +25,7 @@ import {
|
|||
|
||||
import { ProgressIcon } from './progress_icon';
|
||||
import { PROGRESS_STATES } from './progress_states';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
|
||||
export function ForecastProgress({
|
||||
|
@ -38,7 +39,12 @@ export function ForecastProgress({
|
|||
<EuiFlexGroup gutterSize="s" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="xs">
|
||||
<h3>Opening job...</h3>
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.timeSeriesExplorer.forecastingModal.openingJobTitle"
|
||||
defaultMessage="Opening job…"
|
||||
/>
|
||||
</h3>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
|
@ -53,7 +59,12 @@ export function ForecastProgress({
|
|||
<EuiFlexGroup gutterSize="s" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="xs">
|
||||
<h3>Running forecast...</h3>
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.timeSeriesExplorer.forecastingModal.runningForecastTitle"
|
||||
defaultMessage="Running forecast…"
|
||||
/>
|
||||
</h3>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
{forecastProgress >= 0 &&
|
||||
|
@ -80,7 +91,12 @@ export function ForecastProgress({
|
|||
<EuiFlexGroup gutterSize="s" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="xs">
|
||||
<h3>Closing job...</h3>
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.timeSeriesExplorer.forecastingModal.closingJobTitle"
|
||||
defaultMessage="Closing job…"
|
||||
/>
|
||||
</h3>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
|
|
|
@ -33,6 +33,7 @@ import { PROGRESS_STATES } from './progress_states';
|
|||
import { ml } from 'plugins/ml/services/ml_api_service';
|
||||
import { mlJobService } from 'plugins/ml/services/job_service';
|
||||
import { mlForecastService } from 'plugins/ml/services/forecast_service';
|
||||
import { FormattedMessage, injectI18n } from '@kbn/i18n/react';
|
||||
|
||||
const FORECAST_JOB_MIN_VERSION = '6.1.0'; // Forecasting only allowed for jobs created >= 6.1.0.
|
||||
const FORECASTS_VIEW_MAX = 5; // Display links to a maximum of 5 forecasts.
|
||||
|
@ -58,7 +59,16 @@ function getDefaultState() {
|
|||
}
|
||||
|
||||
|
||||
class ForecastingModal extends Component {
|
||||
export const ForecastingModal = injectI18n(class ForecastingModal extends Component {
|
||||
static propTypes = {
|
||||
isDisabled: PropTypes.bool,
|
||||
job: PropTypes.object,
|
||||
detectorIndex: PropTypes.number,
|
||||
entities: PropTypes.array,
|
||||
loadForForecastId: PropTypes.func,
|
||||
timefilter: PropTypes.object,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = getDefaultState();
|
||||
|
@ -81,18 +91,34 @@ class ForecastingModal extends Component {
|
|||
};
|
||||
|
||||
onNewForecastDurationChange = (event) => {
|
||||
const { intl } = this.props;
|
||||
const newForecastDurationErrors = [];
|
||||
let isNewForecastDurationValid = true;
|
||||
const duration = parseInterval(event.target.value);
|
||||
if(duration === null) {
|
||||
isNewForecastDurationValid = false;
|
||||
newForecastDurationErrors.push('Invalid duration format');
|
||||
newForecastDurationErrors.push(
|
||||
intl.formatMessage({
|
||||
id: 'xpack.ml.timeSeriesExplorer.forecastingModal.invalidDurationFormatErrorMessage',
|
||||
defaultMessage: 'Invalid duration format',
|
||||
})
|
||||
);
|
||||
} else if (duration.asMilliseconds() > FORECAST_DURATION_MAX_MS) {
|
||||
isNewForecastDurationValid = false;
|
||||
newForecastDurationErrors.push('Forecast duration must not be greater than 8 weeks');
|
||||
newForecastDurationErrors.push(
|
||||
intl.formatMessage({
|
||||
id: 'xpack.ml.timeSeriesExplorer.forecastingModal.forecastDurationMustNotBeGreaterThanMaximumErrorMessage',
|
||||
defaultMessage: 'Forecast duration must not be greater than {maximumForecastDurationValue} weeks',
|
||||
}, { maximumForecastDurationValue: 8 })
|
||||
);
|
||||
} else if (duration.asMilliseconds() === 0) {
|
||||
isNewForecastDurationValid = false;
|
||||
newForecastDurationErrors.push('Forecast duration must not be zero');
|
||||
newForecastDurationErrors.push(
|
||||
intl.formatMessage({
|
||||
id: 'xpack.ml.timeSeriesExplorer.forecastingModal.forecastDurationMustNotBeZeroErrorMessage',
|
||||
defaultMessage: 'Forecast duration must not be zero',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
|
@ -133,7 +159,13 @@ class ForecastingModal extends Component {
|
|||
})
|
||||
.catch((resp) => {
|
||||
console.log('Time series forecast modal - could not open job:', resp);
|
||||
this.addMessage('Error opening job before running forecast', MESSAGE_LEVEL.ERROR);
|
||||
this.addMessage(
|
||||
this.props.intl.formatMessage({
|
||||
id: 'xpack.ml.timeSeriesExplorer.forecastingModal.errorWithOpeningJobBeforeRunningForecastErrorMessage',
|
||||
defaultMessage: 'Error opening job before running forecast',
|
||||
}),
|
||||
MESSAGE_LEVEL.ERROR
|
||||
);
|
||||
this.setState({
|
||||
jobOpeningState: PROGRESS_STATES.ERROR
|
||||
});
|
||||
|
@ -146,7 +178,11 @@ class ForecastingModal extends Component {
|
|||
if (resp && resp.message) {
|
||||
this.addMessage(resp.message, MESSAGE_LEVEL.ERROR, true);
|
||||
} else {
|
||||
this.addMessage('Unexpected response from running forecast. The request may have failed.',
|
||||
this.addMessage(
|
||||
this.props.intl.formatMessage({
|
||||
id: 'xpack.ml.timeSeriesExplorer.forecastingModal.unexpectedResponseFromRunningForecastErrorMessage',
|
||||
defaultMessage: 'Unexpected response from running forecast. The request may have failed.',
|
||||
}),
|
||||
MESSAGE_LEVEL.ERROR, true);
|
||||
}
|
||||
};
|
||||
|
@ -177,6 +213,7 @@ class ForecastingModal extends Component {
|
|||
// Obtain the stats for the forecast request and check forecast is progressing.
|
||||
// When the stats show the forecast is finished, load the
|
||||
// forecast results into the view.
|
||||
const { intl } = this.props;
|
||||
let previousProgress = 0;
|
||||
let noProgressMs = 0;
|
||||
this.forecastChecker = setInterval(() => {
|
||||
|
@ -210,7 +247,13 @@ class ForecastingModal extends Component {
|
|||
// Load the forecast data in the main page,
|
||||
// but leave this dialog open so the error can be viewed.
|
||||
console.log('Time series forecast modal - could not close job:', response);
|
||||
this.addMessage('Error closing job after running forecast', MESSAGE_LEVEL.ERROR);
|
||||
this.addMessage(
|
||||
intl.formatMessage({
|
||||
id: 'xpack.ml.timeSeriesExplorer.forecastingModal.errorWithClosingJobAfterRunningForecastErrorMessage',
|
||||
defaultMessage: 'Error closing job after running forecast',
|
||||
}),
|
||||
MESSAGE_LEVEL.ERROR
|
||||
);
|
||||
this.setState({
|
||||
jobClosingState: PROGRESS_STATES.ERROR
|
||||
});
|
||||
|
@ -227,8 +270,14 @@ class ForecastingModal extends Component {
|
|||
noProgressMs += FORECAST_STATS_POLL_FREQUENCY;
|
||||
if (noProgressMs > WARN_NO_PROGRESS_MS) {
|
||||
console.log(`Forecast request has not progressed for ${WARN_NO_PROGRESS_MS}ms. Cancelling check.`);
|
||||
this.addMessage(`No progress reported for the new forecast for ${WARN_NO_PROGRESS_MS}ms. ` +
|
||||
'An error may have occurred whilst running the forecast.', MESSAGE_LEVEL.ERROR);
|
||||
this.addMessage(
|
||||
intl.formatMessage({
|
||||
id: 'xpack.ml.timeSeriesExplorer.forecastingModal.noProgressReportedForNewForecastErrorMessage',
|
||||
defaultMessage: 'No progress reported for the new forecast for {WarnNoProgressMs}ms.' +
|
||||
'An error may have occurred whilst running the forecast.'
|
||||
}, { WarnNoProgressMs: WARN_NO_PROGRESS_MS }),
|
||||
MESSAGE_LEVEL.ERROR
|
||||
);
|
||||
|
||||
// Try and load any results which may have been created.
|
||||
this.props.loadForForecastId(forecastId);
|
||||
|
@ -243,7 +292,13 @@ class ForecastingModal extends Component {
|
|||
|
||||
}).catch((resp) => {
|
||||
console.log('Time series forecast modal - error loading stats of forecast from elasticsearch:', resp);
|
||||
this.addMessage('Error loading stats of running forecast.', MESSAGE_LEVEL.ERROR);
|
||||
this.addMessage(
|
||||
intl.formatMessage({
|
||||
id: 'xpack.ml.timeSeriesExplorer.forecastingModal.errorWithLoadingStatsOfRunningForecastErrorMessage',
|
||||
defaultMessage: 'Error loading stats of running forecast.',
|
||||
}),
|
||||
MESSAGE_LEVEL.ERROR
|
||||
);
|
||||
this.setState({
|
||||
forecastProgress: PROGRESS_STATES.ERROR
|
||||
});
|
||||
|
@ -253,6 +308,7 @@ class ForecastingModal extends Component {
|
|||
};
|
||||
|
||||
openModal = () => {
|
||||
const { intl } = this.props;
|
||||
const job = this.props.job;
|
||||
|
||||
if (typeof job === 'object') {
|
||||
|
@ -275,7 +331,13 @@ class ForecastingModal extends Component {
|
|||
})
|
||||
.catch((resp) => {
|
||||
console.log('Time series forecast modal - error obtaining forecasts summary:', resp);
|
||||
this.addMessage('Error obtaining list of previous forecasts', MESSAGE_LEVEL.ERROR);
|
||||
this.addMessage(
|
||||
intl.formatMessage({
|
||||
id: 'xpack.ml.timeSeriesExplorer.forecastingModal.errorWithObtainingListOfPreviousForecastsErrorMessage',
|
||||
defaultMessage: 'Error obtaining list of previous forecasts',
|
||||
}),
|
||||
MESSAGE_LEVEL.ERROR
|
||||
);
|
||||
});
|
||||
|
||||
// Display a warning about running a forecast if there is high number
|
||||
|
@ -297,9 +359,13 @@ class ForecastingModal extends Component {
|
|||
});
|
||||
if (numPartitions > WARN_NUM_PARTITIONS) {
|
||||
this.addMessage(
|
||||
`Note that this data contains more than ${WARN_NUM_PARTITIONS} ` +
|
||||
`partitions so running a forecast may take a long time and consume a high amount of resource`,
|
||||
MESSAGE_LEVEL.WARNING);
|
||||
intl.formatMessage({
|
||||
id: 'xpack.ml.timeSeriesExplorer.forecastingModal.dataContainsMorePartitionsMessage',
|
||||
defaultMessage: 'Note that this data contains more than {warnNumPartitions} ' +
|
||||
'partitions so running a forecast may take a long time and consume a high amount of resource',
|
||||
}, { warnNumPartitions: WARN_NUM_PARTITIONS }),
|
||||
MESSAGE_LEVEL.WARNING
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch((resp) => {
|
||||
|
@ -335,17 +401,22 @@ class ForecastingModal extends Component {
|
|||
// Forecasting disabled if detector has an over field or job created < 6.1.0.
|
||||
let isForecastingDisabled = false;
|
||||
let forecastingDisabledMessage = null;
|
||||
const job = this.props.job;
|
||||
const { intl, job } = this.props;
|
||||
if (job !== undefined) {
|
||||
const detector = job.analysis_config.detectors[this.props.detectorIndex];
|
||||
const overFieldName = detector.over_field_name;
|
||||
if (overFieldName !== undefined) {
|
||||
isForecastingDisabled = true;
|
||||
forecastingDisabledMessage = 'Forecasting is not available for population detectors with an over field';
|
||||
forecastingDisabledMessage = intl.formatMessage({
|
||||
id: 'xpack.ml.timeSeriesExplorer.forecastingModal.forecastingNotAvailableForPopulationDetectorsMessage',
|
||||
defaultMessage: 'Forecasting is not available for population detectors with an over field',
|
||||
});
|
||||
} else if (isJobVersionGte(job, FORECAST_JOB_MIN_VERSION) === false) {
|
||||
isForecastingDisabled = true;
|
||||
forecastingDisabledMessage = `Forecasting is only available for jobs created in version ` +
|
||||
`${FORECAST_JOB_MIN_VERSION} or later`;
|
||||
forecastingDisabledMessage = intl.formatMessage({
|
||||
id: 'xpack.ml.timeSeriesExplorer.forecastingModal.forecastingOnlyAvailableForJobsCreatedInSpecifiedVersionMessage',
|
||||
defaultMessage: 'Forecasting is only available for jobs created in version {minVersion} or later',
|
||||
}, { minVersion: FORECAST_JOB_MIN_VERSION });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -356,7 +427,10 @@ class ForecastingModal extends Component {
|
|||
isDisabled={isForecastingDisabled}
|
||||
fill
|
||||
>
|
||||
Forecast
|
||||
<FormattedMessage
|
||||
id="xpack.ml.timeSeriesExplorer.forecastingModal.forecastButtonLabel"
|
||||
defaultMessage="Forecast"
|
||||
/>
|
||||
</EuiButton>
|
||||
);
|
||||
|
||||
|
@ -392,14 +466,4 @@ class ForecastingModal extends Component {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
ForecastingModal.propTypes = {
|
||||
isDisabled: PropTypes.bool,
|
||||
job: PropTypes.object,
|
||||
detectorIndex: PropTypes.number,
|
||||
entities: PropTypes.array,
|
||||
loadForForecastId: PropTypes.func,
|
||||
timefilter: PropTypes.object,
|
||||
};
|
||||
|
||||
export { ForecastingModal };
|
||||
});
|
||||
|
|
|
@ -13,11 +13,15 @@ const module = uiModules.get('apps/ml', ['react']);
|
|||
|
||||
import { ForecastingModal } from './forecasting_modal';
|
||||
|
||||
import { injectI18nProvider } from '@kbn/i18n/react';
|
||||
|
||||
module.directive('mlForecastingModal', function ($injector) {
|
||||
const reactDirective = $injector.get('reactDirective');
|
||||
return reactDirective(
|
||||
ForecastingModal,
|
||||
undefined,
|
||||
injectI18nProvider(ForecastingModal),
|
||||
// reactDirective service requires for react component to have propTypes, but injectI18n doesn't copy propTypes from wrapped component.
|
||||
// That's why we pass propTypes directly to reactDirective service.
|
||||
Object.keys(ForecastingModal.WrappedComponent.propTypes || {}),
|
||||
{ restrict: 'E' },
|
||||
{ timefilter }
|
||||
);
|
||||
|
|
|
@ -22,32 +22,42 @@ import {
|
|||
} from '@elastic/eui';
|
||||
|
||||
import { formatHumanReadableDateTimeSeconds } from '../../../util/date_utils';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
function getColumns(viewForecast) {
|
||||
return [
|
||||
{
|
||||
field: 'forecast_create_timestamp',
|
||||
name: 'Created',
|
||||
name: i18n.translate('xpack.ml.timeSeriesExplorer.forecastsList.createdColumnName', {
|
||||
defaultMessage: 'Created'
|
||||
}),
|
||||
dataType: 'date',
|
||||
render: (date) => formatHumanReadableDateTimeSeconds(date),
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
field: 'forecast_start_timestamp',
|
||||
name: 'From',
|
||||
name: i18n.translate('xpack.ml.timeSeriesExplorer.forecastsList.fromColumnName', {
|
||||
defaultMessage: 'From'
|
||||
}),
|
||||
dataType: 'date',
|
||||
render: (date) => formatHumanReadableDateTimeSeconds(date),
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
field: 'forecast_end_timestamp',
|
||||
name: 'To',
|
||||
name: i18n.translate('xpack.ml.timeSeriesExplorer.forecastsList.toColumnName', {
|
||||
defaultMessage: 'To'
|
||||
}),
|
||||
dataType: 'date',
|
||||
render: (date) => formatHumanReadableDateTimeSeconds(date),
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
name: 'View',
|
||||
name: i18n.translate('xpack.ml.timeSeriesExplorer.forecastsList.viewColumnName', {
|
||||
defaultMessage: 'View'
|
||||
}),
|
||||
render: (forecast) => (
|
||||
<EuiButton
|
||||
className="view-forecast-btn"
|
||||
|
@ -69,11 +79,17 @@ export function ForecastsList({ forecasts, viewForecast }) {
|
|||
aria-describedby="ml_aria_description_forecasting_modal_view_list"
|
||||
style={{ display: 'inline', paddingRight: '5px' }}
|
||||
>
|
||||
Previous forecasts
|
||||
<FormattedMessage
|
||||
id="xpack.ml.timeSeriesExplorer.forecastsList.previousForecastsTitle"
|
||||
defaultMessage="Previous forecasts"
|
||||
/>
|
||||
</h3>
|
||||
<EuiToolTip
|
||||
position="right"
|
||||
content="Lists a maximum of five of the most recently run forecasts."
|
||||
content={<FormattedMessage
|
||||
id="xpack.ml.timeSeriesExplorer.forecastsList.listsOfFiveRecentlyRunForecastsTooltip"
|
||||
defaultMessage="Lists a maximum of five of the most recently run forecasts."
|
||||
/>}
|
||||
>
|
||||
<EuiIcon
|
||||
type="questionInCircle"
|
||||
|
|
|
@ -29,6 +29,8 @@ import { MessageCallOut } from 'plugins/ml/components/message_call_out';
|
|||
import { ForecastsList } from './forecasts_list';
|
||||
import { RunControls } from './run_controls';
|
||||
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
|
||||
export function Modal(props) {
|
||||
return (
|
||||
|
@ -39,7 +41,12 @@ export function Modal(props) {
|
|||
>
|
||||
|
||||
<EuiModalHeader>
|
||||
<EuiModalHeaderTitle>Forecasting</EuiModalHeaderTitle>
|
||||
<EuiModalHeaderTitle>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.timeSeriesExplorer.forecastingModal.forecastingTitle"
|
||||
defaultMessage="Forecasting"
|
||||
/>
|
||||
</EuiModalHeaderTitle>
|
||||
</EuiModalHeader>
|
||||
|
||||
<EuiModalBody>
|
||||
|
@ -70,7 +77,10 @@ export function Modal(props) {
|
|||
onClick={props.close}
|
||||
size="s"
|
||||
>
|
||||
Close
|
||||
<FormattedMessage
|
||||
id="xpack.ml.timeSeriesExplorer.forecastingModal.closeButtonLabel"
|
||||
defaultMessage="Close"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiModalFooter>
|
||||
</EuiModal>
|
||||
|
|
|
@ -32,6 +32,8 @@ import { JOB_STATE } from '../../../../common/constants/states';
|
|||
import { ForecastProgress } from './forecast_progress';
|
||||
import { mlNodesAvailable } from 'plugins/ml/ml_nodes_check/check_ml_nodes';
|
||||
import { checkPermission, createPermissionFailureMessage } from 'plugins/ml/privilege/check_privilege';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
|
||||
function getRunInputDisabledState(
|
||||
|
@ -45,7 +47,9 @@ function getRunInputDisabledState(
|
|||
if (mlNodesAvailable() === false) {
|
||||
return {
|
||||
isDisabled: true,
|
||||
isDisabledToolTipText: 'There are no ML nodes available.'
|
||||
isDisabledToolTipText: i18n.translate('xpack.ml.timeSeriesExplorer.runControls.noMLNodesAvailableTooltip', {
|
||||
defaultMessage: 'There are no ML nodes available.'
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -60,7 +64,10 @@ function getRunInputDisabledState(
|
|||
if (job.state !== JOB_STATE.OPENED && job.state !== JOB_STATE.CLOSED) {
|
||||
return {
|
||||
isDisabled: true,
|
||||
isDisabledToolTipText: `Forecasts cannot be run on ${job.state} jobs`
|
||||
isDisabledToolTipText: i18n.translate('xpack.ml.timeSeriesExplorer.runControls.forecastsCanNotBeRunOnJobsTooltip', {
|
||||
defaultMessage: 'Forecasts cannot be run on {jobState} jobs',
|
||||
values: { jobState: job.state }
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -100,26 +107,40 @@ export function RunControls({
|
|||
onClick={runForecast}
|
||||
isDisabled={disabledState.isDisabled || !isNewForecastDurationValid}
|
||||
>
|
||||
Run
|
||||
<FormattedMessage
|
||||
id="xpack.ml.timeSeriesExplorer.runControls.runButtonLabel"
|
||||
defaultMessage="Run"
|
||||
/>
|
||||
</EuiButton>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<EuiText>
|
||||
<h3>Run a new forecast</h3>
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.timeSeriesExplorer.runControls.runNewForecastTitle"
|
||||
defaultMessage="Run a new forecast"
|
||||
/>
|
||||
</h3>
|
||||
</EuiText>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiForm>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
label="Duration"
|
||||
label={<FormattedMessage
|
||||
id="xpack.ml.timeSeriesExplorer.runControls.durationLabel"
|
||||
defaultMessage="Duration"
|
||||
/>}
|
||||
fullWidth
|
||||
isInvalid={!isNewForecastDurationValid}
|
||||
error={newForecastDurationErrors}
|
||||
helpText={'Length of forecast, up to a maximum of 8 weeks. ' +
|
||||
'Use s for seconds, m for minutes, h for hours, d for days, w for weeks.'}
|
||||
helpText={<FormattedMessage
|
||||
id="xpack.ml.timeSeriesExplorer.runControls.forecastMaximumLengthHelpText"
|
||||
defaultMessage="Length of forecast, up to a maximum of 8 weeks.
|
||||
Use s for seconds, m for minutes, h for hours, d for days, w for weeks."
|
||||
/>}
|
||||
>
|
||||
{disabledState.isDisabledToolTipText === undefined ? durationInput
|
||||
: (
|
||||
|
|
|
@ -56,6 +56,8 @@ import {
|
|||
unhighlightFocusChartAnnotation
|
||||
} from './timeseries_chart_annotations';
|
||||
|
||||
import { injectI18n } from '@kbn/i18n/react';
|
||||
|
||||
const mlAnnotationsEnabled = chrome.getInjected('mlAnnotationsEnabled', false);
|
||||
|
||||
const focusZoomPanelHeight = 25;
|
||||
|
@ -89,7 +91,7 @@ function getSvgHeight() {
|
|||
return focusHeight + contextChartHeight + swimlaneHeight + chartSpacing + margin.top + margin.bottom;
|
||||
}
|
||||
|
||||
export class TimeseriesChart extends React.Component {
|
||||
export const TimeseriesChart = injectI18n(class TimeseriesChart extends React.Component {
|
||||
static propTypes = {
|
||||
indexAnnotation: PropTypes.func,
|
||||
autoZoomDuration: PropTypes.number,
|
||||
|
@ -160,17 +162,28 @@ export class TimeseriesChart extends React.Component {
|
|||
const {
|
||||
deleteAnnotation,
|
||||
refresh,
|
||||
toastNotifications
|
||||
toastNotifications,
|
||||
intl
|
||||
} = this.props;
|
||||
|
||||
const { annotation } = this.state;
|
||||
|
||||
try {
|
||||
await deleteAnnotation(annotation._id);
|
||||
toastNotifications.addSuccess(`Deleted annotation for job with ID ${annotation.job_id}.`);
|
||||
toastNotifications.addSuccess(
|
||||
intl.formatMessage({
|
||||
id: 'xpack.ml.timeSeriesExplorer.timeSeriesChart.deletedAnnotationNotificationMessage',
|
||||
defaultMessage: 'Deleted annotation for job with ID {jobId}.',
|
||||
}, { jobId: annotation.job_id })
|
||||
);
|
||||
} catch (err) {
|
||||
toastNotifications
|
||||
.addDanger(`An error occured deleting the annotation for job with ID ${annotation.job_id}: ${JSON.stringify(err)}`);
|
||||
.addDanger(
|
||||
intl.formatMessage({
|
||||
id: 'xpack.ml.timeSeriesExplorer.timeSeriesChart.errorWithDeletingAnnotationNotificationErrorMessage',
|
||||
defaultMessage: 'An error occured deleting the annotation for job with ID {jobId}: {error}',
|
||||
}, { jobId: annotation.job_id, error: JSON.stringify(err) })
|
||||
);
|
||||
}
|
||||
|
||||
this.closeDeleteModal();
|
||||
|
@ -183,7 +196,8 @@ export class TimeseriesChart extends React.Component {
|
|||
const {
|
||||
indexAnnotation,
|
||||
refresh,
|
||||
toastNotifications
|
||||
toastNotifications,
|
||||
intl
|
||||
} = this.props;
|
||||
|
||||
this.closeFlyout();
|
||||
|
@ -191,17 +205,38 @@ export class TimeseriesChart extends React.Component {
|
|||
indexAnnotation(annotation)
|
||||
.then(() => {
|
||||
refresh();
|
||||
const action = (typeof annotation._id === 'undefined') ? 'Added an' : 'Updated';
|
||||
if (typeof annotation._id === 'undefined') {
|
||||
toastNotifications.addSuccess(`${action} annotation for job with ID ${annotation.job_id}.`);
|
||||
toastNotifications.addSuccess(
|
||||
intl.formatMessage({
|
||||
id: 'xpack.ml.timeSeriesExplorer.timeSeriesChart.addedAnnotationNotificationMessage',
|
||||
defaultMessage: 'Added an annotation for job with ID {jobId}.',
|
||||
}, { jobId: annotation.job_id })
|
||||
);
|
||||
} else {
|
||||
toastNotifications.addSuccess(`${action} annotation for job with ID ${annotation.job_id}.`);
|
||||
toastNotifications.addSuccess(
|
||||
intl.formatMessage({
|
||||
id: 'xpack.ml.timeSeriesExplorer.timeSeriesChart.updatedAnnotationNotificationMessage',
|
||||
defaultMessage: 'Updated annotation for job with ID {jobId}.',
|
||||
}, { jobId: annotation.job_id })
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch((resp) => {
|
||||
const action = (typeof annotation._id === 'undefined') ? 'creating' : 'updating';
|
||||
toastNotifications
|
||||
.addDanger(`An error occured ${action} the annotation for job with ID ${annotation.job_id}: ${JSON.stringify(resp)}`);
|
||||
if (typeof annotation._id === 'undefined') {
|
||||
toastNotifications.addDanger(
|
||||
intl.formatMessage({
|
||||
id: 'xpack.ml.timeSeriesExplorer.timeSeriesChart.errorWithCreatingAnnotationNotificationErrorMessage',
|
||||
defaultMessage: 'An error occured creating the annotation for job with ID {jobId}: {error}',
|
||||
}, { jobId: annotation.job_id, error: JSON.stringify(resp) })
|
||||
);
|
||||
} else {
|
||||
toastNotifications.addDanger(
|
||||
intl.formatMessage({
|
||||
id: 'xpack.ml.timeSeriesExplorer.timeSeriesChart.errorWithUpdatingAnnotationNotificationErrorMessage',
|
||||
defaultMessage: 'An error occured updating the annotation for job with ID {jobId}: {error}',
|
||||
}, { jobId: annotation.job_id, error: JSON.stringify(resp) })
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -580,7 +615,8 @@ export class TimeseriesChart extends React.Component {
|
|||
selectedJob,
|
||||
showAnnotations,
|
||||
showForecast,
|
||||
showModelBounds
|
||||
showModelBounds,
|
||||
intl
|
||||
} = this.props;
|
||||
|
||||
if (focusChartData === undefined) {
|
||||
|
@ -600,7 +636,11 @@ export class TimeseriesChart extends React.Component {
|
|||
const bucketSpan = selectedJob.analysis_config.bucket_span;
|
||||
const chartElement = d3.select(this.rootNode);
|
||||
chartElement.select('.zoom-aggregation-interval').text(
|
||||
`(aggregation interval: ${focusAggInt}, bucket span: ${bucketSpan})`);
|
||||
intl.formatMessage({
|
||||
id: 'xpack.ml.timeSeriesExplorer.timeSeriesChart.zoomAggregationIntervalLabel',
|
||||
defaultMessage: '(aggregation interval: {focusAggInt}, bucket span: {bucketSpan})',
|
||||
}, { focusAggInt, bucketSpan })
|
||||
);
|
||||
|
||||
// Render the axes.
|
||||
|
||||
|
@ -818,7 +858,8 @@ export class TimeseriesChart extends React.Component {
|
|||
const {
|
||||
autoZoomDuration,
|
||||
modelPlotEnabled,
|
||||
timefilter
|
||||
timefilter,
|
||||
intl
|
||||
} = this.props;
|
||||
|
||||
const setZoomInterval = this.setZoomInterval.bind(this);
|
||||
|
@ -834,7 +875,12 @@ export class TimeseriesChart extends React.Component {
|
|||
.attr('x', xPos)
|
||||
.attr('y', 17)
|
||||
.attr('class', 'zoom-info-text')
|
||||
.text('Zoom:');
|
||||
.text(
|
||||
intl.formatMessage({
|
||||
id: 'xpack.ml.timeSeriesExplorer.timeSeriesChart.zoomLabel',
|
||||
defaultMessage: 'Zoom:',
|
||||
})
|
||||
);
|
||||
|
||||
const zoomOptions = [{ durationMs: autoZoomDuration, label: 'auto' }];
|
||||
_.each(ZOOM_INTERVAL_OPTIONS, (option) => {
|
||||
|
@ -862,14 +908,22 @@ export class TimeseriesChart extends React.Component {
|
|||
.attr('x', (xPos + 6))
|
||||
.attr('y', 17)
|
||||
.attr('class', 'zoom-info-text zoom-aggregation-interval')
|
||||
.text('(aggregation interval: , bucket span: )');
|
||||
.text(
|
||||
intl.formatMessage({
|
||||
id: 'xpack.ml.timeSeriesExplorer.timeSeriesChart.zoomGroupAggregationIntervalLabel',
|
||||
defaultMessage: '(aggregation interval: , bucket span: )',
|
||||
})
|
||||
);
|
||||
|
||||
if (modelPlotEnabled === false) {
|
||||
const modelPlotLabel = zoomGroup.append('text')
|
||||
.attr('x', 300)
|
||||
.attr('y', 17)
|
||||
.attr('class', 'zoom-info-text')
|
||||
.text('Model bounds are not available');
|
||||
.text(intl.formatMessage({
|
||||
id: 'xpack.ml.timeSeriesExplorer.timeSeriesChart.modelBoundsNotAvailableLabel',
|
||||
defaultMessage: 'Model bounds are not available',
|
||||
}));
|
||||
|
||||
modelPlotLabel.attr('x', (fcsWidth - (modelPlotLabel.node().getBBox().width + 10)));
|
||||
}
|
||||
|
@ -1285,7 +1339,8 @@ export class TimeseriesChart extends React.Component {
|
|||
|
||||
showFocusChartTooltip(marker, circle) {
|
||||
const {
|
||||
modelPlotEnabled
|
||||
modelPlotEnabled,
|
||||
intl
|
||||
} = this.props;
|
||||
|
||||
const fieldFormat = this.fieldFormat;
|
||||
|
@ -1298,10 +1353,19 @@ export class TimeseriesChart extends React.Component {
|
|||
if (_.has(marker, 'anomalyScore')) {
|
||||
const score = parseInt(marker.anomalyScore);
|
||||
const displayScore = (score > 0 ? score : '< 1');
|
||||
contents += `anomaly score: ${displayScore}<br/>`;
|
||||
contents += intl.formatMessage({
|
||||
id: 'xpack.ml.timeSeriesExplorer.timeSeriesChart.anomalyScoreLabel',
|
||||
defaultMessage: 'anomaly score: {displayScore}{br}'
|
||||
}, { displayScore, br: '<br />' });
|
||||
|
||||
if (showMultiBucketAnomalyTooltip(marker) === true) {
|
||||
contents += `multi-bucket impact: ${getMultiBucketImpactLabel(marker.multiBucketImpact)}<br/>`;
|
||||
contents += intl.formatMessage({
|
||||
id: 'xpack.ml.timeSeriesExplorer.timeSeriesChart.multiBucketImpactLabel',
|
||||
defaultMessage: 'multi-bucket impact: {multiBucketImpactLabel}{br}'
|
||||
}, {
|
||||
br: '<br />',
|
||||
multiBucketImpactLabel: getMultiBucketImpactLabel(marker.multiBucketImpact)
|
||||
});
|
||||
}
|
||||
|
||||
if (modelPlotEnabled === false) {
|
||||
|
@ -1311,43 +1375,108 @@ export class TimeseriesChart extends React.Component {
|
|||
if (_.has(marker, 'actual') && marker.function !== 'rare') {
|
||||
// Display the record actual in preference to the chart value, which may be
|
||||
// different depending on the aggregation interval of the chart.
|
||||
contents += `actual: ${formatValue(marker.actual, marker.function, fieldFormat)}`;
|
||||
contents += `<br/>typical: ${formatValue(marker.typical, marker.function, fieldFormat)}`;
|
||||
contents += intl.formatMessage({
|
||||
id: 'xpack.ml.timeSeriesExplorer.timeSeriesChart.actualLabel',
|
||||
defaultMessage: 'actual: {actualValue}'
|
||||
}, {
|
||||
actualValue: formatValue(marker.actual, marker.function, fieldFormat)
|
||||
});
|
||||
contents += intl.formatMessage({
|
||||
id: 'xpack.ml.timeSeriesExplorer.timeSeriesChart.typicalLabel',
|
||||
defaultMessage: '{br}typical: {typicalValue}'
|
||||
}, {
|
||||
br: '<br />',
|
||||
typicalValue: formatValue(marker.typical, marker.function, fieldFormat)
|
||||
});
|
||||
} else {
|
||||
contents += `value: ${formatValue(marker.value, marker.function, fieldFormat)}`;
|
||||
contents += intl.formatMessage({
|
||||
id: 'xpack.ml.timeSeriesExplorer.timeSeriesChart.valueLabel',
|
||||
defaultMessage: 'value: {value}'
|
||||
}, {
|
||||
value: formatValue(marker.value, marker.function, fieldFormat)
|
||||
});
|
||||
if (_.has(marker, 'byFieldName') && _.has(marker, 'numberOfCauses')) {
|
||||
const numberOfCauses = marker.numberOfCauses;
|
||||
// If numberOfCauses === 1, won't go into this block as actual/typical copied to top level fields.
|
||||
const byFieldName = mlEscape(marker.byFieldName);
|
||||
if (numberOfCauses < 10) {
|
||||
// If numberOfCauses === 1, won't go into this block as actual/typical copied to top level fields.
|
||||
contents += `<br/> ${numberOfCauses} unusual ${byFieldName} values`;
|
||||
} else {
|
||||
contents += intl.formatMessage({
|
||||
id: 'xpack.ml.timeSeriesExplorer.timeSeriesChart.moreThanOneUnusualByFieldValuesLabel',
|
||||
defaultMessage: '{br} {numberOfCauses}{plusSign} unusual {byFieldName} values'
|
||||
}, {
|
||||
br: '<br />',
|
||||
numberOfCauses,
|
||||
byFieldName,
|
||||
// Maximum of 10 causes are stored in the record, so '10' may mean more than 10.
|
||||
contents += `<br/> ${numberOfCauses}+ unusual ${byFieldName} values`;
|
||||
}
|
||||
plusSign: numberOfCauses < 10 ? '' : '+'
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
contents += `value: ${formatValue(marker.value, marker.function, fieldFormat)}`;
|
||||
contents += `<br/>upper bounds: ${formatValue(marker.upper, marker.function, fieldFormat)}`;
|
||||
contents += `<br/>lower bounds: ${formatValue(marker.lower, marker.function, fieldFormat)}`;
|
||||
contents += intl.formatMessage({
|
||||
id: 'xpack.ml.timeSeriesExplorer.timeSeriesChart.modelPlotEnabled.valueLabel',
|
||||
defaultMessage: 'value: {value}'
|
||||
}, {
|
||||
value: formatValue(marker.value, marker.function, fieldFormat)
|
||||
});
|
||||
contents += intl.formatMessage({
|
||||
id: 'xpack.ml.timeSeriesExplorer.timeSeriesChart.modelPlotEnabled.upperBoundsLabel',
|
||||
defaultMessage: '{br}upper bounds: {upperBoundsValue}'
|
||||
}, {
|
||||
br: '<br />',
|
||||
upperBoundsValue: formatValue(marker.upper, marker.function, fieldFormat)
|
||||
});
|
||||
contents += intl.formatMessage({
|
||||
id: 'xpack.ml.timeSeriesExplorer.timeSeriesChart.modelPlotEnabled.lowerBoundsLabel',
|
||||
defaultMessage: '{br}lower bounds: {lowerBoundsValue}'
|
||||
}, {
|
||||
br: '<br />',
|
||||
lowerBoundsValue: formatValue(marker.lower, marker.function, fieldFormat)
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// TODO - need better formatting for small decimals.
|
||||
if (_.get(marker, 'isForecast', false) === true) {
|
||||
contents += `prediction: ${formatValue(marker.value, marker.function, fieldFormat)}`;
|
||||
contents += intl.formatMessage({
|
||||
id: 'xpack.ml.timeSeriesExplorer.timeSeriesChart.withoutAnomalyScore.predictionLabel',
|
||||
defaultMessage: 'prediction: {predictionValue}'
|
||||
}, {
|
||||
predictionValue: formatValue(marker.value, marker.function, fieldFormat)
|
||||
});
|
||||
} else {
|
||||
contents += `value: ${formatValue(marker.value, marker.function, fieldFormat)}`;
|
||||
contents += intl.formatMessage({
|
||||
id: 'xpack.ml.timeSeriesExplorer.timeSeriesChart.withoutAnomalyScore.valueLabel',
|
||||
defaultMessage: 'value: {value}'
|
||||
}, {
|
||||
value: formatValue(marker.value, marker.function, fieldFormat)
|
||||
});
|
||||
}
|
||||
|
||||
if (modelPlotEnabled === true) {
|
||||
contents += `<br/>upper bounds: ${formatValue(marker.upper, marker.function, fieldFormat)}`;
|
||||
contents += `<br/>lower bounds: ${formatValue(marker.lower, marker.function, fieldFormat)}`;
|
||||
contents += intl.formatMessage({
|
||||
id: 'xpack.ml.timeSeriesExplorer.timeSeriesChart.withoutAnomalyScoreAndModelPlotEnabled.upperBoundsLabel',
|
||||
defaultMessage: '{br}upper bounds: {upperBoundsValue}'
|
||||
}, {
|
||||
br: '<br />',
|
||||
upperBoundsValue: formatValue(marker.upper, marker.function, fieldFormat)
|
||||
});
|
||||
contents += intl.formatMessage({
|
||||
id: 'xpack.ml.timeSeriesExplorer.timeSeriesChart.withoutAnomalyScoreAndModelPlotEnabled.lowerBoundsLabel',
|
||||
defaultMessage: '{br}lower bounds: {lowerBoundsValue}'
|
||||
}, {
|
||||
br: '<br />',
|
||||
lowerBoundsValue: formatValue(marker.lower, marker.function, fieldFormat)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (_.has(marker, 'scheduledEvents')) {
|
||||
contents += `<br/><hr/>Scheduled events:<br/>${marker.scheduledEvents.map(mlEscape).join('<br/>')}`;
|
||||
contents += '<br/><hr/>' + intl.formatMessage({
|
||||
id: 'xpack.ml.timeSeriesExplorer.timeSeriesChart.scheduledEventsLabel',
|
||||
defaultMessage: 'Scheduled events:{br}{scheduledEventsValue}'
|
||||
}, {
|
||||
br: '<br />',
|
||||
scheduledEventsValue: marker.scheduledEvents.map(mlEscape).join('<br/>')
|
||||
});
|
||||
}
|
||||
|
||||
if (mlAnnotationsEnabled && _.has(marker, 'annotation')) {
|
||||
|
@ -1451,4 +1580,4 @@ export class TimeseriesChart extends React.Component {
|
|||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
//import mockOverallSwimlaneData from './__mocks__/mock_overall_swimlane.json';
|
||||
|
||||
import moment from 'moment-timezone';
|
||||
import { mount } from 'enzyme';
|
||||
import { mountWithIntl } from 'test_utils/enzyme_helpers';
|
||||
import React from 'react';
|
||||
|
||||
import { TimeseriesChart } from './timeseries_chart';
|
||||
|
@ -62,7 +62,7 @@ describe('TimeseriesChart', () => {
|
|||
test('Minimal initialization', () => {
|
||||
const props = getTimeseriesChartPropsMock();
|
||||
|
||||
const wrapper = mount(<TimeseriesChart {...props}/>);
|
||||
const wrapper = mountWithIntl(<TimeseriesChart.WrappedComponent {...props}/>);
|
||||
|
||||
expect(wrapper.html()).toBe(
|
||||
`<div class="ml-timeseries-chart-react"></div>`
|
||||
|
|
|
@ -27,6 +27,8 @@ const module = uiModules.get('apps/ml');
|
|||
|
||||
import { ml } from 'plugins/ml/services/ml_api_service';
|
||||
|
||||
import { I18nProvider } from '@kbn/i18n/react';
|
||||
|
||||
import chrome from 'ui/chrome';
|
||||
const mlAnnotationsEnabled = chrome.getInjected('mlAnnotationsEnabled', false);
|
||||
|
||||
|
@ -73,7 +75,9 @@ module.directive('mlTimeseriesChart', function () {
|
|||
};
|
||||
|
||||
ReactDOM.render(
|
||||
React.createElement(TimeseriesChart, props),
|
||||
<I18nProvider>
|
||||
{React.createElement(TimeseriesChart, props)}
|
||||
</I18nProvider>,
|
||||
element[0]
|
||||
);
|
||||
}
|
||||
|
|
|
@ -10,13 +10,27 @@
|
|||
|
||||
<div class="no-results-container" ng-if="jobs.length === 0 && loading === false">
|
||||
<div class="no-results">
|
||||
<div><i class="fa fa-exclamation-triangle"></i>No single metric jobs found</div>
|
||||
<div><a href="ml#/jobs">Create new single metric job</a></div>
|
||||
<div
|
||||
i18n-id="xpack.ml.timeSeriesExplorer.noSingleMetricJobsFoundLabel"
|
||||
i18n-default-message="{icon} No single metric jobs found"
|
||||
i18n-values="{ html_icon: '<i class=\'fa fa-exclamation-triangle\'></i>' }">
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
href="ml#/jobs"
|
||||
i18n-id="xpack.ml.timeSeriesExplorer.createNewSingleMetricJobLinkText"
|
||||
i18n-default-message="Create new single metric job"
|
||||
></a></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="series-controls" ng-show="jobs.length > 0">
|
||||
<label for="selectDetector" class="kuiLabel">Detector:</label>
|
||||
<label
|
||||
for="selectDetector"
|
||||
class="kuiLabel"
|
||||
i18n-id="xpack.ml.timeSeriesExplorer.detectorLabel"
|
||||
i18n-default-message="Detector:"
|
||||
></label>
|
||||
<select id="selectDetector" class="kuiSelect kuiSelect--large" ng-model="detectorId" ng-change="detectorIndexChanged()">
|
||||
<option ng-repeat="detector in detectors track by detector.index" value="{{detector.index}}">{{detector.detector_description}}</option>
|
||||
</select>
|
||||
|
@ -29,11 +43,16 @@
|
|||
<label for="select{{entity.fieldName}}" class="kuiLabel">{{entity.fieldName}}:</label>
|
||||
<input id="select{{entity.fieldName}}" class="kuiTextInput entity-input"
|
||||
ng-class="{ 'entity-input-blank': entity.fieldValue.length === 0 }"
|
||||
ng-model="entity.fieldValue" ng-model-options="{ updateOn: 'blur' }" placeholder="Enter value"
|
||||
ng-model="entity.fieldValue" ng-model-options="{ updateOn: 'blur' }"
|
||||
placeholder="{{ ::'xpack.ml.timeSeriesExplorer.enterValuePlaceholder' | i18n: {defaultMessage: 'Enter value'} }}"
|
||||
list='{{entity.fieldName}}_datalist' />
|
||||
</div>
|
||||
|
||||
<button class="kuiButton kuiButton--primary" ng-click="saveSeriesPropertiesAndRefresh()" aria-label="refresh">
|
||||
<button
|
||||
class="kuiButton kuiButton--primary"
|
||||
ng-click="saveSeriesPropertiesAndRefresh()"
|
||||
aria-label="{{ ::'xpack.ml.timeSeriesExplorer.refreshButtonAriLabel' | i18n: {defaultMessage: 'refresh'} }}"
|
||||
>
|
||||
<i class="fa fa-play" ></i>
|
||||
</button>
|
||||
|
||||
|
@ -54,25 +73,44 @@
|
|||
|
||||
<div class="no-results-container" ng-show="jobs.length > 0 && loading === false && hasResults === false">
|
||||
<div class="no-results">
|
||||
<div><i class="fa fa-info-circle" ></i>No results found</div>
|
||||
<div>Try widening the time selection or moving further back in time</div>
|
||||
<div
|
||||
i18n-id="xpack.ml.timeSeriesExplorer.noResultsFoundLabel"
|
||||
i18n-default-message="{icon} No results found"
|
||||
i18n-values="{ html_icon: '<i class=\'fa fa-info-circle\'></i>' }"
|
||||
></div>
|
||||
<div
|
||||
i18n-id="xpack.ml.timeSeriesExplorer.tryWideningTheTimeSelectionDescription"
|
||||
i18n-default-message="Try widening the time selection or moving further back in time"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-show="jobs.length > 0 && loading === false && hasResults === true">
|
||||
<div class="results-container">
|
||||
|
||||
<span class="panel-title euiText">
|
||||
Single time series analysis of {{chartDetails.functionLabel}}
|
||||
</span>
|
||||
<span
|
||||
class="panel-title euiText"
|
||||
i18n-id="xpack.ml.timeSeriesExplorer.singleTimeSeriesAnalysisTitle"
|
||||
i18n-default-message="Single time series analysis of {functionLabel}"
|
||||
i18n-values="{ functionLabel: chartDetails.functionLabel }"
|
||||
></span>
|
||||
<span ng-if="chartDetails.entityData.count === 1" class="entity-count-text">
|
||||
<span ng-repeat="entity in chartDetails.entityData.entities">
|
||||
{{$first ? '(' : ''}}{{entity.fieldName}}: {{entity.fieldValue}}{{$last ? ')' : ', '}}
|
||||
</span>
|
||||
</span>
|
||||
<span ng-if="chartDetails.entityData.count !== 1" class="entity-count-text">
|
||||
<span ng-repeat="countData in chartDetails.entityData.entities">
|
||||
{{$first ? '(' : ''}}{{countData.cardinality}} distinct {{countData.fieldName}}{{countData.cardinality > 1 ? ' values' : ''}}{{$last ? ')' : ', '}}
|
||||
<span
|
||||
ng-repeat="countData in chartDetails.entityData.entities"
|
||||
i18n-id="xpack.ml.timeSeriesExplorer.countDataInChartDetailsDescription"
|
||||
i18n-default-message="{openBrace}{cardinality} distinct {fieldName} {cardinality, plural, one {} other { values}}{closeBrace}"
|
||||
i18n-values="{
|
||||
openBrace: $first ? '(' : '',
|
||||
closeBrace: $last ? ')' : ', ',
|
||||
cardinality: countData.cardinality,
|
||||
fieldName: countData.fieldName
|
||||
}"
|
||||
>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
|
@ -83,7 +121,12 @@
|
|||
class="kuiCheckBox"
|
||||
ng-click="toggleShowModelBounds()"
|
||||
ng-checked="showModelBounds === true">
|
||||
<label for="toggleShowModelBoundsCheckbox" class="kuiCheckBoxLabel">show model bounds</label>
|
||||
<label
|
||||
for="toggleShowModelBoundsCheckbox"
|
||||
class="kuiCheckBoxLabel"
|
||||
i18n-id="xpack.ml.timeSeriesExplorer.showModelBoundsLabel"
|
||||
i18n-default-message="show model bounds"
|
||||
></label>
|
||||
</div>
|
||||
|
||||
<div ng-show="showAnnotationsCheckbox === true">
|
||||
|
@ -92,7 +135,12 @@
|
|||
class="kuiCheckBox"
|
||||
ng-click="toggleShowAnnotations()"
|
||||
ng-checked="showAnnotations === true">
|
||||
<label for="toggleAnnotationsCheckbox" class="kuiCheckBoxLabel">annotations</label>
|
||||
<label
|
||||
for="toggleAnnotationsCheckbox"
|
||||
class="kuiCheckBoxLabel"
|
||||
i18n-id="xpack.ml.timeSeriesExplorer.annotationsLabel"
|
||||
i18n-default-message="annotations"
|
||||
></label>
|
||||
</div>
|
||||
|
||||
<div ng-show="showForecastCheckbox === true">
|
||||
|
@ -101,7 +149,12 @@
|
|||
class="kuiCheckBox"
|
||||
ng-click="toggleShowForecast()"
|
||||
ng-checked="showForecast === true">
|
||||
<label for="toggleShowForecastCheckbox" class="kuiCheckBoxLabel">show forecast</label>
|
||||
<label
|
||||
for="toggleShowForecastCheckbox"
|
||||
class="kuiCheckBoxLabel"
|
||||
i18n-id="xpack.ml.timeSeriesExplorer.showForecastLabel"
|
||||
i18n-default-message="show forecast"
|
||||
></label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -131,9 +184,11 @@
|
|||
</div>
|
||||
|
||||
<div ng-show="showAnnotations && focusAnnotationData.length > 0">
|
||||
<span class="panel-title euiText">
|
||||
Annotations
|
||||
</span>
|
||||
<span
|
||||
class="panel-title euiText"
|
||||
i18n-id="xpack.ml.timeSeriesExplorer.annotationsTitle"
|
||||
i18n-default-message="Annotations"
|
||||
></span>
|
||||
|
||||
<ml-annotation-table
|
||||
annotations="focusAnnotationData"
|
||||
|
@ -144,14 +199,21 @@
|
|||
<br /><br />
|
||||
</div>
|
||||
|
||||
<span class="panel-title euiText">
|
||||
Anomalies
|
||||
</span>
|
||||
<span
|
||||
class="panel-title euiText"
|
||||
i18n-id="xpack.ml.timeSeriesExplorer.anomaliesTitle"
|
||||
i18n-default-message="Anomalies"
|
||||
></span>
|
||||
|
||||
<div class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive ml-anomalies-controls">
|
||||
<div class="euiFlexItem euiFlexItem--flexGrowZero" style="width:170px">
|
||||
<div class="euiFormRow" id="select_severity_control">
|
||||
<label class="euiFormLabel" for="select_severity">Severity threshold</label>
|
||||
<label
|
||||
class="euiFormLabel"
|
||||
for="select_severity"
|
||||
i18n-id="xpack.ml.timeSeriesExplorer.severityThresholdLabel"
|
||||
i18n-default-message="Severity threshold"
|
||||
></label>
|
||||
<div class="euiFormControlLayout">
|
||||
<ml-select-severity id="select_severity" />
|
||||
</div>
|
||||
|
@ -159,7 +221,12 @@
|
|||
</div>
|
||||
<div class="euiFlexItem euiFlexItem--flexGrowZero" style="width:170px">
|
||||
<div class="euiFormRow" id="select_interval_control">
|
||||
<label class="euiFormLabel" for="select_interval">Interval</label>
|
||||
<label
|
||||
class="euiFormLabel"
|
||||
for="select_interval"
|
||||
i18n-id="xpack.ml.timeSeriesExplorer.intervalLabel"
|
||||
i18n-default-message="Interval"
|
||||
></label>
|
||||
<div class="euiFormControlLayout">
|
||||
<ml-select-interval id="select_interval" />
|
||||
</div>
|
||||
|
|
|
@ -81,7 +81,8 @@ module.controller('MlTimeSeriesExplorerController', function (
|
|||
AppState,
|
||||
config,
|
||||
mlSelectIntervalService,
|
||||
mlSelectSeverityService) {
|
||||
mlSelectSeverityService,
|
||||
i18n) {
|
||||
|
||||
$scope.timeFieldName = 'timestamp';
|
||||
timefilter.enableTimeRangeSelector();
|
||||
|
@ -143,10 +144,17 @@ module.controller('MlTimeSeriesExplorerController', function (
|
|||
const invalidIds = _.difference(selectedJobIds, timeSeriesJobIds);
|
||||
selectedJobIds = _.without(selectedJobIds, ...invalidIds);
|
||||
if (invalidIds.length > 0) {
|
||||
const s = invalidIds.length === 1 ? '' : 's';
|
||||
let warningText = `You can't view requested job${s} ${invalidIds} in this dashboard`;
|
||||
let warningText = i18n('xpack.ml.timeSeriesExplorer.canNotViewRequestedJobsWarningMessage', {
|
||||
defaultMessage: `You can't view requested {invalidIdsCount, plural, one {job} other {jobs}} {invalidIds} in this dashboard`,
|
||||
values: {
|
||||
invalidIdsCount: invalidIds.length,
|
||||
invalidIds
|
||||
}
|
||||
});
|
||||
if (selectedJobIds.length === 0 && timeSeriesJobIds.length > 0) {
|
||||
warningText += ', auto selecting first job';
|
||||
warningText += i18n('xpack.ml.timeSeriesExplorer.autoSelectingFirstJobText', {
|
||||
defaultMessage: ', auto selecting first job'
|
||||
});
|
||||
}
|
||||
toastNotifications.addWarning(warningText);
|
||||
}
|
||||
|
@ -155,13 +163,21 @@ module.controller('MlTimeSeriesExplorerController', function (
|
|||
// if more than one job or a group has been loaded from the URL
|
||||
if (selectedJobIds.length > 1) {
|
||||
// if more than one job, select the first job from the selection.
|
||||
toastNotifications.addWarning('You can only view one job at a time in this dashboard');
|
||||
toastNotifications.addWarning(
|
||||
i18n('xpack.ml.timeSeriesExplorer.youCanViewOneJobAtTimeWarningMessage', {
|
||||
defaultMessage: 'You can only view one job at a time in this dashboard'
|
||||
})
|
||||
);
|
||||
mlJobSelectService.setJobIds([selectedJobIds[0]]);
|
||||
} else {
|
||||
// if a group has been loaded
|
||||
if (selectedJobIds.length > 0) {
|
||||
// if the group contains valid jobs, select the first
|
||||
toastNotifications.addWarning('You can only view one job at a time in this dashboard');
|
||||
toastNotifications.addWarning(
|
||||
i18n('xpack.ml.timeSeriesExplorer.youCanViewOneJobAtTimeWarningMessage', {
|
||||
defaultMessage: 'You can only view one job at a time in this dashboard'
|
||||
})
|
||||
);
|
||||
mlJobSelectService.setJobIds([selectedJobIds[0]]);
|
||||
} else if ($scope.jobs.length > 0) {
|
||||
// if there are no valid jobs in the group but there are valid jobs
|
||||
|
@ -714,7 +730,13 @@ module.controller('MlTimeSeriesExplorerController', function (
|
|||
const appStateDtrIdx = $scope.appState.mlTimeSeriesExplorer.detectorIndex;
|
||||
let detectorIndex = appStateDtrIdx !== undefined ? appStateDtrIdx : +(viewableDetectors[0].index);
|
||||
if (_.find(viewableDetectors, { 'index': '' + detectorIndex }) === undefined) {
|
||||
const warningText = `Requested detector index ${detectorIndex} is not valid for job ${$scope.selectedJob.job_id}`;
|
||||
const warningText = i18n('xpack.ml.timeSeriesExplorer.requestedDetectorIndexNotValidWarningMessage', {
|
||||
defaultMessage: 'Requested detector index {detectorIndex} is not valid for job {jobId}',
|
||||
values: {
|
||||
detectorIndex,
|
||||
jobId: $scope.selectedJob.job_id
|
||||
}
|
||||
});
|
||||
toastNotifications.addWarning(warningText);
|
||||
detectorIndex = +(viewableDetectors[0].index);
|
||||
$scope.appState.mlTimeSeriesExplorer.detectorIndex = detectorIndex;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue