From 858adac0d2f0c7368466f51e4a5bd876aaaadb64 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Wed, 3 Jun 2020 22:31:30 +0100 Subject: [PATCH 01/18] [ML] Model snapshot management --- .../types/anomaly_detection_jobs/index.ts | 1 + .../anomaly_detection_jobs/model_snapshot.ts | 21 ++ .../edit_model_snapshot_flyout.tsx | 190 ++++++++++++++++ .../edit_model_snapshot_flyout/index.ts | 7 + .../components/model_snapshots/index.ts | 7 + .../model_snapshots/model_snapshots_table.tsx | 202 ++++++++++++++++++ .../revert_model_snapshot_flyout/index.ts | 7 + .../revert_model_snapshot_flyout.tsx | 5 + .../components/job_details/job_details.js | 15 ++ .../services/ml_api_service/index.ts | 47 ++++ .../services/ml_api_service/jobs.ts | 8 +- .../ml/server/client/elasticsearch_ml.ts | 88 ++++++++ .../ml/server/routes/anomaly_detectors.ts | 146 ++++++++++++- .../schemas/anomaly_detectors_schema.ts | 14 ++ 14 files changed, 752 insertions(+), 6 deletions(-) create mode 100644 x-pack/plugins/ml/common/types/anomaly_detection_jobs/model_snapshot.ts create mode 100644 x-pack/plugins/ml/public/application/components/model_snapshots/edit_model_snapshot_flyout/edit_model_snapshot_flyout.tsx create mode 100644 x-pack/plugins/ml/public/application/components/model_snapshots/edit_model_snapshot_flyout/index.ts create mode 100644 x-pack/plugins/ml/public/application/components/model_snapshots/index.ts create mode 100644 x-pack/plugins/ml/public/application/components/model_snapshots/model_snapshots_table.tsx create mode 100644 x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/index.ts create mode 100644 x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx diff --git a/x-pack/plugins/ml/common/types/anomaly_detection_jobs/index.ts b/x-pack/plugins/ml/common/types/anomaly_detection_jobs/index.ts index 9c299c628426a..deb41fbb832cd 100644 --- a/x-pack/plugins/ml/common/types/anomaly_detection_jobs/index.ts +++ b/x-pack/plugins/ml/common/types/anomaly_detection_jobs/index.ts @@ -10,3 +10,4 @@ export * from './datafeed'; export * from './datafeed_stats'; export * from './combined_job'; export * from './summary_job'; +export * from './model_snapshot'; diff --git a/x-pack/plugins/ml/common/types/anomaly_detection_jobs/model_snapshot.ts b/x-pack/plugins/ml/common/types/anomaly_detection_jobs/model_snapshot.ts new file mode 100644 index 0000000000000..367a16965a90b --- /dev/null +++ b/x-pack/plugins/ml/common/types/anomaly_detection_jobs/model_snapshot.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { JobId } from './job'; +import { ModelSizeStats } from './job_stats'; + +export interface ModelSnapshot { + job_id: JobId; + min_version: string; + timestamp: number; + description: string; + snapshot_id: string; + snapshot_doc_count: number; + model_size_stats: ModelSizeStats; + latest_record_time_stamp: number; + latest_result_time_stamp: number; + retain: boolean; +} diff --git a/x-pack/plugins/ml/public/application/components/model_snapshots/edit_model_snapshot_flyout/edit_model_snapshot_flyout.tsx b/x-pack/plugins/ml/public/application/components/model_snapshots/edit_model_snapshot_flyout/edit_model_snapshot_flyout.tsx new file mode 100644 index 0000000000000..e88ea19084ae2 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/model_snapshots/edit_model_snapshot_flyout/edit_model_snapshot_flyout.tsx @@ -0,0 +1,190 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiFlyout, + EuiFlyoutFooter, + EuiFlexGroup, + EuiFlexItem, + EuiButton, + EuiButtonEmpty, + EuiTitle, + EuiFlyoutBody, + EuiSpacer, + EuiTextArea, + EuiFormRow, + EuiSwitch, + EuiConfirmModal, + EuiOverlayMask, + EuiCallOut, +} from '@elastic/eui'; + +import { + ModelSnapshot, + CombinedJobWithStats, +} from '../../../../../common/types/anomaly_detection_jobs'; +import { ml } from '../../../services/ml_api_service'; +import { useNotifications } from '../../../contexts/kibana'; + +interface Props { + snapshot: ModelSnapshot; + job: CombinedJobWithStats; + closeFlyout(reload: boolean): void; +} + +export const EditModelSnapshotFlyout: FC = ({ snapshot, job, closeFlyout }) => { + const { toasts } = useNotifications(); + const [description, setDescription] = useState(snapshot.description); + const [retain, setRetain] = useState(snapshot.retain); + const [deleteModalVisible, setDeleteModalVisible] = useState(false); + const isCurrentSnapshot = snapshot.snapshot_id === job.model_snapshot_id; + + async function updateSnapshot() { + try { + await ml.updateModelSnapshot(snapshot.job_id, snapshot.snapshot_id, { + description, + retain, + }); + closeWithReload(); + } catch (error) { + toasts.addError(new Error(error.body.message), { + title: i18n.translate('xpack.ml.newJob.wizard.editModelSnapshotFlyout.saveErrorTitle', { + defaultMessage: 'Model snapshot update failed', + }), + }); + } + } + + async function deleteSnapshot() { + try { + await ml.deleteModelSnapshot(snapshot.job_id, snapshot.snapshot_id); + hideDeleteModal(); + closeWithReload(); + } catch (error) { + toasts.addError(new Error(error.body.message), { + title: i18n.translate('xpack.ml.newJob.wizard.editModelSnapshotFlyout.saveErrorTitle', { + defaultMessage: 'Model snapshot deletion failed', + }), + }); + } + } + + function closeWithReload() { + closeFlyout(true); + } + function closeWithoutReload() { + closeFlyout(false); + } + function showDeleteModal() { + setDeleteModalVisible(true); + } + function hideDeleteModal() { + setDeleteModalVisible(false); + } + + return ( + <> + + + + +
+ +
+
+ + {isCurrentSnapshot && ( + <> + + + This is the current snapshot being used by job THING and so cannot be deleted. + + + )} + + + + + setDescription(e.target.value)} + /> + + + + setRetain(e.target.checked)} + /> + +
+
+ + + + + + + + + + + + + + + + + + + + +
+ + {deleteModalVisible && ( + + + + )} + + ); +}; diff --git a/x-pack/plugins/ml/public/application/components/model_snapshots/edit_model_snapshot_flyout/index.ts b/x-pack/plugins/ml/public/application/components/model_snapshots/edit_model_snapshot_flyout/index.ts new file mode 100644 index 0000000000000..fcb534620e438 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/model_snapshots/edit_model_snapshot_flyout/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { EditModelSnapshotFlyout } from './edit_model_snapshot_flyout'; diff --git a/x-pack/plugins/ml/public/application/components/model_snapshots/index.ts b/x-pack/plugins/ml/public/application/components/model_snapshots/index.ts new file mode 100644 index 0000000000000..e16d69ea3eb83 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/model_snapshots/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { ModelSnapshotTable } from './model_snapshots_table'; diff --git a/x-pack/plugins/ml/public/application/components/model_snapshots/model_snapshots_table.tsx b/x-pack/plugins/ml/public/application/components/model_snapshots/model_snapshots_table.tsx new file mode 100644 index 0000000000000..b240e2a3c7d7f --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/model_snapshots/model_snapshots_table.tsx @@ -0,0 +1,202 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, useEffect, useState } from 'react'; +import { i18n } from '@kbn/i18n'; + +// @ts-ignore +import { formatDate } from '@elastic/eui/lib/services/format'; + +import { + // EuiBadge, + // EuiButtonIcon, + // EuiCallOut, + EuiFlexGroup, + EuiFlexItem, + EuiInMemoryTable, + // EuiLink, + EuiLoadingSpinner, + // EuiToolTip, +} from '@elastic/eui'; + +import { EditModelSnapshotFlyout } from './edit_model_snapshot_flyout'; +import { ml } from '../../services/ml_api_service'; +import { JOB_STATE, DATAFEED_STATE } from '../../../../common/constants/states'; +import { + ModelSnapshot, + CombinedJobWithStats, +} from '../../../../common/types/anomaly_detection_jobs'; + +const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'; + +interface Props { + job: CombinedJobWithStats; +} + +export const ModelSnapshotTable: FC = ({ job }) => { + const [snapshots, setSnapshots] = useState([]); + const [snapshotsLoaded, setSnapshotsLoaded] = useState(false); + const [editSnapshot, setEditSnapshot] = useState(null); + + useEffect(() => { + loadModelSnapshots(); + }, []); + + async function loadModelSnapshots() { + const { model_snapshots: ms } = await ml.getModelSnapshots(job.job_id); + setSnapshots(ms); + setSnapshotsLoaded(true); + } + + async function isJobOk() { + const gg = await canJobBeReverted(job.job_id); + if (gg) { + // console.log('ok!!'); + } else { + // console.log('not ok!!'); + } + } + + function renderDate(date: number) { + return formatDate(date, TIME_FORMAT); + } + + const columns = [ + { + field: 'snapshot_id', + name: i18n.translate('xpack.ml.modelSnapshotTable.id', { + defaultMessage: 'ID', + }), + sortable: true, + // width: '50%', + // scope: 'row', + }, + { + field: 'description', + name: i18n.translate('xpack.ml.modelSnapshotTable.description', { + defaultMessage: 'Description', + }), + sortable: true, + }, + { + field: 'timestamp', + name: i18n.translate('xpack.ml.modelSnapshotTable.time', { + defaultMessage: 'Date', + }), + dataType: 'date', + render: renderDate, + sortable: true, + }, + { + field: 'retain', + name: i18n.translate('xpack.ml.modelSnapshotTable.retain', { + defaultMessage: 'Retain', + }), + width: '100px', + sortable: true, + }, + { + field: '', + width: '100px', + name: i18n.translate('xpack.ml.modelSnapshotTable.actions', { + defaultMessage: 'Actions', + }), + actions: [ + { + name: i18n.translate('xpack.ml.modelSnapshotTable.actions.revert.name', { + defaultMessage: 'Revert', + }), + description: i18n.translate('xpack.ml.modelSnapshotTable.actions.revert.description', { + defaultMessage: 'Revert this snapshot', + }), + type: 'icon', + icon: 'crosshairs', + onClick: isJobOk, + }, + { + name: i18n.translate('xpack.ml.modelSnapshotTable.actions.edit.name', { + defaultMessage: 'Edit', + }), + description: i18n.translate('xpack.ml.modelSnapshotTable.actions.edit.description', { + defaultMessage: 'Edit this snapshot', + }), + type: 'icon', + icon: 'pencil', + onClick: (a: any) => { + setEditSnapshot(a); + }, + }, + // { + // name: i18n.translate('xpack.ml.modelSnapshotTable.actions.delete.name', { + // defaultMessage: 'Delete', + // }), + // description: i18n.translate('xpack.ml.modelSnapshotTable.actions.delete.description', { + // defaultMessage: 'Delete this snapshot', + // }), + // type: 'icon', + // icon: 'trash', + // onClick: (a) => { + // console.log(a); + // }, + // }, + ], + }, + ]; + + if (snapshotsLoaded === false) { + return ( + <> + + + + + + + ); + } + + return ( + <> + + {editSnapshot !== null && ( + { + setEditSnapshot(null); + if (reload) { + loadModelSnapshots(); + } + }} + /> + )} + + ); +}; + +async function canJobBeReverted(jobId: string) { + const jobs = await ml.jobs.jobs([jobId]); + return ( + jobs.length === 1 && + jobs[0].state === JOB_STATE.CLOSED && + jobs[0].datafeed_config.state === DATAFEED_STATE.STOPPED + ); +} diff --git a/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/index.ts b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/index.ts new file mode 100644 index 0000000000000..7aa3c1b41cced --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export {} from './revert_model_snapshot_flyout'; diff --git a/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx new file mode 100644 index 0000000000000..41bc2aa258807 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx @@ -0,0 +1,5 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details.js index 56da4f1e0ff84..123343c2a4bbd 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details.js @@ -14,6 +14,7 @@ import { JsonPane } from './json_tab'; import { DatafeedPreviewPane } from './datafeed_preview_tab'; import { AnnotationsTable } from '../../../../components/annotations/annotations_table'; 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'; @@ -175,6 +176,20 @@ export class JobDetails extends Component { ), }); + + tabs.push({ + id: 'modelSnapshots', + 'data-test-subj': 'mlJobListTab-modelSnapshots', + name: i18n.translate('xpack.ml.jobsList.jobDetails.tabs.modelSnapshotsLabel', { + defaultMessage: 'Model snapshots', + }), + content: ( + + + {/* */} + + ), + }); } return ( diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts index 6e3fd08e90e38..fdaa3c2ffe79e 100644 --- a/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts +++ b/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts @@ -23,6 +23,7 @@ import { CombinedJob, Detector, AnalysisConfig, + ModelSnapshot, } from '../../../../common/types/anomaly_detection_jobs'; import { ES_AGGREGATION } from '../../../../common/constants/aggregation_types'; import { FieldRequestConfig } from '../../datavisualizer/index_based/common'; @@ -77,6 +78,11 @@ export interface CardinalityModelPlotHigh { export type CardinalityValidationResult = SuccessCardinality | CardinalityModelPlotHigh; export type CardinalityValidationResults = CardinalityValidationResult[]; +export interface GetModelSnapshotsResponse { + count: number; + model_snapshots: ModelSnapshot[]; +} + export function basePath() { return '/api/ml'; } @@ -119,6 +125,13 @@ export const ml = { }); }, + forceCloseJob({ jobId }: { jobId: string }) { + return http({ + path: `${basePath()}/anomaly_detectors/${jobId}/_close?force=true`, + method: 'POST', + }); + }, + deleteJob({ jobId }: { jobId: string }) { return http({ path: `${basePath()}/anomaly_detectors/${jobId}`, @@ -242,6 +255,13 @@ export const ml = { }); }, + forceStopDatafeed({ datafeedId }: { datafeedId: string }) { + return http({ + path: `${basePath()}/datafeeds/${datafeedId}/_stop?force=true`, + method: 'POST', + }); + }, + datafeedPreview({ datafeedId }: { datafeedId: string }) { return http({ path: `${basePath()}/datafeeds/${datafeedId}/_preview`, @@ -640,6 +660,33 @@ export const ml = { }); }, + getModelSnapshots(jobId: string, snapshotId?: string) { + return http({ + path: `${basePath()}/anomaly_detectors/${jobId}/model_snapshots${ + snapshotId !== undefined ? `/${snapshotId}` : '' + }`, + }); + }, + + updateModelSnapshot( + jobId: string, + snapshotId: string, + body: { description?: string; retain?: boolean } + ) { + return http({ + path: `${basePath()}/anomaly_detectors/${jobId}/model_snapshots/${snapshotId}/_update`, + method: 'POST', + body: JSON.stringify(body), + }); + }, + + deleteModelSnapshot(jobId: string, snapshotId: string) { + return http({ + path: `${basePath()}/anomaly_detectors/${jobId}/model_snapshots/${snapshotId}`, + method: 'DELETE', + }); + }, + annotations, dataFrameAnalytics, filters, diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts index e2569f6217b34..19942f6a664ef 100644 --- a/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts +++ b/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts @@ -8,7 +8,11 @@ import { http } from '../http_service'; import { basePath } from './index'; import { Dictionary } from '../../../../common/types/common'; -import { MlJobWithTimeRange, MlSummaryJobs } from '../../../../common/types/anomaly_detection_jobs'; +import { + MlJobWithTimeRange, + MlSummaryJobs, + CombinedJobWithStats, +} from '../../../../common/types/anomaly_detection_jobs'; import { JobMessage } from '../../../../common/types/audit_message'; import { AggFieldNamePair } from '../../../../common/types/fields'; import { ExistingJobsAndGroups } from '../job_service'; @@ -41,7 +45,7 @@ export const jobs = { jobs(jobIds: string[]) { const body = JSON.stringify({ jobIds }); - return http({ + return http({ path: `${basePath()}/jobs/jobs`, method: 'POST', body, diff --git a/x-pack/plugins/ml/server/client/elasticsearch_ml.ts b/x-pack/plugins/ml/server/client/elasticsearch_ml.ts index d5c7882a30d20..07159534e1e2c 100644 --- a/x-pack/plugins/ml/server/client/elasticsearch_ml.ts +++ b/x-pack/plugins/ml/server/client/elasticsearch_ml.ts @@ -393,6 +393,17 @@ export const elasticsearchJsPlugin = (Client: any, config: any, components: any) ml.stopDatafeed = ca({ urls: [ + { + fmt: '/_ml/datafeeds/<%=datafeedId%>/_stop?force=<%=force%>', + req: { + datafeedId: { + type: 'string', + }, + force: { + type: 'boolean', + }, + }, + }, { fmt: '/_ml/datafeeds/<%=datafeedId%>/_stop', req: { @@ -823,4 +834,81 @@ export const elasticsearchJsPlugin = (Client: any, config: any, components: any) ], method: 'GET', }); + + ml.modelSnapshots = ca({ + urls: [ + { + fmt: '/_xpack/ml/anomaly_detectors/<%=jobId%>/model_snapshots/<%=snapshotId%>', + req: { + jobId: { + type: 'string', + }, + snapshotId: { + type: 'string', + }, + }, + }, + { + fmt: '/_xpack/ml/anomaly_detectors/<%=jobId%>/model_snapshots', + req: { + jobId: { + type: 'string', + }, + }, + }, + ], + method: 'GET', + }); + + ml.updateModelSnapshot = ca({ + urls: [ + { + fmt: '/_xpack/ml/anomaly_detectors/<%=jobId%>/model_snapshots/<%=snapshotId%>/_update', + req: { + jobId: { + type: 'string', + }, + snapshotId: { + type: 'string', + }, + }, + }, + ], + method: 'POST', + needBody: true, + }); + + ml.deleteModelSnapshot = ca({ + urls: [ + { + fmt: '/_xpack/ml/anomaly_detectors/<%=jobId%>/model_snapshots/<%=snapshotId%>', + req: { + jobId: { + type: 'string', + }, + snapshotId: { + type: 'string', + }, + }, + }, + ], + method: 'DELETE', + }); + + ml.revertModelSnapshot = ca({ + urls: [ + { + fmt: '/_xpack/ml/anomaly_detectors/<%=jobId%>/model_snapshots/<%=snapshotId%>/_revert', + req: { + jobId: { + type: 'string', + }, + snapshotId: { + type: 'string', + }, + }, + }, + ], + method: 'POST', + }); }; diff --git a/x-pack/plugins/ml/server/routes/anomaly_detectors.ts b/x-pack/plugins/ml/server/routes/anomaly_detectors.ts index 63cd5498231af..bec2bd5997394 100644 --- a/x-pack/plugins/ml/server/routes/anomaly_detectors.ts +++ b/x-pack/plugins/ml/server/routes/anomaly_detectors.ts @@ -17,6 +17,8 @@ import { getCategoriesSchema, forecastAnomalyDetector, getBucketParamsSchema, + getModelSnapshotsSchema, + updateModelSnapshotSchema, } from './schemas/anomaly_detectors_schema'; /** @@ -526,7 +528,7 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { /** * @apiGroup AnomalyDetectors * - * @api {get} /api/ml/anomaly_detectors/:jobId/results/categories/:categoryId Get results category data by job id and category id + * @api {get} /api/ml/anomaly_detectors/:jobId/results/categories/:categoryId Get results category data by job ID and category ID * @apiName GetCategories * @apiDescription Returns the categories results for the specified job ID and category ID. * @@ -544,11 +546,147 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { - const options = { + const results = await context.ml!.mlClient.callAsCurrentUser('ml.categories', { jobId: request.params.jobId, categoryId: request.params.categoryId, - }; - const results = await context.ml!.mlClient.callAsCurrentUser('ml.categories', options); + }); + return response.ok({ + body: results, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup AnomalyDetectors + * + * @api {get} /api/ml/anomaly_detectors/:jobId/model_snapshots Get model snapshots by job ID + * @apiName GetModelSnapshots + * @apiDescription Returns the model snapshots for the specified job ID + * + * @apiSchema (params) getModelSnapshotsSchema + */ + router.get( + { + path: '/api/ml/anomaly_detectors/{jobId}/model_snapshots', + validate: { + params: getModelSnapshotsSchema, + }, + options: { + tags: ['access:ml:canGetJobs'], + }, + }, + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { + try { + const results = await context.ml!.mlClient.callAsCurrentUser('ml.modelSnapshots', { + jobId: request.params.jobId, + }); + return response.ok({ + body: results, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup AnomalyDetectors + * + * @api {get} /api/ml/anomaly_detectors/:jobId/model_snapshots/:snapshotId Get model snapshots by job ID and snapshot ID + * @apiName GetModelSnapshotsById + * @apiDescription Returns the model snapshots for the specified job ID and snapshot ID + * + * @apiSchema (params) getModelSnapshotsSchema + */ + router.get( + { + path: '/api/ml/anomaly_detectors/{jobId}/model_snapshots/{snapshotId}', + validate: { + params: getModelSnapshotsSchema, + }, + options: { + tags: ['access:ml:canGetJobs'], + }, + }, + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { + try { + const results = await context.ml!.mlClient.callAsCurrentUser('ml.modelSnapshots', { + jobId: request.params.jobId, + snapshotId: request.params.snapshotId, + }); + return response.ok({ + body: results, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup AnomalyDetectors + * + * @api {get} /api/ml/anomaly_detectors/:jobId/model_snapshots/:snapshotId Get model snapshots by job ID and snapshot ID + * @apiName GetModelSnapshotsById + * @apiDescription Returns the model snapshots for the specified job ID and snapshot ID + * + * @apiSchema (params) getModelSnapshotsSchema + */ + router.post( + { + path: '/api/ml/anomaly_detectors/{jobId}/model_snapshots/{snapshotId}/_update', + validate: { + params: getModelSnapshotsSchema, + body: updateModelSnapshotSchema, + }, + options: { + tags: ['access:ml:canCreateJob'], // TODO IS THIS CORRECT? + }, + }, + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { + try { + const results = await context.ml!.mlClient.callAsCurrentUser('ml.updateModelSnapshot', { + jobId: request.params.jobId, + snapshotId: request.params.snapshotId, + body: request.body, + }); + return response.ok({ + body: results, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup AnomalyDetectors + * + * @api {get} /api/ml/anomaly_detectors/:jobId/model_snapshots/:snapshotId Get model snapshots by job ID and snapshot ID + * @apiName GetModelSnapshotsById + * @apiDescription Returns the model snapshots for the specified job ID and snapshot ID + * + * @apiSchema (params) getModelSnapshotsSchema + */ + router.delete( + { + path: '/api/ml/anomaly_detectors/{jobId}/model_snapshots/{snapshotId}', + validate: { + params: getModelSnapshotsSchema, + }, + options: { + tags: ['access:ml:canCreateJob'], // TODO IS THIS CORRECT? + }, + }, + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { + try { + const results = await context.ml!.mlClient.callAsCurrentUser('ml.deleteModelSnapshot', { + jobId: request.params.jobId, + snapshotId: request.params.snapshotId, + }); return response.ok({ body: results, }); diff --git a/x-pack/plugins/ml/server/routes/schemas/anomaly_detectors_schema.ts b/x-pack/plugins/ml/server/routes/schemas/anomaly_detectors_schema.ts index 3a01a616a4f7c..acfcb82e4ff2c 100644 --- a/x-pack/plugins/ml/server/routes/schemas/anomaly_detectors_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/anomaly_detectors_schema.ts @@ -184,4 +184,18 @@ export const getCategoriesSchema = schema.object({ jobId: schema.string(), }); +export const getModelSnapshotsSchema = schema.object({ + /** Snapshot id */ + snapshotId: schema.maybe(schema.string()), + /** Job id */ + jobId: schema.string(), +}); + +export const updateModelSnapshotSchema = schema.object({ + /** description */ + description: schema.maybe(schema.string()), + /** retain */ + retain: schema.maybe(schema.boolean()), +}); + export const forecastAnomalyDetector = schema.object({ duration: schema.any() }); From 6da46e38575e5e73deab4341a372ef64831e3b42 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Fri, 5 Jun 2020 17:31:10 +0100 Subject: [PATCH 02/18] more updates --- .../edit_model_snapshot_flyout.tsx | 47 ++- .../model_snapshots/model_snapshots_table.tsx | 122 ++++++-- .../revert_model_snapshot_flyout/index.ts | 2 +- .../revert_model_snapshot_flyout.tsx | 271 ++++++++++++++++++ .../revert_model_snapshot_flyout/utils.ts | 62 ++++ .../event_rate_chart/event_rate_chart.tsx | 16 +- .../charts/event_rate_chart/overlay_range.tsx | 69 +++++ .../services/ml_api_service/jobs.ts | 25 ++ .../models/calendar/calendar_manager.ts | 26 +- .../server/models/calendar/event_manager.ts | 14 +- .../ml/server/models/calendar/index.ts | 1 + .../ml/server/models/job_service/datafeeds.ts | 4 +- .../ml/server/models/job_service/index.ts | 2 + .../ml/server/models/job_service/jobs.ts | 19 ++ .../models/job_service/model_snapshots.ts | 97 +++++++ .../plugins/ml/server/routes/job_service.ts | 78 +++++ .../routes/schemas/job_service_schema.ts | 14 + 17 files changed, 814 insertions(+), 55 deletions(-) create mode 100644 x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/utils.ts create mode 100644 x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/event_rate_chart/overlay_range.tsx create mode 100644 x-pack/plugins/ml/server/models/job_service/model_snapshots.ts diff --git a/x-pack/plugins/ml/public/application/components/model_snapshots/edit_model_snapshot_flyout/edit_model_snapshot_flyout.tsx b/x-pack/plugins/ml/public/application/components/model_snapshots/edit_model_snapshot_flyout/edit_model_snapshot_flyout.tsx index e88ea19084ae2..2b3d3d2ce359e 100644 --- a/x-pack/plugins/ml/public/application/components/model_snapshots/edit_model_snapshot_flyout/edit_model_snapshot_flyout.tsx +++ b/x-pack/plugins/ml/public/application/components/model_snapshots/edit_model_snapshot_flyout/edit_model_snapshot_flyout.tsx @@ -60,7 +60,7 @@ export const EditModelSnapshotFlyout: FC = ({ snapshot, job, closeFlyout closeWithReload(); } catch (error) { toasts.addError(new Error(error.body.message), { - title: i18n.translate('xpack.ml.newJob.wizard.editModelSnapshotFlyout.saveErrorTitle', { + title: i18n.translate('xpack.ml.editModelSnapshotFlyout.saveErrorTitle', { defaultMessage: 'Model snapshot update failed', }), }); @@ -74,7 +74,7 @@ export const EditModelSnapshotFlyout: FC = ({ snapshot, job, closeFlyout closeWithReload(); } catch (error) { toasts.addError(new Error(error.body.message), { - title: i18n.translate('xpack.ml.newJob.wizard.editModelSnapshotFlyout.saveErrorTitle', { + title: i18n.translate('xpack.ml.editModelSnapshotFlyout.saveErrorTitle', { defaultMessage: 'Model snapshot deletion failed', }), }); @@ -102,7 +102,7 @@ export const EditModelSnapshotFlyout: FC = ({ snapshot, job, closeFlyout
@@ -112,15 +112,28 @@ export const EditModelSnapshotFlyout: FC = ({ snapshot, job, closeFlyout {isCurrentSnapshot && ( <> - - This is the current snapshot being used by job THING and so cannot be deleted. + + )} - + = ({ snapshot, job, closeFlyout setRetain(e.target.checked)} /> @@ -142,7 +157,7 @@ export const EditModelSnapshotFlyout: FC = ({ snapshot, job, closeFlyout @@ -155,7 +170,7 @@ export const EditModelSnapshotFlyout: FC = ({ snapshot, job, closeFlyout disabled={isCurrentSnapshot === true} > @@ -163,7 +178,7 @@ export const EditModelSnapshotFlyout: FC = ({ snapshot, job, closeFlyout @@ -175,11 +190,17 @@ export const EditModelSnapshotFlyout: FC = ({ snapshot, job, closeFlyout {deleteModalVisible && ( diff --git a/x-pack/plugins/ml/public/application/components/model_snapshots/model_snapshots_table.tsx b/x-pack/plugins/ml/public/application/components/model_snapshots/model_snapshots_table.tsx index b240e2a3c7d7f..462536eb9368a 100644 --- a/x-pack/plugins/ml/public/application/components/model_snapshots/model_snapshots_table.tsx +++ b/x-pack/plugins/ml/public/application/components/model_snapshots/model_snapshots_table.tsx @@ -19,10 +19,13 @@ import { EuiInMemoryTable, // EuiLink, EuiLoadingSpinner, + EuiOverlayMask, + EuiConfirmModal, // EuiToolTip, } from '@elastic/eui'; import { EditModelSnapshotFlyout } from './edit_model_snapshot_flyout'; +import { RevertModelSnapshotFlyout } from './revert_model_snapshot_flyout'; import { ml } from '../../services/ml_api_service'; import { JOB_STATE, DATAFEED_STATE } from '../../../../common/constants/states'; import { @@ -36,10 +39,20 @@ interface Props { job: CombinedJobWithStats; } +enum COMBINED_JOB_STATE { + OPEN_AND_RUNNING, + OPEN_AND_STOPPED, + CLOSED, + UNKNOWN, +} + export const ModelSnapshotTable: FC = ({ job }) => { const [snapshots, setSnapshots] = useState([]); const [snapshotsLoaded, setSnapshotsLoaded] = useState(false); - const [editSnapshot, setEditSnapshot] = useState(null); + const [editSnapshot, setEditSnapshot] = useState(null); + const [revertSnapshot, setRevertSnapshot] = useState(null); + const [closeJobModalVisible, setCloseJobModalVisible] = useState(false); + const [combinedJobState, setCombinedJobState] = useState(null); useEffect(() => { loadModelSnapshots(); @@ -51,15 +64,35 @@ export const ModelSnapshotTable: FC = ({ job }) => { setSnapshotsLoaded(true); } - async function isJobOk() { - const gg = await canJobBeReverted(job.job_id); - if (gg) { - // console.log('ok!!'); + async function checkJobIsClosed(snapshot: ModelSnapshot) { + const state = await getCombinedJobState(job.job_id); + if (state === COMBINED_JOB_STATE.UNKNOWN) { + // show toast + return; + } + + if (state === COMBINED_JOB_STATE.CLOSED) { + // show flyout + setRevertSnapshot(snapshot); } else { - // console.log('not ok!!'); + setCombinedJobState(state); + showCloseJobModalVisible(); } } + function showCloseJobModalVisible() { + setCloseJobModalVisible(true); + } + function hideCloseJobModalVisible() { + setCombinedJobState(null); + setCloseJobModalVisible(false); + } + + async function forceCloseJob() { + ml.jobs.forceStopAndCloseJob(job.job_id); + hideCloseJobModalVisible(); + } + function renderDate(date: number) { return formatDate(date, TIME_FORMAT); } @@ -84,7 +117,16 @@ export const ModelSnapshotTable: FC = ({ job }) => { { field: 'timestamp', name: i18n.translate('xpack.ml.modelSnapshotTable.time', { - defaultMessage: 'Date', + defaultMessage: 'Date created', + }), + dataType: 'date', + render: renderDate, + sortable: true, + }, + { + field: 'latest_record_time_stamp', + name: i18n.translate('xpack.ml.modelSnapshotTable.latestTimestamp', { + defaultMessage: 'Latest timestamp', }), dataType: 'date', render: renderDate, @@ -114,7 +156,7 @@ export const ModelSnapshotTable: FC = ({ job }) => { }), type: 'icon', icon: 'crosshairs', - onClick: isJobOk, + onClick: checkJobIsClosed, }, { name: i18n.translate('xpack.ml.modelSnapshotTable.actions.edit.name', { @@ -125,9 +167,7 @@ export const ModelSnapshotTable: FC = ({ job }) => { }), type: 'icon', icon: 'pencil', - onClick: (a: any) => { - setEditSnapshot(a); - }, + onClick: setEditSnapshot, }, // { // name: i18n.translate('xpack.ml.modelSnapshotTable.actions.delete.name', { @@ -188,15 +228,63 @@ export const ModelSnapshotTable: FC = ({ job }) => { }} /> )} + + {revertSnapshot !== null && ( + { + setRevertSnapshot(null); + if (reload) { + loadModelSnapshots(); + } + }} + /> + )} + + {closeJobModalVisible && combinedJobState !== null && ( + + +

Close this job blah

+
+
+ )} ); }; -async function canJobBeReverted(jobId: string) { +async function getCombinedJobState(jobId: string) { const jobs = await ml.jobs.jobs([jobId]); - return ( - jobs.length === 1 && - jobs[0].state === JOB_STATE.CLOSED && - jobs[0].datafeed_config.state === DATAFEED_STATE.STOPPED - ); + + if (jobs.length !== 1) { + return COMBINED_JOB_STATE.UNKNOWN; + } + + if (jobs[0].state !== JOB_STATE.CLOSED) { + if (jobs[0].datafeed_config.state !== DATAFEED_STATE.STOPPED) { + return COMBINED_JOB_STATE.OPEN_AND_RUNNING; + } + return COMBINED_JOB_STATE.OPEN_AND_STOPPED; + } + return COMBINED_JOB_STATE.CLOSED; } diff --git a/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/index.ts b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/index.ts index 7aa3c1b41cced..33adc65a9e327 100644 --- a/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/index.ts +++ b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export {} from './revert_model_snapshot_flyout'; +export { RevertModelSnapshotFlyout } from './revert_model_snapshot_flyout'; diff --git a/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx index 41bc2aa258807..578090aac8848 100644 --- a/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx +++ b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx @@ -3,3 +3,274 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, useState, useEffect } from 'react'; +import { i18n } from '@kbn/i18n'; +import moment from 'moment'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiFlyout, + EuiFlyoutFooter, + EuiFlexGroup, + EuiFlexItem, + EuiButton, + EuiButtonEmpty, + EuiTitle, + EuiFlyoutBody, + EuiSpacer, + EuiTextArea, + EuiFormRow, + EuiCheckbox, + EuiConfirmModal, + EuiOverlayMask, + EuiCallOut, + EuiDatePicker, + EuiHorizontalRule, +} from '@elastic/eui'; + +import { + ModelSnapshot, + CombinedJobWithStats, +} from '../../../../../common/types/anomaly_detection_jobs'; +import { ml } from '../../../services/ml_api_service'; +import { useNotifications } from '../../../contexts/kibana'; +import { loadEventRateForJob, loadAnomalyDataForJob } from './utils'; +import { EventRateChart } from '../../../jobs/new_job/pages/components/charts/event_rate_chart/event_rate_chart'; +import { Anomaly } from '../../../jobs/new_job/common/results_loader/results_loader'; + +interface Props { + snapshot: ModelSnapshot; + snapshots: ModelSnapshot[]; + job: CombinedJobWithStats; + closeFlyout(reload: boolean): void; +} + +export const RevertModelSnapshotFlyout: FC = ({ snapshot, snapshots, job, closeFlyout }) => { + const { toasts } = useNotifications(); + const [currentSnapshot, setCurrentSnapshot] = useState(snapshot); + const [revertModalVisible, setRevertModalVisible] = useState(false); + const [replay, setReplay] = useState(true); + const [runInRealTime, setRunInRealTime] = useState(true); + const [createCalendar, setCreateCalendar] = useState(false); + const [startDate, setStartDate] = useState( + moment(snapshot.latest_record_time_stamp) + ); + const [endDate, setEndDate] = useState( + moment(job.data_counts.latest_record_timestamp) + ); + + const [eventRateData, setEventRateData] = useState([]); + const [anomalies, setAnomalies] = useState([]); + const [chartReady, setChartReady] = useState(false); + + useEffect(() => { + createChartData(); + }, []); + + async function createChartData() { + const d = await loadEventRateForJob(job, 100); + const a = await loadAnomalyDataForJob(job, 100); + setEventRateData(d); + setAnomalies(a[0]); + setChartReady(true); + } + + function closeWithReload() { + closeFlyout(true); + } + function closeWithoutReload() { + closeFlyout(false); + } + + function showRevertModal() { + setRevertModalVisible(true); + } + function hideRevertModal() { + setRevertModalVisible(false); + } + + async function revert() { + const end = + replay && runInRealTime === false ? job.data_counts.latest_record_timestamp : undefined; + try { + await ml.jobs.revertModelSnapshot( + job.job_id, + currentSnapshot.snapshot_id, + replay, + end, + createCalendar && startDate !== null && endDate !== null + ? { start: startDate.valueOf(), end: endDate.valueOf() } + : undefined + ); + hideRevertModal(); + closeWithReload(); + } catch (error) { + toasts.addError(new Error(error.body.message), { + title: i18n.translate('xpack.ml.revertModelSnapshotFlyout.revertErrorTitle', { + defaultMessage: 'Model snapshot revert failed', + }), + }); + } + } + + return ( + <> + + + +
+ +
+
+ + + + + + + + + + setReplay(e.target.checked)} + /> + + + setRunInRealTime(e.target.checked)} + /> + + + + setCreateCalendar(e.target.checked)} + /> + + + + + + + + setStartDate(d)} + /> + + + + + setEndDate(d)} + /> + + + +
+ + + + + + + + + + + + + + + + +
+ + {revertModalVisible && ( + + + + )} + + ); +}; diff --git a/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/utils.ts b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/utils.ts new file mode 100644 index 0000000000000..ab777a761e5c5 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/utils.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +// import { ChartLoader } from '../../../jobs/new_job/common/chart_loader'; +import { mlResultsService } from '../../../services/results_service'; +import { + // ModelSnapshot, + CombinedJobWithStats, +} from '../../../../../common/types/anomaly_detection_jobs'; +import { getSeverityType } from '../../../../../common/util/anomaly_utils'; +import { Anomaly } from '../../../jobs/new_job/common/results_loader/results_loader'; + +export async function loadEventRateForJob(job: CombinedJobWithStats, bars: number) { + const interval = Math.floor( + (job.data_counts.latest_record_timestamp - job.data_counts.earliest_record_timestamp) / bars + ); + const resp = await mlResultsService.getEventRateData( + job.datafeed_config.indices.join(), + job.datafeed_config.query, + job.data_description.time_field, + job.data_counts.earliest_record_timestamp, + job.data_counts.latest_record_timestamp, + interval + ); + if (resp.error !== undefined) { + throw resp.error; + } + + return Object.entries(resp.results).map(([time, value]) => ({ + time: +time, + value: value as number, + })); +} + +export async function loadAnomalyDataForJob(job: CombinedJobWithStats, bars: number) { + const interval = Math.floor( + (job.data_counts.latest_record_timestamp - job.data_counts.earliest_record_timestamp) / bars + ); + + const resp = await mlResultsService.getScoresByBucket( + [job.job_id], + job.data_counts.earliest_record_timestamp, + job.data_counts.latest_record_timestamp, + interval, + 1 + ); + + const results = resp.results[job.job_id]; + if (results === undefined) { + return []; + } + + const anomalies: Record = {}; + anomalies[0] = Object.entries(results).map( + ([time, value]) => + ({ time: +time, value, severity: getSeverityType(value as number) } as Anomaly) + ); + return anomalies; +} diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/event_rate_chart/event_rate_chart.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/event_rate_chart/event_rate_chart.tsx index 2fb8ea2820b29..5af319be7a161 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/event_rate_chart/event_rate_chart.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/event_rate_chart/event_rate_chart.tsx @@ -12,6 +12,7 @@ import { Anomaly } from '../../../../common/results_loader'; import { useChartColors } from '../common/settings'; import { LoadingWrapper } from '../loading_wrapper'; import { Anomalies } from '../common/anomalies'; +import { OverlayRange } from './overlay_range'; interface Props { eventRateChartData: LineChartPoint[]; @@ -21,6 +22,10 @@ interface Props { showAxis?: boolean; loading?: boolean; fadeChart?: boolean; + overlayRange?: { + start: number; + end: number; + }; } export const EventRateChart: FC = ({ @@ -31,6 +36,7 @@ export const EventRateChart: FC = ({ showAxis, loading = false, fadeChart, + overlayRange, }) => { const { EVENT_RATE_COLOR_WITH_ANOMALIES, EVENT_RATE_COLOR } = useChartColors(); const barColor = fadeChart ? EVENT_RATE_COLOR_WITH_ANOMALIES : EVENT_RATE_COLOR; @@ -43,8 +49,16 @@ export const EventRateChart: FC = ({ 0} loading={loading}> {showAxis === true && } - + + {overlayRange && ( + + )} + = ({ eventRateChartData, start, end }) => { + const maxHeight = Math.max(...eventRateChartData.map((e) => e.value)); + + return ( + <> + + +
+
+ +
+
+ {formatDate(start, TIME_FORMAT)} +
+
+ + } + /> + + ); +}; diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts index 19942f6a664ef..bccb796993656 100644 --- a/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts +++ b/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts @@ -99,6 +99,7 @@ export const jobs = { body, }); }, + closeJobs(jobIds: string[]) { const body = JSON.stringify({ jobIds }); return http({ @@ -108,6 +109,15 @@ export const jobs = { }); }, + forceStopAndCloseJob(jobId: string) { + const body = JSON.stringify({ jobId }); + return http({ + path: `${basePath()}/jobs/force_stop_and_close_job`, + method: 'POST', + body, + }); + }, + jobAuditMessages(jobId: string, from?: number) { const jobIdString = jobId !== undefined ? `/${jobId}` : ''; const query = from !== undefined ? { from } : {}; @@ -259,4 +269,19 @@ export const jobs = { body, }); }, + + revertModelSnapshot( + jobId: string, + snapshotId: string, + replay: boolean, + end?: number, + skip?: { start: number; end: number } + ) { + const body = JSON.stringify({ jobId, snapshotId, replay, end, skip }); + return http<{ total: number; categories: Array<{ count?: number; category: Category }> }>({ + path: `${basePath()}/jobs/revert_model_snapshot`, + method: 'POST', + body, + }); + }, }; diff --git a/x-pack/plugins/ml/server/models/calendar/calendar_manager.ts b/x-pack/plugins/ml/server/models/calendar/calendar_manager.ts index acb1bed6a37c0..2eec704f1e784 100644 --- a/x-pack/plugins/ml/server/models/calendar/calendar_manager.ts +++ b/x-pack/plugins/ml/server/models/calendar/calendar_manager.ts @@ -5,7 +5,7 @@ */ import { difference } from 'lodash'; -import { IScopedClusterClient } from 'kibana/server'; +import { APICaller } from 'kibana/server'; import { EventManager, CalendarEvent } from './event_manager'; interface BasicCalendar { @@ -23,16 +23,16 @@ export interface FormCalendar extends BasicCalendar { } export class CalendarManager { - private _client: IScopedClusterClient['callAsCurrentUser']; - private _eventManager: any; + private _callAsCurrentUser: APICaller; + private _eventManager: EventManager; - constructor(client: any) { - this._client = client; - this._eventManager = new EventManager(client); + constructor(callAsCurrentUser: APICaller) { + this._callAsCurrentUser = callAsCurrentUser; + this._eventManager = new EventManager(callAsCurrentUser); } async getCalendar(calendarId: string) { - const resp = await this._client('ml.calendars', { + const resp = await this._callAsCurrentUser('ml.calendars', { calendarId, }); @@ -43,7 +43,7 @@ export class CalendarManager { } async getAllCalendars() { - const calendarsResp = await this._client('ml.calendars'); + const calendarsResp = await this._callAsCurrentUser('ml.calendars'); const events: CalendarEvent[] = await this._eventManager.getAllEvents(); const calendars: Calendar[] = calendarsResp.calendars; @@ -74,7 +74,7 @@ export class CalendarManager { const events = calendar.events; delete calendar.calendarId; delete calendar.events; - await this._client('ml.addCalendar', { + await this._callAsCurrentUser('ml.addCalendar', { calendarId, body: calendar, }); @@ -109,7 +109,7 @@ export class CalendarManager { // add all new jobs if (jobsToAdd.length) { - await this._client('ml.addJobToCalendar', { + await this._callAsCurrentUser('ml.addJobToCalendar', { calendarId, jobId: jobsToAdd.join(','), }); @@ -117,7 +117,7 @@ export class CalendarManager { // remove all removed jobs if (jobsToRemove.length) { - await this._client('ml.removeJobFromCalendar', { + await this._callAsCurrentUser('ml.removeJobFromCalendar', { calendarId, jobId: jobsToRemove.join(','), }); @@ -131,7 +131,7 @@ export class CalendarManager { // remove all removed events await Promise.all( eventsToRemove.map(async (event) => { - await this._eventManager.deleteEvent(calendarId, event.event_id); + await this._eventManager.deleteEvent(calendarId, event.event_id!); }) ); @@ -140,6 +140,6 @@ export class CalendarManager { } async deleteCalendar(calendarId: string) { - return this._client('ml.deleteCalendar', { calendarId }); + return this._callAsCurrentUser('ml.deleteCalendar', { calendarId }); } } diff --git a/x-pack/plugins/ml/server/models/calendar/event_manager.ts b/x-pack/plugins/ml/server/models/calendar/event_manager.ts index 41240e2695f6f..02da0718d83ae 100644 --- a/x-pack/plugins/ml/server/models/calendar/event_manager.ts +++ b/x-pack/plugins/ml/server/models/calendar/event_manager.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { APICaller } from 'kibana/server'; import { GLOBAL_CALENDAR } from '../../../common/constants/calendars'; export interface CalendarEvent { @@ -15,13 +16,10 @@ export interface CalendarEvent { } export class EventManager { - private _client: any; - constructor(client: any) { - this._client = client; - } + constructor(private _callAsCurrentUser: APICaller) {} async getCalendarEvents(calendarId: string) { - const resp = await this._client('ml.events', { calendarId }); + const resp = await this._callAsCurrentUser('ml.events', { calendarId }); return resp.events; } @@ -29,7 +27,7 @@ export class EventManager { // jobId is optional async getAllEvents(jobId?: string) { const calendarId = GLOBAL_CALENDAR; - const resp = await this._client('ml.events', { + const resp = await this._callAsCurrentUser('ml.events', { calendarId, jobId, }); @@ -40,14 +38,14 @@ export class EventManager { async addEvents(calendarId: string, events: CalendarEvent[]) { const body = { events }; - return await this._client('ml.addEvent', { + return await this._callAsCurrentUser('ml.addEvent', { calendarId, body, }); } async deleteEvent(calendarId: string, eventId: string) { - return this._client('ml.deleteEvent', { calendarId, eventId }); + return this._callAsCurrentUser('ml.deleteEvent', { calendarId, eventId }); } isEqual(ev1: CalendarEvent, ev2: CalendarEvent) { diff --git a/x-pack/plugins/ml/server/models/calendar/index.ts b/x-pack/plugins/ml/server/models/calendar/index.ts index 2364c3ac73811..1a35f9f13368e 100644 --- a/x-pack/plugins/ml/server/models/calendar/index.ts +++ b/x-pack/plugins/ml/server/models/calendar/index.ts @@ -5,3 +5,4 @@ */ export { CalendarManager, Calendar, FormCalendar } from './calendar_manager'; +export { CalendarEvent } from './event_manager'; diff --git a/x-pack/plugins/ml/server/models/job_service/datafeeds.ts b/x-pack/plugins/ml/server/models/job_service/datafeeds.ts index 4090a59c461da..f016898075918 100644 --- a/x-pack/plugins/ml/server/models/job_service/datafeeds.ts +++ b/x-pack/plugins/ml/server/models/job_service/datafeeds.ts @@ -27,7 +27,7 @@ interface Results { } export function datafeedsProvider(callAsCurrentUser: APICaller) { - async function forceStartDatafeeds(datafeedIds: string[], start: number, end: number) { + async function forceStartDatafeeds(datafeedIds: string[], start?: number, end?: number) { const jobIds = await getJobIdsByDatafeedId(); const doStartsCalled = datafeedIds.reduce((acc, cur) => { acc[cur] = false; @@ -96,7 +96,7 @@ export function datafeedsProvider(callAsCurrentUser: APICaller) { return opened; } - async function startDatafeed(datafeedId: string, start: number, end: number) { + async function startDatafeed(datafeedId: string, start?: number, end?: number) { return callAsCurrentUser('ml.startDatafeed', { datafeedId, start, end }); } diff --git a/x-pack/plugins/ml/server/models/job_service/index.ts b/x-pack/plugins/ml/server/models/job_service/index.ts index eb70a3ccecfc1..b6e87b98735ee 100644 --- a/x-pack/plugins/ml/server/models/job_service/index.ts +++ b/x-pack/plugins/ml/server/models/job_service/index.ts @@ -10,6 +10,7 @@ import { jobsProvider } from './jobs'; import { groupsProvider } from './groups'; import { newJobCapsProvider } from './new_job_caps'; import { newJobChartsProvider, topCategoriesProvider } from './new_job'; +import { modelSnapshotProvider } from './model_snapshots'; export function jobServiceProvider(callAsCurrentUser: APICaller) { return { @@ -19,5 +20,6 @@ export function jobServiceProvider(callAsCurrentUser: APICaller) { ...newJobCapsProvider(callAsCurrentUser), ...newJobChartsProvider(callAsCurrentUser), ...topCategoriesProvider(callAsCurrentUser), + ...modelSnapshotProvider(callAsCurrentUser), }; } diff --git a/x-pack/plugins/ml/server/models/job_service/jobs.ts b/x-pack/plugins/ml/server/models/job_service/jobs.ts index 5503169f2d371..852264b3d0337 100644 --- a/x-pack/plugins/ml/server/models/job_service/jobs.ts +++ b/x-pack/plugins/ml/server/models/job_service/jobs.ts @@ -6,6 +6,7 @@ import { i18n } from '@kbn/i18n'; import { uniq } from 'lodash'; +import Boom from 'boom'; import { APICaller } from 'kibana/server'; import { JOB_STATE, DATAFEED_STATE } from '../../../common/constants/states'; import { @@ -128,6 +129,23 @@ export function jobsProvider(callAsCurrentUser: APICaller) { return results; } + async function forceStopAndCloseJob(jobId: string) { + const datafeedIds = await getDatafeedIdsByJobId(); + const datafeedId = datafeedIds[jobId]; + if (datafeedId === undefined) { + throw Boom.notFound(`Cannot find datafeed for job ${jobId}`); + } + + const dfResult = await callAsCurrentUser('ml.stopDatafeed', { datafeedId, force: true }); + if (!dfResult || dfResult.stopped !== true) { + return { success: false }; + } + + await callAsCurrentUser('ml.closeJob', { jobId, force: true }); + + return { success: true }; + } + async function jobsSummary(jobIds: string[] = []) { const fullJobsList: CombinedJobWithStats[] = await createFullJobsList(); const fullJobsIds = fullJobsList.map((job) => job.job_id); @@ -472,6 +490,7 @@ export function jobsProvider(callAsCurrentUser: APICaller) { forceDeleteJob, deleteJobs, closeJobs, + forceStopAndCloseJob, jobsSummary, jobsWithTimerange, createFullJobsList, diff --git a/x-pack/plugins/ml/server/models/job_service/model_snapshots.ts b/x-pack/plugins/ml/server/models/job_service/model_snapshots.ts new file mode 100644 index 0000000000000..b827763b14cd6 --- /dev/null +++ b/x-pack/plugins/ml/server/models/job_service/model_snapshots.ts @@ -0,0 +1,97 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { uniq } from 'lodash'; +import Boom from 'boom'; +import { APICaller } from 'kibana/server'; +import { JOB_STATE, DATAFEED_STATE } from '../../../common/constants/states'; +import { + MlSummaryJob, + AuditMessage, + Job, + JobStats, + DatafeedWithStats, + CombinedJobWithStats, + ModelSnapshot, +} from '../../../common/types/anomaly_detection_jobs'; +import { datafeedsProvider, MlDatafeedsResponse, MlDatafeedsStatsResponse } from './datafeeds'; +import { jobsProvider, MlJobsResponse, MlJobsStatsResponse } from './jobs'; +import { FormCalendar, CalendarManager } from '../calendar'; + +export interface ModelSnapshotsResponse { + count: number; + model_snapshots: ModelSnapshot[]; +} +export interface RevertModelSnapshotResponse { + model: ModelSnapshot; +} + +export function modelSnapshotProvider(callAsCurrentUser: APICaller) { + const { forceStartDatafeeds, getDatafeedIdsByJobId } = datafeedsProvider(callAsCurrentUser); + // const { MlJobsResponse } = jobsProvider(callAsCurrentUser); + + async function revertModelSnapshot( + jobId: string, + snapshotId: string, + replay: boolean, + end: number, + deleteInterveningResults: boolean = true, + skip?: { start: number; end: number } + ) { + const job = await callAsCurrentUser('ml.jobs', { jobId: [jobId] }); + const jobStats = await callAsCurrentUser('ml.jobStats', { + jobId: [jobId], + }); + + const datafeedIds = await getDatafeedIdsByJobId(); + const datafeedId = datafeedIds[jobId]; + if (datafeedId === undefined) { + throw Boom.notFound(`Cannot find datafeed for job ${jobId}`); + } + const datafeed = await callAsCurrentUser('ml.datafeeds', { + datafeedId: [datafeedId], + }); + + if (skip !== undefined) { + const calendar: FormCalendar = { + calendarId: String(Date.now()), + job_ids: [jobId], + description: 'auto created', + events: [ + { + description: 'Auto created', + start_time: skip.start, + end_time: skip.end, + }, + ], + }; + const cm = new CalendarManager(callAsCurrentUser); + await cm.newCalendar(calendar); + } + + const snapshot = await callAsCurrentUser('ml.modelSnapshots', { + jobId, + snapshotId, + }); + + const resp = await callAsCurrentUser('ml.revertModelSnapshot', { + jobId, + snapshotId, + body: { + delete_intervening_results: deleteInterveningResults, + }, + }); + + if (replay && snapshot && snapshot.model_snapshots.length) { + forceStartDatafeeds([datafeedId], snapshot.model_snapshots[0].latest_record_time_stamp, end); + } + + return { success: true }; + } + + return { revertModelSnapshot }; +} diff --git a/x-pack/plugins/ml/server/routes/job_service.ts b/x-pack/plugins/ml/server/routes/job_service.ts index 05c44e1da9757..f32bcdd3d9e9e 100644 --- a/x-pack/plugins/ml/server/routes/job_service.ts +++ b/x-pack/plugins/ml/server/routes/job_service.ts @@ -17,8 +17,11 @@ import { lookBackProgressSchema, topCategoriesSchema, updateGroupsSchema, + revertModelSnapshotSchema, } from './schemas/job_service_schema'; +import { jobIdSchema } from './schemas/anomaly_detectors_schema'; + import { jobServiceProvider } from '../models/job_service'; import { categorizationExamplesProvider } from '../models/job_service/new_job'; @@ -162,6 +165,40 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { }) ); + /** + * @apiGroup JobService + * + * @api {post} /api/ml/jobs/close_jobs Close jobs + * @apiName CloseJobs + * @apiDescription Closes one or more anomaly detection jobs + * + * @apiSchema (body) jobIdsSchema + */ + router.post( + { + path: '/api/ml/jobs/force_stop_and_close_job', + validate: { + body: jobIdSchema, + }, + options: { + tags: ['access:ml:canCloseJob', 'access:ml:canStartStopDatafeed'], + }, + }, + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { + try { + const { forceStopAndCloseJob } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); + const { jobId } = request.body; + const resp = await forceStopAndCloseJob(jobId); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + /** * @apiGroup JobService * @@ -691,4 +728,45 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { } }) ); + + /** + * @apiGroup JobService + * + * @api {post} /api/ml/jobs/top_categories Get top categories + * @apiName TopCategories + * @apiDescription Returns list of top categories + * + * @apiSchema (body) topCategoriesSchema + */ + router.post( + { + path: '/api/ml/jobs/revert_model_snapshot', + validate: { + body: revertModelSnapshotSchema, + }, + options: { + tags: ['access:ml:canCreateJob', 'access:ml:canStartStopDatafeed'], + }, + }, + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { + try { + const { revertModelSnapshot } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); + const { jobId, snapshotId, replay, end, deleteInterveningResults, skip } = request.body; + const resp = await revertModelSnapshot( + jobId, + snapshotId, + replay, + end, + deleteInterveningResults, + skip + ); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); } diff --git a/x-pack/plugins/ml/server/routes/schemas/job_service_schema.ts b/x-pack/plugins/ml/server/routes/schemas/job_service_schema.ts index be107db9508fd..01f4920d795de 100644 --- a/x-pack/plugins/ml/server/routes/schemas/job_service_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/job_service_schema.ts @@ -66,3 +66,17 @@ export const updateGroupsSchema = { ) ), }; + +export const revertModelSnapshotSchema = schema.object({ + jobId: schema.string(), + snapshotId: schema.string(), + replay: schema.boolean(), + end: schema.maybe(schema.number()), + deleteInterveningResults: schema.maybe(schema.boolean()), + skip: schema.maybe( + schema.object({ + start: schema.number(), + end: schema.number(), + }) + ), +}); From 074af6573734c5e303a723e98369a269666132a1 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Mon, 8 Jun 2020 10:09:32 +0100 Subject: [PATCH 03/18] adding calendar range --- .../revert_model_snapshot_flyout.tsx | 134 +++++++++++++----- .../revert_model_snapshot_flyout/utils.ts | 30 ++-- .../components/job_details/job_details.js | 7 +- .../jobs_list_view/jobs_list_view.js | 6 +- .../event_rate_chart/event_rate_chart.tsx | 22 ++- .../charts/event_rate_chart/overlay_range.tsx | 32 +++-- 6 files changed, 169 insertions(+), 62 deletions(-) diff --git a/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx index 578090aac8848..77e00723cb9ac 100644 --- a/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx +++ b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx @@ -13,6 +13,9 @@ import React, { FC, useState, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import moment from 'moment'; +// @ts-ignore +import { formatDate } from '@elastic/eui/lib/services/format'; +import { XYBrushArea } from '@elastic/charts'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiFlyout, @@ -43,6 +46,9 @@ import { useNotifications } from '../../../contexts/kibana'; import { loadEventRateForJob, loadAnomalyDataForJob } from './utils'; import { EventRateChart } from '../../../jobs/new_job/pages/components/charts/event_rate_chart/event_rate_chart'; import { Anomaly } from '../../../jobs/new_job/common/results_loader/results_loader'; +import { parseInterval } from '../../../../../common/util/parse_interval'; + +const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'; interface Props { snapshot: ModelSnapshot; @@ -56,13 +62,15 @@ export const RevertModelSnapshotFlyout: FC = ({ snapshot, snapshots, job, const [currentSnapshot, setCurrentSnapshot] = useState(snapshot); const [revertModalVisible, setRevertModalVisible] = useState(false); const [replay, setReplay] = useState(true); - const [runInRealTime, setRunInRealTime] = useState(true); + const [runInRealTime, setRunInRealTime] = useState(false); const [createCalendar, setCreateCalendar] = useState(false); const [startDate, setStartDate] = useState( - moment(snapshot.latest_record_time_stamp) + // moment(snapshot.latest_record_time_stamp) + null ); const [endDate, setEndDate] = useState( - moment(job.data_counts.latest_record_timestamp) + // moment(job.data_counts.latest_record_timestamp) + null ); const [eventRateData, setEventRateData] = useState([]); @@ -74,11 +82,14 @@ export const RevertModelSnapshotFlyout: FC = ({ snapshot, snapshots, job, }, []); async function createChartData() { - const d = await loadEventRateForJob(job, 100); - const a = await loadAnomalyDataForJob(job, 100); + // setTimeout(async () => { + const bucketSpanMs = parseInterval(job.analysis_config.bucket_span)!.asMilliseconds(); + const d = await loadEventRateForJob(job, bucketSpanMs, 100); + const a = await loadAnomalyDataForJob(job, bucketSpanMs, 100); setEventRateData(d); setAnomalies(a[0]); setChartReady(true); + // }, 250); } function closeWithReload() { @@ -95,6 +106,13 @@ export const RevertModelSnapshotFlyout: FC = ({ snapshot, snapshots, job, setRevertModalVisible(false); } + function onBrushEnd({ x }: XYBrushArea) { + if (x && x.length === 2) { + setStartDate(moment(x[0])); + setEndDate(moment(x[1])); + } + } + async function revert() { const end = replay && runInRealTime === false ? job.data_counts.latest_record_timestamp : undefined; @@ -145,11 +163,31 @@ export const RevertModelSnapshotFlyout: FC = ({ snapshot, snapshots, job, overlayRange={{ start: snapshot.latest_record_time_stamp, end: job.data_counts.latest_record_timestamp, + color: '#ff0000', }} /> - + + + + + + + = ({ snapshot, snapshots, job, /> - + {createCalendar && replay && ( + <> + - - - - setStartDate(d)} - /> - - - - - setEndDate(d)} - /> - - - + + + + setStartDate(d)} + /> + + + + + setEndDate(d)} + /> + + + + + + + )} @@ -235,9 +295,13 @@ export const RevertModelSnapshotFlyout: FC = ({ snapshot, snapshots, job, - + diff --git a/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/utils.ts b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/utils.ts index ab777a761e5c5..15eaaa7746472 100644 --- a/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/utils.ts +++ b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/utils.ts @@ -13,9 +13,16 @@ import { import { getSeverityType } from '../../../../../common/util/anomaly_utils'; import { Anomaly } from '../../../jobs/new_job/common/results_loader/results_loader'; -export async function loadEventRateForJob(job: CombinedJobWithStats, bars: number) { - const interval = Math.floor( - (job.data_counts.latest_record_timestamp - job.data_counts.earliest_record_timestamp) / bars +export async function loadEventRateForJob( + job: CombinedJobWithStats, + bucketSpanMs: number, + bars: number +) { + const intervalMs = Math.max( + Math.floor( + (job.data_counts.latest_record_timestamp - job.data_counts.earliest_record_timestamp) / bars + ), + bucketSpanMs ); const resp = await mlResultsService.getEventRateData( job.datafeed_config.indices.join(), @@ -23,7 +30,7 @@ export async function loadEventRateForJob(job: CombinedJobWithStats, bars: numbe job.data_description.time_field, job.data_counts.earliest_record_timestamp, job.data_counts.latest_record_timestamp, - interval + intervalMs ); if (resp.error !== undefined) { throw resp.error; @@ -35,16 +42,23 @@ export async function loadEventRateForJob(job: CombinedJobWithStats, bars: numbe })); } -export async function loadAnomalyDataForJob(job: CombinedJobWithStats, bars: number) { - const interval = Math.floor( - (job.data_counts.latest_record_timestamp - job.data_counts.earliest_record_timestamp) / bars +export async function loadAnomalyDataForJob( + job: CombinedJobWithStats, + bucketSpanMs: number, + bars: number +) { + const intervalMs = Math.max( + Math.floor( + (job.data_counts.latest_record_timestamp - job.data_counts.earliest_record_timestamp) / bars + ), + bucketSpanMs ); const resp = await mlResultsService.getScoresByBucket( [job.job_id], job.data_counts.earliest_record_timestamp, job.data_counts.latest_record_timestamp, - interval, + intervalMs, 1 ); diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details.js index 123343c2a4bbd..8931fc15df55a 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details.js @@ -26,7 +26,7 @@ export class JobDetails extends Component { this.state = {}; if (this.props.addYourself) { - this.props.addYourself(props.jobId, this); + this.props.addYourself(props.jobId, (j) => this.updateJob(j)); } } @@ -34,9 +34,8 @@ export class JobDetails extends Component { this.props.removeYourself(this.props.jobId); } - static getDerivedStateFromProps(props) { - const { job, loading } = props; - return { job, loading }; + updateJob(job) { + this.setState({ job }); } render() { diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js index bb9e532245d6d..5db09eb7d07bb 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js @@ -146,7 +146,9 @@ export class JobsListView extends Component { /> ); } - this.setState({ itemIdToExpandedRowMap }); + this.setState({ itemIdToExpandedRowMap }, () => { + this.updateFunctions[jobId](job); + }); }); }) .catch((error) => { @@ -254,7 +256,7 @@ export class JobsListView extends Component { ); Object.keys(this.updateFunctions).forEach((j) => { - this.updateFunctions[j].setState({ job: fullJobsList[j] }); + this.updateFunctions[j](fullJobsList[j]); }); jobs.forEach((job) => { diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/event_rate_chart/event_rate_chart.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/event_rate_chart/event_rate_chart.tsx index 5af319be7a161..74ece35c00fe7 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/event_rate_chart/event_rate_chart.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/event_rate_chart/event_rate_chart.tsx @@ -5,7 +5,14 @@ */ import React, { FC } from 'react'; -import { BarSeries, Chart, ScaleType, Settings, TooltipType } from '@elastic/charts'; +import { + BarSeries, + Chart, + ScaleType, + Settings, + TooltipType, + BrushEndListener, +} from '@elastic/charts'; import { Axes } from '../common/axes'; import { LineChartPoint } from '../../../../common/chart_loader'; import { Anomaly } from '../../../../common/results_loader'; @@ -25,7 +32,10 @@ interface Props { overlayRange?: { start: number; end: number; + color: string; + showMarker?: boolean; }; + onBrushEnd?: BrushEndListener; } export const EventRateChart: FC = ({ @@ -37,6 +47,7 @@ export const EventRateChart: FC = ({ loading = false, fadeChart, overlayRange, + onBrushEnd, }) => { const { EVENT_RATE_COLOR_WITH_ANOMALIES, EVENT_RATE_COLOR } = useChartColors(); const barColor = fadeChart ? EVENT_RATE_COLOR_WITH_ANOMALIES : EVENT_RATE_COLOR; @@ -49,13 +60,20 @@ export const EventRateChart: FC = ({ 0} loading={loading}> {showAxis === true && } - + + {onBrushEnd === undefined ? ( + + ) : ( + + )} {overlayRange && ( )} diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/event_rate_chart/overlay_range.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/event_rate_chart/overlay_range.tsx index 5bfcf6e3787fc..5adfda7a6241b 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/event_rate_chart/overlay_range.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/event_rate_chart/overlay_range.tsx @@ -17,9 +17,17 @@ interface Props { eventRateChartData: LineChartPoint[]; start: number; end: number; + color: string; + showMarker?: boolean; } -export const OverlayRange: FC = ({ eventRateChartData, start, end }) => { +export const OverlayRange: FC = ({ + eventRateChartData, + start, + end, + color, + showMarker = true, +}) => { const maxHeight = Math.max(...eventRateChartData.map((e) => e.value)); return ( @@ -38,7 +46,7 @@ export const OverlayRange: FC = ({ eventRateChartData, start, end }) => { }, }, ]} - style={{ fill: 'red' }} + style={{ fill: color, strokeWidth: 0 }} /> = ({ eventRateChartData, start, end }) => { }, }} marker={ - <> -
-
- + showMarker ? ( + <> +
+
+ +
+
+ {formatDate(start, TIME_FORMAT)} +
-
- {formatDate(start, TIME_FORMAT)} -
-
- + + ) : undefined } /> From 7b278ca9b85754a2b34b6bd5b144311a778f3d3f Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Mon, 8 Jun 2020 16:13:10 +0100 Subject: [PATCH 04/18] updating layout --- .../model_snapshots/model_snapshots_table.tsx | 39 ++- .../revert_model_snapshot_flyout.tsx | 256 ++++++++++++------ .../components/job_details/job_details.js | 6 +- .../jobs_list_view/jobs_list_view.js | 3 + .../charts/event_rate_chart/overlay_range.tsx | 5 +- 5 files changed, 197 insertions(+), 112 deletions(-) diff --git a/x-pack/plugins/ml/public/application/components/model_snapshots/model_snapshots_table.tsx b/x-pack/plugins/ml/public/application/components/model_snapshots/model_snapshots_table.tsx index 462536eb9368a..44b015418c4e1 100644 --- a/x-pack/plugins/ml/public/application/components/model_snapshots/model_snapshots_table.tsx +++ b/x-pack/plugins/ml/public/application/components/model_snapshots/model_snapshots_table.tsx @@ -37,6 +37,7 @@ const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'; interface Props { job: CombinedJobWithStats; + refreshJobList: () => void; } enum COMBINED_JOB_STATE { @@ -46,12 +47,12 @@ enum COMBINED_JOB_STATE { UNKNOWN, } -export const ModelSnapshotTable: FC = ({ job }) => { +export const ModelSnapshotTable: FC = ({ job, refreshJobList }) => { const [snapshots, setSnapshots] = useState([]); const [snapshotsLoaded, setSnapshotsLoaded] = useState(false); const [editSnapshot, setEditSnapshot] = useState(null); const [revertSnapshot, setRevertSnapshot] = useState(null); - const [closeJobModalVisible, setCloseJobModalVisible] = useState(false); + const [closeJobModalVisible, setCloseJobModalVisible] = useState(null); const [combinedJobState, setCombinedJobState] = useState(null); useEffect(() => { @@ -70,26 +71,30 @@ export const ModelSnapshotTable: FC = ({ job }) => { // show toast return; } + setCombinedJobState(state); if (state === COMBINED_JOB_STATE.CLOSED) { // show flyout setRevertSnapshot(snapshot); } else { - setCombinedJobState(state); - showCloseJobModalVisible(); + // show close job modal + setCloseJobModalVisible(snapshot); } } - function showCloseJobModalVisible() { - setCloseJobModalVisible(true); - } function hideCloseJobModalVisible() { setCombinedJobState(null); - setCloseJobModalVisible(false); + setCloseJobModalVisible(null); } async function forceCloseJob() { - ml.jobs.forceStopAndCloseJob(job.job_id); + await ml.jobs.forceStopAndCloseJob(job.job_id); + if (closeJobModalVisible !== null) { + const state = await getCombinedJobState(job.job_id); + if (state === COMBINED_JOB_STATE.CLOSED) { + setRevertSnapshot(closeJobModalVisible); + } + } hideCloseJobModalVisible(); } @@ -169,19 +174,6 @@ export const ModelSnapshotTable: FC = ({ job }) => { icon: 'pencil', onClick: setEditSnapshot, }, - // { - // name: i18n.translate('xpack.ml.modelSnapshotTable.actions.delete.name', { - // defaultMessage: 'Delete', - // }), - // description: i18n.translate('xpack.ml.modelSnapshotTable.actions.delete.description', { - // defaultMessage: 'Delete this snapshot', - // }), - // type: 'icon', - // icon: 'trash', - // onClick: (a) => { - // console.log(a); - // }, - // }, ], }, ]; @@ -238,12 +230,13 @@ export const ModelSnapshotTable: FC = ({ job }) => { setRevertSnapshot(null); if (reload) { loadModelSnapshots(); + setTimeout(refreshJobList, 500); } }} /> )} - {closeJobModalVisible && combinedJobState !== null && ( + {closeJobModalVisible !== null && combinedJobState !== null && ( = ({ snapshot, snapshots, job, const { toasts } = useNotifications(); const [currentSnapshot, setCurrentSnapshot] = useState(snapshot); const [revertModalVisible, setRevertModalVisible] = useState(false); - const [replay, setReplay] = useState(true); + const [replay, setReplay] = useState(false); const [runInRealTime, setRunInRealTime] = useState(false); const [createCalendar, setCreateCalendar] = useState(false); const [startDate, setStartDate] = useState( - // moment(snapshot.latest_record_time_stamp) + // moment(currentSnapshot.latest_record_time_stamp) null ); const [endDate, setEndDate] = useState( @@ -77,9 +81,13 @@ export const RevertModelSnapshotFlyout: FC = ({ snapshot, snapshots, job, const [anomalies, setAnomalies] = useState([]); const [chartReady, setChartReady] = useState(false); + // useEffect(() => { + // createChartData(); + // }, []); + useEffect(() => { createChartData(); - }, []); + }, [currentSnapshot]); async function createChartData() { // setTimeout(async () => { @@ -108,8 +116,15 @@ export const RevertModelSnapshotFlyout: FC = ({ snapshot, snapshots, job, function onBrushEnd({ x }: XYBrushArea) { if (x && x.length === 2) { - setStartDate(moment(x[0])); - setEndDate(moment(x[1])); + const end = x[1] < currentSnapshot.latest_record_time_stamp ? null : x[1]; + if (end !== null) { + const start = + x[0] < currentSnapshot.latest_record_time_stamp + ? currentSnapshot.latest_record_time_stamp + : x[0]; + setStartDate(moment(start)); + setEndDate(moment(end)); + } } } @@ -137,10 +152,17 @@ export const RevertModelSnapshotFlyout: FC = ({ snapshot, snapshots, job, } } + function onSnapshotChange(ssId: string) { + const ss = snapshots.find((s) => s.snapshot_id === ssId); + if (ss !== undefined) { + setCurrentSnapshot(ss); + } + } + return ( <> - +
= ({ snapshot, snapshots, job,
- + +

{currentSnapshot.description}

+
+
+ + {false && ( + <> + + + + ({ + value: s.snapshot_id, + inputDisplay: s.snapshot_id, + dropdownDisplay: ( + <> + {s.snapshot_id} + +

{s.description}

+
+ + ), + })) + .reverse()} + valueOfSelected={currentSnapshot.snapshot_id} + onChange={onSnapshotChange} + itemLayoutAlign="top" + hasDividers + /> +
+ + )} + {/* */} + {/* */} = ({ snapshot, snapshots, job, width={'100%'} fadeChart={true} overlayRange={{ - start: snapshot.latest_record_time_stamp, + start: currentSnapshot.latest_record_time_stamp, end: job.data_counts.latest_record_timestamp, color: '#ff0000', }} @@ -174,7 +238,7 @@ export const RevertModelSnapshotFlyout: FC = ({ snapshot, snapshots, job, title={i18n.translate( 'xpack.ml.newJob.wizard.revertModelSnapshotFlyout.warningCallout.title', { - defaultMessage: 'Anomalies will be deleted', + defaultMessage: 'Anomaly data will be deleted', } )} color="warning" @@ -183,102 +247,124 @@ export const RevertModelSnapshotFlyout: FC = ({ snapshot, snapshots, job, - - + setReplay(e.target.checked)} /> - - setRunInRealTime(e.target.checked)} - /> - - - + setCreateCalendar(e.target.checked)} - /> - + > + setRunInRealTime(e.target.checked)} + /> + - {createCalendar && replay && ( - <> - + + setCreateCalendar(e.target.checked)} + /> + - - - - setStartDate(d)} - /> - - - - - setEndDate(d)} - /> - - - + {createCalendar && ( + <> + +
Select time range for calendar event.
+ + + - + + + + setStartDate(d)} + /> + + + + + setEndDate(d)} + /> + + + + + )} )}
diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details.js index 8931fc15df55a..d146548783f22 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details.js @@ -64,8 +64,7 @@ export class JobDetails extends Component { datafeedTimingStats, } = extractJobDetails(job); - const { showFullDetails } = this.props; - + const { showFullDetails, refreshJobList } = this.props; const tabs = [ { id: 'job-settings', @@ -184,7 +183,7 @@ export class JobDetails extends Component { }), content: ( - + {/* */} ), @@ -205,4 +204,5 @@ JobDetails.propTypes = { addYourself: PropTypes.func.isRequired, removeYourself: PropTypes.func.isRequired, showFullDetails: PropTypes.bool, + refreshJobList: PropTypes.func, }; diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js index 5db09eb7d07bb..a3b6cb39815a3 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js @@ -112,6 +112,7 @@ export class JobsListView extends Component { addYourself={this.addUpdateFunction} removeYourself={this.removeUpdateFunction} showFullDetails={this.props.isManagementTable !== true} + refreshJobList={this.onRefreshClick} /> ); } else { @@ -121,6 +122,7 @@ export class JobsListView extends Component { addYourself={this.addUpdateFunction} removeYourself={this.removeUpdateFunction} showFullDetails={this.props.isManagementTable !== true} + refreshJobList={this.onRefreshClick} /> ); } @@ -143,6 +145,7 @@ export class JobsListView extends Component { addYourself={this.addUpdateFunction} removeYourself={this.removeUpdateFunction} showFullDetails={this.props.isManagementTable !== true} + refreshJobList={this.onRefreshClick} /> ); } diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/event_rate_chart/overlay_range.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/event_rate_chart/overlay_range.tsx index 5adfda7a6241b..af4f91b3d643a 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/event_rate_chart/overlay_range.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/event_rate_chart/overlay_range.tsx @@ -46,7 +46,10 @@ export const OverlayRange: FC = ({ }, }, ]} - style={{ fill: color, strokeWidth: 0 }} + style={{ + fill: color, + strokeWidth: 0, + }} /> Date: Mon, 8 Jun 2020 21:52:46 +0100 Subject: [PATCH 05/18] multiple calendars --- .../revert_model_snapshot_flyout.tsx | 190 ++++++++++++------ .../event_rate_chart/event_rate_chart.tsx | 27 +-- .../charts/event_rate_chart/overlay_range.tsx | 4 +- .../services/ml_api_service/jobs.ts | 2 +- .../models/job_service/model_snapshots.ts | 16 +- .../routes/schemas/job_service_schema.ts | 11 +- 6 files changed, 166 insertions(+), 84 deletions(-) diff --git a/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx index 690483a9367f8..978a9990f37a5 100644 --- a/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx +++ b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx @@ -39,6 +39,7 @@ import { EuiHorizontalRule, EuiSuperSelect, EuiText, + EuiButtonIcon, } from '@elastic/eui'; import { @@ -54,6 +55,12 @@ import { parseInterval } from '../../../../../common/util/parse_interval'; const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'; +interface CalendarEvent { + start: moment.Moment | null; + end: moment.Moment | null; + description: string; +} + interface Props { snapshot: ModelSnapshot; snapshots: ModelSnapshot[]; @@ -68,23 +75,14 @@ export const RevertModelSnapshotFlyout: FC = ({ snapshot, snapshots, job, const [replay, setReplay] = useState(false); const [runInRealTime, setRunInRealTime] = useState(false); const [createCalendar, setCreateCalendar] = useState(false); - const [startDate, setStartDate] = useState( - // moment(currentSnapshot.latest_record_time_stamp) - null - ); - const [endDate, setEndDate] = useState( - // moment(job.data_counts.latest_record_timestamp) - null - ); + const [calendarEvents, setCalendarEvents] = useState([]); + // const [startDate, setStartDate] = useState(null); + // const [endDate, setEndDate] = useState(null); const [eventRateData, setEventRateData] = useState([]); const [anomalies, setAnomalies] = useState([]); const [chartReady, setChartReady] = useState(false); - // useEffect(() => { - // createChartData(); - // }, []); - useEffect(() => { createChartData(); }, [currentSnapshot]); @@ -122,24 +120,74 @@ export const RevertModelSnapshotFlyout: FC = ({ snapshot, snapshots, job, x[0] < currentSnapshot.latest_record_time_stamp ? currentSnapshot.latest_record_time_stamp : x[0]; - setStartDate(moment(start)); - setEndDate(moment(end)); + + setCalendarEvents([ + ...calendarEvents, + { + start: moment(start), + end: moment(end), + description: `Auto created ${calendarEvents.length}`, + }, + ]); + // setStartDate(moment(start)); + // setEndDate(moment(end)); } } } - async function revert() { + function setStartDate(start: moment.Moment | null, index: number) { + const event = calendarEvents[index]; + if (event === undefined) { + setCalendarEvents([ + ...calendarEvents, + { start, end: null, description: `Auto created ${index}` }, + ]); + } else { + event.start = start; + setCalendarEvents([...calendarEvents]); + } + } + + function setEndDate(end: moment.Moment | null, index: number) { + const event = calendarEvents[index]; + if (event === undefined) { + setCalendarEvents([ + ...calendarEvents, + { start: null, end, description: `Auto created ${index}` }, + ]); + } else { + event.end = end; + setCalendarEvents([...calendarEvents]); + } + } + + function removeCalendarEvent(index: number) { + if (calendarEvents[index] !== undefined) { + const ce = [...calendarEvents]; + ce.splice(index, 1); + setCalendarEvents(ce); + } + } + + async function applyRevert() { const end = replay && runInRealTime === false ? job.data_counts.latest_record_timestamp : undefined; try { + const events = calendarEvents.filter(filterIncompleteEvents).map((c) => ({ + start: c.start!.valueOf(), + end: c.end!.valueOf(), + description: c.description, + })); + await ml.jobs.revertModelSnapshot( job.job_id, currentSnapshot.snapshot_id, replay, end, - createCalendar && startDate !== null && endDate !== null - ? { start: startDate.valueOf(), end: endDate.valueOf() } - : undefined + events + // createCalendar && startDate !== null && endDate !== null + // ? { start: startDate.valueOf(), end: endDate.valueOf() } + // : undefined ); hideRevertModal(); closeWithReload(); @@ -224,11 +272,13 @@ export const RevertModelSnapshotFlyout: FC = ({ snapshot, snapshots, job, height={'100px'} width={'100%'} fadeChart={true} - overlayRange={{ - start: currentSnapshot.latest_record_time_stamp, - end: job.data_counts.latest_record_timestamp, - color: '#ff0000', - }} + overlayRanges={[ + { + start: currentSnapshot.latest_record_time_stamp, + end: job.data_counts.latest_record_timestamp, + color: '#ff0000', + }, + ]} /> @@ -326,43 +376,58 @@ export const RevertModelSnapshotFlyout: FC = ({ snapshot, snapshots, job, height={'100px'} width={'100%'} fadeChart={true} - overlayRange={ - startDate !== null && endDate !== null - ? { - start: startDate.valueOf(), - end: endDate.valueOf(), - color: '#0000ff', - showMarker: false, - } - : undefined - } + overlayRanges={calendarEvents.filter(filterIncompleteEvents).map((c) => ({ + start: c.start!.valueOf(), + end: c.end!.valueOf(), + color: '#0000ff', + showMarker: false, + }))} + // calendarEvents.length !== null && endDate !== null + // ? { + // start: startDate.valueOf(), + // end: endDate.valueOf(), + // color: '#0000ff', + // showMarker: false, + // } + // : undefined + // } onBrushEnd={onBrushEnd} /> - - - - setStartDate(d)} + {calendarEvents.map((c, i) => ( + + + + setStartDate(d, i)} + /> + + + + + setEndDate(d, i)} + /> + + + + removeCalendarEvent(i)} + iconType="trash" + aria-label="replace me" /> - - - - - setEndDate(d)} - /> - - - + + + ))} )} @@ -383,7 +448,10 @@ export const RevertModelSnapshotFlyout: FC = ({ snapshot, snapshots, job, c.start === null || c.end === null) + } fill > = ({ snapshot, snapshots, job, defaultMessage: 'Apply snapshot revert', })} onCancel={hideRevertModal} - onConfirm={revert} + onConfirm={applyRevert} cancelButtonText={i18n.translate( 'xpack.ml.newJob.wizard.revertModelSnapshotFlyout.cancelButton', { @@ -424,3 +492,11 @@ export const RevertModelSnapshotFlyout: FC = ({ snapshot, snapshots, job, ); }; + +// function filterNonNullable(value: T | null | undefined): value is T { +// return value !== null && value !== undefined; +// } + +function filterIncompleteEvents(event: CalendarEvent): event is CalendarEvent { + return event.start !== null && event.end !== null; +} diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/event_rate_chart/event_rate_chart.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/event_rate_chart/event_rate_chart.tsx index 74ece35c00fe7..fa4cddb886fd3 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/event_rate_chart/event_rate_chart.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/event_rate_chart/event_rate_chart.tsx @@ -29,12 +29,12 @@ interface Props { showAxis?: boolean; loading?: boolean; fadeChart?: boolean; - overlayRange?: { + overlayRanges?: Array<{ start: number; end: number; color: string; showMarker?: boolean; - }; + }>; onBrushEnd?: BrushEndListener; } @@ -46,7 +46,7 @@ export const EventRateChart: FC = ({ showAxis, loading = false, fadeChart, - overlayRange, + overlayRanges, onBrushEnd, }) => { const { EVENT_RATE_COLOR_WITH_ANOMALIES, EVENT_RATE_COLOR } = useChartColors(); @@ -67,15 +67,18 @@ export const EventRateChart: FC = ({ )} - {overlayRange && ( - - )} + {overlayRanges && + overlayRanges.map((range, i) => ( + + ))} = ({ + overlayKey, eventRateChartData, start, end, @@ -33,7 +35,7 @@ export const OverlayRange: FC = ({ return ( <> ) { const body = JSON.stringify({ jobId, snapshotId, replay, end, skip }); return http<{ total: number; categories: Array<{ count?: number; category: Category }> }>({ diff --git a/x-pack/plugins/ml/server/models/job_service/model_snapshots.ts b/x-pack/plugins/ml/server/models/job_service/model_snapshots.ts index b827763b14cd6..b95cc80985334 100644 --- a/x-pack/plugins/ml/server/models/job_service/model_snapshots.ts +++ b/x-pack/plugins/ml/server/models/job_service/model_snapshots.ts @@ -40,7 +40,7 @@ export function modelSnapshotProvider(callAsCurrentUser: APICaller) { replay: boolean, end: number, deleteInterveningResults: boolean = true, - skip?: { start: number; end: number } + skip?: [{ start: number; end: number; description: string }] ) { const job = await callAsCurrentUser('ml.jobs', { jobId: [jobId] }); const jobStats = await callAsCurrentUser('ml.jobStats', { @@ -56,18 +56,16 @@ export function modelSnapshotProvider(callAsCurrentUser: APICaller) { datafeedId: [datafeedId], }); - if (skip !== undefined) { + if (skip !== undefined && skip.length) { const calendar: FormCalendar = { calendarId: String(Date.now()), job_ids: [jobId], description: 'auto created', - events: [ - { - description: 'Auto created', - start_time: skip.start, - end_time: skip.end, - }, - ], + events: skip.map((s) => ({ + description: s.description, + start_time: s.start, + end_time: s.end, + })), }; const cm = new CalendarManager(callAsCurrentUser); await cm.newCalendar(calendar); diff --git a/x-pack/plugins/ml/server/routes/schemas/job_service_schema.ts b/x-pack/plugins/ml/server/routes/schemas/job_service_schema.ts index 01f4920d795de..1f7cd1da5ca31 100644 --- a/x-pack/plugins/ml/server/routes/schemas/job_service_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/job_service_schema.ts @@ -74,9 +74,12 @@ export const revertModelSnapshotSchema = schema.object({ end: schema.maybe(schema.number()), deleteInterveningResults: schema.maybe(schema.boolean()), skip: schema.maybe( - schema.object({ - start: schema.number(), - end: schema.number(), - }) + schema.arrayOf( + schema.object({ + start: schema.number(), + end: schema.number(), + description: schema.string(), + }) + ) ), }); From 9456c04c3f94b020ee19440d497329f94e2bbeee Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Tue, 9 Jun 2020 21:51:36 +0100 Subject: [PATCH 06/18] moving calendar creator --- .../create_calendar.tsx | 218 ++++++++++++++++++ .../revert_model_snapshot_flyout.tsx | 214 ++++------------- 2 files changed, 262 insertions(+), 170 deletions(-) create mode 100644 x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/create_calendar.tsx diff --git a/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/create_calendar.tsx b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/create_calendar.tsx new file mode 100644 index 0000000000000..b309a2b0309c8 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/create_calendar.tsx @@ -0,0 +1,218 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, useState, useEffect } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import moment from 'moment'; +// @ts-ignore +import { formatDate } from '@elastic/eui/lib/services/format'; +import { XYBrushArea } from '@elastic/charts'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiFormRow, + EuiFieldText, + EuiDatePicker, + EuiButtonIcon, + EuiPanel, +} from '@elastic/eui'; + +import { EventRateChart } from '../../../jobs/new_job/pages/components/charts/event_rate_chart/event_rate_chart'; +import { Anomaly } from '../../../jobs/new_job/common/results_loader/results_loader'; + +interface CalendarEvent { + start: moment.Moment | null; + end: moment.Moment | null; + description: string; +} + +interface Props { + calendarEvents: CalendarEvent[]; + setCalendarEvents: (calendars: CalendarEvent[]) => void; + minSelectableTimeStamp: number; + maxSelectableTimeStamp: number; + eventRateData: any[]; + anomalies: Anomaly[]; + chartReady: boolean; +} + +export const CreateCalendar: FC = ({ + calendarEvents, + setCalendarEvents, + minSelectableTimeStamp, + maxSelectableTimeStamp, + eventRateData, + anomalies, + chartReady, +}) => { + const maxSelectableTimeMoment = moment(maxSelectableTimeStamp); + const minSelectableTimeMoment = moment(minSelectableTimeStamp); + + function onBrushEnd({ x }: XYBrushArea) { + if (x && x.length === 2) { + const end = x[1] < minSelectableTimeStamp ? null : x[1]; + if (end !== null) { + const start = x[0] < minSelectableTimeStamp ? minSelectableTimeStamp : x[0]; + + setCalendarEvents([ + ...calendarEvents, + { + start: moment(start), + end: moment(end), + description: `Auto created ${calendarEvents.length}`, + }, + ]); + } + } + } + + function setStartDate(start: moment.Moment | null, index: number) { + const event = calendarEvents[index]; + if (event === undefined) { + setCalendarEvents([ + ...calendarEvents, + { start, end: null, description: `Auto created ${index}` }, + ]); + } else { + event.start = start; + setCalendarEvents([...calendarEvents]); + } + } + + function setEndDate(end: moment.Moment | null, index: number) { + const event = calendarEvents[index]; + if (event === undefined) { + setCalendarEvents([ + ...calendarEvents, + { start: null, end, description: `Auto created ${index}` }, + ]); + } else { + event.end = end; + setCalendarEvents([...calendarEvents]); + } + } + + function setDescription(description: string, index: number) { + const event = calendarEvents[index]; + if (event !== undefined) { + event.description = description; + setCalendarEvents([...calendarEvents]); + } + } + + function removeCalendarEvent(index: number) { + if (calendarEvents[index] !== undefined) { + const ce = [...calendarEvents]; + ce.splice(index, 1); + setCalendarEvents(ce); + } + } + + return ( + <> + +
Select time range for calendar event.
+ + ({ + start: c.start!.valueOf(), + end: c.end!.valueOf(), + color: '#0000ff', + showMarker: false, + }))} + onBrushEnd={onBrushEnd} + /> + + + {calendarEvents.map((c, i) => ( +
+ + + + + + + setStartDate(d, i)} + /> + + + + + setEndDate(d, i)} + /> + + + + + + + + setDescription(e.target.value, i)} + aria-label="CHANGE ME" + /> + + + + + + + removeCalendarEvent(i)} + iconType="trash" + aria-label="replace me" + /> + + + + +
+ ))} + + ); +}; + +function filterIncompleteEvents(event: CalendarEvent): event is CalendarEvent { + return event.start !== null && event.end !== null; +} diff --git a/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx index 978a9990f37a5..e56e2d8bfb663 100644 --- a/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx +++ b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx @@ -15,7 +15,6 @@ import { i18n } from '@kbn/i18n'; import moment from 'moment'; // @ts-ignore import { formatDate } from '@elastic/eui/lib/services/format'; -import { XYBrushArea } from '@elastic/charts'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiFlyout, @@ -28,18 +27,14 @@ import { EuiTitle, EuiFlyoutBody, EuiSpacer, - EuiTextArea, EuiFormRow, - EuiCheckbox, EuiSwitch, EuiConfirmModal, EuiOverlayMask, EuiCallOut, - EuiDatePicker, EuiHorizontalRule, EuiSuperSelect, EuiText, - EuiButtonIcon, } from '@elastic/eui'; import { @@ -52,6 +47,7 @@ import { loadEventRateForJob, loadAnomalyDataForJob } from './utils'; import { EventRateChart } from '../../../jobs/new_job/pages/components/charts/event_rate_chart/event_rate_chart'; import { Anomaly } from '../../../jobs/new_job/common/results_loader/results_loader'; import { parseInterval } from '../../../../../common/util/parse_interval'; +import { CreateCalendar } from './create_calendar'; const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'; @@ -76,8 +72,6 @@ export const RevertModelSnapshotFlyout: FC = ({ snapshot, snapshots, job, const [runInRealTime, setRunInRealTime] = useState(false); const [createCalendar, setCreateCalendar] = useState(false); const [calendarEvents, setCalendarEvents] = useState([]); - // const [startDate, setStartDate] = useState(null); - // const [endDate, setEndDate] = useState(null); const [eventRateData, setEventRateData] = useState([]); const [anomalies, setAnomalies] = useState([]); @@ -112,63 +106,6 @@ export const RevertModelSnapshotFlyout: FC = ({ snapshot, snapshots, job, setRevertModalVisible(false); } - function onBrushEnd({ x }: XYBrushArea) { - if (x && x.length === 2) { - const end = x[1] < currentSnapshot.latest_record_time_stamp ? null : x[1]; - if (end !== null) { - const start = - x[0] < currentSnapshot.latest_record_time_stamp - ? currentSnapshot.latest_record_time_stamp - : x[0]; - - setCalendarEvents([ - ...calendarEvents, - { - start: moment(start), - end: moment(end), - description: `Auto created ${calendarEvents.length}`, - }, - ]); - // setStartDate(moment(start)); - // setEndDate(moment(end)); - } - } - } - - function setStartDate(start: moment.Moment | null, index: number) { - const event = calendarEvents[index]; - if (event === undefined) { - setCalendarEvents([ - ...calendarEvents, - { start, end: null, description: `Auto created ${index}` }, - ]); - } else { - event.start = start; - setCalendarEvents([...calendarEvents]); - } - } - - function setEndDate(end: moment.Moment | null, index: number) { - const event = calendarEvents[index]; - if (event === undefined) { - setCalendarEvents([ - ...calendarEvents, - { start: null, end, description: `Auto created ${index}` }, - ]); - } else { - event.end = end; - setCalendarEvents([...calendarEvents]); - } - } - - function removeCalendarEvent(index: number) { - if (calendarEvents[index] !== undefined) { - const ce = [...calendarEvents]; - ce.splice(index, 1); - setCalendarEvents(ce); - } - } - async function applyRevert() { const end = replay && runInRealTime === false ? job.data_counts.latest_record_timestamp : undefined; @@ -185,9 +122,6 @@ export const RevertModelSnapshotFlyout: FC = ({ snapshot, snapshots, job, replay, end, events - // createCalendar && startDate !== null && endDate !== null - // ? { start: startDate.valueOf(), end: endDate.valueOf() } - // : undefined ); hideRevertModal(); closeWithReload(); @@ -226,42 +160,41 @@ export const RevertModelSnapshotFlyout: FC = ({ snapshot, snapshots, job, - {false && ( - <> - + {/* <> + + + + ({ + value: s.snapshot_id, + inputDisplay: s.snapshot_id, + dropdownDisplay: ( + <> + {s.snapshot_id} + +

