Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ML] Allows temporary data views in AD job wizards #170112

Expand Up @@ -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) {
Expand Down
Expand Up @@ -399,6 +399,7 @@ export class JobsList extends Component {
rowProps={(item) => ({
'data-test-subj': `mlJobListRow row-${item.id}`,
})}
css={{ '.euiTableRow-isExpandedRow .euiTableCellContent': { animation: 'none' } }}
/>
);
}
Expand Down
Expand Up @@ -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';
Expand Down Expand Up @@ -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 &&
Expand Down
Expand Up @@ -80,7 +80,7 @@ export const Page: FC<PageProps> = ({ nextStepPath }) => {
uiSettings,
}}
>
<CreateDataViewButton onDataViewCreated={onObjectSelection} />
<CreateDataViewButton onDataViewCreated={onObjectSelection} allowAdHocDataView={true} />
</SavedObjectFinder>
</EuiPanel>
</EuiPageBody>
Expand Down
Expand Up @@ -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(
Expand All @@ -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();
}
Expand Down Expand Up @@ -72,17 +72,29 @@ 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> {
if (dataViewsService === null) {
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;
}
Expand Up @@ -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
Expand Down
19 changes: 18 additions & 1 deletion x-pack/plugins/ml/public/application/util/index_utils.ts
Expand Up @@ -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() {
Expand All @@ -20,14 +21,30 @@ 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!');
}
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();
Expand Down
1 change: 0 additions & 1 deletion x-pack/plugins/translations/translations/fr-FR.json
Expand Up @@ -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}}",
Expand Down
1 change: 0 additions & 1 deletion x-pack/plugins/translations/translations/ja-JP.json
Expand Up @@ -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 {ジョブ}}を閉じる",
Expand Down
1 change: 0 additions & 1 deletion x-pack/plugins/translations/translations/zh-CN.json
Expand Up @@ -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 {作业}}",
Expand Down
Expand Up @@ -228,23 +228,27 @@ 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);

// Refresh page to ensure page has correct cache of data views
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();

Expand Down