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 Spaces column to Anomaly Detection, Data Frame Analytics and Trained Models management pages #206696

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
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 @@ -7,8 +7,11 @@

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

export type JobType = 'anomaly-detector' | 'data-frame-analytics';
export type TrainedModelType = 'trained-model';
export const ANOMALY_DETECTOR_SAVED_OBJECT_TYPE = 'anomaly-detector';
export const DFA_SAVED_OBJECT_TYPE = 'data-frame-analytics';
export const TRAINED_MODEL_SAVED_OBJECT_TYPE = 'trained-model';
export type JobType = typeof ANOMALY_DETECTOR_SAVED_OBJECT_TYPE | typeof DFA_SAVED_OBJECT_TYPE;
export type TrainedModelType = typeof TRAINED_MODEL_SAVED_OBJECT_TYPE;
export type MlSavedObjectType = JobType | TrainedModelType;

export const ML_JOB_SAVED_OBJECT_TYPE = 'ml-job';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,10 @@ interface BaseModelItem {
* Indices with associated pipelines that have inference processors utilizing the model deployments.
*/
indices?: string[];
/**
* Spaces associated with the model
*/
spaces?: string[];
}

/** Common properties for existing NLP models and NLP model download configs */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ const App: FC<AppProps> = ({
unifiedSearch: deps.unifiedSearch,
usageCollection: deps.usageCollection,
mlServices: getMlGlobalServices(coreStart, deps.data.dataViews, deps.usageCollection),
spaces: deps.spaces,
};
}, [deps, coreStart]);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*
* 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 { SpacesContextProps } from '@kbn/spaces-plugin/public';

export const getEmptyFunctionComponent: React.FC<SpacesContextProps> = ({ children }) => (
<>{children}</>
);
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { useToastNotificationService } from '../../services/toast_notification_s

interface Props {
spacesApi: SpacesPluginStart; // this component is only ever used when spaces is enabled
spaceIds: string[];
spaceIds?: string[];
id: string;
mlSavedObjectType: MlSavedObjectType;
refresh(): void;
Expand All @@ -33,9 +33,10 @@ const modelObjectNoun = i18n.translate('xpack.ml.management.jobsSpacesList.model
defaultMessage: 'trained model',
});

const FALLBACK_SPACES_ID: string[] = [];
export const MLSavedObjectsSpacesList: FC<Props> = ({
spacesApi,
spaceIds,
spaceIds = FALLBACK_SPACES_ID,
id,
mlSavedObjectType,
refresh,
Expand Down Expand Up @@ -93,7 +94,7 @@ export const MLSavedObjectsSpacesList: FC<Props> = ({

const shareToSpaceFlyoutProps: ShareToSpaceFlyoutProps = {
savedObjectTarget: {
type: ML_JOB_SAVED_OBJECT_TYPE,
type: mlSavedObjectType === 'anomaly-detector' ? ML_JOB_SAVED_OBJECT_TYPE : mlSavedObjectType,
id,
namespaces: spaceIds,
title: id,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ interface StartPlugins {
savedSearch: SavedSearchPublicPluginStart;
security?: SecurityPluginStart;
share: SharePluginStart;
spaces?: SpacesPluginStart;
spacesApi?: SpacesPluginStart;
triggersActionsUi?: TriggersAndActionsUIPublicPluginStart;
uiActions: UiActionsStart;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import { AnalyticsEmptyPrompt } from '../empty_prompt';
import { useTableSettings } from './use_table_settings';
import { JobsAwaitingNodeWarning } from '../../../../../components/jobs_awaiting_node_warning';
import { useRefresh } from '../../../../../routing/use_refresh';
import { useSpacesContextWrapper } from '../../../../../hooks/use_spaces';

const filters: EuiSearchBarProps['filters'] = [
{
Expand Down Expand Up @@ -170,6 +171,8 @@ export const DataFrameAnalyticsList: FC<Props> = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
const getAnalyticsCallback = useCallback(() => getAnalytics(true), []);

const SpacesContextWrapper = useSpacesContextWrapper();

// Subscribe to the refresh observable to trigger reloading the analytics list.
const { refresh } = useRefreshAnalyticsList({
isLoading: setIsLoading,
Expand Down Expand Up @@ -259,43 +262,45 @@ export const DataFrameAnalyticsList: FC<Props> = ({
};

return (
<div data-test-subj="mlAnalyticsJobList">
{modals}
<JobsAwaitingNodeWarning jobCount={jobsAwaitingNodeCount} />
<EuiFlexGroup justifyContent="spaceBetween">
{stats}
<EuiFlexItem grow={false}>
<EuiFlexGroup alignItems="center" gutterSize="s">
<EuiFlexItem grow={false}>
<CreateAnalyticsButton
isDisabled={disabled}
navigateToSourceSelection={navigateToSourceSelection}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="m" />
<div data-test-subj="mlAnalyticsTableContainer">
<EuiInMemoryTable<DataFrameAnalyticsListRow>
rowHeader={DataFrameAnalyticsListColumn.id}
allowNeutralSort={false}
columns={columns}
itemIdToExpandedRowMap={itemIdToExpandedRowMap}
items={analytics}
itemId={DataFrameAnalyticsListColumn.id}
loading={isLoading}
onTableChange={onTableChange}
pagination={pagination}
sorting={sorting}
search={search}
data-test-subj={isLoading ? 'mlAnalyticsTable loading' : 'mlAnalyticsTable loaded'}
rowProps={(item) => ({
'data-test-subj': `mlAnalyticsTableRow row-${item.id}`,
})}
error={searchError}
/>
<SpacesContextWrapper>
<div data-test-subj="mlAnalyticsJobList">
{modals}
<JobsAwaitingNodeWarning jobCount={jobsAwaitingNodeCount} />
<EuiFlexGroup justifyContent="spaceBetween">
{stats}
<EuiFlexItem grow={false}>
<EuiFlexGroup alignItems="center" gutterSize="s">
<EuiFlexItem grow={false}>
<CreateAnalyticsButton
isDisabled={disabled}
navigateToSourceSelection={navigateToSourceSelection}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="m" />
<div data-test-subj="mlAnalyticsTableContainer">
<EuiInMemoryTable<DataFrameAnalyticsListRow>
rowHeader={DataFrameAnalyticsListColumn.id}
allowNeutralSort={false}
columns={columns}
itemIdToExpandedRowMap={itemIdToExpandedRowMap}
items={analytics}
itemId={DataFrameAnalyticsListColumn.id}
loading={isLoading}
onTableChange={onTableChange}
pagination={pagination}
sorting={sorting}
search={search}
data-test-subj={isLoading ? 'mlAnalyticsTable loading' : 'mlAnalyticsTable loaded'}
rowProps={(item) => ({
'data-test-subj': `mlAnalyticsTableRow row-${item.id}`,
})}
error={searchError}
/>
</div>
</div>
</div>
</SpacesContextWrapper>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ export interface DataFrameAnalyticsListRow {
mode: string;
state: DataFrameAnalyticsStats['state'];
stats: DataFrameAnalyticsStats;
spaces?: string[];
}

// Used to pass on attribute names to table columns
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,11 @@ import {
DataFrameAnalyticsListColumn,
} from './common';
import { useActions } from './use_actions';
import { useMlLink } from '../../../../../contexts/kibana';
import { useMlLink, useMlKibana } from '../../../../../contexts/kibana';
import { ML_PAGES } from '../../../../../../../common/constants/locator';
import { MLSavedObjectsSpacesList } from '../../../../../components/ml_saved_objects_spaces_list';
import { DFA_SAVED_OBJECT_TYPE } from '../../../../../../../common/types/saved_objects';
import { useCanManageSpacesAndSavedObjects } from '../../../../../hooks/use_spaces';

const TRUNCATE_TEXT_LINES = 3;

Expand Down Expand Up @@ -164,6 +167,9 @@ export const useColumns = (
isMlEnabledInSpace: boolean = true,
refresh: () => void = () => {}
) => {
const {
services: { spaces },
} = useMlKibana();
const { actions, modals } = useActions();
function toggleDetails(item: DataFrameAnalyticsListRow) {
const index = expandedRowItemIds.indexOf(item.config.id);
Expand All @@ -177,6 +183,9 @@ export const useColumns = (
// spread to a new array otherwise the component wouldn't re-render
setExpandedRowItemIds([...expandedRowItemIds]);
}

const canManageSpacesAndSavedObjects = useCanManageSpacesAndSavedObjects();

// update possible column types to something like (FieldDataColumn | ComputedColumn | ActionsColumn)[] when they have been added to EUI
const columns: any[] = [
{
Expand Down Expand Up @@ -283,6 +292,31 @@ export const useColumns = (
'data-test-subj': 'mlAnalyticsTableColumnStatus',
},
progressColumn,
...(canManageSpacesAndSavedObjects && spaces
? [
{
name: i18n.translate('xpack.ml.jobsList.jobActionsColumn.spaces', {
defaultMessage: 'Spaces',
}),
'data-test-subj': 'mlTableColumnSpaces',
truncateText: true,
align: 'right',
width: '10%',
render: (item: DataFrameAnalyticsListRow) => {
return (
<MLSavedObjectsSpacesList
spacesApi={spaces}
spaceIds={item.spaces}
id={item.id}
mlSavedObjectType={DFA_SAVED_OBJECT_TYPE}
refresh={refresh}
/>
);
},
},
]
: []),

{
name: i18n.translate('xpack.ml.dataframe.analyticsList.tableActionLabel', {
defaultMessage: 'Actions',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
type DataFrameAnalysisConfigType,
DATA_FRAME_TASK_STATE,
} from '@kbn/ml-data-frame-analytics-utils';
import { isPopulatedObject } from '@kbn/ml-is-populated-object';
import { useMlApi } from '../../../../../contexts/kibana';
import type {
GetDataFrameAnalyticsStatsResponseError,
Expand All @@ -27,6 +28,8 @@ import {
isDataFrameAnalyticsStopped,
} from '../../components/analytics_list/common';
import type { AnalyticStatsBarStats } from '../../../../../components/stats_bar';
import { DFA_SAVED_OBJECT_TYPE } from '../../../../../../../common/types/saved_objects';
import { useCanManageSpacesAndSavedObjects } from '../../../../../hooks/use_spaces';

export const isGetDataFrameAnalyticsStatsResponseOk = (
arg: any
Expand Down Expand Up @@ -117,6 +120,7 @@ export const useGetAnalytics = (
blockRefresh: boolean
): GetAnalytics => {
const mlApi = useMlApi();
const canManageSpacesAndSavedObjects = useCanManageSpacesAndSavedObjects();

let concurrentLoads = 0;

Expand All @@ -133,6 +137,13 @@ export const useGetAnalytics = (
const analyticsConfigs = await mlApi.dataFrameAnalytics.getDataFrameAnalytics();
const analyticsStats = await mlApi.dataFrameAnalytics.getDataFrameAnalyticsStats();

let savedObjectsSpaces: Record<string, string[]> = {};
if (canManageSpacesAndSavedObjects && mlApi.savedObjects.jobsSpaces) {
const results = await mlApi.savedObjects.jobsSpaces();
if (isPopulatedObject(results, [DFA_SAVED_OBJECT_TYPE])) {
savedObjectsSpaces = results[DFA_SAVED_OBJECT_TYPE];
}
}
const analyticsStatsResult = isGetDataFrameAnalyticsStatsResponseOk(analyticsStats)
? getAnalyticsJobsStats(analyticsStats)
: undefined;
Expand Down Expand Up @@ -164,6 +175,7 @@ export const useGetAnalytics = (
mode: DATA_FRAME_MODE.BATCH,
state: stats.state,
stats,
spaces: savedObjectsSpaces[config.id],
});
return reducedtableRows;
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* 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 { useMemo } from 'react';
import { useMlKibana } from '../contexts/kibana';
import { getEmptyFunctionComponent } from '../components/empty_component/get_empty_function_component';

export const useCanManageSpacesAndSavedObjects = () => {
const {
services: { spaces, application },
} = useMlKibana();

const canManageSpacesAndSavedObjects = useMemo(
() =>
spaces !== undefined &&
spaces.ui.components.getSpacesContextProvider !== undefined &&
application.capabilities &&
application.capabilities.spaces?.manage === true &&
application.capabilities.savedObjectsManagement?.shareIntoSpace === true,
[spaces, application]
);

return canManageSpacesAndSavedObjects;
};

export const useSpacesContextWrapper = () => {
const {
services: { spaces },
} = useMlKibana();
const canManageSpacesAndSavedObjects = useCanManageSpacesAndSavedObjects();

return useMemo(
() =>
canManageSpacesAndSavedObjects && spaces
? spaces.ui.components.getSpacesContextProvider
: getEmptyFunctionComponent,
[canManageSpacesAndSavedObjects, spaces]
);
};
Loading