[7.x] add SavedObjectType.management.displayName (#113091) (#113249)

* add `SavedObjectType.management.displayName` (#113091)

* add `SavedObjectType.management.displayName`

* fix unit tests

* add FTR test

* update generated doc

* also update labels

* fix unit tests
# Conflicts:
#	src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.test.tsx
#	src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx
#	src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx
#	src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx
#	src/plugins/saved_objects_management/public/management_section/saved_objects_table_page.tsx

* fix types

* fix allowedTypes usage

* fix unit tests
This commit is contained in:
Pierre Gayvallet 2021-09-28 22:03:09 +02:00 committed by GitHub
parent 3508430fc9
commit 102822f4fd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 333 additions and 69 deletions

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsTypeManagementDefinition](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.md) &gt; [displayName](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.displayname.md)
## SavedObjectsTypeManagementDefinition.displayName property
When specified, will be used instead of the type's name in SO management section's labels.
<b>Signature:</b>
```typescript
displayName?: string;
```

View file

@ -17,6 +17,7 @@ export interface SavedObjectsTypeManagementDefinition<Attributes = any>
| Property | Type | Description |
| --- | --- | --- |
| [defaultSearchField](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.defaultsearchfield.md) | <code>string</code> | The default search field to use for this type. Defaults to <code>id</code>. |
| [displayName](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.displayname.md) | <code>string</code> | When specified, will be used instead of the type's name in SO management section's labels. |
| [getEditUrl](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.getediturl.md) | <code>(savedObject: SavedObject&lt;Attributes&gt;) =&gt; string</code> | Function returning the url to use to redirect to the editing page of this object. If not defined, editing will not be allowed. |
| [getInAppUrl](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.getinappurl.md) | <code>(savedObject: SavedObject&lt;Attributes&gt;) =&gt; {</code><br/><code> path: string;</code><br/><code> uiCapabilitiesPath: string;</code><br/><code> }</code> | Function returning the url to use to redirect to this object from the management section. If not defined, redirecting to the object will not be allowed. |
| [getTitle](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.gettitle.md) | <code>(savedObject: SavedObject&lt;Attributes&gt;) =&gt; string</code> | Function returning the title to display in the management table. If not defined, will use the object's type and id to generate a label. |

View file

@ -356,6 +356,10 @@ export interface SavedObjectsTypeManagementDefinition<Attributes = any> {
* Is the type importable or exportable. Defaults to `false`.
*/
importableAndExportable?: boolean;
/**
* When specified, will be used instead of the type's name in SO management section's labels.
*/
displayName?: string;
/**
* When set to false, the type will not be listed or searchable in the SO management section.
* Main usage of setting this property to false for a type is when objects from the type should

View file

@ -2716,6 +2716,7 @@ export interface SavedObjectsType<Attributes = any> {
// @public
export interface SavedObjectsTypeManagementDefinition<Attributes = any> {
defaultSearchField?: string;
displayName?: string;
getEditUrl?: (savedObject: SavedObject<Attributes>) => string;
getInAppUrl?: (savedObject: SavedObject<Attributes>) => {
path: string;

View file

@ -13,4 +13,5 @@ export type {
SavedObjectRelationKind,
SavedObjectInvalidRelation,
SavedObjectGetRelationshipsResponse,
SavedObjectManagementTypeInfo,
} from './types';

View file

@ -6,8 +6,8 @@
* Side Public License, v 1.
*/
import { SavedObject } from 'src/core/types';
import { SavedObjectsNamespaceType } from 'src/core/public';
import type { SavedObject } from 'src/core/types';
import type { SavedObjectsNamespaceType } from 'src/core/public';
/**
* The metadata injected into a {@link SavedObject | saved object} when returning
@ -52,3 +52,10 @@ export interface SavedObjectGetRelationshipsResponse {
relations: SavedObjectRelation[];
invalidRelations: SavedObjectInvalidRelation[];
}
export interface SavedObjectManagementTypeInfo {
name: string;
namespaceType: SavedObjectsNamespaceType;
hidden: boolean;
displayName: string;
}

View file

@ -6,13 +6,14 @@
* Side Public License, v 1.
*/
import { HttpStart } from 'src/core/public';
import type { HttpStart } from 'src/core/public';
import type { SavedObjectManagementTypeInfo } from '../../common/types';
interface GetAllowedTypesResponse {
types: string[];
types: SavedObjectManagementTypeInfo[];
}
export async function getAllowedTypes(http: HttpStart) {
export async function getAllowedTypes(http: HttpStart): Promise<SavedObjectManagementTypeInfo[]> {
const response = await http.get<GetAllowedTypesResponse>(
'/api/kibana/management/saved_objects/_allowed_types'
);

View file

@ -0,0 +1,31 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type { SavedObjectManagementTypeInfo } from '../../common/types';
import { getSavedObjectLabel } from './get_saved_object_label';
const toTypeInfo = (name: string, displayName?: string): SavedObjectManagementTypeInfo => ({
name,
displayName: displayName ?? name,
hidden: false,
namespaceType: 'single',
});
describe('getSavedObjectLabel', () => {
it('returns the type name if no types are provided', () => {
expect(getSavedObjectLabel('foo', [])).toEqual('foo');
});
it('returns the type name if type does not specify a display name', () => {
expect(getSavedObjectLabel('foo', [toTypeInfo('foo')])).toEqual('foo');
});
it('returns the type display name if type does specify a display name', () => {
expect(getSavedObjectLabel('foo', [toTypeInfo('foo', 'fooDisplay')])).toEqual('fooDisplay');
});
});

View file

@ -6,13 +6,12 @@
* Side Public License, v 1.
*/
export function getSavedObjectLabel(type: string) {
switch (type) {
case 'index-pattern':
case 'index-patterns':
case 'indexPatterns':
return 'index patterns';
default:
return type;
}
import type { SavedObjectManagementTypeInfo } from '../../common/types';
/**
* Returns the label to be used for given saved object type.
*/
export function getSavedObjectLabel(type: string, types: SavedObjectManagementTypeInfo[]) {
const typeInfo = types.find((t) => t.name === type);
return typeInfo?.displayName ?? type;
}

View file

@ -14,6 +14,7 @@ import { i18n } from '@kbn/i18n';
import { EuiLoadingSpinner } from '@elastic/eui';
import { CoreSetup } from 'src/core/public';
import { ManagementAppMountParams } from '../../../management/public';
import type { SavedObjectManagementTypeInfo } from '../../common/types';
import { StartDependencies, SavedObjectsManagementPluginStart } from '../plugin';
import { ISavedObjectsManagementServiceRegistry } from '../services';
import { getAllowedTypes } from './../lib';
@ -24,7 +25,7 @@ interface MountParams {
mountParams: ManagementAppMountParams;
}
let allowedObjectTypes: string[] | undefined;
let allowedObjectTypes: SavedObjectManagementTypeInfo[] | undefined;
const title = i18n.translate('savedObjectsManagement.objects.savedObjectsTitle', {
defaultMessage: 'Saved Objects',
@ -39,15 +40,15 @@ export const mountManagementSection = async ({
}: MountParams) => {
const [coreStart, { data, savedObjectsTaggingOss, spaces: spacesApi }, pluginStart] =
await core.getStartServices();
const { capabilities } = coreStart.application;
const { element, history, setBreadcrumbs } = mountParams;
if (allowedObjectTypes === undefined) {
if (!allowedObjectTypes) {
allowedObjectTypes = await getAllowedTypes(coreStart.http);
}
coreStart.chrome.docTitle.change(title);
const capabilities = coreStart.application.capabilities;
const RedirectToHomeIfUnauthorized: React.FunctionComponent = ({ children }) => {
const allowed = capabilities?.management?.kibana?.objects ?? false;

View file

@ -2,6 +2,34 @@
exports[`SavedObjectsTable delete should show a confirm modal 1`] = `
<DeleteConfirmModal
allowedTypes={
Array [
Object {
"displayName": "index-pattern",
"hidden": false,
"name": "index-pattern",
"namespaceType": "single",
},
Object {
"displayName": "visualization",
"hidden": false,
"name": "visualization",
"namespaceType": "single",
},
Object {
"displayName": "dashboard",
"hidden": false,
"name": "dashboard",
"namespaceType": "single",
},
Object {
"displayName": "search",
"hidden": false,
"name": "search",
"namespaceType": "single",
},
]
}
isDeleting={false}
onCancel={[Function]}
onConfirm={[Function]}
@ -118,6 +146,34 @@ exports[`SavedObjectsTable should render normally 1`] = `
"has": [MockFunction],
}
}
allowedTypes={
Array [
Object {
"displayName": "index-pattern",
"hidden": false,
"name": "index-pattern",
"namespaceType": "single",
},
Object {
"displayName": "visualization",
"hidden": false,
"name": "visualization",
"namespaceType": "single",
},
Object {
"displayName": "dashboard",
"hidden": false,
"name": "dashboard",
"namespaceType": "single",
},
Object {
"displayName": "search",
"hidden": false,
"name": "search",
"namespaceType": "single",
},
]
}
basePath={
BasePath {
"basePath": "",

View file

@ -672,6 +672,22 @@ exports[`Flyout should render import step 1`] = `
exports[`Flyout summary should display summary when import is complete 1`] = `
<ImportSummary
allowedTypes={
Array [
Object {
"displayName": "search",
"hidden": false,
"name": "search",
"namespaceType": "single",
},
Object {
"displayName": "index-pattern",
"hidden": false,
"name": "index-pattern",
"namespaceType": "single",
},
]
}
basePath={
BasePath {
"basePath": "",

View file

@ -215,13 +215,13 @@ exports[`Relationships should render index patterns normally 1`] = `
>
<h2>
<EuiToolTip
content="index patterns"
content="index-pattern"
delay="regular"
display="inlineBlock"
position="top"
>
<EuiIcon
aria-label="index patterns"
aria-label="index-pattern"
size="m"
type="indexPatternApp"
/>
@ -378,13 +378,13 @@ exports[`Relationships should render invalid relations 1`] = `
>
<h2>
<EuiToolTip
content="index patterns"
content="index-pattern"
delay="regular"
display="inlineBlock"
position="top"
>
<EuiIcon
aria-label="index patterns"
aria-label="index-pattern"
size="m"
type="indexPatternApp"
/>

View file

@ -9,7 +9,7 @@
import React from 'react';
import { findTestSubject } from '@elastic/eui/lib/test';
import { mountWithIntl } from '@kbn/test/jest';
import { SavedObjectWithMetadata } from '../../../../common';
import type { SavedObjectWithMetadata, SavedObjectManagementTypeInfo } from '../../../../common';
import { DeleteConfirmModal } from './delete_confirm_modal';
interface CreateObjectOptions {
@ -32,6 +32,7 @@ const createObject = ({
});
describe('DeleteConfirmModal', () => {
const allowedTypes: SavedObjectManagementTypeInfo[] = [];
let onConfirm: jest.Mock;
let onCancel: jest.Mock;
@ -47,6 +48,7 @@ describe('DeleteConfirmModal', () => {
onConfirm={onConfirm}
onCancel={onCancel}
selectedObjects={[]}
allowedTypes={allowedTypes}
/>
);
expect(wrapper.find('EuiLoadingElastic')).toHaveLength(1);
@ -61,6 +63,7 @@ describe('DeleteConfirmModal', () => {
onConfirm={onConfirm}
onCancel={onCancel}
selectedObjects={objs}
allowedTypes={allowedTypes}
/>
);
expect(wrapper.find('.euiTableRow')).toHaveLength(3);
@ -73,6 +76,7 @@ describe('DeleteConfirmModal', () => {
onConfirm={onConfirm}
onCancel={onCancel}
selectedObjects={[]}
allowedTypes={allowedTypes}
/>
);
wrapper.find('EuiButtonEmpty').simulate('click');
@ -88,6 +92,7 @@ describe('DeleteConfirmModal', () => {
onConfirm={onConfirm}
onCancel={onCancel}
selectedObjects={[createObject()]}
allowedTypes={allowedTypes}
/>
);
wrapper.find('EuiButton').simulate('click');
@ -109,6 +114,7 @@ describe('DeleteConfirmModal', () => {
onConfirm={onConfirm}
onCancel={onCancel}
selectedObjects={objs}
allowedTypes={allowedTypes}
/>
);
expect(wrapper.find('.euiTableRow')).toHaveLength(1);
@ -126,6 +132,7 @@ describe('DeleteConfirmModal', () => {
onConfirm={onConfirm}
onCancel={onCancel}
selectedObjects={objs}
allowedTypes={allowedTypes}
/>
);
@ -145,6 +152,7 @@ describe('DeleteConfirmModal', () => {
onConfirm={onConfirm}
onCancel={onCancel}
selectedObjects={objs}
allowedTypes={allowedTypes}
/>
);
@ -164,6 +172,7 @@ describe('DeleteConfirmModal', () => {
onConfirm={onConfirm}
onCancel={onCancel}
selectedObjects={objs}
allowedTypes={allowedTypes}
/>
);
@ -184,6 +193,7 @@ describe('DeleteConfirmModal', () => {
onConfirm={onConfirm}
onCancel={onCancel}
selectedObjects={objs}
allowedTypes={allowedTypes}
/>
);
const callout = findTestSubject(wrapper, 'sharedObjectsWarning');
@ -202,6 +212,7 @@ describe('DeleteConfirmModal', () => {
onConfirm={onConfirm}
onCancel={onCancel}
selectedObjects={objs}
allowedTypes={allowedTypes}
/>
);
const callout = findTestSubject(wrapper, 'sharedObjectsWarning');

View file

@ -27,7 +27,7 @@ import {
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { SavedObjectWithMetadata } from '../../../../common';
import type { SavedObjectWithMetadata, SavedObjectManagementTypeInfo } from '../../../../common';
import { getSavedObjectLabel } from '../../../lib';
export interface DeleteConfirmModalProps {
@ -35,6 +35,7 @@ export interface DeleteConfirmModalProps {
onConfirm: () => void;
onCancel: () => void;
selectedObjects: SavedObjectWithMetadata[];
allowedTypes: SavedObjectManagementTypeInfo[];
}
export const DeleteConfirmModal: FC<DeleteConfirmModalProps> = ({
@ -42,6 +43,7 @@ export const DeleteConfirmModal: FC<DeleteConfirmModalProps> = ({
onConfirm,
onCancel,
selectedObjects,
allowedTypes,
}) => {
const undeletableObjects = useMemo(() => {
return selectedObjects.filter((obj) => obj.meta.hiddenType);
@ -145,7 +147,7 @@ export const DeleteConfirmModal: FC<DeleteConfirmModalProps> = ({
),
width: '50px',
render: (type, { icon }) => (
<EuiToolTip position="top" content={getSavedObjectLabel(type)}>
<EuiToolTip position="top" content={getSavedObjectLabel(type, allowedTypes)}>
<EuiIcon type={icon} />
</EuiToolTip>
),

View file

@ -61,7 +61,20 @@ describe('Flyout', () => {
} as any,
overlays,
http,
allowedTypes: ['search', 'index-pattern', 'visualization'],
allowedTypes: [
{
name: 'search',
displayName: 'search',
hidden: false,
namespaceType: 'single',
},
{
name: 'index-pattern',
displayName: 'index-pattern',
hidden: false,
namespaceType: 'single',
},
],
serviceRegistry: serviceRegistryMock.create(),
search,
basePath,

View file

@ -37,6 +37,7 @@ import {
IndexPattern,
DataPublicPluginStart,
} from '../../../../../data/public';
import type { SavedObjectManagementTypeInfo } from '../../../../common/types';
import {
importFile,
importLegacyFile,
@ -62,7 +63,6 @@ const OVERWRITE_ALL_DEFAULT = true;
export interface FlyoutProps {
serviceRegistry: ISavedObjectsManagementServiceRegistry;
allowedTypes: string[];
close: () => void;
done: () => void;
newIndexPatternUrl: string;
@ -71,6 +71,7 @@ export interface FlyoutProps {
http: HttpStart;
basePath: IBasePath;
search: DataPublicPluginStart['search'];
allowedTypes: SavedObjectManagementTypeInfo[];
}
export interface FlyoutState {
@ -280,8 +281,9 @@ export class Flyout extends Component<FlyoutProps, FlyoutState> {
return;
}
const allowedTypeNames = allowedTypes.map((type) => type.name);
contents = contents
.filter((content) => allowedTypes.includes(content._type))
.filter((content) => allowedTypeNames.includes(content._type))
.map((doc) => ({
...doc,
// The server assumes that documents with no migrationVersion are up to date.
@ -610,6 +612,7 @@ export class Flyout extends Component<FlyoutProps, FlyoutState> {
}
renderBody() {
const { allowedTypes } = this.props;
const {
status,
loadingMessage,
@ -642,6 +645,7 @@ export class Flyout extends Component<FlyoutProps, FlyoutState> {
failedImports={failedImports}
successfulImports={successfulImports}
importWarnings={importWarnings ?? []}
allowedTypes={allowedTypes}
/>
);
}

View file

@ -24,6 +24,7 @@ describe('ImportSummary', () => {
failedImports: [],
successfulImports: [],
importWarnings: [],
allowedTypes: [],
...parts,
});

View file

@ -29,6 +29,7 @@ import type {
SavedObjectsImportWarning,
IBasePath,
} from 'kibana/public';
import type { SavedObjectManagementTypeInfo } from '../../../../common/types';
import { getDefaultTitle, getSavedObjectLabel, FailedImport } from '../../../lib';
const DEFAULT_ICON = 'apps';
@ -38,6 +39,7 @@ export interface ImportSummaryProps {
successfulImports: SavedObjectsImportSuccess[];
importWarnings: SavedObjectsImportWarning[];
basePath: IBasePath;
allowedTypes: SavedObjectManagementTypeInfo[];
}
interface ImportItem {
@ -244,6 +246,7 @@ export const ImportSummary: FC<ImportSummaryProps> = ({
successfulImports,
importWarnings,
basePath,
allowedTypes,
}) => {
const importItems: ImportItem[] = useMemo(
() =>
@ -279,6 +282,7 @@ export const ImportSummary: FC<ImportSummaryProps> = ({
<EuiHorizontalRule />
{importItems.map((item, index) => {
const { type, title, icon } = item;
const typeLabel = getSavedObjectLabel(type, allowedTypes);
return (
<EuiFlexGroup
responsive={false}
@ -288,8 +292,8 @@ export const ImportSummary: FC<ImportSummaryProps> = ({
className="savedObjectsManagementImportSummary__row"
>
<EuiFlexItem grow={false}>
<EuiToolTip position="top" content={getSavedObjectLabel(type)}>
<EuiIcon aria-label={getSavedObjectLabel(type)} type={icon} size="s" />
<EuiToolTip position="top" content={typeLabel}>
<EuiIcon aria-label={typeLabel} type={icon} size="s" />
</EuiToolTip>
</EuiFlexItem>
<EuiFlexItem className="savedObjectsManagementImportSummary__title">

View file

@ -9,6 +9,7 @@
import React from 'react';
import { shallowWithI18nProvider } from '@kbn/test/jest';
import { httpServiceMock } from '../../../../../../core/public/mocks';
import type { SavedObjectManagementTypeInfo } from '../../../../common/types';
import { Relationships, RelationshipsProps } from './relationships';
jest.mock('../../../lib/fetch_export_by_type_and_search', () => ({
@ -19,6 +20,15 @@ jest.mock('../../../lib/fetch_export_objects', () => ({
fetchExportObjects: jest.fn(),
}));
const allowedTypes: SavedObjectManagementTypeInfo[] = [
{
name: 'index-pattern',
displayName: 'index-pattern',
namespaceType: 'single',
hidden: false,
},
];
describe('Relationships', () => {
it('should render index patterns normally', async () => {
const props: RelationshipsProps = {
@ -73,6 +83,7 @@ describe('Relationships', () => {
},
},
},
allowedTypes,
close: jest.fn(),
};
@ -143,6 +154,7 @@ describe('Relationships', () => {
},
},
},
allowedTypes,
close: jest.fn(),
};
@ -213,6 +225,7 @@ describe('Relationships', () => {
},
},
},
allowedTypes,
close: jest.fn(),
};
@ -283,6 +296,7 @@ describe('Relationships', () => {
},
},
},
allowedTypes,
close: jest.fn(),
};
@ -323,6 +337,7 @@ describe('Relationships', () => {
},
},
},
allowedTypes,
close: jest.fn(),
};
@ -368,6 +383,7 @@ describe('Relationships', () => {
},
},
},
allowedTypes,
close: jest.fn(),
};

View file

@ -25,6 +25,7 @@ import { SearchFilterConfig } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { IBasePath } from 'src/core/public';
import type { SavedObjectManagementTypeInfo } from '../../../../common/types';
import { getDefaultTitle, getSavedObjectLabel } from '../../../lib';
import {
SavedObjectWithMetadata,
@ -41,6 +42,7 @@ export interface RelationshipsProps {
close: () => void;
goInspectObject: (obj: SavedObjectWithMetadata) => void;
canGoInApp: (obj: SavedObjectWithMetadata) => boolean;
allowedTypes: SavedObjectManagementTypeInfo[];
}
export interface RelationshipsState {
@ -213,7 +215,7 @@ export class Relationships extends Component<RelationshipsProps, RelationshipsSt
}
renderRelationshipsTable() {
const { goInspectObject, basePath, savedObject } = this.props;
const { goInspectObject, basePath, savedObject, allowedTypes } = this.props;
const { relations, isLoading, error } = this.state;
if (error) {
@ -238,10 +240,11 @@ export class Relationships extends Component<RelationshipsProps, RelationshipsSt
),
sortable: false,
render: (type: string, object: SavedObjectWithMetadata) => {
const typeLabel = getSavedObjectLabel(type, allowedTypes);
return (
<EuiToolTip position="top" content={getSavedObjectLabel(type)}>
<EuiToolTip position="top" content={typeLabel}>
<EuiIcon
aria-label={getSavedObjectLabel(type)}
aria-label={typeLabel}
type={object.meta.icon || 'apps'}
size="s"
data-test-subj="relationshipsObjectType"
@ -390,19 +393,16 @@ export class Relationships extends Component<RelationshipsProps, RelationshipsSt
}
render() {
const { close, savedObject } = this.props;
const { close, savedObject, allowedTypes } = this.props;
const typeLabel = getSavedObjectLabel(savedObject.type, allowedTypes);
return (
<EuiFlyout onClose={close}>
<EuiFlyoutHeader hasBorder>
<EuiTitle size="m">
<h2>
<EuiToolTip position="top" content={getSavedObjectLabel(savedObject.type)}>
<EuiIcon
aria-label={getSavedObjectLabel(savedObject.type)}
size="m"
type={savedObject.meta.icon || 'apps'}
/>
<EuiToolTip position="top" content={typeLabel}>
<EuiIcon aria-label={typeLabel} size="m" type={savedObject.meta.icon || 'apps'} />
</EuiToolTip>
&nbsp;&nbsp;
{savedObject.meta.title || getDefaultTitle(savedObject)}

View file

@ -36,6 +36,9 @@ const defaultProps: TableProps = {
},
},
],
allowedTypes: [
{ name: 'index-pattern', displayName: 'index-pattern', hidden: false, namespaceType: 'single' },
],
selectionConfig: {
onSelectionChange: () => {},
},

View file

@ -28,6 +28,7 @@ import {
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { SavedObjectsTaggingApi } from '../../../../../saved_objects_tagging_oss/public';
import type { SavedObjectManagementTypeInfo } from '../../../../common/types';
import { getDefaultTitle, getSavedObjectLabel } from '../../../lib';
import { SavedObjectWithMetadata } from '../../../types';
import {
@ -39,6 +40,7 @@ import {
export interface TableProps {
taggingApi?: SavedObjectsTaggingApi;
basePath: IBasePath;
allowedTypes: SavedObjectManagementTypeInfo[];
actionRegistry: SavedObjectsManagementActionServiceStart;
columnRegistry: SavedObjectsManagementColumnServiceStart;
selectedSavedObjects: SavedObjectWithMetadata[];
@ -145,6 +147,7 @@ export class Table extends PureComponent<TableProps, TableState> {
actionRegistry,
columnRegistry,
taggingApi,
allowedTypes,
} = this.props;
const pagination = {
@ -182,10 +185,11 @@ export class Table extends PureComponent<TableProps, TableState> {
sortable: false,
'data-test-subj': 'savedObjectsTableRowType',
render: (type: string, object: SavedObjectWithMetadata) => {
const typeLabel = getSavedObjectLabel(type, allowedTypes);
return (
<EuiToolTip position="top" content={getSavedObjectLabel(type)}>
<EuiToolTip position="top" content={typeLabel}>
<EuiIcon
aria-label={getSavedObjectLabel(type)}
aria-label={typeLabel}
type={object.meta.icon || 'apps'}
size="s"
data-test-subj="objectType"

View file

@ -29,6 +29,7 @@ import {
} from '../../../../../core/public/mocks';
import { dataPluginMock } from '../../../../data/public/mocks';
import { serviceRegistryMock } from '../../services/service_registry.mock';
import type { SavedObjectManagementTypeInfo } from '../../../common/types';
import { actionServiceMock } from '../../services/action_service.mock';
import { columnServiceMock } from '../../services/column_service.mock';
import {
@ -39,7 +40,14 @@ import {
import { Flyout, Relationships } from './components';
import { SavedObjectWithMetadata } from '../../types';
const allowedTypes = ['index-pattern', 'visualization', 'dashboard', 'search'];
const convertType = (type: string): SavedObjectManagementTypeInfo => ({
name: type,
displayName: type,
hidden: false,
namespaceType: 'single',
});
const allowedTypes = ['index-pattern', 'visualization', 'dashboard', 'search'].map(convertType);
const allSavedObjects = [
{
@ -384,7 +392,7 @@ describe('SavedObjectsTable', () => {
expect(fetchExportByTypeAndSearchMock).toHaveBeenCalledWith({
http,
types: allowedTypes,
types: allowedTypes.map((type) => type.name),
includeReferencesDeep: true,
});
expect(saveAsMock).toHaveBeenCalledWith(blob, 'export.ndjson');
@ -413,7 +421,7 @@ describe('SavedObjectsTable', () => {
expect(fetchExportByTypeAndSearchMock).toHaveBeenCalledWith({
http,
types: allowedTypes,
types: allowedTypes.map((type) => type.name),
search: 'test*',
includeReferencesDeep: true,
});

View file

@ -23,6 +23,7 @@ import {
import { RedirectAppLinks } from '../../../../kibana_react/public';
import { SavedObjectsTaggingApi } from '../../../../saved_objects_tagging_oss/public';
import { IndexPatternsContract } from '../../../../data/public';
import type { SavedObjectManagementTypeInfo } from '../../../common/types';
import {
parseQuery,
getSavedObjectCounts,
@ -57,7 +58,7 @@ interface ExportAllOption {
}
export interface SavedObjectsTableProps {
allowedTypes: string[];
allowedTypes: SavedObjectManagementTypeInfo[];
serviceRegistry: ISavedObjectsManagementServiceRegistry;
actionRegistry: SavedObjectsManagementActionServiceStart;
columnRegistry: SavedObjectsManagementColumnServiceStart;
@ -104,6 +105,7 @@ const unableFindSavedObjectNotificationMessage = i18n.translate(
'savedObjectsManagement.objectsTable.unableFindSavedObjectNotificationMessage',
{ defaultMessage: 'Unable to find saved object' }
);
export class SavedObjectsTable extends Component<SavedObjectsTableProps, SavedObjectsTableState> {
private _isMounted = false;
@ -116,7 +118,7 @@ export class SavedObjectsTable extends Component<SavedObjectsTableProps, SavedOb
perPage: props.perPageConfig || 50,
savedObjects: [],
savedObjectCounts: props.allowedTypes.reduce((typeToCountMap, type) => {
typeToCountMap[type] = 0;
typeToCountMap[type.name] = 0;
return typeToCountMap;
}, {} as Record<string, number>),
activeQuery: props.initialQuery ?? Query.parse(''),
@ -148,9 +150,11 @@ export class SavedObjectsTable extends Component<SavedObjectsTableProps, SavedOb
}
fetchCounts = async () => {
const { allowedTypes, taggingApi } = this.props;
const { taggingApi } = this.props;
const { queryText, visibleTypes, selectedTags } = parseQuery(this.state.activeQuery);
const allowedTypes = this.props.allowedTypes.map((type) => type.name);
const selectedTypes = allowedTypes.filter(
(type) => !visibleTypes || visibleTypes.includes(type)
);
@ -209,6 +213,11 @@ export class SavedObjectsTable extends Component<SavedObjectsTableProps, SavedOb
const { activeQuery: query, page, perPage } = this.state;
const { notifications, http, allowedTypes, taggingApi } = this.props;
const { queryText, visibleTypes, selectedTags } = parseQuery(query);
const searchTypes = allowedTypes
.map((type) => type.name)
.filter((type) => !visibleTypes || visibleTypes.includes(type));
// "searchFields" is missing from the "findOptions" but gets injected via the API.
// The API extracts the fields from each uiExports.savedObjectsManagement "defaultSearchField" attribute
const findOptions: SavedObjectsFindOptions = {
@ -216,7 +225,7 @@ export class SavedObjectsTable extends Component<SavedObjectsTableProps, SavedOb
perPage,
page: page + 1,
fields: ['id'],
type: allowedTypes.filter((type) => !visibleTypes || visibleTypes.includes(type)),
type: searchTypes,
};
if (findOptions.type.length > 1) {
findOptions.sortField = 'type';
@ -522,8 +531,9 @@ export class SavedObjectsTable extends Component<SavedObjectsTableProps, SavedOb
};
getRelationships = async (type: string, id: string) => {
const { allowedTypes, http } = this.props;
return await getRelationships(http, type, id, allowedTypes);
const { http } = this.props;
const allowedTypeNames = this.props.allowedTypes.map((t) => t.name);
return await getRelationships(http, type, id, allowedTypeNames);
};
renderFlyout() {
@ -543,10 +553,10 @@ export class SavedObjectsTable extends Component<SavedObjectsTableProps, SavedOb
serviceRegistry={this.props.serviceRegistry}
indexPatterns={this.props.indexPatterns}
newIndexPatternUrl={newIndexPatternUrl}
allowedTypes={this.props.allowedTypes}
overlays={this.props.overlays}
basePath={this.props.http.basePath}
search={this.props.search}
allowedTypes={this.props.allowedTypes}
/>
);
}
@ -564,12 +574,15 @@ export class SavedObjectsTable extends Component<SavedObjectsTableProps, SavedOb
close={this.onHideRelationships}
goInspectObject={this.props.goInspectObject}
canGoInApp={this.props.canGoInApp}
allowedTypes={this.props.allowedTypes}
/>
);
}
renderDeleteConfirmModal() {
const { isShowingDeleteConfirmModal, isDeleting, selectedSavedObjects } = this.state;
const { allowedTypes } = this.props;
if (!isShowingDeleteConfirmModal) {
return null;
}
@ -584,6 +597,7 @@ export class SavedObjectsTable extends Component<SavedObjectsTableProps, SavedOb
this.setState({ isShowingDeleteConfirmModal: false });
}}
selectedObjects={selectedSavedObjects}
allowedTypes={allowedTypes}
/>
);
}
@ -642,9 +656,9 @@ export class SavedObjectsTable extends Component<SavedObjectsTableProps, SavedOb
};
const filterOptions = allowedTypes.map((type) => ({
value: type,
name: type,
view: `${type} (${savedObjectCounts[type] || 0})`,
value: type.name,
name: type.name,
view: `${type.displayName} (${savedObjectCounts[type.name] || 0})`,
}));
return (
@ -665,6 +679,7 @@ export class SavedObjectsTable extends Component<SavedObjectsTableProps, SavedOb
basePath={http.basePath}
taggingApi={taggingApi}
initialQuery={this.props.initialQuery}
allowedTypes={allowedTypes}
itemId={'id'}
actionRegistry={this.props.actionRegistry}
columnRegistry={this.props.columnRegistry}

View file

@ -16,6 +16,7 @@ import { CoreStart, ChromeBreadcrumb } from 'src/core/public';
import type { SpacesApi, SpacesContextProps } from '../../../../../x-pack/plugins/spaces/public';
import { DataPublicPluginStart } from '../../../data/public';
import { SavedObjectsTaggingApi } from '../../../saved_objects_tagging_oss/public';
import type { SavedObjectManagementTypeInfo } from '../../common/types';
import {
ISavedObjectsManagementServiceRegistry,
SavedObjectsManagementActionServiceStart,
@ -40,7 +41,7 @@ const SavedObjectsTablePage = ({
dataStart: DataPublicPluginStart;
taggingApi?: SavedObjectsTaggingApi;
spacesApi?: SpacesApi;
allowedTypes: string[];
allowedTypes: SavedObjectManagementTypeInfo[];
serviceRegistry: ISavedObjectsManagementServiceRegistry;
actionRegistry: SavedObjectsManagementActionServiceStart;
columnRegistry: SavedObjectsManagementColumnServiceStart;

View file

@ -6,7 +6,17 @@
* Side Public License, v 1.
*/
import { IRouter } from 'src/core/server';
import { IRouter, SavedObjectsType } from 'src/core/server';
import { SavedObjectManagementTypeInfo } from '../../common';
const convertType = (sot: SavedObjectsType): SavedObjectManagementTypeInfo => {
return {
name: sot.name,
namespaceType: sot.namespaceType,
hidden: sot.hidden,
displayName: sot.management?.displayName ?? sot.name,
};
};
export const registerGetAllowedTypesRoute = (router: IRouter) => {
router.get(
@ -18,7 +28,7 @@ export const registerGetAllowedTypesRoute = (router: IRouter) => {
const allowedTypes = context.core.savedObjects.typeRegistry
.getImportableAndExportableTypes()
.filter((type) => type.management!.visibleInManagement ?? true)
.map((type) => type.name);
.map(convertType);
return res.ok({
body: {

View file

@ -212,6 +212,24 @@ export class SavedObjectExportTransformsPlugin implements Plugin {
visibleInManagement: true,
},
});
// example of a SO type specifying a display name
savedObjects.registerType<{ enabled: boolean; title: string }>({
name: 'test-with-display-name',
hidden: false,
namespaceType: 'single',
mappings: {
properties: {
title: { type: 'text' },
enabled: { type: 'boolean' },
},
},
management: {
defaultSearchField: 'title',
importableAndExportable: true,
displayName: 'my display name',
},
});
}
public start() {}

View file

@ -11,6 +11,7 @@ import expect from '@kbn/expect';
import type { Response } from 'supertest';
import type { PluginFunctionalProviderContext } from '../../services';
import { SavedObject } from '../../../../src/core/types';
import type { SavedObjectManagementTypeInfo } from '../../../../src/plugins/saved_objects_management/common/types';
function parseNdJson(input: string): Array<SavedObject<any>> {
return input.split('\n').map((str) => JSON.parse(str));
@ -97,17 +98,39 @@ export default function ({ getService }: PluginFunctionalProviderContext) {
});
describe('savedObjects management APIS', () => {
it('GET /api/kibana/management/saved_objects/_allowed_types should only return types that are `visibleInManagement: true`', async () =>
await supertest
.get('/api/kibana/management/saved_objects/_allowed_types')
.set('kbn-xsrf', 'true')
.expect(200)
.then((response: Response) => {
const { types } = response.body;
expect(types.includes('test-is-exportable')).to.eql(true);
expect(types.includes('test-visible-in-management')).to.eql(true);
expect(types.includes('test-not-visible-in-management')).to.eql(false);
}));
describe('GET /api/kibana/management/saved_objects/_allowed_types', () => {
let types: SavedObjectManagementTypeInfo[];
before(async () => {
await supertest
.get('/api/kibana/management/saved_objects/_allowed_types')
.set('kbn-xsrf', 'true')
.expect(200)
.then((response: Response) => {
types = response.body.types as SavedObjectManagementTypeInfo[];
});
});
it('should only return types that are `visibleInManagement: true`', () => {
const typeNames = types.map((type) => type.name);
expect(typeNames.includes('test-is-exportable')).to.eql(true);
expect(typeNames.includes('test-visible-in-management')).to.eql(true);
expect(typeNames.includes('test-not-visible-in-management')).to.eql(false);
});
it('should return displayName for types specifying it', () => {
const typeWithDisplayName = types.find((type) => type.name === 'test-with-display-name');
expect(typeWithDisplayName !== undefined).to.eql(true);
expect(typeWithDisplayName!.displayName).to.eql('my display name');
const typeWithoutDisplayName = types.find(
(type) => type.name === 'test-visible-in-management'
);
expect(typeWithoutDisplayName !== undefined).to.eql(true);
expect(typeWithoutDisplayName!.displayName).to.eql('test-visible-in-management');
});
});
});
});
}