[8.x] [ML] Add Spaces column to Anomaly Detection, Data Frame Analytics and Trained Models management pages (#206696) (#208652)

# Backport

This will backport the following commits from `main` to `8.x`:
- [[ML] Add Spaces column to Anomaly Detection, Data Frame Analytics and
Trained Models management pages
(#206696)](https://github.com/elastic/kibana/pull/206696)

<!--- Backport version: 9.4.3 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Quynh Nguyen
(Quinn)","email":"43350163+qn895@users.noreply.github.com"},"sourceCommit":{"committedDate":"2025-01-28T23:12:17Z","message":"[ML]
Add Spaces column to Anomaly Detection, Data Frame Analytics and Trained
Models management pages (#206696)\n\n## Summary\r\n\r\nThis PR adds a
new `Spaces` column to Anomaly detection, Data Frame\r\nAnalytics, and
Trained Models tables for management of
spaces.\r\n\r\n\r\n\r\n85f9be3a-a56e-4ba8-9bcf-06f2b8e01cf7\r\n\r\nIf
user does not have permission to share to other spaces, the
spaces\r\nbutton will be disabled
completely.\r\n\r\n\r\n\r\nhttps://github.com/user-attachments/assets/8c188240-9adf-439b-a6ea-5ad2b4c3ad0a\r\n\r\n**Reviewer's
note:**\r\n\r\n- For kibana-security: A small change in the hook was
updated to fix an\r\nerror with component set state after
unmounting\r\n\r\n\r\n\r\n\r\n### Checklist\r\n\r\n\r\n\r\nCheck the PR
satisfies following conditions. \r\n\r\nReviewers should verify this PR
satisfies this list as well.\r\n\r\n- [ ] Any text added follows [EUI's
writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
uses\r\nsentence case text and includes
[i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md)\r\n-
[
]\r\n[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)\r\nwas
added for features that require explanation or tutorials\r\n- [ ] [Unit
or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common scenarios\r\n- [ ] If a plugin
configuration key changed, check if it needs to be\r\nallowlisted in the
cloud and added to the
[docker\r\nlist](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)\r\n-
[ ] This was checked for breaking HTTP API changes, and any
breaking\r\nchanges have been approved by the breaking-change committee.
The\r\n`release_note:breaking` label should be applied in these
situations.\r\n- [ ] [Flaky
Test\r\nRunner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1)
was\r\nused on any tests changed\r\n- [ ] The PR description includes
the appropriate Release Notes section,\r\nand the correct
`release_note:*` label is applied per
the\r\n[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)\r\n\r\n###
Identify risks\r\n\r\nDoes this PR introduce any risks? For example,
consider risks like hard\r\nto test bugs, performance regression,
potential of data loss.\r\n\r\nDescribe the risk, its severity, and
mitigation for each identified\r\nrisk. Invite stakeholders and evaluate
how to proceed before merging.\r\n\r\n- [ ] [See some
risk\r\nexamples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx)\r\n-
[ ] ...\r\n\r\n---------\r\n\r\nCo-authored-by: Elastic Machine
<elasticmachine@users.noreply.github.com>","sha":"b5de0a7fc4a240c3d73c747b77768bbc401a5a14","branchLabelMapping":{"^v9.0.0$":"main","^v8.18.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:enhancement",":ml","Feature:Anomaly
Detection","Feature:Data Frame
Analytics","v9.0.0","backport:version","v8.18.0"],"title":"[ML] Add
Spaces column to Anomaly Detection, Data Frame Analytics and Trained
Models management
pages","number":206696,"url":"https://github.com/elastic/kibana/pull/206696","mergeCommit":{"message":"[ML]
Add Spaces column to Anomaly Detection, Data Frame Analytics and Trained
Models management pages (#206696)\n\n## Summary\r\n\r\nThis PR adds a
new `Spaces` column to Anomaly detection, Data Frame\r\nAnalytics, and
Trained Models tables for management of
spaces.\r\n\r\n\r\n\r\n85f9be3a-a56e-4ba8-9bcf-06f2b8e01cf7\r\n\r\nIf
user does not have permission to share to other spaces, the
spaces\r\nbutton will be disabled
completely.\r\n\r\n\r\n\r\nhttps://github.com/user-attachments/assets/8c188240-9adf-439b-a6ea-5ad2b4c3ad0a\r\n\r\n**Reviewer's
note:**\r\n\r\n- For kibana-security: A small change in the hook was
updated to fix an\r\nerror with component set state after
unmounting\r\n\r\n\r\n\r\n\r\n### Checklist\r\n\r\n\r\n\r\nCheck the PR
satisfies following conditions. \r\n\r\nReviewers should verify this PR
satisfies this list as well.\r\n\r\n- [ ] Any text added follows [EUI's
writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
uses\r\nsentence case text and includes
[i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md)\r\n-
[
]\r\n[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)\r\nwas
added for features that require explanation or tutorials\r\n- [ ] [Unit
or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common scenarios\r\n- [ ] If a plugin
configuration key changed, check if it needs to be\r\nallowlisted in the
cloud and added to the
[docker\r\nlist](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)\r\n-
[ ] This was checked for breaking HTTP API changes, and any
breaking\r\nchanges have been approved by the breaking-change committee.
The\r\n`release_note:breaking` label should be applied in these
situations.\r\n- [ ] [Flaky
Test\r\nRunner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1)
was\r\nused on any tests changed\r\n- [ ] The PR description includes
the appropriate Release Notes section,\r\nand the correct
`release_note:*` label is applied per
the\r\n[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)\r\n\r\n###
Identify risks\r\n\r\nDoes this PR introduce any risks? For example,
consider risks like hard\r\nto test bugs, performance regression,
potential of data loss.\r\n\r\nDescribe the risk, its severity, and
mitigation for each identified\r\nrisk. Invite stakeholders and evaluate
how to proceed before merging.\r\n\r\n- [ ] [See some
risk\r\nexamples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx)\r\n-
[ ] ...\r\n\r\n---------\r\n\r\nCo-authored-by: Elastic Machine
<elasticmachine@users.noreply.github.com>","sha":"b5de0a7fc4a240c3d73c747b77768bbc401a5a14"}},"sourceBranch":"main","suggestedTargetBranches":["8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/206696","number":206696,"mergeCommit":{"message":"[ML]
Add Spaces column to Anomaly Detection, Data Frame Analytics and Trained
Models management pages (#206696)\n\n## Summary\r\n\r\nThis PR adds a
new `Spaces` column to Anomaly detection, Data Frame\r\nAnalytics, and
Trained Models tables for management of
spaces.\r\n\r\n\r\n\r\n85f9be3a-a56e-4ba8-9bcf-06f2b8e01cf7\r\n\r\nIf
user does not have permission to share to other spaces, the
spaces\r\nbutton will be disabled
completely.\r\n\r\n\r\n\r\nhttps://github.com/user-attachments/assets/8c188240-9adf-439b-a6ea-5ad2b4c3ad0a\r\n\r\n**Reviewer's
note:**\r\n\r\n- For kibana-security: A small change in the hook was
updated to fix an\r\nerror with component set state after
unmounting\r\n\r\n\r\n\r\n\r\n### Checklist\r\n\r\n\r\n\r\nCheck the PR
satisfies following conditions. \r\n\r\nReviewers should verify this PR
satisfies this list as well.\r\n\r\n- [ ] Any text added follows [EUI's
writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
uses\r\nsentence case text and includes
[i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md)\r\n-
[
]\r\n[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)\r\nwas
added for features that require explanation or tutorials\r\n- [ ] [Unit
or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common scenarios\r\n- [ ] If a plugin
configuration key changed, check if it needs to be\r\nallowlisted in the
cloud and added to the
[docker\r\nlist](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)\r\n-
[ ] This was checked for breaking HTTP API changes, and any
breaking\r\nchanges have been approved by the breaking-change committee.
The\r\n`release_note:breaking` label should be applied in these
situations.\r\n- [ ] [Flaky
Test\r\nRunner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1)
was\r\nused on any tests changed\r\n- [ ] The PR description includes
the appropriate Release Notes section,\r\nand the correct
`release_note:*` label is applied per
the\r\n[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)\r\n\r\n###
Identify risks\r\n\r\nDoes this PR introduce any risks? For example,
consider risks like hard\r\nto test bugs, performance regression,
potential of data loss.\r\n\r\nDescribe the risk, its severity, and
mitigation for each identified\r\nrisk. Invite stakeholders and evaluate
how to proceed before merging.\r\n\r\n- [ ] [See some
risk\r\nexamples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx)\r\n-
[ ] ...\r\n\r\n---------\r\n\r\nCo-authored-by: Elastic Machine
<elasticmachine@users.noreply.github.com>","sha":"b5de0a7fc4a240c3d73c747b77768bbc401a5a14"}},{"branch":"8.x","label":"v8.18.0","branchLabelMappingKey":"^v8.18.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->

Co-authored-by: Quynh Nguyen (Quinn) <43350163+qn895@users.noreply.github.com>
This commit is contained in:
Kibana Machine 2025-01-29 12:24:59 +11:00 committed by GitHub
parent 2b0ccf33a4
commit b65acc7c66
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 561 additions and 309 deletions

View file

@ -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';

View file

@ -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 */

View file

@ -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]);

View file

@ -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}</>
);

View file

@ -12,6 +12,7 @@ import { Redirect } from 'react-router-dom';
import { Routes, Route } from '@kbn/shared-ux-router';
import { Subscription } from 'rxjs';
import { EuiPageSection } from '@elastic/eui';
import { map, distinctUntilChanged } from 'rxjs';
import { i18n } from '@kbn/i18n';
import { type AppMountParameters } from '@kbn/core/public';
@ -67,15 +68,19 @@ export const MlPage: FC<{ pageDeps: PageDependencies }> = React.memo(({ pageDeps
const subscriptions = new Subscription();
subscriptions.add(
httpService.getLoadingCount$.subscribe((v) => {
setIsLoading(v !== 0);
})
httpService.getLoadingCount$
.pipe(
map((v) => v !== 0),
distinctUntilChanged()
)
.subscribe((loading) => {
setIsLoading(loading);
})
);
return function cleanup() {
subscriptions.unsubscribe();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
}, [httpService?.getLoadingCount$]);
const routeList = useMemo(
() =>

View file

@ -18,10 +18,11 @@ 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;
disabled?: boolean;
}
const ALL_SPACES_ID = '*';
@ -33,12 +34,14 @@ 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,
disabled = false,
}) => {
const {
savedObjects: { updateJobsSpaces, updateModelsSpaces },
@ -107,6 +110,7 @@ export const MLSavedObjectsSpacesList: FC<Props> = ({
return (
<>
<EuiButtonEmpty
disabled={disabled}
onClick={() => setShowFlyout(true)}
style={{ height: 'auto' }}
data-test-subj="mlJobListRowManageSpacesButton"

View file

@ -0,0 +1,8 @@
/*
* 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.
*/
export { SpaceManagementContextWrapper } from './spaces_management_context_wrapper';

View file

@ -0,0 +1,17 @@
/*
* 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, { type FC, type PropsWithChildren } from 'react';
import { useSpacesContextWrapper } from '../../hooks/use_spaces';
export const SpaceManagementContextWrapper: FC<PropsWithChildren<{ feature?: string }>> = ({
children,
feature,
}) => {
const ContextWrapper = useSpacesContextWrapper();
return <ContextWrapper feature={feature}>{children}</ContextWrapper>;
};

View file

@ -56,7 +56,7 @@ interface StartPlugins {
savedSearch: SavedSearchPublicPluginStart;
security?: SecurityPluginStart;
share: SharePluginStart;
spacesApi?: SpacesPluginStart;
spaces?: SpacesPluginStart;
triggersActionsUi?: TriggersAndActionsUIPublicPluginStart;
uiActions: UiActionsStart;
unifiedSearch: UnifiedSearchPublicPluginStart;

View file

@ -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 { SpaceManagementContextWrapper } from '../../../../../components/space_management_context_wrapper';
const filters: EuiSearchBarProps['filters'] = [
{
@ -259,43 +260,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}
/>
<SpaceManagementContextWrapper>
<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>
</SpaceManagementContextWrapper>
);
};

View file

@ -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

View file

@ -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;
@ -164,6 +167,9 @@ export const useColumns = (
isMlEnabledInSpace: boolean = true,
refresh: () => void = () => {}
) => {
const {
services: { spaces, application },
} = useMlKibana();
const { actions, modals } = useActions();
function toggleDetails(item: DataFrameAnalyticsListRow) {
const index = expandedRowItemIds.indexOf(item.config.id);
@ -177,6 +183,12 @@ export const useColumns = (
// spread to a new array otherwise the component wouldn't re-render
setExpandedRowItemIds([...expandedRowItemIds]);
}
const canManageSpacesAndSavedObjects = useCanManageSpacesAndSavedObjects();
const shouldDisableSpacesColumn =
!canManageSpacesAndSavedObjects ||
!application.capabilities.savedObjectsManagement?.shareIntoSpace;
// update possible column types to something like (FieldDataColumn | ComputedColumn | ActionsColumn)[] when they have been added to EUI
const columns: any[] = [
{
@ -283,6 +295,33 @@ 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%',
disabled: shouldDisableSpacesColumn,
render: (item: DataFrameAnalyticsListRow) => {
return (
<MLSavedObjectsSpacesList
disabled={shouldDisableSpacesColumn}
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',

View file

@ -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,
@ -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
@ -117,6 +120,7 @@ export const useGetAnalytics = (
blockRefresh: boolean
): GetAnalytics => {
const mlApi = useMlApi();
const canManageSpacesAndSavedObjects = useCanManageSpacesAndSavedObjects();
let concurrentLoads = 0;
@ -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;
@ -164,6 +175,7 @@ export const useGetAnalytics = (
mode: DATA_FRAME_MODE.BATCH,
state: stats.state,
stats,
spaces: savedObjectsSpaces[config.id],
});
return reducedtableRows;
},

View file

@ -0,0 +1,30 @@
/*
* 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 },
} = useMlKibana();
const canManageSpacesAndSavedObjects = useMemo(() => spaces !== undefined, [spaces]);
return canManageSpacesAndSavedObjects;
};
export const useSpacesContextWrapper = () => {
const {
services: { spaces },
} = useMlKibana();
return useMemo(
() => (spaces ? spaces.ui.components.getSpacesContextProvider : getEmptyFunctionComponent),
[spaces]
);
};

View file

@ -32,6 +32,8 @@ import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { AnomalyDetectionJobIdLink } from './job_id_link';
import { isManagedJob } from '../../../jobs_utils';
import { MLSavedObjectsSpacesList } from '../../../../components/ml_saved_objects_spaces_list';
import { ANOMALY_DETECTOR_SAVED_OBJECT_TYPE } from '../../../../../../common/types/saved_objects';
const PAGE_SIZE_OPTIONS = [10, 25, 50];
@ -327,6 +329,35 @@ export class JobsListUI extends Component {
render: (item) => <ResultLinks jobs={[item]} />,
width: '64px',
},
...(this.props.kibana.services.spaces
? [
{
name: i18n.translate('xpack.ml.jobsList.jobActionsColumn.spaces', {
defaultMessage: 'Spaces',
}),
'data-test-subj': 'mlTableColumnSpaces',
truncateText: true,
align: 'right',
width: '10%',
render: (item) => {
return (
<MLSavedObjectsSpacesList
disabled={
!this.props.kibana.services.application?.capabilities?.savedObjectsManagement
?.shareIntoSpace
}
spacesApi={this.props.kibana.services.spaces}
spaceIds={item.spaces}
id={item.id}
mlSavedObjectType={ANOMALY_DETECTOR_SAVED_OBJECT_TYPE}
refresh={this.props.refreshJobs}
/>
);
},
},
]
: []),
{
name: i18n.translate('xpack.ml.jobsList.actionsLabel', {
defaultMessage: 'Actions',
@ -375,33 +406,35 @@ export class JobsListUI extends Component {
const selectedJobsClass = this.props.selectedJobsCount ? 'jobs-selected' : '';
return (
<EuiBasicTable
data-test-subj={loading ? 'mlJobListTable loading' : 'mlJobListTable loaded'}
loading={loading === true}
noItemsMessage={
loading
? i18n.translate('xpack.ml.jobsList.loadingJobsLabel', {
defaultMessage: 'Loading jobs…',
})
: i18n.translate('xpack.ml.jobsList.noJobsFoundLabel', {
defaultMessage: 'No jobs found',
})
}
itemId="id"
className={`jobs-list-table ${selectedJobsClass}`}
items={pageOfItems}
columns={columns}
pagination={pagination}
onChange={this.onTableChange}
selection={selectionControls}
itemIdToExpandedRowMap={this.state.itemIdToExpandedRowMap}
sorting={sorting}
rowProps={(item) => ({
'data-test-subj': `mlJobListRow row-${item.id}`,
})}
css={{ '.euiTableRow-isExpandedRow .euiTableCellContent': { animation: 'none' } }}
rowHeader="id"
/>
<>
<EuiBasicTable
data-test-subj={loading ? 'mlJobListTable loading' : 'mlJobListTable loaded'}
loading={loading === true}
noItemsMessage={
loading
? i18n.translate('xpack.ml.jobsList.loadingJobsLabel', {
defaultMessage: 'Loading jobs…',
})
: i18n.translate('xpack.ml.jobsList.noJobsFoundLabel', {
defaultMessage: 'No jobs found',
})
}
itemId="id"
className={`jobs-list-table ${selectedJobsClass}`}
items={pageOfItems}
columns={columns}
pagination={pagination}
onChange={this.onTableChange}
selection={selectionControls}
itemIdToExpandedRowMap={this.state.itemIdToExpandedRowMap}
sorting={sorting}
rowProps={(item) => ({
'data-test-subj': `mlJobListRow row-${item.id}`,
})}
css={{ '.euiTableRow-isExpandedRow .euiTableCellContent': { animation: 'none' } }}
rowHeader="id"
/>
</>
);
}
}

View file

@ -38,6 +38,8 @@ import { CloseJobsConfirmModal } from '../confirm_modals/close_jobs_confirm_moda
import { AnomalyDetectionEmptyState } from '../anomaly_detection_empty_state';
import { removeNodeInfo } from '../../../../../../common/util/job_utils';
import { jobCloningService } from '../../../../services/job_cloning_service';
import { ANOMALY_DETECTOR_SAVED_OBJECT_TYPE } from '../../../../../../common/types/saved_objects';
import { SpaceManagementContextWrapper } from '../../../../components/space_management_context_wrapper';
let blockingJobsRefreshTimeout = null;
@ -322,7 +324,10 @@ export class JobsListViewUI extends Component {
const expandedJobsIds = Object.keys(this.state.itemIdToExpandedRowMap);
try {
let jobsAwaitingNodeCount = 0;
const jobs = await mlApi.jobs.jobsSummary(expandedJobsIds);
const [jobs, jobsSpaces] = await Promise.all([
mlApi.jobs.jobsSummary(expandedJobsIds),
mlApi.savedObjects.jobsSpaces(),
]);
const fullJobsList = {};
const jobsSummaryList = jobs.map((job) => {
if (job.fullJob !== undefined) {
@ -332,6 +337,13 @@ export class JobsListViewUI extends Component {
fullJobsList[job.id] = job.fullJob;
delete job.fullJob;
}
if (
jobsSpaces &&
jobsSpaces[ANOMALY_DETECTOR_SAVED_OBJECT_TYPE] &&
jobsSpaces[ANOMALY_DETECTOR_SAVED_OBJECT_TYPE][job.id]
) {
job.spaces = jobsSpaces[ANOMALY_DETECTOR_SAVED_OBJECT_TYPE][job.id];
}
job.latestTimestampSortValue = job.latestTimestampMs || 0;
if (job.awaitingNodeAssignment === true) {
@ -434,75 +446,77 @@ export class JobsListViewUI extends Component {
<UpgradeWarning />
<>
{noJobsFound ? <AnomalyDetectionEmptyState /> : null}
<SpaceManagementContextWrapper>
{noJobsFound ? <AnomalyDetectionEmptyState /> : null}
{jobIds.length > 0 ? (
<>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<JobStatsBar
jobsSummaryList={jobsSummaryList}
showNodeInfo={this.props.showNodeInfo}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<NewJobButton />
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="s" />
<div>
<EuiFlexGroup
css={{
alignItems: 'center',
minHeight: '60px',
}}
gutterSize="none"
>
{jobIds.length > 0 ? (
<>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<MultiJobActions
selectedJobs={this.state.selectedJobs}
allJobIds={jobIds}
showCloseJobsConfirmModal={this.showCloseJobsConfirmModal}
showStartDatafeedModal={this.showStartDatafeedModal}
showDeleteJobModal={this.showDeleteJobModal}
showResetJobModal={this.showResetJobModal}
showCreateAlertFlyout={this.showCreateAlertFlyout}
showStopDatafeedsConfirmModal={this.showStopDatafeedsConfirmModal}
refreshJobs={() => this.refreshJobSummaryList()}
<JobStatsBar
jobsSummaryList={jobsSummaryList}
showNodeInfo={this.props.showNodeInfo}
/>
</EuiFlexItem>
<EuiFlexItem>
<JobFilterBar
setFilters={this.setFilters}
queryText={this.props.jobsViewState.queryText}
/>
<EuiFlexItem grow={false}>
<NewJobButton />
</EuiFlexItem>
</EuiFlexGroup>
<JobsList
jobsSummaryList={this.state.filteredJobsSummaryList}
fullJobsList={this.state.fullJobsList}
itemIdToExpandedRowMap={this.state.itemIdToExpandedRowMap}
toggleRow={this.toggleRow}
selectJobChange={this.selectJobChange}
showEditJobFlyout={this.showEditJobFlyout}
showDatafeedChartFlyout={this.showDatafeedChartFlyout}
showDeleteJobModal={this.showDeleteJobModal}
showResetJobModal={this.showResetJobModal}
showCloseJobsConfirmModal={this.showCloseJobsConfirmModal}
showStartDatafeedModal={this.showStartDatafeedModal}
showStopDatafeedsConfirmModal={this.showStopDatafeedsConfirmModal}
refreshJobs={() => this.refreshJobSummaryList()}
jobsViewState={this.props.jobsViewState}
onJobsViewStateUpdate={this.props.onJobsViewStateUpdate}
selectedJobsCount={this.state.selectedJobs.length}
showCreateAlertFlyout={this.showCreateAlertFlyout}
loading={loading}
/>
</div>
</>
) : null}
<EuiSpacer size="s" />
<div>
<EuiFlexGroup
css={{
alignItems: 'center',
minHeight: '60px',
}}
gutterSize="none"
>
<EuiFlexItem grow={false}>
<MultiJobActions
selectedJobs={this.state.selectedJobs}
allJobIds={jobIds}
showCloseJobsConfirmModal={this.showCloseJobsConfirmModal}
showStartDatafeedModal={this.showStartDatafeedModal}
showDeleteJobModal={this.showDeleteJobModal}
showResetJobModal={this.showResetJobModal}
showCreateAlertFlyout={this.showCreateAlertFlyout}
showStopDatafeedsConfirmModal={this.showStopDatafeedsConfirmModal}
refreshJobs={() => this.refreshJobSummaryList()}
/>
</EuiFlexItem>
<EuiFlexItem>
<JobFilterBar
setFilters={this.setFilters}
queryText={this.props.jobsViewState.queryText}
/>
</EuiFlexItem>
</EuiFlexGroup>
<JobsList
jobsSummaryList={this.state.filteredJobsSummaryList}
fullJobsList={this.state.fullJobsList}
itemIdToExpandedRowMap={this.state.itemIdToExpandedRowMap}
toggleRow={this.toggleRow}
selectJobChange={this.selectJobChange}
showEditJobFlyout={this.showEditJobFlyout}
showDatafeedChartFlyout={this.showDatafeedChartFlyout}
showDeleteJobModal={this.showDeleteJobModal}
showResetJobModal={this.showResetJobModal}
showCloseJobsConfirmModal={this.showCloseJobsConfirmModal}
showStartDatafeedModal={this.showStartDatafeedModal}
showStopDatafeedsConfirmModal={this.showStopDatafeedsConfirmModal}
refreshJobs={() => this.refreshJobSummaryList()}
jobsViewState={this.props.jobsViewState}
onJobsViewStateUpdate={this.props.onJobsViewStateUpdate}
selectedJobsCount={this.state.selectedJobs.length}
showCreateAlertFlyout={this.showCreateAlertFlyout}
loading={loading}
/>
</div>
</>
) : null}
</SpaceManagementContextWrapper>
<EditJobFlyout
setShowFunction={this.setShowEditJobFlyoutFunction}

View file

@ -47,6 +47,7 @@ export const JobsPage: FC<JobsPageProps> = ({ isMlEnabledInSpace, lastRefresh })
const { showNodeInfo } = useEnabledFeatures();
const helpLink = docLinks.links.ml.anomalyDetection;
return (
<>
<MlPageHeader>

View file

@ -6,7 +6,7 @@
*/
import type { FC } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import { Router } from '@kbn/shared-ux-router';
import { i18n } from '@kbn/i18n';
import { FormattedMessage, I18nProvider } from '@kbn/i18n-react';
@ -27,8 +27,9 @@ import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
import { RedirectAppLinks } from '@kbn/shared-ux-link-redirect-app';
import type { SharePluginStart } from '@kbn/share-plugin/public';
import type { SpacesContextProps, SpacesPluginStart } from '@kbn/spaces-plugin/public';
import type { SpacesPluginStart } from '@kbn/spaces-plugin/public';
import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
import { SpaceManagementContextWrapper } from '../../../../components/space_management_context_wrapper';
import { UpgradeWarning } from '../../../../components/upgrade/upgrade_warning';
import { getMlGlobalServices } from '../../../../util/get_services';
import { EnabledFeaturesContextProvider } from '../../../../contexts/ml';
@ -45,13 +46,11 @@ import type { MlSavedObjectType } from '../../../../../../common/types/saved_obj
import { SpaceManagement } from './space_management';
import { DocsLink } from './docs_link';
const getEmptyFunctionComponent: React.FC<SpacesContextProps> = ({ children }) => <>{children}</>;
interface Props {
coreStart: CoreStart;
share: SharePluginStart;
history: ManagementAppMountParams['history'];
spacesApi?: SpacesPluginStart;
spaces?: SpacesPluginStart;
data: DataPublicPluginStart;
usageCollection?: UsageCollectionSetup;
fieldFormats: FieldFormatsStart;
@ -63,7 +62,7 @@ export const JobsListPage: FC<Props> = ({
coreStart,
share,
history,
spacesApi,
spaces,
data,
usageCollection,
fieldFormats,
@ -104,12 +103,6 @@ export const JobsListPage: FC<Props> = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// eslint-disable-next-line react-hooks/exhaustive-deps
const ContextWrapper = useCallback(
spacesApi ? spacesApi.ui.components.getSpacesContextProvider : getEmptyFunctionComponent,
[spacesApi]
);
if (initialized === false) {
return null;
}
@ -132,7 +125,7 @@ export const JobsListPage: FC<Props> = ({
data,
usageCollection,
fieldFormats,
spacesApi,
spaces,
mlServices,
}}
>
@ -178,11 +171,11 @@ export const JobsListPage: FC<Props> = ({
data,
usageCollection,
fieldFormats,
spacesApi,
spaces,
mlServices,
}}
>
<ContextWrapper feature={PLUGIN_ID}>
<SpaceManagementContextWrapper feature={PLUGIN_ID}>
<EnabledFeaturesContextProvider isServerless={isServerless} mlFeatures={mlFeatures}>
<Router history={history}>
<EuiPageTemplate.Header
@ -238,14 +231,14 @@ export const JobsListPage: FC<Props> = ({
</EuiFlexItem>
</EuiFlexGroup>
<SpaceManagement
spacesApi={spacesApi}
spacesApi={spaces}
onTabChange={setCurrentTabId}
onReload={setRefreshJobs}
/>
</EuiPageTemplate.Section>
</Router>
</EnabledFeaturesContextProvider>
</ContextWrapper>
</SpaceManagementContextWrapper>
</KibanaContextProvider>
</RedirectAppLinks>
</KibanaRenderContextProvider>

View file

@ -31,6 +31,7 @@ import { useManagementApiService } from '../../../../../services/ml_api_service/
import { getColumns } from './columns';
import { MLSavedObjectsSpacesList } from '../../../../../components/ml_saved_objects_spaces_list';
import { getFilters } from './filters';
import { useMlKibana } from '../../../../../contexts/kibana/kibana_context';
interface Props {
spacesApi?: SpacesPluginStart;
@ -39,6 +40,9 @@ interface Props {
}
export const SpaceManagement: FC<Props> = ({ spacesApi, onTabChange, onReload }) => {
const {
services: { application },
} = useMlKibana();
const { getList } = useManagementApiService();
const [currentTabId, setCurrentTabId] = useState<MlSavedObjectType | null>(null);
@ -123,6 +127,9 @@ export const SpaceManagement: FC<Props> = ({ spacesApi, onTabChange, onReload })
[currentTabId]
);
const canShareIntoSpace = useMemo(() => {
return !application?.capabilities?.savedObjectsManagement?.shareIntoSpace;
}, [application]);
const createColumns = useCallback(() => {
if (currentTabId === null) {
return [];
@ -143,10 +150,11 @@ export const SpaceManagement: FC<Props> = ({ spacesApi, onTabChange, onReload })
render: (item: ManagementItems) => {
return (
<MLSavedObjectsSpacesList
disabled={canShareIntoSpace}
spacesApi={spacesApi}
spaceIds={item.spaces ?? []}
id={item.id}
spaceIds={item.spaces}
mlSavedObjectType={currentTabId}
id={item.id}
refresh={refresh.bind(null, currentTabId)}
/>
);
@ -155,7 +163,7 @@ export const SpaceManagement: FC<Props> = ({ spacesApi, onTabChange, onReload })
]
: []),
] as Array<EuiBasicTableColumn<ManagementItems>>;
}, [currentTabId, spacesApi, refresh]);
}, [currentTabId, spacesApi, refresh, canShareIntoSpace]);
const getTable = useCallback(() => {
return (

View file

@ -28,7 +28,7 @@ const renderApp = (
fieldFormats: FieldFormatsStart,
isServerless: boolean,
mlFeatures: MlFeatures,
spacesApi?: SpacesPluginStart,
spaces?: SpacesPluginStart,
usageCollection?: UsageCollectionSetup
) => {
ReactDOM.render(
@ -37,7 +37,7 @@ const renderApp = (
history,
share,
data,
spacesApi,
spaces,
usageCollection,
fieldFormats,
isServerless,

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import type { SearchFilterConfig } from '@elastic/eui';
import type { HorizontalAlignment, SearchFilterConfig } from '@elastic/eui';
import {
EuiBadge,
EuiButton,
@ -70,6 +70,11 @@ import { getModelStateColor } from './get_model_state';
import { useModelActions } from './model_actions';
import { TestDfaModelsFlyout } from './test_dfa_models_flyout';
import { TestModelAndPipelineCreationFlyout } from './test_models';
import { MLSavedObjectsSpacesList } from '../components/ml_saved_objects_spaces_list';
import { useCanManageSpacesAndSavedObjects } from '../hooks/use_spaces';
import { useSavedObjectsApiService } from '../services/ml_api_service/saved_objects';
import { TRAINED_MODEL_SAVED_OBJECT_TYPE } from '../../../common/types/saved_objects';
import { SpaceManagementContextWrapper } from '../components/space_management_context_wrapper';
interface PageUrlState {
pageKey: typeof ML_PAGES.TRAINED_MODELS_MANAGE;
@ -111,11 +116,13 @@ export const ModelsList: FC<Props> = ({
const {
services: {
spaces,
application: { capabilities },
docLinks,
},
} = useMlKibana();
const savedObjectsApiService = useSavedObjectsApiService();
const nlpElserDocUrl = docLinks.links.ml.nlpElser;
const { isNLPEnabled } = useEnabledFeatures();
@ -173,7 +180,20 @@ export const ModelsList: FC<Props> = ({
const fetchModelsData = useCallback(async () => {
setIsLoading(true);
try {
const resultItems = await trainedModelsApiService.getTrainedModelsList();
const [trainedModelsResult, trainedModelsSpacesResult] = await Promise.allSettled([
trainedModelsApiService.getTrainedModelsList(),
canManageSpacesAndSavedObjects
? savedObjectsApiService.trainedModelsSpaces()
: ({} as Record<string, Record<string, string[]>>),
]);
const resultItems =
trainedModelsResult.status === 'fulfilled' ? trainedModelsResult.value : [];
const trainedModelsSpaces =
trainedModelsSpacesResult.status === 'fulfilled' ? trainedModelsSpacesResult.value : {};
const trainedModelsSavedObjects: Record<string, string[]> =
trainedModelsSpaces?.trainedModels ?? {};
setItems((prevItems) => {
// Need to merge existing items with new items
@ -182,6 +202,7 @@ export const ModelsList: FC<Props> = ({
const prevItem = prevItems.find((i) => i.model_id === item.model_id);
return {
...item,
spaces: trainedModelsSavedObjects[item.model_id],
...(isBaseNLPModelItem(prevItem) && prevItem?.state === MODEL_STATE.DOWNLOADING
? {
state: prevItem.state,
@ -381,6 +402,9 @@ export const ModelsList: FC<Props> = ({
modelAndDeploymentIds,
onModelDownloadRequest,
});
const canManageSpacesAndSavedObjects = useCanManageSpacesAndSavedObjects();
const shouldDisableSpacesColumn =
!canManageSpacesAndSavedObjects || !capabilities.savedObjectsManagement?.shareIntoSpace;
const toggleDetails = async (item: TrainedModelUIItem) => {
const itemIdToExpandedRowMapValues = { ...itemIdToExpandedRowMap };
@ -555,6 +579,31 @@ export const ModelsList: FC<Props> = ({
},
'data-test-subj': 'mlModelsTableColumnDeploymentState',
},
...(canManageSpacesAndSavedObjects && spaces
? [
{
name: i18n.translate('xpack.ml.jobsList.jobActionsColumn.spaces', {
defaultMessage: 'Spaces',
}),
'data-test-subj': 'mlTableColumnSpaces',
truncateText: true,
align: 'right' as HorizontalAlignment,
width: '10%',
render: (item: TrainedModelUIItem) => {
return (
<MLSavedObjectsSpacesList
disabled={shouldDisableSpacesColumn}
spacesApi={spaces}
spaceIds={item.spaces}
id={item.model_id}
mlSavedObjectType={TRAINED_MODEL_SAVED_OBJECT_TYPE}
refresh={fetchModelsData}
/>
);
},
},
]
: []),
{
name: i18n.translate('xpack.ml.trainedModels.modelsList.actionsHeader', {
defaultMessage: 'Actions',
@ -688,155 +737,160 @@ export const ModelsList: FC<Props> = ({
return (
<>
<SavedObjectsWarning onCloseFlyout={fetchModelsData} forceRefresh={isLoading} />
<EuiFlexGroup justifyContent="spaceBetween">
{modelsStats ? (
<EuiFlexItem>
<EuiFlexGroup alignItems="center">
<EuiFlexItem grow={false}>
<StatsBar stats={modelsStats} dataTestSub={'mlInferenceModelsStatsBar'} />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiSwitch
label={
<FormattedMessage
id="xpack.ml.trainedModels.modelsList.showAllLabel"
defaultMessage="Show all"
/>
}
checked={!!pageState.showAll}
onChange={(e) => updatePageState({ showAll: e.target.checked })}
data-test-subj="mlModelsShowAllSwitch"
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
) : null}
<EuiFlexItem grow={false}>
<EuiButton
fill
iconType={'plusInCircle'}
color={'primary'}
onClick={setIsAddModelFlyoutVisible.bind(null, true)}
data-test-subj="mlModelsAddTrainedModelButton"
>
<FormattedMessage
id="xpack.ml.trainedModels.modelsList.addModelButtonLabel"
defaultMessage="Add trained model"
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="m" />
<div data-test-subj="mlModelsTableContainer">
<EuiInMemoryTable<TrainedModelUIItem>
tableLayout={'auto'}
responsiveBreakpoint={'xl'}
allowNeutralSort={false}
columns={columns}
itemIdToExpandedRowMap={itemIdToExpandedRowMap}
items={tableItems}
itemId={ModelsTableToConfigMapping.id}
loading={isLoading}
search={search}
selection={selection}
rowProps={(item) => ({
'data-test-subj': `mlModelsTableRow row-${item.model_id}`,
})}
pagination={pagination}
onTableChange={onTableChange}
sorting={sorting}
data-test-subj={isLoading ? 'mlModelsTable loading' : 'mlModelsTable loaded'}
childrenBetween={
isElserCalloutVisible ? (
<>
<EuiCallOut
size="s"
title={
<FormattedMessage
id="xpack.ml.trainedModels.modelsList.newElserModelTitle"
defaultMessage="New ELSER model now available"
/>
}
onDismiss={setIsElserCalloutDismissed.bind(null, true)}
>
<FormattedMessage
id="xpack.ml.trainedModels.modelsList.newElserModelDescription"
defaultMessage="A new version of ELSER that shows faster performance and improved relevance is now available. {docLink} for information on how to start using it."
values={{
docLink: (
<EuiLink href={nlpElserDocUrl} external target={'_blank'}>
<FormattedMessage
id="xpack.ml.trainedModels.modelsList.startDeployment.viewElserDocLink"
defaultMessage="View documentation"
/>
</EuiLink>
),
}}
<SpaceManagementContextWrapper>
<SavedObjectsWarning onCloseFlyout={fetchModelsData} forceRefresh={isLoading} />
<EuiFlexGroup justifyContent="spaceBetween">
{modelsStats ? (
<EuiFlexItem>
<EuiFlexGroup alignItems="center">
<EuiFlexItem grow={false}>
<StatsBar stats={modelsStats} dataTestSub={'mlInferenceModelsStatsBar'} />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiSwitch
label={
<FormattedMessage
id="xpack.ml.trainedModels.modelsList.showAllLabel"
defaultMessage="Show all"
/>
}
checked={!!pageState.showAll}
onChange={(e) => updatePageState({ showAll: e.target.checked })}
data-test-subj="mlModelsShowAllSwitch"
/>
</EuiCallOut>
<EuiSpacer size="m" />
</>
) : null
}
/>
</div>
{modelsToDelete.length > 0 && (
<DeleteModelsModal
onClose={(refreshList) => {
modelsToDelete.forEach((model) => {
if (isBaseNLPModelItem(model) && model.state === MODEL_STATE.DOWNLOADING) {
abortedDownload.current.add(model.model_id);
}
});
setItemIdToExpandedRowMap((prev) => {
const newMap = { ...prev };
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
) : null}
<EuiFlexItem grow={false}>
<EuiButton
fill
iconType={'plusInCircle'}
color={'primary'}
onClick={setIsAddModelFlyoutVisible.bind(null, true)}
data-test-subj="mlModelsAddTrainedModelButton"
>
<FormattedMessage
id="xpack.ml.trainedModels.modelsList.addModelButtonLabel"
defaultMessage="Add trained model"
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="m" />
<div data-test-subj="mlModelsTableContainer">
<EuiInMemoryTable<TrainedModelUIItem>
tableLayout={'auto'}
responsiveBreakpoint={'xl'}
allowNeutralSort={false}
columns={columns}
itemIdToExpandedRowMap={itemIdToExpandedRowMap}
items={tableItems}
itemId={ModelsTableToConfigMapping.id}
loading={isLoading}
search={search}
selection={selection}
rowProps={(item) => ({
'data-test-subj': `mlModelsTableRow row-${item.model_id}`,
})}
pagination={pagination}
onTableChange={onTableChange}
sorting={sorting}
data-test-subj={isLoading ? 'mlModelsTable loading' : 'mlModelsTable loaded'}
childrenBetween={
isElserCalloutVisible ? (
<>
<EuiCallOut
size="s"
title={
<FormattedMessage
id="xpack.ml.trainedModels.modelsList.newElserModelTitle"
defaultMessage="New ELSER model now available"
/>
}
onDismiss={setIsElserCalloutDismissed.bind(null, true)}
>
<FormattedMessage
id="xpack.ml.trainedModels.modelsList.newElserModelDescription"
defaultMessage="A new version of ELSER that shows faster performance and improved relevance is now available. {docLink} for information on how to start using it."
values={{
docLink: (
<EuiLink href={nlpElserDocUrl} external target={'_blank'}>
<FormattedMessage
id="xpack.ml.trainedModels.modelsList.startDeployment.viewElserDocLink"
defaultMessage="View documentation"
/>
</EuiLink>
),
}}
/>
</EuiCallOut>
<EuiSpacer size="m" />
</>
) : null
}
/>
</div>
{modelsToDelete.length > 0 && (
<DeleteModelsModal
onClose={(refreshList) => {
modelsToDelete.forEach((model) => {
delete newMap[model.model_id];
if (isBaseNLPModelItem(model) && model.state === MODEL_STATE.DOWNLOADING) {
abortedDownload.current.add(model.model_id);
}
});
return newMap;
});
setModelsToDelete([]);
setItemIdToExpandedRowMap((prev) => {
const newMap = { ...prev };
modelsToDelete.forEach((model) => {
delete newMap[model.model_id];
});
return newMap;
});
if (refreshList) {
fetchModelsData();
}
}}
models={modelsToDelete}
/>
)}
{modelToTest === null ? null : (
<TestModelAndPipelineCreationFlyout
model={modelToTest}
onClose={(refreshList?: boolean) => {
setModelToTest(null);
if (refreshList) {
fetchModelsData();
}
}}
/>
)}
{dfaModelToTest === null ? null : (
<TestDfaModelsFlyout model={dfaModelToTest} onClose={setDfaModelToTest.bind(null, null)} />
)}
{modelToDeploy !== undefined ? (
<AddInferencePipelineFlyout
onClose={setModelToDeploy.bind(null, undefined)}
model={modelToDeploy}
/>
) : null}
{isAddModelFlyoutVisible ? (
<AddModelFlyout
modelDownloads={items.filter(isModelDownloadItem)}
onClose={setIsAddModelFlyoutVisible.bind(null, false)}
onSubmit={(modelId) => {
onModelDownloadRequest(modelId);
setIsAddModelFlyoutVisible(false);
}}
/>
) : null}
setModelsToDelete([]);
if (refreshList) {
fetchModelsData();
}
}}
models={modelsToDelete}
/>
)}
{modelToTest === null ? null : (
<TestModelAndPipelineCreationFlyout
model={modelToTest}
onClose={(refreshList?: boolean) => {
setModelToTest(null);
if (refreshList) {
fetchModelsData();
}
}}
/>
)}
{dfaModelToTest === null ? null : (
<TestDfaModelsFlyout
model={dfaModelToTest}
onClose={setDfaModelToTest.bind(null, null)}
/>
)}
{modelToDeploy !== undefined ? (
<AddInferencePipelineFlyout
onClose={setModelToDeploy.bind(null, undefined)}
model={modelToDeploy}
/>
) : null}
{isAddModelFlyoutVisible ? (
<AddModelFlyout
modelDownloads={items.filter(isModelDownloadItem)}
onClose={setIsAddModelFlyoutVisible.bind(null, false)}
onSubmit={(modelId) => {
onModelDownloadRequest(modelId);
setIsAddModelFlyoutVisible(false);
}}
/>
) : null}
</SpaceManagementContextWrapper>
</>
);
};

View file

@ -52,7 +52,6 @@ const PageWrapper: FC = () => {
...basicResolvers(),
initSavedObjects,
});
return (
<PageLoader context={context}>
<MlPageHeader>
@ -65,6 +64,7 @@ const PageWrapper: FC = () => {
</EuiFlexItem>
</EuiFlexGroup>
</MlPageHeader>
<ModelsList />
</PageLoader>
);

View file

@ -212,6 +212,7 @@ export class MlPlugin implements Plugin<MlPluginSetup, MlPluginStart> {
uiActions: pluginsStart.uiActions,
unifiedSearch: pluginsStart.unifiedSearch,
usageCollection: pluginsSetup.usageCollection,
spaces: pluginsStart.spaces,
},
params,
this.isServerless,

View file

@ -60,11 +60,19 @@ export const SpacesContextWrapperInternal = (
);
useEffect(() => {
let unmounted = false;
getStartServices().then(([coreStart]) => {
if (unmounted) {
return;
}
const { application, docLinks, notifications } = coreStart;
const services = { application, docLinks, notifications };
setContext(createSpacesReactContext(services, spacesManager, spacesDataPromise));
});
return () => {
unmounted = true;
};
}, [getStartServices, spacesDataPromise, spacesManager]);
if (!context) {

View file

@ -6,6 +6,7 @@
*/
import { FtrProviderContext } from '../../../ftr_provider_context';
import { USER } from '../../../services/ml/security_common';
export default function ({ getService }: FtrProviderContext) {
const browser = getService('browser');
@ -115,7 +116,7 @@ export default function ({ getService }: FtrProviderContext) {
await ml.testResources.createDataViewIfNeeded('ft_ihp_outlier', '@timestamp');
await ml.testResources.setKibanaTimeZoneToUTC();
await ml.securityUI.loginAsMlPowerUser();
await ml.securityUI.loginAs(USER.ML_POWERUSER_ALL_SPACES);
for (const spaceId of Object.values(spaceIds)) {
if (spaceId !== 'default') {