Skip to content

Commit

Permalink
[8.x] [ML] Anomaly Detection: add never expire option to forecast cre…
Browse files Browse the repository at this point in the history
…ation modal (#195151) (#197680)

# Backport

This will backport the following commits from `main` to `8.x`:
- [[ML] Anomaly Detection: add never expire option to forecast creation
modal (#195151)](#195151)

<!--- Backport version: 9.4.3 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Melissa
Alvarez","email":"[email protected]"},"sourceCommit":{"committedDate":"2024-10-24T15:44:56Z","message":"[ML]
Anomaly Detection: add never expire option to forecast creation modal
(#195151)\n\n## Summary\r\nThis PR adds an option in the forecast
creation modal to prevent a\r\nforecast from expiring.\r\n\r\nRelated
issue:
https://github.com/elastic/kibana/issues/160741\r\n\r\n\r\n![image](https://github.com/user-attachments/assets/2fb2a73b-5d64-4018-809a-7c610ef44ee3)\r\n\r\n\r\n![image](https://github.com/user-attachments/assets/1df768ff-98ce-441b-ad4f-b5b31cc62432)\r\n\r\n\r\n###
Checklist\r\n\r\nDelete any items that are not applicable to this
PR.\r\n\r\n- [ ] Any text added follows [EUI's
writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
uses\r\nsentence case text and includes
[i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\r\n-
[
]\r\n[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)\r\nwas
added for features that require explanation or tutorials\r\n- [ ] [Unit
or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common scenarios\r\n- [ ] [Flaky
Test\r\nRunner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1)
was\r\nused on any tests changed\r\n- [ ] Any UI touched in this PR is
usable by keyboard only (learn more\r\nabout [keyboard
accessibility](https://webaim.org/techniques/keyboard/))\r\n- [ ] Any UI
touched in this PR does not create any new axe failures\r\n(run axe in
browser:\r\n[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),\r\n[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))\r\n-
[ ] If a plugin configuration key changed, check if it needs to
be\r\nallowlisted in the cloud and added to the
[docker\r\nlist](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)\r\n-
[ ] This renders correctly on smaller devices using a
responsive\r\nlayout. (You can test this [in
your\r\nbrowser](https://www.browserstack.com/guide/responsive-testing-on-local-server))\r\n-
[ ] This was checked for
[cross-browser\r\ncompatibility](https://www.elastic.co/support/matrix#matrix_browsers)\r\n\r\n---------\r\n\r\nCo-authored-by:
Elastic Machine
<[email protected]>","sha":"d885bbebe896fd04c88fb556635fd69938614074","branchLabelMapping":{"^v9.0.0$":"main","^v8.17.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:enhancement",":ml","Feature:Anomaly
Detection","v9.0.0","backport:version","v8.17.0"],"title":"[ML] Anomaly
Detection: add never expire option to forecast creation
modal","number":195151,"url":"https://github.com/elastic/kibana/pull/195151","mergeCommit":{"message":"[ML]
Anomaly Detection: add never expire option to forecast creation modal
(#195151)\n\n## Summary\r\nThis PR adds an option in the forecast
creation modal to prevent a\r\nforecast from expiring.\r\n\r\nRelated
issue:
https://github.com/elastic/kibana/issues/160741\r\n\r\n\r\n![image](https://github.com/user-attachments/assets/2fb2a73b-5d64-4018-809a-7c610ef44ee3)\r\n\r\n\r\n![image](https://github.com/user-attachments/assets/1df768ff-98ce-441b-ad4f-b5b31cc62432)\r\n\r\n\r\n###
Checklist\r\n\r\nDelete any items that are not applicable to this
PR.\r\n\r\n- [ ] Any text added follows [EUI's
writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
uses\r\nsentence case text and includes
[i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\r\n-
[
]\r\n[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)\r\nwas
added for features that require explanation or tutorials\r\n- [ ] [Unit
or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common scenarios\r\n- [ ] [Flaky
Test\r\nRunner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1)
was\r\nused on any tests changed\r\n- [ ] Any UI touched in this PR is
usable by keyboard only (learn more\r\nabout [keyboard
accessibility](https://webaim.org/techniques/keyboard/))\r\n- [ ] Any UI
touched in this PR does not create any new axe failures\r\n(run axe in
browser:\r\n[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),\r\n[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))\r\n-
[ ] If a plugin configuration key changed, check if it needs to
be\r\nallowlisted in the cloud and added to the
[docker\r\nlist](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)\r\n-
[ ] This renders correctly on smaller devices using a
responsive\r\nlayout. (You can test this [in
your\r\nbrowser](https://www.browserstack.com/guide/responsive-testing-on-local-server))\r\n-
[ ] This was checked for
[cross-browser\r\ncompatibility](https://www.elastic.co/support/matrix#matrix_browsers)\r\n\r\n---------\r\n\r\nCo-authored-by:
Elastic Machine
<[email protected]>","sha":"d885bbebe896fd04c88fb556635fd69938614074"}},"sourceBranch":"main","suggestedTargetBranches":["8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/195151","number":195151,"mergeCommit":{"message":"[ML]
Anomaly Detection: add never expire option to forecast creation modal
(#195151)\n\n## Summary\r\nThis PR adds an option in the forecast
creation modal to prevent a\r\nforecast from expiring.\r\n\r\nRelated
issue:
https://github.com/elastic/kibana/issues/160741\r\n\r\n\r\n![image](https://github.com/user-attachments/assets/2fb2a73b-5d64-4018-809a-7c610ef44ee3)\r\n\r\n\r\n![image](https://github.com/user-attachments/assets/1df768ff-98ce-441b-ad4f-b5b31cc62432)\r\n\r\n\r\n###
Checklist\r\n\r\nDelete any items that are not applicable to this
PR.\r\n\r\n- [ ] Any text added follows [EUI's
writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
uses\r\nsentence case text and includes
[i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\r\n-
[
]\r\n[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)\r\nwas
added for features that require explanation or tutorials\r\n- [ ] [Unit
or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common scenarios\r\n- [ ] [Flaky
Test\r\nRunner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1)
was\r\nused on any tests changed\r\n- [ ] Any UI touched in this PR is
usable by keyboard only (learn more\r\nabout [keyboard
accessibility](https://webaim.org/techniques/keyboard/))\r\n- [ ] Any UI
touched in this PR does not create any new axe failures\r\n(run axe in
browser:\r\n[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),\r\n[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))\r\n-
[ ] If a plugin configuration key changed, check if it needs to
be\r\nallowlisted in the cloud and added to the
[docker\r\nlist](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)\r\n-
[ ] This renders correctly on smaller devices using a
responsive\r\nlayout. (You can test this [in
your\r\nbrowser](https://www.browserstack.com/guide/responsive-testing-on-local-server))\r\n-
[ ] This was checked for
[cross-browser\r\ncompatibility](https://www.elastic.co/support/matrix#matrix_browsers)\r\n\r\n---------\r\n\r\nCo-authored-by:
Elastic Machine
<[email protected]>","sha":"d885bbebe896fd04c88fb556635fd69938614074"}},{"branch":"8.x","label":"v8.17.0","branchLabelMappingKey":"^v8.17.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->

Co-authored-by: Melissa Alvarez <[email protected]>
  • Loading branch information
kibanamachine and alvarezmelissa87 authored Oct 24, 2024
1 parent e29a121 commit f64488a
Show file tree
Hide file tree
Showing 9 changed files with 85 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,14 @@ export class ForecastsTable extends Component {
name: i18n.translate('xpack.ml.jobsList.jobDetails.forecastsTable.expiresLabel', {
defaultMessage: 'Expires',
}),
render: timeFormatter,
render: (value) => {
if (value === undefined) {
return i18n.translate('xpack.ml.jobsList.jobDetails.forecastsTable.neverExpiresLabel', {
defaultMessage: 'Never expires',
});
}
return timeFormatter(value);
},
textOnly: true,
sortable: true,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -319,14 +319,15 @@ export function forecastServiceFactory(mlApi: MlApi) {
);
}
// Runs a forecast
function runForecast(jobId: string, duration?: string) {
function runForecast(jobId: string, duration?: string, neverExpires?: boolean) {
// eslint-disable-next-line no-console
console.log('ML forecast service run forecast with duration:', duration);
return new Promise((resolve, reject) => {
mlApi
.forecast({
jobId,
duration,
neverExpires,
})
.then((resp) => {
resolve(resp);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -359,9 +359,18 @@ export function mlApiProvider(httpService: HttpService) {
});
},

forecast({ jobId, duration }: { jobId: string; duration?: string }) {
forecast({
jobId,
duration,
neverExpires,
}: {
jobId: string;
duration?: string;
neverExpires?: boolean;
}) {
const body = JSON.stringify({
...(duration !== undefined ? { duration } : {}),
...(neverExpires === true ? { expires_in: '0' } : {}),
});

return httpService.http<any>({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ function getDefaultState() {
newForecastDuration: '1d',
isNewForecastDurationValid: true,
newForecastDurationErrors: [],
neverExpires: false,
messages: [],
};
}
Expand Down Expand Up @@ -109,6 +110,12 @@ export class ForecastingModal extends Component {
this.closeModal();
};

onNeverExpiresChange = (event) => {
this.setState({
neverExpires: event.target.checked,
});
};

onNewForecastDurationChange = (event) => {
const newForecastDurationErrors = [];
let isNewForecastDurationValid = true;
Expand Down Expand Up @@ -263,7 +270,7 @@ export class ForecastingModal extends Component {
const durationInSeconds = parseInterval(this.state.newForecastDuration).asSeconds();

this.mlForecastService
.runForecast(this.props.job.job_id, `${durationInSeconds}s`)
.runForecast(this.props.job.job_id, `${durationInSeconds}s`, this.state.neverExpires)
.then((resp) => {
// Endpoint will return { acknowledged:true, id: <now timestamp> } before forecast is complete.
// So wait for results and then refresh the dashboard to the end of the forecast.
Expand Down Expand Up @@ -551,6 +558,8 @@ export class ForecastingModal extends Component {
runForecast={this.checkJobStateAndRunForecast}
newForecastDuration={this.state.newForecastDuration}
onNewForecastDurationChange={this.onNewForecastDurationChange}
onNeverExpiresChange={this.onNeverExpiresChange}
neverExpires={this.state.neverExpires}
isNewForecastDurationValid={this.state.isNewForecastDurationValid}
newForecastDurationErrors={this.state.newForecastDurationErrors}
isForecastRequested={this.state.isForecastRequested}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
EuiForm,
EuiFormRow,
EuiSpacer,
EuiSwitch,
EuiText,
EuiToolTip,
} from '@elastic/eui';
Expand Down Expand Up @@ -82,6 +83,8 @@ export function RunControls({
newForecastDuration,
isNewForecastDurationValid,
newForecastDurationErrors,
neverExpires,
onNeverExpiresChange,
onNewForecastDurationChange,
runForecast,
isForecastRequested,
Expand Down Expand Up @@ -133,8 +136,8 @@ export function RunControls({
</EuiText>
<EuiSpacer size="s" />
<EuiForm>
<EuiFlexGroup>
<EuiFlexItem>
<EuiFlexGroup direction="column">
<EuiFlexItem grow={false}>
<EuiFormRow
label={
<FormattedMessage
Expand Down Expand Up @@ -163,16 +166,43 @@ export function RunControls({
)}
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFormRow hasEmptyLabelSpace>
{disabledState.isDisabledToolTipText === undefined ? (
runButton
) : (
<EuiToolTip position="left" content={disabledState.isDisabledToolTipText}>
{runButton}
</EuiToolTip>
)}
</EuiFormRow>
<EuiFlexItem>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiFormRow
helpText={i18n.translate(
'xpack.ml.timeSeriesExplorer.runControls.neverExpireHelpText',
{
defaultMessage: 'If disabled, forecasts will be retained for 14 days.',
}
)}
>
<EuiSwitch
data-test-subj="mlModalForecastNeverExpireSwitch"
disabled={disabledState.isDisabled}
label={i18n.translate(
'xpack.ml.timeSeriesExplorer.runControls.neverExpireLabel',
{
defaultMessage: 'Never expire',
}
)}
checked={neverExpires}
onChange={onNeverExpiresChange}
/>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFormRow hasEmptyLabelSpace>
{disabledState.isDisabledToolTipText === undefined ? (
runButton
) : (
<EuiToolTip position="left" content={disabledState.isDisabledToolTipText}>
{runButton}
</EuiToolTip>
)}
</EuiFormRow>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</EuiForm>
Expand All @@ -193,7 +223,9 @@ RunControls.propType = {
newForecastDuration: PropTypes.string,
isNewForecastDurationValid: PropTypes.bool,
newForecastDurationErrors: PropTypes.array,
neverExpires: PropTypes.bool.isRequired,
onNewForecastDurationChange: PropTypes.func.isRequired,
onNeverExpiresChange: PropTypes.func.isRequired,
runForecast: PropTypes.func.isRequired,
isForecastRequested: PropTypes.bool,
forecastProgress: PropTypes.number,
Expand Down
3 changes: 1 addition & 2 deletions x-pack/plugins/ml/server/routes/anomaly_detectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -439,11 +439,10 @@ export function jobRoutes({ router, routeGuard }: RouteInitialization) {
routeGuard.fullLicenseAPIGuard(async ({ mlClient, request, response }) => {
try {
const jobId = request.params.jobId;
const duration = request.body.duration;
const body = await mlClient.forecast({
job_id: jobId,
body: {
duration,
...request.body,
},
});
return response.ok({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,10 @@ export const updateModelSnapshotBodySchema = schema.object({
retain: schema.maybe(schema.boolean()),
});

export const forecastAnomalyDetector = schema.object({ duration: schema.any() });
export const forecastAnomalyDetector = schema.object({
duration: schema.any(),
expires_in: schema.maybe(schema.any()),
});

export const forceQuerySchema = schema.object({
force: schema.maybe(schema.boolean()),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ export default function ({ getService }: FtrProviderContext) {
await ml.forecast.assertForecastButtonExists();
await ml.forecast.assertForecastButtonEnabled(true);
await ml.forecast.openForecastModal();
await ml.forecast.assertForecastNeverExpireSwitchExists();
await ml.forecast.assertForecastModalRunButtonEnabled(true);

await ml.testExecution.logTestStep('should run the forecast and close the modal');
Expand Down
5 changes: 5 additions & 0 deletions x-pack/test/functional/services/ml/forecast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,11 @@ export function MachineLearningForecastProvider({ getPageObject, getService }: F
});
},

async assertForecastNeverExpireSwitchExists() {
await testSubjects.existOrFail('mlModalForecastNeverExpireSwitch');
expect(await testSubjects.isChecked('mlModalForecastNeverExpireSwitch')).to.be(false);
},

async assertForecastModalRunButtonEnabled(expectedValue: boolean) {
await headerPage.waitUntilLoadingHasFinished();
const isEnabled = await testSubjects.isEnabled('mlModalForecast > mlModalForecastButtonRun');
Expand Down

0 comments on commit f64488a

Please sign in to comment.