{s.description}

+
+ + ), + })) + .reverse()} + valueOfSelected={currentSnapshot.snapshot_id} + onChange={onSnapshotChange} + itemLayoutAlign="top" + hasDividers + /> +
+ */} - - ({ - value: s.snapshot_id, - inputDisplay: s.snapshot_id, - dropdownDisplay: ( - <> - {s.snapshot_id} - -

{s.description}

-
- - ), - })) - .reverse()} - valueOfSelected={currentSnapshot.snapshot_id} - onChange={onSnapshotChange} - itemLayoutAlign="top" - hasDividers - /> -
- - )} {/* */} {/* */} @@ -365,70 +298,15 @@ export const RevertModelSnapshotFlyout: FC = ({ snapshot, snapshots, job, {createCalendar && ( - <> - -
Select time range for calendar event.
- - ({ - start: c.start!.valueOf(), - end: c.end!.valueOf(), - color: '#0000ff', - showMarker: false, - }))} - // calendarEvents.length !== null && endDate !== null - // ? { - // start: startDate.valueOf(), - // end: endDate.valueOf(), - // color: '#0000ff', - // showMarker: false, - // } - // : undefined - // } - onBrushEnd={onBrushEnd} - /> - - - {calendarEvents.map((c, i) => ( - - - - setStartDate(d, i)} - /> - - - - - setEndDate(d, i)} - /> - - - - removeCalendarEvent(i)} - iconType="trash" - aria-label="replace me" - /> - - - ))} - + )} )} @@ -493,10 +371,6 @@ export const RevertModelSnapshotFlyout: FC = ({ snapshot, snapshots, job, ); }; -// function filterNonNullable(value: T | null | undefined): value is T { -// return value !== null && value !== undefined; -// } - function filterIncompleteEvents(event: CalendarEvent): event is CalendarEvent { return event.start !== null && event.end !== null; } From 2dcd95fd55d144f704657a6227a214aaa1624199 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Wed, 10 Jun 2020 14:21:51 +0100 Subject: [PATCH 07/18] fixing chart issues --- .../model_snapshots/model_snapshots_table.tsx | 46 +++++++++++-------- .../create_calendar.tsx | 2 +- .../revert_model_snapshot_flyout.tsx | 42 ++++++++++------- .../revert_model_snapshot_flyout/utils.ts | 3 +- .../event_rate_chart/event_rate_chart.tsx | 12 +++-- .../charts/event_rate_chart/overlay_range.tsx | 2 +- 6 files changed, 67 insertions(+), 40 deletions(-) diff --git a/x-pack/plugins/ml/public/application/components/model_snapshots/model_snapshots_table.tsx b/x-pack/plugins/ml/public/application/components/model_snapshots/model_snapshots_table.tsx index 44b015418c4e1..48869cb52b363 100644 --- a/x-pack/plugins/ml/public/application/components/model_snapshots/model_snapshots_table.tsx +++ b/x-pack/plugins/ml/public/application/components/model_snapshots/model_snapshots_table.tsx @@ -9,19 +9,15 @@ import { i18n } from '@kbn/i18n'; // @ts-ignore import { formatDate } from '@elastic/eui/lib/services/format'; +import { FormattedMessage } from '@kbn/i18n/react'; import { - // EuiBadge, - // EuiButtonIcon, - // EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiInMemoryTable, - // EuiLink, EuiLoadingSpinner, EuiOverlayMask, EuiConfirmModal, - // EuiToolTip, } from '@elastic/eui'; import { EditModelSnapshotFlyout } from './edit_model_snapshot_flyout'; @@ -109,8 +105,6 @@ export const ModelSnapshotTable: FC = ({ job, refreshJobList }) => { defaultMessage: 'ID', }), sortable: true, - // width: '50%', - // scope: 'row', }, { field: 'description', @@ -206,7 +200,6 @@ export const ModelSnapshotTable: FC = ({ job, refreshJobList }) => { direction: 'asc', }, }} - // rowProps={getRowProps} /> {editSnapshot !== null && ( = ({ job, refreshJobList }) => { {closeJobModalVisible !== null && combinedJobState !== null && ( = ({ job, refreshJobList }) => { defaultMessage: 'Cancel', } )} - confirmButtonText={i18n.translate( - 'xpack.ml.modelSnapshotTable.closeJobConfirm.deleteButton', - { - defaultMessage: 'Force close', - } - )} + confirmButtonText={ + combinedJobState === COMBINED_JOB_STATE.OPEN_AND_RUNNING + ? i18n.translate( + 'xpack.ml.modelSnapshotTable.closeJobConfirm.stopAndClose.button', + { + defaultMessage: 'Force stop and close', + } + ) + : i18n.translate('xpack.ml.modelSnapshotTable.closeJobConfirm.close.button', { + defaultMessage: 'Force close', + }) + } defaultFocusedButton="confirm" > -

Close this job blah

+

+ +

)} diff --git a/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/create_calendar.tsx b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/create_calendar.tsx index b309a2b0309c8..9efba348ba80d 100644 --- a/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/create_calendar.tsx +++ b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/create_calendar.tsx @@ -37,7 +37,7 @@ import { import { EventRateChart } from '../../../jobs/new_job/pages/components/charts/event_rate_chart/event_rate_chart'; import { Anomaly } from '../../../jobs/new_job/common/results_loader/results_loader'; -interface CalendarEvent { +export interface CalendarEvent { start: moment.Moment | null; end: moment.Moment | null; description: string; diff --git a/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx index e56e2d8bfb663..b0633ed0ea5ca 100644 --- a/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx +++ b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx @@ -12,7 +12,7 @@ import React, { FC, useState, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; -import moment from 'moment'; +import {} from 'lodash'; // @ts-ignore import { formatDate } from '@elastic/eui/lib/services/format'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -33,7 +33,7 @@ import { EuiOverlayMask, EuiCallOut, EuiHorizontalRule, - EuiSuperSelect, + // EuiSuperSelect, EuiText, } from '@elastic/eui'; @@ -44,19 +44,14 @@ import { import { ml } from '../../../services/ml_api_service'; import { useNotifications } from '../../../contexts/kibana'; import { loadEventRateForJob, loadAnomalyDataForJob } from './utils'; +import { LineChartPoint } from '../../../jobs/new_job/common/chart_loader/chart_loader'; import { EventRateChart } from '../../../jobs/new_job/pages/components/charts/event_rate_chart/event_rate_chart'; import { Anomaly } from '../../../jobs/new_job/common/results_loader/results_loader'; import { parseInterval } from '../../../../../common/util/parse_interval'; -import { CreateCalendar } from './create_calendar'; +import { CreateCalendar, CalendarEvent } from './create_calendar'; const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'; -interface CalendarEvent { - start: moment.Moment | null; - end: moment.Moment | null; - description: string; -} - interface Props { snapshot: ModelSnapshot; snapshots: ModelSnapshot[]; @@ -73,7 +68,7 @@ export const RevertModelSnapshotFlyout: FC = ({ snapshot, snapshots, job, const [createCalendar, setCreateCalendar] = useState(false); const [calendarEvents, setCalendarEvents] = useState([]); - const [eventRateData, setEventRateData] = useState([]); + const [eventRateData, setEventRateData] = useState([]); const [anomalies, setAnomalies] = useState([]); const [chartReady, setChartReady] = useState(false); @@ -81,6 +76,21 @@ export const RevertModelSnapshotFlyout: FC = ({ snapshot, snapshots, job, createChartData(); }, [currentSnapshot]); + useEffect(() => { + // a bug in elastic charts selection can + // cause duplicate selected areas to be added + // dedupe the calendars based on start and end times + const calMap = new Map( + calendarEvents.map((c) => [`${c.start?.valueOf()}${c.end?.valueOf()}`, c]) + ); + const dedupedCalendarEvents = [...calMap.values()]; + + if (calendarEvents.length !== dedupedCalendarEvents.length) { + // deduped list is shorter, we must have removed something. + setCalendarEvents(dedupedCalendarEvents); + } + }, [calendarEvents]); + async function createChartData() { // setTimeout(async () => { const bucketSpanMs = parseInterval(job.analysis_config.bucket_span)!.asMilliseconds(); @@ -134,12 +144,12 @@ export const RevertModelSnapshotFlyout: FC = ({ snapshot, snapshots, job, } } - function onSnapshotChange(ssId: string) { - const ss = snapshots.find((s) => s.snapshot_id === ssId); - if (ss !== undefined) { - setCurrentSnapshot(ss); - } - } + // function onSnapshotChange(ssId: string) { + // const ss = snapshots.find((s) => s.snapshot_id === ssId); + // if (ss !== undefined) { + // setCurrentSnapshot(ss); + // } + // } return ( <> diff --git a/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/utils.ts b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/utils.ts index 15eaaa7746472..102d660db26c4 100644 --- a/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/utils.ts +++ b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/utils.ts @@ -12,12 +12,13 @@ import { } from '../../../../../common/types/anomaly_detection_jobs'; import { getSeverityType } from '../../../../../common/util/anomaly_utils'; import { Anomaly } from '../../../jobs/new_job/common/results_loader/results_loader'; +import { LineChartPoint } from '../../../jobs/new_job/common/chart_loader/chart_loader'; export async function loadEventRateForJob( job: CombinedJobWithStats, bucketSpanMs: number, bars: number -) { +): Promise { const intervalMs = Math.max( Math.floor( (job.data_counts.latest_record_timestamp - job.data_counts.earliest_record_timestamp) / bars diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/event_rate_chart/event_rate_chart.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/event_rate_chart/event_rate_chart.tsx index fa4cddb886fd3..44571da312b47 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/event_rate_chart/event_rate_chart.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/event_rate_chart/event_rate_chart.tsx @@ -7,11 +7,13 @@ import React, { FC } from 'react'; import { BarSeries, + HistogramBarSeries, Chart, ScaleType, Settings, TooltipType, BrushEndListener, + PartialTheme, } from '@elastic/charts'; import { Axes } from '../common/axes'; import { LineChartPoint } from '../../../../common/chart_loader'; @@ -52,6 +54,10 @@ export const EventRateChart: FC = ({ const { EVENT_RATE_COLOR_WITH_ANOMALIES, EVENT_RATE_COLOR } = useChartColors(); const barColor = fadeChart ? EVENT_RATE_COLOR_WITH_ANOMALIES : EVENT_RATE_COLOR; + const theme: PartialTheme = { + scales: { histogramPadding: 0.2 }, + }; + return (
= ({ {showAxis === true && } {onBrushEnd === undefined ? ( - + ) : ( - + )} {overlayRanges && @@ -81,7 +87,7 @@ export const EventRateChart: FC = ({ ))} - = ({ marker={ showMarker ? ( <> -
+
From 96df1d4c64a33fad46278ed8335d52ea813c0f99 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Wed, 10 Jun 2020 18:37:46 +0100 Subject: [PATCH 08/18] fixing chart issues --- .../components/color_range_legend/index.ts | 1 + .../color_range_legend/use_color_range.ts | 6 ++- .../create_calendar.tsx | 47 +++++++++++++++---- .../revert_model_snapshot_flyout.tsx | 13 +++-- .../revert_model_snapshot_flyout/utils.ts | 12 ++++- 5 files changed, 64 insertions(+), 15 deletions(-) diff --git a/x-pack/plugins/ml/public/application/components/color_range_legend/index.ts b/x-pack/plugins/ml/public/application/components/color_range_legend/index.ts index 93a1ec40f1d5e..8c92f47e5aa07 100644 --- a/x-pack/plugins/ml/public/application/components/color_range_legend/index.ts +++ b/x-pack/plugins/ml/public/application/components/color_range_legend/index.ts @@ -11,4 +11,5 @@ export { useColorRange, COLOR_RANGE, COLOR_RANGE_SCALE, + useCurrentEuiTheme, } from './use_color_range'; diff --git a/x-pack/plugins/ml/public/application/components/color_range_legend/use_color_range.ts b/x-pack/plugins/ml/public/application/components/color_range_legend/use_color_range.ts index 1d5f5cf3a0309..6a064f683b3aa 100644 --- a/x-pack/plugins/ml/public/application/components/color_range_legend/use_color_range.ts +++ b/x-pack/plugins/ml/public/application/components/color_range_legend/use_color_range.ts @@ -150,7 +150,7 @@ export const useColorRange = ( colorRangeScale = COLOR_RANGE_SCALE.LINEAR, featureCount = 1 ) => { - const euiTheme = useUiSettings().get('theme:darkMode') ? euiThemeDark : euiThemeLight; + const { euiTheme } = useCurrentEuiTheme(); const colorRanges: Record = { [COLOR_RANGE.BLUE]: [ @@ -186,3 +186,7 @@ export const useColorRange = ( return scaleTypes[colorRangeScale]; }; + +export function useCurrentEuiTheme() { + return { euiTheme: useUiSettings().get('theme:darkMode') ? euiThemeDark : euiThemeLight }; +} diff --git a/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/create_calendar.tsx b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/create_calendar.tsx index 9efba348ba80d..0b6c21be35fe8 100644 --- a/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/create_calendar.tsx +++ b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/create_calendar.tsx @@ -36,6 +36,7 @@ import { import { EventRateChart } from '../../../jobs/new_job/pages/components/charts/event_rate_chart/event_rate_chart'; import { Anomaly } from '../../../jobs/new_job/common/results_loader/results_loader'; +import { useCurrentEuiTheme } from '../../../components/color_range_legend'; export interface CalendarEvent { start: moment.Moment | null; @@ -65,6 +66,8 @@ export const CreateCalendar: FC = ({ const maxSelectableTimeMoment = moment(maxSelectableTimeStamp); const minSelectableTimeMoment = moment(minSelectableTimeStamp); + const { euiTheme } = useCurrentEuiTheme(); + function onBrushEnd({ x }: XYBrushArea) { if (x && x.length === 2) { const end = x[1] < minSelectableTimeStamp ? null : x[1]; @@ -76,7 +79,7 @@ export const CreateCalendar: FC = ({ { start: moment(start), end: moment(end), - description: `Auto created ${calendarEvents.length}`, + description: `Auto created ${calendarEvents.length + 1}`, }, ]); } @@ -154,7 +157,14 @@ export const CreateCalendar: FC = ({ - + = ({ - + = ({ - + setDescription(e.target.value, i)} - aria-label="CHANGE ME" /> @@ -193,7 +216,10 @@ export const CreateCalendar: FC = ({ = ({ color={'danger'} onClick={() => removeCalendarEvent(i)} iconType="trash" - aria-label="replace me" + aria-label={i18n.translate( + 'xpack.ml.revertModelSnapshotFlyout.createCalendar.deleteLabel', + { + defaultMessage: 'Delete event', + } + )} /> diff --git a/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx index b0633ed0ea5ca..d7bbbac40646e 100644 --- a/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx +++ b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx @@ -67,6 +67,7 @@ export const RevertModelSnapshotFlyout: FC = ({ snapshot, snapshots, job, const [runInRealTime, setRunInRealTime] = useState(false); const [createCalendar, setCreateCalendar] = useState(false); const [calendarEvents, setCalendarEvents] = useState([]); + const [calendarEventsValid, setCalendarEventsValid] = useState(true); const [eventRateData, setEventRateData] = useState([]); const [anomalies, setAnomalies] = useState([]); @@ -77,6 +78,11 @@ export const RevertModelSnapshotFlyout: FC = ({ snapshot, snapshots, job, }, [currentSnapshot]); useEffect(() => { + const invalid = calendarEvents.some( + (c) => c.description === '' || c.end === null || c.start === null + ); + setCalendarEventsValid(invalid === false); + // a bug in elastic charts selection can // cause duplicate selected areas to be added // dedupe the calendars based on start and end times @@ -85,7 +91,7 @@ export const RevertModelSnapshotFlyout: FC = ({ snapshot, snapshots, job, ); const dedupedCalendarEvents = [...calMap.values()]; - if (calendarEvents.length !== dedupedCalendarEvents.length) { + if (dedupedCalendarEvents.length < calendarEvents.length) { // deduped list is shorter, we must have removed something. setCalendarEvents(dedupedCalendarEvents); } @@ -336,10 +342,7 @@ export const RevertModelSnapshotFlyout: FC = ({ snapshot, snapshots, job, c.start === null || c.end === null) - } + disabled={createCalendar === true && calendarEventsValid === false} fill > ({ + const events = Object.entries(resp.results).map(([time, value]) => ({ time: +time, value: value as number, })); + + if (events.length) { + // add one extra bucket with a value of 0 + // so that an extra blank bar gets drawn at the end of the chart + // this solves an issue with elastic charts where the rect annotation + // never covers the last bar. + events.push({ time: events[events.length - 1].time + intervalMs, value: 0 }); + } + + return events; } export async function loadAnomalyDataForJob( From 157ef4327fd6eb1cd66d09756651d20ca1711a83 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Thu, 11 Jun 2020 13:45:52 +0100 Subject: [PATCH 09/18] improving calendar rendering --- .../create_calendar.tsx | 66 ++++++++++---- .../revert_model_snapshot_flyout.tsx | 87 ++++++++++--------- 2 files changed, 94 insertions(+), 59 deletions(-) diff --git a/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/create_calendar.tsx b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/create_calendar.tsx index 0b6c21be35fe8..14ebff911a0be 100644 --- a/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/create_calendar.tsx +++ b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/create_calendar.tsx @@ -16,9 +16,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC, useState, useEffect } from 'react'; +import React, { FC, memo } from 'react'; import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; import moment from 'moment'; // @ts-ignore import { formatDate } from '@elastic/eui/lib/services/format'; @@ -37,6 +36,7 @@ import { import { EventRateChart } from '../../../jobs/new_job/pages/components/charts/event_rate_chart/event_rate_chart'; import { Anomaly } from '../../../jobs/new_job/common/results_loader/results_loader'; import { useCurrentEuiTheme } from '../../../components/color_range_legend'; +import { LineChartPoint } from '../../../jobs/new_job/common/chart_loader/chart_loader'; export interface CalendarEvent { start: moment.Moment | null; @@ -49,7 +49,7 @@ interface Props { setCalendarEvents: (calendars: CalendarEvent[]) => void; minSelectableTimeStamp: number; maxSelectableTimeStamp: number; - eventRateData: any[]; + eventRateData: LineChartPoint[]; anomalies: Anomaly[]; chartReady: boolean; } @@ -79,7 +79,7 @@ export const CreateCalendar: FC = ({ { start: moment(start), end: moment(end), - description: `Auto created ${calendarEvents.length + 1}`, + description: createDefaultEventDescription(calendarEvents.length + 1), }, ]); } @@ -91,7 +91,7 @@ export const CreateCalendar: FC = ({ if (event === undefined) { setCalendarEvents([ ...calendarEvents, - { start, end: null, description: `Auto created ${index}` }, + { start, end: null, description: createDefaultEventDescription(index) }, ]); } else { event.start = start; @@ -104,7 +104,7 @@ export const CreateCalendar: FC = ({ if (event === undefined) { setCalendarEvents([ ...calendarEvents, - { start: null, end, description: `Auto created ${index}` }, + { start: null, end, description: createDefaultEventDescription(index) }, ]); } else { event.end = end; @@ -112,13 +112,13 @@ export const CreateCalendar: FC = ({ } } - function setDescription(description: string, index: number) { + const setDescription = (description: string, index: number) => { const event = calendarEvents[index]; if (event !== undefined) { event.description = description; setCalendarEvents([...calendarEvents]); } - } + }; function removeCalendarEvent(index: number) { if (calendarEvents[index] !== undefined) { @@ -133,18 +133,13 @@ export const CreateCalendar: FC = ({
Select time range for calendar event.
- ({ start: c.start!.valueOf(), end: c.end!.valueOf(), - color: '#0000ff', - showMarker: false, }))} onBrushEnd={onBrushEnd} /> @@ -244,6 +239,45 @@ export const CreateCalendar: FC = ({ ); }; +interface ChartProps { + eventRateData: LineChartPoint[]; + anomalies: Anomaly[]; + loading: boolean; + onBrushEnd(area: XYBrushArea): void; + overlayRanges: Array<{ start: number; end: number }>; +} + +const Chart: FC = memo( + ({ eventRateData, anomalies, loading, onBrushEnd, overlayRanges }) => ( + ({ + start: c.start, + end: c.end, + color: '#0000ff', + showMarker: false, + }))} + onBrushEnd={onBrushEnd} + /> + ), + (prev: ChartProps, next: ChartProps) => { + // only redraw if the calendar ranges have changes + return ( + prev.overlayRanges.length === next.overlayRanges.length && + JSON.stringify(prev.overlayRanges) === JSON.stringify(next.overlayRanges) + ); + } +); + function filterIncompleteEvents(event: CalendarEvent): event is CalendarEvent { return event.start !== null && event.end !== null; } + +function createDefaultEventDescription(index: number) { + return `Auto created event ${index}`; +} diff --git a/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx index d7bbbac40646e..4bb363ed8c391 100644 --- a/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx +++ b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx @@ -33,7 +33,7 @@ import { EuiOverlayMask, EuiCallOut, EuiHorizontalRule, - // EuiSuperSelect, + EuiSuperSelect, EuiText, } from '@elastic/eui'; @@ -150,12 +150,12 @@ export const RevertModelSnapshotFlyout: FC = ({ snapshot, snapshots, job, } } - // function onSnapshotChange(ssId: string) { - // const ss = snapshots.find((s) => s.snapshot_id === ssId); - // if (ss !== undefined) { - // setCurrentSnapshot(ss); - // } - // } + function onSnapshotChange(ssId: string) { + const ss = snapshots.find((s) => s.snapshot_id === ssId); + if (ss !== undefined) { + setCurrentSnapshot(ss); + } + } return ( <> @@ -176,43 +176,44 @@ export const RevertModelSnapshotFlyout: FC = ({ snapshot, snapshots, job, - {/* <> - - - - ({ - value: s.snapshot_id, - inputDisplay: s.snapshot_id, - dropdownDisplay: ( - <> - {s.snapshot_id} - -

{s.description}

-
- - ), - })) - .reverse()} - valueOfSelected={currentSnapshot.snapshot_id} - onChange={onSnapshotChange} - itemLayoutAlign="top" - hasDividers - /> -
- */} + {false && ( + <> + - {/* */} - {/* */} + + ({ + value: s.snapshot_id, + inputDisplay: s.snapshot_id, + dropdownDisplay: ( + <> + {s.snapshot_id} + +

{s.description}

+
+ + ), + })) + .reverse()} + valueOfSelected={currentSnapshot.snapshot_id} + onChange={onSnapshotChange} + itemLayoutAlign="top" + hasDividers + /> +
+ + + + )} Date: Thu, 11 Jun 2020 18:47:38 +0100 Subject: [PATCH 10/18] adding capabilities checks --- .../components/model_snapshots/model_snapshots_table.tsx | 6 ++++++ .../revert_model_snapshot_flyout.tsx | 4 ++++ x-pack/plugins/ml/server/routes/anomaly_detectors.ts | 2 +- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/ml/public/application/components/model_snapshots/model_snapshots_table.tsx b/x-pack/plugins/ml/public/application/components/model_snapshots/model_snapshots_table.tsx index 48869cb52b363..52b67f30c1abd 100644 --- a/x-pack/plugins/ml/public/application/components/model_snapshots/model_snapshots_table.tsx +++ b/x-pack/plugins/ml/public/application/components/model_snapshots/model_snapshots_table.tsx @@ -20,6 +20,7 @@ import { EuiConfirmModal, } from '@elastic/eui'; +import { checkPermission } from '../../capabilities/check_capabilities'; import { EditModelSnapshotFlyout } from './edit_model_snapshot_flyout'; import { RevertModelSnapshotFlyout } from './revert_model_snapshot_flyout'; import { ml } from '../../services/ml_api_service'; @@ -44,6 +45,9 @@ enum COMBINED_JOB_STATE { } export const ModelSnapshotTable: FC = ({ job, refreshJobList }) => { + const canCreateJob = checkPermission('canCreateJob'); + const canStartStopDatafeed = checkPermission('canStartStopDatafeed'); + const [snapshots, setSnapshots] = useState([]); const [snapshotsLoaded, setSnapshotsLoaded] = useState(false); const [editSnapshot, setEditSnapshot] = useState(null); @@ -153,6 +157,7 @@ export const ModelSnapshotTable: FC = ({ job, refreshJobList }) => { description: i18n.translate('xpack.ml.modelSnapshotTable.actions.revert.description', { defaultMessage: 'Revert this snapshot', }), + enabled: () => canCreateJob && canStartStopDatafeed, type: 'icon', icon: 'crosshairs', onClick: checkJobIsClosed, @@ -164,6 +169,7 @@ export const ModelSnapshotTable: FC = ({ job, refreshJobList }) => { description: i18n.translate('xpack.ml.modelSnapshotTable.actions.edit.description', { defaultMessage: 'Edit this snapshot', }), + enabled: () => canCreateJob, type: 'icon', icon: 'pencil', onClick: setEditSnapshot, diff --git a/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx index 4bb363ed8c391..b299c24e998c6 100644 --- a/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx +++ b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx @@ -72,6 +72,7 @@ export const RevertModelSnapshotFlyout: FC = ({ snapshot, snapshots, job, const [eventRateData, setEventRateData] = useState([]); const [anomalies, setAnomalies] = useState([]); const [chartReady, setChartReady] = useState(false); + const [applying, setApplying] = useState(false); useEffect(() => { createChartData(); @@ -123,6 +124,7 @@ export const RevertModelSnapshotFlyout: FC = ({ snapshot, snapshots, job, } async function applyRevert() { + setApplying(true); const end = replay && runInRealTime === false ? job.data_counts.latest_record_timestamp : undefined; try { @@ -142,6 +144,7 @@ export const RevertModelSnapshotFlyout: FC = ({ snapshot, snapshots, job, hideRevertModal(); closeWithReload(); } catch (error) { + setApplying(false); toasts.addError(new Error(error.body.message), { title: i18n.translate('xpack.ml.revertModelSnapshotFlyout.revertErrorTitle', { defaultMessage: 'Model snapshot revert failed', @@ -376,6 +379,7 @@ export const RevertModelSnapshotFlyout: FC = ({ snapshot, snapshots, job, defaultMessage: 'Apply', } )} + confirmButtonDisabled={applying} buttonColor="danger" defaultFocusedButton="confirm" /> diff --git a/x-pack/plugins/ml/server/routes/anomaly_detectors.ts b/x-pack/plugins/ml/server/routes/anomaly_detectors.ts index bec2bd5997394..eb41bfcf57244 100644 --- a/x-pack/plugins/ml/server/routes/anomaly_detectors.ts +++ b/x-pack/plugins/ml/server/routes/anomaly_detectors.ts @@ -643,7 +643,7 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { body: updateModelSnapshotSchema, }, options: { - tags: ['access:ml:canCreateJob'], // TODO IS THIS CORRECT? + tags: ['access:ml:canCreateJob'], }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { From a576f0642dc57f5d9b37ba017aab8a880a2a36ac Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Thu, 11 Jun 2020 19:54:33 +0100 Subject: [PATCH 11/18] code clean up --- .../revert_model_snapshot_flyout.tsx | 12 ++- .../event_rate_chart/event_rate_chart.tsx | 1 - .../services/ml_api_service/jobs.ts | 4 +- .../models/job_service/model_snapshots.ts | 101 +++++++++--------- .../plugins/ml/server/routes/job_service.ts | 11 +- .../routes/schemas/job_service_schema.ts | 2 +- 6 files changed, 70 insertions(+), 61 deletions(-) diff --git a/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx index b299c24e998c6..58c71aa73ba53 100644 --- a/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx +++ b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx @@ -128,11 +128,13 @@ export const RevertModelSnapshotFlyout: FC = ({ snapshot, snapshots, job, const end = replay && runInRealTime === false ? job.data_counts.latest_record_timestamp : undefined; try { - const events = calendarEvents.filter(filterIncompleteEvents).map((c) => ({ - start: c.start!.valueOf(), - end: c.end!.valueOf(), - description: c.description, - })); + const events = replay + ? calendarEvents.filter(filterIncompleteEvents).map((c) => ({ + start: c.start!.valueOf(), + end: c.end!.valueOf(), + description: c.description, + })) + : undefined; await ml.jobs.revertModelSnapshot( job.job_id, diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/event_rate_chart/event_rate_chart.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/event_rate_chart/event_rate_chart.tsx index 44571da312b47..bd6fedd4ba21c 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/event_rate_chart/event_rate_chart.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/event_rate_chart/event_rate_chart.tsx @@ -6,7 +6,6 @@ import React, { FC } from 'react'; import { - BarSeries, HistogramBarSeries, Chart, ScaleType, diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts index 5a5b79977dccd..a9bd1316f7e5c 100644 --- a/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts +++ b/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts @@ -275,9 +275,9 @@ export const jobs = { snapshotId: string, replay: boolean, end?: number, - skip?: Array<{ start: number; end: number; description: string }> + calendarEvents?: Array<{ start: number; end: number; description: string }> ) { - const body = JSON.stringify({ jobId, snapshotId, replay, end, skip }); + const body = JSON.stringify({ jobId, snapshotId, replay, end, calendarEvents }); return http<{ total: number; categories: Array<{ count?: number; category: Category }> }>({ path: `${basePath()}/jobs/revert_model_snapshot`, method: 'POST', diff --git a/x-pack/plugins/ml/server/models/job_service/model_snapshots.ts b/x-pack/plugins/ml/server/models/job_service/model_snapshots.ts index b95cc80985334..500bc2f6c6f49 100644 --- a/x-pack/plugins/ml/server/models/job_service/model_snapshots.ts +++ b/x-pack/plugins/ml/server/models/job_service/model_snapshots.ts @@ -4,22 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { i18n } from '@kbn/i18n'; -import { uniq } from 'lodash'; import Boom from 'boom'; import { APICaller } from 'kibana/server'; -import { JOB_STATE, DATAFEED_STATE } from '../../../common/constants/states'; -import { - MlSummaryJob, - AuditMessage, - Job, - JobStats, - DatafeedWithStats, - CombinedJobWithStats, - ModelSnapshot, -} from '../../../common/types/anomaly_detection_jobs'; -import { datafeedsProvider, MlDatafeedsResponse, MlDatafeedsStatsResponse } from './datafeeds'; -import { jobsProvider, MlJobsResponse, MlJobsStatsResponse } from './jobs'; +import { ModelSnapshot } from '../../../common/types/anomaly_detection_jobs'; +import { datafeedsProvider, MlDatafeedsResponse } from './datafeeds'; +import { MlJobsResponse } from './jobs'; import { FormCalendar, CalendarManager } from '../calendar'; export interface ModelSnapshotsResponse { @@ -32,7 +21,6 @@ export interface RevertModelSnapshotResponse { export function modelSnapshotProvider(callAsCurrentUser: APICaller) { const { forceStartDatafeeds, getDatafeedIdsByJobId } = datafeedsProvider(callAsCurrentUser); - // const { MlJobsResponse } = jobsProvider(callAsCurrentUser); async function revertModelSnapshot( jobId: string, @@ -40,51 +28,64 @@ export function modelSnapshotProvider(callAsCurrentUser: APICaller) { replay: boolean, end: number, deleteInterveningResults: boolean = true, - skip?: [{ start: number; end: number; description: string }] + calendarEvents?: [{ start: number; end: number; description: string }] ) { - const job = await callAsCurrentUser('ml.jobs', { jobId: [jobId] }); - const jobStats = await callAsCurrentUser('ml.jobStats', { - jobId: [jobId], - }); - - const datafeedIds = await getDatafeedIdsByJobId(); - const datafeedId = datafeedIds[jobId]; - if (datafeedId === undefined) { - throw Boom.notFound(`Cannot find datafeed for job ${jobId}`); - } - const datafeed = await callAsCurrentUser('ml.datafeeds', { - datafeedId: [datafeedId], - }); + let datafeedId = `datafeed-${jobId}`; + // ensure job exists + await callAsCurrentUser('ml.jobs', { jobId: [jobId] }); - if (skip !== undefined && skip.length) { - const calendar: FormCalendar = { - calendarId: String(Date.now()), - job_ids: [jobId], - description: 'auto created', - events: skip.map((s) => ({ - description: s.description, - start_time: s.start, - end_time: s.end, - })), - }; - const cm = new CalendarManager(callAsCurrentUser); - await cm.newCalendar(calendar); + try { + // ensure the datafeed exists + // the datafeed is probably called datafeed- + await callAsCurrentUser('ml.datafeeds', { + datafeedId: [datafeedId], + }); + } catch (e) { + // if the datafeed isn't called datafeed- + // check all datafeeds to see if one exists that is matched to this job id + const datafeedIds = await getDatafeedIdsByJobId(); + datafeedId = datafeedIds[jobId]; + if (datafeedId === undefined) { + throw Boom.notFound(`Cannot find datafeed for job ${jobId}`); + } } + // ensure the snapshot exists const snapshot = await callAsCurrentUser('ml.modelSnapshots', { jobId, snapshotId, }); - const resp = await callAsCurrentUser('ml.revertModelSnapshot', { - jobId, - snapshotId, - body: { - delete_intervening_results: deleteInterveningResults, - }, - }); + // apply the snapshot revert + const { model } = await callAsCurrentUser( + 'ml.revertModelSnapshot', + { + jobId, + snapshotId, + body: { + delete_intervening_results: deleteInterveningResults, + }, + } + ); + + // create calendar (if specified) and replay datafeed + if (replay && model.snapshot_id === snapshotId && snapshot.model_snapshots.length) { + // create calendar before starting restarting the datafeed + if (calendarEvents !== undefined && calendarEvents.length) { + const calendar: FormCalendar = { + calendarId: String(Date.now()), + job_ids: [jobId], + description: 'auto created', + events: calendarEvents.map((s) => ({ + description: s.description, + start_time: s.start, + end_time: s.end, + })), + }; + const cm = new CalendarManager(callAsCurrentUser); + await cm.newCalendar(calendar); + } - if (replay && snapshot && snapshot.model_snapshots.length) { forceStartDatafeeds([datafeedId], snapshot.model_snapshots[0].latest_record_time_stamp, end); } diff --git a/x-pack/plugins/ml/server/routes/job_service.ts b/x-pack/plugins/ml/server/routes/job_service.ts index f32bcdd3d9e9e..d8b9b313e91bd 100644 --- a/x-pack/plugins/ml/server/routes/job_service.ts +++ b/x-pack/plugins/ml/server/routes/job_service.ts @@ -751,14 +751,21 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const { revertModelSnapshot } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); - const { jobId, snapshotId, replay, end, deleteInterveningResults, skip } = request.body; + const { + jobId, + snapshotId, + replay, + end, + deleteInterveningResults, + calendarEvents, + } = request.body; const resp = await revertModelSnapshot( jobId, snapshotId, replay, end, deleteInterveningResults, - skip + calendarEvents ); return response.ok({ diff --git a/x-pack/plugins/ml/server/routes/schemas/job_service_schema.ts b/x-pack/plugins/ml/server/routes/schemas/job_service_schema.ts index 1f7cd1da5ca31..36e340adf0b31 100644 --- a/x-pack/plugins/ml/server/routes/schemas/job_service_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/job_service_schema.ts @@ -73,7 +73,7 @@ export const revertModelSnapshotSchema = schema.object({ replay: schema.boolean(), end: schema.maybe(schema.number()), deleteInterveningResults: schema.maybe(schema.boolean()), - skip: schema.maybe( + calendarEvents: schema.maybe( schema.arrayOf( schema.object({ start: schema.number(), From 836f31a0efc1ee81563d7950267bdc6c6f795dc5 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Thu, 11 Jun 2020 19:59:02 +0100 Subject: [PATCH 12/18] fixing end time argument type --- x-pack/plugins/ml/server/models/job_service/model_snapshots.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/ml/server/models/job_service/model_snapshots.ts b/x-pack/plugins/ml/server/models/job_service/model_snapshots.ts index 500bc2f6c6f49..0a20ebadd4cfc 100644 --- a/x-pack/plugins/ml/server/models/job_service/model_snapshots.ts +++ b/x-pack/plugins/ml/server/models/job_service/model_snapshots.ts @@ -26,7 +26,7 @@ export function modelSnapshotProvider(callAsCurrentUser: APICaller) { jobId: string, snapshotId: string, replay: boolean, - end: number, + end?: number, deleteInterveningResults: boolean = true, calendarEvents?: [{ start: number; end: number; description: string }] ) { From 40c6f11ac95170c04fae8bdacb9f8871448b1318 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Fri, 12 Jun 2020 08:50:39 +0100 Subject: [PATCH 13/18] fix translations --- .../edit_model_snapshot_flyout/edit_model_snapshot_flyout.tsx | 2 +- .../jobs/new_job/recognize/components/create_result_callout.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/ml/public/application/components/model_snapshots/edit_model_snapshot_flyout/edit_model_snapshot_flyout.tsx b/x-pack/plugins/ml/public/application/components/model_snapshots/edit_model_snapshot_flyout/edit_model_snapshot_flyout.tsx index 2b3d3d2ce359e..46eb20e7b23af 100644 --- a/x-pack/plugins/ml/public/application/components/model_snapshots/edit_model_snapshot_flyout/edit_model_snapshot_flyout.tsx +++ b/x-pack/plugins/ml/public/application/components/model_snapshots/edit_model_snapshot_flyout/edit_model_snapshot_flyout.tsx @@ -74,7 +74,7 @@ export const EditModelSnapshotFlyout: FC = ({ snapshot, job, closeFlyout closeWithReload(); } catch (error) { toasts.addError(new Error(error.body.message), { - title: i18n.translate('xpack.ml.editModelSnapshotFlyout.saveErrorTitle', { + title: i18n.translate('xpack.ml.editModelSnapshotFlyout.deleteErrorTitle', { defaultMessage: 'Model snapshot deletion failed', }), }); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/create_result_callout.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/create_result_callout.tsx index 9d0cf705aaba6..4602ceeec905f 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/create_result_callout.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/create_result_callout.tsx @@ -67,7 +67,7 @@ export const CreateResultCallout: FC = memo( color="primary" fill={false} aria-label={i18n.translate( - 'xpack.ml.newJi18n(ob.recognize.jobsCreationFailed.resetButtonAriaLabel', + 'xpack.ml.newJob.recognize.jobsCreationFailed.resetButtonAriaLabel', { defaultMessage: 'Reset' } )} onClick={onReset} From 583d4cd1df9b1d0e7280f89adb03fc422e70ef19 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Fri, 12 Jun 2020 10:11:54 +0100 Subject: [PATCH 14/18] code clean up --- .../model_snapshots/model_snapshots_table.tsx | 46 ++++++++++--------- .../{utils.ts => chart_loader.ts} | 6 +-- .../revert_model_snapshot_flyout.tsx | 18 ++++---- .../components/job_details/job_details.js | 1 - .../services/ml_api_service/jobs.ts | 2 +- .../ml/server/routes/anomaly_detectors.ts | 12 ++--- .../plugins/ml/server/routes/job_service.ts | 14 +++--- 7 files changed, 48 insertions(+), 51 deletions(-) rename x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/{utils.ts => chart_loader.ts} (92%) diff --git a/x-pack/plugins/ml/public/application/components/model_snapshots/model_snapshots_table.tsx b/x-pack/plugins/ml/public/application/components/model_snapshots/model_snapshots_table.tsx index 52b67f30c1abd..1b41f0de38755 100644 --- a/x-pack/plugins/ml/public/application/components/model_snapshots/model_snapshots_table.tsx +++ b/x-pack/plugins/ml/public/application/components/model_snapshots/model_snapshots_table.tsx @@ -18,6 +18,7 @@ import { EuiLoadingSpinner, EuiOverlayMask, EuiConfirmModal, + EuiBasicTableColumn, } from '@elastic/eui'; import { checkPermission } from '../../capabilities/check_capabilities'; @@ -98,11 +99,23 @@ export const ModelSnapshotTable: FC = ({ job, refreshJobList }) => { hideCloseJobModalVisible(); } - function renderDate(date: number) { - return formatDate(date, TIME_FORMAT); + function closeEditFlyout(reload: boolean) { + setEditSnapshot(null); + if (reload) { + loadModelSnapshots(); + } } - const columns = [ + function closeRevertFlyout(reload: boolean) { + setRevertSnapshot(null); + if (reload) { + loadModelSnapshots(); + // wait half a second before refreshing the jobs list + setTimeout(refreshJobList, 500); + } + } + + const columns: Array> = [ { field: 'snapshot_id', name: i18n.translate('xpack.ml.modelSnapshotTable.id', { @@ -195,8 +208,8 @@ export const ModelSnapshotTable: FC = ({ job, refreshJobList }) => { = ({ job, refreshJobList }) => { }} /> {editSnapshot !== null && ( - { - setEditSnapshot(null); - if (reload) { - loadModelSnapshots(); - } - }} - /> + )} {revertSnapshot !== null && ( @@ -225,13 +229,7 @@ export const ModelSnapshotTable: FC = ({ job, refreshJobList }) => { snapshot={revertSnapshot} snapshots={snapshots} job={job} - closeFlyout={(reload: boolean) => { - setRevertSnapshot(null); - if (reload) { - loadModelSnapshots(); - setTimeout(refreshJobList, 500); - } - }} + closeFlyout={closeRevertFlyout} /> )} @@ -282,6 +280,10 @@ export const ModelSnapshotTable: FC = ({ job, refreshJobList }) => { ); }; +function renderDate(date: number) { + return formatDate(date, TIME_FORMAT); +} + async function getCombinedJobState(jobId: string) { const jobs = await ml.jobs.jobs([jobId]); diff --git a/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/utils.ts b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/chart_loader.ts similarity index 92% rename from x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/utils.ts rename to x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/chart_loader.ts index a2fbd332bf9cc..a4a6926c1bbda 100644 --- a/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/utils.ts +++ b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/chart_loader.ts @@ -4,12 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -// import { ChartLoader } from '../../../jobs/new_job/common/chart_loader'; import { mlResultsService } from '../../../services/results_service'; -import { - // ModelSnapshot, - CombinedJobWithStats, -} from '../../../../../common/types/anomaly_detection_jobs'; +import { CombinedJobWithStats } from '../../../../../common/types/anomaly_detection_jobs'; import { getSeverityType } from '../../../../../common/util/anomaly_utils'; import { Anomaly } from '../../../jobs/new_job/common/results_loader/results_loader'; import { LineChartPoint } from '../../../jobs/new_job/common/chart_loader/chart_loader'; diff --git a/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx index 58c71aa73ba53..4a68e99c01fb4 100644 --- a/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx +++ b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx @@ -43,8 +43,8 @@ import { } from '../../../../../common/types/anomaly_detection_jobs'; import { ml } from '../../../services/ml_api_service'; import { useNotifications } from '../../../contexts/kibana'; -import { loadEventRateForJob, loadAnomalyDataForJob } from './utils'; -import { LineChartPoint } from '../../../jobs/new_job/common/chart_loader/chart_loader'; +import { loadEventRateForJob, loadAnomalyDataForJob } from './chart_loader'; +import { LineChartPoint } from '../../../jobs/new_job/common/chart_loader'; import { EventRateChart } from '../../../jobs/new_job/pages/components/charts/event_rate_chart/event_rate_chart'; import { Anomaly } from '../../../jobs/new_job/common/results_loader/results_loader'; import { parseInterval } from '../../../../../common/util/parse_interval'; @@ -99,14 +99,14 @@ export const RevertModelSnapshotFlyout: FC = ({ snapshot, snapshots, job, }, [calendarEvents]); async function createChartData() { - // setTimeout(async () => { const bucketSpanMs = parseInterval(job.analysis_config.bucket_span)!.asMilliseconds(); - const d = await loadEventRateForJob(job, bucketSpanMs, 100); - const a = await loadAnomalyDataForJob(job, bucketSpanMs, 100); - setEventRateData(d); - setAnomalies(a[0]); + const eventRate = await loadEventRateForJob(job, bucketSpanMs, 100); + const anomalyData = await loadAnomalyDataForJob(job, bucketSpanMs, 100); + setEventRateData(eventRate); + if (anomalyData[0] !== undefined) { + setAnomalies(anomalyData[0]); + } setChartReady(true); - // }, 250); } function closeWithReload() { @@ -181,7 +181,7 @@ export const RevertModelSnapshotFlyout: FC = ({ snapshot, snapshots, job, - {false && ( + {false && ( // disabled for now <> diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details.js index d146548783f22..9a5cea62cf6ff 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details.js @@ -184,7 +184,6 @@ export class JobDetails extends Component { content: ( - {/* */} ), }); diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts index a9bd1316f7e5c..6aa62da3f0768 100644 --- a/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts +++ b/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts @@ -111,7 +111,7 @@ export const jobs = { forceStopAndCloseJob(jobId: string) { const body = JSON.stringify({ jobId }); - return http({ + return http<{ success: boolean }>({ path: `${basePath()}/jobs/force_stop_and_close_job`, method: 'POST', body, diff --git a/x-pack/plugins/ml/server/routes/anomaly_detectors.ts b/x-pack/plugins/ml/server/routes/anomaly_detectors.ts index eb41bfcf57244..4716beaab164a 100644 --- a/x-pack/plugins/ml/server/routes/anomaly_detectors.ts +++ b/x-pack/plugins/ml/server/routes/anomaly_detectors.ts @@ -629,9 +629,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { /** * @apiGroup AnomalyDetectors * - * @api {get} /api/ml/anomaly_detectors/:jobId/model_snapshots/:snapshotId Get model snapshots by job ID and snapshot ID - * @apiName GetModelSnapshotsById - * @apiDescription Returns the model snapshots for the specified job ID and snapshot ID + * @api {post} /api/ml/anomaly_detectors/:jobId/model_snapshots/:snapshotId/_update update model snapshot by snapshot ID + * @apiName UpdateModelSnapshotsById + * @apiDescription Updates the model snapshot for the specified snapshot ID * * @apiSchema (params) getModelSnapshotsSchema */ @@ -665,9 +665,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { /** * @apiGroup AnomalyDetectors * - * @api {get} /api/ml/anomaly_detectors/:jobId/model_snapshots/:snapshotId Get model snapshots by job ID and snapshot ID + * @api {delete} /api/ml/anomaly_detectors/:jobId/model_snapshots/:snapshotId Delete model snapshots by snapshot ID * @apiName GetModelSnapshotsById - * @apiDescription Returns the model snapshots for the specified job ID and snapshot ID + * @apiDescription Deletes the model snapshot for the specified snapshot ID * * @apiSchema (params) getModelSnapshotsSchema */ @@ -678,7 +678,7 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { params: getModelSnapshotsSchema, }, options: { - tags: ['access:ml:canCreateJob'], // TODO IS THIS CORRECT? + tags: ['access:ml:canCreateJob'], }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { diff --git a/x-pack/plugins/ml/server/routes/job_service.ts b/x-pack/plugins/ml/server/routes/job_service.ts index d8b9b313e91bd..9a7e2c944faae 100644 --- a/x-pack/plugins/ml/server/routes/job_service.ts +++ b/x-pack/plugins/ml/server/routes/job_service.ts @@ -168,11 +168,11 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { /** * @apiGroup JobService * - * @api {post} /api/ml/jobs/close_jobs Close jobs - * @apiName CloseJobs - * @apiDescription Closes one or more anomaly detection jobs + * @api {post} /api/ml/jobs/force_stop_and_close_job Force stop and close job + * @apiName ForceStopAndCloseJob + * @apiDescription Force stops the datafeed and then force closes the anomaly detection job specified by job ID * - * @apiSchema (body) jobIdsSchema + * @apiSchema (body) jobIdSchema */ router.post( { @@ -732,11 +732,11 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { /** * @apiGroup JobService * - * @api {post} /api/ml/jobs/top_categories Get top categories + * @api {post} /api/ml/jobs/revert_model_snapshot Revert model snapshot * @apiName TopCategories - * @apiDescription Returns list of top categories + * @apiDescription Reverts a job to a specified snapshot. Also allows the job to replayed to a specified date and to auto create calendars to skip analysis of specified date ranges * - * @apiSchema (body) topCategoriesSchema + * @apiSchema (body) revertModelSnapshotSchema */ router.post( { From 136a096ec5763827dbfdf470f3eddc4c4d51b062 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Fri, 12 Jun 2020 14:48:33 +0100 Subject: [PATCH 15/18] comments based on review --- .../edit_model_snapshot_flyout.tsx | 3 +- .../model_snapshots/model_snapshots_table.tsx | 23 +++++++++++-- .../create_calendar.tsx | 8 ++++- .../revert_model_snapshot_flyout.tsx | 33 ++++++++++++------- .../plugins/ml/server/routes/job_service.ts | 2 +- .../schemas/anomaly_detectors_schema.ts | 8 ++--- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 8 files changed, 55 insertions(+), 24 deletions(-) diff --git a/x-pack/plugins/ml/public/application/components/model_snapshots/edit_model_snapshot_flyout/edit_model_snapshot_flyout.tsx b/x-pack/plugins/ml/public/application/components/model_snapshots/edit_model_snapshot_flyout/edit_model_snapshot_flyout.tsx index 46eb20e7b23af..c50f826a623c6 100644 --- a/x-pack/plugins/ml/public/application/components/model_snapshots/edit_model_snapshot_flyout/edit_model_snapshot_flyout.tsx +++ b/x-pack/plugins/ml/public/application/components/model_snapshots/edit_model_snapshot_flyout/edit_model_snapshot_flyout.tsx @@ -120,7 +120,8 @@ export const EditModelSnapshotFlyout: FC = ({ snapshot, job, closeFlyout > diff --git a/x-pack/plugins/ml/public/application/components/model_snapshots/model_snapshots_table.tsx b/x-pack/plugins/ml/public/application/components/model_snapshots/model_snapshots_table.tsx index 1b41f0de38755..e3f1b81b483c0 100644 --- a/x-pack/plugins/ml/public/application/components/model_snapshots/model_snapshots_table.tsx +++ b/x-pack/plugins/ml/public/application/components/model_snapshots/model_snapshots_table.tsx @@ -69,9 +69,13 @@ export const ModelSnapshotTable: FC = ({ job, refreshJobList }) => { async function checkJobIsClosed(snapshot: ModelSnapshot) { const state = await getCombinedJobState(job.job_id); if (state === COMBINED_JOB_STATE.UNKNOWN) { - // show toast + // this will only happen if the job has been deleted by another user + // between the time the row has been expended and now + // eslint-disable-next-line no-console + console.error(`Error retrieving state for job ${job.job_id}`); return; } + setCombinedJobState(state); if (state === COMBINED_JOB_STATE.CLOSED) { @@ -168,7 +172,7 @@ export const ModelSnapshotTable: FC = ({ job, refreshJobList }) => { defaultMessage: 'Revert', }), description: i18n.translate('xpack.ml.modelSnapshotTable.actions.revert.description', { - defaultMessage: 'Revert this snapshot', + defaultMessage: 'Revert to this snapshot', }), enabled: () => canCreateJob && canStartStopDatafeed, type: 'icon', @@ -268,9 +272,22 @@ export const ModelSnapshotTable: FC = ({ job, refreshJobList }) => { defaultFocusedButton="confirm" >

+ {combinedJobState === COMBINED_JOB_STATE.OPEN_AND_RUNNING && ( + + )} + {combinedJobState === COMBINED_JOB_STATE.OPEN_AND_STOPPED && ( + + )} +

diff --git a/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/create_calendar.tsx b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/create_calendar.tsx index 14ebff911a0be..57ab498e259a1 100644 --- a/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/create_calendar.tsx +++ b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/create_calendar.tsx @@ -18,6 +18,7 @@ import React, { FC, memo } from 'react'; import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import moment from 'moment'; // @ts-ignore import { formatDate } from '@elastic/eui/lib/services/format'; @@ -131,7 +132,12 @@ export const CreateCalendar: FC = ({ return ( <> -
Select time range for calendar event.
+
+ +
= ({ snapshot, snapshots, job,
@@ -260,7 +260,12 @@ export const RevertModelSnapshotFlyout: FC = ({ snapshot, snapshots, job, = ({ snapshot, snapshots, job, <> = ({ snapshot, snapshots, job, Date: Mon, 15 Jun 2020 21:29:11 +0100 Subject: [PATCH 16/18] changes based on review --- .../ml/common/constants/time_format.ts | 7 + .../annotations_table/annotations_table.js | 3 +- .../components/job_messages/job_messages.tsx | 3 +- .../close_job_confirm/close_job_confirm.tsx | 79 ++++++++++ .../close_job_confirm/index.ts | 7 + .../edit_model_snapshot_flyout.tsx | 18 ++- .../model_snapshots/model_snapshots_table.tsx | 129 +++++------------ .../chart_loader.ts | 128 +++++++++-------- .../create_calendar.tsx | 135 ++++++++++-------- .../revert_model_snapshot_flyout.tsx | 34 +++-- .../forecasts_table/forecasts_table.js | 2 +- .../components/job_details/format_values.js | 2 +- .../components/jobs_list/jobs_list.js | 2 +- .../time_range_selector.js | 3 +- .../charts/event_rate_chart/overlay_range.tsx | 3 +- .../application/services/job_service.js | 7 +- .../edit/events_table/events_table.js | 3 +- .../models/job_service/model_snapshots.ts | 8 +- .../ml/server/routes/anomaly_detectors.ts | 1 + 19 files changed, 325 insertions(+), 249 deletions(-) create mode 100644 x-pack/plugins/ml/common/constants/time_format.ts create mode 100644 x-pack/plugins/ml/public/application/components/model_snapshots/close_job_confirm/close_job_confirm.tsx create mode 100644 x-pack/plugins/ml/public/application/components/model_snapshots/close_job_confirm/index.ts diff --git a/x-pack/plugins/ml/common/constants/time_format.ts b/x-pack/plugins/ml/common/constants/time_format.ts new file mode 100644 index 0000000000000..109dad2d40ac8 --- /dev/null +++ b/x-pack/plugins/ml/common/constants/time_format.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'; diff --git a/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js b/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js index 52d266cde1a2c..a091da6c359d1 100644 --- a/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js +++ b/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js @@ -43,6 +43,7 @@ import { getLatestDataOrBucketTimestamp, isTimeSeriesViewJob, } from '../../../../../common/util/job_utils'; +import { TIME_FORMAT } from '../../../../../common/constants/time_format'; import { annotation$, @@ -50,8 +51,6 @@ import { annotationsRefreshed, } from '../../../services/annotations_service'; -const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'; - /** * Table component for rendering the lists of annotations for an ML job. */ diff --git a/x-pack/plugins/ml/public/application/components/job_messages/job_messages.tsx b/x-pack/plugins/ml/public/application/components/job_messages/job_messages.tsx index fd2b7902833a6..798ceae0f0732 100644 --- a/x-pack/plugins/ml/public/application/components/job_messages/job_messages.tsx +++ b/x-pack/plugins/ml/public/application/components/job_messages/job_messages.tsx @@ -13,10 +13,9 @@ import { i18n } from '@kbn/i18n'; import theme from '@elastic/eui/dist/eui_theme_light.json'; import { JobMessage } from '../../../../common/types/audit_message'; +import { TIME_FORMAT } from '../../../../common/constants/time_format'; import { JobIcon } from '../job_message_icon'; -const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'; - interface JobMessagesProps { messages: JobMessage[]; loading: boolean; diff --git a/x-pack/plugins/ml/public/application/components/model_snapshots/close_job_confirm/close_job_confirm.tsx b/x-pack/plugins/ml/public/application/components/model_snapshots/close_job_confirm/close_job_confirm.tsx new file mode 100644 index 0000000000000..8716f8c85f208 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/model_snapshots/close_job_confirm/close_job_confirm.tsx @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { i18n } from '@kbn/i18n'; + +import { FormattedMessage } from '@kbn/i18n/react'; + +import { EuiOverlayMask, EuiConfirmModal } from '@elastic/eui'; + +import { COMBINED_JOB_STATE } from '../model_snapshots_table'; + +interface Props { + combinedJobState: COMBINED_JOB_STATE; + hideCloseJobModalVisible(): void; + forceCloseJob(): void; +} +export const CloseJobConfirm: FC = ({ + combinedJobState, + hideCloseJobModalVisible, + forceCloseJob, +}) => { + return ( + + +

+ {combinedJobState === COMBINED_JOB_STATE.OPEN_AND_RUNNING && ( + + )} + {combinedJobState === COMBINED_JOB_STATE.OPEN_AND_STOPPED && ( + + )} +
+ +

+
+
+ ); +}; diff --git a/x-pack/plugins/ml/public/application/components/model_snapshots/close_job_confirm/index.ts b/x-pack/plugins/ml/public/application/components/model_snapshots/close_job_confirm/index.ts new file mode 100644 index 0000000000000..195d14160b5e6 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/model_snapshots/close_job_confirm/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { CloseJobConfirm } from './close_job_confirm'; diff --git a/x-pack/plugins/ml/public/application/components/model_snapshots/edit_model_snapshot_flyout/edit_model_snapshot_flyout.tsx b/x-pack/plugins/ml/public/application/components/model_snapshots/edit_model_snapshot_flyout/edit_model_snapshot_flyout.tsx index c50f826a623c6..1e99fd12a9fa6 100644 --- a/x-pack/plugins/ml/public/application/components/model_snapshots/edit_model_snapshot_flyout/edit_model_snapshot_flyout.tsx +++ b/x-pack/plugins/ml/public/application/components/model_snapshots/edit_model_snapshot_flyout/edit_model_snapshot_flyout.tsx @@ -10,7 +10,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC, useState } from 'react'; +import React, { FC, useCallback, useState, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { @@ -49,9 +49,15 @@ export const EditModelSnapshotFlyout: FC = ({ snapshot, job, closeFlyout const [description, setDescription] = useState(snapshot.description); const [retain, setRetain] = useState(snapshot.retain); const [deleteModalVisible, setDeleteModalVisible] = useState(false); - const isCurrentSnapshot = snapshot.snapshot_id === job.model_snapshot_id; + const [isCurrentSnapshot, setIsCurrentSnapshot] = useState( + snapshot.snapshot_id === job.model_snapshot_id + ); + + useEffect(() => { + setIsCurrentSnapshot(snapshot.snapshot_id === job.model_snapshot_id); + }, [snapshot]); - async function updateSnapshot() { + const updateSnapshot = useCallback(async () => { try { await ml.updateModelSnapshot(snapshot.job_id, snapshot.snapshot_id, { description, @@ -65,9 +71,9 @@ export const EditModelSnapshotFlyout: FC = ({ snapshot, job, closeFlyout }), }); } - } + }, [retain, description, snapshot]); - async function deleteSnapshot() { + const deleteSnapshot = useCallback(async () => { try { await ml.deleteModelSnapshot(snapshot.job_id, snapshot.snapshot_id); hideDeleteModal(); @@ -79,7 +85,7 @@ export const EditModelSnapshotFlyout: FC = ({ snapshot, job, closeFlyout }), }); } - } + }, [snapshot]); function closeWithReload() { closeFlyout(true); diff --git a/x-pack/plugins/ml/public/application/components/model_snapshots/model_snapshots_table.tsx b/x-pack/plugins/ml/public/application/components/model_snapshots/model_snapshots_table.tsx index e3f1b81b483c0..64fdd97903b60 100644 --- a/x-pack/plugins/ml/public/application/components/model_snapshots/model_snapshots_table.tsx +++ b/x-pack/plugins/ml/public/application/components/model_snapshots/model_snapshots_table.tsx @@ -4,21 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC, useEffect, useState } from 'react'; +import React, { FC, useEffect, useCallback, useState } from 'react'; import { i18n } from '@kbn/i18n'; -// @ts-ignore -import { formatDate } from '@elastic/eui/lib/services/format'; -import { FormattedMessage } from '@kbn/i18n/react'; - import { EuiFlexGroup, EuiFlexItem, EuiInMemoryTable, EuiLoadingSpinner, - EuiOverlayMask, - EuiConfirmModal, EuiBasicTableColumn, + formatDate, } from '@elastic/eui'; import { checkPermission } from '../../capabilities/check_capabilities'; @@ -26,19 +21,19 @@ import { EditModelSnapshotFlyout } from './edit_model_snapshot_flyout'; import { RevertModelSnapshotFlyout } from './revert_model_snapshot_flyout'; import { ml } from '../../services/ml_api_service'; import { JOB_STATE, DATAFEED_STATE } from '../../../../common/constants/states'; +import { TIME_FORMAT } from '../../../../common/constants/time_format'; +import { CloseJobConfirm } from './close_job_confirm'; import { ModelSnapshot, CombinedJobWithStats, } from '../../../../common/types/anomaly_detection_jobs'; -const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'; - interface Props { job: CombinedJobWithStats; refreshJobList: () => void; } -enum COMBINED_JOB_STATE { +export enum COMBINED_JOB_STATE { OPEN_AND_RUNNING, OPEN_AND_STOPPED, CLOSED, @@ -60,39 +55,42 @@ export const ModelSnapshotTable: FC = ({ job, refreshJobList }) => { loadModelSnapshots(); }, []); - async function loadModelSnapshots() { + const loadModelSnapshots = useCallback(async () => { const { model_snapshots: ms } = await ml.getModelSnapshots(job.job_id); setSnapshots(ms); setSnapshotsLoaded(true); - } + }, [job]); - async function checkJobIsClosed(snapshot: ModelSnapshot) { - const state = await getCombinedJobState(job.job_id); - if (state === COMBINED_JOB_STATE.UNKNOWN) { - // this will only happen if the job has been deleted by another user - // between the time the row has been expended and now - // eslint-disable-next-line no-console - console.error(`Error retrieving state for job ${job.job_id}`); - return; - } + const checkJobIsClosed = useCallback( + async (snapshot: ModelSnapshot) => { + const state = await getCombinedJobState(job.job_id); + if (state === COMBINED_JOB_STATE.UNKNOWN) { + // this will only happen if the job has been deleted by another user + // between the time the row has been expended and now + // eslint-disable-next-line no-console + console.error(`Error retrieving state for job ${job.job_id}`); + return; + } - setCombinedJobState(state); + setCombinedJobState(state); - if (state === COMBINED_JOB_STATE.CLOSED) { - // show flyout - setRevertSnapshot(snapshot); - } else { - // show close job modal - setCloseJobModalVisible(snapshot); - } - } + if (state === COMBINED_JOB_STATE.CLOSED) { + // show flyout + setRevertSnapshot(snapshot); + } else { + // show close job modal + setCloseJobModalVisible(snapshot); + } + }, + [job] + ); function hideCloseJobModalVisible() { setCombinedJobState(null); setCloseJobModalVisible(null); } - async function forceCloseJob() { + const forceCloseJob = useCallback(async () => { await ml.jobs.forceStopAndCloseJob(job.job_id); if (closeJobModalVisible !== null) { const state = await getCombinedJobState(job.job_id); @@ -101,23 +99,23 @@ export const ModelSnapshotTable: FC = ({ job, refreshJobList }) => { } } hideCloseJobModalVisible(); - } + }, [job, closeJobModalVisible]); - function closeEditFlyout(reload: boolean) { + const closeEditFlyout = useCallback((reload: boolean) => { setEditSnapshot(null); if (reload) { loadModelSnapshots(); } - } + }, []); - function closeRevertFlyout(reload: boolean) { + const closeRevertFlyout = useCallback((reload: boolean) => { setRevertSnapshot(null); if (reload) { loadModelSnapshots(); // wait half a second before refreshing the jobs list setTimeout(refreshJobList, 500); } - } + }, []); const columns: Array> = [ { @@ -238,60 +236,11 @@ export const ModelSnapshotTable: FC = ({ job, refreshJobList }) => { )} {closeJobModalVisible !== null && combinedJobState !== null && ( - - -

- {combinedJobState === COMBINED_JOB_STATE.OPEN_AND_RUNNING && ( - - )} - {combinedJobState === COMBINED_JOB_STATE.OPEN_AND_STOPPED && ( - - )} -
- -

-
-
+ )} ); diff --git a/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/chart_loader.ts b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/chart_loader.ts index a4a6926c1bbda..2da1e914b8139 100644 --- a/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/chart_loader.ts +++ b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/chart_loader.ts @@ -4,80 +4,84 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mlResultsService } from '../../../services/results_service'; +import { MlResultsService } from '../../../services/results_service'; import { CombinedJobWithStats } from '../../../../../common/types/anomaly_detection_jobs'; import { getSeverityType } from '../../../../../common/util/anomaly_utils'; import { Anomaly } from '../../../jobs/new_job/common/results_loader/results_loader'; import { LineChartPoint } from '../../../jobs/new_job/common/chart_loader/chart_loader'; -export async function loadEventRateForJob( - job: CombinedJobWithStats, - bucketSpanMs: number, - bars: number -): Promise { - const intervalMs = Math.max( - Math.floor( - (job.data_counts.latest_record_timestamp - job.data_counts.earliest_record_timestamp) / bars - ), - bucketSpanMs - ); - const resp = await mlResultsService.getEventRateData( - job.datafeed_config.indices.join(), - job.datafeed_config.query, - job.data_description.time_field, - job.data_counts.earliest_record_timestamp, - job.data_counts.latest_record_timestamp, - intervalMs - ); - if (resp.error !== undefined) { - throw resp.error; - } +export function chartLoaderProvider(mlResultsService: MlResultsService) { + async function loadEventRateForJob( + job: CombinedJobWithStats, + bucketSpanMs: number, + bars: number + ): Promise { + const intervalMs = Math.max( + Math.floor( + (job.data_counts.latest_record_timestamp - job.data_counts.earliest_record_timestamp) / bars + ), + bucketSpanMs + ); + const resp = await mlResultsService.getEventRateData( + job.datafeed_config.indices.join(), + job.datafeed_config.query, + job.data_description.time_field, + job.data_counts.earliest_record_timestamp, + job.data_counts.latest_record_timestamp, + intervalMs + ); + if (resp.error !== undefined) { + throw resp.error; + } + + const events = Object.entries(resp.results).map(([time, value]) => ({ + time: +time, + value: value as number, + })); - const events = Object.entries(resp.results).map(([time, value]) => ({ - time: +time, - value: value as number, - })); + if (events.length) { + // add one extra bucket with a value of 0 + // so that an extra blank bar gets drawn at the end of the chart + // this solves an issue with elastic charts where the rect annotation + // never covers the last bar. + events.push({ time: events[events.length - 1].time + intervalMs, value: 0 }); + } - if (events.length) { - // add one extra bucket with a value of 0 - // so that an extra blank bar gets drawn at the end of the chart - // this solves an issue with elastic charts where the rect annotation - // never covers the last bar. - events.push({ time: events[events.length - 1].time + intervalMs, value: 0 }); + return events; } - return events; -} + async function loadAnomalyDataForJob( + job: CombinedJobWithStats, + bucketSpanMs: number, + bars: number + ) { + const intervalMs = Math.max( + Math.floor( + (job.data_counts.latest_record_timestamp - job.data_counts.earliest_record_timestamp) / bars + ), + bucketSpanMs + ); -export async function loadAnomalyDataForJob( - job: CombinedJobWithStats, - bucketSpanMs: number, - bars: number -) { - const intervalMs = Math.max( - Math.floor( - (job.data_counts.latest_record_timestamp - job.data_counts.earliest_record_timestamp) / bars - ), - bucketSpanMs - ); + const resp = await mlResultsService.getScoresByBucket( + [job.job_id], + job.data_counts.earliest_record_timestamp, + job.data_counts.latest_record_timestamp, + intervalMs, + 1 + ); - const resp = await mlResultsService.getScoresByBucket( - [job.job_id], - job.data_counts.earliest_record_timestamp, - job.data_counts.latest_record_timestamp, - intervalMs, - 1 - ); + const results = resp.results[job.job_id]; + if (results === undefined) { + return []; + } - const results = resp.results[job.job_id]; - if (results === undefined) { - return []; + const anomalies: Record = {}; + anomalies[0] = Object.entries(results).map( + ([time, value]) => + ({ time: +time, value, severity: getSeverityType(value as number) } as Anomaly) + ); + return anomalies; } - const anomalies: Record = {}; - anomalies[0] = Object.entries(results).map( - ([time, value]) => - ({ time: +time, value, severity: getSeverityType(value as number) } as Anomaly) - ); - return anomalies; + return { loadEventRateForJob, loadAnomalyDataForJob }; } diff --git a/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/create_calendar.tsx b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/create_calendar.tsx index 57ab498e259a1..c93800d907af0 100644 --- a/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/create_calendar.tsx +++ b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/create_calendar.tsx @@ -16,12 +16,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC, memo } from 'react'; +import React, { FC, Fragment, useCallback, memo } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import moment from 'moment'; -// @ts-ignore -import { formatDate } from '@elastic/eui/lib/services/format'; import { XYBrushArea } from '@elastic/charts'; import { EuiFlexGroup, @@ -69,65 +67,80 @@ export const CreateCalendar: FC = ({ const { euiTheme } = useCurrentEuiTheme(); - function onBrushEnd({ x }: XYBrushArea) { - if (x && x.length === 2) { - const end = x[1] < minSelectableTimeStamp ? null : x[1]; - if (end !== null) { - const start = x[0] < minSelectableTimeStamp ? minSelectableTimeStamp : x[0]; + const onBrushEnd = useCallback( + ({ x }: XYBrushArea) => { + if (x && x.length === 2) { + const end = x[1] < minSelectableTimeStamp ? null : x[1]; + if (end !== null) { + const start = x[0] < minSelectableTimeStamp ? minSelectableTimeStamp : x[0]; + setCalendarEvents([ + ...calendarEvents, + { + start: moment(start), + end: moment(end), + description: createDefaultEventDescription(calendarEvents.length + 1), + }, + ]); + } + } + }, + [calendarEvents] + ); + + const setStartDate = useCallback( + (start: moment.Moment | null, index: number) => { + const event = calendarEvents[index]; + if (event === undefined) { setCalendarEvents([ ...calendarEvents, - { - start: moment(start), - end: moment(end), - description: createDefaultEventDescription(calendarEvents.length + 1), - }, + { start, end: null, description: createDefaultEventDescription(index) }, ]); + } else { + event.start = start; + setCalendarEvents([...calendarEvents]); } - } - } - - function setStartDate(start: moment.Moment | null, index: number) { - const event = calendarEvents[index]; - if (event === undefined) { - setCalendarEvents([ - ...calendarEvents, - { start, end: null, description: createDefaultEventDescription(index) }, - ]); - } else { - event.start = start; - setCalendarEvents([...calendarEvents]); - } - } + }, + [calendarEvents] + ); - function setEndDate(end: moment.Moment | null, index: number) { - const event = calendarEvents[index]; - if (event === undefined) { - setCalendarEvents([ - ...calendarEvents, - { start: null, end, description: createDefaultEventDescription(index) }, - ]); - } else { - event.end = end; - setCalendarEvents([...calendarEvents]); - } - } + const setEndDate = useCallback( + (end: moment.Moment | null, index: number) => { + const event = calendarEvents[index]; + if (event === undefined) { + setCalendarEvents([ + ...calendarEvents, + { start: null, end, description: createDefaultEventDescription(index) }, + ]); + } else { + event.end = end; + setCalendarEvents([...calendarEvents]); + } + }, + [calendarEvents] + ); - const setDescription = (description: string, index: number) => { - const event = calendarEvents[index]; - if (event !== undefined) { - event.description = description; - setCalendarEvents([...calendarEvents]); - } - }; + const setDescription = useCallback( + (description: string, index: number) => { + const event = calendarEvents[index]; + if (event !== undefined) { + event.description = description; + setCalendarEvents([...calendarEvents]); + } + }, + [calendarEvents] + ); - function removeCalendarEvent(index: number) { - if (calendarEvents[index] !== undefined) { - const ce = [...calendarEvents]; - ce.splice(index, 1); - setCalendarEvents(ce); - } - } + const removeCalendarEvent = useCallback( + (index: number) => { + if (calendarEvents[index] !== undefined) { + const ce = [...calendarEvents]; + ce.splice(index, 1); + setCalendarEvents(ce); + } + }, + [calendarEvents] + ); return ( <> @@ -152,7 +165,7 @@ export const CreateCalendar: FC = ({ {calendarEvents.map((c, i) => ( -
+ @@ -207,7 +220,7 @@ export const CreateCalendar: FC = ({ )} > setDescription(e.target.value, i)} /> @@ -239,7 +252,7 @@ export const CreateCalendar: FC = ({ -
+ ))} ); @@ -285,5 +298,11 @@ function filterIncompleteEvents(event: CalendarEvent): event is CalendarEvent { } function createDefaultEventDescription(index: number) { - return `Auto created event ${index}`; + return i18n.translate( + 'xpack.ml.revertModelSnapshotFlyout.createCalendar.defaultEventDescription', + { + defaultMessage: 'Auto created event {index}', + values: { index }, + } + ); } diff --git a/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx index b643daaf12c1e..ad5915b39d521 100644 --- a/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx +++ b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx @@ -10,11 +10,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC, useState, useEffect } from 'react'; +import React, { FC, useState, useCallback, useMemo, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import {} from 'lodash'; -// @ts-ignore -import { formatDate } from '@elastic/eui/lib/services/format'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiFlyout, @@ -35,6 +33,7 @@ import { EuiHorizontalRule, EuiSuperSelect, EuiText, + formatDate, } from '@elastic/eui'; import { @@ -43,15 +42,15 @@ import { } from '../../../../../common/types/anomaly_detection_jobs'; import { ml } from '../../../services/ml_api_service'; import { useNotifications } from '../../../contexts/kibana'; -import { loadEventRateForJob, loadAnomalyDataForJob } from './chart_loader'; +import { chartLoaderProvider } from './chart_loader'; +import { mlResultsService } from '../../../services/results_service'; import { LineChartPoint } from '../../../jobs/new_job/common/chart_loader'; import { EventRateChart } from '../../../jobs/new_job/pages/components/charts/event_rate_chart/event_rate_chart'; import { Anomaly } from '../../../jobs/new_job/common/results_loader/results_loader'; import { parseInterval } from '../../../../../common/util/parse_interval'; +import { TIME_FORMAT } from '../../../../../common/constants/time_format'; import { CreateCalendar, CalendarEvent } from './create_calendar'; -const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'; - interface Props { snapshot: ModelSnapshot; snapshots: ModelSnapshot[]; @@ -61,6 +60,10 @@ interface Props { export const RevertModelSnapshotFlyout: FC = ({ snapshot, snapshots, job, closeFlyout }) => { const { toasts } = useNotifications(); + const { loadAnomalyDataForJob, loadEventRateForJob } = useMemo( + () => chartLoaderProvider(mlResultsService), + [] + ); const [currentSnapshot, setCurrentSnapshot] = useState(snapshot); const [revertModalVisible, setRevertModalVisible] = useState(false); const [replay, setReplay] = useState(false); @@ -98,7 +101,7 @@ export const RevertModelSnapshotFlyout: FC = ({ snapshot, snapshots, job, } }, [calendarEvents]); - async function createChartData() { + const createChartData = useCallback(async () => { const bucketSpanMs = parseInterval(job.analysis_config.bucket_span)!.asMilliseconds(); const eventRate = await loadEventRateForJob(job, bucketSpanMs, 100); const anomalyData = await loadAnomalyDataForJob(job, bucketSpanMs, 100); @@ -107,7 +110,7 @@ export const RevertModelSnapshotFlyout: FC = ({ snapshot, snapshots, job, setAnomalies(anomalyData[0]); } setChartReady(true); - } + }, [job]); function closeWithReload() { closeFlyout(true); @@ -128,13 +131,14 @@ export const RevertModelSnapshotFlyout: FC = ({ snapshot, snapshots, job, const end = replay && runInRealTime === false ? job.data_counts.latest_record_timestamp : undefined; try { - const events = replay - ? calendarEvents.filter(filterIncompleteEvents).map((c) => ({ - start: c.start!.valueOf(), - end: c.end!.valueOf(), - description: c.description, - })) - : undefined; + const events = + replay && createCalendar + ? calendarEvents.filter(filterIncompleteEvents).map((c) => ({ + start: c.start!.valueOf(), + end: c.end!.valueOf(), + description: c.description, + })) + : undefined; await ml.jobs.revertModelSnapshot( job.job_id, diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/forecasts_table/forecasts_table.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/forecasts_table/forecasts_table.js index 817715dbf6413..2f40941fd20fe 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/forecasts_table/forecasts_table.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/forecasts_table/forecasts_table.js @@ -21,6 +21,7 @@ import { import { formatDate, formatNumber } from '@elastic/eui/lib/services/format'; import { FORECAST_REQUEST_STATE } from '../../../../../../../common/constants/states'; +import { TIME_FORMAT } from '../../../../../../../common/constants/time_format'; import { addItemToRecentlyAccessed } from '../../../../../util/recently_accessed'; import { mlForecastService } from '../../../../../services/forecast_service'; import { i18n } from '@kbn/i18n'; @@ -31,7 +32,6 @@ import { } from '../../../../../../../common/util/job_utils'; const MAX_FORECASTS = 500; -const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'; /** * Table component for rendering the lists of forecasts run on an ML job. diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/format_values.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/format_values.js index 9194f7537cf3d..883ddfca70cd7 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/format_values.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/format_values.js @@ -8,8 +8,8 @@ import numeral from '@elastic/numeral'; import { formatDate } from '@elastic/eui/lib/services/format'; import { roundToDecimalPlace } from '../../../../formatters/round_to_decimal_place'; import { toLocaleString } from '../../../../util/string_utils'; +import { TIME_FORMAT } from '../../../../../../common/constants/time_format'; -const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'; const DATA_FORMAT = '0.0 b'; function formatData(txt) { diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js index 0afaca3ec12e1..23b68551ca0f5 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js @@ -15,6 +15,7 @@ import { ResultLinks, actionsMenuContent } from '../job_actions'; import { JobDescription } from './job_description'; import { JobIcon } from '../../../../components/job_message_icon'; import { getJobIdUrl } from '../../../../util/get_job_id_url'; +import { TIME_FORMAT } from '../../../../../../common/constants/time_format'; import { EuiBadge, EuiBasicTable, EuiButtonIcon, EuiLink, EuiScreenReaderOnly } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -22,7 +23,6 @@ import { FormattedMessage } from '@kbn/i18n/react'; const PAGE_SIZE = 10; const PAGE_SIZE_OPTIONS = [10, 25, 50]; -const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'; // 'isManagementTable' bool prop to determine when to configure table for use in Kibana management page export class JobsList extends Component { diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/start_datafeed_modal/time_range_selector/time_range_selector.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/start_datafeed_modal/time_range_selector/time_range_selector.js index 55c87bbc90b10..8cf244c3c7d8a 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/start_datafeed_modal/time_range_selector/time_range_selector.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/start_datafeed_modal/time_range_selector/time_range_selector.js @@ -13,8 +13,7 @@ import { EuiDatePicker, EuiFieldText } from '@elastic/eui'; import moment from 'moment'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; - -const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'; +import { TIME_FORMAT } from '../../../../../../../common/constants/time_format'; export class TimeRangeSelector extends Component { constructor(props) { diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/event_rate_chart/overlay_range.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/event_rate_chart/overlay_range.tsx index d5f88146d777a..a35c2d9c833a8 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/event_rate_chart/overlay_range.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/event_rate_chart/overlay_range.tsx @@ -10,8 +10,7 @@ import { formatDate } from '@elastic/eui/lib/services/format'; import { EuiIcon } from '@elastic/eui'; import { RectAnnotation, LineAnnotation, AnnotationDomainTypes } from '@elastic/charts'; import { LineChartPoint } from '../../../../common/chart_loader'; - -const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'; +import { TIME_FORMAT } from '../../../../../../../../common/constants/time_format'; interface Props { overlayKey: number; diff --git a/x-pack/plugins/ml/public/application/services/job_service.js b/x-pack/plugins/ml/public/application/services/job_service.js index a3be479571702..6c0f393c267aa 100644 --- a/x-pack/plugins/ml/public/application/services/job_service.js +++ b/x-pack/plugins/ml/public/application/services/job_service.js @@ -13,6 +13,7 @@ import { ml } from './ml_api_service'; import { mlMessageBarService } from '../components/messagebar'; import { isWebUrl } from '../util/url_utils'; import { ML_DATA_PREVIEW_COUNT } from '../../../common/util/job_utils'; +import { TIME_FORMAT } from '../../../common/constants/time_format'; import { parseInterval } from '../../../common/util/parse_interval'; const msgs = mlMessageBarService; @@ -929,10 +930,8 @@ function createResultsUrlForJobs(jobsList, resultsPage) { } } - const timeFormat = 'YYYY-MM-DD HH:mm:ss'; - - const fromString = moment(from).format(timeFormat); // Defaults to 'now' if 'from' is undefined - const toString = moment(to).format(timeFormat); // Defaults to 'now' if 'to' is undefined + const fromString = moment(from).format(TIME_FORMAT); // Defaults to 'now' if 'from' is undefined + const toString = moment(to).format(TIME_FORMAT); // Defaults to 'now' if 'to' is undefined const jobIds = jobsList.map((j) => j.id); return createResultsUrl(jobIds, fromString, toString, resultsPage); diff --git a/x-pack/plugins/ml/public/application/settings/calendars/edit/events_table/events_table.js b/x-pack/plugins/ml/public/application/settings/calendars/edit/events_table/events_table.js index 815aa4810ebaa..9069e8078fca6 100644 --- a/x-pack/plugins/ml/public/application/settings/calendars/edit/events_table/events_table.js +++ b/x-pack/plugins/ml/public/application/settings/calendars/edit/events_table/events_table.js @@ -12,8 +12,7 @@ import { EuiButton, EuiButtonEmpty, EuiInMemoryTable, EuiSpacer } from '@elastic import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; - -export const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'; +import { TIME_FORMAT } from '../../../../../../common/constants/time_format'; function DeleteButton({ onClick, canDeleteCalendar }) { return ( diff --git a/x-pack/plugins/ml/server/models/job_service/model_snapshots.ts b/x-pack/plugins/ml/server/models/job_service/model_snapshots.ts index 0a20ebadd4cfc..4ffae17fc1c06 100644 --- a/x-pack/plugins/ml/server/models/job_service/model_snapshots.ts +++ b/x-pack/plugins/ml/server/models/job_service/model_snapshots.ts @@ -5,6 +5,7 @@ */ import Boom from 'boom'; +import { i18n } from '@kbn/i18n'; import { APICaller } from 'kibana/server'; import { ModelSnapshot } from '../../../common/types/anomaly_detection_jobs'; import { datafeedsProvider, MlDatafeedsResponse } from './datafeeds'; @@ -75,7 +76,12 @@ export function modelSnapshotProvider(callAsCurrentUser: APICaller) { const calendar: FormCalendar = { calendarId: String(Date.now()), job_ids: [jobId], - description: 'auto created', + description: i18n.translate( + 'xpack.ml.models.jobService.revertModelSnapshot.autoCreatedCalendar.description', + { + defaultMessage: 'Auto created', + } + ), events: calendarEvents.map((s) => ({ description: s.description, start_time: s.start, diff --git a/x-pack/plugins/ml/server/routes/anomaly_detectors.ts b/x-pack/plugins/ml/server/routes/anomaly_detectors.ts index 4716beaab164a..0b544d4eca0ed 100644 --- a/x-pack/plugins/ml/server/routes/anomaly_detectors.ts +++ b/x-pack/plugins/ml/server/routes/anomaly_detectors.ts @@ -634,6 +634,7 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { * @apiDescription Updates the model snapshot for the specified snapshot ID * * @apiSchema (params) getModelSnapshotsSchema + * @apiSchema (body) updateModelSnapshotSchema */ router.post( { From 2253b7b7497de7f101bc76c8eec59e098b23d050 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Tue, 16 Jun 2020 06:35:54 +0100 Subject: [PATCH 17/18] fixing include --- .../application/settings/calendars/edit/events_table/index.js | 2 +- .../settings/calendars/edit/new_event_modal/new_event_modal.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/ml/public/application/settings/calendars/edit/events_table/index.js b/x-pack/plugins/ml/public/application/settings/calendars/edit/events_table/index.js index 0910a04051bf4..89bd0235df996 100644 --- a/x-pack/plugins/ml/public/application/settings/calendars/edit/events_table/index.js +++ b/x-pack/plugins/ml/public/application/settings/calendars/edit/events_table/index.js @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { EventsTable, TIME_FORMAT } from './events_table'; +export { EventsTable } from './events_table'; diff --git a/x-pack/plugins/ml/public/application/settings/calendars/edit/new_event_modal/new_event_modal.js b/x-pack/plugins/ml/public/application/settings/calendars/edit/new_event_modal/new_event_modal.js index 8380fd36b458c..d80e248674a8f 100644 --- a/x-pack/plugins/ml/public/application/settings/calendars/edit/new_event_modal/new_event_modal.js +++ b/x-pack/plugins/ml/public/application/settings/calendars/edit/new_event_modal/new_event_modal.js @@ -24,7 +24,7 @@ import { EuiFlexItem, } from '@elastic/eui'; import moment from 'moment'; -import { TIME_FORMAT } from '../events_table'; +import { TIME_FORMAT } from '../../../../../../common/constants/time_format'; import { generateTempId } from '../utils'; import { i18n } from '@kbn/i18n'; From 9dc64c2b0b372a75a69abc50521fd67bcf588750 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Tue, 16 Jun 2020 10:48:04 +0100 Subject: [PATCH 18/18] adding useMemo to theme function --- .../components/color_range_legend/use_color_range.ts | 8 ++++++-- .../revert_model_snapshot_flyout/create_calendar.tsx | 6 ++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/ml/public/application/components/color_range_legend/use_color_range.ts b/x-pack/plugins/ml/public/application/components/color_range_legend/use_color_range.ts index 6a064f683b3aa..f674372da6785 100644 --- a/x-pack/plugins/ml/public/application/components/color_range_legend/use_color_range.ts +++ b/x-pack/plugins/ml/public/application/components/color_range_legend/use_color_range.ts @@ -5,7 +5,7 @@ */ import d3 from 'd3'; - +import { useMemo } from 'react'; import euiThemeLight from '@elastic/eui/dist/eui_theme_light.json'; import euiThemeDark from '@elastic/eui/dist/eui_theme_dark.json'; @@ -188,5 +188,9 @@ export const useColorRange = ( }; export function useCurrentEuiTheme() { - return { euiTheme: useUiSettings().get('theme:darkMode') ? euiThemeDark : euiThemeLight }; + const uiSettings = useUiSettings(); + return useMemo( + () => ({ euiTheme: uiSettings.get('theme:darkMode') ? euiThemeDark : euiThemeLight }), + [uiSettings] + ); } diff --git a/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/create_calendar.tsx b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/create_calendar.tsx index c93800d907af0..937c394e35bc1 100644 --- a/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/create_calendar.tsx +++ b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/create_calendar.tsx @@ -161,6 +161,7 @@ export const CreateCalendar: FC = ({ end: c.end!.valueOf(), }))} onBrushEnd={onBrushEnd} + overlayColor={euiTheme.euiColorPrimary} /> @@ -264,10 +265,11 @@ interface ChartProps { loading: boolean; onBrushEnd(area: XYBrushArea): void; overlayRanges: Array<{ start: number; end: number }>; + overlayColor: string; } const Chart: FC = memo( - ({ eventRateData, anomalies, loading, onBrushEnd, overlayRanges }) => ( + ({ eventRateData, anomalies, loading, onBrushEnd, overlayRanges, overlayColor }) => ( = memo( overlayRanges={overlayRanges.map((c) => ({ start: c.start, end: c.end, - color: '#0000ff', + color: overlayColor, showMarker: false, }))} onBrushEnd={onBrushEnd}