mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
* Modify the relationships API and UI * Remove type validation on export * Update relationship test snapshots * Change relationships table titles * Change relationships UI to share one table * Add server side logic to inject meta data into saved objects from plugins * Manually enable each type of saved object to support * Use injected vars to determine what types are import / exportable * Fix some broken tests * Remove unused translations * Fix relationships mocha tests * Remove tests that ensured types are restricted, functionality removed * Move kfetch logic into separate file * Add inAppUrl to missing types * Add tooltip to management table titles that aren't links * Make relationships screen support filtering by type * Fix failing tests * Add refresh support for inAppUrls * Add error notifications when export API call fails * Add relationship direction * Fix broken tests * Remove graph workspace from import / export * Use parent / child terminology for relationships * Use direct relationship terminology * Flip view / edit logic in saved object management app * Make config saved object redirect to advanced settings * Fix broken tests * Remove unused translations * Code cleanup * Add tests * Add fallback overwrite confirmation object title * Enforce supported types on import, export and resolve import errors * Fix broken tests * Fix broken tests pt2 * Fix broken tests pt3 * Test cleanup * Use server.decorate to access savedobjectschemas * Fix some broken tests * Fix broken tests, add new title to relationships screen * Fix some broken tests * Handle dynamic versions * Fix inAppUrl structure in tests * Re-use generic canGoInApp * Fix broken tests * Apply maps PR feedback * Apply PR feedback pt1 * Apply PR feedback pt2 * Add savedObjectsManagement to uiExports * Fix broken tests * Fix encodeURIComponent implementation * Merge 403 and unsupported type errors into single error * Apply suggestion * Remove import / exportable by default, opt-in instead * Fix type config to show up properly in the table * Change config type title and fix tests * Remove isImportableAndExportable where set to false (new default) * Remove comments referencing to authorization * Add unit tests for spaces * Add unit tests for security plugin * Change can* signature to be the same as their equivalent function, apply PR feedback * Cleanup git diff * Revert "Change can* signature to be the same as their equivalent function, apply PR feedback" This reverts commitb657ac8fc1
. * Revert "Add unit tests for security plugin" This reverts commit6287a8cecf
. * Revert "Add unit tests for spaces" This reverts commit2674a9d78f
. * Revert "Remove comments referencing to authorization" This reverts commit9618c2cc3a
. * Revert "Merge 403 and unsupported type errors into single error" This reverts commit99aea10c0f
. * Add CUSTOM_ELEMENT_TYPE for import / export * Fix broken tests * Fix broken tests pt2 * Prevent crashing app when inAppUrl is undefined
This commit is contained in:
parent
ee084c73e2
commit
5a5e0f170a
73 changed files with 3414 additions and 1170 deletions
|
@ -128,11 +128,101 @@ export default function (kibana) {
|
|||
},
|
||||
],
|
||||
|
||||
savedObjectsManagement: {
|
||||
'index-pattern': {
|
||||
icon: 'indexPatternApp',
|
||||
defaultSearchField: 'title',
|
||||
isImportableAndExportable: true,
|
||||
getTitle(obj) {
|
||||
return obj.attributes.title;
|
||||
},
|
||||
getEditUrl(obj) {
|
||||
return `/management/kibana/index_patterns/${encodeURIComponent(obj.id)}`;
|
||||
},
|
||||
getInAppUrl(obj) {
|
||||
return {
|
||||
path: `/app/kibana#/management/kibana/index_patterns/${encodeURIComponent(obj.id)}`,
|
||||
uiCapabilitiesPath: 'management.kibana.index_patterns',
|
||||
};
|
||||
},
|
||||
},
|
||||
visualization: {
|
||||
icon: 'visualizeApp',
|
||||
defaultSearchField: 'title',
|
||||
isImportableAndExportable: true,
|
||||
getTitle(obj) {
|
||||
return obj.attributes.title;
|
||||
},
|
||||
getEditUrl(obj) {
|
||||
return `/management/kibana/objects/savedVisualizations/${encodeURIComponent(obj.id)}`;
|
||||
},
|
||||
getInAppUrl(obj) {
|
||||
return {
|
||||
path: `/app/kibana#/visualize/edit/${encodeURIComponent(obj.id)}`,
|
||||
uiCapabilitiesPath: 'visualize.show',
|
||||
};
|
||||
},
|
||||
},
|
||||
search: {
|
||||
icon: 'search',
|
||||
defaultSearchField: 'title',
|
||||
isImportableAndExportable: true,
|
||||
getTitle(obj) {
|
||||
return obj.attributes.title;
|
||||
},
|
||||
getEditUrl(obj) {
|
||||
return `/management/kibana/objects/savedSearches/${encodeURIComponent(obj.id)}`;
|
||||
},
|
||||
getInAppUrl(obj) {
|
||||
return {
|
||||
path: `/app/kibana#/discover/${encodeURIComponent(obj.id)}`,
|
||||
uiCapabilitiesPath: 'discover.show',
|
||||
};
|
||||
},
|
||||
},
|
||||
dashboard: {
|
||||
icon: 'dashboardApp',
|
||||
defaultSearchField: 'title',
|
||||
isImportableAndExportable: true,
|
||||
getTitle(obj) {
|
||||
return obj.attributes.title;
|
||||
},
|
||||
getEditUrl(obj) {
|
||||
return `/management/kibana/objects/savedDashboards/${encodeURIComponent(obj.id)}`;
|
||||
},
|
||||
getInAppUrl(obj) {
|
||||
return {
|
||||
path: `/app/kibana#/dashboard/${encodeURIComponent(obj.id)}`,
|
||||
uiCapabilitiesPath: 'dashboard.show',
|
||||
};
|
||||
},
|
||||
},
|
||||
url: {
|
||||
defaultSearchField: 'url',
|
||||
isImportableAndExportable: true,
|
||||
getTitle(obj) {
|
||||
return obj.attributes.url;
|
||||
},
|
||||
},
|
||||
config: {
|
||||
isImportableAndExportable: true,
|
||||
getInAppUrl() {
|
||||
return {
|
||||
path: `/app/kibana#/management/kibana/settings`,
|
||||
uiCapabilitiesPath: 'advancedSettings.show',
|
||||
};
|
||||
},
|
||||
getTitle(obj) {
|
||||
return `Advanced Settings [${obj.id}]`;
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
savedObjectSchemas: {
|
||||
'kql-telemetry': {
|
||||
'sample-data-telemetry': {
|
||||
isNamespaceAgnostic: true,
|
||||
},
|
||||
'sample-data-telemetry': {
|
||||
'kql-telemetry': {
|
||||
isNamespaceAgnostic: true,
|
||||
},
|
||||
},
|
||||
|
@ -169,6 +259,7 @@ export default function (kibana) {
|
|||
index_patterns: true,
|
||||
},
|
||||
advancedSettings: {
|
||||
show: true,
|
||||
save: true
|
||||
},
|
||||
indexPatterns: {
|
||||
|
|
|
@ -26,11 +26,17 @@ export function injectVars(server) {
|
|||
// If url is set, old settings must be used for backward compatibility
|
||||
const isOverridden = typeof tilemap.url === 'string' && tilemap.url !== '';
|
||||
|
||||
// Get types that are import and exportable, by default yes unless isImportableAndExportable is set to false
|
||||
const { types: allTypes } = server.savedObjects;
|
||||
const savedObjectsManagement = server.getSavedObjectsManagement();
|
||||
const importAndExportableTypes = allTypes.filter(type => savedObjectsManagement.isImportAndExportable(type));
|
||||
|
||||
return {
|
||||
kbnDefaultAppId: serverConfig.get('kibana.defaultAppId'),
|
||||
disableWelcomeScreen: serverConfig.get('kibana.disableWelcomeScreen'),
|
||||
regionmapsConfig: regionmap,
|
||||
mapConfig: mapConfig,
|
||||
importAndExportableTypes,
|
||||
tilemapsConfig: {
|
||||
deprecated: {
|
||||
isOverridden: isOverridden,
|
||||
|
|
|
@ -21,20 +21,19 @@ import { savedObjectManagementRegistry } from '../../saved_object_registry';
|
|||
import objectIndexHTML from './_objects.html';
|
||||
import uiRoutes from 'ui/routes';
|
||||
import chrome from 'ui/chrome';
|
||||
import { toastNotifications } from 'ui/notify';
|
||||
import { SavedObjectsClientProvider } from 'ui/saved_objects';
|
||||
import { uiModules } from 'ui/modules';
|
||||
import React from 'react';
|
||||
import { render, unmountComponentAtNode } from 'react-dom';
|
||||
import { ObjectsTable } from './components/objects_table';
|
||||
import { canViewInApp, getInAppUrl } from './lib/in_app_url';
|
||||
import { I18nContext } from 'ui/i18n';
|
||||
import { get } from 'lodash';
|
||||
|
||||
import { getIndexBreadcrumbs } from './breadcrumbs';
|
||||
|
||||
const REACT_OBJECTS_TABLE_DOM_ELEMENT_ID = 'reactSavedObjectsTable';
|
||||
|
||||
function updateObjectsTable($scope, $injector, i18n) {
|
||||
function updateObjectsTable($scope, $injector) {
|
||||
const Private = $injector.get('Private');
|
||||
const indexPatterns = $injector.get('indexPatterns');
|
||||
const $http = $injector.get('$http');
|
||||
|
@ -44,10 +43,6 @@ function updateObjectsTable($scope, $injector, i18n) {
|
|||
|
||||
const savedObjectsClient = Private(SavedObjectsClientProvider);
|
||||
const services = savedObjectManagementRegistry.all().map(obj => $injector.get(obj.service));
|
||||
const allServices = savedObjectManagementRegistry.all();
|
||||
const typeToServiceName = type => allServices.reduce((serviceName, service) => {
|
||||
return service.title.includes(type) ? service.service : serviceName;
|
||||
}, null);
|
||||
|
||||
$scope.$$postDigest(() => {
|
||||
const node = document.getElementById(REACT_OBJECTS_TABLE_DOM_ELEMENT_ID);
|
||||
|
@ -66,27 +61,15 @@ function updateObjectsTable($scope, $injector, i18n) {
|
|||
basePath={chrome.getBasePath()}
|
||||
newIndexPatternUrl={kbnUrl.eval('#/management/kibana/index_pattern')}
|
||||
uiCapabilities={uiCapabilites}
|
||||
getEditUrl={(id, type) => {
|
||||
if (type === 'index-pattern' || type === 'indexPatterns') {
|
||||
return kbnUrl.eval(`#/management/kibana/index_patterns/${id}`);
|
||||
goInspectObject={object => {
|
||||
if (object.meta.editUrl) {
|
||||
kbnUrl.change(object.meta.editUrl);
|
||||
$scope.$apply();
|
||||
}
|
||||
const serviceName = typeToServiceName(type);
|
||||
if (!serviceName) {
|
||||
toastNotifications.addWarning(i18n('kbn.management.objects.unknownSavedObjectTypeNotificationMessage', {
|
||||
defaultMessage: 'Unknown saved object type: {type}',
|
||||
values: { type }
|
||||
}));
|
||||
return null;
|
||||
}
|
||||
|
||||
return kbnUrl.eval(`#/management/kibana/objects/${serviceName}/${id}`);
|
||||
}}
|
||||
canGoInApp={(type) => {
|
||||
return canViewInApp(uiCapabilites, type);
|
||||
}}
|
||||
goInApp={(id, type) => {
|
||||
kbnUrl.change(getInAppUrl(id, type));
|
||||
$scope.$apply();
|
||||
canGoInApp={object => {
|
||||
const { inAppUrl } = object.meta;
|
||||
return inAppUrl && get(uiCapabilites, inAppUrl.uiCapabilitiesPath);
|
||||
}}
|
||||
/>
|
||||
</I18nContext>,
|
||||
|
@ -114,8 +97,8 @@ uiModules.get('apps/management')
|
|||
return {
|
||||
restrict: 'E',
|
||||
controllerAs: 'managementObjectsController',
|
||||
controller: function ($scope, $injector, i18n) {
|
||||
updateObjectsTable($scope, $injector, i18n);
|
||||
controller: function ($scope, $injector) {
|
||||
updateObjectsTable($scope, $injector);
|
||||
$scope.$on('$destroy', destroyObjectsTable);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -49,7 +49,7 @@ exports[`ObjectsTable delete should show a confirm modal 1`] = `
|
|||
"name": "Id",
|
||||
},
|
||||
Object {
|
||||
"field": "title",
|
||||
"field": "meta.title",
|
||||
"name": "Title",
|
||||
},
|
||||
]
|
||||
|
@ -220,14 +220,24 @@ exports[`ObjectsTable import should show the flyout 1`] = `
|
|||
|
||||
exports[`ObjectsTable relationships should show the flyout 1`] = `
|
||||
<InjectIntl(RelationshipsUI)
|
||||
canGoInApp={[Function]}
|
||||
close={[Function]}
|
||||
getEditUrl={[Function]}
|
||||
getRelationships={[Function]}
|
||||
goInApp={[Function]}
|
||||
id="1"
|
||||
title="MySearch"
|
||||
type="search"
|
||||
goInspectObject={[Function]}
|
||||
savedObject={
|
||||
Object {
|
||||
"id": "2",
|
||||
"meta": Object {
|
||||
"editUrl": "#/management/kibana/objects/savedSearches/2",
|
||||
"icon": "search",
|
||||
"inAppUrl": Object {
|
||||
"path": "/discover/2",
|
||||
"uiCapabilitiesPath": "discover.show",
|
||||
},
|
||||
"title": "MySearch",
|
||||
},
|
||||
"type": "search",
|
||||
}
|
||||
}
|
||||
/>
|
||||
`;
|
||||
|
||||
|
@ -247,7 +257,6 @@ exports[`ObjectsTable should render normally 1`] = `
|
|||
/>
|
||||
<InjectIntl(TableUI)
|
||||
canDeleteSavedObjectTypes={Array []}
|
||||
canGoInApp={[Function]}
|
||||
filterOptions={
|
||||
Array [
|
||||
Object {
|
||||
|
@ -272,34 +281,61 @@ exports[`ObjectsTable should render normally 1`] = `
|
|||
},
|
||||
]
|
||||
}
|
||||
getEditUrl={[Function]}
|
||||
goInApp={[Function]}
|
||||
goInspectObject={[Function]}
|
||||
isSearching={false}
|
||||
itemId="id"
|
||||
items={
|
||||
Array [
|
||||
Object {
|
||||
"icon": "indexPatternApp",
|
||||
"id": "1",
|
||||
"title": "MyIndexPattern*",
|
||||
"meta": Object {
|
||||
"editUrl": "#/management/kibana/index_patterns/1",
|
||||
"icon": "indexPatternApp",
|
||||
"inAppUrl": Object {
|
||||
"path": "/management/kibana/index_patterns/1",
|
||||
"uiCapabilitiesPath": "management.kibana.index_patterns",
|
||||
},
|
||||
"title": "MyIndexPattern*",
|
||||
},
|
||||
"type": "index-pattern",
|
||||
},
|
||||
Object {
|
||||
"icon": "search",
|
||||
"id": "2",
|
||||
"title": "MySearch",
|
||||
"meta": Object {
|
||||
"editUrl": "#/management/kibana/objects/savedSearches/2",
|
||||
"icon": "search",
|
||||
"inAppUrl": Object {
|
||||
"path": "/discover/2",
|
||||
"uiCapabilitiesPath": "discover.show",
|
||||
},
|
||||
"title": "MySearch",
|
||||
},
|
||||
"type": "search",
|
||||
},
|
||||
Object {
|
||||
"icon": "dashboardApp",
|
||||
"id": "3",
|
||||
"title": "MyDashboard",
|
||||
"meta": Object {
|
||||
"editUrl": "#/management/kibana/objects/savedDashboards/3",
|
||||
"icon": "dashboardApp",
|
||||
"inAppUrl": Object {
|
||||
"path": "/dashboard/3",
|
||||
"uiCapabilitiesPath": "dashboard.show",
|
||||
},
|
||||
"title": "MyDashboard",
|
||||
},
|
||||
"type": "dashboard",
|
||||
},
|
||||
Object {
|
||||
"icon": "visualizeApp",
|
||||
"id": "4",
|
||||
"title": "MyViz",
|
||||
"meta": Object {
|
||||
"editUrl": "#/management/kibana/objects/savedVisualizations/4",
|
||||
"icon": "visualizeApp",
|
||||
"inAppUrl": Object {
|
||||
"path": "/visualize/edit/4",
|
||||
"uiCapabilitiesPath": "visualize.show",
|
||||
},
|
||||
"title": "MyViz",
|
||||
},
|
||||
"type": "visualization",
|
||||
},
|
||||
]
|
||||
|
|
|
@ -23,9 +23,14 @@ import { shallowWithIntl } from 'test_utils/enzyme_helpers';
|
|||
import { ObjectsTable, POSSIBLE_TYPES } from '../objects_table';
|
||||
import { Flyout } from '../components/flyout/';
|
||||
import { Relationships } from '../components/relationships/';
|
||||
import { findObjects } from '../../../lib';
|
||||
|
||||
jest.mock('ui/kfetch', () => ({ kfetch: jest.fn() }));
|
||||
|
||||
jest.mock('../../../lib/find_objects', () => ({
|
||||
findObjects: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../components/header', () => ({
|
||||
Header: () => 'Header',
|
||||
}));
|
||||
|
@ -44,7 +49,8 @@ jest.mock('ui/errors', () => ({
|
|||
}));
|
||||
|
||||
jest.mock('ui/chrome', () => ({
|
||||
addBasePath: () => ''
|
||||
addBasePath: () => '',
|
||||
getInjected: () => ['index-pattern', 'visualization', 'dashboard', 'search'],
|
||||
}));
|
||||
|
||||
jest.mock('../../../lib/fetch_export_objects', () => ({
|
||||
|
@ -110,29 +116,10 @@ const allSavedObjects = [
|
|||
const $http = () => {};
|
||||
$http.post = jest.fn().mockImplementation(() => ([]));
|
||||
const defaultProps = {
|
||||
goInspectObject: () => {},
|
||||
savedObjectsClient: {
|
||||
find: jest.fn().mockImplementation(({ type }) => {
|
||||
// We pass in a single type when fetching counts
|
||||
if (type && !Array.isArray(type)) {
|
||||
return {
|
||||
total: 1,
|
||||
savedObjects: [
|
||||
{
|
||||
id: '1',
|
||||
type,
|
||||
attributes: {
|
||||
title: `Title${type}`
|
||||
}
|
||||
},
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
total: allSavedObjects.length,
|
||||
savedObjects: allSavedObjects,
|
||||
};
|
||||
}),
|
||||
find: jest.fn(),
|
||||
bulkGet: jest.fn(),
|
||||
},
|
||||
indexPatterns: {
|
||||
cache: {
|
||||
|
@ -144,9 +131,6 @@ const defaultProps = {
|
|||
newIndexPatternUrl: '',
|
||||
kbnIndex: '',
|
||||
services: [],
|
||||
getEditUrl: () => {},
|
||||
canGoInApp: () => {},
|
||||
goInApp: () => {},
|
||||
uiCapabilities: {
|
||||
savedObjectsManagement: {
|
||||
'index-pattern': {
|
||||
|
@ -171,6 +155,66 @@ const defaultProps = {
|
|||
]
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
findObjects.mockImplementation(() => ({
|
||||
total: 4,
|
||||
savedObjects: [
|
||||
{
|
||||
id: '1',
|
||||
type: 'index-pattern',
|
||||
meta: {
|
||||
title: `MyIndexPattern*`,
|
||||
icon: 'indexPatternApp',
|
||||
editUrl: '#/management/kibana/index_patterns/1',
|
||||
inAppUrl: {
|
||||
path: '/management/kibana/index_patterns/1',
|
||||
uiCapabilitiesPath: 'management.kibana.index_patterns',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: 'search',
|
||||
meta: {
|
||||
title: `MySearch`,
|
||||
icon: 'search',
|
||||
editUrl: '#/management/kibana/objects/savedSearches/2',
|
||||
inAppUrl: {
|
||||
path: '/discover/2',
|
||||
uiCapabilitiesPath: 'discover.show',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
type: 'dashboard',
|
||||
meta: {
|
||||
title: `MyDashboard`,
|
||||
icon: 'dashboardApp',
|
||||
editUrl: '#/management/kibana/objects/savedDashboards/3',
|
||||
inAppUrl: {
|
||||
path: '/dashboard/3',
|
||||
uiCapabilitiesPath: 'dashboard.show',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
type: 'visualization',
|
||||
meta: {
|
||||
title: `MyViz`,
|
||||
icon: 'visualizeApp',
|
||||
editUrl: '#/management/kibana/objects/savedVisualizations/4',
|
||||
inAppUrl: {
|
||||
path: '/visualize/edit/4',
|
||||
uiCapabilitiesPath: 'visualize.show',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}));
|
||||
});
|
||||
|
||||
let addDangerMock;
|
||||
let addSuccessMock;
|
||||
|
||||
|
@ -209,15 +253,12 @@ describe('ObjectsTable', () => {
|
|||
});
|
||||
|
||||
it('should add danger toast when find fails', async () => {
|
||||
const savedObjectsClientWithFindError = {
|
||||
find: () => {
|
||||
throw new Error('Simulated find error');
|
||||
}
|
||||
};
|
||||
const customizedProps = { ...defaultProps, savedObjectsClient: savedObjectsClientWithFindError };
|
||||
findObjects.mockImplementation(() => {
|
||||
throw new Error('Simulated find error');
|
||||
});
|
||||
const component = shallowWithIntl(
|
||||
<ObjectsTable.WrappedComponent
|
||||
{...customizedProps}
|
||||
{...defaultProps}
|
||||
perPageConfig={15}
|
||||
/>
|
||||
);
|
||||
|
@ -260,7 +301,7 @@ describe('ObjectsTable', () => {
|
|||
// Ensure the state changes are reflected
|
||||
component.update();
|
||||
|
||||
expect(defaultProps.savedObjectsClient.find).toHaveBeenCalledWith(expect.objectContaining({
|
||||
expect(findObjects).toHaveBeenCalledWith(expect.objectContaining({
|
||||
type: ['search']
|
||||
}));
|
||||
});
|
||||
|
@ -458,13 +499,35 @@ describe('ObjectsTable', () => {
|
|||
// Ensure the state changes are reflected
|
||||
component.update();
|
||||
|
||||
component.instance().onShowRelationships('1', 'search', 'MySearch');
|
||||
component.instance().onShowRelationships({
|
||||
id: '2',
|
||||
type: 'search',
|
||||
meta: {
|
||||
title: `MySearch`,
|
||||
icon: 'search',
|
||||
editUrl: '#/management/kibana/objects/savedSearches/2',
|
||||
inAppUrl: {
|
||||
path: '/discover/2',
|
||||
uiCapabilitiesPath: 'discover.show',
|
||||
},
|
||||
},
|
||||
});
|
||||
component.update();
|
||||
|
||||
expect(component.find(Relationships)).toMatchSnapshot();
|
||||
expect(component.state('relationshipId')).toBe('1');
|
||||
expect(component.state('relationshipType')).toBe('search');
|
||||
expect(component.state('relationshipTitle')).toBe('MySearch');
|
||||
expect(component.state('relationshipObject')).toEqual({
|
||||
id: '2',
|
||||
type: 'search',
|
||||
meta: {
|
||||
title: 'MySearch',
|
||||
editUrl: '#/management/kibana/objects/savedSearches/2',
|
||||
icon: 'search',
|
||||
inAppUrl: {
|
||||
path: '/discover/2',
|
||||
uiCapabilitiesPath: 'discover.show',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should hide the flyout', async () => {
|
||||
|
|
|
@ -273,6 +273,37 @@ exports[`Flyout conflicts should handle errors 1`] = `
|
|||
</EuiCallOut>
|
||||
`;
|
||||
|
||||
exports[`Flyout errors should display unsupported type errors properly 1`] = `
|
||||
<EuiCallOut
|
||||
color="warning"
|
||||
iconType="help"
|
||||
size="m"
|
||||
title={
|
||||
<FormattedMessage
|
||||
defaultMessage="Import failed"
|
||||
id="kbn.management.objects.objectsTable.flyout.importFailedTitle"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="Failed to import {failedImportCount} of {totalImportCount} objects. Import failed"
|
||||
id="kbn.management.objects.objectsTable.flyout.importFailedDescription"
|
||||
values={
|
||||
Object {
|
||||
"failedImportCount": 1,
|
||||
"totalImportCount": 1,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
wigwags [id=1] unsupported type
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
`;
|
||||
|
||||
exports[`Flyout legacy conflicts should allow conflict resolution 1`] = `
|
||||
<EuiFlyout
|
||||
closeButtonAriaLabel="Closes this dialog"
|
||||
|
|
|
@ -47,6 +47,7 @@ jest.mock('../../../../../lib/resolve_import_errors', () => ({
|
|||
|
||||
jest.mock('ui/chrome', () => ({
|
||||
addBasePath: () => {},
|
||||
getInjected: () => ['index-pattern', 'visualization', 'dashboard', 'search'],
|
||||
}));
|
||||
|
||||
jest.mock('../../../../../lib/import_legacy_file', () => ({
|
||||
|
@ -312,6 +313,75 @@ describe('Flyout', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('errors', () => {
|
||||
const { importFile } = require('../../../../../lib/import_file');
|
||||
const { resolveImportErrors } = require('../../../../../lib/resolve_import_errors');
|
||||
|
||||
it('should display unsupported type errors properly', async () => {
|
||||
const component = shallowWithIntl(<Flyout.WrappedComponent {...defaultProps} />);
|
||||
|
||||
// Ensure all promises resolve
|
||||
await Promise.resolve();
|
||||
// Ensure the state changes are reflected
|
||||
component.update();
|
||||
|
||||
importFile.mockImplementation(() => ({
|
||||
success: false,
|
||||
successCount: 0,
|
||||
errors: [
|
||||
{
|
||||
id: '1',
|
||||
type: 'wigwags',
|
||||
title: 'My Title',
|
||||
error: {
|
||||
type: 'unsupported_type',
|
||||
}
|
||||
},
|
||||
],
|
||||
}));
|
||||
resolveImportErrors.mockImplementation(() => ({
|
||||
status: 'success',
|
||||
importCount: 0,
|
||||
failedImports: [
|
||||
{
|
||||
error: {
|
||||
type: 'unsupported_type',
|
||||
},
|
||||
obj: {
|
||||
id: '1',
|
||||
type: 'wigwags',
|
||||
title: 'My Title',
|
||||
},
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
component.setState({ file: mockFile, isLegacyFile: false });
|
||||
|
||||
// Go through the import flow
|
||||
await component.instance().import();
|
||||
component.update();
|
||||
|
||||
// Ensure all promises resolve
|
||||
await Promise.resolve();
|
||||
|
||||
expect(component.state('status')).toBe('success');
|
||||
expect(component.state('failedImports')).toEqual([
|
||||
{
|
||||
error: {
|
||||
type: 'unsupported_type',
|
||||
},
|
||||
obj: {
|
||||
id: '1',
|
||||
type: 'wigwags',
|
||||
title: 'My Title',
|
||||
},
|
||||
},
|
||||
]);
|
||||
expect(component.find('EuiFlyout EuiCallOut')).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('legacy conflicts', () => {
|
||||
const { importLegacyFile } = require('../../../../../lib/import_legacy_file');
|
||||
const {
|
||||
|
|
|
@ -51,6 +51,7 @@ import {
|
|||
resolveImportErrors,
|
||||
logLegacyImport,
|
||||
processImportResponse,
|
||||
getDefaultTitle,
|
||||
} from '../../../../lib';
|
||||
import {
|
||||
resolveSavedObjects,
|
||||
|
@ -600,6 +601,17 @@ class FlyoutUI extends Component {
|
|||
}
|
||||
);
|
||||
});
|
||||
} else if (error.type === 'unsupported_type') {
|
||||
return intl.formatMessage(
|
||||
{
|
||||
id: 'kbn.management.objects.objectsTable.flyout.importFailedUnsupportedType',
|
||||
defaultMessage: '{type} [id={id}] unsupported type',
|
||||
},
|
||||
{
|
||||
id: obj.id,
|
||||
type: obj.type,
|
||||
},
|
||||
);
|
||||
}
|
||||
return getField(error, 'body.message', error.message || '');
|
||||
}).join(' ')}
|
||||
|
@ -887,7 +899,9 @@ class FlyoutUI extends Component {
|
|||
<FormattedMessage
|
||||
id="kbn.management.objects.objectsTable.flyout.confirmOverwriteBody"
|
||||
defaultMessage="Are you sure you want to overwrite {title}?"
|
||||
values={{ title: this.state.conflictingRecord.title }}
|
||||
values={{
|
||||
title: this.state.conflictingRecord.title || getDefaultTitle(this.state.conflictingRecord)
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</EuiConfirmModal>
|
||||
|
|
|
@ -33,55 +33,53 @@ exports[`Relationships should render dashboards normally 1`] = `
|
|||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
<EuiDescriptionList>
|
||||
<EuiDescriptionListTitle
|
||||
style={
|
||||
Object {
|
||||
"marginBottom": "1rem",
|
||||
}
|
||||
}
|
||||
<div>
|
||||
<EuiCallOut
|
||||
color="primary"
|
||||
size="m"
|
||||
>
|
||||
<EuiCallOut
|
||||
color="success"
|
||||
size="m"
|
||||
title={
|
||||
<FormattedMessage
|
||||
defaultMessage="Dashboard"
|
||||
id="kbn.management.objects.objectsTable.relationships.dashboard.calloutTitle"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="Here are some visualizations used on this dashboard. You can safely delete this dashboard and the visualizations will still work properly."
|
||||
id="kbn.management.objects.objectsTable.relationships.dashboard.calloutText"
|
||||
values={Object {}}
|
||||
/>
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
</EuiDescriptionListTitle>
|
||||
<p>
|
||||
Here are the saved objects related to MyDashboard. Deleting this dashboard affects its parent objects, but not its children.
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
<EuiSpacer />
|
||||
<EuiInMemoryTable
|
||||
columns={
|
||||
Array [
|
||||
Object {
|
||||
"align": "center",
|
||||
"description": "Type of the saved object",
|
||||
"field": "type",
|
||||
"name": "Type",
|
||||
"render": [Function],
|
||||
"width": "24px",
|
||||
"sortable": false,
|
||||
"width": "50px",
|
||||
},
|
||||
Object {
|
||||
"field": "title",
|
||||
"dataType": "string",
|
||||
"field": "relationship",
|
||||
"name": "Direct relationship",
|
||||
"render": [Function],
|
||||
"sortable": false,
|
||||
"width": "125px",
|
||||
},
|
||||
Object {
|
||||
"dataType": "string",
|
||||
"description": "Title of the saved object",
|
||||
"field": "meta.title",
|
||||
"name": "Title",
|
||||
"render": [Function],
|
||||
"sortable": false,
|
||||
},
|
||||
Object {
|
||||
"actions": Array [
|
||||
Object {
|
||||
"available": [Function],
|
||||
"description": "View this saved object within Kibana",
|
||||
"icon": "eye",
|
||||
"name": "In app",
|
||||
"description": "Inspect this saved object",
|
||||
"icon": "inspect",
|
||||
"name": "Inspect",
|
||||
"onClick": [Function],
|
||||
"testId": "savedObjectsManagementRelationshipsViewInApp",
|
||||
"type": "icon",
|
||||
},
|
||||
],
|
||||
"name": "Actions",
|
||||
|
@ -93,17 +91,76 @@ exports[`Relationships should render dashboards normally 1`] = `
|
|||
Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"meta": Object {
|
||||
"editUrl": "/management/kibana/objects/savedVisualizations/1",
|
||||
"icon": "visualizeApp",
|
||||
"inAppUrl": Object {
|
||||
"path": "/app/kibana#/visualize/edit/1",
|
||||
"uiCapabilitiesPath": "visualize.show",
|
||||
},
|
||||
"title": "My Visualization Title 1",
|
||||
},
|
||||
"relationship": "child",
|
||||
"type": "visualization",
|
||||
},
|
||||
Object {
|
||||
"id": "2",
|
||||
"meta": Object {
|
||||
"editUrl": "/management/kibana/objects/savedVisualizations/2",
|
||||
"icon": "visualizeApp",
|
||||
"inAppUrl": Object {
|
||||
"path": "/app/kibana#/visualize/edit/2",
|
||||
"uiCapabilitiesPath": "visualize.show",
|
||||
},
|
||||
"title": "My Visualization Title 2",
|
||||
},
|
||||
"relationship": "child",
|
||||
"type": "visualization",
|
||||
},
|
||||
]
|
||||
}
|
||||
pagination={true}
|
||||
responsive={true}
|
||||
search={
|
||||
Object {
|
||||
"filters": Array [
|
||||
Object {
|
||||
"field": "relationship",
|
||||
"multiSelect": "or",
|
||||
"name": "Direct relationship",
|
||||
"options": Array [
|
||||
Object {
|
||||
"name": "parent",
|
||||
"value": "parent",
|
||||
"view": "Parent",
|
||||
},
|
||||
Object {
|
||||
"name": "child",
|
||||
"value": "child",
|
||||
"view": "Child",
|
||||
},
|
||||
],
|
||||
"type": "field_value_selection",
|
||||
},
|
||||
Object {
|
||||
"field": "type",
|
||||
"multiSelect": "or",
|
||||
"name": "Type",
|
||||
"options": Array [
|
||||
Object {
|
||||
"name": "visualization",
|
||||
"value": "visualization",
|
||||
"view": "visualization",
|
||||
},
|
||||
],
|
||||
"type": "field_value_selection",
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
sorting={false}
|
||||
/>
|
||||
</EuiDescriptionList>
|
||||
</div>
|
||||
</EuiFlyoutBody>
|
||||
</EuiFlyout>
|
||||
`;
|
||||
|
@ -191,49 +248,53 @@ exports[`Relationships should render index patterns normally 1`] = `
|
|||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
<EuiDescriptionList>
|
||||
<EuiDescriptionListTitle
|
||||
style={
|
||||
Object {
|
||||
"marginBottom": "1rem",
|
||||
}
|
||||
}
|
||||
<div>
|
||||
<EuiCallOut
|
||||
color="primary"
|
||||
size="m"
|
||||
>
|
||||
<EuiCallOut
|
||||
color="warning"
|
||||
size="m"
|
||||
title={
|
||||
<FormattedMessage
|
||||
defaultMessage="Warning"
|
||||
id="kbn.management.objects.objectsTable.relationships.warningTitle"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<p />
|
||||
</EuiCallOut>
|
||||
</EuiDescriptionListTitle>
|
||||
<p>
|
||||
Here are the saved objects related to MyIndexPattern*. Deleting this index-pattern affects its parent objects, but not its children.
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
<EuiSpacer />
|
||||
<EuiInMemoryTable
|
||||
columns={
|
||||
Array [
|
||||
Object {
|
||||
"align": "center",
|
||||
"description": "Type of the saved object",
|
||||
"field": "type",
|
||||
"name": "Type",
|
||||
"render": [Function],
|
||||
"width": "24px",
|
||||
"sortable": false,
|
||||
"width": "50px",
|
||||
},
|
||||
Object {
|
||||
"field": "title",
|
||||
"dataType": "string",
|
||||
"field": "relationship",
|
||||
"name": "Direct relationship",
|
||||
"render": [Function],
|
||||
"sortable": false,
|
||||
"width": "125px",
|
||||
},
|
||||
Object {
|
||||
"dataType": "string",
|
||||
"description": "Title of the saved object",
|
||||
"field": "meta.title",
|
||||
"name": "Title",
|
||||
"render": [Function],
|
||||
"sortable": false,
|
||||
},
|
||||
Object {
|
||||
"actions": Array [
|
||||
Object {
|
||||
"available": [Function],
|
||||
"description": "View this saved object within Kibana",
|
||||
"icon": "eye",
|
||||
"name": "In app",
|
||||
"description": "Inspect this saved object",
|
||||
"icon": "inspect",
|
||||
"name": "Inspect",
|
||||
"onClick": [Function],
|
||||
"testId": "savedObjectsManagementRelationshipsViewInApp",
|
||||
"type": "icon",
|
||||
},
|
||||
],
|
||||
"name": "Actions",
|
||||
|
@ -245,74 +306,81 @@ exports[`Relationships should render index patterns normally 1`] = `
|
|||
Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
},
|
||||
]
|
||||
}
|
||||
pagination={true}
|
||||
responsive={true}
|
||||
sorting={false}
|
||||
/>
|
||||
<EuiDescriptionListTitle
|
||||
style={
|
||||
Object {
|
||||
"marginBottom": "1rem",
|
||||
}
|
||||
}
|
||||
>
|
||||
<EuiCallOut
|
||||
color="warning"
|
||||
size="m"
|
||||
title={
|
||||
<FormattedMessage
|
||||
defaultMessage="Warning"
|
||||
id="kbn.management.objects.objectsTable.relationships.warningTitle"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<p />
|
||||
</EuiCallOut>
|
||||
</EuiDescriptionListTitle>
|
||||
<EuiInMemoryTable
|
||||
columns={
|
||||
Array [
|
||||
Object {
|
||||
"render": [Function],
|
||||
"width": "24px",
|
||||
},
|
||||
Object {
|
||||
"field": "title",
|
||||
"name": "Title",
|
||||
"render": [Function],
|
||||
},
|
||||
Object {
|
||||
"actions": Array [
|
||||
Object {
|
||||
"available": [Function],
|
||||
"description": "View this saved object within Kibana",
|
||||
"icon": "eye",
|
||||
"name": "In app",
|
||||
"onClick": [Function],
|
||||
"testId": "savedObjectsManagementRelationshipsViewInApp",
|
||||
"meta": Object {
|
||||
"editUrl": "/management/kibana/objects/savedSearches/1",
|
||||
"icon": "search",
|
||||
"inAppUrl": Object {
|
||||
"path": "/app/kibana#/discover/1",
|
||||
"uiCapabilitiesPath": "discover.show",
|
||||
},
|
||||
],
|
||||
"name": "Actions",
|
||||
"title": "My Search Title",
|
||||
},
|
||||
"relationship": "parent",
|
||||
"type": "search",
|
||||
},
|
||||
]
|
||||
}
|
||||
executeQueryOptions={Object {}}
|
||||
items={
|
||||
Array [
|
||||
Object {
|
||||
"id": "2",
|
||||
"meta": Object {
|
||||
"editUrl": "/management/kibana/objects/savedVisualizations/2",
|
||||
"icon": "visualizeApp",
|
||||
"inAppUrl": Object {
|
||||
"path": "/app/kibana#/visualize/edit/2",
|
||||
"uiCapabilitiesPath": "visualize.show",
|
||||
},
|
||||
"title": "My Visualization Title",
|
||||
},
|
||||
"relationship": "parent",
|
||||
"type": "visualization",
|
||||
},
|
||||
]
|
||||
}
|
||||
pagination={true}
|
||||
responsive={true}
|
||||
search={
|
||||
Object {
|
||||
"filters": Array [
|
||||
Object {
|
||||
"field": "relationship",
|
||||
"multiSelect": "or",
|
||||
"name": "Direct relationship",
|
||||
"options": Array [
|
||||
Object {
|
||||
"name": "parent",
|
||||
"value": "parent",
|
||||
"view": "Parent",
|
||||
},
|
||||
Object {
|
||||
"name": "child",
|
||||
"value": "child",
|
||||
"view": "Child",
|
||||
},
|
||||
],
|
||||
"type": "field_value_selection",
|
||||
},
|
||||
Object {
|
||||
"field": "type",
|
||||
"multiSelect": "or",
|
||||
"name": "Type",
|
||||
"options": Array [
|
||||
Object {
|
||||
"name": "search",
|
||||
"value": "search",
|
||||
"view": "search",
|
||||
},
|
||||
Object {
|
||||
"name": "visualization",
|
||||
"value": "visualization",
|
||||
"view": "visualization",
|
||||
},
|
||||
],
|
||||
"type": "field_value_selection",
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
sorting={false}
|
||||
/>
|
||||
</EuiDescriptionList>
|
||||
</div>
|
||||
</EuiFlyoutBody>
|
||||
</EuiFlyout>
|
||||
`;
|
||||
|
@ -350,55 +418,53 @@ exports[`Relationships should render searches normally 1`] = `
|
|||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
<EuiDescriptionList>
|
||||
<EuiDescriptionListTitle
|
||||
style={
|
||||
Object {
|
||||
"marginBottom": "1rem",
|
||||
}
|
||||
}
|
||||
<div>
|
||||
<EuiCallOut
|
||||
color="primary"
|
||||
size="m"
|
||||
>
|
||||
<EuiCallOut
|
||||
color="success"
|
||||
size="m"
|
||||
title={
|
||||
<FormattedMessage
|
||||
defaultMessage="Saved Search"
|
||||
id="kbn.management.objects.objectsTable.relationships.search.calloutTitle"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="Here is the index pattern tied to this saved search."
|
||||
id="kbn.management.objects.objectsTable.relationships.search.calloutText"
|
||||
values={Object {}}
|
||||
/>
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
</EuiDescriptionListTitle>
|
||||
<p>
|
||||
Here are the saved objects related to MySearch. Deleting this search affects its parent objects, but not its children.
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
<EuiSpacer />
|
||||
<EuiInMemoryTable
|
||||
columns={
|
||||
Array [
|
||||
Object {
|
||||
"align": "center",
|
||||
"description": "Type of the saved object",
|
||||
"field": "type",
|
||||
"name": "Type",
|
||||
"render": [Function],
|
||||
"width": "24px",
|
||||
"sortable": false,
|
||||
"width": "50px",
|
||||
},
|
||||
Object {
|
||||
"field": "title",
|
||||
"dataType": "string",
|
||||
"field": "relationship",
|
||||
"name": "Direct relationship",
|
||||
"render": [Function],
|
||||
"sortable": false,
|
||||
"width": "125px",
|
||||
},
|
||||
Object {
|
||||
"dataType": "string",
|
||||
"description": "Title of the saved object",
|
||||
"field": "meta.title",
|
||||
"name": "Title",
|
||||
"render": [Function],
|
||||
"sortable": false,
|
||||
},
|
||||
Object {
|
||||
"actions": Array [
|
||||
Object {
|
||||
"available": [Function],
|
||||
"description": "View this saved object within Kibana",
|
||||
"icon": "eye",
|
||||
"name": "In app",
|
||||
"description": "Inspect this saved object",
|
||||
"icon": "inspect",
|
||||
"name": "Inspect",
|
||||
"onClick": [Function],
|
||||
"testId": "savedObjectsManagementRelationshipsViewInApp",
|
||||
"type": "icon",
|
||||
},
|
||||
],
|
||||
"name": "Actions",
|
||||
|
@ -410,80 +476,81 @@ exports[`Relationships should render searches normally 1`] = `
|
|||
Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
},
|
||||
]
|
||||
}
|
||||
pagination={true}
|
||||
responsive={true}
|
||||
sorting={false}
|
||||
/>
|
||||
<EuiDescriptionListTitle
|
||||
style={
|
||||
Object {
|
||||
"marginBottom": "1rem",
|
||||
}
|
||||
}
|
||||
>
|
||||
<EuiCallOut
|
||||
color="warning"
|
||||
size="m"
|
||||
title={
|
||||
<FormattedMessage
|
||||
defaultMessage="Warning"
|
||||
id="kbn.management.objects.objectsTable.relationships.warningTitle"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="Here are some visualizations that use this saved search. If you delete this saved search, these visualizations will not longer work properly."
|
||||
id="kbn.management.objects.objectsTable.relationships.search.visualizations.calloutText"
|
||||
values={Object {}}
|
||||
/>
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
</EuiDescriptionListTitle>
|
||||
<EuiInMemoryTable
|
||||
columns={
|
||||
Array [
|
||||
Object {
|
||||
"render": [Function],
|
||||
"width": "24px",
|
||||
},
|
||||
Object {
|
||||
"field": "title",
|
||||
"name": "Title",
|
||||
"render": [Function],
|
||||
},
|
||||
Object {
|
||||
"actions": Array [
|
||||
Object {
|
||||
"available": [Function],
|
||||
"description": "View this saved object within Kibana",
|
||||
"icon": "eye",
|
||||
"name": "In app",
|
||||
"onClick": [Function],
|
||||
"testId": "savedObjectsManagementRelationshipsViewInApp",
|
||||
"meta": Object {
|
||||
"editUrl": "/management/kibana/index_patterns/1",
|
||||
"icon": "indexPatternApp",
|
||||
"inAppUrl": Object {
|
||||
"path": "/app/kibana#/management/kibana/index_patterns/1",
|
||||
"uiCapabilitiesPath": "management.kibana.index_patterns",
|
||||
},
|
||||
],
|
||||
"name": "Actions",
|
||||
"title": "My Index Pattern",
|
||||
},
|
||||
"relationship": "child",
|
||||
"type": "index-pattern",
|
||||
},
|
||||
]
|
||||
}
|
||||
executeQueryOptions={Object {}}
|
||||
items={
|
||||
Array [
|
||||
Object {
|
||||
"id": "2",
|
||||
"meta": Object {
|
||||
"editUrl": "/management/kibana/objects/savedVisualizations/2",
|
||||
"icon": "visualizeApp",
|
||||
"inAppUrl": Object {
|
||||
"path": "/app/kibana#/visualize/edit/2",
|
||||
"uiCapabilitiesPath": "visualize.show",
|
||||
},
|
||||
"title": "My Visualization Title",
|
||||
},
|
||||
"relationship": "parent",
|
||||
"type": "visualization",
|
||||
},
|
||||
]
|
||||
}
|
||||
pagination={true}
|
||||
responsive={true}
|
||||
search={
|
||||
Object {
|
||||
"filters": Array [
|
||||
Object {
|
||||
"field": "relationship",
|
||||
"multiSelect": "or",
|
||||
"name": "Direct relationship",
|
||||
"options": Array [
|
||||
Object {
|
||||
"name": "parent",
|
||||
"value": "parent",
|
||||
"view": "Parent",
|
||||
},
|
||||
Object {
|
||||
"name": "child",
|
||||
"value": "child",
|
||||
"view": "Child",
|
||||
},
|
||||
],
|
||||
"type": "field_value_selection",
|
||||
},
|
||||
Object {
|
||||
"field": "type",
|
||||
"multiSelect": "or",
|
||||
"name": "Type",
|
||||
"options": Array [
|
||||
Object {
|
||||
"name": "index-pattern",
|
||||
"value": "index-pattern",
|
||||
"view": "index-pattern",
|
||||
},
|
||||
Object {
|
||||
"name": "visualization",
|
||||
"value": "visualization",
|
||||
"view": "visualization",
|
||||
},
|
||||
],
|
||||
"type": "field_value_selection",
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
sorting={false}
|
||||
/>
|
||||
</EuiDescriptionList>
|
||||
</div>
|
||||
</EuiFlyoutBody>
|
||||
</EuiFlyout>
|
||||
`;
|
||||
|
@ -521,55 +588,53 @@ exports[`Relationships should render visualizations normally 1`] = `
|
|||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
<EuiDescriptionList>
|
||||
<EuiDescriptionListTitle
|
||||
style={
|
||||
Object {
|
||||
"marginBottom": "1rem",
|
||||
}
|
||||
}
|
||||
<div>
|
||||
<EuiCallOut
|
||||
color="primary"
|
||||
size="m"
|
||||
>
|
||||
<EuiCallOut
|
||||
color="warning"
|
||||
size="m"
|
||||
title={
|
||||
<FormattedMessage
|
||||
defaultMessage="Warning"
|
||||
id="kbn.management.objects.objectsTable.relationships.warningTitle"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="Here are some dashboards which contain this visualization. If you delete this visualization, these dashboards will no longer show them."
|
||||
id="kbn.management.objects.objectsTable.relationships.visualization.calloutText"
|
||||
values={Object {}}
|
||||
/>
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
</EuiDescriptionListTitle>
|
||||
<p>
|
||||
Here are the saved objects related to MyViz. Deleting this visualization affects its parent objects, but not its children.
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
<EuiSpacer />
|
||||
<EuiInMemoryTable
|
||||
columns={
|
||||
Array [
|
||||
Object {
|
||||
"align": "center",
|
||||
"description": "Type of the saved object",
|
||||
"field": "type",
|
||||
"name": "Type",
|
||||
"render": [Function],
|
||||
"width": "24px",
|
||||
"sortable": false,
|
||||
"width": "50px",
|
||||
},
|
||||
Object {
|
||||
"field": "title",
|
||||
"dataType": "string",
|
||||
"field": "relationship",
|
||||
"name": "Direct relationship",
|
||||
"render": [Function],
|
||||
"sortable": false,
|
||||
"width": "125px",
|
||||
},
|
||||
Object {
|
||||
"dataType": "string",
|
||||
"description": "Title of the saved object",
|
||||
"field": "meta.title",
|
||||
"name": "Title",
|
||||
"render": [Function],
|
||||
"sortable": false,
|
||||
},
|
||||
Object {
|
||||
"actions": Array [
|
||||
Object {
|
||||
"available": [Function],
|
||||
"description": "View this saved object within Kibana",
|
||||
"icon": "eye",
|
||||
"name": "In app",
|
||||
"description": "Inspect this saved object",
|
||||
"icon": "inspect",
|
||||
"name": "Inspect",
|
||||
"onClick": [Function],
|
||||
"testId": "savedObjectsManagementRelationshipsViewInApp",
|
||||
"type": "icon",
|
||||
},
|
||||
],
|
||||
"name": "Actions",
|
||||
|
@ -581,17 +646,76 @@ exports[`Relationships should render visualizations normally 1`] = `
|
|||
Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"meta": Object {
|
||||
"editUrl": "/management/kibana/objects/savedDashboards/1",
|
||||
"icon": "dashboardApp",
|
||||
"inAppUrl": Object {
|
||||
"path": "/app/kibana#/dashboard/1",
|
||||
"uiCapabilitiesPath": "dashboard.show",
|
||||
},
|
||||
"title": "My Dashboard 1",
|
||||
},
|
||||
"relationship": "parent",
|
||||
"type": "dashboard",
|
||||
},
|
||||
Object {
|
||||
"id": "2",
|
||||
"meta": Object {
|
||||
"editUrl": "/management/kibana/objects/savedDashboards/2",
|
||||
"icon": "dashboardApp",
|
||||
"inAppUrl": Object {
|
||||
"path": "/app/kibana#/dashboard/2",
|
||||
"uiCapabilitiesPath": "dashboard.show",
|
||||
},
|
||||
"title": "My Dashboard 2",
|
||||
},
|
||||
"relationship": "parent",
|
||||
"type": "dashboard",
|
||||
},
|
||||
]
|
||||
}
|
||||
pagination={true}
|
||||
responsive={true}
|
||||
search={
|
||||
Object {
|
||||
"filters": Array [
|
||||
Object {
|
||||
"field": "relationship",
|
||||
"multiSelect": "or",
|
||||
"name": "Direct relationship",
|
||||
"options": Array [
|
||||
Object {
|
||||
"name": "parent",
|
||||
"value": "parent",
|
||||
"view": "Parent",
|
||||
},
|
||||
Object {
|
||||
"name": "child",
|
||||
"value": "child",
|
||||
"view": "Child",
|
||||
},
|
||||
],
|
||||
"type": "field_value_selection",
|
||||
},
|
||||
Object {
|
||||
"field": "type",
|
||||
"multiSelect": "or",
|
||||
"name": "Type",
|
||||
"options": Array [
|
||||
Object {
|
||||
"name": "dashboard",
|
||||
"value": "dashboard",
|
||||
"view": "dashboard",
|
||||
},
|
||||
],
|
||||
"type": "field_value_selection",
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
sorting={false}
|
||||
/>
|
||||
</EuiDescriptionList>
|
||||
</div>
|
||||
</EuiFlyoutBody>
|
||||
</EuiFlyout>
|
||||
`;
|
||||
|
|
|
@ -53,24 +53,50 @@ describe('Relationships', () => {
|
|||
|
||||
it('should render index patterns normally', async () => {
|
||||
const props = {
|
||||
getRelationships: jest.fn().mockImplementation(() => ({
|
||||
searches: [
|
||||
{
|
||||
id: '1',
|
||||
}
|
||||
],
|
||||
visualizations: [
|
||||
{
|
||||
id: '2',
|
||||
}
|
||||
],
|
||||
})),
|
||||
getEditUrl: () => '',
|
||||
canGoInApp: () => true,
|
||||
goInApp: jest.fn(),
|
||||
id: '1',
|
||||
type: 'index-pattern',
|
||||
title: 'MyIndexPattern*',
|
||||
goInspectObject: () => {},
|
||||
getRelationships: jest.fn().mockImplementation(() => ([
|
||||
{
|
||||
type: 'search',
|
||||
id: '1',
|
||||
relationship: 'parent',
|
||||
meta: {
|
||||
editUrl: '/management/kibana/objects/savedSearches/1',
|
||||
icon: 'search',
|
||||
inAppUrl: {
|
||||
path: '/app/kibana#/discover/1',
|
||||
uiCapabilitiesPath: 'discover.show',
|
||||
},
|
||||
title: 'My Search Title',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'visualization',
|
||||
id: '2',
|
||||
relationship: 'parent',
|
||||
meta: {
|
||||
editUrl: '/management/kibana/objects/savedVisualizations/2',
|
||||
icon: 'visualizeApp',
|
||||
inAppUrl: {
|
||||
path: '/app/kibana#/visualize/edit/2',
|
||||
uiCapabilitiesPath: 'visualize.show',
|
||||
},
|
||||
title: 'My Visualization Title',
|
||||
},
|
||||
},
|
||||
])),
|
||||
savedObject: {
|
||||
id: '1',
|
||||
type: 'index-pattern',
|
||||
meta: {
|
||||
title: 'MyIndexPattern*',
|
||||
icon: 'indexPatternApp',
|
||||
editUrl: '#/management/kibana/index_patterns/1',
|
||||
inAppUrl: {
|
||||
path: '/management/kibana/index_patterns/1',
|
||||
uiCapabilitiesPath: 'management.kibana.index_patterns',
|
||||
},
|
||||
},
|
||||
},
|
||||
close: jest.fn(),
|
||||
};
|
||||
|
||||
|
@ -94,24 +120,50 @@ describe('Relationships', () => {
|
|||
|
||||
it('should render searches normally', async () => {
|
||||
const props = {
|
||||
getRelationships: jest.fn().mockImplementation(() => ({
|
||||
'index-pattern': [
|
||||
{
|
||||
id: '1',
|
||||
}
|
||||
],
|
||||
visualization: [
|
||||
{
|
||||
id: '2',
|
||||
}
|
||||
],
|
||||
})),
|
||||
getEditUrl: () => '',
|
||||
canGoInApp: () => true,
|
||||
goInApp: jest.fn(),
|
||||
id: '1',
|
||||
type: 'search',
|
||||
title: 'MySearch',
|
||||
goInspectObject: () => {},
|
||||
getRelationships: jest.fn().mockImplementation(() => ([
|
||||
{
|
||||
type: 'index-pattern',
|
||||
id: '1',
|
||||
relationship: 'child',
|
||||
meta: {
|
||||
editUrl: '/management/kibana/index_patterns/1',
|
||||
icon: 'indexPatternApp',
|
||||
inAppUrl: {
|
||||
path: '/app/kibana#/management/kibana/index_patterns/1',
|
||||
uiCapabilitiesPath: 'management.kibana.index_patterns',
|
||||
},
|
||||
title: 'My Index Pattern',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'visualization',
|
||||
id: '2',
|
||||
relationship: 'parent',
|
||||
meta: {
|
||||
editUrl: '/management/kibana/objects/savedVisualizations/2',
|
||||
icon: 'visualizeApp',
|
||||
inAppUrl: {
|
||||
path: '/app/kibana#/visualize/edit/2',
|
||||
uiCapabilitiesPath: 'visualize.show',
|
||||
},
|
||||
title: 'My Visualization Title',
|
||||
},
|
||||
},
|
||||
])),
|
||||
savedObject: {
|
||||
id: '1',
|
||||
type: 'search',
|
||||
meta: {
|
||||
title: 'MySearch',
|
||||
icon: 'search',
|
||||
editUrl: '#/management/kibana/objects/savedSearches/1',
|
||||
inAppUrl: {
|
||||
path: '/discover/1',
|
||||
uiCapabilitiesPath: 'discover.show',
|
||||
},
|
||||
},
|
||||
},
|
||||
close: jest.fn(),
|
||||
};
|
||||
|
||||
|
@ -135,22 +187,50 @@ describe('Relationships', () => {
|
|||
|
||||
it('should render visualizations normally', async () => {
|
||||
const props = {
|
||||
getRelationships: jest.fn().mockImplementation(() => ({
|
||||
dashboard: [
|
||||
{
|
||||
id: '1',
|
||||
goInspectObject: () => {},
|
||||
getRelationships: jest.fn().mockImplementation(() => ([
|
||||
{
|
||||
type: 'dashboard',
|
||||
id: '1',
|
||||
relationship: 'parent',
|
||||
meta: {
|
||||
editUrl: '/management/kibana/objects/savedDashboards/1',
|
||||
icon: 'dashboardApp',
|
||||
inAppUrl: {
|
||||
path: '/app/kibana#/dashboard/1',
|
||||
uiCapabilitiesPath: 'dashboard.show',
|
||||
},
|
||||
title: 'My Dashboard 1',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
}
|
||||
],
|
||||
})),
|
||||
getEditUrl: () => '',
|
||||
canGoInApp: () => true,
|
||||
goInApp: jest.fn(),
|
||||
id: '1',
|
||||
type: 'visualization',
|
||||
title: 'MyViz',
|
||||
},
|
||||
{
|
||||
type: 'dashboard',
|
||||
id: '2',
|
||||
relationship: 'parent',
|
||||
meta: {
|
||||
editUrl: '/management/kibana/objects/savedDashboards/2',
|
||||
icon: 'dashboardApp',
|
||||
inAppUrl: {
|
||||
path: '/app/kibana#/dashboard/2',
|
||||
uiCapabilitiesPath: 'dashboard.show',
|
||||
},
|
||||
title: 'My Dashboard 2',
|
||||
},
|
||||
},
|
||||
])),
|
||||
savedObject: {
|
||||
id: '1',
|
||||
type: 'visualization',
|
||||
meta: {
|
||||
title: 'MyViz',
|
||||
icon: 'visualizeApp',
|
||||
editUrl: '#/management/kibana/objects/savedVisualizations/1',
|
||||
inAppUrl: {
|
||||
path: '/visualize/edit/1',
|
||||
uiCapabilitiesPath: 'visualize.show',
|
||||
},
|
||||
},
|
||||
},
|
||||
close: jest.fn(),
|
||||
};
|
||||
|
||||
|
@ -174,22 +254,50 @@ describe('Relationships', () => {
|
|||
|
||||
it('should render dashboards normally', async () => {
|
||||
const props = {
|
||||
getRelationships: jest.fn().mockImplementation(() => ({
|
||||
visualization: [
|
||||
{
|
||||
id: '1',
|
||||
goInspectObject: () => {},
|
||||
getRelationships: jest.fn().mockImplementation(() => ([
|
||||
{
|
||||
type: 'visualization',
|
||||
id: '1',
|
||||
relationship: 'child',
|
||||
meta: {
|
||||
editUrl: '/management/kibana/objects/savedVisualizations/1',
|
||||
icon: 'visualizeApp',
|
||||
inAppUrl: {
|
||||
path: '/app/kibana#/visualize/edit/1',
|
||||
uiCapabilitiesPath: 'visualize.show',
|
||||
},
|
||||
title: 'My Visualization Title 1',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
}
|
||||
],
|
||||
})),
|
||||
getEditUrl: () => '',
|
||||
canGoInApp: () => true,
|
||||
goInApp: jest.fn(),
|
||||
id: '1',
|
||||
type: 'dashboard',
|
||||
title: 'MyDashboard',
|
||||
},
|
||||
{
|
||||
type: 'visualization',
|
||||
id: '2',
|
||||
relationship: 'child',
|
||||
meta: {
|
||||
editUrl: '/management/kibana/objects/savedVisualizations/2',
|
||||
icon: 'visualizeApp',
|
||||
inAppUrl: {
|
||||
path: '/app/kibana#/visualize/edit/2',
|
||||
uiCapabilitiesPath: 'visualize.show',
|
||||
},
|
||||
title: 'My Visualization Title 2',
|
||||
},
|
||||
},
|
||||
])),
|
||||
savedObject: {
|
||||
id: '1',
|
||||
type: 'dashboard',
|
||||
meta: {
|
||||
title: 'MyDashboard',
|
||||
icon: 'dashboardApp',
|
||||
editUrl: '#/management/kibana/objects/savedDashboards/1',
|
||||
inAppUrl: {
|
||||
path: '/dashboard/1',
|
||||
uiCapabilitiesPath: 'dashboard.show',
|
||||
},
|
||||
},
|
||||
},
|
||||
close: jest.fn(),
|
||||
};
|
||||
|
||||
|
@ -213,15 +321,23 @@ describe('Relationships', () => {
|
|||
|
||||
it('should render errors', async () => {
|
||||
const props = {
|
||||
goInspectObject: () => {},
|
||||
getRelationships: jest.fn().mockImplementation(() => {
|
||||
throw new Error('foo');
|
||||
}),
|
||||
getEditUrl: () => '',
|
||||
canGoInApp: () => true,
|
||||
goInApp: jest.fn(),
|
||||
id: '1',
|
||||
type: 'dashboard',
|
||||
title: 'MyDashboard',
|
||||
savedObject: {
|
||||
id: '1',
|
||||
type: 'dashboard',
|
||||
meta: {
|
||||
title: 'MyDashboard',
|
||||
icon: 'dashboardApp',
|
||||
editUrl: '#/management/kibana/objects/savedDashboards/1',
|
||||
inAppUrl: {
|
||||
path: '/dashboard/1',
|
||||
uiCapabilitiesPath: 'dashboard.show',
|
||||
},
|
||||
},
|
||||
},
|
||||
close: jest.fn(),
|
||||
};
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import {
|
||||
|
@ -25,28 +25,26 @@ import {
|
|||
EuiFlyout,
|
||||
EuiFlyoutBody,
|
||||
EuiFlyoutHeader,
|
||||
EuiDescriptionList,
|
||||
EuiDescriptionListTitle,
|
||||
EuiLink,
|
||||
EuiIcon,
|
||||
EuiCallOut,
|
||||
EuiLoadingKibana,
|
||||
EuiInMemoryTable,
|
||||
EuiToolTip
|
||||
EuiToolTip,
|
||||
EuiText,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import chrome from 'ui/chrome';
|
||||
import { FormattedMessage, injectI18n } from '@kbn/i18n/react';
|
||||
import { getSavedObjectIcon, getSavedObjectLabel } from '../../../../lib';
|
||||
import { getDefaultTitle, getSavedObjectLabel } from '../../../../lib';
|
||||
|
||||
class RelationshipsUI extends Component {
|
||||
static propTypes = {
|
||||
getRelationships: PropTypes.func.isRequired,
|
||||
id: PropTypes.string.isRequired,
|
||||
type: PropTypes.string.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
savedObject: PropTypes.object.isRequired,
|
||||
close: PropTypes.func.isRequired,
|
||||
getEditUrl: PropTypes.func.isRequired,
|
||||
goInspectObject: PropTypes.func.isRequired,
|
||||
canGoInApp: PropTypes.func.isRequired,
|
||||
goInApp: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
|
@ -64,18 +62,18 @@ class RelationshipsUI extends Component {
|
|||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.id !== this.props.id) {
|
||||
if (nextProps.savedObject.id !== this.props.savedObject.id) {
|
||||
this.getRelationshipData();
|
||||
}
|
||||
}
|
||||
|
||||
async getRelationshipData() {
|
||||
const { id, type, getRelationships } = this.props;
|
||||
const { savedObject, getRelationships } = this.props;
|
||||
|
||||
this.setState({ isLoading: true });
|
||||
|
||||
try {
|
||||
const relationships = await getRelationships(type, id);
|
||||
const relationships = await getRelationships(savedObject.type, savedObject.id);
|
||||
this.setState({ relationships, isLoading: false, error: undefined });
|
||||
} catch (err) {
|
||||
this.setState({ error: err.message, isLoading: false });
|
||||
|
@ -102,7 +100,7 @@ class RelationshipsUI extends Component {
|
|||
}
|
||||
|
||||
renderRelationships() {
|
||||
const { getEditUrl, canGoInApp, goInApp, intl } = this.props;
|
||||
const { intl, goInspectObject, savedObject } = this.props;
|
||||
const { relationships, isLoading, error } = this.state;
|
||||
|
||||
if (error) {
|
||||
|
@ -113,195 +111,214 @@ class RelationshipsUI extends Component {
|
|||
return <EuiLoadingKibana size="xl" />;
|
||||
}
|
||||
|
||||
const items = [];
|
||||
const columns = [
|
||||
{
|
||||
field: 'type',
|
||||
name: intl.formatMessage({
|
||||
id: 'kbn.management.objects.objectsTable.relationships.columnTypeName',
|
||||
defaultMessage: 'Type',
|
||||
}),
|
||||
width: '50px',
|
||||
align: 'center',
|
||||
description:
|
||||
intl.formatMessage({
|
||||
id: 'kbn.management.objects.objectsTable.relationships.columnTypeDescription',
|
||||
defaultMessage: 'Type of the saved object',
|
||||
}),
|
||||
sortable: false,
|
||||
render: (type, object) => {
|
||||
return (
|
||||
<EuiToolTip
|
||||
position="top"
|
||||
content={getSavedObjectLabel(type)}
|
||||
>
|
||||
<EuiIcon
|
||||
aria-label={getSavedObjectLabel(type)}
|
||||
type={object.meta.icon || 'apps'}
|
||||
size="s"
|
||||
/>
|
||||
</EuiToolTip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'relationship',
|
||||
name: intl.formatMessage({
|
||||
id: 'kbn.management.objects.objectsTable.relationships.columnRelationshipName',
|
||||
defaultMessage: 'Direct relationship',
|
||||
}),
|
||||
dataType: 'string',
|
||||
sortable: false,
|
||||
width: '125px',
|
||||
render: relationship => {
|
||||
if (relationship === 'parent') {
|
||||
return (
|
||||
<EuiText size="s">
|
||||
<FormattedMessage
|
||||
id="kbn.management.objects.objectsTable.relationships.columnRelationship.parentAsValue"
|
||||
defaultMessage="Parent"
|
||||
/>
|
||||
</EuiText>
|
||||
);
|
||||
}
|
||||
if (relationship === 'child') {
|
||||
return (
|
||||
<EuiText size="s">
|
||||
<FormattedMessage
|
||||
id="kbn.management.objects.objectsTable.relationships.columnRelationship.childAsValue"
|
||||
defaultMessage="Child"
|
||||
/>
|
||||
</EuiText>
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'meta.title',
|
||||
name: intl.formatMessage({
|
||||
id: 'kbn.management.objects.objectsTable.relationships.columnTitleName',
|
||||
defaultMessage: 'Title',
|
||||
}),
|
||||
description:
|
||||
intl.formatMessage({
|
||||
id: 'kbn.management.objects.objectsTable.relationships.columnTitleDescription',
|
||||
defaultMessage: 'Title of the saved object',
|
||||
}),
|
||||
dataType: 'string',
|
||||
sortable: false,
|
||||
render: (title, object) => {
|
||||
const { path } = object.meta.inAppUrl || {};
|
||||
const canGoInApp = this.props.canGoInApp(object);
|
||||
if (!canGoInApp) {
|
||||
return (
|
||||
<EuiText size="s">{title || getDefaultTitle(object)}</EuiText>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<EuiLink href={chrome.addBasePath(path)}>{title || getDefaultTitle(object)}</EuiLink>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: intl.formatMessage({
|
||||
id: 'kbn.management.objects.objectsTable.relationships.columnActionsName',
|
||||
defaultMessage: 'Actions',
|
||||
}),
|
||||
actions: [
|
||||
{
|
||||
name: intl.formatMessage({
|
||||
id: 'kbn.management.objects.objectsTable.relationships.columnActions.inspectActionName',
|
||||
defaultMessage: 'Inspect',
|
||||
}),
|
||||
description:
|
||||
intl.formatMessage({
|
||||
id: 'kbn.management.objects.objectsTable.relationships.columnActions.inspectActionDescription',
|
||||
defaultMessage: 'Inspect this saved object',
|
||||
}),
|
||||
type: 'icon',
|
||||
icon: 'inspect',
|
||||
onClick: object => goInspectObject(object),
|
||||
available: object => !!object.meta.editUrl,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
for (const [type, list] of Object.entries(relationships)) {
|
||||
if (list.length === 0) {
|
||||
items.push(
|
||||
<EuiDescriptionListTitle key={`${type}_not_found`}>
|
||||
<FormattedMessage
|
||||
id="kbn.management.objects.objectsTable.relationships.itemNotFoundText"
|
||||
defaultMessage="No {type} found."
|
||||
values={{ type }}
|
||||
/>
|
||||
</EuiDescriptionListTitle>
|
||||
);
|
||||
} else {
|
||||
// let node;
|
||||
let calloutTitle = (<FormattedMessage
|
||||
id="kbn.management.objects.objectsTable.relationships.warningTitle"
|
||||
defaultMessage="Warning"
|
||||
/>);
|
||||
let calloutColor = 'warning';
|
||||
let calloutText;
|
||||
const filterTypesMap = new Map(
|
||||
relationships.map(relationship => [
|
||||
relationship.type,
|
||||
{
|
||||
value: relationship.type,
|
||||
name: relationship.type,
|
||||
view: relationship.type,
|
||||
},
|
||||
])
|
||||
);
|
||||
|
||||
switch (this.props.type) {
|
||||
case 'dashboard':
|
||||
calloutColor = 'success';
|
||||
calloutTitle = (<FormattedMessage
|
||||
id="kbn.management.objects.objectsTable.relationships.dashboard.calloutTitle"
|
||||
defaultMessage="Dashboard"
|
||||
/>);
|
||||
calloutText = (<FormattedMessage
|
||||
id="kbn.management.objects.objectsTable.relationships.dashboard.calloutText"
|
||||
defaultMessage="Here are some visualizations used on this dashboard.
|
||||
You can safely delete this dashboard and the visualizations will still work properly."
|
||||
/>);
|
||||
break;
|
||||
case 'search':
|
||||
if (type === 'visualization') {
|
||||
calloutText = (<FormattedMessage
|
||||
id="kbn.management.objects.objectsTable.relationships.search.visualizations.calloutText"
|
||||
defaultMessage="Here are some visualizations that use this saved search. If
|
||||
you delete this saved search, these visualizations will not
|
||||
longer work properly."
|
||||
/>);
|
||||
} else {
|
||||
calloutColor = 'success';
|
||||
calloutTitle = (<FormattedMessage
|
||||
id="kbn.management.objects.objectsTable.relationships.search.calloutTitle"
|
||||
defaultMessage="Saved Search"
|
||||
/>);
|
||||
calloutText = (<FormattedMessage
|
||||
id="kbn.management.objects.objectsTable.relationships.search.calloutText"
|
||||
defaultMessage="Here is the index pattern tied to this saved search."
|
||||
/>);
|
||||
}
|
||||
break;
|
||||
case 'visualization':
|
||||
if (type === 'index-pattern') {
|
||||
calloutColor = 'success';
|
||||
calloutTitle = (<FormattedMessage
|
||||
id="kbn.management.objects.objectsTable.relationships.visualization.indexPattern.calloutTitle"
|
||||
defaultMessage="Index Pattern"
|
||||
/>);
|
||||
calloutText = (<FormattedMessage
|
||||
id="kbn.management.objects.objectsTable.relationships.visualization.indexPattern.calloutText"
|
||||
defaultMessage="Here is the index pattern tied to this visualization."
|
||||
/>);
|
||||
} else if (type === 'search') {
|
||||
calloutColor = 'success';
|
||||
calloutTitle = (<FormattedMessage
|
||||
id="kbn.management.objects.objectsTable.relationships.visualization.search.calloutTitle"
|
||||
defaultMessage="Saved Search"
|
||||
/>);
|
||||
calloutText = (<FormattedMessage
|
||||
id="kbn.management.objects.objectsTable.relationships.visualization.search.calloutText"
|
||||
defaultMessage="Here is the saved search tied to this visualization."
|
||||
/>);
|
||||
} else {
|
||||
calloutText = (<FormattedMessage
|
||||
id="kbn.management.objects.objectsTable.relationships.visualization.calloutText"
|
||||
defaultMessage="Here are some dashboards which contain this visualization. If
|
||||
you delete this visualization, these dashboards will no longer
|
||||
show them."
|
||||
/>);
|
||||
}
|
||||
break;
|
||||
case 'index-pattern':
|
||||
if (type === 'visualization') {
|
||||
calloutText = (<FormattedMessage
|
||||
id="kbn.management.objects.objectsTable.relationships.indexPattern.visualizations.calloutText"
|
||||
defaultMessage="Here are some visualizations that use this index pattern. If
|
||||
you delete this index pattern, these visualizations will not
|
||||
longer work properly."
|
||||
/>);
|
||||
} else if (type === 'search') {
|
||||
calloutText = (<FormattedMessage
|
||||
id="kbn.management.objects.objectsTable.relationships.indexPattern.searches.calloutText"
|
||||
defaultMessage="Here are some saved searches that use this index pattern. If
|
||||
you delete this index pattern, these saved searches will not
|
||||
longer work properly."
|
||||
/>);
|
||||
}
|
||||
break;
|
||||
}
|
||||
const search = {
|
||||
filters: [
|
||||
{
|
||||
type: 'field_value_selection',
|
||||
field: 'relationship',
|
||||
name: intl.formatMessage({
|
||||
id: 'kbn.management.objects.objectsTable.relationships.search.filters.relationship.name',
|
||||
defaultMessage: 'Direct relationship',
|
||||
}),
|
||||
multiSelect: 'or',
|
||||
options: [
|
||||
{
|
||||
value: 'parent',
|
||||
name: 'parent',
|
||||
view: intl.formatMessage({
|
||||
id: 'kbn.management.objects.objectsTable.relationships.search.filters.relationship.parentAsValue.view',
|
||||
defaultMessage: 'Parent',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: 'child',
|
||||
name: 'child',
|
||||
view: intl.formatMessage({
|
||||
id: 'kbn.management.objects.objectsTable.relationships.search.filters.relationship.childAsValue.view',
|
||||
defaultMessage: 'Child',
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'field_value_selection',
|
||||
field: 'type',
|
||||
name: intl.formatMessage({
|
||||
id: 'kbn.management.objects.objectsTable.relationships.search.filters.type.name',
|
||||
defaultMessage: 'Type',
|
||||
}),
|
||||
multiSelect: 'or',
|
||||
options: [...filterTypesMap.values()],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
items.push(
|
||||
<Fragment key={type}>
|
||||
<EuiDescriptionListTitle style={{ marginBottom: '1rem' }}>
|
||||
<EuiCallOut color={calloutColor} title={calloutTitle}>
|
||||
<p>{calloutText}</p>
|
||||
</EuiCallOut>
|
||||
</EuiDescriptionListTitle>
|
||||
<EuiInMemoryTable
|
||||
items={list}
|
||||
columns={[
|
||||
{
|
||||
width: '24px',
|
||||
render: () => (
|
||||
<EuiToolTip
|
||||
position="top"
|
||||
content={getSavedObjectLabel(type)}
|
||||
>
|
||||
<EuiIcon
|
||||
aria-label={getSavedObjectLabel(type)}
|
||||
size="s"
|
||||
type={getSavedObjectIcon(type)}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: intl.formatMessage({
|
||||
id: 'kbn.management.objects.objectsTable.relationships.columnTitleName', defaultMessage: 'Title'
|
||||
}),
|
||||
field: 'title',
|
||||
render: (title, item) => (
|
||||
<EuiLink href={`${getEditUrl(item.id, type)}`}>
|
||||
{title}
|
||||
</EuiLink>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: intl.formatMessage({
|
||||
id: 'kbn.management.objects.objectsTable.relationships.columnActionsName', defaultMessage: 'Actions'
|
||||
}),
|
||||
actions: [
|
||||
{
|
||||
name: intl.formatMessage({
|
||||
id: 'kbn.management.objects.objectsTable.relationships.columnActions.inAppName',
|
||||
defaultMessage: 'In app'
|
||||
}),
|
||||
description: intl.formatMessage({
|
||||
id: 'kbn.management.objects.objectsTable.relationships.columnActions.inAppDescription',
|
||||
defaultMessage: 'View this saved object within Kibana'
|
||||
}),
|
||||
icon: 'eye',
|
||||
available: () => canGoInApp(type),
|
||||
onClick: object => goInApp(object.id, type),
|
||||
testId: 'savedObjectsManagementRelationshipsViewInApp'
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
pagination={true}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return <EuiDescriptionList>{items}</EuiDescriptionList>;
|
||||
return (
|
||||
<div>
|
||||
<EuiCallOut>
|
||||
<p>
|
||||
{intl.formatMessage({
|
||||
id: 'kbn.management.objects.objectsTable.relationships.relationshipsTitle',
|
||||
defaultMessage: 'Here are the saved objects related to {title}. ' +
|
||||
'Deleting this {type} affects its parent objects, but not its children.',
|
||||
}, {
|
||||
type: savedObject.type,
|
||||
title: savedObject.meta.title || getDefaultTitle(savedObject)
|
||||
})}
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
<EuiSpacer />
|
||||
<EuiInMemoryTable
|
||||
items={relationships}
|
||||
columns={columns}
|
||||
pagination={true}
|
||||
search={search}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { close, title, type } = this.props;
|
||||
const { close, savedObject } = this.props;
|
||||
|
||||
return (
|
||||
<EuiFlyout onClose={close}>
|
||||
<EuiFlyoutHeader hasBorder>
|
||||
<EuiTitle size="m">
|
||||
<h2>
|
||||
<EuiToolTip position="top" content={getSavedObjectLabel(type)}>
|
||||
<EuiToolTip position="top" content={getSavedObjectLabel(savedObject.type)}>
|
||||
<EuiIcon
|
||||
aria-label={getSavedObjectLabel(type)}
|
||||
aria-label={getSavedObjectLabel(savedObject.type)}
|
||||
size="m"
|
||||
type={getSavedObjectIcon(type)}
|
||||
type={savedObject.meta.icon || 'apps'}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
|
||||
{title}
|
||||
{savedObject.meta.title || getDefaultTitle(savedObject)}
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
|
|
|
@ -143,7 +143,7 @@ exports[`Table restricts which saved objects can be deleted based on type 1`] =
|
|||
Object {
|
||||
"dataType": "string",
|
||||
"description": "Title of the saved object",
|
||||
"field": "title",
|
||||
"field": "meta.title",
|
||||
"name": "Title",
|
||||
"render": [Function],
|
||||
"sortable": false,
|
||||
|
@ -152,9 +152,9 @@ exports[`Table restricts which saved objects can be deleted based on type 1`] =
|
|||
"actions": Array [
|
||||
Object {
|
||||
"available": [Function],
|
||||
"description": "View this saved object within Kibana",
|
||||
"icon": "eye",
|
||||
"name": "In app",
|
||||
"description": "Inspect this saved object",
|
||||
"icon": "inspect",
|
||||
"name": "Inspect",
|
||||
"onClick": [Function],
|
||||
"type": "icon",
|
||||
},
|
||||
|
@ -173,7 +173,19 @@ exports[`Table restricts which saved objects can be deleted based on type 1`] =
|
|||
itemId="id"
|
||||
items={
|
||||
Array [
|
||||
3,
|
||||
Object {
|
||||
"id": "1",
|
||||
"meta": Object {
|
||||
"editUrl": "#/management/kibana/index_patterns/1",
|
||||
"icon": "indexPatternApp",
|
||||
"inAppUrl": Object {
|
||||
"path": "/management/kibana/index_patterns/1",
|
||||
"uiCapabilitiesPath": "management.kibana.index_patterns",
|
||||
},
|
||||
"title": "MyIndexPattern*",
|
||||
},
|
||||
"type": "index-pattern",
|
||||
},
|
||||
]
|
||||
}
|
||||
loading={false}
|
||||
|
@ -235,9 +247,10 @@ exports[`Table should render normally 1`] = `
|
|||
fill={false}
|
||||
iconSide="left"
|
||||
iconType="trash"
|
||||
isDisabled={false}
|
||||
isDisabled={true}
|
||||
onClick={[Function]}
|
||||
size="m"
|
||||
title="Unable to delete index-pattern"
|
||||
type="button"
|
||||
>
|
||||
<FormattedMessage
|
||||
|
@ -345,7 +358,7 @@ exports[`Table should render normally 1`] = `
|
|||
Object {
|
||||
"dataType": "string",
|
||||
"description": "Title of the saved object",
|
||||
"field": "title",
|
||||
"field": "meta.title",
|
||||
"name": "Title",
|
||||
"render": [Function],
|
||||
"sortable": false,
|
||||
|
@ -354,9 +367,9 @@ exports[`Table should render normally 1`] = `
|
|||
"actions": Array [
|
||||
Object {
|
||||
"available": [Function],
|
||||
"description": "View this saved object within Kibana",
|
||||
"icon": "eye",
|
||||
"name": "In app",
|
||||
"description": "Inspect this saved object",
|
||||
"icon": "inspect",
|
||||
"name": "Inspect",
|
||||
"onClick": [Function],
|
||||
"type": "icon",
|
||||
},
|
||||
|
@ -375,7 +388,19 @@ exports[`Table should render normally 1`] = `
|
|||
itemId="id"
|
||||
items={
|
||||
Array [
|
||||
3,
|
||||
Object {
|
||||
"id": "1",
|
||||
"meta": Object {
|
||||
"editUrl": "#/management/kibana/index_patterns/1",
|
||||
"icon": "indexPatternApp",
|
||||
"inAppUrl": Object {
|
||||
"path": "/management/kibana/index_patterns/1",
|
||||
"uiCapabilitiesPath": "management.kibana.index_patterns",
|
||||
},
|
||||
"title": "MyIndexPattern*",
|
||||
},
|
||||
"type": "index-pattern",
|
||||
},
|
||||
]
|
||||
}
|
||||
loading={false}
|
||||
|
|
|
@ -44,19 +44,42 @@ jest.mock('ui/chrome', () => ({
|
|||
import { Table } from '../table';
|
||||
|
||||
const defaultProps = {
|
||||
selectedSavedObjects: [{ type: 'visualization' }],
|
||||
selectedSavedObjects: [{
|
||||
id: '1',
|
||||
type: 'index-pattern',
|
||||
meta: {
|
||||
title: `MyIndexPattern*`,
|
||||
icon: 'indexPatternApp',
|
||||
editUrl: '#/management/kibana/index_patterns/1',
|
||||
inAppUrl: {
|
||||
path: '/management/kibana/index_patterns/1',
|
||||
uiCapabilitiesPath: 'management.kibana.index_patterns',
|
||||
},
|
||||
},
|
||||
}],
|
||||
selectionConfig: {
|
||||
onSelectionChange: () => {},
|
||||
},
|
||||
filterOptions: [{ value: 2 }],
|
||||
onDelete: () => {},
|
||||
onExport: () => {},
|
||||
getEditUrl: () => {},
|
||||
goInspectObject: () => {},
|
||||
canGoInApp: () => {},
|
||||
goInApp: () => {},
|
||||
pageIndex: 1,
|
||||
pageSize: 2,
|
||||
items: [3],
|
||||
items: [{
|
||||
id: '1',
|
||||
type: 'index-pattern',
|
||||
meta: {
|
||||
title: `MyIndexPattern*`,
|
||||
icon: 'indexPatternApp',
|
||||
editUrl: '#/management/kibana/index_patterns/1',
|
||||
inAppUrl: {
|
||||
path: '/management/kibana/index_patterns/1',
|
||||
uiCapabilitiesPath: 'management.kibana.index_patterns',
|
||||
},
|
||||
},
|
||||
}],
|
||||
itemId: 'id',
|
||||
totalItemCount: 3,
|
||||
onQueryChange: () => {},
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import chrome from 'ui/chrome';
|
||||
import React, { PureComponent, Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
|
@ -31,9 +32,10 @@ import {
|
|||
EuiFormErrorText,
|
||||
EuiPopover,
|
||||
EuiSwitch,
|
||||
EuiFormRow
|
||||
EuiFormRow,
|
||||
EuiText
|
||||
} from '@elastic/eui';
|
||||
import { getSavedObjectLabel, getSavedObjectIcon } from '../../../../lib';
|
||||
import { getDefaultTitle, getSavedObjectLabel } from '../../../../lib';
|
||||
import { FormattedMessage, injectI18n } from '@kbn/i18n/react';
|
||||
|
||||
class TableUI extends PureComponent {
|
||||
|
@ -48,9 +50,7 @@ class TableUI extends PureComponent {
|
|||
canDeleteSavedObjectTypes: PropTypes.array.isRequired,
|
||||
onDelete: PropTypes.func.isRequired,
|
||||
onExport: PropTypes.func.isRequired,
|
||||
getEditUrl: PropTypes.func.isRequired,
|
||||
canGoInApp: PropTypes.func.isRequired,
|
||||
goInApp: PropTypes.func.isRequired,
|
||||
goInspectObject: PropTypes.func.isRequired,
|
||||
|
||||
pageIndex: PropTypes.number.isRequired,
|
||||
pageSize: PropTypes.number.isRequired,
|
||||
|
@ -126,9 +126,7 @@ class TableUI extends PureComponent {
|
|||
onDelete,
|
||||
selectedSavedObjects,
|
||||
onTableChange,
|
||||
canGoInApp,
|
||||
goInApp,
|
||||
getEditUrl,
|
||||
goInspectObject,
|
||||
onShowRelationships,
|
||||
intl,
|
||||
} = this.props;
|
||||
|
@ -169,7 +167,7 @@ class TableUI extends PureComponent {
|
|||
id: 'kbn.management.objects.objectsTable.table.columnTypeDescription', defaultMessage: 'Type of the saved object'
|
||||
}),
|
||||
sortable: false,
|
||||
render: type => {
|
||||
render: (type, object) => {
|
||||
return (
|
||||
<EuiToolTip
|
||||
position="top"
|
||||
|
@ -177,7 +175,7 @@ class TableUI extends PureComponent {
|
|||
>
|
||||
<EuiIcon
|
||||
aria-label={getSavedObjectLabel(type)}
|
||||
type={getSavedObjectIcon(type)}
|
||||
type={object.meta.icon || 'apps'}
|
||||
size="s"
|
||||
/>
|
||||
</EuiToolTip>
|
||||
|
@ -185,7 +183,7 @@ class TableUI extends PureComponent {
|
|||
},
|
||||
},
|
||||
{
|
||||
field: 'title',
|
||||
field: 'meta.title',
|
||||
name: intl.formatMessage({ id: 'kbn.management.objects.objectsTable.table.columnTitleName', defaultMessage: 'Title' }),
|
||||
description:
|
||||
intl.formatMessage({
|
||||
|
@ -193,26 +191,36 @@ class TableUI extends PureComponent {
|
|||
}),
|
||||
dataType: 'string',
|
||||
sortable: false,
|
||||
render: (title, object) => (
|
||||
<EuiLink href={getEditUrl(object.id, object.type)}>{title}</EuiLink>
|
||||
),
|
||||
render: (title, object) => {
|
||||
const { path } = object.meta.inAppUrl || {};
|
||||
const canGoInApp = this.props.canGoInApp(object);
|
||||
if (!canGoInApp) {
|
||||
return (
|
||||
<EuiText size="s">{title || getDefaultTitle(object)}</EuiText>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<EuiLink href={chrome.addBasePath(path)}>{title || getDefaultTitle(object)}</EuiLink>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: intl.formatMessage({ id: 'kbn.management.objects.objectsTable.table.columnActionsName', defaultMessage: 'Actions' }),
|
||||
actions: [
|
||||
{
|
||||
name: intl.formatMessage({
|
||||
id: 'kbn.management.objects.objectsTable.table.columnActions.viewInAppActionName', defaultMessage: 'In app'
|
||||
id: 'kbn.management.objects.objectsTable.table.columnActions.inspectActionName',
|
||||
defaultMessage: 'Inspect'
|
||||
}),
|
||||
description:
|
||||
intl.formatMessage({
|
||||
id: 'kbn.management.objects.objectsTable.table.columnActions.viewInAppActionDescription',
|
||||
defaultMessage: 'View this saved object within Kibana'
|
||||
id: 'kbn.management.objects.objectsTable.table.columnActions.inspectActionDescription',
|
||||
defaultMessage: 'Inspect this saved object'
|
||||
}),
|
||||
type: 'icon',
|
||||
icon: 'eye',
|
||||
available: object => canGoInApp(object.type),
|
||||
onClick: object => goInApp(object.id, object.type),
|
||||
icon: 'inspect',
|
||||
onClick: object => goInspectObject(object),
|
||||
available: object => !!object.meta.editUrl,
|
||||
},
|
||||
{
|
||||
name:
|
||||
|
@ -227,8 +235,7 @@ class TableUI extends PureComponent {
|
|||
}),
|
||||
type: 'icon',
|
||||
icon: 'kqlSelector',
|
||||
onClick: object =>
|
||||
onShowRelationships(object.id, object.type, object.title),
|
||||
onClick: object => onShowRelationships(object),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import chrome from 'ui/chrome';
|
||||
import { saveAs } from '@elastic/filesaver';
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
@ -53,21 +54,16 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import {
|
||||
parseQuery,
|
||||
getSavedObjectIcon,
|
||||
getSavedObjectCounts,
|
||||
getRelationships,
|
||||
getSavedObjectLabel,
|
||||
fetchExportObjects,
|
||||
fetchExportByType,
|
||||
findObjects,
|
||||
} from '../../lib';
|
||||
import { FormattedMessage, injectI18n } from '@kbn/i18n/react';
|
||||
|
||||
export const POSSIBLE_TYPES = [
|
||||
'index-pattern',
|
||||
'visualization',
|
||||
'dashboard',
|
||||
'search',
|
||||
];
|
||||
export const POSSIBLE_TYPES = chrome.getInjected('importAndExportableTypes');
|
||||
|
||||
class ObjectsTableUI extends Component {
|
||||
static propTypes = {
|
||||
|
@ -78,10 +74,9 @@ class ObjectsTableUI extends Component {
|
|||
perPageConfig: PropTypes.number,
|
||||
newIndexPatternUrl: PropTypes.string.isRequired,
|
||||
services: PropTypes.array.isRequired,
|
||||
getEditUrl: PropTypes.func.isRequired,
|
||||
canGoInApp: PropTypes.func.isRequired,
|
||||
goInApp: PropTypes.func.isRequired,
|
||||
uiCapabilities: PropTypes.object.isRequired,
|
||||
goInspectObject: PropTypes.func.isRequired,
|
||||
canGoInApp: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
|
@ -105,9 +100,7 @@ class ObjectsTableUI extends Component {
|
|||
isSearching: false,
|
||||
filteredItemCount: 0,
|
||||
isShowingRelationships: false,
|
||||
relationshipId: undefined,
|
||||
relationshipType: undefined,
|
||||
relationshipTitle: undefined,
|
||||
relationshipObject: undefined,
|
||||
isShowingDeleteConfirmModal: false,
|
||||
isShowingExportAllOptionsModal: false,
|
||||
isDeleting: false,
|
||||
|
@ -179,15 +172,16 @@ class ObjectsTableUI extends Component {
|
|||
}
|
||||
|
||||
debouncedFetch = debounce(async () => {
|
||||
const { intl, savedObjectsClient } = this.props;
|
||||
const { intl } = this.props;
|
||||
const { activeQuery: query, page, perPage } = this.state;
|
||||
const { queryText, visibleTypes } = parseQuery(query);
|
||||
// "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 = {
|
||||
search: queryText ? `${queryText}*` : undefined,
|
||||
perPage,
|
||||
page: page + 1,
|
||||
fields: ['title', 'id'],
|
||||
searchFields: ['title'],
|
||||
fields: ['id'],
|
||||
type: this.savedObjectTypes.filter(
|
||||
type => !visibleTypes || visibleTypes.includes(type)
|
||||
),
|
||||
|
@ -198,7 +192,7 @@ class ObjectsTableUI extends Component {
|
|||
|
||||
let resp;
|
||||
try {
|
||||
resp = await savedObjectsClient.find(findOptions);
|
||||
resp = await findObjects(findOptions);
|
||||
} catch (error) {
|
||||
if (this._isMounted) {
|
||||
this.setState({
|
||||
|
@ -226,12 +220,7 @@ class ObjectsTableUI extends Component {
|
|||
}
|
||||
|
||||
return {
|
||||
savedObjects: resp.savedObjects.map(savedObject => ({
|
||||
title: savedObject.attributes.title,
|
||||
type: savedObject.type,
|
||||
id: savedObject.id,
|
||||
icon: getSavedObjectIcon(savedObject.type),
|
||||
})),
|
||||
savedObjects: resp.savedObjects,
|
||||
filteredItemCount: resp.total,
|
||||
isSearching: false,
|
||||
};
|
||||
|
@ -243,12 +232,7 @@ class ObjectsTableUI extends Component {
|
|||
};
|
||||
|
||||
onSelectionChanged = selection => {
|
||||
const selectedSavedObjects = selection.map(item => ({
|
||||
id: item.id,
|
||||
type: item.type,
|
||||
title: item.title,
|
||||
}));
|
||||
this.setState({ selectedSavedObjects });
|
||||
this.setState({ selectedSavedObjects: selection });
|
||||
};
|
||||
|
||||
onQueryChange = ({ query }) => {
|
||||
|
@ -277,21 +261,17 @@ class ObjectsTableUI extends Component {
|
|||
}, this.fetchSavedObjects);
|
||||
};
|
||||
|
||||
onShowRelationships = (id, type, title) => {
|
||||
onShowRelationships = (object) => {
|
||||
this.setState({
|
||||
isShowingRelationships: true,
|
||||
relationshipId: id,
|
||||
relationshipType: type,
|
||||
relationshipTitle: title,
|
||||
relationshipObject: object,
|
||||
});
|
||||
};
|
||||
|
||||
onHideRelationships = () => {
|
||||
this.setState({
|
||||
isShowingRelationships: false,
|
||||
relationshipId: undefined,
|
||||
relationshipType: undefined,
|
||||
relationshipTitle: undefined,
|
||||
relationshipObject: undefined,
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -299,7 +279,20 @@ class ObjectsTableUI extends Component {
|
|||
const { intl } = this.props;
|
||||
const { selectedSavedObjects } = this.state;
|
||||
const objectsToExport = selectedSavedObjects.map(obj => ({ id: obj.id, type: obj.type }));
|
||||
const blob = await fetchExportObjects(objectsToExport, includeReferencesDeep);
|
||||
|
||||
let blob;
|
||||
try {
|
||||
blob = await fetchExportObjects(objectsToExport, includeReferencesDeep);
|
||||
} catch (e) {
|
||||
toastNotifications.addDanger({
|
||||
title: intl.formatMessage({
|
||||
id: 'kbn.management.objects.objectsTable.export.dangerNotification',
|
||||
defaultMessage: 'Unable to generate export',
|
||||
}),
|
||||
});
|
||||
throw e;
|
||||
}
|
||||
|
||||
saveAs(blob, 'export.ndjson');
|
||||
toastNotifications.addSuccess({
|
||||
title: intl.formatMessage({
|
||||
|
@ -321,7 +314,20 @@ class ObjectsTableUI extends Component {
|
|||
},
|
||||
[]
|
||||
);
|
||||
const blob = await fetchExportByType(exportTypes, isIncludeReferencesDeepChecked);
|
||||
|
||||
let blob;
|
||||
try {
|
||||
blob = await fetchExportByType(exportTypes, isIncludeReferencesDeepChecked);
|
||||
} catch (e) {
|
||||
toastNotifications.addDanger({
|
||||
title: intl.formatMessage({
|
||||
id: 'kbn.management.objects.objectsTable.exportAll.dangerNotification',
|
||||
defaultMessage: 'Unable to generate export',
|
||||
}),
|
||||
});
|
||||
throw e;
|
||||
}
|
||||
|
||||
saveAs(blob, 'export.ndjson');
|
||||
toastNotifications.addSuccess({
|
||||
title: intl.formatMessage({
|
||||
|
@ -423,15 +429,12 @@ class ObjectsTableUI extends Component {
|
|||
|
||||
return (
|
||||
<Relationships
|
||||
id={this.state.relationshipId}
|
||||
type={this.state.relationshipType}
|
||||
title={this.state.relationshipTitle}
|
||||
savedObject={this.state.relationshipObject}
|
||||
getRelationships={this.getRelationships}
|
||||
close={this.onHideRelationships}
|
||||
getDashboardUrl={this.props.getDashboardUrl}
|
||||
getEditUrl={this.props.getEditUrl}
|
||||
goInspectObject={this.props.goInspectObject}
|
||||
canGoInApp={this.props.canGoInApp}
|
||||
goInApp={this.props.goInApp}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -509,12 +512,12 @@ class ObjectsTableUI extends Component {
|
|||
id: 'kbn.management.objects.objectsTable.deleteSavedObjectsConfirmModal.typeColumnName', defaultMessage: 'Type'
|
||||
}),
|
||||
width: '50px',
|
||||
render: type => (
|
||||
render: (type, object) => (
|
||||
<EuiToolTip
|
||||
position="top"
|
||||
content={getSavedObjectLabel(type)}
|
||||
>
|
||||
<EuiIcon type={getSavedObjectIcon(type)} />
|
||||
<EuiIcon type={object.meta.icon || 'apps'} />
|
||||
</EuiToolTip>
|
||||
),
|
||||
},
|
||||
|
@ -525,7 +528,7 @@ class ObjectsTableUI extends Component {
|
|||
}),
|
||||
},
|
||||
{
|
||||
field: 'title',
|
||||
field: 'meta.title',
|
||||
name: intl.formatMessage({
|
||||
id: 'kbn.management.objects.objectsTable.deleteSavedObjectsConfirmModal.titleColumnName',
|
||||
defaultMessage: 'Title',
|
||||
|
@ -703,15 +706,14 @@ class ObjectsTableUI extends Component {
|
|||
onExport={this.onExport}
|
||||
canDeleteSavedObjectTypes={canDeleteSavedObjectTypes}
|
||||
onDelete={this.onDelete}
|
||||
getEditUrl={this.props.getEditUrl}
|
||||
canGoInApp={this.props.canGoInApp}
|
||||
goInApp={this.props.goInApp}
|
||||
goInspectObject={this.props.goInspectObject}
|
||||
pageIndex={page}
|
||||
pageSize={perPage}
|
||||
items={savedObjects}
|
||||
totalItemCount={filteredItemCount}
|
||||
isSearching={isSearching}
|
||||
onShowRelationships={this.onShowRelationships}
|
||||
canGoInApp={this.props.canGoInApp}
|
||||
/>
|
||||
</EuiPageContent>
|
||||
);
|
||||
|
|
|
@ -1,47 +0,0 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { getSavedObjectIcon } from '../get_saved_object_icon';
|
||||
|
||||
describe('getSavedObjectIcon', () => {
|
||||
it('should handle saved searches', () => {
|
||||
expect(getSavedObjectIcon('search')).toEqual('search');
|
||||
expect(getSavedObjectIcon('searches')).toEqual('search');
|
||||
});
|
||||
|
||||
it('should handle visualizations', () => {
|
||||
expect(getSavedObjectIcon('visualization')).toEqual('visualizeApp');
|
||||
expect(getSavedObjectIcon('visualizations')).toEqual('visualizeApp');
|
||||
});
|
||||
|
||||
it('should handle index patterns', () => {
|
||||
expect(getSavedObjectIcon('index-pattern')).toEqual('indexPatternApp');
|
||||
expect(getSavedObjectIcon('index-patterns')).toEqual('indexPatternApp');
|
||||
expect(getSavedObjectIcon('indexPatterns')).toEqual('indexPatternApp');
|
||||
});
|
||||
|
||||
it('should handle dashboards', () => {
|
||||
expect(getSavedObjectIcon('dashboard')).toEqual('dashboardApp');
|
||||
expect(getSavedObjectIcon('dashboards')).toEqual('dashboardApp');
|
||||
});
|
||||
|
||||
it('should have a default case', () => {
|
||||
expect(getSavedObjectIcon('foo')).toEqual('apps');
|
||||
});
|
||||
});
|
|
@ -17,40 +17,7 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { getInAppUrl, canViewInApp } from '../in_app_url';
|
||||
|
||||
describe('getInAppUrl', () => {
|
||||
it('should handle saved searches', () => {
|
||||
expect(getInAppUrl(1, 'search')).toEqual('/discover/1');
|
||||
expect(getInAppUrl(1, 'searches')).toEqual('/discover/1');
|
||||
});
|
||||
|
||||
it('should handle visualizations', () => {
|
||||
expect(getInAppUrl(1, 'visualization')).toEqual('/visualize/edit/1');
|
||||
expect(getInAppUrl(1, 'visualizations')).toEqual('/visualize/edit/1');
|
||||
});
|
||||
|
||||
it('should handle index patterns', () => {
|
||||
expect(getInAppUrl(1, 'index-pattern')).toEqual(
|
||||
'/management/kibana/index_patterns/1'
|
||||
);
|
||||
expect(getInAppUrl(1, 'index-patterns')).toEqual(
|
||||
'/management/kibana/index_patterns/1'
|
||||
);
|
||||
expect(getInAppUrl(1, 'indexPatterns')).toEqual(
|
||||
'/management/kibana/index_patterns/1'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle dashboards', () => {
|
||||
expect(getInAppUrl(1, 'dashboard')).toEqual('/dashboard/1');
|
||||
expect(getInAppUrl(1, 'dashboards')).toEqual('/dashboard/1');
|
||||
});
|
||||
|
||||
it('should have a default case', () => {
|
||||
expect(getInAppUrl(1, 'foo')).toEqual('/foo/1');
|
||||
});
|
||||
});
|
||||
import { canViewInApp } from '../in_app_url';
|
||||
|
||||
describe('canViewInApp', () => {
|
||||
it('should handle saved searches', () => {
|
||||
|
|
|
@ -17,22 +17,14 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export function getSavedObjectIcon(type) {
|
||||
switch (type) {
|
||||
case 'search':
|
||||
case 'searches':
|
||||
return 'search';
|
||||
case 'visualization':
|
||||
case 'visualizations':
|
||||
return 'visualizeApp';
|
||||
case 'dashboard':
|
||||
case 'dashboards':
|
||||
return 'dashboardApp';
|
||||
case 'index-pattern':
|
||||
case 'index-patterns':
|
||||
case 'indexPatterns':
|
||||
return 'indexPatternApp';
|
||||
default:
|
||||
return 'apps';
|
||||
}
|
||||
import { kfetch } from 'ui/kfetch';
|
||||
import { keysToCamelCaseShallow } from 'ui/utils/case_conversion';
|
||||
|
||||
export async function findObjects(findOptions) {
|
||||
const response = await kfetch({
|
||||
method: 'GET',
|
||||
pathname: '/api/kibana/management/saved_objects/_find',
|
||||
query: findOptions,
|
||||
});
|
||||
return keysToCamelCaseShallow(response);
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
export function getDefaultTitle(object) {
|
||||
return `${object.type} [id=${object.id}]`;
|
||||
}
|
|
@ -17,26 +17,6 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export function getInAppUrl(id, type) {
|
||||
switch (type) {
|
||||
case 'search':
|
||||
case 'searches':
|
||||
return `/discover/${id}`;
|
||||
case 'visualization':
|
||||
case 'visualizations':
|
||||
return `/visualize/edit/${id}`;
|
||||
case 'index-pattern':
|
||||
case 'index-patterns':
|
||||
case 'indexPatterns':
|
||||
return `/management/kibana/index_patterns/${id}`;
|
||||
case 'dashboard':
|
||||
case 'dashboards':
|
||||
return `/dashboard/${id}`;
|
||||
default:
|
||||
return `/${type.toLowerCase()}/${id}`;
|
||||
}
|
||||
}
|
||||
|
||||
export function canViewInApp(uiCapabilities, type) {
|
||||
switch (type) {
|
||||
case 'search':
|
||||
|
|
|
@ -22,7 +22,6 @@ export * from './fetch_export_objects';
|
|||
export * from './in_app_url';
|
||||
export * from './get_relationships';
|
||||
export * from './get_saved_object_counts';
|
||||
export * from './get_saved_object_icon';
|
||||
export * from './get_saved_object_label';
|
||||
export * from './import_file';
|
||||
export * from './import_legacy_file';
|
||||
|
@ -31,3 +30,5 @@ export * from './resolve_import_errors';
|
|||
export * from './resolve_saved_objects';
|
||||
export * from './log_legacy_import';
|
||||
export * from './process_import_response';
|
||||
export * from './get_default_title';
|
||||
export * from './find_objects';
|
||||
|
|
|
@ -20,6 +20,108 @@
|
|||
import expect from '@kbn/expect';
|
||||
import { findRelationships } from '../management/saved_objects/relationships';
|
||||
|
||||
function getManagementaMock(savedObjectSchemas) {
|
||||
return {
|
||||
isImportAndExportable(type) {
|
||||
return !savedObjectSchemas[type] || savedObjectSchemas[type].isImportableAndExportable !== false;
|
||||
},
|
||||
getDefaultSearchField(type) {
|
||||
return savedObjectSchemas[type] && savedObjectSchemas[type].defaultSearchField;
|
||||
},
|
||||
getIcon(type) {
|
||||
return savedObjectSchemas[type] && savedObjectSchemas[type].icon;
|
||||
},
|
||||
getTitle(savedObject) {
|
||||
const { type } = savedObject;
|
||||
const getTitle = savedObjectSchemas[type] && savedObjectSchemas[type].getTitle;
|
||||
if (getTitle) {
|
||||
return getTitle(savedObject);
|
||||
}
|
||||
},
|
||||
getEditUrl(savedObject) {
|
||||
const { type } = savedObject;
|
||||
const getEditUrl = savedObjectSchemas[type] && savedObjectSchemas[type].getEditUrl;
|
||||
if (getEditUrl) {
|
||||
return getEditUrl(savedObject);
|
||||
}
|
||||
},
|
||||
getInAppUrl(savedObject) {
|
||||
const { type } = savedObject;
|
||||
const getInAppUrl = savedObjectSchemas[type] && savedObjectSchemas[type].getInAppUrl;
|
||||
if (getInAppUrl) {
|
||||
return getInAppUrl(savedObject);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const savedObjectsManagement = getManagementaMock({
|
||||
'index-pattern': {
|
||||
icon: 'indexPatternApp',
|
||||
defaultSearchField: 'title',
|
||||
getTitle(obj) {
|
||||
return obj.attributes.title;
|
||||
},
|
||||
getEditUrl(obj) {
|
||||
return `/management/kibana/index_patterns/${encodeURIComponent(obj.id)}`;
|
||||
},
|
||||
getInAppUrl(obj) {
|
||||
return {
|
||||
path: `/app/kibana#/management/kibana/index_patterns/${encodeURIComponent(obj.id)}`,
|
||||
uiCapabilitiesPath: 'management.kibana.index_patterns',
|
||||
};
|
||||
},
|
||||
},
|
||||
visualization: {
|
||||
icon: 'visualizeApp',
|
||||
defaultSearchField: 'title',
|
||||
getTitle(obj) {
|
||||
return obj.attributes.title;
|
||||
},
|
||||
getEditUrl(obj) {
|
||||
return `/management/kibana/objects/savedVisualizations/${encodeURIComponent(obj.id)}`;
|
||||
},
|
||||
getInAppUrl(obj) {
|
||||
return {
|
||||
path: `/app/kibana#/visualize/edit/${encodeURIComponent(obj.id)}`,
|
||||
uiCapabilitiesPath: 'visualize.show',
|
||||
};
|
||||
},
|
||||
},
|
||||
search: {
|
||||
icon: 'search',
|
||||
defaultSearchField: 'title',
|
||||
getTitle(obj) {
|
||||
return obj.attributes.title;
|
||||
},
|
||||
getEditUrl(obj) {
|
||||
return `/management/kibana/objects/savedSearches/${encodeURIComponent(obj.id)}`;
|
||||
},
|
||||
getInAppUrl(obj) {
|
||||
return {
|
||||
path: `/app/kibana#/discover/${encodeURIComponent(obj.id)}`,
|
||||
uiCapabilitiesPath: 'discover.show',
|
||||
};
|
||||
},
|
||||
},
|
||||
dashboard: {
|
||||
icon: 'dashboardApp',
|
||||
defaultSearchField: 'title',
|
||||
getTitle(obj) {
|
||||
return obj.attributes.title;
|
||||
},
|
||||
getEditUrl(obj) {
|
||||
return `/management/kibana/objects/savedDashboards/${encodeURIComponent(obj.id)}`;
|
||||
},
|
||||
getInAppUrl(obj) {
|
||||
return {
|
||||
path: `/app/kibana#/dashboard/${encodeURIComponent(obj.id)}`,
|
||||
uiCapabilitiesPath: 'dashboard.show',
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
describe('findRelationships', () => {
|
||||
it('should find relationships for dashboards', async () => {
|
||||
const type = 'dashboard';
|
||||
|
@ -78,16 +180,54 @@ describe('findRelationships', () => {
|
|||
{
|
||||
size,
|
||||
savedObjectsClient,
|
||||
savedObjectsManagement,
|
||||
savedObjectTypes: ['dashboard', 'visualization', 'search', 'index-pattern'],
|
||||
},
|
||||
);
|
||||
expect(result).to.eql({
|
||||
visualization: [
|
||||
{ id: '1', title: 'Foo' },
|
||||
{ id: '2', title: 'Bar' },
|
||||
{ id: '3', title: 'FooBar' },
|
||||
],
|
||||
});
|
||||
expect(result).to.eql([
|
||||
{
|
||||
id: '1',
|
||||
type: 'visualization',
|
||||
relationship: 'parent',
|
||||
meta: {
|
||||
icon: 'visualizeApp',
|
||||
title: 'Foo',
|
||||
editUrl: '/management/kibana/objects/savedVisualizations/1',
|
||||
inAppUrl: {
|
||||
path: '/app/kibana#/visualize/edit/1',
|
||||
uiCapabilitiesPath: 'visualize.show',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: 'visualization',
|
||||
relationship: 'parent',
|
||||
meta: {
|
||||
icon: 'visualizeApp',
|
||||
title: 'Bar',
|
||||
editUrl: '/management/kibana/objects/savedVisualizations/2',
|
||||
inAppUrl: {
|
||||
path: '/app/kibana#/visualize/edit/2',
|
||||
uiCapabilitiesPath: 'visualize.show',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
type: 'visualization',
|
||||
relationship: 'parent',
|
||||
meta: {
|
||||
icon: 'visualizeApp',
|
||||
title: 'FooBar',
|
||||
editUrl: '/management/kibana/objects/savedVisualizations/3',
|
||||
inAppUrl: {
|
||||
path: '/app/kibana#/visualize/edit/3',
|
||||
uiCapabilitiesPath: 'visualize.show',
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should find relationships for visualizations', async () => {
|
||||
|
@ -167,18 +307,57 @@ describe('findRelationships', () => {
|
|||
{
|
||||
size,
|
||||
savedObjectsClient,
|
||||
savedObjectsManagement,
|
||||
savedObjectTypes: ['dashboard', 'visualization', 'search', 'index-pattern'],
|
||||
},
|
||||
);
|
||||
expect(result).to.eql({
|
||||
'index-pattern': [
|
||||
{ id: '1', title: 'My Index Pattern' },
|
||||
],
|
||||
dashboard: [
|
||||
{ id: '1', title: 'My Dashboard' },
|
||||
{ id: '2', title: 'Your Dashboard' },
|
||||
],
|
||||
});
|
||||
expect(result).to.eql([
|
||||
{
|
||||
id: '1',
|
||||
type: 'index-pattern',
|
||||
relationship: 'child',
|
||||
meta:
|
||||
{
|
||||
icon: 'indexPatternApp',
|
||||
title: 'My Index Pattern',
|
||||
editUrl: '/management/kibana/index_patterns/1',
|
||||
inAppUrl: {
|
||||
path: '/app/kibana#/management/kibana/index_patterns/1',
|
||||
uiCapabilitiesPath: 'management.kibana.index_patterns',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '1',
|
||||
type: 'dashboard',
|
||||
relationship: 'parent',
|
||||
meta:
|
||||
{
|
||||
icon: 'dashboardApp',
|
||||
title: 'My Dashboard',
|
||||
editUrl: '/management/kibana/objects/savedDashboards/1',
|
||||
inAppUrl: {
|
||||
path: '/app/kibana#/dashboard/1',
|
||||
uiCapabilitiesPath: 'dashboard.show',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: 'dashboard',
|
||||
relationship: 'parent',
|
||||
meta:
|
||||
{
|
||||
icon: 'dashboardApp',
|
||||
title: 'Your Dashboard',
|
||||
editUrl: '/management/kibana/objects/savedDashboards/2',
|
||||
inAppUrl: {
|
||||
path: '/app/kibana#/dashboard/2',
|
||||
uiCapabilitiesPath: 'dashboard.show',
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should find relationships for saved searches', async () => {
|
||||
|
@ -247,17 +426,72 @@ describe('findRelationships', () => {
|
|||
{
|
||||
size,
|
||||
savedObjectsClient,
|
||||
savedObjectsManagement,
|
||||
savedObjectTypes: ['dashboard', 'visualization', 'search', 'index-pattern'],
|
||||
},
|
||||
);
|
||||
expect(result).to.eql({
|
||||
visualization: [
|
||||
{ id: '1', title: 'Foo' },
|
||||
{ id: '2', title: 'Bar' },
|
||||
{ id: '3', title: 'FooBar' },
|
||||
],
|
||||
'index-pattern': [{ id: '1', title: 'My Index Pattern' }],
|
||||
});
|
||||
expect(result).to.eql([
|
||||
{
|
||||
id: '1',
|
||||
type: 'index-pattern',
|
||||
relationship: 'child',
|
||||
meta:
|
||||
{
|
||||
icon: 'indexPatternApp',
|
||||
title: 'My Index Pattern',
|
||||
editUrl: '/management/kibana/index_patterns/1',
|
||||
inAppUrl: {
|
||||
path: '/app/kibana#/management/kibana/index_patterns/1',
|
||||
uiCapabilitiesPath: 'management.kibana.index_patterns',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '1',
|
||||
type: 'visualization',
|
||||
relationship: 'parent',
|
||||
meta:
|
||||
{
|
||||
icon: 'visualizeApp',
|
||||
title: 'Foo',
|
||||
editUrl: '/management/kibana/objects/savedVisualizations/1',
|
||||
inAppUrl: {
|
||||
path: '/app/kibana#/visualize/edit/1',
|
||||
uiCapabilitiesPath: 'visualize.show',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: 'visualization',
|
||||
relationship: 'parent',
|
||||
meta:
|
||||
{
|
||||
icon: 'visualizeApp',
|
||||
title: 'Bar',
|
||||
editUrl: '/management/kibana/objects/savedVisualizations/2',
|
||||
inAppUrl: {
|
||||
path: '/app/kibana#/visualize/edit/2',
|
||||
uiCapabilitiesPath: 'visualize.show',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
type: 'visualization',
|
||||
relationship: 'parent',
|
||||
meta:
|
||||
{
|
||||
icon: 'visualizeApp',
|
||||
title: 'FooBar',
|
||||
editUrl: '/management/kibana/objects/savedVisualizations/3',
|
||||
inAppUrl: {
|
||||
path: '/app/kibana#/visualize/edit/3',
|
||||
uiCapabilitiesPath: 'visualize.show',
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should find relationships for index patterns', async () => {
|
||||
|
@ -328,13 +562,72 @@ describe('findRelationships', () => {
|
|||
{
|
||||
size,
|
||||
savedObjectsClient,
|
||||
savedObjectsManagement,
|
||||
savedObjectTypes: ['dashboard', 'visualization', 'search', 'index-pattern'],
|
||||
},
|
||||
);
|
||||
expect(result).to.eql({
|
||||
visualization: [{ id: '1', title: 'Foo' }, { id: '2', title: 'Bar' }, { id: '3', title: 'FooBar' }],
|
||||
search: [{ id: '1', title: 'My Saved Search' }],
|
||||
});
|
||||
expect(result).to.eql([
|
||||
{
|
||||
id: '1',
|
||||
type: 'visualization',
|
||||
relationship: 'parent',
|
||||
meta:
|
||||
{
|
||||
icon: 'visualizeApp',
|
||||
title: 'Foo',
|
||||
editUrl: '/management/kibana/objects/savedVisualizations/1',
|
||||
inAppUrl: {
|
||||
path: '/app/kibana#/visualize/edit/1',
|
||||
uiCapabilitiesPath: 'visualize.show',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: 'visualization',
|
||||
relationship: 'parent',
|
||||
meta:
|
||||
{
|
||||
icon: 'visualizeApp',
|
||||
title: 'Bar',
|
||||
editUrl: '/management/kibana/objects/savedVisualizations/2',
|
||||
inAppUrl: {
|
||||
path: '/app/kibana#/visualize/edit/2',
|
||||
uiCapabilitiesPath: 'visualize.show',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
type: 'visualization',
|
||||
relationship: 'parent',
|
||||
meta:
|
||||
{
|
||||
icon: 'visualizeApp',
|
||||
title: 'FooBar',
|
||||
editUrl: '/management/kibana/objects/savedVisualizations/3',
|
||||
inAppUrl: {
|
||||
path: '/app/kibana#/visualize/edit/3',
|
||||
uiCapabilitiesPath: 'visualize.show',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '1',
|
||||
type: 'search',
|
||||
relationship: 'parent',
|
||||
meta:
|
||||
{
|
||||
icon: 'search',
|
||||
title: 'My Saved Search',
|
||||
editUrl: '/management/kibana/objects/savedSearches/1',
|
||||
inAppUrl: {
|
||||
path: '/app/kibana#/discover/1',
|
||||
uiCapabilitiesPath: 'discover.show',
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return an empty object for non related objects', async () => {
|
||||
|
@ -360,6 +653,7 @@ describe('findRelationships', () => {
|
|||
{
|
||||
size,
|
||||
savedObjectsClient,
|
||||
savedObjectsManagement,
|
||||
savedObjectTypes: ['dashboard', 'visualization', 'search', 'index-pattern'],
|
||||
},
|
||||
);
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
export function injectMetaAttributes(savedObject, savedObjectsManagement) {
|
||||
const result = {
|
||||
...savedObject,
|
||||
meta: savedObject.meta || {},
|
||||
};
|
||||
|
||||
// Add extra meta information
|
||||
result.meta.icon = savedObjectsManagement.getIcon(savedObject.type);
|
||||
result.meta.title = savedObjectsManagement.getTitle(savedObject);
|
||||
result.meta.editUrl = savedObjectsManagement.getEditUrl(savedObject);
|
||||
result.meta.inAppUrl = savedObjectsManagement.getInAppUrl(savedObject);
|
||||
|
||||
return result;
|
||||
}
|
|
@ -0,0 +1,146 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { injectMetaAttributes } from './inject_meta_attributes';
|
||||
|
||||
function getManagementMock(savedObjectSchemas) {
|
||||
return {
|
||||
isImportAndExportable(type) {
|
||||
return !savedObjectSchemas[type] || savedObjectSchemas[type].isImportableAndExportable !== false;
|
||||
},
|
||||
getDefaultSearchField(type) {
|
||||
return savedObjectSchemas[type] && savedObjectSchemas[type].defaultSearchField;
|
||||
},
|
||||
getIcon(type) {
|
||||
return savedObjectSchemas[type] && savedObjectSchemas[type].icon;
|
||||
},
|
||||
getTitle(savedObject) {
|
||||
const { type } = savedObject;
|
||||
const getTitle = savedObjectSchemas[type] && savedObjectSchemas[type].getTitle;
|
||||
if (getTitle) {
|
||||
return getTitle(savedObject);
|
||||
}
|
||||
},
|
||||
getEditUrl(savedObject) {
|
||||
const { type } = savedObject;
|
||||
const getEditUrl = savedObjectSchemas[type] && savedObjectSchemas[type].getEditUrl;
|
||||
if (getEditUrl) {
|
||||
return getEditUrl(savedObject);
|
||||
}
|
||||
},
|
||||
getInAppUrl(savedObject) {
|
||||
const { type } = savedObject;
|
||||
const getInAppUrl = savedObjectSchemas[type] && savedObjectSchemas[type].getInAppUrl;
|
||||
if (getInAppUrl) {
|
||||
return getInAppUrl(savedObject);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test('works when no schema is defined for the type', () => {
|
||||
const savedObject = { type: 'a' };
|
||||
const savedObjectsManagement = getManagementMock({});
|
||||
const result = injectMetaAttributes(savedObject, savedObjectsManagement);
|
||||
expect(result).toEqual({ type: 'a', meta: {} });
|
||||
});
|
||||
|
||||
test('inject icon into meta attribute', () => {
|
||||
const savedObject = {
|
||||
type: 'a',
|
||||
};
|
||||
const savedObjectsManagement = getManagementMock({
|
||||
a: {
|
||||
icon: 'my-icon',
|
||||
},
|
||||
});
|
||||
const result = injectMetaAttributes(savedObject, savedObjectsManagement);
|
||||
expect(result).toEqual({
|
||||
type: 'a',
|
||||
meta: {
|
||||
icon: 'my-icon',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('injects title into meta attribute', () => {
|
||||
const savedObject = {
|
||||
type: 'a',
|
||||
};
|
||||
const savedObjectsManagement = getManagementMock({
|
||||
a: {
|
||||
getTitle() {
|
||||
return 'my-title';
|
||||
},
|
||||
},
|
||||
});
|
||||
const result = injectMetaAttributes(savedObject, savedObjectsManagement);
|
||||
expect(result).toEqual({
|
||||
type: 'a',
|
||||
meta: {
|
||||
title: 'my-title',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('injects editUrl into meta attribute', () => {
|
||||
const savedObject = {
|
||||
type: 'a',
|
||||
};
|
||||
const savedObjectsManagement = getManagementMock({
|
||||
a: {
|
||||
getEditUrl() {
|
||||
return 'my-edit-url';
|
||||
},
|
||||
},
|
||||
});
|
||||
const result = injectMetaAttributes(savedObject, savedObjectsManagement);
|
||||
expect(result).toEqual({
|
||||
type: 'a',
|
||||
meta: {
|
||||
editUrl: 'my-edit-url',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('injects inAppUrl meta attribute', () => {
|
||||
const savedObject = {
|
||||
type: 'a',
|
||||
};
|
||||
const savedObjectsManagement = getManagementMock({
|
||||
a: {
|
||||
getInAppUrl() {
|
||||
return {
|
||||
path: 'my-in-app-url',
|
||||
uiCapabilitiesPath: 'ui.path',
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
const result = injectMetaAttributes(savedObject, savedObjectsManagement);
|
||||
expect(result).toEqual({
|
||||
type: 'a',
|
||||
meta: {
|
||||
inAppUrl: {
|
||||
path: 'my-in-app-url',
|
||||
uiCapabilitiesPath: 'ui.path',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
|
@ -17,50 +17,53 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { pick } from 'lodash';
|
||||
import { injectMetaAttributes } from './inject_meta_attributes';
|
||||
|
||||
export async function findRelationships(type, id, options = {}) {
|
||||
const {
|
||||
size,
|
||||
savedObjectsClient,
|
||||
savedObjectTypes,
|
||||
savedObjectsManagement,
|
||||
} = options;
|
||||
|
||||
const { references = [] } = await savedObjectsClient.get(type, id);
|
||||
|
||||
// we filter the objects which we execute bulk requests for based on the saved
|
||||
// object types as well, these are the only types we should be concerned with
|
||||
const bulkGetOpts = references
|
||||
.filter(({ type }) => savedObjectTypes.includes(type))
|
||||
.map(ref => ({ id: ref.id, type: ref.type }));
|
||||
// Use a map to avoid duplicates, it does happen but have a different "name" in the reference
|
||||
const referencedToBulkGetOpts = new Map(
|
||||
references.map(({ type, id }) => [`${type}:${id}`, { id, type }])
|
||||
);
|
||||
|
||||
const [referencedObjects, referencedResponse] = await Promise.all([
|
||||
bulkGetOpts.length > 0
|
||||
? savedObjectsClient.bulkGet(bulkGetOpts)
|
||||
referencedToBulkGetOpts.size > 0
|
||||
? savedObjectsClient.bulkGet([...referencedToBulkGetOpts.values()])
|
||||
: Promise.resolve({ saved_objects: [] }),
|
||||
savedObjectsClient.find({
|
||||
hasReference: { type, id },
|
||||
perPage: size,
|
||||
fields: ['title'],
|
||||
type: savedObjectTypes,
|
||||
}),
|
||||
]);
|
||||
|
||||
const relationshipObjects = [].concat(
|
||||
referencedObjects.saved_objects.map(extractCommonProperties),
|
||||
referencedResponse.saved_objects.map(extractCommonProperties),
|
||||
return [].concat(
|
||||
referencedObjects.saved_objects
|
||||
.map(obj => injectMetaAttributes(obj, savedObjectsManagement))
|
||||
.map(extractCommonProperties)
|
||||
.map(obj => ({
|
||||
...obj,
|
||||
relationship: 'child',
|
||||
})),
|
||||
referencedResponse.saved_objects
|
||||
.map(obj => injectMetaAttributes(obj, savedObjectsManagement))
|
||||
.map(extractCommonProperties)
|
||||
.map(obj => ({
|
||||
...obj,
|
||||
relationship: 'parent',
|
||||
})),
|
||||
);
|
||||
|
||||
return relationshipObjects.reduce((result, relationshipObject) => {
|
||||
const objectsForType = (result[relationshipObject.type] || []);
|
||||
const { type, ...relationshipObjectWithoutType } = relationshipObject;
|
||||
result[type] = objectsForType.concat(relationshipObjectWithoutType);
|
||||
return result;
|
||||
}, {});
|
||||
}
|
||||
|
||||
function extractCommonProperties(savedObject) {
|
||||
return {
|
||||
id: savedObject.id,
|
||||
type: savedObject.type,
|
||||
title: savedObject.attributes.title,
|
||||
};
|
||||
return pick(savedObject, ['id', 'type', 'meta']);
|
||||
}
|
||||
|
|
|
@ -17,11 +17,13 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { registerFind } from './saved_objects/find';
|
||||
import { registerRelationships } from './saved_objects/relationships';
|
||||
import { registerScrollForExportRoute, registerScrollForCountRoute } from './saved_objects/scroll';
|
||||
|
||||
export function managementApi(server) {
|
||||
registerRelationships(server);
|
||||
registerFind(server);
|
||||
registerScrollForExportRoute(server);
|
||||
registerScrollForCountRoute(server);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,105 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* This file wraps the saved object `_find` API and is designed specifically for the saved object
|
||||
* management UI. The main difference is this will inject a root `meta` attribute on each saved object
|
||||
* that the UI depends on. The meta fields come from functions within uiExports which can't be
|
||||
* injected into the front end when defined within uiExports. There are alternatives to this but have
|
||||
* decided to go with this approach at the time of development.
|
||||
*/
|
||||
|
||||
import Joi from 'joi';
|
||||
import { injectMetaAttributes } from '../../../../lib/management/saved_objects/inject_meta_attributes';
|
||||
|
||||
export function registerFind(server) {
|
||||
server.route({
|
||||
path: '/api/kibana/management/saved_objects/_find',
|
||||
method: 'GET',
|
||||
config: {
|
||||
validate: {
|
||||
query: Joi.object()
|
||||
.keys({
|
||||
perPage: Joi.number()
|
||||
.min(0)
|
||||
.default(20),
|
||||
page: Joi.number()
|
||||
.min(0)
|
||||
.default(1),
|
||||
type: Joi.array()
|
||||
.items(Joi.string())
|
||||
.single()
|
||||
.required(),
|
||||
search: Joi.string()
|
||||
.allow('')
|
||||
.optional(),
|
||||
defaultSearchOperator: Joi.string()
|
||||
.valid('OR', 'AND')
|
||||
.default('OR'),
|
||||
sortField: Joi.string(),
|
||||
hasReference: Joi.object()
|
||||
.keys({
|
||||
type: Joi.string().required(),
|
||||
id: Joi.string().required(),
|
||||
})
|
||||
.optional(),
|
||||
fields: Joi.array()
|
||||
.items(Joi.string())
|
||||
.single(),
|
||||
})
|
||||
.default(),
|
||||
},
|
||||
},
|
||||
async handler(request) {
|
||||
const searchFields = new Set();
|
||||
const searchTypes = request.query.type;
|
||||
const savedObjectsClient = request.getSavedObjectsClient();
|
||||
const savedObjectsManagement = server.getSavedObjectsManagement();
|
||||
const importAndExportableTypes = searchTypes.filter(type => savedObjectsManagement.isImportAndExportable(type));
|
||||
|
||||
// Accumulate "defaultSearchField" attributes from savedObjectsManagement. Unfortunately
|
||||
// search fields apply to all types of saved objects, the sum of these fields will
|
||||
// be searched on for each object.
|
||||
for (const type of importAndExportableTypes) {
|
||||
const searchField = savedObjectsManagement.getDefaultSearchField(type);
|
||||
if (searchField) {
|
||||
searchFields.add(searchField);
|
||||
}
|
||||
}
|
||||
|
||||
const findResponse = await savedObjectsClient.find({
|
||||
...request.query,
|
||||
fields: undefined,
|
||||
searchFields: [...searchFields],
|
||||
});
|
||||
return {
|
||||
...findResponse,
|
||||
saved_objects: findResponse.saved_objects
|
||||
.map(obj => injectMetaAttributes(obj, savedObjectsManagement))
|
||||
.map(obj => {
|
||||
const result = { ...obj, attributes: {} };
|
||||
for (const field of request.query.fields || []) {
|
||||
result.attributes[field] = obj.attributes[field];
|
||||
}
|
||||
return result;
|
||||
})
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
|
@ -43,10 +43,12 @@ export function registerRelationships(server) {
|
|||
const size = req.query.size;
|
||||
const savedObjectTypes = req.query.savedObjectTypes;
|
||||
const savedObjectsClient = req.getSavedObjectsClient();
|
||||
const savedObjectsManagement = req.server.getSavedObjectsManagement();
|
||||
|
||||
return await findRelationships(type, id, {
|
||||
size,
|
||||
savedObjectsClient,
|
||||
savedObjectsManagement,
|
||||
savedObjectTypes,
|
||||
});
|
||||
},
|
||||
|
|
8
src/legacy/server/kbn_server.d.ts
vendored
8
src/legacy/server/kbn_server.d.ts
vendored
|
@ -31,7 +31,12 @@ import { ApmOssPlugin } from '../core_plugins/apm_oss';
|
|||
import { CallClusterWithRequest, ElasticsearchPlugin } from '../core_plugins/elasticsearch';
|
||||
|
||||
import { IndexPatternsServiceFactory } from './index_patterns';
|
||||
import { SavedObjectsClient, SavedObjectsService } from './saved_objects';
|
||||
import {
|
||||
SavedObjectsClient,
|
||||
SavedObjectsService,
|
||||
SavedObjectsSchema,
|
||||
SavedObjectsManagement,
|
||||
} from './saved_objects';
|
||||
|
||||
export interface KibanaConfig {
|
||||
get<T>(key: string): T;
|
||||
|
@ -58,6 +63,7 @@ declare module 'hapi' {
|
|||
savedObjects: SavedObjectsService;
|
||||
injectUiAppVars: (pluginName: string, getAppVars: () => { [key: string]: any }) => void;
|
||||
getHiddenUiAppById(appId: string): UiApp;
|
||||
savedObjectsManagement(): SavedObjectsManagement;
|
||||
}
|
||||
|
||||
interface Request {
|
||||
|
|
|
@ -27,56 +27,132 @@ describe('collectSavedObjects()', () => {
|
|||
this.push(null);
|
||||
},
|
||||
});
|
||||
const objects = await collectSavedObjects(readStream, 10);
|
||||
expect(objects).toMatchInlineSnapshot(`Array []`);
|
||||
const result = await collectSavedObjects({ readStream, objectLimit: 10, supportedTypes: [] });
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"collectedObjects": Array [],
|
||||
"errors": Array [],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('collects objects from stream', async () => {
|
||||
const readStream = new Readable({
|
||||
read() {
|
||||
this.push('{"foo":true}');
|
||||
this.push('{"foo":true,"type":"a"}');
|
||||
this.push(null);
|
||||
},
|
||||
});
|
||||
const objects = await collectSavedObjects(readStream, 1);
|
||||
expect(objects).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"foo": true,
|
||||
"migrationVersion": Object {},
|
||||
},
|
||||
]
|
||||
const result = await collectSavedObjects({
|
||||
readStream,
|
||||
objectLimit: 1,
|
||||
supportedTypes: ['a'],
|
||||
});
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"collectedObjects": Array [
|
||||
Object {
|
||||
"foo": true,
|
||||
"migrationVersion": Object {},
|
||||
"type": "a",
|
||||
},
|
||||
],
|
||||
"errors": Array [],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('filters out empty lines', async () => {
|
||||
const readStream = new Readable({
|
||||
read() {
|
||||
this.push('{"foo":true}\n\n');
|
||||
this.push('{"foo":true,"type":"a"}\n\n');
|
||||
this.push(null);
|
||||
},
|
||||
});
|
||||
const objects = await collectSavedObjects(readStream, 1);
|
||||
expect(objects).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"foo": true,
|
||||
"migrationVersion": Object {},
|
||||
},
|
||||
]
|
||||
const result = await collectSavedObjects({
|
||||
readStream,
|
||||
objectLimit: 1,
|
||||
supportedTypes: ['a'],
|
||||
});
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"collectedObjects": Array [
|
||||
Object {
|
||||
"foo": true,
|
||||
"migrationVersion": Object {},
|
||||
"type": "a",
|
||||
},
|
||||
],
|
||||
"errors": Array [],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('throws error when object limit is reached', async () => {
|
||||
const readStream = new Readable({
|
||||
read() {
|
||||
this.push('{"foo":true}\n');
|
||||
this.push('{"bar":true}\n');
|
||||
this.push('{"foo":true,"type":"a"}\n');
|
||||
this.push('{"bar":true,"type":"a"}\n');
|
||||
this.push(null);
|
||||
},
|
||||
});
|
||||
await expect(collectSavedObjects(readStream, 1)).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"Can't import more than 1 objects"`
|
||||
);
|
||||
await expect(
|
||||
collectSavedObjects({
|
||||
readStream,
|
||||
objectLimit: 1,
|
||||
supportedTypes: ['a'],
|
||||
})
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(`"Can't import more than 1 objects"`);
|
||||
});
|
||||
|
||||
test('unsupported types return as import errors', async () => {
|
||||
const readStream = new Readable({
|
||||
read() {
|
||||
this.push('{"id":"1","type":"a","attributes":{"title":"my title"}}\n');
|
||||
this.push('{"id":"2","type":"b","attributes":{"title":"my title 2"}}\n');
|
||||
this.push(null);
|
||||
},
|
||||
});
|
||||
const result = await collectSavedObjects({ readStream, objectLimit: 2, supportedTypes: ['1'] });
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"collectedObjects": Array [],
|
||||
"errors": Array [
|
||||
Object {
|
||||
"error": Object {
|
||||
"type": "unsupported_type",
|
||||
},
|
||||
"id": "1",
|
||||
"title": "my title",
|
||||
"type": "a",
|
||||
},
|
||||
Object {
|
||||
"error": Object {
|
||||
"type": "unsupported_type",
|
||||
},
|
||||
"id": "2",
|
||||
"title": "my title 2",
|
||||
"type": "b",
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('unsupported types still count towards object limit', async () => {
|
||||
const readStream = new Readable({
|
||||
read() {
|
||||
this.push('{"foo":true,"type":"a"}\n');
|
||||
this.push('{"bar":true,"type":"b"}\n');
|
||||
this.push(null);
|
||||
},
|
||||
});
|
||||
await expect(
|
||||
collectSavedObjects({
|
||||
readStream,
|
||||
objectLimit: 1,
|
||||
supportedTypes: ['a'],
|
||||
})
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(`"Can't import more than 1 objects"`);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -27,13 +27,23 @@ import {
|
|||
} from '../../../utils/streams';
|
||||
import { SavedObject } from '../service';
|
||||
import { createLimitStream } from './create_limit_stream';
|
||||
import { ImportError } from './types';
|
||||
|
||||
export async function collectSavedObjects(
|
||||
readStream: Readable,
|
||||
objectLimit: number,
|
||||
filter?: (obj: SavedObject) => boolean
|
||||
): Promise<SavedObject[]> {
|
||||
return (await createPromiseFromStreams([
|
||||
interface CollectSavedObjectsOptions {
|
||||
readStream: Readable;
|
||||
objectLimit: number;
|
||||
filter?: (obj: SavedObject) => boolean;
|
||||
supportedTypes: string[];
|
||||
}
|
||||
|
||||
export async function collectSavedObjects({
|
||||
readStream,
|
||||
objectLimit,
|
||||
filter,
|
||||
supportedTypes,
|
||||
}: CollectSavedObjectsOptions) {
|
||||
const errors: ImportError[] = [];
|
||||
const collectedObjects: SavedObject[] = await createPromiseFromStreams([
|
||||
readStream,
|
||||
createSplitStream('\n'),
|
||||
createMapStream((str: string) => {
|
||||
|
@ -43,11 +53,29 @@ export async function collectSavedObjects(
|
|||
}),
|
||||
createFilterStream<SavedObject>(obj => !!obj),
|
||||
createLimitStream(objectLimit),
|
||||
createFilterStream<SavedObject>(obj => {
|
||||
if (supportedTypes.includes(obj.type)) {
|
||||
return true;
|
||||
}
|
||||
errors.push({
|
||||
id: obj.id,
|
||||
type: obj.type,
|
||||
title: obj.attributes.title,
|
||||
error: {
|
||||
type: 'unsupported_type',
|
||||
},
|
||||
});
|
||||
return false;
|
||||
}),
|
||||
createFilterStream<SavedObject>(obj => (filter ? filter(obj) : true)),
|
||||
createMapStream((obj: SavedObject) => {
|
||||
// Ensure migrations execute on every saved object
|
||||
return Object.assign({ migrationVersion: {} }, obj);
|
||||
}),
|
||||
createConcatStream([]),
|
||||
])) as SavedObject[];
|
||||
]);
|
||||
return {
|
||||
errors,
|
||||
collectedObjects,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -82,6 +82,7 @@ describe('importSavedObjects()', () => {
|
|||
objectLimit: 1,
|
||||
overwrite: false,
|
||||
savedObjectsClient,
|
||||
supportedTypes: [],
|
||||
});
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
|
@ -107,6 +108,7 @@ Object {
|
|||
objectLimit: 4,
|
||||
overwrite: false,
|
||||
savedObjectsClient,
|
||||
supportedTypes: ['index-pattern', 'search', 'visualization', 'dashboard'],
|
||||
});
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
|
@ -187,6 +189,7 @@ Object {
|
|||
objectLimit: 4,
|
||||
overwrite: true,
|
||||
savedObjectsClient,
|
||||
supportedTypes: ['index-pattern', 'search', 'visualization', 'dashboard'],
|
||||
});
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
|
@ -274,6 +277,7 @@ Object {
|
|||
objectLimit: 4,
|
||||
overwrite: false,
|
||||
savedObjectsClient,
|
||||
supportedTypes: ['index-pattern', 'search', 'visualization', 'dashboard'],
|
||||
});
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
|
@ -372,6 +376,7 @@ Object {
|
|||
objectLimit: 4,
|
||||
overwrite: false,
|
||||
savedObjectsClient,
|
||||
supportedTypes: ['index-pattern', 'search', 'visualization', 'dashboard'],
|
||||
});
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
|
@ -423,6 +428,98 @@ Object {
|
|||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('validates supported types', async () => {
|
||||
const readStream = new Readable({
|
||||
read() {
|
||||
savedObjects.forEach(obj => this.push(JSON.stringify(obj) + '\n'));
|
||||
this.push('{"id":"1","type":"wigwags","attributes":{"title":"my title"},"references":[]}');
|
||||
this.push(null);
|
||||
},
|
||||
});
|
||||
savedObjectsClient.find.mockResolvedValueOnce({ saved_objects: [] });
|
||||
savedObjectsClient.bulkCreate.mockResolvedValue({
|
||||
saved_objects: savedObjects,
|
||||
});
|
||||
const result = await importSavedObjects({
|
||||
readStream,
|
||||
objectLimit: 5,
|
||||
overwrite: false,
|
||||
savedObjectsClient,
|
||||
supportedTypes: ['index-pattern', 'search', 'visualization', 'dashboard'],
|
||||
});
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"errors": Array [
|
||||
Object {
|
||||
"error": Object {
|
||||
"type": "unsupported_type",
|
||||
},
|
||||
"id": "1",
|
||||
"title": "my title",
|
||||
"type": "wigwags",
|
||||
},
|
||||
],
|
||||
"success": false,
|
||||
"successCount": 4,
|
||||
}
|
||||
`);
|
||||
expect(savedObjectsClient.bulkCreate).toMatchInlineSnapshot(`
|
||||
[MockFunction] {
|
||||
"calls": Array [
|
||||
Array [
|
||||
Array [
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"title": "My Index Pattern",
|
||||
},
|
||||
"id": "1",
|
||||
"migrationVersion": Object {},
|
||||
"references": Array [],
|
||||
"type": "index-pattern",
|
||||
},
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"title": "My Search",
|
||||
},
|
||||
"id": "2",
|
||||
"migrationVersion": Object {},
|
||||
"references": Array [],
|
||||
"type": "search",
|
||||
},
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"title": "My Visualization",
|
||||
},
|
||||
"id": "3",
|
||||
"migrationVersion": Object {},
|
||||
"references": Array [],
|
||||
"type": "visualization",
|
||||
},
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"title": "My Dashboard",
|
||||
},
|
||||
"id": "4",
|
||||
"migrationVersion": Object {},
|
||||
"references": Array [],
|
||||
"type": "dashboard",
|
||||
},
|
||||
],
|
||||
Object {
|
||||
"overwrite": false,
|
||||
},
|
||||
],
|
||||
],
|
||||
"results": Array [
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": Promise {},
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -29,6 +29,7 @@ interface ImportSavedObjectsOptions {
|
|||
objectLimit: number;
|
||||
overwrite: boolean;
|
||||
savedObjectsClient: SavedObjectsClient;
|
||||
supportedTypes: string[];
|
||||
}
|
||||
|
||||
interface ImportResponse {
|
||||
|
@ -42,34 +43,45 @@ export async function importSavedObjects({
|
|||
objectLimit,
|
||||
overwrite,
|
||||
savedObjectsClient,
|
||||
supportedTypes,
|
||||
}: ImportSavedObjectsOptions): Promise<ImportResponse> {
|
||||
let errorAccumulator: ImportError[] = [];
|
||||
|
||||
// Get the objects to import
|
||||
const objectsFromStream = await collectSavedObjects(readStream, objectLimit);
|
||||
const {
|
||||
errors: collectorErrors,
|
||||
collectedObjects: objectsFromStream,
|
||||
} = await collectSavedObjects({ readStream, objectLimit, supportedTypes });
|
||||
errorAccumulator = [...errorAccumulator, ...collectorErrors];
|
||||
|
||||
// Validate references
|
||||
const { filteredObjects, errors: validationErrors } = await validateReferences(
|
||||
objectsFromStream,
|
||||
savedObjectsClient
|
||||
);
|
||||
errorAccumulator = [...errorAccumulator, ...validationErrors];
|
||||
|
||||
// Exit early if no objects to import
|
||||
if (filteredObjects.length === 0) {
|
||||
return {
|
||||
success: validationErrors.length === 0,
|
||||
success: errorAccumulator.length === 0,
|
||||
successCount: 0,
|
||||
...(validationErrors.length ? { errors: validationErrors } : {}),
|
||||
...(errorAccumulator.length ? { errors: errorAccumulator } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
// Create objects in bulk
|
||||
const bulkCreateResult = await savedObjectsClient.bulkCreate(filteredObjects, {
|
||||
overwrite,
|
||||
});
|
||||
const errors = [
|
||||
...validationErrors,
|
||||
errorAccumulator = [
|
||||
...errorAccumulator,
|
||||
...extractErrors(bulkCreateResult.saved_objects, filteredObjects),
|
||||
];
|
||||
|
||||
return {
|
||||
success: errors.length === 0,
|
||||
success: errorAccumulator.length === 0,
|
||||
successCount: bulkCreateResult.saved_objects.filter(obj => !obj.error).length,
|
||||
...(errors.length ? { errors } : {}),
|
||||
...(errorAccumulator.length ? { errors: errorAccumulator } : {}),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -92,6 +92,7 @@ describe('resolveImportErrors()', () => {
|
|||
objectLimit: 4,
|
||||
retries: [],
|
||||
savedObjectsClient,
|
||||
supportedTypes: ['index-pattern', 'search', 'visualization', 'dashboard'],
|
||||
});
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
|
@ -124,6 +125,7 @@ Object {
|
|||
},
|
||||
],
|
||||
savedObjectsClient,
|
||||
supportedTypes: ['index-pattern', 'search', 'visualization', 'dashboard'],
|
||||
});
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
|
@ -180,6 +182,7 @@ Object {
|
|||
},
|
||||
],
|
||||
savedObjectsClient,
|
||||
supportedTypes: ['index-pattern', 'search', 'visualization', 'dashboard'],
|
||||
});
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
|
@ -245,6 +248,7 @@ Object {
|
|||
},
|
||||
],
|
||||
savedObjectsClient,
|
||||
supportedTypes: ['index-pattern', 'search', 'visualization', 'dashboard'],
|
||||
});
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
|
@ -312,6 +316,7 @@ Object {
|
|||
replaceReferences: [],
|
||||
})),
|
||||
savedObjectsClient,
|
||||
supportedTypes: ['index-pattern', 'search', 'visualization', 'dashboard'],
|
||||
});
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
|
@ -423,6 +428,7 @@ Object {
|
|||
},
|
||||
],
|
||||
savedObjectsClient,
|
||||
supportedTypes: ['index-pattern', 'search', 'visualization', 'dashboard'],
|
||||
});
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
|
@ -476,4 +482,48 @@ Object {
|
|||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('validates object types', async () => {
|
||||
const readStream = new Readable({
|
||||
read() {
|
||||
savedObjects.forEach(obj => this.push(JSON.stringify(obj) + '\n'));
|
||||
this.push('{"id":"1","type":"wigwags","attributes":{"title":"my title"},"references":[]}');
|
||||
this.push(null);
|
||||
},
|
||||
});
|
||||
savedObjectsClient.bulkCreate.mockResolvedValue({
|
||||
saved_objects: [],
|
||||
});
|
||||
const result = await resolveImportErrors({
|
||||
readStream,
|
||||
objectLimit: 5,
|
||||
retries: [
|
||||
{
|
||||
id: 'i',
|
||||
type: 'wigwags',
|
||||
overwrite: false,
|
||||
replaceReferences: [],
|
||||
},
|
||||
],
|
||||
savedObjectsClient,
|
||||
supportedTypes: ['index-pattern', 'search', 'visualization', 'dashboard'],
|
||||
});
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"errors": Array [
|
||||
Object {
|
||||
"error": Object {
|
||||
"type": "unsupported_type",
|
||||
},
|
||||
"id": "1",
|
||||
"title": "my title",
|
||||
"type": "wigwags",
|
||||
},
|
||||
],
|
||||
"success": false,
|
||||
"successCount": 0,
|
||||
}
|
||||
`);
|
||||
expect(savedObjectsClient.bulkCreate).toMatchInlineSnapshot(`[MockFunction]`);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -31,6 +31,7 @@ interface ResolveImportErrorsOptions {
|
|||
objectLimit: number;
|
||||
savedObjectsClient: SavedObjectsClient;
|
||||
retries: Retry[];
|
||||
supportedTypes: string[];
|
||||
}
|
||||
|
||||
interface ImportResponse {
|
||||
|
@ -44,13 +45,22 @@ export async function resolveImportErrors({
|
|||
objectLimit,
|
||||
retries,
|
||||
savedObjectsClient,
|
||||
supportedTypes,
|
||||
}: ResolveImportErrorsOptions): Promise<ImportResponse> {
|
||||
let successCount = 0;
|
||||
let errors: ImportError[] = [];
|
||||
let errorAccumulator: ImportError[] = [];
|
||||
const filter = createObjectsFilter(retries);
|
||||
|
||||
// Get the objects to resolve errors
|
||||
const objectsToResolve = await collectSavedObjects(readStream, objectLimit, filter);
|
||||
const { errors: collectorErrors, collectedObjects: objectsToResolve } = await collectSavedObjects(
|
||||
{
|
||||
readStream,
|
||||
objectLimit,
|
||||
filter,
|
||||
supportedTypes,
|
||||
}
|
||||
);
|
||||
errorAccumulator = [...errorAccumulator, ...collectorErrors];
|
||||
|
||||
// Create a map of references to replace for each object to avoid iterating through
|
||||
// retries for every object to resolve
|
||||
|
@ -81,7 +91,7 @@ export async function resolveImportErrors({
|
|||
objectsToResolve,
|
||||
savedObjectsClient
|
||||
);
|
||||
errors = errors.concat(validationErrors);
|
||||
errorAccumulator = [...errorAccumulator, ...validationErrors];
|
||||
|
||||
// Bulk create in two batches, overwrites and non-overwrites
|
||||
const { objectsToOverwrite, objectsToNotOverwrite } = splitOverwrites(filteredObjects, retries);
|
||||
|
@ -89,18 +99,24 @@ export async function resolveImportErrors({
|
|||
const bulkCreateResult = await savedObjectsClient.bulkCreate(objectsToOverwrite, {
|
||||
overwrite: true,
|
||||
});
|
||||
errors = errors.concat(extractErrors(bulkCreateResult.saved_objects, objectsToOverwrite));
|
||||
errorAccumulator = [
|
||||
...errorAccumulator,
|
||||
...extractErrors(bulkCreateResult.saved_objects, objectsToOverwrite),
|
||||
];
|
||||
successCount += bulkCreateResult.saved_objects.filter(obj => !obj.error).length;
|
||||
}
|
||||
if (objectsToNotOverwrite.length) {
|
||||
const bulkCreateResult = await savedObjectsClient.bulkCreate(objectsToNotOverwrite);
|
||||
errors = errors.concat(extractErrors(bulkCreateResult.saved_objects, objectsToNotOverwrite));
|
||||
errorAccumulator = [
|
||||
...errorAccumulator,
|
||||
...extractErrors(bulkCreateResult.saved_objects, objectsToNotOverwrite),
|
||||
];
|
||||
successCount += bulkCreateResult.saved_objects.filter(obj => !obj.error).length;
|
||||
}
|
||||
|
||||
return {
|
||||
successCount,
|
||||
success: errors.length === 0,
|
||||
...(errors.length ? { errors } : {}),
|
||||
success: errorAccumulator.length === 0,
|
||||
...(errorAccumulator.length ? { errors: errorAccumulator } : {}),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -32,6 +32,10 @@ export interface ConflictError {
|
|||
type: 'conflict';
|
||||
}
|
||||
|
||||
export interface UnsupportedTypeError {
|
||||
type: 'unsupported_type';
|
||||
}
|
||||
|
||||
export interface UnknownError {
|
||||
type: 'unknown';
|
||||
message: string;
|
||||
|
@ -54,5 +58,5 @@ export interface ImportError {
|
|||
id: string;
|
||||
type: string;
|
||||
title?: string;
|
||||
error: ConflictError | MissingReferencesError | UnknownError;
|
||||
error: ConflictError | UnsupportedTypeError | MissingReferencesError | UnknownError;
|
||||
}
|
||||
|
|
4
src/legacy/server/saved_objects/index.d.ts
vendored
4
src/legacy/server/saved_objects/index.d.ts
vendored
|
@ -26,3 +26,7 @@ export {
|
|||
SavedObjectReference,
|
||||
SavedObjectsService,
|
||||
} from './service';
|
||||
|
||||
export { SavedObjectsSchema } from './schema';
|
||||
|
||||
export { SavedObjectsManagement } from './management';
|
||||
|
|
20
src/legacy/server/saved_objects/management/index.ts
Normal file
20
src/legacy/server/saved_objects/management/index.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
export { SavedObjectsManagement, SavedObjectsManagementDefinition } from './management';
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { SavedObjectsManagement } from './management';
|
||||
|
||||
type Management = PublicMethodsOf<SavedObjectsManagement>;
|
||||
const createManagementMock = () => {
|
||||
const mocked: jest.Mocked<Management> = {
|
||||
isImportAndExportable: jest.fn().mockReturnValue(true),
|
||||
getDefaultSearchField: jest.fn(),
|
||||
getIcon: jest.fn(),
|
||||
getTitle: jest.fn(),
|
||||
getEditUrl: jest.fn(),
|
||||
getInAppUrl: jest.fn(),
|
||||
};
|
||||
return mocked;
|
||||
};
|
||||
|
||||
export const managementMock = {
|
||||
create: createManagementMock,
|
||||
};
|
174
src/legacy/server/saved_objects/management/management.test.ts
Normal file
174
src/legacy/server/saved_objects/management/management.test.ts
Normal file
|
@ -0,0 +1,174 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { SavedObjectsManagement } from './management';
|
||||
|
||||
describe('isImportAndExportable()', () => {
|
||||
it('returns false for unknown types', () => {
|
||||
const management = new SavedObjectsManagement();
|
||||
const result = management.isImportAndExportable('bar');
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true for explicitly importable and exportable type', () => {
|
||||
const management = new SavedObjectsManagement({
|
||||
foo: {
|
||||
isImportableAndExportable: true,
|
||||
},
|
||||
});
|
||||
const result = management.isImportAndExportable('foo');
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for explicitly importable and exportable type', () => {
|
||||
const management = new SavedObjectsManagement({
|
||||
foo: {
|
||||
isImportableAndExportable: false,
|
||||
},
|
||||
});
|
||||
const result = management.isImportAndExportable('foo');
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDefaultSearchField()', () => {
|
||||
it('returns empty for unknown types', () => {
|
||||
const management = new SavedObjectsManagement();
|
||||
const result = management.getDefaultSearchField('bar');
|
||||
expect(result).toEqual(undefined);
|
||||
});
|
||||
|
||||
it('returns explicit value', () => {
|
||||
const management = new SavedObjectsManagement({
|
||||
foo: {
|
||||
defaultSearchField: 'value',
|
||||
},
|
||||
});
|
||||
const result = management.getDefaultSearchField('foo');
|
||||
expect(result).toEqual('value');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getIcon', () => {
|
||||
it('returns empty for unknown types', () => {
|
||||
const management = new SavedObjectsManagement();
|
||||
const result = management.getIcon('bar');
|
||||
expect(result).toEqual(undefined);
|
||||
});
|
||||
|
||||
it('returns explicit value', () => {
|
||||
const management = new SavedObjectsManagement({
|
||||
foo: {
|
||||
icon: 'value',
|
||||
},
|
||||
});
|
||||
const result = management.getIcon('foo');
|
||||
expect(result).toEqual('value');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTitle', () => {
|
||||
it('returns empty for unknown type', () => {
|
||||
const management = new SavedObjectsManagement();
|
||||
const result = management.getTitle({
|
||||
id: '1',
|
||||
type: 'foo',
|
||||
attributes: {},
|
||||
references: [],
|
||||
});
|
||||
expect(result).toEqual(undefined);
|
||||
});
|
||||
|
||||
it('returns explicit value', () => {
|
||||
const management = new SavedObjectsManagement({
|
||||
foo: {
|
||||
getTitle() {
|
||||
return 'called';
|
||||
},
|
||||
},
|
||||
});
|
||||
const result = management.getTitle({
|
||||
id: '1',
|
||||
type: 'foo',
|
||||
attributes: {},
|
||||
references: [],
|
||||
});
|
||||
expect(result).toEqual('called');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getEditUrl()', () => {
|
||||
it('returns empty for unknown type', () => {
|
||||
const management = new SavedObjectsManagement();
|
||||
const result = management.getEditUrl({
|
||||
id: '1',
|
||||
type: 'foo',
|
||||
attributes: {},
|
||||
references: [],
|
||||
});
|
||||
expect(result).toEqual(undefined);
|
||||
});
|
||||
|
||||
it('returns explicit value', () => {
|
||||
const management = new SavedObjectsManagement({
|
||||
foo: {
|
||||
getEditUrl() {
|
||||
return 'called';
|
||||
},
|
||||
},
|
||||
});
|
||||
const result = management.getEditUrl({
|
||||
id: '1',
|
||||
type: 'foo',
|
||||
attributes: {},
|
||||
references: [],
|
||||
});
|
||||
expect(result).toEqual('called');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getInAppUrl()', () => {
|
||||
it('returns empty array for unknown type', () => {
|
||||
const management = new SavedObjectsManagement();
|
||||
const result = management.getInAppUrl({
|
||||
id: '1',
|
||||
type: 'foo',
|
||||
attributes: {},
|
||||
references: [],
|
||||
});
|
||||
expect(result).toEqual(undefined);
|
||||
});
|
||||
|
||||
it('returns explicit value', () => {
|
||||
const management = new SavedObjectsManagement({
|
||||
foo: {
|
||||
getInAppUrl() {
|
||||
return { path: 'called', uiCapabilitiesPath: 'my.path' };
|
||||
},
|
||||
},
|
||||
});
|
||||
const result = management.getInAppUrl({
|
||||
id: '1',
|
||||
type: 'foo',
|
||||
attributes: {},
|
||||
references: [],
|
||||
});
|
||||
expect(result).toEqual({ path: 'called', uiCapabilitiesPath: 'my.path' });
|
||||
});
|
||||
});
|
91
src/legacy/server/saved_objects/management/management.ts
Normal file
91
src/legacy/server/saved_objects/management/management.ts
Normal file
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { SavedObject } from '../service';
|
||||
|
||||
interface SavedObjectsManagementTypeDefinition {
|
||||
isImportableAndExportable?: boolean;
|
||||
defaultSearchField?: string;
|
||||
icon?: string;
|
||||
getTitle?: (savedObject: SavedObject) => string;
|
||||
getEditUrl?: (savedObject: SavedObject) => string;
|
||||
getInAppUrl?: (savedObject: SavedObject) => { path: string; uiCapabilitiesPath: string };
|
||||
}
|
||||
|
||||
export interface SavedObjectsManagementDefinition {
|
||||
[key: string]: SavedObjectsManagementTypeDefinition;
|
||||
}
|
||||
|
||||
export class SavedObjectsManagement {
|
||||
private readonly definition?: SavedObjectsManagementDefinition;
|
||||
|
||||
constructor(managementDefinition?: SavedObjectsManagementDefinition) {
|
||||
this.definition = managementDefinition;
|
||||
}
|
||||
|
||||
public isImportAndExportable(type: string) {
|
||||
if (this.definition && this.definition.hasOwnProperty(type)) {
|
||||
return this.definition[type].isImportableAndExportable === true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public getDefaultSearchField(type: string) {
|
||||
if (this.definition && this.definition.hasOwnProperty(type)) {
|
||||
return this.definition[type].defaultSearchField;
|
||||
}
|
||||
}
|
||||
|
||||
public getIcon(type: string) {
|
||||
if (this.definition && this.definition.hasOwnProperty(type)) {
|
||||
return this.definition[type].icon;
|
||||
}
|
||||
}
|
||||
|
||||
public getTitle(savedObject: SavedObject) {
|
||||
const { type } = savedObject;
|
||||
if (this.definition && this.definition.hasOwnProperty(type) && this.definition[type].getTitle) {
|
||||
const { getTitle } = this.definition[type];
|
||||
if (getTitle) {
|
||||
return getTitle(savedObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public getEditUrl(savedObject: SavedObject) {
|
||||
const { type } = savedObject;
|
||||
if (this.definition && this.definition.hasOwnProperty(type)) {
|
||||
const { getEditUrl } = this.definition[type];
|
||||
if (getEditUrl) {
|
||||
return getEditUrl(savedObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public getInAppUrl(savedObject: SavedObject) {
|
||||
const { type } = savedObject;
|
||||
if (this.definition && this.definition.hasOwnProperty(type)) {
|
||||
const { getInAppUrl } = this.definition[type];
|
||||
if (getInAppUrl) {
|
||||
return getInAppUrl(savedObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -98,6 +98,7 @@ function mockKbnServer({ configValues }: { configValues?: any } = {}) {
|
|||
savedObjectMigrations: {},
|
||||
savedObjectMappings: [],
|
||||
savedObjectSchemas: {},
|
||||
savedObjectsManagement: {},
|
||||
},
|
||||
server: {
|
||||
config: () => ({
|
||||
|
|
|
@ -25,6 +25,7 @@
|
|||
import { once } from 'lodash';
|
||||
import { MappingProperties } from '../../../mappings';
|
||||
import { SavedObjectsSchema, SavedObjectsSchemaDefinition } from '../../schema';
|
||||
import { SavedObjectsManagementDefinition } from '../../management';
|
||||
import { RawSavedObjectDoc, SavedObjectsSerializer } from '../../serialization';
|
||||
import { docValidator } from '../../validation';
|
||||
import { buildActiveMappings, CallCluster, IndexMigrator, LogFn } from '../core';
|
||||
|
@ -39,6 +40,7 @@ export interface KbnServer {
|
|||
savedObjectMigrations: any;
|
||||
savedObjectValidations: any;
|
||||
savedObjectSchemas: SavedObjectsSchemaDefinition;
|
||||
savedObjectsManagement: SavedObjectsManagementDefinition;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -52,7 +52,7 @@ describe('POST /api/saved_objects/_export', () => {
|
|||
},
|
||||
};
|
||||
|
||||
server.route(createExportRoute(prereqs, server));
|
||||
server.route(createExportRoute(prereqs, server, ['index-pattern', 'search']));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
|
|
@ -24,8 +24,6 @@ import { SavedObjectsClient } from '../';
|
|||
import { getSortedObjectsForExport } from '../export';
|
||||
import { Prerequisites } from './types';
|
||||
|
||||
const ALLOWED_TYPES = ['index-pattern', 'search', 'visualization', 'dashboard'];
|
||||
|
||||
interface ExportRequest extends Hapi.Request {
|
||||
pre: {
|
||||
savedObjectsClient: SavedObjectsClient;
|
||||
|
@ -40,7 +38,11 @@ interface ExportRequest extends Hapi.Request {
|
|||
};
|
||||
}
|
||||
|
||||
export const createExportRoute = (prereqs: Prerequisites, server: Hapi.Server) => ({
|
||||
export const createExportRoute = (
|
||||
prereqs: Prerequisites,
|
||||
server: Hapi.Server,
|
||||
supportedTypes: string[]
|
||||
) => ({
|
||||
path: '/api/saved_objects/_export',
|
||||
method: 'POST',
|
||||
config: {
|
||||
|
@ -49,13 +51,13 @@ export const createExportRoute = (prereqs: Prerequisites, server: Hapi.Server) =
|
|||
payload: Joi.object()
|
||||
.keys({
|
||||
type: Joi.array()
|
||||
.items(Joi.string().valid(ALLOWED_TYPES))
|
||||
.items(Joi.string().valid(supportedTypes))
|
||||
.single()
|
||||
.optional(),
|
||||
objects: Joi.array()
|
||||
.items({
|
||||
type: Joi.string()
|
||||
.valid(ALLOWED_TYPES)
|
||||
.valid(supportedTypes)
|
||||
.required(),
|
||||
id: Joi.string().required(),
|
||||
})
|
||||
|
|
|
@ -47,7 +47,9 @@ describe('POST /api/saved_objects/_import', () => {
|
|||
},
|
||||
};
|
||||
|
||||
server.route(createImportRoute(prereqs, server));
|
||||
server.route(
|
||||
createImportRoute(prereqs, server, ['index-pattern', 'visualization', 'dashboard'])
|
||||
);
|
||||
});
|
||||
|
||||
test('formats successful response', async () => {
|
||||
|
|
|
@ -44,7 +44,11 @@ interface ImportRequest extends WithoutQueryAndParams<Hapi.Request> {
|
|||
};
|
||||
}
|
||||
|
||||
export const createImportRoute = (prereqs: Prerequisites, server: Hapi.Server) => ({
|
||||
export const createImportRoute = (
|
||||
prereqs: Prerequisites,
|
||||
server: Hapi.Server,
|
||||
supportedTypes: string[]
|
||||
) => ({
|
||||
path: '/api/saved_objects/_import',
|
||||
method: 'POST',
|
||||
config: {
|
||||
|
@ -73,6 +77,7 @@ export const createImportRoute = (prereqs: Prerequisites, server: Hapi.Server) =
|
|||
return Boom.badRequest(`Invalid file extension ${fileExtension}`);
|
||||
}
|
||||
return await importSavedObjects({
|
||||
supportedTypes,
|
||||
savedObjectsClient,
|
||||
readStream: request.payload.file,
|
||||
objectLimit: request.server.config().get('savedObjects.maxImportExportSize'),
|
||||
|
|
|
@ -47,7 +47,13 @@ describe('POST /api/saved_objects/_resolve_import_errors', () => {
|
|||
},
|
||||
};
|
||||
|
||||
server.route(createResolveImportErrorsRoute(prereqs, server));
|
||||
server.route(
|
||||
createResolveImportErrorsRoute(prereqs, server, [
|
||||
'index-pattern',
|
||||
'visualization',
|
||||
'dashboard',
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
test('formats successful response', async () => {
|
||||
|
|
|
@ -51,7 +51,11 @@ interface ImportRequest extends Hapi.Request {
|
|||
};
|
||||
}
|
||||
|
||||
export const createResolveImportErrorsRoute = (prereqs: Prerequisites, server: Hapi.Server) => ({
|
||||
export const createResolveImportErrorsRoute = (
|
||||
prereqs: Prerequisites,
|
||||
server: Hapi.Server,
|
||||
supportedTypes: string[]
|
||||
) => ({
|
||||
path: '/api/saved_objects/_resolve_import_errors',
|
||||
method: 'POST',
|
||||
config: {
|
||||
|
@ -95,6 +99,7 @@ export const createResolveImportErrorsRoute = (prereqs: Prerequisites, server: H
|
|||
}
|
||||
|
||||
return await resolveImportErrors({
|
||||
supportedTypes,
|
||||
savedObjectsClient,
|
||||
readStream: request.payload.file,
|
||||
retries: request.payload.retries,
|
||||
|
|
|
@ -26,6 +26,7 @@ import {
|
|||
ScopedSavedObjectsClientProvider,
|
||||
} from './service';
|
||||
import { getRootPropertiesObjects } from '../mappings';
|
||||
import { SavedObjectsManagement } from './management';
|
||||
|
||||
import {
|
||||
createBulkCreateRoute,
|
||||
|
@ -41,10 +42,29 @@ import {
|
|||
createLogLegacyImportRoute,
|
||||
} from './routes';
|
||||
|
||||
function getImportableAndExportableTypes({ kbnServer, visibleTypes }) {
|
||||
const { savedObjectsManagement = {} } = kbnServer.uiExports;
|
||||
return visibleTypes.filter(
|
||||
type =>
|
||||
savedObjectsManagement[type] &&
|
||||
savedObjectsManagement[type].isImportableAndExportable === true
|
||||
);
|
||||
}
|
||||
|
||||
export function savedObjectsMixin(kbnServer, server) {
|
||||
const migrator = new KibanaMigrator({ kbnServer });
|
||||
const mappings = migrator.getActiveMappings();
|
||||
const allTypes = Object.keys(getRootPropertiesObjects(mappings));
|
||||
const schema = new SavedObjectsSchema(kbnServer.uiExports.savedObjectSchemas);
|
||||
const visibleTypes = allTypes.filter(type => !schema.isHiddenType(type));
|
||||
const importableAndExportableTypes = getImportableAndExportableTypes({ kbnServer, visibleTypes });
|
||||
|
||||
server.decorate('server', 'kibanaMigrator', migrator);
|
||||
server.decorate(
|
||||
'server',
|
||||
'getSavedObjectsManagement',
|
||||
() => new SavedObjectsManagement(kbnServer.uiExports.savedObjectsManagement)
|
||||
);
|
||||
|
||||
const warn = message => server.log(['warning', 'saved-objects'], message);
|
||||
// we use kibana.index which is technically defined in the kibana plugin, so if
|
||||
|
@ -69,16 +89,12 @@ export function savedObjectsMixin(kbnServer, server) {
|
|||
server.route(createFindRoute(prereqs));
|
||||
server.route(createGetRoute(prereqs));
|
||||
server.route(createUpdateRoute(prereqs));
|
||||
server.route(createExportRoute(prereqs, server));
|
||||
server.route(createImportRoute(prereqs, server));
|
||||
server.route(createResolveImportErrorsRoute(prereqs, server));
|
||||
server.route(createExportRoute(prereqs, server, importableAndExportableTypes));
|
||||
server.route(createImportRoute(prereqs, server, importableAndExportableTypes));
|
||||
server.route(createResolveImportErrorsRoute(prereqs, server, importableAndExportableTypes));
|
||||
server.route(createLogLegacyImportRoute());
|
||||
|
||||
const schema = new SavedObjectsSchema(kbnServer.uiExports.savedObjectSchemas);
|
||||
const serializer = new SavedObjectsSerializer(schema);
|
||||
const mappings = migrator.getActiveMappings();
|
||||
const allTypes = Object.keys(getRootPropertiesObjects(mappings));
|
||||
const visibleTypes = allTypes.filter(type => !schema.isHiddenType(type));
|
||||
|
||||
const createRepository = (callCluster, extraTypes = []) => {
|
||||
if (typeof callCluster !== 'function') {
|
||||
|
|
|
@ -104,7 +104,7 @@ describe('Saved Objects Mixin', () => {
|
|||
'kibanaMigrator',
|
||||
expect.any(Object)
|
||||
);
|
||||
expect(mockServer.decorate).toHaveBeenCalledTimes(1);
|
||||
expect(mockServer.decorate).toHaveBeenCalledTimes(2);
|
||||
expect(mockServer.route).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -26,6 +26,7 @@ export {
|
|||
mappings,
|
||||
migrations,
|
||||
savedObjectSchemas,
|
||||
savedObjectsManagement,
|
||||
validations,
|
||||
} from './saved_object';
|
||||
|
||||
|
|
|
@ -56,6 +56,8 @@ export const migrations = wrap(
|
|||
|
||||
export const savedObjectSchemas = wrap(uniqueKeys(), mergeAtType);
|
||||
|
||||
export const savedObjectsManagement = wrap(uniqueKeys(), mergeAtType);
|
||||
|
||||
// Combines the `validations` property of each plugin,
|
||||
// ensuring that properties are unique across plugins.
|
||||
// See saved_objects/validation for more details.
|
||||
|
|
305
test/api_integration/apis/management/saved_objects/find.js
Normal file
305
test/api_integration/apis/management/saved_objects/find.js
Normal file
|
@ -0,0 +1,305 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
|
||||
export default function ({ getService }) {
|
||||
const es = getService('es');
|
||||
const supertest = getService('supertest');
|
||||
const esArchiver = getService('esArchiver');
|
||||
|
||||
describe('find', () => {
|
||||
describe('with kibana index', () => {
|
||||
before(() => esArchiver.load('saved_objects/basic'));
|
||||
after(() => esArchiver.unload('saved_objects/basic'));
|
||||
|
||||
it('should return 200 with individual responses', async () => (
|
||||
await supertest
|
||||
.get('/api/kibana/management/saved_objects/_find?type=visualization&fields=title')
|
||||
.expect(200)
|
||||
.then(resp => {
|
||||
expect(resp.body).to.eql({
|
||||
page: 1,
|
||||
per_page: 20,
|
||||
total: 1,
|
||||
saved_objects: [
|
||||
{
|
||||
type: 'visualization',
|
||||
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
|
||||
version: 'WzIsMV0=',
|
||||
attributes: {
|
||||
'title': 'Count of requests'
|
||||
},
|
||||
migrationVersion: resp.body.saved_objects[0].migrationVersion,
|
||||
references: [
|
||||
{
|
||||
id: '91200a00-9efd-11e7-acb3-3dab96693fab',
|
||||
name: 'kibanaSavedObjectMeta.searchSourceJSON.index',
|
||||
type: 'index-pattern',
|
||||
},
|
||||
],
|
||||
updated_at: '2017-09-21T18:51:23.794Z',
|
||||
meta: {
|
||||
editUrl: '/management/kibana/objects/savedVisualizations/dd7caf20-9efd-11e7-acb3-3dab96693fab',
|
||||
icon: 'visualizeApp',
|
||||
inAppUrl: {
|
||||
path: '/app/kibana#/visualize/edit/dd7caf20-9efd-11e7-acb3-3dab96693fab',
|
||||
uiCapabilitiesPath: 'visualize.show',
|
||||
},
|
||||
title: 'Count of requests',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
})
|
||||
));
|
||||
|
||||
describe('unknown type', () => {
|
||||
it('should return 200 with empty response', async () => (
|
||||
await supertest
|
||||
.get('/api/kibana/management/saved_objects/_find?type=wigwags')
|
||||
.expect(200)
|
||||
.then(resp => {
|
||||
expect(resp.body).to.eql({
|
||||
page: 1,
|
||||
per_page: 20,
|
||||
total: 0,
|
||||
saved_objects: []
|
||||
});
|
||||
})
|
||||
));
|
||||
});
|
||||
|
||||
describe('page beyond total', () => {
|
||||
it('should return 200 with empty response', async () => (
|
||||
await supertest
|
||||
.get('/api/kibana/management/saved_objects/_find?type=visualization&page=100&perPage=100')
|
||||
.expect(200)
|
||||
.then(resp => {
|
||||
expect(resp.body).to.eql({
|
||||
page: 100,
|
||||
per_page: 100,
|
||||
total: 1,
|
||||
saved_objects: []
|
||||
});
|
||||
})
|
||||
));
|
||||
});
|
||||
|
||||
describe('unknown search field', () => {
|
||||
it('should return 400 when using searchFields', async () => (
|
||||
await supertest
|
||||
.get('/api/kibana/management/saved_objects/_find?type=url&searchFields=a')
|
||||
.expect(400)
|
||||
.then(resp => {
|
||||
expect(resp.body).to.eql({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message: '"searchFields" is not allowed',
|
||||
validation: {
|
||||
source: 'query',
|
||||
keys: ['searchFields'],
|
||||
},
|
||||
});
|
||||
})
|
||||
));
|
||||
});
|
||||
});
|
||||
|
||||
describe('without kibana index', () => {
|
||||
before(async () => (
|
||||
// just in case the kibana server has recreated it
|
||||
await es.indices.delete({
|
||||
index: '.kibana',
|
||||
ignore: [404],
|
||||
})
|
||||
));
|
||||
|
||||
it('should return 200 with empty response', async () => (
|
||||
await supertest
|
||||
.get('/api/kibana/management/saved_objects/_find?type=visualization')
|
||||
.expect(200)
|
||||
.then(resp => {
|
||||
expect(resp.body).to.eql({
|
||||
page: 1,
|
||||
per_page: 20,
|
||||
total: 0,
|
||||
saved_objects: [],
|
||||
});
|
||||
})
|
||||
));
|
||||
|
||||
describe('unknown type', () => {
|
||||
it('should return 200 with empty response', async () => (
|
||||
await supertest
|
||||
.get('/api/kibana/management/saved_objects/_find?type=wigwags')
|
||||
.expect(200)
|
||||
.then(resp => {
|
||||
expect(resp.body).to.eql({
|
||||
page: 1,
|
||||
per_page: 20,
|
||||
total: 0,
|
||||
saved_objects: [],
|
||||
});
|
||||
})
|
||||
));
|
||||
});
|
||||
|
||||
describe('missing type', () => {
|
||||
it('should return 400', async () => (
|
||||
await supertest
|
||||
.get('/api/kibana/management/saved_objects/_find')
|
||||
.expect(400)
|
||||
.then(resp => {
|
||||
expect(resp.body).to.eql({
|
||||
error: 'Bad Request',
|
||||
message: 'child "type" fails because ["type" is required]',
|
||||
statusCode: 400,
|
||||
validation: {
|
||||
keys: ['type'],
|
||||
source: 'query'
|
||||
},
|
||||
});
|
||||
})
|
||||
));
|
||||
});
|
||||
|
||||
describe('page beyond total', () => {
|
||||
it('should return 200 with empty response', async () => (
|
||||
await supertest
|
||||
.get('/api/kibana/management/saved_objects/_find?type=visualization&page=100&perPage=100')
|
||||
.expect(200)
|
||||
.then(resp => {
|
||||
expect(resp.body).to.eql({
|
||||
page: 100,
|
||||
per_page: 100,
|
||||
total: 0,
|
||||
saved_objects: [],
|
||||
});
|
||||
})
|
||||
));
|
||||
});
|
||||
|
||||
describe('unknown search field', () => {
|
||||
it('should return 400 when using searchFields', async () => (
|
||||
await supertest
|
||||
.get('/api/kibana/management/saved_objects/_find?type=url&searchFields=a')
|
||||
.expect(400)
|
||||
.then(resp => {
|
||||
expect(resp.body).to.eql({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message: '"searchFields" is not allowed',
|
||||
validation: {
|
||||
source: 'query',
|
||||
keys: ['searchFields'],
|
||||
},
|
||||
});
|
||||
})
|
||||
));
|
||||
});
|
||||
});
|
||||
|
||||
describe('meta attributes injected properly', () => {
|
||||
before(() => esArchiver.load('management/saved_objects'));
|
||||
after(() => esArchiver.unload('management/saved_objects'));
|
||||
|
||||
it('should inject meta attributes for searches', async () => (
|
||||
await supertest
|
||||
.get('/api/kibana/management/saved_objects/_find?type=search')
|
||||
.expect(200)
|
||||
.then(resp => {
|
||||
expect(resp.body.saved_objects).to.have.length(1);
|
||||
expect(resp.body.saved_objects[0].meta).to.eql({
|
||||
icon: 'search',
|
||||
title: 'OneRecord',
|
||||
editUrl: '/management/kibana/objects/savedSearches/960372e0-3224-11e8-a572-ffca06da1357',
|
||||
inAppUrl: {
|
||||
path: '/app/kibana#/discover/960372e0-3224-11e8-a572-ffca06da1357',
|
||||
uiCapabilitiesPath: 'discover.show',
|
||||
},
|
||||
});
|
||||
})
|
||||
));
|
||||
|
||||
it('should inject meta attributes for dashboards', async () => (
|
||||
await supertest
|
||||
.get('/api/kibana/management/saved_objects/_find?type=dashboard')
|
||||
.expect(200)
|
||||
.then(resp => {
|
||||
expect(resp.body.saved_objects).to.have.length(1);
|
||||
expect(resp.body.saved_objects[0].meta).to.eql({
|
||||
icon: 'dashboardApp',
|
||||
title: 'Dashboard',
|
||||
editUrl: '/management/kibana/objects/savedDashboards/b70c7ae0-3224-11e8-a572-ffca06da1357',
|
||||
inAppUrl: {
|
||||
path: '/app/kibana#/dashboard/b70c7ae0-3224-11e8-a572-ffca06da1357',
|
||||
uiCapabilitiesPath: 'dashboard.show',
|
||||
},
|
||||
});
|
||||
})
|
||||
));
|
||||
|
||||
it('should inject meta attributes for visualizations', async () => (
|
||||
await supertest
|
||||
.get('/api/kibana/management/saved_objects/_find?type=visualization')
|
||||
.expect(200)
|
||||
.then(resp => {
|
||||
expect(resp.body.saved_objects).to.have.length(2);
|
||||
expect(resp.body.saved_objects[0].meta).to.eql({
|
||||
icon: 'visualizeApp',
|
||||
title: 'VisualizationFromSavedSearch',
|
||||
editUrl: '/management/kibana/objects/savedVisualizations/a42c0580-3224-11e8-a572-ffca06da1357',
|
||||
inAppUrl: {
|
||||
path: '/app/kibana#/visualize/edit/a42c0580-3224-11e8-a572-ffca06da1357',
|
||||
uiCapabilitiesPath: 'visualize.show',
|
||||
},
|
||||
});
|
||||
expect(resp.body.saved_objects[1].meta).to.eql({
|
||||
icon: 'visualizeApp',
|
||||
title: 'Visualization',
|
||||
editUrl: '/management/kibana/objects/savedVisualizations/add810b0-3224-11e8-a572-ffca06da1357',
|
||||
inAppUrl: {
|
||||
path: '/app/kibana#/visualize/edit/add810b0-3224-11e8-a572-ffca06da1357',
|
||||
uiCapabilitiesPath: 'visualize.show',
|
||||
},
|
||||
});
|
||||
})
|
||||
));
|
||||
|
||||
it('should inject meta attributes for index patterns', async () => (
|
||||
await supertest
|
||||
.get('/api/kibana/management/saved_objects/_find?type=index-pattern')
|
||||
.expect(200)
|
||||
.then(resp => {
|
||||
expect(resp.body.saved_objects).to.have.length(1);
|
||||
expect(resp.body.saved_objects[0].meta).to.eql({
|
||||
icon: 'indexPatternApp',
|
||||
title: 'saved_objects*',
|
||||
editUrl: '/management/kibana/index_patterns/8963ca30-3224-11e8-a572-ffca06da1357',
|
||||
inAppUrl: {
|
||||
path: '/app/kibana#/management/kibana/index_patterns/8963ca30-3224-11e8-a572-ffca06da1357',
|
||||
uiCapabilitiesPath: 'management.kibana.index_patterns',
|
||||
},
|
||||
});
|
||||
})
|
||||
));
|
||||
});
|
||||
});
|
||||
}
|
|
@ -19,6 +19,7 @@
|
|||
|
||||
export default function ({ loadTestFile }) {
|
||||
describe('saved_objects', () => {
|
||||
loadTestFile(require.resolve('./find'));
|
||||
loadTestFile(require.resolve('./relationships'));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -29,9 +29,17 @@ export default function ({ getService }) {
|
|||
id: Joi.string()
|
||||
.uuid()
|
||||
.required(),
|
||||
title: Joi.string()
|
||||
.required()
|
||||
.min(1),
|
||||
type: Joi.string().required(),
|
||||
relationship: Joi.string().valid('parent', 'child').required(),
|
||||
meta: Joi.object().keys({
|
||||
title: Joi.string().required(),
|
||||
icon: Joi.string().required(),
|
||||
editUrl: Joi.string().required(),
|
||||
inAppUrl: Joi.object().keys({
|
||||
path: Joi.string().required(),
|
||||
uiCapabilitiesPath: Joi.string().required(),
|
||||
}).required(),
|
||||
}).required(),
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -39,10 +47,6 @@ export default function ({ getService }) {
|
|||
before(() => esArchiver.load('management/saved_objects'));
|
||||
after(() => esArchiver.unload('management/saved_objects'));
|
||||
|
||||
const SEARCH_RESPONSE_SCHEMA = Joi.object().keys({
|
||||
visualization: GENERIC_RESPONSE_SCHEMA,
|
||||
'index-pattern': GENERIC_RESPONSE_SCHEMA,
|
||||
});
|
||||
const baseApiUrl = `/api/kibana/management/saved_objects/relationships`;
|
||||
const coerceToArray = itemOrItems => [].concat(itemOrItems);
|
||||
const getSavedObjectTypesQuery = types => coerceToArray(types).map(type => `savedObjectTypes=${type}`).join('&');
|
||||
|
@ -54,7 +58,7 @@ export default function ({ getService }) {
|
|||
.get(`${baseApiUrl}/search/960372e0-3224-11e8-a572-ffca06da1357?${defaultQuery}`)
|
||||
.expect(200)
|
||||
.then(resp => {
|
||||
const validationResult = Joi.validate(resp.body, SEARCH_RESPONSE_SCHEMA);
|
||||
const validationResult = Joi.validate(resp.body, GENERIC_RESPONSE_SCHEMA);
|
||||
expect(validationResult.error).to.be(null);
|
||||
});
|
||||
});
|
||||
|
@ -64,37 +68,74 @@ export default function ({ getService }) {
|
|||
.get(`${baseApiUrl}/search/960372e0-3224-11e8-a572-ffca06da1357?${defaultQuery}`)
|
||||
.expect(200)
|
||||
.then(resp => {
|
||||
expect(resp.body).to.eql({
|
||||
visualization: [
|
||||
{
|
||||
id: 'a42c0580-3224-11e8-a572-ffca06da1357',
|
||||
title: 'VisualizationFromSavedSearch',
|
||||
},
|
||||
],
|
||||
'index-pattern': [
|
||||
{
|
||||
id: '8963ca30-3224-11e8-a572-ffca06da1357',
|
||||
expect(resp.body).to.eql([
|
||||
{
|
||||
id: '8963ca30-3224-11e8-a572-ffca06da1357',
|
||||
type: 'index-pattern',
|
||||
relationship: 'child',
|
||||
meta: {
|
||||
title: 'saved_objects*',
|
||||
icon: 'indexPatternApp',
|
||||
editUrl: '/management/kibana/index_patterns/8963ca30-3224-11e8-a572-ffca06da1357',
|
||||
inAppUrl: {
|
||||
path: '/app/kibana#/management/kibana/index_patterns/8963ca30-3224-11e8-a572-ffca06da1357',
|
||||
uiCapabilitiesPath: 'management.kibana.index_patterns',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
{
|
||||
id: 'a42c0580-3224-11e8-a572-ffca06da1357',
|
||||
type: 'visualization',
|
||||
relationship: 'parent',
|
||||
meta: {
|
||||
title: 'VisualizationFromSavedSearch',
|
||||
icon: 'visualizeApp',
|
||||
editUrl: '/management/kibana/objects/savedVisualizations/a42c0580-3224-11e8-a572-ffca06da1357',
|
||||
inAppUrl: {
|
||||
path: '/app/kibana#/visualize/edit/a42c0580-3224-11e8-a572-ffca06da1357',
|
||||
uiCapabilitiesPath: 'visualize.show',
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it('should filter based on savedObjectTypes', async () => {
|
||||
await supertest
|
||||
.get(`${baseApiUrl}/search/960372e0-3224-11e8-a572-ffca06da1357?${getSavedObjectTypesQuery('visualization')}`)
|
||||
.expect(res => console.log(res.text))
|
||||
.expect(200)
|
||||
.then(resp => {
|
||||
expect(resp.body).to.eql({
|
||||
visualization: [
|
||||
{
|
||||
id: 'a42c0580-3224-11e8-a572-ffca06da1357',
|
||||
title: 'VisualizationFromSavedSearch',
|
||||
expect(resp.body).to.eql([
|
||||
{
|
||||
id: '8963ca30-3224-11e8-a572-ffca06da1357',
|
||||
type: 'index-pattern',
|
||||
meta: {
|
||||
icon: 'indexPatternApp',
|
||||
title: 'saved_objects*',
|
||||
editUrl: '/management/kibana/index_patterns/8963ca30-3224-11e8-a572-ffca06da1357',
|
||||
inAppUrl: {
|
||||
path: '/app/kibana#/management/kibana/index_patterns/8963ca30-3224-11e8-a572-ffca06da1357',
|
||||
uiCapabilitiesPath: 'management.kibana.index_patterns',
|
||||
},
|
||||
},
|
||||
]
|
||||
});
|
||||
relationship: 'child'
|
||||
},
|
||||
{
|
||||
id: 'a42c0580-3224-11e8-a572-ffca06da1357',
|
||||
type: 'visualization',
|
||||
meta: {
|
||||
icon: 'visualizeApp',
|
||||
title: 'VisualizationFromSavedSearch',
|
||||
editUrl: '/management/kibana/objects/savedVisualizations/a42c0580-3224-11e8-a572-ffca06da1357',
|
||||
inAppUrl: {
|
||||
path: '/app/kibana#/visualize/edit/a42c0580-3224-11e8-a572-ffca06da1357',
|
||||
uiCapabilitiesPath: 'visualize.show',
|
||||
},
|
||||
},
|
||||
relationship: 'parent',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -105,16 +146,12 @@ export default function ({ getService }) {
|
|||
});
|
||||
|
||||
describe('dashboards', async () => {
|
||||
const DASHBOARD_RESPONSE_SCHEMA = Joi.object().keys({
|
||||
visualization: GENERIC_RESPONSE_SCHEMA,
|
||||
});
|
||||
|
||||
it('should validate dashboard response schema', async () => {
|
||||
await supertest
|
||||
.get(`${baseApiUrl}/dashboard/b70c7ae0-3224-11e8-a572-ffca06da1357?${defaultQuery}`)
|
||||
.expect(200)
|
||||
.then(resp => {
|
||||
const validationResult = Joi.validate(resp.body, DASHBOARD_RESPONSE_SCHEMA);
|
||||
const validationResult = Joi.validate(resp.body, GENERIC_RESPONSE_SCHEMA);
|
||||
expect(validationResult.error).to.be(null);
|
||||
});
|
||||
});
|
||||
|
@ -124,18 +161,36 @@ export default function ({ getService }) {
|
|||
.get(`${baseApiUrl}/dashboard/b70c7ae0-3224-11e8-a572-ffca06da1357?${defaultQuery}`)
|
||||
.expect(200)
|
||||
.then(resp => {
|
||||
expect(resp.body).to.eql({
|
||||
visualization: [
|
||||
{
|
||||
id: 'add810b0-3224-11e8-a572-ffca06da1357',
|
||||
expect(resp.body).to.eql([
|
||||
{
|
||||
id: 'add810b0-3224-11e8-a572-ffca06da1357',
|
||||
type: 'visualization',
|
||||
relationship: 'child',
|
||||
meta: {
|
||||
icon: 'visualizeApp',
|
||||
title: 'Visualization',
|
||||
editUrl: '/management/kibana/objects/savedVisualizations/add810b0-3224-11e8-a572-ffca06da1357',
|
||||
inAppUrl: {
|
||||
path: '/app/kibana#/visualize/edit/add810b0-3224-11e8-a572-ffca06da1357',
|
||||
uiCapabilitiesPath: 'visualize.show',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'a42c0580-3224-11e8-a572-ffca06da1357',
|
||||
},
|
||||
{
|
||||
id: 'a42c0580-3224-11e8-a572-ffca06da1357',
|
||||
type: 'visualization',
|
||||
relationship: 'child',
|
||||
meta: {
|
||||
icon: 'visualizeApp',
|
||||
title: 'VisualizationFromSavedSearch',
|
||||
editUrl: '/management/kibana/objects/savedVisualizations/a42c0580-3224-11e8-a572-ffca06da1357',
|
||||
inAppUrl: {
|
||||
path: '/app/kibana#/visualize/edit/a42c0580-3224-11e8-a572-ffca06da1357',
|
||||
uiCapabilitiesPath: 'visualize.show',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -144,7 +199,36 @@ export default function ({ getService }) {
|
|||
.get(`${baseApiUrl}/dashboard/b70c7ae0-3224-11e8-a572-ffca06da1357?${getSavedObjectTypesQuery('search')}`)
|
||||
.expect(200)
|
||||
.then(resp => {
|
||||
expect(resp.body).to.eql({});
|
||||
expect(resp.body).to.eql([
|
||||
{
|
||||
id: 'add810b0-3224-11e8-a572-ffca06da1357',
|
||||
type: 'visualization',
|
||||
meta: {
|
||||
icon: 'visualizeApp',
|
||||
title: 'Visualization',
|
||||
editUrl: '/management/kibana/objects/savedVisualizations/add810b0-3224-11e8-a572-ffca06da1357',
|
||||
inAppUrl: {
|
||||
path: '/app/kibana#/visualize/edit/add810b0-3224-11e8-a572-ffca06da1357',
|
||||
uiCapabilitiesPath: 'visualize.show',
|
||||
},
|
||||
},
|
||||
relationship: 'child',
|
||||
},
|
||||
{
|
||||
id: 'a42c0580-3224-11e8-a572-ffca06da1357',
|
||||
type: 'visualization',
|
||||
meta: {
|
||||
icon: 'visualizeApp',
|
||||
title: 'VisualizationFromSavedSearch',
|
||||
editUrl: '/management/kibana/objects/savedVisualizations/a42c0580-3224-11e8-a572-ffca06da1357',
|
||||
inAppUrl: {
|
||||
path: '/app/kibana#/visualize/edit/a42c0580-3224-11e8-a572-ffca06da1357',
|
||||
uiCapabilitiesPath: 'visualize.show',
|
||||
},
|
||||
},
|
||||
relationship: 'child',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -157,17 +241,12 @@ export default function ({ getService }) {
|
|||
});
|
||||
|
||||
describe('visualizations', async () => {
|
||||
const VISUALIZATIONS_RESPONSE_SCHEMA = Joi.object().keys({
|
||||
dashboard: GENERIC_RESPONSE_SCHEMA,
|
||||
search: GENERIC_RESPONSE_SCHEMA,
|
||||
});
|
||||
|
||||
it('should validate visualization response schema', async () => {
|
||||
await supertest
|
||||
.get(`${baseApiUrl}/visualization/a42c0580-3224-11e8-a572-ffca06da1357?${defaultQuery}`)
|
||||
.expect(200)
|
||||
.then(resp => {
|
||||
const validationResult = Joi.validate(resp.body, VISUALIZATIONS_RESPONSE_SCHEMA);
|
||||
const validationResult = Joi.validate(resp.body, GENERIC_RESPONSE_SCHEMA);
|
||||
expect(validationResult.error).to.be(null);
|
||||
});
|
||||
});
|
||||
|
@ -177,20 +256,36 @@ export default function ({ getService }) {
|
|||
.get(`${baseApiUrl}/visualization/a42c0580-3224-11e8-a572-ffca06da1357?${defaultQuery}`)
|
||||
.expect(200)
|
||||
.then(resp => {
|
||||
expect(resp.body).to.eql({
|
||||
search: [
|
||||
{
|
||||
id: '960372e0-3224-11e8-a572-ffca06da1357',
|
||||
title: 'OneRecord'
|
||||
expect(resp.body).to.eql([
|
||||
{
|
||||
id: '960372e0-3224-11e8-a572-ffca06da1357',
|
||||
type: 'search',
|
||||
relationship: 'child',
|
||||
meta: {
|
||||
icon: 'search',
|
||||
title: 'OneRecord',
|
||||
editUrl: '/management/kibana/objects/savedSearches/960372e0-3224-11e8-a572-ffca06da1357',
|
||||
inAppUrl: {
|
||||
path: '/app/kibana#/discover/960372e0-3224-11e8-a572-ffca06da1357',
|
||||
uiCapabilitiesPath: 'discover.show',
|
||||
},
|
||||
},
|
||||
],
|
||||
dashboard: [
|
||||
{
|
||||
id: 'b70c7ae0-3224-11e8-a572-ffca06da1357',
|
||||
},
|
||||
{
|
||||
id: 'b70c7ae0-3224-11e8-a572-ffca06da1357',
|
||||
type: 'dashboard',
|
||||
relationship: 'parent',
|
||||
meta: {
|
||||
icon: 'dashboardApp',
|
||||
title: 'Dashboard',
|
||||
editUrl: '/management/kibana/objects/savedDashboards/b70c7ae0-3224-11e8-a572-ffca06da1357',
|
||||
inAppUrl: {
|
||||
path: '/app/kibana#/dashboard/b70c7ae0-3224-11e8-a572-ffca06da1357',
|
||||
uiCapabilitiesPath: 'dashboard.show',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -199,14 +294,22 @@ export default function ({ getService }) {
|
|||
.get(`${baseApiUrl}/visualization/a42c0580-3224-11e8-a572-ffca06da1357?${getSavedObjectTypesQuery('search')}`)
|
||||
.expect(200)
|
||||
.then(resp => {
|
||||
expect(resp.body).to.eql({
|
||||
search: [
|
||||
{
|
||||
id: '960372e0-3224-11e8-a572-ffca06da1357',
|
||||
title: 'OneRecord'
|
||||
expect(resp.body).to.eql([
|
||||
{
|
||||
id: '960372e0-3224-11e8-a572-ffca06da1357',
|
||||
type: 'search',
|
||||
meta: {
|
||||
icon: 'search',
|
||||
title: 'OneRecord',
|
||||
editUrl: '/management/kibana/objects/savedSearches/960372e0-3224-11e8-a572-ffca06da1357',
|
||||
inAppUrl: {
|
||||
path: '/app/kibana#/discover/960372e0-3224-11e8-a572-ffca06da1357',
|
||||
uiCapabilitiesPath: 'discover.show',
|
||||
},
|
||||
},
|
||||
]
|
||||
});
|
||||
relationship: 'child',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -218,17 +321,12 @@ export default function ({ getService }) {
|
|||
});
|
||||
|
||||
describe('index patterns', async () => {
|
||||
const INDEX_PATTERN_RESPONSE_SCHEMA = Joi.object().keys({
|
||||
search: GENERIC_RESPONSE_SCHEMA,
|
||||
visualization: GENERIC_RESPONSE_SCHEMA,
|
||||
});
|
||||
|
||||
it('should validate visualization response schema', async () => {
|
||||
await supertest
|
||||
.get(`${baseApiUrl}/index-pattern/8963ca30-3224-11e8-a572-ffca06da1357?${defaultQuery}`)
|
||||
.expect(200)
|
||||
.then(resp => {
|
||||
const validationResult = Joi.validate(resp.body, INDEX_PATTERN_RESPONSE_SCHEMA);
|
||||
const validationResult = Joi.validate(resp.body, GENERIC_RESPONSE_SCHEMA);
|
||||
expect(validationResult.error).to.be(null);
|
||||
});
|
||||
});
|
||||
|
@ -238,20 +336,36 @@ export default function ({ getService }) {
|
|||
.get(`${baseApiUrl}/index-pattern/8963ca30-3224-11e8-a572-ffca06da1357?${defaultQuery}`)
|
||||
.expect(200)
|
||||
.then(resp => {
|
||||
expect(resp.body).to.eql({
|
||||
search: [
|
||||
{
|
||||
id: '960372e0-3224-11e8-a572-ffca06da1357',
|
||||
expect(resp.body).to.eql([
|
||||
{
|
||||
id: '960372e0-3224-11e8-a572-ffca06da1357',
|
||||
type: 'search',
|
||||
relationship: 'parent',
|
||||
meta: {
|
||||
icon: 'search',
|
||||
title: 'OneRecord',
|
||||
editUrl: '/management/kibana/objects/savedSearches/960372e0-3224-11e8-a572-ffca06da1357',
|
||||
inAppUrl: {
|
||||
path: '/app/kibana#/discover/960372e0-3224-11e8-a572-ffca06da1357',
|
||||
uiCapabilitiesPath: 'discover.show',
|
||||
},
|
||||
},
|
||||
],
|
||||
visualization: [
|
||||
{
|
||||
id: 'add810b0-3224-11e8-a572-ffca06da1357',
|
||||
},
|
||||
{
|
||||
id: 'add810b0-3224-11e8-a572-ffca06da1357',
|
||||
type: 'visualization',
|
||||
relationship: 'parent',
|
||||
meta: {
|
||||
icon: 'visualizeApp',
|
||||
title: 'Visualization',
|
||||
editUrl: '/management/kibana/objects/savedVisualizations/add810b0-3224-11e8-a572-ffca06da1357',
|
||||
inAppUrl: {
|
||||
path: '/app/kibana#/visualize/edit/add810b0-3224-11e8-a572-ffca06da1357',
|
||||
uiCapabilitiesPath: 'visualize.show',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -260,14 +374,22 @@ export default function ({ getService }) {
|
|||
.get(`${baseApiUrl}/index-pattern/8963ca30-3224-11e8-a572-ffca06da1357?${getSavedObjectTypesQuery('search')}`)
|
||||
.expect(200)
|
||||
.then(resp => {
|
||||
expect(resp.body).to.eql({
|
||||
search: [
|
||||
{
|
||||
id: '960372e0-3224-11e8-a572-ffca06da1357',
|
||||
expect(resp.body).to.eql([
|
||||
{
|
||||
id: '960372e0-3224-11e8-a572-ffca06da1357',
|
||||
type: 'search',
|
||||
meta: {
|
||||
icon: 'search',
|
||||
title: 'OneRecord',
|
||||
editUrl: '/management/kibana/objects/savedSearches/960372e0-3224-11e8-a572-ffca06da1357',
|
||||
inAppUrl: {
|
||||
path: '/app/kibana#/discover/960372e0-3224-11e8-a572-ffca06da1357',
|
||||
uiCapabilitiesPath: 'discover.show',
|
||||
},
|
||||
},
|
||||
]
|
||||
});
|
||||
relationship: 'parent',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -49,47 +49,6 @@ export default function ({ getService }) {
|
|||
});
|
||||
});
|
||||
|
||||
it('should validate types', async () => {
|
||||
await supertest
|
||||
.post('/api/saved_objects/_export')
|
||||
.send({
|
||||
type: ['foo'],
|
||||
})
|
||||
.expect(400)
|
||||
.then((resp) => {
|
||||
expect(resp.body).to.eql({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
// eslint-disable-next-line max-len
|
||||
message: 'child "type" fails because ["type" at position 0 fails because ["0" must be one of [index-pattern, search, visualization, dashboard]]]',
|
||||
validation: { source: 'payload', keys: [ 'type.0' ] },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should validate types in objects', async () => {
|
||||
await supertest
|
||||
.post('/api/saved_objects/_export')
|
||||
.send({
|
||||
objects: [
|
||||
{
|
||||
type: 'foo',
|
||||
id: '1',
|
||||
},
|
||||
],
|
||||
})
|
||||
.expect(400)
|
||||
.then((resp) => {
|
||||
expect(resp.body).to.eql({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
// eslint-disable-next-line max-len
|
||||
message: 'child "objects" fails because ["objects" at position 0 fails because [child "type" fails because ["type" must be one of [index-pattern, search, visualization, dashboard]]]]',
|
||||
validation: { source: 'payload', keys: [ 'objects.0.type' ] },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should support including dependencies when exporting selected objects', async () => {
|
||||
await supertest
|
||||
.post('/api/saved_objects/_export')
|
||||
|
@ -167,6 +126,27 @@ export default function ({ getService }) {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
it(`should return 400 when exporting unsupported type`, async () => {
|
||||
await supertest
|
||||
.post('/api/saved_objects/_export')
|
||||
.send({
|
||||
type: ['wigwags'],
|
||||
})
|
||||
.expect(400)
|
||||
.then(resp => {
|
||||
expect(resp.body).to.eql({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message: 'child "type" fails because ["type" at position 0 fails because ' +
|
||||
'["0" must be one of [config, index-pattern, visualization, search, dashboard, url]]]',
|
||||
validation: {
|
||||
source: 'payload',
|
||||
keys: ['type.0'],
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('10,000 objects', () => {
|
||||
|
|
|
@ -112,6 +112,30 @@ export default function ({ getService }) {
|
|||
});
|
||||
});
|
||||
|
||||
it('should return 200 when trying to import unsupported types', async () => {
|
||||
const fileBuffer = Buffer.from('{"id":"1","type":"wigwags","attributes":{"title":"my title"},"references":[]}', 'utf8');
|
||||
await supertest
|
||||
.post('/api/saved_objects/_import')
|
||||
.attach('file', fileBuffer, 'export.ndjson')
|
||||
.expect(200)
|
||||
.then(resp => {
|
||||
expect(resp.body).to.eql({
|
||||
success: false,
|
||||
successCount: 0,
|
||||
errors: [
|
||||
{
|
||||
id: '1',
|
||||
type: 'wigwags',
|
||||
title: 'my title',
|
||||
error: {
|
||||
type: 'unsupported_type',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 400 when trying to import more than 10,000 objects', async () => {
|
||||
const fileChunks = [];
|
||||
for (let i = 0; i < 10001; i++) {
|
||||
|
|
|
@ -88,6 +88,31 @@ export default function ({ getService }) {
|
|||
});
|
||||
});
|
||||
|
||||
it('should return 200 when retrying unsupported types', async () => {
|
||||
const fileBuffer = Buffer.from('{"id":"1","type":"wigwags","attributes":{"title":"my title"},"references":[]}', 'utf8');
|
||||
await supertest
|
||||
.post('/api/saved_objects/_resolve_import_errors')
|
||||
.field('retries', JSON.stringify([{ type: 'wigwags', id: '1' }]))
|
||||
.attach('file', fileBuffer, 'export.ndjson')
|
||||
.expect(200)
|
||||
.then(resp => {
|
||||
expect(resp.body).to.eql({
|
||||
success: false,
|
||||
successCount: 0,
|
||||
errors: [
|
||||
{
|
||||
id: '1',
|
||||
type: 'wigwags',
|
||||
title: 'my title',
|
||||
error: {
|
||||
type: 'unsupported_type',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 400 when resolving conflicts with a file containing more than 10,000 objects', async () => {
|
||||
const fileChunks = [];
|
||||
for (let i = 0; i < 10001; i++) {
|
||||
|
|
|
@ -638,7 +638,7 @@ export function SettingsPageProvider({ getService, getPageObjects }) {
|
|||
const title = await titleCell.getVisibleText();
|
||||
|
||||
|
||||
const viewInAppButtons = await row.findAllByCssSelector('[aria-label="In app"]');
|
||||
const viewInAppButtons = await row.findAllByCssSelector('td:nth-child(3) a');
|
||||
const canViewInApp = Boolean(viewInAppButtons.length);
|
||||
summary.push({
|
||||
title,
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
import { resolve } from 'path';
|
||||
import init from './init';
|
||||
import { mappings } from './server/mappings';
|
||||
import { CANVAS_APP } from './common/lib';
|
||||
import { CANVAS_APP, CANVAS_TYPE, CUSTOM_ELEMENT_TYPE } from './common/lib';
|
||||
import { migrations } from './migrations';
|
||||
|
||||
export function canvas(kibana) {
|
||||
|
@ -33,6 +33,30 @@ export function canvas(kibana) {
|
|||
home: ['plugins/canvas/register_feature'],
|
||||
mappings,
|
||||
migrations,
|
||||
savedObjectsManagement: {
|
||||
[CANVAS_TYPE]: {
|
||||
icon: 'canvasApp',
|
||||
defaultSearchField: 'name',
|
||||
isImportableAndExportable: true,
|
||||
getTitle(obj) {
|
||||
return obj.attributes.name;
|
||||
},
|
||||
getInAppUrl(obj) {
|
||||
return {
|
||||
path: `/app/canvas#/workpad/${encodeURIComponent(obj.id)}`,
|
||||
uiCapabilitiesPath: 'canvas.show',
|
||||
};
|
||||
},
|
||||
},
|
||||
[CUSTOM_ELEMENT_TYPE]: {
|
||||
icon: 'canvasApp',
|
||||
defaultSearchField: 'name',
|
||||
isImportableAndExportable: true,
|
||||
getTitle(obj) {
|
||||
return obj.attributes.displayName;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
config: Joi => {
|
||||
|
|
|
@ -48,14 +48,14 @@ export default async function(server /*options*/) {
|
|||
all: ['canvas-workpad', 'canvas-element'],
|
||||
read: ['index-pattern'],
|
||||
},
|
||||
ui: ['save'],
|
||||
ui: ['save', 'show'],
|
||||
},
|
||||
read: {
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: ['index-pattern', 'canvas-workpad', 'canvas-element'],
|
||||
},
|
||||
ui: [],
|
||||
ui: ['show'],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -57,6 +57,22 @@ export function maps(kibana) {
|
|||
isNamespaceAgnostic: true
|
||||
}
|
||||
},
|
||||
savedObjectsManagement: {
|
||||
'map': {
|
||||
icon: APP_ICON,
|
||||
defaultSearchField: 'title',
|
||||
isImportableAndExportable: true,
|
||||
getTitle(obj) {
|
||||
return obj.attributes.title;
|
||||
},
|
||||
getInAppUrl(obj) {
|
||||
return {
|
||||
path: createMapPath(obj.id),
|
||||
uiCapabilitiesPath: 'maps.show',
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
mappings,
|
||||
migrations,
|
||||
},
|
||||
|
@ -94,14 +110,14 @@ export function maps(kibana) {
|
|||
all: ['map'],
|
||||
read: ['index-pattern']
|
||||
},
|
||||
ui: ['save'],
|
||||
ui: ['save', 'show'],
|
||||
},
|
||||
read: {
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: ['map', 'index-pattern']
|
||||
},
|
||||
ui: [],
|
||||
ui: ['show'],
|
||||
},
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1558,24 +1558,10 @@
|
|||
"kbn.management.objects.objectsTable.header.refreshButtonLabel": "刷新",
|
||||
"kbn.management.objects.objectsTable.header.savedObjectsTitle": "已保存对象",
|
||||
"kbn.management.objects.objectsTable.howToDeleteSavedObjectsDescription": "从这里您可以删除已保存对象,如已保存搜索。还可以编辑已保存对象的原始数据。通常,对象只能通过其关联的应用程序进行修改;或许您应该遵循这一原则,而非使用此屏幕进行修改。",
|
||||
"kbn.management.objects.objectsTable.relationships.columnActions.inAppDescription": "在 Kibana 内查看此已保存对象",
|
||||
"kbn.management.objects.objectsTable.relationships.columnActions.inAppName": "应用内",
|
||||
"kbn.management.objects.objectsTable.relationships.columnActionsName": "操作",
|
||||
"kbn.management.objects.objectsTable.relationships.columnTitleName": "标题",
|
||||
"kbn.management.objects.objectsTable.relationships.dashboard.calloutText": "以下是此仪表板上使用的某些可视化。删除此仪表板不会有任何问题,可视化仍会正常工作。",
|
||||
"kbn.management.objects.objectsTable.relationships.dashboard.calloutTitle": "仪表板",
|
||||
"kbn.management.objects.objectsTable.relationships.indexPattern.searches.calloutText": "以下是使用此索引模式的一些已保存搜索。如果您删除此索引模式,这些已保存搜索将无法再正常工作。",
|
||||
"kbn.management.objects.objectsTable.relationships.indexPattern.visualizations.calloutText": "以下是使用此索引模式的一些可视化。如果您删除此索引模式,这些可视化将无法再正常工作。",
|
||||
"kbn.management.objects.objectsTable.relationships.itemNotFoundText": "未找到任何{type}。",
|
||||
"kbn.management.objects.objectsTable.relationships.renderErrorMessage": "错误",
|
||||
"kbn.management.objects.objectsTable.relationships.search.calloutText": "以下是与此已保存搜索绑定的索引模式。",
|
||||
"kbn.management.objects.objectsTable.relationships.search.calloutTitle": "已保存搜索",
|
||||
"kbn.management.objects.objectsTable.relationships.search.visualizations.calloutText": "以下是使用此已保存搜索的一些可视化。如果您删除此已保存搜索,这些可视化将无法再正常工作。",
|
||||
"kbn.management.objects.objectsTable.relationships.visualization.calloutText": "以下是包含此可视化的一些仪表板。如果您删除此可视化,这些仪表板将不再显示它们。",
|
||||
"kbn.management.objects.objectsTable.relationships.warningTitle": "警告",
|
||||
"kbn.management.objects.objectsTable.searchBar.unableToParseQueryErrorMessage": "无法解析查询",
|
||||
"kbn.management.objects.objectsTable.table.columnActions.viewInAppActionDescription": "在 Kibana 内查看此已保存对象",
|
||||
"kbn.management.objects.objectsTable.table.columnActions.viewInAppActionName": "应用内",
|
||||
"kbn.management.objects.objectsTable.table.columnActions.viewRelationshipsActionDescription": "查看此已保存对象与其他已保存对象的关系",
|
||||
"kbn.management.objects.objectsTable.table.columnActions.viewRelationshipsActionName": "关系",
|
||||
"kbn.management.objects.objectsTable.table.columnActionsName": "操作",
|
||||
|
@ -1591,7 +1577,6 @@
|
|||
"kbn.management.objects.savedObjectsDescription": "导入、导出和管理您的已保存搜索、可视化和仪表板。",
|
||||
"kbn.management.objects.savedObjectsSectionLabel": "已保存对象",
|
||||
"kbn.management.objects.savedObjectsTitle": "已保存对象",
|
||||
"kbn.management.objects.unknownSavedObjectTypeNotificationMessage": "未知已保存对象类型:{type}",
|
||||
"kbn.management.objects.view.cancelButtonAriaLabel": "取消",
|
||||
"kbn.management.objects.view.cancelButtonLabel": "取消",
|
||||
"kbn.management.objects.view.deleteItemButtonLabel": "删除“{title}”",
|
||||
|
|
|
@ -678,6 +678,7 @@ export default function({ getService }: KibanaFunctionalTestDefaultProviders) {
|
|||
`ui:${version}:savedObjectsManagement/index-pattern/read`,
|
||||
`ui:${version}:savedObjectsManagement/config/read`,
|
||||
`ui:${version}:maps/save`,
|
||||
`ui:${version}:maps/show`,
|
||||
'allHack:',
|
||||
],
|
||||
read: [
|
||||
|
@ -699,6 +700,7 @@ export default function({ getService }: KibanaFunctionalTestDefaultProviders) {
|
|||
`ui:${version}:savedObjectsManagement/map/read`,
|
||||
`ui:${version}:savedObjectsManagement/index-pattern/read`,
|
||||
`ui:${version}:savedObjectsManagement/config/read`,
|
||||
`ui:${version}:maps/show`,
|
||||
],
|
||||
},
|
||||
canvas: {
|
||||
|
@ -748,6 +750,7 @@ export default function({ getService }: KibanaFunctionalTestDefaultProviders) {
|
|||
`ui:${version}:savedObjectsManagement/index-pattern/read`,
|
||||
`ui:${version}:savedObjectsManagement/config/read`,
|
||||
`ui:${version}:canvas/save`,
|
||||
`ui:${version}:canvas/show`,
|
||||
'allHack:',
|
||||
],
|
||||
read: [
|
||||
|
@ -773,6 +776,7 @@ export default function({ getService }: KibanaFunctionalTestDefaultProviders) {
|
|||
`ui:${version}:savedObjectsManagement/canvas-workpad/read`,
|
||||
`ui:${version}:savedObjectsManagement/canvas-element/read`,
|
||||
`ui:${version}:savedObjectsManagement/config/read`,
|
||||
`ui:${version}:canvas/show`,
|
||||
],
|
||||
},
|
||||
infrastructure: {
|
||||
|
@ -1138,6 +1142,7 @@ export default function({ getService }: KibanaFunctionalTestDefaultProviders) {
|
|||
`ui:${version}:savedObjectsManagement/map/delete`,
|
||||
`ui:${version}:savedObjectsManagement/map/edit`,
|
||||
`ui:${version}:maps/save`,
|
||||
`ui:${version}:maps/show`,
|
||||
`app:${version}:canvas`,
|
||||
`ui:${version}:catalogue/canvas`,
|
||||
`ui:${version}:navLinks/canvas`,
|
||||
|
@ -1158,6 +1163,7 @@ export default function({ getService }: KibanaFunctionalTestDefaultProviders) {
|
|||
`ui:${version}:savedObjectsManagement/canvas-element/edit`,
|
||||
`ui:${version}:savedObjectsManagement/canvas-element/read`,
|
||||
`ui:${version}:canvas/save`,
|
||||
`ui:${version}:canvas/show`,
|
||||
`api:${version}:infra`,
|
||||
`app:${version}:infra`,
|
||||
`ui:${version}:catalogue/infraops`,
|
||||
|
@ -1274,6 +1280,7 @@ export default function({ getService }: KibanaFunctionalTestDefaultProviders) {
|
|||
`app:${version}:maps`,
|
||||
`ui:${version}:catalogue/maps`,
|
||||
`ui:${version}:navLinks/maps`,
|
||||
`ui:${version}:maps/show`,
|
||||
`app:${version}:canvas`,
|
||||
`ui:${version}:catalogue/canvas`,
|
||||
`ui:${version}:navLinks/canvas`,
|
||||
|
@ -1281,6 +1288,7 @@ export default function({ getService }: KibanaFunctionalTestDefaultProviders) {
|
|||
`saved_object:${version}:canvas-element/get`,
|
||||
`saved_object:${version}:canvas-element/find`,
|
||||
`ui:${version}:savedObjectsManagement/canvas-element/read`,
|
||||
`ui:${version}:canvas/show`,
|
||||
`api:${version}:infra`,
|
||||
`app:${version}:infra`,
|
||||
`ui:${version}:catalogue/infraops`,
|
||||
|
@ -1468,6 +1476,7 @@ export default function({ getService }: KibanaFunctionalTestDefaultProviders) {
|
|||
`ui:${version}:savedObjectsManagement/map/delete`,
|
||||
`ui:${version}:savedObjectsManagement/map/edit`,
|
||||
`ui:${version}:maps/save`,
|
||||
`ui:${version}:maps/show`,
|
||||
`app:${version}:canvas`,
|
||||
`ui:${version}:catalogue/canvas`,
|
||||
`ui:${version}:navLinks/canvas`,
|
||||
|
@ -1488,6 +1497,7 @@ export default function({ getService }: KibanaFunctionalTestDefaultProviders) {
|
|||
`ui:${version}:savedObjectsManagement/canvas-element/edit`,
|
||||
`ui:${version}:savedObjectsManagement/canvas-element/read`,
|
||||
`ui:${version}:canvas/save`,
|
||||
`ui:${version}:canvas/show`,
|
||||
`api:${version}:infra`,
|
||||
`app:${version}:infra`,
|
||||
`ui:${version}:catalogue/infraops`,
|
||||
|
@ -1604,6 +1614,7 @@ export default function({ getService }: KibanaFunctionalTestDefaultProviders) {
|
|||
`app:${version}:maps`,
|
||||
`ui:${version}:catalogue/maps`,
|
||||
`ui:${version}:navLinks/maps`,
|
||||
`ui:${version}:maps/show`,
|
||||
`app:${version}:canvas`,
|
||||
`ui:${version}:catalogue/canvas`,
|
||||
`ui:${version}:navLinks/canvas`,
|
||||
|
@ -1611,6 +1622,7 @@ export default function({ getService }: KibanaFunctionalTestDefaultProviders) {
|
|||
`saved_object:${version}:canvas-element/get`,
|
||||
`saved_object:${version}:canvas-element/find`,
|
||||
`ui:${version}:savedObjectsManagement/canvas-element/read`,
|
||||
`ui:${version}:canvas/show`,
|
||||
`api:${version}:infra`,
|
||||
`app:${version}:infra`,
|
||||
`ui:${version}:catalogue/infraops`,
|
||||
|
|
|
@ -12,10 +12,13 @@ export default function({ getPageObjects, getService }: KibanaFunctionalTestDefa
|
|||
const security = getService('security');
|
||||
const testSubjects = getService('testSubjects');
|
||||
const PageObjects = getPageObjects(['common', 'settings', 'security']);
|
||||
let version: string = '';
|
||||
|
||||
describe('feature controls saved objects management', () => {
|
||||
before(async () => {
|
||||
await esArchiver.load('saved_objects_management/feature_controls/security');
|
||||
const versionService = getService('kibanaServer').version;
|
||||
version = await versionService.get();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
|
@ -65,12 +68,26 @@ export default function({ getPageObjects, getService }: KibanaFunctionalTestDefa
|
|||
|
||||
it('shows all saved objects', async () => {
|
||||
const objects = await PageObjects.settings.getSavedObjectsInTable();
|
||||
expect(objects).to.eql(['A Dashboard', 'logstash-*', 'A Pie']);
|
||||
expect(objects).to.eql([
|
||||
'Advanced Settings [6.0.0]',
|
||||
`Advanced Settings [${version}]`,
|
||||
'A Dashboard',
|
||||
'logstash-*',
|
||||
'A Pie',
|
||||
]);
|
||||
});
|
||||
|
||||
it('can view all saved objects in applications', async () => {
|
||||
const bools = await PageObjects.settings.getSavedObjectsTableSummary();
|
||||
expect(bools).to.eql([
|
||||
{
|
||||
title: 'Advanced Settings [6.0.0]',
|
||||
canViewInApp: false,
|
||||
},
|
||||
{
|
||||
title: `Advanced Settings [${version}]`,
|
||||
canViewInApp: false,
|
||||
},
|
||||
{
|
||||
title: 'A Dashboard',
|
||||
canViewInApp: true,
|
||||
|
@ -171,14 +188,27 @@ export default function({ getPageObjects, getService }: KibanaFunctionalTestDefa
|
|||
await PageObjects.settings.clickKibanaSavedObjects();
|
||||
});
|
||||
|
||||
it('shows a visualization and an index pattern', async () => {
|
||||
it('shows two configs, a visualization and an index pattern', async () => {
|
||||
const objects = await PageObjects.settings.getSavedObjectsInTable();
|
||||
expect(objects).to.eql(['logstash-*', 'A Pie']);
|
||||
expect(objects).to.eql([
|
||||
'Advanced Settings [6.0.0]',
|
||||
`Advanced Settings [${version}]`,
|
||||
'logstash-*',
|
||||
'A Pie',
|
||||
]);
|
||||
});
|
||||
|
||||
it('can view only the visualization in application', async () => {
|
||||
it('can view only two configs and the visualization in application', async () => {
|
||||
const bools = await PageObjects.settings.getSavedObjectsTableSummary();
|
||||
expect(bools).to.eql([
|
||||
{
|
||||
title: 'Advanced Settings [6.0.0]',
|
||||
canViewInApp: false,
|
||||
},
|
||||
{
|
||||
title: `Advanced Settings [${version}]`,
|
||||
canViewInApp: false,
|
||||
},
|
||||
{
|
||||
title: 'logstash-*',
|
||||
canViewInApp: false,
|
||||
|
|
|
@ -11,6 +11,11 @@ export default function (kibana) {
|
|||
require: ['kibana', 'elasticsearch', 'xpack_main'],
|
||||
name: 'namespace_agnostic_type_plugin',
|
||||
uiExports: {
|
||||
savedObjectsManagement: {
|
||||
globaltype: {
|
||||
isImportableAndExportable: true,
|
||||
},
|
||||
},
|
||||
savedObjectSchemas: {
|
||||
globaltype: {
|
||||
isNamespaceAgnostic: true
|
||||
|
|
|
@ -63,10 +63,7 @@ export function importTestSuiteFactory(es: any, esArchiver: any, supertest: Supe
|
|||
type: 'wigwags',
|
||||
title: 'Wigwags title',
|
||||
error: {
|
||||
message: `Unsupported saved object type: 'wigwags': Bad Request`,
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
type: 'unknown',
|
||||
type: 'unsupported_type',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
@ -81,22 +78,6 @@ export function importTestSuiteFactory(es: any, esArchiver: any, supertest: Supe
|
|||
});
|
||||
};
|
||||
|
||||
const expectRbacForbiddenWithUnknownType = (resp: { [key: string]: any }) => {
|
||||
expect(resp.body).to.eql({
|
||||
statusCode: 403,
|
||||
error: 'Forbidden',
|
||||
message: `Unable to bulk_create dashboard,globaltype,wigwags`,
|
||||
});
|
||||
};
|
||||
|
||||
const expectRbacForbiddenForUnknownType = (resp: { [key: string]: any }) => {
|
||||
expect(resp.body).to.eql({
|
||||
statusCode: 403,
|
||||
error: 'Forbidden',
|
||||
message: `Unable to bulk_create wigwags`,
|
||||
});
|
||||
};
|
||||
|
||||
const makeImportTest = (describeFn: DescribeFn) => (
|
||||
description: string,
|
||||
definition: ImportTestDefinition
|
||||
|
@ -156,7 +137,5 @@ export function importTestSuiteFactory(es: any, esArchiver: any, supertest: Supe
|
|||
createExpectResults,
|
||||
expectRbacForbidden,
|
||||
expectUnknownType,
|
||||
expectRbacForbiddenWithUnknownType,
|
||||
expectRbacForbiddenForUnknownType,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -67,10 +67,7 @@ export function resolveImportErrorsTestSuiteFactory(
|
|||
type: 'wigwags',
|
||||
title: 'Wigwags title',
|
||||
error: {
|
||||
message: `Unsupported saved object type: 'wigwags': Bad Request`,
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
type: 'unknown',
|
||||
type: 'unsupported_type',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
@ -85,22 +82,6 @@ export function resolveImportErrorsTestSuiteFactory(
|
|||
});
|
||||
};
|
||||
|
||||
const expectRbacForbiddenWithUnknownType = (resp: { [key: string]: any }) => {
|
||||
expect(resp.body).to.eql({
|
||||
statusCode: 403,
|
||||
error: 'Forbidden',
|
||||
message: `Unable to bulk_create dashboard,wigwags`,
|
||||
});
|
||||
};
|
||||
|
||||
const expectRbacForbiddenForUnknownType = (resp: { [key: string]: any }) => {
|
||||
expect(resp.body).to.eql({
|
||||
statusCode: 403,
|
||||
error: 'Forbidden',
|
||||
message: `Unable to bulk_create wigwags`,
|
||||
});
|
||||
};
|
||||
|
||||
const makeResolveImportErrorsTest = (describeFn: DescribeFn) => (
|
||||
description: string,
|
||||
definition: ResolveImportErrorsTestDefinition
|
||||
|
@ -184,7 +165,5 @@ export function resolveImportErrorsTestSuiteFactory(
|
|||
createExpectResults,
|
||||
expectRbacForbidden,
|
||||
expectUnknownType,
|
||||
expectRbacForbiddenWithUnknownType,
|
||||
expectRbacForbiddenForUnknownType,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -20,8 +20,6 @@ export default function({ getService }: TestInvoker) {
|
|||
createExpectResults,
|
||||
expectRbacForbidden,
|
||||
expectUnknownType,
|
||||
expectRbacForbiddenWithUnknownType,
|
||||
expectRbacForbiddenForUnknownType,
|
||||
} = importTestSuiteFactory(es, esArchiver, supertest);
|
||||
|
||||
describe('_import', () => {
|
||||
|
@ -67,7 +65,7 @@ export default function({ getService }: TestInvoker) {
|
|||
},
|
||||
unknownType: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbiddenWithUnknownType,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -97,7 +95,7 @@ export default function({ getService }: TestInvoker) {
|
|||
},
|
||||
unknownType: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbiddenWithUnknownType,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -111,8 +109,8 @@ export default function({ getService }: TestInvoker) {
|
|||
response: createExpectResults(scenario.spaceId),
|
||||
},
|
||||
unknownType: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbiddenForUnknownType,
|
||||
statusCode: 200,
|
||||
response: expectUnknownType,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -127,7 +125,7 @@ export default function({ getService }: TestInvoker) {
|
|||
},
|
||||
unknownType: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbiddenWithUnknownType,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -141,8 +139,8 @@ export default function({ getService }: TestInvoker) {
|
|||
response: createExpectResults(scenario.spaceId),
|
||||
},
|
||||
unknownType: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbiddenForUnknownType,
|
||||
statusCode: 200,
|
||||
response: expectUnknownType,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -157,7 +155,7 @@ export default function({ getService }: TestInvoker) {
|
|||
},
|
||||
unknownType: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbiddenWithUnknownType,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -171,8 +169,8 @@ export default function({ getService }: TestInvoker) {
|
|||
response: createExpectResults(scenario.spaceId),
|
||||
},
|
||||
unknownType: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbiddenForUnknownType,
|
||||
statusCode: 200,
|
||||
response: expectUnknownType,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -187,7 +185,7 @@ export default function({ getService }: TestInvoker) {
|
|||
},
|
||||
unknownType: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbiddenWithUnknownType,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -202,7 +200,7 @@ export default function({ getService }: TestInvoker) {
|
|||
},
|
||||
unknownType: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbiddenWithUnknownType,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -20,8 +20,6 @@ export default function({ getService }: TestInvoker) {
|
|||
createExpectResults,
|
||||
expectRbacForbidden,
|
||||
expectUnknownType,
|
||||
expectRbacForbiddenWithUnknownType,
|
||||
expectRbacForbiddenForUnknownType,
|
||||
} = resolveImportErrorsTestSuiteFactory(es, esArchiver, supertest);
|
||||
|
||||
describe('_resolve_import_errors', () => {
|
||||
|
@ -67,7 +65,7 @@ export default function({ getService }: TestInvoker) {
|
|||
},
|
||||
unknownType: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbiddenWithUnknownType,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -97,7 +95,7 @@ export default function({ getService }: TestInvoker) {
|
|||
},
|
||||
unknownType: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbiddenWithUnknownType,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -111,8 +109,8 @@ export default function({ getService }: TestInvoker) {
|
|||
response: createExpectResults(scenario.spaceId),
|
||||
},
|
||||
unknownType: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbiddenForUnknownType,
|
||||
statusCode: 200,
|
||||
response: expectUnknownType,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -129,7 +127,7 @@ export default function({ getService }: TestInvoker) {
|
|||
},
|
||||
unknownType: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbiddenWithUnknownType,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -144,8 +142,8 @@ export default function({ getService }: TestInvoker) {
|
|||
response: createExpectResults(scenario.spaceId),
|
||||
},
|
||||
unknownType: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbiddenForUnknownType,
|
||||
statusCode: 200,
|
||||
response: expectUnknownType,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -160,7 +158,7 @@ export default function({ getService }: TestInvoker) {
|
|||
},
|
||||
unknownType: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbiddenWithUnknownType,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -176,8 +174,8 @@ export default function({ getService }: TestInvoker) {
|
|||
response: createExpectResults(scenario.spaceId),
|
||||
},
|
||||
unknownType: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbiddenForUnknownType,
|
||||
statusCode: 200,
|
||||
response: expectUnknownType,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -195,7 +193,7 @@ export default function({ getService }: TestInvoker) {
|
|||
},
|
||||
unknownType: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbiddenWithUnknownType,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -213,7 +211,7 @@ export default function({ getService }: TestInvoker) {
|
|||
},
|
||||
unknownType: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbiddenWithUnknownType,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -19,8 +19,6 @@ export default function({ getService }: TestInvoker) {
|
|||
createExpectResults,
|
||||
expectRbacForbidden,
|
||||
expectUnknownType,
|
||||
expectRbacForbiddenWithUnknownType,
|
||||
expectRbacForbiddenForUnknownType,
|
||||
} = importTestSuiteFactory(es, esArchiver, supertest);
|
||||
|
||||
describe('_import', () => {
|
||||
|
@ -33,7 +31,7 @@ export default function({ getService }: TestInvoker) {
|
|||
},
|
||||
unknownType: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbiddenWithUnknownType,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -61,7 +59,7 @@ export default function({ getService }: TestInvoker) {
|
|||
},
|
||||
unknownType: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbiddenWithUnknownType,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -74,8 +72,8 @@ export default function({ getService }: TestInvoker) {
|
|||
response: createExpectResults(),
|
||||
},
|
||||
unknownType: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbiddenForUnknownType,
|
||||
statusCode: 200,
|
||||
response: expectUnknownType,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -89,7 +87,7 @@ export default function({ getService }: TestInvoker) {
|
|||
},
|
||||
unknownType: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbiddenWithUnknownType,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -102,8 +100,8 @@ export default function({ getService }: TestInvoker) {
|
|||
response: createExpectResults(),
|
||||
},
|
||||
unknownType: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbiddenForUnknownType,
|
||||
statusCode: 200,
|
||||
response: expectUnknownType,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -117,7 +115,7 @@ export default function({ getService }: TestInvoker) {
|
|||
},
|
||||
unknownType: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbiddenWithUnknownType,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -131,7 +129,7 @@ export default function({ getService }: TestInvoker) {
|
|||
},
|
||||
unknownType: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbiddenWithUnknownType,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -145,7 +143,7 @@ export default function({ getService }: TestInvoker) {
|
|||
},
|
||||
unknownType: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbiddenWithUnknownType,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -159,7 +157,7 @@ export default function({ getService }: TestInvoker) {
|
|||
},
|
||||
unknownType: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbiddenWithUnknownType,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -173,7 +171,7 @@ export default function({ getService }: TestInvoker) {
|
|||
},
|
||||
unknownType: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbiddenWithUnknownType,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -19,8 +19,6 @@ export default function({ getService }: TestInvoker) {
|
|||
createExpectResults,
|
||||
expectRbacForbidden,
|
||||
expectUnknownType,
|
||||
expectRbacForbiddenWithUnknownType,
|
||||
expectRbacForbiddenForUnknownType,
|
||||
} = resolveImportErrorsTestSuiteFactory(es, esArchiver, supertest);
|
||||
|
||||
describe('_resolve_import_errors', () => {
|
||||
|
@ -33,7 +31,7 @@ export default function({ getService }: TestInvoker) {
|
|||
},
|
||||
unknownType: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbiddenWithUnknownType,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -61,7 +59,7 @@ export default function({ getService }: TestInvoker) {
|
|||
},
|
||||
unknownType: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbiddenWithUnknownType,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -74,8 +72,8 @@ export default function({ getService }: TestInvoker) {
|
|||
response: createExpectResults(),
|
||||
},
|
||||
unknownType: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbiddenForUnknownType,
|
||||
statusCode: 200,
|
||||
response: expectUnknownType,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -89,7 +87,7 @@ export default function({ getService }: TestInvoker) {
|
|||
},
|
||||
unknownType: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbiddenWithUnknownType,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -102,8 +100,8 @@ export default function({ getService }: TestInvoker) {
|
|||
response: createExpectResults(),
|
||||
},
|
||||
unknownType: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbiddenForUnknownType,
|
||||
statusCode: 200,
|
||||
response: expectUnknownType,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -117,7 +115,7 @@ export default function({ getService }: TestInvoker) {
|
|||
},
|
||||
unknownType: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbiddenWithUnknownType,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -131,7 +129,7 @@ export default function({ getService }: TestInvoker) {
|
|||
},
|
||||
unknownType: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbiddenWithUnknownType,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -145,7 +143,7 @@ export default function({ getService }: TestInvoker) {
|
|||
},
|
||||
unknownType: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbiddenWithUnknownType,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -159,7 +157,7 @@ export default function({ getService }: TestInvoker) {
|
|||
},
|
||||
unknownType: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbiddenWithUnknownType,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -173,7 +171,7 @@ export default function({ getService }: TestInvoker) {
|
|||
},
|
||||
unknownType: {
|
||||
statusCode: 403,
|
||||
response: expectRbacForbiddenWithUnknownType,
|
||||
response: expectRbacForbidden,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue