From 0ff3940a1551915c881205d8812dca9cc7af87ed Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Wed, 9 Oct 2024 02:40:45 -0600 Subject: [PATCH] [ML] Anomaly Detection: adds ability to delete forecasts from job (#194896) ## Summary Related issues: - https://github.com/elastic/kibana/issues/18511 - https://github.com/elastic/kibana/issues/192301 In this PR, in Job management > expanded row > Forecasts tab - a delete action has been added to each row in the forecasts table. A confirmation modal allows the user to confirm the delete action. In the SMV view, the forecast being currently viewed is now highlighted in the Forecast modal to make it easier to identify. ![image](https://github.com/user-attachments/assets/87814889-d41d-4780-98ab-695c6f12a023) image image Dark mode: image ### Checklist Delete any items that are not applicable to this PR. - [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed - [ ] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [ ] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [ ] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [ ] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) --------- Co-authored-by: Elastic Machine Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> (cherry picked from commit f4a4a681f58c1c64eb8a05070b44f0605c625458) --- .../plugins/ml/common/types/capabilities.ts | 2 + .../capabilities/check_capabilities.ts | 4 + .../forecasts_table/forecasts_table.js | 168 ++++++++++++++---- .../timeseriesexplorer/state_manager.tsx | 7 +- .../services/ml_api_service/index.ts | 12 ++ .../forecasting_modal/forecasting_modal.js | 17 +- .../forecasting_modal/forecasts_list.js | 44 +++-- .../components/forecasting_modal/modal.js | 7 +- .../timeseriesexplorer/timeseriesexplorer.js | 1 + .../timeseriesexplorer_embeddable_chart.js | 1 + .../capabilities/check_capabilities.test.ts | 8 +- .../ml/server/routes/anomaly_detectors.ts | 36 ++++ .../schemas/anomaly_detectors_schema.ts | 5 + .../translations/translations/fr-FR.json | 1 - .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - .../anomaly_detectors/forecast_with_spaces.ts | 32 +++- .../apis/ml/system/capabilities.ts | 4 +- .../apis/ml/system/space_capabilities.ts | 6 +- 19 files changed, 283 insertions(+), 74 deletions(-) diff --git a/x-pack/plugins/ml/common/types/capabilities.ts b/x-pack/plugins/ml/common/types/capabilities.ts index 7dc841a5cb4d7..e7a097fae7dc4 100644 --- a/x-pack/plugins/ml/common/types/capabilities.ts +++ b/x-pack/plugins/ml/common/types/capabilities.ts @@ -58,6 +58,7 @@ export const adminMlCapabilities = { canResetJob: false, canUpdateJob: false, canForecastJob: false, + canDeleteForecast: false, canCreateDatafeed: false, canDeleteDatafeed: false, canStartStopDatafeed: false, @@ -222,6 +223,7 @@ export const featureCapabilities: FeatureCapabilities = { 'canResetJob', 'canUpdateJob', 'canForecastJob', + 'canDeleteForecast', 'canCreateDatafeed', 'canDeleteDatafeed', 'canStartStopDatafeed', diff --git a/x-pack/plugins/ml/public/application/capabilities/check_capabilities.ts b/x-pack/plugins/ml/public/application/capabilities/check_capabilities.ts index ed930b857473d..734993a4e4a6e 100644 --- a/x-pack/plugins/ml/public/application/capabilities/check_capabilities.ts +++ b/x-pack/plugins/ml/public/application/capabilities/check_capabilities.ts @@ -269,6 +269,10 @@ export function createPermissionFailureMessage(privilegeType: keyof MlCapabiliti message = i18n.translate('xpack.ml.privilege.noPermission.runForecastsTooltip', { defaultMessage: 'You do not have permission to run forecasts.', }); + } else if (privilegeType === 'canDeleteForecast') { + message = i18n.translate('xpack.ml.privilege.noPermission.deleteForecastsTooltip', { + defaultMessage: 'You do not have permission to delete forecasts.', + }); } return i18n.translate('xpack.ml.privilege.pleaseContactAdministratorTooltip', { defaultMessage: '{message} Please contact your administrator.', 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 5f1cbb1c76ca0..bfed613b9ad5d 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 @@ -9,8 +9,8 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { - EuiButtonIcon, EuiCallOut, + EuiConfirmModal, EuiFlexGroup, EuiFlexItem, EuiInMemoryTable, @@ -32,9 +32,43 @@ import { isTimeSeriesViewJob, } from '../../../../../../../common/util/job_utils'; import { ML_APP_LOCATOR, ML_PAGES } from '../../../../../../../common/constants/locator'; +import { checkPermission } from '../../../../../capabilities/check_capabilities'; const MAX_FORECASTS = 500; +const DeleteForecastConfirm = ({ onCancel, onConfirm }) => ( + +

+ +

+
+); + /** * Table component for rendering the lists of forecasts run on an ML job. */ @@ -44,6 +78,7 @@ export class ForecastsTable extends Component { this.state = { isLoading: props.job.data_counts.processed_record_count !== 0, forecasts: [], + forecastIdToDelete: undefined, }; this.mlForecastService = forecastServiceFactory(constructorContext.services.mlServices.mlApi); } @@ -54,6 +89,11 @@ export class ForecastsTable extends Component { static contextType = context; componentDidMount() { + this.loadForecasts(); + this.canDeleteJobForecast = checkPermission('canDeleteForecast'); + } + + async loadForecasts() { const dataCounts = this.props.job.data_counts; if (dataCounts.processed_record_count > 0) { // Get the list of all the forecasts with results at or later than the specified 'from' time. @@ -163,6 +203,36 @@ export class ForecastsTable extends Component { await navigateToUrl(singleMetricViewerForecastLink); } + async deleteForecast(forecastId) { + const { + services: { + mlServices: { mlApi }, + }, + } = this.context; + + this.setState({ + isLoading: true, + forecastIdToDelete: undefined, + }); + + try { + await mlApi.deleteForecast({ jobId: this.props.job.job_id, forecastId }); + } catch (error) { + this.setState({ + forecastIdToDelete: undefined, + isLoading: false, + errorMessage: i18n.translate( + 'xpack.ml.jobsList.jobDetails.forecastsTable.deleteForecastErrorMessage', + { + defaultMessage: 'An error occurred when deleting the forecast.', + } + ), + }); + } + + this.loadForecasts(); + } + render() { if (this.state.isLoading === true) { return ( @@ -302,48 +372,74 @@ export class ForecastsTable extends Component { textOnly: true, }, { - name: i18n.translate('xpack.ml.jobsList.jobDetails.forecastsTable.viewLabel', { - defaultMessage: 'View', + width: '75px', + name: i18n.translate('xpack.ml.jobsList.jobDetails.forecastsTable.actionsLabel', { + defaultMessage: 'Actions', }), - width: '60px', - render: (forecast) => { - const viewForecastAriaLabel = i18n.translate( - 'xpack.ml.jobsList.jobDetails.forecastsTable.viewAriaLabel', - { - defaultMessage: 'View forecast created at {createdDate}', - values: { - createdDate: timeFormatter(forecast.forecast_create_timestamp), - }, - } - ); - - return ( - this.openSingleMetricView(forecast)} - isDisabled={ + actions: [ + { + description: i18n.translate('xpack.ml.jobsList.jobDetails.forecastsTable.viewLabel', { + defaultMessage: 'View', + }), + type: 'icon', + icon: 'eye', + enabled: (forecast) => + !( this.props.job.blocked !== undefined || forecast.forecast_status !== FORECAST_REQUEST_STATE.FINISHED - } - iconType="singleMetricViewer" - aria-label={viewForecastAriaLabel} - data-test-subj="mlJobListForecastTabOpenSingleMetricViewButton" - /> - ); - }, + ), + onClick: (forecast) => this.openSingleMetricView(forecast), + 'data-test-subj': 'mlJobListForecastTabOpenSingleMetricViewButton', + }, + ...(this.canDeleteJobForecast + ? [ + { + description: i18n.translate( + 'xpack.ml.jobsList.jobDetails.forecastsTable.deleteForecastDescription', + { + defaultMessage: 'Delete forecast', + } + ), + type: 'icon', + icon: 'trash', + color: 'danger', + enabled: () => this.state.isLoading === false, + onClick: (item) => { + this.setState({ + forecastIdToDelete: item.forecast_id, + }); + }, + 'data-test-subj': 'mlJobListForecastTabDeleteForecastButton', + }, + ] + : []), + ], }, ]; return ( - + <> + + {this.state.forecastIdToDelete !== undefined ? ( + + this.setState({ + forecastIdToDelete: undefined, + }) + } + onConfirm={() => this.deleteForecast(this.state.forecastIdToDelete)} + /> + ) : null} + ); } } diff --git a/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer/state_manager.tsx b/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer/state_manager.tsx index fa1753a4342fc..309f24dd1c62b 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer/state_manager.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer/state_manager.tsx @@ -216,6 +216,10 @@ export const TimeSeriesExplorerUrlStateManager: FC { + if (selectedForecastIdProp !== selectedForecastId) { + setSelectedForecastIdProp(undefined); + } + if ( autoZoomDuration !== undefined && boundsMinMs !== undefined && @@ -223,9 +227,6 @@ export const TimeSeriesExplorerUrlStateManager: FC { 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 fa6d179059eec..868ca0d5baa0f 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 @@ -103,6 +103,10 @@ export interface GetModelSnapshotsResponse { model_snapshots: ModelSnapshot[]; } +export interface DeleteForecastResponse { + acknowledged: boolean; +} + export function mlApiProvider(httpService: HttpService) { return { getJobs(obj?: { jobId?: string }) { @@ -368,6 +372,14 @@ export function mlApiProvider(httpService: HttpService) { }); }, + deleteForecast({ jobId, forecastId }: { jobId: string; forecastId: string }) { + return httpService.http({ + path: `${ML_INTERNAL_BASE_PATH}/anomaly_detectors/${jobId}/_forecast/${forecastId}`, + method: 'DELETE', + version: '1', + }); + }, + overallBuckets({ jobId, topN, diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/forecasting_modal.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/forecasting_modal.js index 5ac0dd68700d6..1bd47ff69ebc6 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/forecasting_modal.js +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/forecasting_modal.js @@ -30,9 +30,13 @@ import { forecastServiceFactory } from '../../../services/forecast_service'; import { ForecastButton } from './forecast_button'; export const FORECAST_DURATION_MAX_DAYS = 3650; // Max forecast duration allowed by analytics. - -const FORECAST_JOB_MIN_VERSION = '6.1.0'; // Forecasting only allowed for jobs created >= 6.1.0. +const STATUS_FINISHED_QUERY = { + term: { + forecast_status: FORECAST_REQUEST_STATE.FINISHED, + }, +}; const FORECASTS_VIEW_MAX = 5; // Display links to a maximum of 5 forecasts. +const FORECAST_JOB_MIN_VERSION = '6.1.0'; // Forecasting only allowed for jobs created >= 6.1.0. const FORECAST_DURATION_MAX_MS = FORECAST_DURATION_MAX_DAYS * 86400000; const WARN_NUM_PARTITIONS = 100; // Warn about running a forecast with this number of field values. const FORECAST_STATS_POLL_FREQUENCY = 250; // Frequency in ms at which to poll for forecast request stats. @@ -64,6 +68,7 @@ export class ForecastingModal extends Component { latestRecordTimestamp: PropTypes.number, entities: PropTypes.array, setForecastId: PropTypes.func, + selectedForecastId: PropTypes.string, }; constructor(props) { @@ -405,13 +410,8 @@ export class ForecastingModal extends Component { // Get the list of all the finished forecasts for this job with results at or later than the dashboard 'from' time. const { timefilter } = this.context.services.data.query.timefilter; const bounds = timefilter.getActiveBounds(); - const statusFinishedQuery = { - term: { - forecast_status: FORECAST_REQUEST_STATE.FINISHED, - }, - }; this.mlForecastService - .getForecastsSummary(job, statusFinishedQuery, bounds.min.valueOf(), FORECASTS_VIEW_MAX) + .getForecastsSummary(job, STATUS_FINISHED_QUERY, bounds.min.valueOf(), FORECASTS_VIEW_MAX) .then((resp) => { this.setState({ previousForecasts: resp.forecasts, @@ -558,6 +558,7 @@ export class ForecastingModal extends Component { jobOpeningState={this.state.jobOpeningState} jobClosingState={this.state.jobClosingState} messages={this.state.messages} + selectedForecastId={this.props.selectedForecastId} /> )} diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/forecasts_list.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/forecasts_list.js index 9e01f06094451..52ce2b201dd8d 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/forecasts_list.js +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/forecasts_list.js @@ -12,11 +12,12 @@ import PropTypes from 'prop-types'; import React from 'react'; -import { EuiButtonIcon, EuiIcon, EuiInMemoryTable, EuiText, EuiToolTip } from '@elastic/eui'; +import { EuiButtonIcon, EuiIconTip, EuiInMemoryTable, EuiText } from '@elastic/eui'; import { formatHumanReadableDateTimeSeconds } from '@kbn/ml-date-utils'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; +import { useCurrentThemeVars } from '../../../contexts/kibana'; function getColumns(viewForecast) { return [ @@ -75,37 +76,41 @@ function getColumns(viewForecast) { ]; } -// TODO - add in ml-info-icon to the h3 element, -// then remove tooltip and inline style. -export function ForecastsList({ forecasts, viewForecast }) { +export function ForecastsList({ forecasts, viewForecast, selectedForecastId }) { + const { euiTheme } = useCurrentThemeVars(); + const getRowProps = (item) => { return { 'data-test-subj': `mlForecastsListRow row-${item.rowId}`, + ...(item.forecast_id === selectedForecastId + ? { + style: { + backgroundColor: `${euiTheme.euiPanelBackgroundColorModifiers.primary}`, + }, + } + : {}), }; }; return ( -

+

+   + + } + />

- - } - > - - 0 && ( - + )} @@ -104,4 +108,5 @@ Modal.propType = { jobOpeningState: PropTypes.number, jobClosingState: PropTypes.number, messages: PropTypes.array, + selectedForecastId: PropTypes.string, }; diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js index 601bbf058868f..57ded98fc8374 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js @@ -1081,6 +1081,7 @@ export class TimeSeriesExplorer extends React.Component { latestRecordTimestamp={selectedJob.data_counts.latest_record_timestamp} setForecastId={this.setForecastId} className="forecast-controls" + selectedForecastId={this.props.selectedForecastId} /> diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_embeddable_chart/timeseriesexplorer_embeddable_chart.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_embeddable_chart/timeseriesexplorer_embeddable_chart.js index 90b0e76167517..48ef63c2eae37 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_embeddable_chart/timeseriesexplorer_embeddable_chart.js +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_embeddable_chart/timeseriesexplorer_embeddable_chart.js @@ -1051,6 +1051,7 @@ export class TimeSeriesExplorerEmbeddableChart extends React.Component { setForecastId={this.setForecastId} className="forecast-controls" onForecastComplete={onForecastComplete} + selectedForecastId={this.props.selectedForecastId} /> )} diff --git a/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.test.ts b/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.test.ts index d19ff7f723d0a..e82371c358152 100644 --- a/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.test.ts +++ b/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.test.ts @@ -47,7 +47,7 @@ describe('check_capabilities', () => { ); const { capabilities } = await getCapabilities(); const count = Object.keys(capabilities).length; - expect(count).toBe(43); + expect(count).toBe(44); }); }); @@ -86,6 +86,7 @@ describe('check_capabilities', () => { expect(capabilities.canCloseJob).toBe(false); expect(capabilities.canResetJob).toBe(false); expect(capabilities.canForecastJob).toBe(false); + expect(capabilities.canDeleteForecast).toBe(false); expect(capabilities.canStartStopDatafeed).toBe(false); expect(capabilities.canUpdateJob).toBe(false); expect(capabilities.canCreateDatafeed).toBe(false); @@ -146,6 +147,7 @@ describe('check_capabilities', () => { expect(capabilities.canCloseJob).toBe(true); expect(capabilities.canResetJob).toBe(true); expect(capabilities.canForecastJob).toBe(true); + expect(capabilities.canDeleteForecast).toBe(true); expect(capabilities.canStartStopDatafeed).toBe(true); expect(capabilities.canUpdateJob).toBe(true); expect(capabilities.canCreateDatafeed).toBe(true); @@ -206,6 +208,7 @@ describe('check_capabilities', () => { expect(capabilities.canCloseJob).toBe(false); expect(capabilities.canResetJob).toBe(false); expect(capabilities.canForecastJob).toBe(false); + expect(capabilities.canDeleteForecast).toBe(false); expect(capabilities.canStartStopDatafeed).toBe(false); expect(capabilities.canUpdateJob).toBe(false); expect(capabilities.canCreateDatafeed).toBe(false); @@ -266,6 +269,7 @@ describe('check_capabilities', () => { expect(capabilities.canCloseJob).toBe(false); expect(capabilities.canResetJob).toBe(false); expect(capabilities.canForecastJob).toBe(false); + expect(capabilities.canDeleteForecast).toBe(false); expect(capabilities.canStartStopDatafeed).toBe(false); expect(capabilities.canUpdateJob).toBe(false); expect(capabilities.canCreateDatafeed).toBe(false); @@ -326,6 +330,7 @@ describe('check_capabilities', () => { expect(capabilities.canCloseJob).toBe(false); expect(capabilities.canResetJob).toBe(false); expect(capabilities.canForecastJob).toBe(false); + expect(capabilities.canDeleteForecast).toBe(false); expect(capabilities.canStartStopDatafeed).toBe(false); expect(capabilities.canUpdateJob).toBe(false); expect(capabilities.canCreateDatafeed).toBe(false); @@ -387,6 +392,7 @@ describe('check_capabilities', () => { expect(capabilities.canCloseJob).toBe(false); expect(capabilities.canResetJob).toBe(false); expect(capabilities.canForecastJob).toBe(false); + expect(capabilities.canDeleteForecast).toBe(false); expect(capabilities.canStartStopDatafeed).toBe(false); expect(capabilities.canUpdateJob).toBe(false); expect(capabilities.canCreateDatafeed).toBe(false); diff --git a/x-pack/plugins/ml/server/routes/anomaly_detectors.ts b/x-pack/plugins/ml/server/routes/anomaly_detectors.ts index 1fafd467595e9..4f843620003ba 100644 --- a/x-pack/plugins/ml/server/routes/anomaly_detectors.ts +++ b/x-pack/plugins/ml/server/routes/anomaly_detectors.ts @@ -13,6 +13,7 @@ import type { RouteInitialization } from '../types'; import { anomalyDetectionJobSchema, anomalyDetectionUpdateJobSchema, + deleteForecastSchema, jobIdSchema, getBucketsSchema, getOverallBucketsSchema, @@ -379,6 +380,41 @@ export function jobRoutes({ router, routeGuard }: RouteInitialization) { }) ); + router.versioned + .delete({ + path: `${ML_INTERNAL_BASE_PATH}/anomaly_detectors/{jobId}/_forecast/{forecastId}`, + access: 'internal', + options: { + tags: ['access:ml:canDeleteForecast'], + }, + summary: 'Deletes specified forecast for specified job', + description: 'Deletes a specified forecast for the specified anomaly detection job.', + }) + .addVersion( + { + version: '1', + validate: { + request: { + params: deleteForecastSchema, + }, + }, + }, + routeGuard.fullLicenseAPIGuard(async ({ mlClient, request, response }) => { + try { + const { jobId, forecastId } = request.params; + const body = await mlClient.deleteForecast({ + job_id: jobId, + forecast_id: forecastId, + }); + return response.ok({ + body, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + router.versioned .post({ path: `${ML_INTERNAL_BASE_PATH}/anomaly_detectors/{jobId}/_forecast`, 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 370b5d657e7c1..3b1eb0b481e46 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 @@ -154,6 +154,11 @@ export const jobIdSchema = schema.object({ ...jobIdSchemaBasic, }); +export const deleteForecastSchema = schema.object({ + ...jobIdSchemaBasic, + forecastId: schema.string(), +}); + export const getBucketsSchema = schema.object({ anomaly_score: schema.maybe(schema.number()), desc: schema.maybe(schema.boolean()), diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index e0f8600ea07b3..bf1c6f1960258 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -28644,7 +28644,6 @@ "xpack.ml.jobsList.jobDetails.forecastsTable.processingTimeLabel": "Temps de traitement", "xpack.ml.jobsList.jobDetails.forecastsTable.statusLabel": "Statut", "xpack.ml.jobsList.jobDetails.forecastsTable.toLabel": "À", - "xpack.ml.jobsList.jobDetails.forecastsTable.viewAriaLabel": "Afficher la prévision créée le {createdDate}", "xpack.ml.jobsList.jobDetails.forecastsTable.viewLabel": "Afficher", "xpack.ml.jobsList.jobDetails.generalTitle": "Général", "xpack.ml.jobsList.jobDetails.influencersTitle": "Influenceurs", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 6bbfe89951e47..87de6f82e48e7 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -28393,7 +28393,6 @@ "xpack.ml.jobsList.jobDetails.forecastsTable.processingTimeLabel": "処理時間", "xpack.ml.jobsList.jobDetails.forecastsTable.statusLabel": "ステータス", "xpack.ml.jobsList.jobDetails.forecastsTable.toLabel": "終了:", - "xpack.ml.jobsList.jobDetails.forecastsTable.viewAriaLabel": "{createdDate} に作成された予測を表示", "xpack.ml.jobsList.jobDetails.forecastsTable.viewLabel": "表示", "xpack.ml.jobsList.jobDetails.generalTitle": "一般", "xpack.ml.jobsList.jobDetails.influencersTitle": "影響", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 652ae39ab0aa9..94d68253b3356 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -28430,7 +28430,6 @@ "xpack.ml.jobsList.jobDetails.forecastsTable.processingTimeLabel": "处理时间", "xpack.ml.jobsList.jobDetails.forecastsTable.statusLabel": "状态", "xpack.ml.jobsList.jobDetails.forecastsTable.toLabel": "至", - "xpack.ml.jobsList.jobDetails.forecastsTable.viewAriaLabel": "查看在 {createdDate} 创建的预测", "xpack.ml.jobsList.jobDetails.forecastsTable.viewLabel": "查看", "xpack.ml.jobsList.jobDetails.generalTitle": "常规", "xpack.ml.jobsList.jobDetails.influencersTitle": "影响因素", diff --git a/x-pack/test/api_integration/apis/ml/anomaly_detectors/forecast_with_spaces.ts b/x-pack/test/api_integration/apis/ml/anomaly_detectors/forecast_with_spaces.ts index 1176452408762..a330edd9a41d7 100644 --- a/x-pack/test/api_integration/apis/ml/anomaly_detectors/forecast_with_spaces.ts +++ b/x-pack/test/api_integration/apis/ml/anomaly_detectors/forecast_with_spaces.ts @@ -38,7 +38,28 @@ export default ({ getService }: FtrProviderContext) => { return body; } + async function deleteForecast( + jobId: string, + forecastId: string, + space: string, + user: USER, + expectedStatusCode: number + ) { + const { body, status } = await supertest + .delete( + `${ + space ? `/s/${space}` : '' + }/internal/ml/anomaly_detectors/${jobId}/_forecast/${forecastId}` + ) + .auth(user, ml.securityCommon.getPasswordForUser(user)) + .set(getCommonRequestHeader('1')); + ml.api.assertResponseStatusCode(expectedStatusCode, status, body); + + return body; + } + describe('POST anomaly_detectors _forecast with spaces', function () { + let forecastId: string; before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote'); await ml.testResources.setKibanaTimeZoneToUTC(); @@ -79,13 +100,22 @@ export default ({ getService }: FtrProviderContext) => { await ml.api.waitForDatafeedState(forecastJobDatafeedId, DATAFEED_STATE.STOPPED); await ml.api.waitForJobState(forecastJobId, JOB_STATE.CLOSED); await ml.api.openAnomalyDetectionJob(forecastJobId); - await runForecast(forecastJobId, idSpace1, '1d', USER.ML_POWERUSER, 200); + const resp = await runForecast(forecastJobId, idSpace1, '1d', USER.ML_POWERUSER, 200); + forecastId = resp.forecast_id; await ml.testExecution.logTestStep( `forecast results should exist for job '${forecastJobId}'` ); await ml.api.assertForecastResultsExist(forecastJobId); }); + it('should not delete forecast for user without permissions', async () => { + await await deleteForecast(forecastJobId, forecastId, idSpace1, USER.ML_VIEWER, 403); + }); + + it('should delete forecast for user with permissions', async () => { + await await deleteForecast(forecastJobId, forecastId, idSpace1, USER.ML_POWERUSER, 200); + }); + it('should not run forecast for open job with invalid duration', async () => { await runForecast(forecastJobId, idSpace1, 3600000, USER.ML_POWERUSER, 400); }); diff --git a/x-pack/test/api_integration/apis/ml/system/capabilities.ts b/x-pack/test/api_integration/apis/ml/system/capabilities.ts index b653632432310..c4775cacdfa66 100644 --- a/x-pack/test/api_integration/apis/ml/system/capabilities.ts +++ b/x-pack/test/api_integration/apis/ml/system/capabilities.ts @@ -12,7 +12,7 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; import { getCommonRequestHeader } from '../../../../functional/services/ml/common_api'; import { USER } from '../../../../functional/services/ml/security_common'; -const NUMBER_OF_CAPABILITIES = 43; +const NUMBER_OF_CAPABILITIES = 44; export default ({ getService }: FtrProviderContext) => { const supertest = getService('supertestWithoutAuth'); @@ -61,6 +61,7 @@ export default ({ getService }: FtrProviderContext) => { canResetJob: false, canUpdateJob: false, canForecastJob: false, + canDeleteForecast: false, canCreateDatafeed: false, canDeleteDatafeed: false, canStartStopDatafeed: false, @@ -111,6 +112,7 @@ export default ({ getService }: FtrProviderContext) => { canResetJob: true, canUpdateJob: true, canForecastJob: true, + canDeleteForecast: true, canCreateDatafeed: true, canDeleteDatafeed: true, canStartStopDatafeed: true, diff --git a/x-pack/test/api_integration/apis/ml/system/space_capabilities.ts b/x-pack/test/api_integration/apis/ml/system/space_capabilities.ts index f45d54a741da3..1832e5d096e34 100644 --- a/x-pack/test/api_integration/apis/ml/system/space_capabilities.ts +++ b/x-pack/test/api_integration/apis/ml/system/space_capabilities.ts @@ -15,7 +15,7 @@ import { USER } from '../../../../functional/services/ml/security_common'; const idSpaceWithMl = 'space_with_ml'; const idSpaceNoMl = 'space_no_ml'; -const NUMBER_OF_CAPABILITIES = 43; +const NUMBER_OF_CAPABILITIES = 44; export default ({ getService }: FtrProviderContext) => { const supertest = getService('supertestWithoutAuth'); @@ -90,6 +90,7 @@ export default ({ getService }: FtrProviderContext) => { canResetJob: false, canUpdateJob: false, canForecastJob: false, + canDeleteForecast: false, canCreateDatafeed: false, canDeleteDatafeed: false, canStartStopDatafeed: false, @@ -139,6 +140,7 @@ export default ({ getService }: FtrProviderContext) => { canResetJob: false, canUpdateJob: false, canForecastJob: false, + canDeleteForecast: false, canCreateDatafeed: false, canDeleteDatafeed: false, canStartStopDatafeed: false, @@ -188,6 +190,7 @@ export default ({ getService }: FtrProviderContext) => { canResetJob: true, canUpdateJob: true, canForecastJob: true, + canDeleteForecast: true, canCreateDatafeed: true, canDeleteDatafeed: true, canStartStopDatafeed: true, @@ -237,6 +240,7 @@ export default ({ getService }: FtrProviderContext) => { canResetJob: false, canUpdateJob: false, canForecastJob: false, + canDeleteForecast: false, canCreateDatafeed: false, canDeleteDatafeed: false, canStartStopDatafeed: false,