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

[8.x] [ML] Sync ML saved objects to all spaces (#202175) #205693

Merged
merged 1 commit into from
Jan 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ export interface CanDeleteMLSpaceAwareItemsResponse {
};
}

export interface CanSyncToAllSpacesResponse {
canSync: boolean;
}

export type JobsSpacesResponse = {
[jobType in JobType]: { [jobId: string]: string[] };
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
Expand Down Expand Up @@ -118,6 +124,12 @@ export const JobSpacesSyncFlyout: FC<Props> = ({ onClose }) => {
/>
</EuiText>
</EuiCallOut>
{canSyncToAllSpaces === false ? (
<>
<EuiSpacer size="s" />
<SyncToAllSpacesWarning />
</>
) : null}
<EuiSpacer />
<SyncList syncItems={syncResp} />
</EuiFlyoutBody>
Expand Down
Original file line number Diff line number Diff line change
@@ -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>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,7 @@ export const basicResolvers = (): Resolvers => ({
getMlNodeCount,
loadMlServerInfo,
});

export const initSavedObjects = async (mlApi: MlApi) => {
return mlApi.savedObjects.initSavedObjects().catch(() => {});
};
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => ({
Expand Down Expand Up @@ -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 />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 });

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'));

Expand Down Expand Up @@ -48,6 +49,7 @@ const PageWrapper: FC<PageProps> = () => {
const { context } = useRouteResolver('full', ['canGetMlInfo'], {
getMlNodeCount,
loadMlServerInfo,
initSavedObjects,
});

useTimefilter({ timeRangeSelector: false, autoRefreshSelector: false });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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,
Expand Down Expand Up @@ -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 (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import type {
JobsSpacesResponse,
TrainedModelsSpacesResponse,
SyncCheckResponse,
CanSyncToAllSpacesResponse,
} from '../../../../common/types/saved_objects';

export const savedObjectsApiProvider = (httpService: HttpService) => ({
Expand Down Expand Up @@ -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',
});
},
Expand Down Expand Up @@ -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`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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));
}
})
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()) });

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -319,8 +319,8 @@ export function checksFactory(
}, {} as DeleteMLSpaceAwareItemsCheckResponse);
}
const canCreateGlobalMlSavedObjects = await mlSavedObjectService.canCreateGlobalMlSavedObjects(
mlSavedObjectType,
request
request,
mlSavedObjectType
);

const savedObjects =
Expand Down
Loading
Loading