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,