mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[ML] Sync ML saved objects to all spaces (#202175)
When manually syncing ML saved objects using the sync flyout, the saved objects are now tagged to the `*` space. This now matches the behaviour of the server side auto sync and the sync which happens when the trained models page is loaded. The trained models page load sync has been extended to the AD and DA jobs lists and the overview page. If the user does not have write permission for ML in every space they cannot sync jobs to the `*` space. In this situation a warning is shown in the flyout and when they sync, the jobs/models will only be added to the current space. 
This commit is contained in:
parent
128739dc46
commit
3d65e892a0
19 changed files with 265 additions and 51 deletions
|
@ -39,6 +39,10 @@ export interface CanDeleteMLSpaceAwareItemsResponse {
|
|||
};
|
||||
}
|
||||
|
||||
export interface CanSyncToAllSpacesResponse {
|
||||
canSync: boolean;
|
||||
}
|
||||
|
||||
export type JobsSpacesResponse = {
|
||||
[jobType in JobType]: { [jobId: string]: string[] };
|
||||
};
|
||||
|
|
|
@ -28,6 +28,7 @@ import { useMlApi } from '../../contexts/kibana';
|
|||
import type { SyncSavedObjectResponse, SyncResult } from '../../../../common/types/saved_objects';
|
||||
import { SyncList } from './sync_list';
|
||||
import { useToastNotificationService } from '../../services/toast_notification_service';
|
||||
import { SyncToAllSpacesWarning } from './sync_to_all_spaces_warning';
|
||||
|
||||
export interface Props {
|
||||
onClose: () => void;
|
||||
|
@ -37,17 +38,22 @@ export const JobSpacesSyncFlyout: FC<Props> = ({ onClose }) => {
|
|||
const { displayErrorToast, displaySuccessToast } = useToastNotificationService();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [canSync, setCanSync] = useState(false);
|
||||
const [canSyncToAllSpaces, setCanSyncToAllSpaces] = useState(true);
|
||||
const [syncResp, setSyncResp] = useState<SyncSavedObjectResponse | null>(null);
|
||||
const {
|
||||
savedObjects: { syncSavedObjects },
|
||||
savedObjects: { syncSavedObjects, canSyncToAllSpaces: canSyncToAllSpacesFunc },
|
||||
} = useMlApi();
|
||||
|
||||
async function loadSyncList(simulate: boolean = true) {
|
||||
setLoading(true);
|
||||
try {
|
||||
const resp = await syncSavedObjects(simulate);
|
||||
const resp = await syncSavedObjects(simulate, canSyncToAllSpaces);
|
||||
setSyncResp(resp);
|
||||
|
||||
if (simulate === true) {
|
||||
setCanSyncToAllSpaces((await canSyncToAllSpacesFunc()).canSync);
|
||||
}
|
||||
|
||||
const count = Object.values(resp).reduce((acc, cur) => acc + Object.keys(cur).length, 0);
|
||||
setCanSync(count > 0);
|
||||
setLoading(false);
|
||||
|
@ -118,6 +124,12 @@ export const JobSpacesSyncFlyout: FC<Props> = ({ onClose }) => {
|
|||
/>
|
||||
</EuiText>
|
||||
</EuiCallOut>
|
||||
{canSyncToAllSpaces === false ? (
|
||||
<>
|
||||
<EuiSpacer size="s" />
|
||||
<SyncToAllSpacesWarning />
|
||||
</>
|
||||
) : null}
|
||||
<EuiSpacer />
|
||||
<SyncList syncItems={syncResp} />
|
||||
</EuiFlyoutBody>
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import type { FC } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { EuiCallOut, EuiLink, EuiText } from '@elastic/eui';
|
||||
import { useMlKibana } from '../../contexts/kibana/kibana_context';
|
||||
|
||||
export const SyncToAllSpacesWarning: FC = () => {
|
||||
const {
|
||||
services: {
|
||||
docLinks: { links },
|
||||
},
|
||||
} = useMlKibana();
|
||||
const docLink = links.security.kibanaPrivileges;
|
||||
return (
|
||||
<EuiCallOut
|
||||
size="s"
|
||||
iconType="help"
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.ml.management.syncSavedObjectsFlyout.allSpacesWarning.title"
|
||||
defaultMessage="Sync can only add items to the current space"
|
||||
/>
|
||||
}
|
||||
color="warning"
|
||||
>
|
||||
<EuiText size="s">
|
||||
<FormattedMessage
|
||||
id="xpack.ml.management.syncSavedObjectsFlyout.allSpacesWarning.description"
|
||||
defaultMessage="Without {readAndWritePrivilegesLink} for all spaces you can only add jobs and trained models to the current space when syncing."
|
||||
values={{
|
||||
readAndWritePrivilegesLink: (
|
||||
<EuiLink href={docLink} target="_blank">
|
||||
<FormattedMessage
|
||||
id="xpack.ml.management.syncSavedObjectsFlyout.privilegeWarningLink"
|
||||
defaultMessage="read and write privileges"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</EuiText>
|
||||
</EuiCallOut>
|
||||
);
|
||||
};
|
|
@ -22,3 +22,7 @@ export const basicResolvers = (): Resolvers => ({
|
|||
getMlNodeCount,
|
||||
loadMlServerInfo,
|
||||
});
|
||||
|
||||
export const initSavedObjects = async (mlApi: MlApi) => {
|
||||
return mlApi.savedObjects.initSavedObjects().catch(() => {});
|
||||
};
|
||||
|
|
|
@ -14,7 +14,7 @@ import type { NavigateToPath } from '../../../contexts/kibana';
|
|||
import type { MlRoute } from '../../router';
|
||||
import { createPath, PageLoader } from '../../router';
|
||||
import { useRouteResolver } from '../../use_resolver';
|
||||
import { basicResolvers } from '../../resolvers';
|
||||
import { basicResolvers, initSavedObjects } from '../../resolvers';
|
||||
import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs';
|
||||
|
||||
const Page = dynamic(async () => ({
|
||||
|
@ -45,7 +45,10 @@ export const analyticsJobsListRouteFactory = (
|
|||
});
|
||||
|
||||
const PageWrapper: FC = () => {
|
||||
const { context } = useRouteResolver('full', ['canGetDataFrameAnalytics'], basicResolvers());
|
||||
const { context } = useRouteResolver('full', ['canGetDataFrameAnalytics'], {
|
||||
...basicResolvers(),
|
||||
initSavedObjects,
|
||||
});
|
||||
return (
|
||||
<PageLoader context={context}>
|
||||
<Page />
|
||||
|
|
|
@ -24,7 +24,7 @@ import { useRouteResolver } from '../use_resolver';
|
|||
import { getBreadcrumbWithUrlForApp } from '../breadcrumbs';
|
||||
import { AnnotationUpdatesService } from '../../services/annotations_service';
|
||||
import { MlAnnotationUpdatesContext } from '../../contexts/ml/ml_annotation_updates_context';
|
||||
import { basicResolvers } from '../resolvers';
|
||||
import { basicResolvers, initSavedObjects } from '../resolvers';
|
||||
|
||||
const JobsPage = dynamic(async () => ({
|
||||
default: (await import('../../jobs/jobs_list')).JobsPage,
|
||||
|
@ -51,7 +51,10 @@ export const jobListRouteFactory = (navigateToPath: NavigateToPath, basePath: st
|
|||
});
|
||||
|
||||
const PageWrapper: FC = () => {
|
||||
const { context } = useRouteResolver('full', ['canGetJobs'], basicResolvers());
|
||||
const { context } = useRouteResolver('full', ['canGetJobs'], {
|
||||
...basicResolvers(),
|
||||
initSavedObjects,
|
||||
});
|
||||
|
||||
const timefilter = useTimefilter({ timeRangeSelector: false, autoRefreshSelector: true });
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@ import { getBreadcrumbWithUrlForApp } from '../breadcrumbs';
|
|||
import type { MlRoute, PageProps } from '../router';
|
||||
import { createPath, PageLoader } from '../router';
|
||||
import { useRouteResolver } from '../use_resolver';
|
||||
import { initSavedObjects } from '../resolvers';
|
||||
|
||||
const OverviewPage = React.lazy(() => import('../../overview/overview_page'));
|
||||
|
||||
|
@ -48,6 +49,7 @@ const PageWrapper: FC<PageProps> = () => {
|
|||
const { context } = useRouteResolver('full', ['canGetMlInfo'], {
|
||||
getMlNodeCount,
|
||||
loadMlServerInfo,
|
||||
initSavedObjects,
|
||||
});
|
||||
|
||||
useTimefilter({ timeRangeSelector: false, autoRefreshSelector: false });
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { type FC, useCallback } from 'react';
|
||||
import type { FC } from 'react';
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
@ -16,10 +16,9 @@ import type { NavigateToPath } from '../../../contexts/kibana';
|
|||
import type { MlRoute } from '../../router';
|
||||
import { createPath, PageLoader } from '../../router';
|
||||
import { useRouteResolver } from '../../use_resolver';
|
||||
import { basicResolvers } from '../../resolvers';
|
||||
import { basicResolvers, initSavedObjects } from '../../resolvers';
|
||||
import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs';
|
||||
import { MlPageHeader } from '../../../components/page_header';
|
||||
import { useSavedObjectsApiService } from '../../../services/ml_api_service/saved_objects';
|
||||
|
||||
const ModelsList = dynamic(async () => ({
|
||||
default: (await import('../../../model_management/models_list')).ModelsList,
|
||||
|
@ -49,19 +48,9 @@ export const modelsListRouteFactory = (
|
|||
});
|
||||
|
||||
const PageWrapper: FC = () => {
|
||||
const { initSavedObjects } = useSavedObjectsApiService();
|
||||
|
||||
const initSavedObjectsWrapper = useCallback(async () => {
|
||||
try {
|
||||
await initSavedObjects();
|
||||
} catch (error) {
|
||||
// ignore error as user may not have permission to sync
|
||||
}
|
||||
}, [initSavedObjects]);
|
||||
|
||||
const { context } = useRouteResolver('full', ['canGetTrainedModels'], {
|
||||
...basicResolvers(),
|
||||
initSavedObjectsWrapper,
|
||||
initSavedObjects,
|
||||
});
|
||||
|
||||
return (
|
||||
|
|
|
@ -23,6 +23,7 @@ import type {
|
|||
JobsSpacesResponse,
|
||||
TrainedModelsSpacesResponse,
|
||||
SyncCheckResponse,
|
||||
CanSyncToAllSpacesResponse,
|
||||
} from '../../../../common/types/saved_objects';
|
||||
|
||||
export const savedObjectsApiProvider = (httpService: HttpService) => ({
|
||||
|
@ -56,11 +57,11 @@ export const savedObjectsApiProvider = (httpService: HttpService) => ({
|
|||
version: '1',
|
||||
});
|
||||
},
|
||||
syncSavedObjects(simulate: boolean = false) {
|
||||
syncSavedObjects(simulate: boolean = false, addToAllSpaces?: boolean) {
|
||||
return httpService.http<SyncSavedObjectResponse>({
|
||||
path: `${ML_EXTERNAL_BASE_PATH}/saved_objects/sync`,
|
||||
method: 'GET',
|
||||
query: { simulate },
|
||||
query: { simulate, addToAllSpaces },
|
||||
version: '2023-10-31',
|
||||
});
|
||||
},
|
||||
|
@ -90,6 +91,15 @@ export const savedObjectsApiProvider = (httpService: HttpService) => ({
|
|||
version: '1',
|
||||
});
|
||||
},
|
||||
canSyncToAllSpaces(mlSavedObjectType?: MlSavedObjectType) {
|
||||
return httpService.http<CanSyncToAllSpacesResponse>({
|
||||
path: `${ML_INTERNAL_BASE_PATH}/saved_objects/can_sync_to_all_spaces${
|
||||
mlSavedObjectType !== undefined ? `/${mlSavedObjectType}` : ''
|
||||
}`,
|
||||
method: 'GET',
|
||||
version: '1',
|
||||
});
|
||||
},
|
||||
trainedModelsSpaces() {
|
||||
return httpService.http<TrainedModelsSpacesResponse>({
|
||||
path: `${ML_INTERNAL_BASE_PATH}/saved_objects/trained_models_spaces`,
|
||||
|
|
|
@ -870,8 +870,8 @@ export class DataRecognizer {
|
|||
);
|
||||
if (applyToAllSpaces === true) {
|
||||
const canCreateGlobalJobs = await this._mlSavedObjectService.canCreateGlobalMlSavedObjects(
|
||||
'anomaly-detector',
|
||||
this._request
|
||||
this._request,
|
||||
'anomaly-detector'
|
||||
);
|
||||
if (canCreateGlobalJobs === true) {
|
||||
await this._mlSavedObjectService.updateJobsSpaces(
|
||||
|
|
|
@ -92,9 +92,9 @@ export function savedObjectsRoutes(
|
|||
routeGuard.fullLicenseAPIGuard(
|
||||
async ({ client, request, response, mlSavedObjectService }) => {
|
||||
try {
|
||||
const { simulate } = request.query;
|
||||
const { simulate, addToAllSpaces } = request.query;
|
||||
const { syncSavedObjects } = syncSavedObjectsFactory(client, mlSavedObjectService);
|
||||
const savedObjects = await syncSavedObjects(simulate);
|
||||
const savedObjects = await syncSavedObjects(simulate, addToAllSpaces ?? true);
|
||||
|
||||
return response.ok({
|
||||
body: savedObjects,
|
||||
|
@ -450,4 +450,46 @@ export function savedObjectsRoutes(
|
|||
}
|
||||
)
|
||||
);
|
||||
|
||||
router.versioned
|
||||
.get({
|
||||
path: `${ML_INTERNAL_BASE_PATH}/saved_objects/can_sync_to_all_spaces/{mlSavedObjectType?}`,
|
||||
access: 'internal',
|
||||
security: {
|
||||
authz: {
|
||||
requiredPrivileges: [
|
||||
'ml:canGetJobs',
|
||||
'ml:canGetDataFrameAnalytics',
|
||||
'ml:canGetTrainedModels',
|
||||
],
|
||||
},
|
||||
},
|
||||
summary: 'Check whether user can sync a job or trained model to the * space',
|
||||
description: `Check the user's ability to sync jobs or trained models to the * space. Returns whether they are able to sync the job or trained model to the * space.`,
|
||||
})
|
||||
.addVersion(
|
||||
{
|
||||
version: '1',
|
||||
validate: {
|
||||
request: {
|
||||
params: syncCheckSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
routeGuard.fullLicenseAPIGuard(async ({ request, response, mlSavedObjectService }) => {
|
||||
try {
|
||||
const { mlSavedObjectType } = request.params;
|
||||
const canSync = await mlSavedObjectService.canCreateGlobalMlSavedObjects(
|
||||
request,
|
||||
mlSavedObjectType as MlSavedObjectType
|
||||
);
|
||||
|
||||
return response.ok({
|
||||
body: { canSync },
|
||||
});
|
||||
} catch (e) {
|
||||
return response.customError(wrapError(e));
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
|
@ -39,7 +39,10 @@ export const itemsAndCurrentSpace = schema.object({
|
|||
ids: schema.arrayOf(schema.string()),
|
||||
});
|
||||
|
||||
export const syncJobObjects = schema.object({ simulate: schema.maybe(schema.boolean()) });
|
||||
export const syncJobObjects = schema.object({
|
||||
simulate: schema.maybe(schema.boolean()),
|
||||
addToAllSpaces: schema.maybe(schema.boolean()),
|
||||
});
|
||||
|
||||
export const syncCheckSchema = schema.object({ mlSavedObjectType: schema.maybe(schema.string()) });
|
||||
|
||||
|
|
|
@ -319,8 +319,8 @@ export function checksFactory(
|
|||
}, {} as DeleteMLSpaceAwareItemsCheckResponse);
|
||||
}
|
||||
const canCreateGlobalMlSavedObjects = await mlSavedObjectService.canCreateGlobalMlSavedObjects(
|
||||
mlSavedObjectType,
|
||||
request
|
||||
request,
|
||||
mlSavedObjectType
|
||||
);
|
||||
|
||||
const savedObjects =
|
||||
|
|
|
@ -102,7 +102,12 @@ export function mlSavedObjectServiceFactory(
|
|||
return jobs.saved_objects;
|
||||
}
|
||||
|
||||
async function _createJob(jobType: JobType, jobId: string, datafeedId?: string) {
|
||||
async function _createJob(
|
||||
jobType: JobType,
|
||||
jobId: string,
|
||||
datafeedId?: string,
|
||||
addToAllSpaces = false
|
||||
) {
|
||||
await isMlReady();
|
||||
|
||||
const job: JobObject = {
|
||||
|
@ -133,6 +138,7 @@ export function mlSavedObjectServiceFactory(
|
|||
|
||||
await savedObjectsClient.create<JobObject>(ML_JOB_SAVED_OBJECT_TYPE, job, {
|
||||
id,
|
||||
...(addToAllSpaces ? { initialNamespaces: ['*'] } : {}),
|
||||
});
|
||||
_clearSavedObjectsClientCache();
|
||||
}
|
||||
|
@ -182,8 +188,12 @@ export function mlSavedObjectServiceFactory(
|
|||
_clearSavedObjectsClientCache();
|
||||
}
|
||||
|
||||
async function createAnomalyDetectionJob(jobId: string, datafeedId?: string) {
|
||||
await _createJob('anomaly-detector', jobId, datafeedId);
|
||||
async function createAnomalyDetectionJob(
|
||||
jobId: string,
|
||||
datafeedId?: string,
|
||||
addToAllSpaces = false
|
||||
) {
|
||||
await _createJob('anomaly-detector', jobId, datafeedId, addToAllSpaces);
|
||||
}
|
||||
|
||||
async function deleteAnomalyDetectionJob(jobId: string) {
|
||||
|
@ -194,8 +204,8 @@ export function mlSavedObjectServiceFactory(
|
|||
await _forceDeleteJob('anomaly-detector', jobId, namespace);
|
||||
}
|
||||
|
||||
async function createDataFrameAnalyticsJob(jobId: string) {
|
||||
await _createJob('data-frame-analytics', jobId);
|
||||
async function createDataFrameAnalyticsJob(jobId: string, addToAllSpaces = false) {
|
||||
await _createJob('data-frame-analytics', jobId, undefined, addToAllSpaces);
|
||||
}
|
||||
|
||||
async function deleteDataFrameAnalyticsJob(jobId: string) {
|
||||
|
@ -418,8 +428,8 @@ export function mlSavedObjectServiceFactory(
|
|||
}
|
||||
|
||||
async function canCreateGlobalMlSavedObjects(
|
||||
mlSavedObjectType: MlSavedObjectType,
|
||||
request: KibanaRequest
|
||||
request: KibanaRequest,
|
||||
mlSavedObjectType?: MlSavedObjectType
|
||||
) {
|
||||
if (authorization === undefined) {
|
||||
return true;
|
||||
|
@ -428,6 +438,10 @@ export function mlSavedObjectServiceFactory(
|
|||
const { canCreateJobsGlobally, canCreateTrainedModelsGlobally } = await authorizationCheck(
|
||||
request
|
||||
);
|
||||
if (mlSavedObjectType === undefined) {
|
||||
return canCreateJobsGlobally && canCreateTrainedModelsGlobally;
|
||||
}
|
||||
|
||||
return mlSavedObjectType === 'trained-model'
|
||||
? canCreateTrainedModelsGlobally
|
||||
: canCreateJobsGlobally;
|
||||
|
@ -441,8 +455,12 @@ export function mlSavedObjectServiceFactory(
|
|||
return modelObject;
|
||||
}
|
||||
|
||||
async function createTrainedModel(modelId: string, job: TrainedModelJob | null) {
|
||||
await _createTrainedModel(modelId, job);
|
||||
async function createTrainedModel(
|
||||
modelId: string,
|
||||
job: TrainedModelJob | null,
|
||||
addToAllSpaces = false
|
||||
) {
|
||||
await _createTrainedModel(modelId, job, addToAllSpaces);
|
||||
}
|
||||
|
||||
async function bulkCreateTrainedModel(models: TrainedModelObject[], namespaceFallback?: string) {
|
||||
|
@ -486,7 +504,11 @@ export function mlSavedObjectServiceFactory(
|
|||
return models.saved_objects;
|
||||
}
|
||||
|
||||
async function _createTrainedModel(modelId: string, job: TrainedModelJob | null) {
|
||||
async function _createTrainedModel(
|
||||
modelId: string,
|
||||
job: TrainedModelJob | null,
|
||||
addToAllSpaces = false
|
||||
) {
|
||||
await isMlReady();
|
||||
|
||||
const modelObject: TrainedModelObject = {
|
||||
|
@ -513,7 +535,7 @@ export function mlSavedObjectServiceFactory(
|
|||
// the saved object may exist if a previous job with the same ID has been deleted.
|
||||
// if not, this error will be throw which we ignore.
|
||||
}
|
||||
let initialNamespaces;
|
||||
let initialNamespaces = addToAllSpaces ? ['*'] : undefined;
|
||||
// if a job exists for this model, ensure the initial namespaces for the model
|
||||
// are the same as the job
|
||||
if (job !== null) {
|
||||
|
@ -522,7 +544,9 @@ export function mlSavedObjectServiceFactory(
|
|||
job.job_id
|
||||
);
|
||||
|
||||
initialNamespaces = existingJobObject?.namespaces ?? undefined;
|
||||
if (existingJobObject?.namespaces !== undefined) {
|
||||
initialNamespaces = existingJobObject?.namespaces;
|
||||
}
|
||||
}
|
||||
|
||||
await savedObjectsClient.create<TrainedModelObject>(
|
||||
|
|
|
@ -34,7 +34,7 @@ export function syncSavedObjectsFactory(
|
|||
) {
|
||||
const { checkStatus } = checksFactory(client, mlSavedObjectService);
|
||||
|
||||
async function syncSavedObjects(simulate: boolean = false) {
|
||||
async function syncSavedObjects(simulate: boolean = false, addToAllSpaces = false) {
|
||||
const results: SyncSavedObjectResponse = {
|
||||
savedObjectsCreated: {},
|
||||
savedObjectsDeleted: {},
|
||||
|
@ -71,7 +71,11 @@ export function syncSavedObjectsFactory(
|
|||
const datafeedId = job.datafeedId;
|
||||
tasks.push(async () => {
|
||||
try {
|
||||
await mlSavedObjectService.createAnomalyDetectionJob(jobId, datafeedId ?? undefined);
|
||||
await mlSavedObjectService.createAnomalyDetectionJob(
|
||||
jobId,
|
||||
datafeedId ?? undefined,
|
||||
addToAllSpaces
|
||||
);
|
||||
results.savedObjectsCreated[type]![job.jobId] = { success: true };
|
||||
} catch (error) {
|
||||
results.savedObjectsCreated[type]![job.jobId] = {
|
||||
|
@ -97,7 +101,7 @@ export function syncSavedObjectsFactory(
|
|||
const jobId = job.jobId;
|
||||
tasks.push(async () => {
|
||||
try {
|
||||
await mlSavedObjectService.createDataFrameAnalyticsJob(jobId);
|
||||
await mlSavedObjectService.createDataFrameAnalyticsJob(jobId, addToAllSpaces);
|
||||
results.savedObjectsCreated[type]![job.jobId] = {
|
||||
success: true,
|
||||
};
|
||||
|
@ -136,7 +140,11 @@ export function syncSavedObjectsFactory(
|
|||
return;
|
||||
}
|
||||
const job = getJobDetailsFromTrainedModel(mod);
|
||||
await mlSavedObjectService.createTrainedModel(modelId, job);
|
||||
await mlSavedObjectService.createTrainedModel(
|
||||
modelId,
|
||||
job,
|
||||
addToAllSpaces || modelId.startsWith('.')
|
||||
);
|
||||
if (modelId.startsWith('.')) {
|
||||
// if the model id starts with a dot, it is an internal model and should be in all spaces
|
||||
await mlSavedObjectService.updateTrainedModelsSpaces([modelId], ['*'], []);
|
||||
|
@ -344,9 +352,9 @@ export function syncSavedObjectsFactory(
|
|||
|
||||
const jobObjects: Array<{ job: JobObject; namespaces: string[] }> = [];
|
||||
const datafeeds: Array<{ jobId: string; datafeedId: string }> = [];
|
||||
const types: JobType[] = ['anomaly-detector', 'data-frame-analytics'];
|
||||
const jobTypes: JobType[] = ['anomaly-detector', 'data-frame-analytics'];
|
||||
|
||||
types.forEach((type) => {
|
||||
jobTypes.forEach((type) => {
|
||||
status.jobs[type].forEach((job) => {
|
||||
if (job.checks.savedObjectExits === false) {
|
||||
if (simulate === true) {
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
import { USER } from '../../../../functional/services/ml/security_common';
|
||||
import { getCommonRequestHeader } from '../../../../functional/services/ml/common_api';
|
||||
|
||||
export default ({ getService }: FtrProviderContext) => {
|
||||
const ml = getService('ml');
|
||||
const spacesService = getService('spaces');
|
||||
const supertest = getService('supertestWithoutAuth');
|
||||
|
||||
const idSpace1 = 'space1';
|
||||
const idSpace2 = 'space2';
|
||||
|
||||
async function runRequest(user: USER, expectedStatusCode: number) {
|
||||
const { body, status } = await supertest
|
||||
.get(`/internal/ml/saved_objects/can_sync_to_all_spaces`)
|
||||
.auth(user, ml.securityCommon.getPasswordForUser(user))
|
||||
.set(getCommonRequestHeader('1'));
|
||||
ml.api.assertResponseStatusCode(expectedStatusCode, status, body);
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
describe('GET saved_objects/can_sync_to_all_spaces', () => {
|
||||
beforeEach(async () => {
|
||||
await spacesService.create({ id: idSpace1, name: 'space_one', disabledFeatures: [] });
|
||||
await spacesService.create({ id: idSpace2, name: 'space_two', disabledFeatures: [] });
|
||||
await ml.testResources.setKibanaTimeZoneToUTC();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await spacesService.delete(idSpace1);
|
||||
await spacesService.delete(idSpace2);
|
||||
});
|
||||
|
||||
it('user can sync to all spaces', async () => {
|
||||
const body = await runRequest(USER.ML_POWERUSER, 200);
|
||||
expect(body).to.eql({ canSync: true });
|
||||
});
|
||||
it('user can not sync to all spaces', async () => {
|
||||
const body = await runRequest(USER.ML_POWERUSER_SPACE1, 200);
|
||||
expect(body).to.eql({ canSync: false });
|
||||
});
|
||||
});
|
||||
};
|
|
@ -21,5 +21,6 @@ export default function ({ loadTestFile }: FtrProviderContext) {
|
|||
loadTestFile(require.resolve('./update_jobs_spaces'));
|
||||
loadTestFile(require.resolve('./update_trained_model_spaces'));
|
||||
loadTestFile(require.resolve('./remove_from_current_space'));
|
||||
loadTestFile(require.resolve('./can_sync_to_all_spaces'));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -187,7 +187,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
const model1 = getTestModel(modelIdSpace1, 'classification', dfaJobId1);
|
||||
await ml.api.createTrainedModelES(model1.model_id, model1.body);
|
||||
|
||||
// create trained model not linked to job, it should have the current space
|
||||
// create trained model not linked to job, it should have * space after sync
|
||||
const model2 = getTestModel(modelIdSpace2, 'classification');
|
||||
await ml.api.createTrainedModelES(model2.model_id, model2.body);
|
||||
|
||||
|
@ -199,7 +199,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
await runSyncRequest(USER.ML_POWERUSER_ALL_SPACES, 200);
|
||||
|
||||
await ml.api.assertTrainedModelSpaces(modelIdSpace1, [idSpace1, idSpace2]);
|
||||
await ml.api.assertTrainedModelSpaces(modelIdSpace2, [idSpace1]);
|
||||
await ml.api.assertTrainedModelSpaces(modelIdSpace2, ['*']);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
|
@ -63,6 +63,12 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
await ml.stackManagementJobs.assertSyncFlyoutSyncButtonEnabled(false);
|
||||
});
|
||||
|
||||
it('should not have objects to sync', async () => {
|
||||
await ml.navigation.navigateToMl();
|
||||
await ml.navigation.navigateToAnomalyDetection();
|
||||
await ml.overviewPage.assertJobSyncRequiredWarningNotExists();
|
||||
});
|
||||
|
||||
it('should prepare test data', async () => {
|
||||
// create jobs
|
||||
|
||||
|
@ -102,8 +108,8 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
});
|
||||
|
||||
it('should have objects to sync', async () => {
|
||||
// sync required warning is displayed
|
||||
await ml.navigation.navigateToMl();
|
||||
await ml.jobTable.refreshJobList();
|
||||
|
||||
await ml.overviewPage.assertJobSyncRequiredWarningExists();
|
||||
|
||||
// object counts in sync flyout are all 1, sync button is enabled
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue