Skip to content

Commit

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

## Summary
This PR adds an option in the forecast creation modal to prevent a
forecast from expiring.

Related issue: #160741


![image](https://github.com/user-attachments/assets/2fb2a73b-5d64-4018-809a-7c610ef44ee3)


![image](https://github.com/user-attachments/assets/1df768ff-98ce-441b-ad4f-b5b31cc62432)


### 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 <elasticmachine@users.noreply.github.com>
  • Loading branch information
alvarezmelissa87 and elasticmachine authored Oct 24, 2024
1 parent 9c92b52 commit d885bbe
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 d885bbe

Please sign in to comment.