[ML] Allow temporary data views in AD job wizards (#170112)

When creating a brand new job, temporary data views can be created and
used in the wizard.

When cloning a job where the data view cannot be found, a new temporary
data view is created to be used in the wizard.
This can happen if the data view used to create the original job has
been deleted or the job was created with a temporary data view.


2b9c2125-2b0c-449d-a226-82267f64567b

Also overrides the animation for the expanded rows in the AD jobs list
which can cause strange behaviour when changing tabs in the expanded
row.

---------

Co-authored-by: Quynh Nguyen (Quinn) <43350163+qn895@users.noreply.github.com>
This commit is contained in:
James Gowdy 2023-10-31 18:24:34 +00:00 committed by GitHub
parent 1fa5d60cca
commit a509a3abf8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 60 additions and 41 deletions

View file

@ -197,7 +197,7 @@ export const LinksMenuUI = (props: LinksMenuProps) => {
const getDataViewId = async () => {
const index = job.datafeed_config.indices[0];
const dataViewId = await getDataViewIdFromName(index);
const dataViewId = await getDataViewIdFromName(index, job);
// If data view doesn't exist for some reasons
if (!dataViewId && !unmounted) {

View file

@ -399,6 +399,7 @@ export class JobsList extends Component {
rowProps={(item) => ({
'data-test-subj': `mlJobListRow row-${item.id}`,
})}
css={{ '.euiTableRow-isExpandedRow .euiTableCellContent': { animation: 'none' } }}
/>
);
}

View file

@ -16,7 +16,6 @@ import {
import { getApplication, getToastNotifications } from '../../../util/dependency_cache';
import { ml } from '../../../services/ml_api_service';
import { stringMatch } from '../../../util/string_utils';
import { getDataViewNames } from '../../../util/index_utils';
import { JOB_STATE, DATAFEED_STATE } from '../../../../../common/constants/states';
import { JOB_ACTION } from '../../../../../common/constants/job_actions';
import { parseInterval } from '../../../../../common/util/parse_interval';
@ -222,25 +221,6 @@ export async function cloneJob(jobId) {
loadFullJob(jobId, false),
]);
const dataViewNames = await getDataViewNames();
const dataViewTitle = datafeed.indices.join(',');
const jobIndicesAvailable = dataViewNames.includes(dataViewTitle);
if (jobIndicesAvailable === false) {
const warningText = i18n.translate(
'xpack.ml.jobsList.managementActions.noSourceDataViewForClone',
{
defaultMessage:
'Unable to clone the anomaly detection job {jobId}. No data view exists for index {dataViewTitle}.',
values: { jobId, dataViewTitle },
}
);
getToastNotificationService().displayDangerToast(warningText, {
'data-test-subj': 'mlCloneJobNoDataViewExistsWarningToast',
});
return;
}
const createdBy = originalJob?.custom_settings?.created_by;
if (
cloneableJob !== undefined &&

View file

@ -80,7 +80,7 @@ export const Page: FC<PageProps> = ({ nextStepPath }) => {
uiSettings,
}}
>
<CreateDataViewButton onDataViewCreated={onObjectSelection} />
<CreateDataViewButton onDataViewCreated={onObjectSelection} allowAdHocDataView={true} />
</SavedObjectFinder>
</EuiPanel>
</EuiPageBody>

View file

@ -8,7 +8,7 @@
import type { ApplicationStart } from '@kbn/core/public';
import type { DataViewsContract } from '@kbn/data-views-plugin/public';
import { mlJobService } from '../../../../services/job_service';
import { Datafeed } from '../../../../../../common/types/anomaly_detection_jobs';
import type { Job, Datafeed } from '../../../../../../common/types/anomaly_detection_jobs';
import { CREATED_BY_LABEL, JOB_TYPE } from '../../../../../../common/constants/new_job';
export async function preConfiguredJobRedirect(
@ -19,7 +19,7 @@ export async function preConfiguredJobRedirect(
const { createdBy, job, datafeed } = mlJobService.tempJobCloningObjects;
if (job && datafeed) {
const dataViewId = await getDataViewIdFromName(datafeed, dataViewsService);
const dataViewId = await getDataViewIdFromDatafeed(job, datafeed, dataViewsService);
if (dataViewId === null) {
return Promise.resolve();
}
@ -72,7 +72,8 @@ async function getWizardUrlFromCloningJob(createdBy: string | undefined, dataVie
return `jobs/new_job/${page}?index=${dataViewId}&_g=()`;
}
async function getDataViewIdFromName(
async function getDataViewIdFromDatafeed(
job: Job,
datafeed: Datafeed,
dataViewsService: DataViewsContract
): Promise<string | null> {
@ -80,9 +81,20 @@ async function getDataViewIdFromName(
throw new Error('Data views are not initialized!');
}
const [dv] = await dataViewsService?.find(datafeed.indices.join(','));
if (!dv) {
return null;
const indexPattern = datafeed.indices.join(',');
const dataViews = await dataViewsService?.find(indexPattern);
const dataView = dataViews.find((dv) => dv.getIndexPattern() === indexPattern);
if (dataView === undefined) {
// create a temporary data view if we can't find one
// matching the index pattern
const tempDataView = await dataViewsService.create({
id: undefined,
name: indexPattern,
title: indexPattern,
timeFieldName: job.data_description.time_field!,
});
return tempDataView.id ?? null;
}
return dv.id ?? dv.title;
return dataView.id ?? null;
}

View file

@ -232,11 +232,19 @@ export const Page: FC<PageProps> = ({ existingJobsAndGroups, jobType }) => {
<div style={{ backgroundColor: 'inherit' }} data-test-subj={`mlPageJobWizard ${jobType}`}>
<EuiText size={'s'}>
<FormattedMessage
id="xpack.ml.newJob.page.createJob.dataViewName"
defaultMessage="Using data view {dataViewName}"
values={{ dataViewName: jobCreator.indexPatternDisplayName }}
/>
{dataSourceContext.selectedDataView.isPersisted() ? (
<FormattedMessage
id="xpack.ml.newJob.page.createJob.dataViewName"
defaultMessage="Using data view {dataViewName}"
values={{ dataViewName: jobCreator.indexPatternDisplayName }}
/>
) : (
<FormattedMessage
id="xpack.ml.newJob.page.createJob.tempDataViewName"
defaultMessage="Using temporary data view {dataViewName}"
values={{ dataViewName: jobCreator.indexPatternDisplayName }}
/>
)}
</EuiText>
<Wizard

View file

@ -10,6 +10,7 @@ import type { DataView } from '@kbn/data-views-plugin/public';
import type { SavedSearch, SavedSearchPublicPluginStart } from '@kbn/saved-search-plugin/public';
import type { Query, Filter } from '@kbn/es-query';
import type { DataViewsContract } from '@kbn/data-views-plugin/common';
import type { Job } from '../../../common/types/anomaly_detection_jobs';
import { getToastNotifications, getDataViews } from './dependency_cache';
export async function getDataViewNames() {
@ -20,7 +21,14 @@ export async function getDataViewNames() {
return (await dataViewsService.getIdsWithTitle()).map(({ title }) => title);
}
export async function getDataViewIdFromName(name: string): Promise<string | null> {
/**
* Retrieves the data view ID from the given name.
* If a job is passed in, a temporary data view will be created if the requested data view doesn't exist.
* @param name - The name or index pattern of the data view.
* @param job - Optional job object.
* @returns The data view ID or null if it doesn't exist.
*/
export async function getDataViewIdFromName(name: string, job?: Job): Promise<string | null> {
const dataViewsService = getDataViews();
if (dataViewsService === null) {
throw new Error('Data views are not initialized!');
@ -28,6 +36,15 @@ export async function getDataViewIdFromName(name: string): Promise<string | null
const dataViews = await dataViewsService.find(name);
const dataView = dataViews.find((dv) => dv.getIndexPattern() === name);
if (!dataView) {
if (job !== undefined) {
const tempDataView = await dataViewsService.create({
id: undefined,
name,
title: name,
timeFieldName: job.data_description.time_field!,
});
return tempDataView.id ?? null;
}
return null;
}
return dataView.id ?? dataView.getIndexPattern();

View file

@ -22522,7 +22522,6 @@
"xpack.ml.jobsList.jobDetails.forecastsTable.viewAriaLabel": "Afficher la prévision créée le {createdDate}",
"xpack.ml.jobsList.jobFilterBar.invalidSearchErrorMessage": "Recherche non valide : {errorMessage}",
"xpack.ml.jobsList.jobFilterBar.jobGroupTitle": "({jobsCount, plural, one {# tâche} many {# tâches} other {# tâches}})",
"xpack.ml.jobsList.managementActions.noSourceDataViewForClone": "Impossible de cloner la tâche de détection des anomalies {jobId}. Il n'existe aucune vue de données pour l'index {dataViewTitle}.",
"xpack.ml.jobsList.missingSavedObjectWarning.link": " {link}",
"xpack.ml.jobsList.multiJobActions.groupSelector.applyGroupsToJobTitle": "Appliquer des groupes {jobsCount, plural, one {tâche} many {tâches} other {aux tâches}}",
"xpack.ml.jobsList.multiJobsActions.closeJobsLabel": "Fermer {jobsCount, plural, one {tâche} many {tâches} other {les tâches}}",

View file

@ -22534,7 +22534,6 @@
"xpack.ml.jobsList.jobDetails.forecastsTable.viewAriaLabel": "{createdDate}に作成された予測を表示",
"xpack.ml.jobsList.jobFilterBar.invalidSearchErrorMessage": "無効な検索:{errorMessage}",
"xpack.ml.jobsList.jobFilterBar.jobGroupTitle": "({jobsCount, plural, other {#個のジョブ}})",
"xpack.ml.jobsList.managementActions.noSourceDataViewForClone": "異常検知ジョブ{jobId}を複製できません。インデックス{dataViewTitle}のデータビューは存在しません。",
"xpack.ml.jobsList.missingSavedObjectWarning.link": " {link}",
"xpack.ml.jobsList.multiJobActions.groupSelector.applyGroupsToJobTitle": "{jobsCount, plural, other {ジョブ}}にグループを適用",
"xpack.ml.jobsList.multiJobsActions.closeJobsLabel": "{jobsCount, plural, other {ジョブ}}を閉じる",

View file

@ -22533,7 +22533,6 @@
"xpack.ml.jobsList.jobDetails.forecastsTable.viewAriaLabel": "查看在 {createdDate} 创建的预测",
"xpack.ml.jobsList.jobFilterBar.invalidSearchErrorMessage": "无效搜索:{errorMessage}",
"xpack.ml.jobsList.jobFilterBar.jobGroupTitle": "({jobsCount, plural, other {# 个作业}})",
"xpack.ml.jobsList.managementActions.noSourceDataViewForClone": "无法克隆异常检测作业 {jobId}。对于索引 {dataViewTitle},不存在数据视图。",
"xpack.ml.jobsList.missingSavedObjectWarning.link": " {link}",
"xpack.ml.jobsList.multiJobActions.groupSelector.applyGroupsToJobTitle": "将组应用到{jobsCount, plural, other {作业}}",
"xpack.ml.jobsList.multiJobsActions.closeJobsLabel": "关闭{jobsCount, plural, other {作业}}",

View file

@ -228,7 +228,7 @@ export default function ({ getService }: FtrProviderContext) {
await ml.api.assertDetectorResultsExist(jobId, 0);
});
it('job cloning fails in the single metric wizard if a matching data view does not exist', async () => {
it('job cloning creates a temporary data view and opens the single metric wizard if a matching data view does not exist', async () => {
await ml.testExecution.logTestStep('delete data view used by job');
await ml.testResources.deleteIndexPatternByTitle(indexPatternString);
@ -236,15 +236,19 @@ export default function ({ getService }: FtrProviderContext) {
await browser.refresh();
await ml.testExecution.logTestStep(
'job cloning clicks the clone action and displays an error toast'
'job cloning clicks the clone action and loads the single metric wizard'
);
await ml.jobTable.clickCloneJobActionWhenNoDataViewExists(jobId);
await ml.jobTable.clickCloneJobAction(jobId);
await ml.jobTypeSelection.assertSingleMetricJobWizardOpen();
});
it('job cloning opens the existing job in the single metric wizard', async () => {
await ml.testExecution.logTestStep('recreate data view used by job');
await ml.testResources.createIndexPatternIfNeeded(indexPatternString, '@timestamp');
await ml.navigation.navigateToMl();
await ml.navigation.navigateToJobManagement();
// Refresh page to ensure page has correct cache of data views
await browser.refresh();