mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
Make Tags shareable across Spaces
This commit is contained in:
parent
707ece79fc
commit
d71ced9edf
12 changed files with 155 additions and 18 deletions
|
@ -8,6 +8,7 @@
|
|||
|
||||
export interface Tag {
|
||||
id: string;
|
||||
namespaces?: string[];
|
||||
name: string;
|
||||
description: string;
|
||||
color: string;
|
||||
|
|
|
@ -18,6 +18,7 @@ export interface TagsCapabilities {
|
|||
delete: boolean;
|
||||
assign: boolean;
|
||||
viewConnections: boolean;
|
||||
shareIntoSpace: boolean;
|
||||
}
|
||||
|
||||
export const getTagsCapabilities = (capabilities: Capabilities): TagsCapabilities => {
|
||||
|
@ -29,5 +30,6 @@ export const getTagsCapabilities = (capabilities: Capabilities): TagsCapabilitie
|
|||
delete: (rawTagCapabilities?.delete as boolean) ?? false,
|
||||
assign: (rawTagCapabilities?.assign as boolean) ?? false,
|
||||
viewConnections: (capabilities.savedObjectsManagement?.read as boolean) ?? false,
|
||||
shareIntoSpace: (capabilities.savedObjectsManagement?.shareIntoSpace as boolean) ?? false,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -17,7 +17,8 @@
|
|||
],
|
||||
"optionalPlugins": [
|
||||
"usageCollection",
|
||||
"security"
|
||||
"security",
|
||||
"spaces"
|
||||
],
|
||||
"requiredBundles": [
|
||||
"kibanaReact"
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* 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, { FC, useState } from 'react';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { SpacesPluginStart, ShareToSpaceFlyoutProps } from '@kbn/spaces-plugin/public';
|
||||
import { tagSavedObjectTypeName } from '../../../common/constants';
|
||||
|
||||
interface Props {
|
||||
spacesApi: SpacesPluginStart;
|
||||
canShareIntoSpace: boolean;
|
||||
spaceIds: string[];
|
||||
id: string;
|
||||
title: string;
|
||||
refresh(): void;
|
||||
}
|
||||
|
||||
const noun = i18n.translate('indexPatternManagement.indexPatternTable.savedObjectName', {
|
||||
defaultMessage: 'data view',
|
||||
});
|
||||
|
||||
export const SpacesList: FC<Props> = ({
|
||||
spacesApi,
|
||||
canShareIntoSpace,
|
||||
spaceIds,
|
||||
id,
|
||||
title,
|
||||
refresh,
|
||||
}) => {
|
||||
const [showFlyout, setShowFlyout] = useState(false);
|
||||
|
||||
function onClose() {
|
||||
setShowFlyout(false);
|
||||
}
|
||||
|
||||
const LazySpaceList = spacesApi.ui.components.getSpaceList;
|
||||
const LazyShareToSpaceFlyout = spacesApi.ui.components.getShareToSpaceFlyout;
|
||||
|
||||
const shareToSpaceFlyoutProps: ShareToSpaceFlyoutProps = {
|
||||
savedObjectTarget: {
|
||||
type: tagSavedObjectTypeName,
|
||||
namespaces: spaceIds,
|
||||
id,
|
||||
title,
|
||||
noun,
|
||||
},
|
||||
onUpdate: refresh,
|
||||
onClose,
|
||||
};
|
||||
|
||||
const clickProperties = canShareIntoSpace
|
||||
? { cursorStyle: 'pointer', listOnClick: () => setShowFlyout(true) }
|
||||
: { cursorStyle: 'not-allowed' };
|
||||
return (
|
||||
<>
|
||||
<LazySpaceList
|
||||
namespaces={spaceIds}
|
||||
displayLimit={8}
|
||||
behaviorContext="outside-space"
|
||||
{...clickProperties}
|
||||
/>
|
||||
{showFlyout && <LazyShareToSpaceFlyout {...shareToSpaceFlyoutProps} />}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -9,9 +9,11 @@ import React, { useRef, useEffect, FC, ReactNode } from 'react';
|
|||
import { EuiInMemoryTable, EuiBasicTableColumn, EuiLink, Query } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { SpacesApi } from '@kbn/spaces-plugin/public';
|
||||
import { TagsCapabilities, TagWithRelations } from '../../../common';
|
||||
import { TagBadge } from '../../components';
|
||||
import { TagAction } from '../actions';
|
||||
import { SpacesList } from './spaces_list';
|
||||
|
||||
interface TagTableProps {
|
||||
loading: boolean;
|
||||
|
@ -19,6 +21,7 @@ interface TagTableProps {
|
|||
tags: TagWithRelations[];
|
||||
initialQuery?: Query;
|
||||
allowSelection: boolean;
|
||||
reloadTags: () => void;
|
||||
onQueryChange: (query?: Query) => void;
|
||||
selectedTags: TagWithRelations[];
|
||||
onSelectionChange: (selection: TagWithRelations[]) => void;
|
||||
|
@ -26,6 +29,7 @@ interface TagTableProps {
|
|||
onShowRelations: (tag: TagWithRelations) => void;
|
||||
actions: TagAction[];
|
||||
actionBar: ReactNode;
|
||||
spaces?: SpacesApi;
|
||||
}
|
||||
|
||||
const tablePagination = {
|
||||
|
@ -49,6 +53,7 @@ export const TagTable: FC<TagTableProps> = ({
|
|||
tags,
|
||||
initialQuery,
|
||||
allowSelection,
|
||||
reloadTags,
|
||||
onQueryChange,
|
||||
selectedTags,
|
||||
onSelectionChange,
|
||||
|
@ -56,6 +61,7 @@ export const TagTable: FC<TagTableProps> = ({
|
|||
getTagRelationUrl,
|
||||
actionBar,
|
||||
actions,
|
||||
spaces,
|
||||
}) => {
|
||||
const tableRef = useRef<EuiInMemoryTable<TagWithRelations>>(null);
|
||||
|
||||
|
@ -65,6 +71,29 @@ export const TagTable: FC<TagTableProps> = ({
|
|||
}
|
||||
}, [selectedTags]);
|
||||
|
||||
const spacesColumn: EuiBasicTableColumn<TagWithRelations> | undefined = spaces
|
||||
? {
|
||||
field: 'spaces',
|
||||
name: i18n.translate('xpack.savedObjectsTagging.management.table.columns.spaces', {
|
||||
defaultMessage: 'Spaces',
|
||||
}),
|
||||
width: '20%',
|
||||
render: (_, tag) => {
|
||||
return (
|
||||
<SpacesList
|
||||
spacesApi={spaces}
|
||||
canShareIntoSpace={capabilities.shareIntoSpace}
|
||||
spaceIds={tag.namespaces ?? []}
|
||||
id={tag.id}
|
||||
title={tag.name}
|
||||
// TODO handle refresh
|
||||
refresh={reloadTags}
|
||||
/>
|
||||
);
|
||||
},
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const columns: Array<EuiBasicTableColumn<TagWithRelations>> = [
|
||||
{
|
||||
field: 'name',
|
||||
|
@ -126,6 +155,7 @@ export const TagTable: FC<TagTableProps> = ({
|
|||
);
|
||||
},
|
||||
},
|
||||
...(spacesColumn ? [spacesColumn] : []),
|
||||
...(actions.length
|
||||
? [
|
||||
{
|
||||
|
|
|
@ -8,10 +8,11 @@
|
|||
import React, { FC } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { I18nProvider } from '@kbn/i18n-react';
|
||||
import { SpacesApi, SpacesContextProps } from '@kbn/spaces-plugin/public';
|
||||
import { CoreSetup, ApplicationStart } from '@kbn/core/public';
|
||||
import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import { ManagementAppMountParams } from '@kbn/management-plugin/public';
|
||||
import { getTagsCapabilities } from '../../common';
|
||||
import { getTagsCapabilities, tagFeatureId } from '../../common';
|
||||
import { SavedObjectTaggingPluginStart } from '../types';
|
||||
import { ITagInternalClient, ITagAssignmentService, ITagsCache } from '../services';
|
||||
import { TagManagementPage } from './tag_management_page';
|
||||
|
@ -20,6 +21,7 @@ interface MountSectionParams {
|
|||
tagClient: ITagInternalClient;
|
||||
tagCache: ITagsCache;
|
||||
assignmentService: ITagAssignmentService;
|
||||
spaces?: SpacesApi;
|
||||
core: CoreSetup<{}, SavedObjectTaggingPluginStart>;
|
||||
mountParams: ManagementAppMountParams;
|
||||
title: string;
|
||||
|
@ -36,6 +38,8 @@ const RedirectToHomeIfUnauthorized: FC<{
|
|||
return children! as React.ReactElement;
|
||||
};
|
||||
|
||||
const getEmptyFunctionComponent: FC<SpacesContextProps> = ({ children }) => <>{children}</>;
|
||||
|
||||
export const mountSection = async ({
|
||||
tagClient,
|
||||
tagCache,
|
||||
|
@ -43,6 +47,7 @@ export const mountSection = async ({
|
|||
core,
|
||||
mountParams,
|
||||
title,
|
||||
spaces,
|
||||
}: MountSectionParams) => {
|
||||
const [coreStart] = await core.getStartServices();
|
||||
const { element, setBreadcrumbs, theme$ } = mountParams;
|
||||
|
@ -50,20 +55,27 @@ export const mountSection = async ({
|
|||
const assignableTypes = await assignmentService.getAssignableTypes();
|
||||
coreStart.chrome.docTitle.change(title);
|
||||
|
||||
const SpacesContextWrapper = spaces
|
||||
? spaces.ui.components.getSpacesContextProvider
|
||||
: getEmptyFunctionComponent;
|
||||
|
||||
ReactDOM.render(
|
||||
<I18nProvider>
|
||||
<KibanaThemeProvider theme$={theme$}>
|
||||
<RedirectToHomeIfUnauthorized applications={coreStart.application}>
|
||||
<TagManagementPage
|
||||
setBreadcrumbs={setBreadcrumbs}
|
||||
core={coreStart}
|
||||
tagClient={tagClient}
|
||||
tagCache={tagCache}
|
||||
assignmentService={assignmentService}
|
||||
capabilities={capabilities}
|
||||
assignableTypes={assignableTypes}
|
||||
/>
|
||||
</RedirectToHomeIfUnauthorized>
|
||||
<SpacesContextWrapper feature={tagFeatureId}>
|
||||
<RedirectToHomeIfUnauthorized applications={coreStart.application}>
|
||||
<TagManagementPage
|
||||
setBreadcrumbs={setBreadcrumbs}
|
||||
core={coreStart}
|
||||
tagClient={tagClient}
|
||||
tagCache={tagCache}
|
||||
assignmentService={assignmentService}
|
||||
capabilities={capabilities}
|
||||
assignableTypes={assignableTypes}
|
||||
spaces={spaces}
|
||||
/>
|
||||
</RedirectToHomeIfUnauthorized>
|
||||
</SpacesContextWrapper>
|
||||
</KibanaThemeProvider>
|
||||
</I18nProvider>,
|
||||
element
|
||||
|
|
|
@ -12,6 +12,7 @@ import { Query } from '@elastic/eui';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { ChromeBreadcrumb, CoreStart } from '@kbn/core/public';
|
||||
import { EuiSpacer } from '@elastic/eui';
|
||||
import { SpacesApi } from '@kbn/spaces-plugin/public';
|
||||
import { TagWithRelations, TagsCapabilities } from '../../common';
|
||||
import { getCreateModalOpener } from '../components/edition_modal';
|
||||
import { ITagInternalClient, ITagAssignmentService, ITagsCache } from '../services';
|
||||
|
@ -27,6 +28,7 @@ interface TagManagementPageParams {
|
|||
tagClient: ITagInternalClient;
|
||||
tagCache: ITagsCache;
|
||||
assignmentService: ITagAssignmentService;
|
||||
spaces?: SpacesApi;
|
||||
capabilities: TagsCapabilities;
|
||||
assignableTypes: string[];
|
||||
}
|
||||
|
@ -39,6 +41,7 @@ export const TagManagementPage: FC<TagManagementPageParams> = ({
|
|||
assignmentService,
|
||||
capabilities,
|
||||
assignableTypes,
|
||||
spaces,
|
||||
}) => {
|
||||
const { overlays, notifications, application, http, theme } = core;
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
|
@ -206,6 +209,7 @@ export const TagManagementPage: FC<TagManagementPageParams> = ({
|
|||
setQuery(newQuery);
|
||||
setSelectedTags([]);
|
||||
}}
|
||||
reloadTags={fetchTags}
|
||||
allowSelection={bulkActions.length > 0}
|
||||
selectedTags={selectedTags}
|
||||
onSelectionChange={(tags) => {
|
||||
|
@ -215,6 +219,7 @@ export const TagManagementPage: FC<TagManagementPageParams> = ({
|
|||
onShowRelations={(tag) => {
|
||||
showTagRelations(tag);
|
||||
}}
|
||||
spaces={spaces}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -76,7 +76,7 @@ describe('SavedObjectTaggingPlugin', () => {
|
|||
});
|
||||
|
||||
it('creates its cache with correct parameters', () => {
|
||||
plugin.start(coreMock.createStart());
|
||||
plugin.start(coreMock.createStart(), {});
|
||||
|
||||
expect(MockedTagsCache).toHaveBeenCalledTimes(1);
|
||||
expect(MockedTagsCache).toHaveBeenCalledWith({
|
||||
|
@ -94,7 +94,7 @@ describe('SavedObjectTaggingPlugin', () => {
|
|||
const coreStart = coreMock.createStart();
|
||||
coreStart.http.anonymousPaths.isAnonymous.mockReturnValue(false);
|
||||
|
||||
plugin.start(coreStart);
|
||||
plugin.start(coreStart, {});
|
||||
|
||||
expect(MockedTagsCache.mock.instances[0].initialize).not.toHaveBeenCalled();
|
||||
});
|
||||
|
@ -103,7 +103,7 @@ describe('SavedObjectTaggingPlugin', () => {
|
|||
const coreStart = coreMock.createStart();
|
||||
coreStart.http.anonymousPaths.isAnonymous.mockReturnValue(true);
|
||||
|
||||
plugin.start(coreStart);
|
||||
plugin.start(coreStart, {});
|
||||
|
||||
expect(MockedTagsCache.mock.instances[0].initialize).not.toHaveBeenCalled();
|
||||
});
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { CoreSetup, CoreStart, PluginInitializerContext, Plugin } from '@kbn/core/public';
|
||||
import { SpacesApi, SpacesPluginStart } from '@kbn/spaces-plugin/public';
|
||||
import { ManagementSetup } from '@kbn/management-plugin/public';
|
||||
import { SavedObjectTaggingOssPluginSetup } from '@kbn/saved-objects-tagging-oss-plugin/public';
|
||||
import { tagManagementSectionId } from '../common/constants';
|
||||
|
@ -21,12 +22,17 @@ interface SetupDeps {
|
|||
savedObjectsTaggingOss: SavedObjectTaggingOssPluginSetup;
|
||||
}
|
||||
|
||||
interface StartDeps {
|
||||
spaces?: SpacesPluginStart;
|
||||
}
|
||||
|
||||
export class SavedObjectTaggingPlugin
|
||||
implements Plugin<{}, SavedObjectTaggingPluginStart, SetupDeps, {}>
|
||||
implements Plugin<{}, SavedObjectTaggingPluginStart, SetupDeps, StartDeps>
|
||||
{
|
||||
private tagClient?: TagsClient;
|
||||
private tagCache?: TagsCache;
|
||||
private assignmentService?: TagAssignmentService;
|
||||
private spaces?: SpacesApi;
|
||||
private readonly config: SavedObjectsTaggingClientConfig;
|
||||
|
||||
constructor(context: PluginInitializerContext) {
|
||||
|
@ -56,6 +62,7 @@ export class SavedObjectTaggingPlugin
|
|||
core,
|
||||
mountParams,
|
||||
title,
|
||||
spaces: this.spaces,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
@ -67,13 +74,17 @@ export class SavedObjectTaggingPlugin
|
|||
return {};
|
||||
}
|
||||
|
||||
public start({ http, application, overlays, theme, analytics, notifications }: CoreStart) {
|
||||
public start(
|
||||
{ http, application, overlays, theme, analytics, notifications }: CoreStart,
|
||||
{ spaces }: StartDeps
|
||||
) {
|
||||
this.tagCache = new TagsCache({
|
||||
refreshHandler: () => this.tagClient!.getAll({ asSystemRequest: true }),
|
||||
refreshInterval: this.config.cacheRefreshInterval,
|
||||
});
|
||||
this.tagClient = new TagsClient({ analytics, http, changeListener: this.tagCache });
|
||||
this.assignmentService = new TagAssignmentService({ http });
|
||||
this.spaces = spaces;
|
||||
|
||||
// do not fetch tags on anonymous page
|
||||
if (!http.anonymousPaths.isAnonymous(window.location.pathname)) {
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import type { NotificationsStart, OverlayStart, ThemeServiceStart } from '@kbn/core/public';
|
||||
import { SpacesApi } from '@kbn/spaces-plugin/public';
|
||||
import { SavedObjectsTaggingApiUi } from '@kbn/saved-objects-tagging-oss-plugin/public';
|
||||
import { TagsCapabilities } from '../../common';
|
||||
import { ITagsCache, ITagInternalClient } from '../services';
|
||||
|
@ -30,6 +31,7 @@ interface GetUiApiOptions {
|
|||
cache: ITagsCache;
|
||||
client: ITagInternalClient;
|
||||
notifications: NotificationsStart;
|
||||
spaces?: SpacesApi;
|
||||
}
|
||||
|
||||
export const getUiApi = ({
|
||||
|
@ -39,6 +41,7 @@ export const getUiApi = ({
|
|||
overlays,
|
||||
theme,
|
||||
notifications,
|
||||
spaces,
|
||||
}: GetUiApiOptions): SavedObjectsTaggingApiUi => {
|
||||
const components = getComponents({
|
||||
cache,
|
||||
|
|
|
@ -10,6 +10,7 @@ import { Tag, TagSavedObject } from '../../../common/types';
|
|||
export const savedObjectToTag = (savedObject: TagSavedObject): Tag => {
|
||||
return {
|
||||
id: savedObject.id,
|
||||
namespaces: savedObject.namespaces,
|
||||
...savedObject.attributes,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
"@kbn/usage-collection-plugin",
|
||||
"@kbn/features-plugin",
|
||||
"@kbn/security-plugin",
|
||||
"@kbn/spaces-plugin",
|
||||
"@kbn/i18n",
|
||||
"@kbn/utility-types",
|
||||
"@kbn/i18n-react",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue