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,