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] Add ML saved objects to Kibana saved objects management page #205177

Open
wants to merge 23 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 9 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 @@ -328,6 +328,9 @@ export class Table extends PureComponent<TableProps, TableState> {
icon: 'kqlSelector',
onClick: (object) => onShowRelationships(object),
'data-test-subj': 'savedObjectsTableAction-relationships',
available: (object) => {
return !object.type.startsWith('ml');
},
},
...actionRegistry.getAll().map((action) => {
action.setActionContext({ capabilities });
Expand Down Expand Up @@ -386,9 +389,30 @@ export class Table extends PureComponent<TableProps, TableState> {
const activeActionContents = this.state.activeAction?.render() ?? null;
const exceededResultCount = totalItemCount > MAX_PAGINATED_ITEM;

const hasMlObjects = selectedSavedObjects.some(({ type }) => type === 'ml-job');

const anySelected = selectedSavedObjects.length > 0;
const allHidden =
anySelected && selectedSavedObjects.every(({ meta: { hiddenType } }) => hiddenType);

const deleteTooltip = () => {
if (hasMlObjects) {
return (
<FormattedMessage
id="savedObjectsManagement.objectsTable.table.hasMlObjects.deleteDisabledTooltip"
defaultMessage="Machine learning objects can only be deleted in the Machine Learning management page."
/>
);
}
if (allHidden) {
return (
<FormattedMessage
id="savedObjectsManagement.objectsTable.table.deleteDisabledTooltip"
defaultMessage="Selected objects can’t be deleted because they are hidden objects."
/>
);
}
};
return (
<Fragment>
{activeActionContents}
Expand All @@ -406,22 +430,18 @@ export class Table extends PureComponent<TableProps, TableState> {
<EuiToolTip
data-test-subj="deleteSOToolTip"
key="deleteSOToolTip"
content={
allHidden ? (
<FormattedMessage
id="savedObjectsManagement.objectsTable.table.deleteDisabledTooltip"
defaultMessage="Selected objects can’t be deleted because they are hidden objects."
/>
) : undefined
}
content={deleteTooltip()}
>
<EuiButton
key="deleteSO"
iconType="trash"
color="danger"
onClick={onDelete}
isDisabled={
!anySelected || allHidden || !capabilities.savedObjectsManagement.delete
hasMlObjects ||
!anySelected ||
allHidden ||
!capabilities.savedObjectsManagement.delete
}
title={
capabilities.savedObjectsManagement.delete
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,13 @@ import {
ExportModal,
} from './components';

// Saved objects for ML job are not importable/exportable because they are wrappers around ES objects
const DISABLED_TYPES_FOR_EXPORT = new Set(['ml-job', 'trained-model']);

interface ExportAllOption {
id: string;
label: string;
disabled?: boolean;
}

export interface SavedObjectsTableProps {
Expand Down Expand Up @@ -186,18 +190,18 @@ export class SavedObjectsTable extends Component<SavedObjectsTableProps, SavedOb
([id, count]) => ({
id,
label: `${id} (${count || 0})`,
disabled: DISABLED_TYPES_FOR_EXPORT.has(id),
})
);
const exportAllSelectedOptions: Record<string, boolean> = exportAllOptions.reduce(
(record, { id }) => {
return {
...record,
[id]: true,
[id]: DISABLED_TYPES_FOR_EXPORT.has(id) ? false : true,
};
},
{}
);

// Fetch all the saved objects that exist so we can accurately populate the counts within
// the table filter dropdown.
const savedObjectCounts = await getSavedObjectCounts({
Expand Down Expand Up @@ -420,7 +424,7 @@ export class SavedObjectsTable extends Component<SavedObjectsTableProps, SavedOb
const { notifications, http, taggingApi, allowedTypes } = this.props;
const { queryText, selectedTags } = parseQuery(activeQuery, allowedTypes);
const exportTypes = Object.entries(exportAllSelectedOptions).reduce((accum, [id, selected]) => {
if (selected) {
if (selected && !DISABLED_TYPES_FOR_EXPORT.has(id)) {
accum.push(id);
}
return accum;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,11 @@ export class CopyToSpaceSavedObjectsManagementAction extends SavedObjectsManagem
icon: 'copy',
type: 'icon',
available: (object: SavedObjectsManagementRecord) => {
return object.meta.namespaceType !== 'agnostic' && !object.meta.hiddenType;
return (
object.meta.namespaceType !== 'agnostic' &&
!object.meta.hiddenType &&
!object.type.startsWith('ml')
);
},
onClick: (object: SavedObjectsManagementRecord) => {
this.start(object);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,15 @@ export class ShareToSpaceSavedObjectsManagementAction extends SavedObjectsManage
!this.actionContext ||
!!this.actionContext.capabilities.savedObjectsManagement.shareIntoSpace;
const { namespaceType, hiddenType } = object.meta;

if (object.type.startsWith('ml-')) {
const hasMlShareCapabilities =
this.actionContext?.capabilities?.ml?.canGetJobs &&
this.actionContext?.capabilities?.ml?.canCreateJob;
return Boolean(
namespaceType === 'multiple' && !hiddenType && hasCapability && hasMlShareCapabilities
);
}
return namespaceType === 'multiple' && !hiddenType && hasCapability;
},
onClick: (object: SavedObjectsManagementRecord) => {
Expand Down
10 changes: 7 additions & 3 deletions src/plugins/saved_objects_management/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import { firstValueFrom, Subject } from 'rxjs';
import { CoreSetup, CoreStart, Logger, Plugin, PluginInitializerContext } from '@kbn/core/server';
import type { CapabilitiesStart } from '@kbn/core-capabilities-server/src/contracts';
import { SavedObjectsManagementPluginSetup, SavedObjectsManagementPluginStart } from './types';
import { SavedObjectsManagement } from './services';
import { registerRoutes } from './routes';
Expand All @@ -19,28 +20,31 @@ export class SavedObjectsManagementPlugin
{
private readonly logger: Logger;
private managementService$ = new Subject<SavedObjectsManagement>();
private capabilitiesService$ = new Subject<CapabilitiesStart>();

constructor(private readonly context: PluginInitializerContext) {
this.logger = this.context.logger.get();
}

public setup({ http, capabilities }: CoreSetup) {
public async setup({ http, capabilities, getStartServices }: CoreSetup) {
this.logger.debug('Setting up SavedObjectsManagement plugin');
registerRoutes({
http,
managementServicePromise: firstValueFrom(this.managementService$),
capabilitiesPromise: firstValueFrom(this.capabilitiesService$),
});

capabilities.registerProvider(capabilitiesProvider);

return {};
}

public start(core: CoreStart) {
public async start(core: CoreStart) {
this.logger.debug('Starting up SavedObjectsManagement plugin');
const managementService = new SavedObjectsManagement(core.savedObjects.getTypeRegistry());
this.managementService$.next(managementService);

this.managementService$.next(managementService);
this.capabilitiesService$.next(core.capabilities);
return {};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
*/

import type { IRouter, SavedObjectsType } from '@kbn/core/server';
import type { CapabilitiesStart } from '@kbn/core-capabilities-server/src/contracts';
import type { SavedObjectManagementTypeInfo } from '../../common';

const convertType = (sot: SavedObjectsType): SavedObjectManagementTypeInfo => {
Expand All @@ -19,7 +20,10 @@ const convertType = (sot: SavedObjectsType): SavedObjectManagementTypeInfo => {
};
};

export const registerGetAllowedTypesRoute = (router: IRouter) => {
export const registerGetAllowedTypesRoute = (
router: IRouter,
capabilitiesPromise?: Promise<CapabilitiesStart>
) => {
router.get(
{
path: '/api/kibana/management/saved_objects/_allowed_types',
Expand All @@ -37,6 +41,34 @@ export const registerGetAllowedTypesRoute = (router: IRouter) => {
.filter((type) => type.management!.visibleInManagement ?? true)
.map(convertType);

if (capabilitiesPromise) {
// Check if the user has the capability to see ML objects
// if so, add it to the allowed types
// ML objects are not importable and exportable within management page
// as they are just wrappers around Elasticsearch objects
const capabilities = await capabilitiesPromise;
const mlCapabilities = await capabilities.resolveCapabilities(req, {
capabilityPath: 'ml.*',
});
const canSeeMLJobs =
mlCapabilities.ml.canGetJobs && mlCapabilities.ml.canGetDataFrameAnalytics;
Copy link
Member

@jgowdyelastic jgowdyelastic Jan 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can't group the two ML job types together, AD and DFA need to be separate.
In a serverless environment where the DFA feature is disabled, canGetDataFrameAnalytics will always be false. Same for AD feature.

Looking at what is contained inside mlJobType below, I'm not sure if this will be possible to filter these different job types server side.

A possible solution would be to change this check to an || and add some additional filtering on the client side when displaying table. The client side will also have the capabilities available.

const canSeeTrainedModels = mlCapabilities.ml.canGetTrainedModels;

const [mlJobType, trainedModelType] = await Promise.all([
canSeeMLJobs ? (await context.core).savedObjects.typeRegistry.getType('ml-job') : null,
canSeeTrainedModels
? (await context.core).savedObjects.typeRegistry.getType('ml-trained-model')
: null,
]);

if (mlJobType) {
allowedTypes.push(convertType(mlJobType));
}
if (trainedModelType) {
allowedTypes.push(convertType(trainedModelType));
}
}

return res.ok({
body: {
types: allowedTypes,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import type { CapabilitiesStart } from '@kbn/core-capabilities-server/src/contracts';
import { registerRoutes } from '.';
import { ISavedObjectsManagement } from '../services';
import type { ISavedObjectsManagement } from '../services';
import { coreMock, httpServiceMock } from '@kbn/core/server/mocks';

describe('registerRoutes', () => {
Expand All @@ -17,10 +18,12 @@ describe('registerRoutes', () => {
const httpSetup = coreMock.createSetup().http;
httpSetup.createRouter.mockReturnValue(router);
const managementPromise = Promise.resolve({} as ISavedObjectsManagement);
const capabilitiesPromise = Promise.resolve({} as CapabilitiesStart);

registerRoutes({
http: httpSetup,
managementServicePromise: managementPromise,
capabilitiesPromise,
});

expect(httpSetup.createRouter).toHaveBeenCalledTimes(1);
Expand Down
11 changes: 8 additions & 3 deletions src/plugins/saved_objects_management/server/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import { HttpServiceSetup } from '@kbn/core/server';
import type { HttpServiceSetup, CapabilitiesStart } from '@kbn/core/server';
import { ISavedObjectsManagement } from '../services';
import { registerFindRoute } from './find';
import { registerBulkDeleteRoute } from './bulk_delete';
Expand All @@ -19,14 +19,19 @@ import { registerGetAllowedTypesRoute } from './get_allowed_types';
interface RegisterRouteOptions {
http: HttpServiceSetup;
managementServicePromise: Promise<ISavedObjectsManagement>;
capabilitiesPromise?: Promise<CapabilitiesStart>;
}

export function registerRoutes({ http, managementServicePromise }: RegisterRouteOptions) {
export function registerRoutes({
http,
managementServicePromise,
capabilitiesPromise,
}: RegisterRouteOptions) {
const router = http.createRouter();
registerFindRoute(router, managementServicePromise);
registerBulkDeleteRoute(router);
registerBulkGetRoute(router, managementServicePromise);
registerScrollForCountRoute(router);
registerRelationshipsRoute(router, managementServicePromise);
registerGetAllowedTypesRoute(router);
registerGetAllowedTypesRoute(router, capabilitiesPromise);
}
1 change: 1 addition & 0 deletions src/plugins/saved_objects_management/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"@kbn/code-editor",
"@kbn/react-kibana-context-render",
"@kbn/shared-ux-table-persist",
"@kbn/core-capabilities-server",
],
"exclude": [
"target/**/*",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,14 +170,6 @@
"avcBanner.body": "Elastic Security passe avec brio le test de protection contre les malwares réalisé par AV-Comparatives",
"avcBanner.readTheBlog.link": "Lire le blog",
"avcBanner.title": "Protection à 100 % sans aucun faux positif.",
"bfetch.advancedSettings.disableBfetchCompressionDeprecation": "Ce paramètre est déclassé et sera supprimé dans la version 9.0 de Kibana.",
"bfetch.advancedSettings.disableBfetchDeprecation": "Ce paramètre est déclassé et sera supprimé dans la version 9.0 de Kibana.",
"bfetch.disableBfetch": "Désactiver la mise en lots de requêtes",
"bfetch.disableBfetchCompression": "Désactiver la compression par lots",
"bfetch.disableBfetchCompressionDesc": "Vous pouvez désactiver la compression par lots. Cela permet de déboguer des requêtes individuelles, mais augmente la taille des réponses.",
"bfetch.disableBfetchDesc": "Désactive la mise en lot des requêtes. Cette option augmente le nombre de requêtes HTTP depuis Kibana, mais permet de les déboguer individuellement.",
"bfetchError.networkError": "Vérifiez votre connexion réseau et réessayez.",
"bfetchError.networkErrorWithStatus": "Vérifiez votre connexion réseau et réessayez. Code {code}",
"cases.components.status.closed": "Fermé",
"cases.components.status.inProgress": "En cours",
"cases.components.status.open": "Ouvrir",
Expand Down Expand Up @@ -49585,4 +49577,4 @@
"xpack.watcher.watchEdit.thresholdWatchExpression.aggType.fieldIsRequiredValidationMessage": "Ce champ est requis.",
"xpack.watcher.watcherDescription": "Détectez les modifications survenant dans vos données en créant, gérant et monitorant des alertes."
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -170,14 +170,6 @@
"avcBanner.body": "AV-Comparativesのマルウェア保護テストで高い評価を受けたElastic Security",
"avcBanner.readTheBlog.link": "ブログを読む",
"avcBanner.title": "誤検知がゼロの100%保護。",
"bfetch.advancedSettings.disableBfetchCompressionDeprecation": "この設定はサポートが終了し、Kibana 9.0では削除されます。",
"bfetch.advancedSettings.disableBfetchDeprecation": "この設定はサポートが終了し、Kibana 9.0では削除されます。",
"bfetch.disableBfetch": "リクエストバッチを無効にする",
"bfetch.disableBfetchCompression": "バッチ圧縮を無効にする",
"bfetch.disableBfetchCompressionDesc": "バッチ圧縮を無効にします。個別の要求をデバッグできますが、応答サイズが大きくなります。",
"bfetch.disableBfetchDesc": "リクエストバッチを無効にします。これにより、KibanaからのHTTPリクエスト数は減りますが、個別にリクエストをデバッグできます。",
"bfetchError.networkError": "ネットワーク接続を確認して再試行してください。",
"bfetchError.networkErrorWithStatus": "ネットワーク接続を確認して再試行してください。コード{code}",
"cases.components.status.closed": "終了",
"cases.components.status.inProgress": "進行中",
"cases.components.status.open": "オープン",
Expand Down Expand Up @@ -49434,4 +49426,4 @@
"xpack.watcher.watchEdit.thresholdWatchExpression.aggType.fieldIsRequiredValidationMessage": "フィールドを選択してください。",
"xpack.watcher.watcherDescription": "アラートの作成、管理、監視によりデータへの変更を検知します。"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -196,14 +196,6 @@
"avcBanner.body": "在 AV-Comparatives 进行的恶意软件防护测试中,Elastic Security 表现突出",
"avcBanner.readTheBlog.link": "阅读博客",
"avcBanner.title": "提供全面保护,误报率为零。",
"bfetch.advancedSettings.disableBfetchCompressionDeprecation": "此设置已过时,将在 Kibana 9.0 中移除。",
"bfetch.advancedSettings.disableBfetchDeprecation": "此设置已过时,将在 Kibana 9.0 中移除。",
"bfetch.disableBfetch": "禁用请求批处理",
"bfetch.disableBfetchCompression": "禁用批量压缩",
"bfetch.disableBfetchCompressionDesc": "禁用批量压缩。这允许您对单个请求进行故障排查,但会增加响应大小。",
"bfetch.disableBfetchDesc": "禁用请求批处理。这会增加来自 Kibana 的 HTTP 请求数,但允许对单个请求进行故障排查。",
"bfetchError.networkError": "检查您的网络连接,然后重试。",
"bfetchError.networkErrorWithStatus": "检查您的网络连接,然后重试。代码 {code}",
"cases.components.status.closed": "已关闭",
"cases.components.status.inProgress": "进行中",
"cases.components.status.open": "打开",
Expand Down Expand Up @@ -48707,4 +48699,4 @@
"xpack.watcher.watchEdit.thresholdWatchExpression.aggType.fieldIsRequiredValidationMessage": "此字段必填。",
"xpack.watcher.watcherDescription": "通过创建、管理和监测警报来检测数据中的更改。"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@

import type { ErrorType } from '@kbn/ml-error-utils';

export type JobType = 'anomaly-detector' | 'data-frame-analytics';
export const AD_SAVED_OBJECT_TYPE = 'anomaly-detector';
export const DFA_SAVED_OBJECT_TYPE = 'data-frame-analytics';

export type JobType = typeof AD_SAVED_OBJECT_TYPE | typeof DFA_SAVED_OBJECT_TYPE;
export type TrainedModelType = 'trained-model';
export type MlSavedObjectType = JobType | TrainedModelType;

Expand Down
Loading