Migrate SO management section to NP (#61700)

* move libs to new plugin

* adapt libs to use NP apis

* add required plugins

* add get_allowed_types route

* move object_view components

* add service registry

* migrate table header component

* migrate table component

* migrate saved_objects_table component

* remove migrated legacy files

* fix re-export from legacy management + section label

* migrate services registration

* adapt management section mock

* fix imports

* migrate flyout component

* migrate relationships component

* migrate saved_objects_table tests

* migrate breadcrumb

* add redirect if unauthorized check

* migrate translations to new savedObjectsManagement prefix

* remove obsolete translations

* convert action registry to service pattern

* wire extra actions

* remove importAndExportableTypes from injected vars

* handle newIndexPatternUrl

* remove duplicate dashboard dependency

* remove old TODO

* remove old TODO

* properly mock lodash in tests

* add async management section loading

* expose createSavedSearchesLoader from discover plugin contract

* address most review comments

* fix merge conflicts
This commit is contained in:
Pierre Gayvallet 2020-04-13 13:28:09 +02:00 committed by GitHub
parent 1199c8c8b0
commit 358d13919b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
110 changed files with 2182 additions and 1687 deletions

View file

@ -48,6 +48,7 @@ export { overlayServiceMock } from './overlays/overlay_service.mock';
export { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock';
export { savedObjectsServiceMock } from './saved_objects/saved_objects_service.mock';
export { scopedHistoryMock } from './application/scoped_history.mock';
export { applicationServiceMock } from './application/application_service.mock';
function createCoreSetupMock({
basePath = '',
@ -62,9 +63,8 @@ function createCoreSetupMock({
application: applicationServiceMock.createSetupContract(),
context: contextServiceMock.createSetupContract(),
fatalErrors: fatalErrorsServiceMock.createSetupContract(),
getStartServices: jest.fn<Promise<[ReturnType<typeof createCoreStartMock>, object, any]>, []>(
() =>
Promise.resolve([createCoreStartMock({ basePath }), pluginStartDeps, pluginStartContract])
getStartServices: jest.fn<Promise<[ReturnType<typeof createCoreStartMock>, any, any]>, []>(() =>
Promise.resolve([createCoreStartMock({ basePath }), pluginStartDeps, pluginStartContract])
),
http: httpServiceMock.createSetupContract({ basePath }),
notifications: notificationServiceMock.createSetupContract(),

View file

@ -36,7 +36,6 @@ export interface SavedObjectsLegacyService {
getScopedSavedObjectsClient: SavedObjectsClientProvider['getClient'];
SavedObjectsClient: typeof SavedObjectsClient;
types: string[];
importAndExportableTypes: string[];
schema: SavedObjectsSchema;
getSavedObjectsRepository(...rest: any[]): any;
importExport: {

View file

@ -2084,8 +2084,6 @@ export interface SavedObjectsLegacyService {
// (undocumented)
getScopedSavedObjectsClient: SavedObjectsClientProvider['getClient'];
// (undocumented)
importAndExportableTypes: string[];
// (undocumented)
importExport: {
objectLimit: number;
importSavedObjects(options: SavedObjectsImportOptions): Promise<SavedObjectsImportResponse>;

View file

@ -20,10 +20,7 @@
export function injectVars(server) {
const serverConfig = server.config();
const { importAndExportableTypes } = server.savedObjects;
return {
importAndExportableTypes,
autocompleteTerminateAfter: serverConfig.get('kibana.autocompleteTerminateAfter'),
autocompleteTimeout: serverConfig.get('kibana.autocompleteTimeout'),
};

View file

@ -20,4 +20,4 @@
export {
ProcessedImportResponse,
processImportResponse,
} from './management/sections/objects/lib/process_import_response';
} from '../../../../plugins/saved_objects_management/public/lib';

View file

@ -17,66 +17,8 @@
* under the License.
*/
import _ from 'lodash';
import { i18n } from '@kbn/i18n';
import { npStart } from 'ui/new_platform';
import { SavedObjectLoader } from '../../../../../plugins/saved_objects/public';
import { createSavedSearchesLoader } from '../../../../../plugins/discover/public';
import { npSetup } from 'ui/new_platform';
/**
* This registry is used for the editing mode of Saved Searches, Visualizations,
* Dashboard and Time Lion saved objects.
*/
interface SavedObjectRegistryEntry {
id: string;
service: SavedObjectLoader;
title: string;
}
const registry = npSetup.plugins.savedObjectsManagement?.serviceRegistry;
export interface ISavedObjectsManagementRegistry {
register(service: SavedObjectRegistryEntry): void;
all(): SavedObjectRegistryEntry[];
get(id: string): SavedObjectRegistryEntry | undefined;
}
const registry: SavedObjectRegistryEntry[] = [];
export const savedObjectManagementRegistry: ISavedObjectsManagementRegistry = {
register: (service: SavedObjectRegistryEntry) => {
registry.push(service);
},
all: () => {
return registry;
},
get: (id: string) => {
return _.find(registry, { id });
},
};
const services = {
savedObjectsClient: npStart.core.savedObjects.client,
indexPatterns: npStart.plugins.data.indexPatterns,
search: npStart.plugins.data.search,
chrome: npStart.core.chrome,
overlays: npStart.core.overlays,
};
savedObjectManagementRegistry.register({
id: 'savedVisualizations',
service: npStart.plugins.visualizations.savedVisualizationsLoader,
title: 'visualizations',
});
savedObjectManagementRegistry.register({
id: 'savedDashboards',
service: npStart.plugins.dashboard.getSavedDashboardLoader(),
title: i18n.translate('kbn.dashboard.savedDashboardsTitle', {
defaultMessage: 'dashboards',
}),
});
savedObjectManagementRegistry.register({
id: 'savedSearches',
service: createSavedSearchesLoader(services),
title: 'searches',
});
export const savedObjectManagementRegistry = registry!;

View file

@ -17,5 +17,4 @@
* under the License.
*/
import './objects';
import './index_patterns';

View file

@ -1,5 +0,0 @@
<kbn-management-app section="kibana/objects">
<kbn-management-objects>
<div id="reactSavedObjectsTable"></div>
</kbn-management-objects>
</kbn-management-app>

View file

@ -1,104 +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 { savedObjectManagementRegistry } from '../../saved_object_registry';
import objectIndexHTML from './_objects.html';
import uiRoutes from 'ui/routes';
import chrome from 'ui/chrome';
import { uiModules } from 'ui/modules';
import React from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import { ObjectsTable } from './components/objects_table';
import { I18nContext } from 'ui/i18n';
import { get } from 'lodash';
import { npStart } from 'ui/new_platform';
import { getIndexBreadcrumbs } from './breadcrumbs';
const REACT_OBJECTS_TABLE_DOM_ELEMENT_ID = 'reactSavedObjectsTable';
function updateObjectsTable($scope, $injector) {
const indexPatterns = npStart.plugins.data.indexPatterns;
const $http = $injector.get('$http');
const kbnUrl = $injector.get('kbnUrl');
const config = $injector.get('config');
const savedObjectsClient = npStart.core.savedObjects.client;
const services = savedObjectManagementRegistry.all().map(obj => obj.service);
const uiCapabilites = npStart.core.application.capabilities;
$scope.$$postDigest(() => {
const node = document.getElementById(REACT_OBJECTS_TABLE_DOM_ELEMENT_ID);
if (!node) {
return;
}
render(
<I18nContext>
<ObjectsTable
savedObjectsClient={savedObjectsClient}
confirmModalPromise={npStart.core.overlays.openConfirm}
services={services}
indexPatterns={indexPatterns}
$http={$http}
perPageConfig={config.get('savedObjects:perPage')}
basePath={chrome.getBasePath()}
newIndexPatternUrl={kbnUrl.eval('#/management/kibana/index_pattern')}
uiCapabilities={uiCapabilites}
goInspectObject={object => {
if (object.meta.editUrl) {
kbnUrl.change(object.meta.editUrl);
$scope.$apply();
}
}}
canGoInApp={object => {
const { inAppUrl } = object.meta;
return inAppUrl && get(uiCapabilites, inAppUrl.uiCapabilitiesPath);
}}
/>
</I18nContext>,
node
);
});
}
function destroyObjectsTable() {
const node = document.getElementById(REACT_OBJECTS_TABLE_DOM_ELEMENT_ID);
node && unmountComponentAtNode(node);
}
uiRoutes
.when('/management/kibana/objects', {
template: objectIndexHTML,
k7Breadcrumbs: getIndexBreadcrumbs,
requireUICapability: 'management.kibana.objects',
})
.when('/management/kibana/objects/:service', {
redirectTo: '/management/kibana/objects',
});
uiModules.get('apps/management').directive('kbnManagementObjects', function() {
return {
restrict: 'E',
controllerAs: 'managementObjectsController',
controller: function($scope, $injector) {
updateObjectsTable($scope, $injector);
$scope.$on('$destroy', destroyObjectsTable);
},
};
});

View file

@ -1,5 +0,0 @@
<kbn-management-app section="kibana/objects" data-test-subj="savedObjectsEdit">
<kbn-management-objects-view>
<div id="reactSavedObjectsView"></div>
</kbn-management-objects-view>
</kbn-management-app>

View file

@ -1,85 +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 React from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import 'angular';
import 'angular-elastic/elastic';
import uiRoutes from 'ui/routes';
import { uiModules } from 'ui/modules';
import { I18nContext } from 'ui/i18n';
import { npStart } from 'ui/new_platform';
import objectViewHTML from './_view.html';
import { getViewBreadcrumbs } from './breadcrumbs';
import { savedObjectManagementRegistry } from '../../saved_object_registry';
import { SavedObjectEdition } from './saved_object_view';
const REACT_OBJECTS_VIEW_DOM_ELEMENT_ID = 'reactSavedObjectsView';
uiRoutes.when('/management/kibana/objects/:service/:id', {
template: objectViewHTML,
k7Breadcrumbs: getViewBreadcrumbs,
requireUICapability: 'management.kibana.objects',
});
function createReactView($scope, $routeParams) {
const { service: serviceName, id: objectId, notFound } = $routeParams;
const { savedObjects, overlays, notifications, application } = npStart.core;
$scope.$$postDigest(() => {
const node = document.getElementById(REACT_OBJECTS_VIEW_DOM_ELEMENT_ID);
if (!node) {
return;
}
render(
<I18nContext>
<SavedObjectEdition
id={objectId}
serviceName={serviceName}
serviceRegistry={savedObjectManagementRegistry}
savedObjectsClient={savedObjects.client}
overlays={overlays}
notifications={notifications}
capabilities={application.capabilities}
notFoundType={notFound}
/>
</I18nContext>,
node
);
});
}
function destroyReactView() {
const node = document.getElementById(REACT_OBJECTS_VIEW_DOM_ELEMENT_ID);
node && unmountComponentAtNode(node);
}
uiModules
.get('apps/management', ['monospaced.elastic'])
.directive('kbnManagementObjectsView', function() {
return {
restrict: 'E',
controller: function($scope, $routeParams) {
createReactView($scope, $routeParams);
$scope.$on('$destroy', destroyReactView);
},
};
});

View file

@ -1,50 +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 { MANAGEMENT_BREADCRUMB } from 'ui/management';
import { i18n } from '@kbn/i18n';
import { savedObjectManagementRegistry } from '../../saved_object_registry';
export function getIndexBreadcrumbs() {
return [
MANAGEMENT_BREADCRUMB,
{
text: i18n.translate('kbn.management.savedObjects.indexBreadcrumb', {
defaultMessage: 'Saved objects',
}),
href: '#/management/kibana/objects',
},
];
}
export function getViewBreadcrumbs($routeParams) {
const serviceObj = savedObjectManagementRegistry.get($routeParams.service);
const { service } = serviceObj;
return [
...getIndexBreadcrumbs(),
{
text: i18n.translate('kbn.management.savedObjects.editBreadcrumb', {
defaultMessage: 'Edit {savedObjectType}',
values: { savedObjectType: service.type },
}),
},
];
}

View file

@ -1,20 +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.
*/
export { ObjectsTable } from './objects_table';

View file

@ -78,13 +78,8 @@ export function savedObjectsMixin(kbnServer, server) {
const provider = kbnServer.newPlatform.__internals.savedObjectsClientProvider;
const importAndExportableTypes = typeRegistry
.getImportableAndExportableTypes()
.map(type => type.name);
const service = {
types: visibleTypes,
importAndExportableTypes,
SavedObjectsClient,
SavedObjectsRepository,
getSavedObjectsRepository: createRepository,

View file

@ -62,6 +62,7 @@ const createStartContract = (): Start => {
},
}),
get: jest.fn().mockReturnValue(Promise.resolve({})),
clearCache: jest.fn(),
} as unknown) as IndexPatternsContract,
};
};

View file

@ -37,6 +37,9 @@ const createStartContract = (): Start => {
docViews: {
DocViewer: jest.fn(() => null),
},
savedSearches: {
createLoader: jest.fn(),
},
};
return startContract;
};

View file

@ -21,12 +21,14 @@ import React from 'react';
import { i18n } from '@kbn/i18n';
import { auto } from 'angular';
import { CoreSetup, Plugin } from 'kibana/public';
import { SavedObjectLoader, SavedObjectKibanaServices } from '../../saved_objects/public';
import { DocViewInput, DocViewInputFn, DocViewRenderProps } from './doc_views/doc_views_types';
import { DocViewsRegistry } from './doc_views/doc_views_registry';
import { DocViewTable } from './components/table/table';
import { JsonCodeBlock } from './components/json_code_block/json_code_block';
import { DocViewer } from './components/doc_viewer/doc_viewer';
import { setDocViewsRegistry } from './services';
import { createSavedSearchesLoader } from './saved_searches';
import './index.scss';
@ -62,6 +64,13 @@ export interface DiscoverStart {
*/
DocViewer: React.ComponentType<DocViewRenderProps>;
};
savedSearches: {
/**
* Create a {@link SavedObjectLoader | loader} to handle the saved searches type.
* @param services
*/
createLoader(services: SavedObjectKibanaServices): SavedObjectLoader;
};
}
/**
@ -105,6 +114,9 @@ export class DiscoverPlugin implements Plugin<DiscoverSetup, DiscoverStart> {
docViews: {
DocViewer,
},
savedSearches: {
createLoader: createSavedSearchesLoader,
},
};
}
}

View file

@ -18,12 +18,21 @@
*/
import { ManagementSetup, ManagementStart } from '../types';
import { ManagementSection } from '../management_section';
const createManagementSectionMock = (): jest.Mocked<PublicMethodsOf<ManagementSection>> => {
return {
registerApp: jest.fn(),
getApp: jest.fn(),
getAppsEnabled: jest.fn().mockReturnValue([]),
};
};
const createSetupContract = (): DeeplyMockedKeys<ManagementSetup> => ({
sections: {
register: jest.fn(),
getSection: jest.fn(),
getAllSections: jest.fn(),
getSection: jest.fn().mockReturnValue(createManagementSectionMock()),
getAllSections: jest.fn().mockReturnValue([]),
},
});

View file

@ -17,4 +17,4 @@
* under the License.
*/
export { Header } from './header';
export { SavedObjectRelation, SavedObjectWithMetadata, SavedObjectMetadata } from './types';

View file

@ -17,8 +17,12 @@
* under the License.
*/
import { SavedObject, SavedObjectReference } from 'src/core/public';
import { SavedObject } from 'src/core/types';
/**
* The metadata injected into a {@link SavedObject | saved object} when returning
* {@link SavedObjectWithMetadata | enhanced objects} from the plugin API endpoints.
*/
export interface SavedObjectMetadata {
icon?: string;
title?: string;
@ -26,31 +30,19 @@ export interface SavedObjectMetadata {
inAppUrl?: { path: string; uiCapabilitiesPath: string };
}
/**
* A {@link SavedObject | saved object} enhanced with meta properties used by the client-side plugin.
*/
export type SavedObjectWithMetadata<T = unknown> = SavedObject<T> & {
meta: SavedObjectMetadata;
};
/**
* Represents a relation between two {@link SavedObject | saved object}
*/
export interface SavedObjectRelation {
id: string;
type: string;
relationship: 'child' | 'parent';
meta: SavedObjectMetadata;
}
export interface ObjectField {
type: FieldType;
name: string;
value: any;
}
export type FieldType = 'text' | 'number' | 'boolean' | 'array' | 'json';
export interface FieldState {
value?: any;
invalid?: boolean;
}
export interface SubmittedFormData {
attributes: any;
references: SavedObjectReference[];
}

View file

@ -3,5 +3,6 @@
"version": "kibana",
"server": true,
"ui": true,
"requiredPlugins": ["home"]
"requiredPlugins": ["home", "management", "data"],
"optionalPlugins": ["dashboard", "visualizations", "discover"]
}

View file

@ -22,10 +22,14 @@ import { SavedObjectsManagementPlugin } from './plugin';
export { SavedObjectsManagementPluginSetup, SavedObjectsManagementPluginStart } from './plugin';
export {
ISavedObjectsManagementActionRegistry,
SavedObjectsManagementActionServiceSetup,
SavedObjectsManagementActionServiceStart,
SavedObjectsManagementAction,
SavedObjectsManagementRecord,
ISavedObjectsManagementServiceRegistry,
SavedObjectsManagementServiceRegistryEntry,
} from './services';
export { SavedObjectRelation, SavedObjectWithMetadata, SavedObjectMetadata } from './types';
export function plugin(initializerContext: PluginInitializerContext) {
return new SavedObjectsManagementPlugin();

View file

@ -17,8 +17,8 @@
* under the License.
*/
import { SimpleSavedObject, SavedObjectReference } from '../../../../../../../../core/public';
import { savedObjectsServiceMock } from '../../../../../../../../core/public/mocks';
import { SimpleSavedObject, SavedObjectReference } from '../../../../core/public';
import { savedObjectsServiceMock } from '../../../../core/public/mocks';
import { createFieldList } from './create_field_list';
const savedObjectClientMock = savedObjectsServiceMock.createStartContract().client;

View file

@ -18,10 +18,10 @@
*/
import { forOwn, indexBy, isNumber, isBoolean, isPlainObject, isString } from 'lodash';
import { SimpleSavedObject } from '../../../../../../../../core/public';
import { castEsToKbnFieldTypeName } from '../../../../../../../../plugins/data/public';
import { ObjectField } from '../types';
import { SavedObjectLoader } from '../../../../../../../../plugins/saved_objects/public';
import { SimpleSavedObject } from '../../../../core/public';
import { castEsToKbnFieldTypeName } from '../../../data/public';
import { ObjectField } from '../management_section/types';
import { SavedObjectLoader } from '../../../saved_objects/public';
const maxRecursiveIterations = 20;

View file

@ -17,16 +17,15 @@
* under the License.
*/
import { kfetch } from 'ui/kfetch';
import { HttpStart } from 'src/core/public';
export async function fetchExportByTypeAndSearch(
http: HttpStart,
types: string[],
search: string | undefined,
includeReferencesDeep: boolean = false
): Promise<Blob> {
return await kfetch({
method: 'POST',
pathname: '/api/saved_objects/_export',
return http.post('/api/saved_objects/_export', {
body: JSON.stringify({
type: types,
search,

View file

@ -17,15 +17,14 @@
* under the License.
*/
import { kfetch } from 'ui/kfetch';
import { HttpStart } from 'src/core/public';
export async function fetchExportObjects(
http: HttpStart,
objects: any[],
includeReferencesDeep: boolean = false
): Promise<Blob> {
return await kfetch({
method: 'POST',
pathname: '/api/saved_objects/_export',
return http.post('/api/saved_objects/_export', {
body: JSON.stringify({
objects,
includeReferencesDeep,

View file

@ -17,16 +17,27 @@
* under the License.
*/
import { kfetch } from 'ui/kfetch';
import { SavedObjectsFindOptions } from 'src/core/public';
import { HttpStart, SavedObjectsFindOptions } from 'src/core/public';
import { keysToCamelCaseShallow } from './case_conversion';
import { SavedObjectWithMetadata } from '../types';
export async function findObjects(findOptions: SavedObjectsFindOptions) {
const response = await kfetch({
method: 'GET',
pathname: '/api/kibana/management/saved_objects/_find',
query: findOptions as Record<string, any>,
});
return keysToCamelCaseShallow(response);
interface SavedObjectsFindResponse {
total: number;
page: number;
perPage: number;
savedObjects: SavedObjectWithMetadata[];
}
export async function findObjects(
http: HttpStart,
findOptions: SavedObjectsFindOptions
): Promise<SavedObjectsFindResponse> {
const response = await http.get<Record<string, any>>(
'/api/kibana/management/saved_objects/_find',
{
query: findOptions as Record<string, any>,
}
);
return keysToCamelCaseShallow(response) as SavedObjectsFindResponse;
}

View file

@ -0,0 +1,31 @@
/*
* 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 { HttpStart } from 'src/core/public';
interface GetAllowedTypesResponse {
types: string[];
}
export async function getAllowedTypes(http: HttpStart) {
const response = await http.get<GetAllowedTypesResponse>(
'/api/kibana/management/saved_objects/_allowed_types'
);
return response.types;
}

View file

@ -17,44 +17,43 @@
* under the License.
*/
import { httpServiceMock } from '../../../../core/public/mocks';
import { getRelationships } from './get_relationships';
describe('getRelationships', () => {
it('should make an http request', async () => {
const $http = jest.fn() as any;
const basePath = 'test';
let httpMock: ReturnType<typeof httpServiceMock.createSetupContract>;
await getRelationships('dashboard', '1', ['search', 'index-pattern'], $http, basePath);
expect($http.mock.calls.length).toBe(1);
beforeEach(() => {
httpMock = httpServiceMock.createSetupContract();
});
it('should make an http request', async () => {
await getRelationships(httpMock, 'dashboard', '1', ['search', 'index-pattern']);
expect(httpMock.get).toHaveBeenCalledTimes(1);
});
it('should handle successful responses', async () => {
const $http = jest.fn().mockImplementation(() => ({ data: [1, 2] })) as any;
const basePath = 'test';
httpMock.get.mockResolvedValue([1, 2]);
const response = await getRelationships(
'dashboard',
'1',
['search', 'index-pattern'],
$http,
basePath
);
const response = await getRelationships(httpMock, 'dashboard', '1', [
'search',
'index-pattern',
]);
expect(response).toEqual([1, 2]);
});
it('should handle errors', async () => {
const $http = jest.fn().mockImplementation(() => {
httpMock.get.mockImplementation(() => {
const err = new Error();
(err as any).data = {
error: 'Test error',
statusCode: 500,
};
throw err;
}) as any;
const basePath = 'test';
});
await expect(
getRelationships('dashboard', '1', ['search', 'index-pattern'], $http, basePath)
getRelationships(httpMock, 'dashboard', '1', ['search', 'index-pattern'])
).rejects.toThrowErrorMatchingInlineSnapshot(`"Test error"`);
});
});

View file

@ -17,36 +17,30 @@
* under the License.
*/
import { IHttpService } from 'angular';
import { HttpStart } from 'src/core/public';
import { get } from 'lodash';
import { SavedObjectRelation } from '../types';
export async function getRelationships(
http: HttpStart,
type: string,
id: string,
savedObjectTypes: string[],
$http: IHttpService,
basePath: string
savedObjectTypes: string[]
): Promise<SavedObjectRelation[]> {
const url = `${basePath}/api/kibana/management/saved_objects/relationships/${encodeURIComponent(
const url = `/api/kibana/management/saved_objects/relationships/${encodeURIComponent(
type
)}/${encodeURIComponent(id)}`;
const options = {
method: 'GET',
url,
params: {
savedObjectTypes,
},
};
try {
const response = await $http<SavedObjectRelation[]>(options);
return response?.data;
} catch (resp) {
const respBody = get(resp, 'data', {}) as any;
const err = new Error(respBody.message || respBody.error || `${resp.status} Response`);
return await http.get<SavedObjectRelation[]>(url, {
query: {
savedObjectTypes,
},
});
} catch (respError) {
const respBody = get(respError, 'data', {}) as any;
const err = new Error(respBody.message || respBody.error || `${respError.status} Response`);
(err as any).statusCode = respBody.statusCode || resp.status;
(err as any).statusCode = respBody.statusCode || respError.status;
(err as any).body = respBody;
throw err;

View file

@ -17,18 +17,15 @@
* under the License.
*/
import { IHttpService } from 'angular';
import chrome from 'ui/chrome';
import { HttpStart } from 'src/core/public';
const apiBase = chrome.addBasePath('/api/kibana/management/saved_objects/scroll');
export async function getSavedObjectCounts(
$http: IHttpService,
http: HttpStart,
typesToInclude: string[],
searchString: string
searchString?: string
): Promise<Record<string, number>> {
const results = await $http.post<Record<string, number>>(`${apiBase}/counts`, {
typesToInclude,
searchString,
});
return results.data;
return await http.post<Record<string, number>>(
`/api/kibana/management/saved_objects/scroll/counts`,
{ body: JSON.stringify({ typesToInclude, searchString }) }
);
}

View file

@ -17,14 +17,18 @@
* under the License.
*/
import { kfetch } from 'ui/kfetch';
import { HttpStart, SavedObjectsImportError } from 'src/core/public';
export async function importFile(file: Blob, overwriteAll: boolean = false) {
interface ImportResponse {
success: boolean;
successCount: number;
errors?: SavedObjectsImportError[];
}
export async function importFile(http: HttpStart, file: File, overwriteAll: boolean = false) {
const formData = new FormData();
formData.append('file', file);
return await kfetch({
method: 'POST',
pathname: '/api/saved_objects/_import',
return await http.post<ImportResponse>('/api/saved_objects/_import', {
body: formData,
headers: {
// Important to be undefined, it forces proper headers to be set for FormData

View file

@ -17,7 +17,7 @@
* under the License.
*/
import { Capabilities } from '../../../../../../../../core/public';
import { Capabilities } from '../../../../core/public';
import { canViewInApp } from './in_app_url';
const createCapabilities = (sections: Record<string, any>): Capabilities => {

View file

@ -43,3 +43,5 @@ export {
export { getDefaultTitle } from './get_default_title';
export { findObjects } from './find_objects';
export { extractExportDetails, SavedObjectsExportResultDetails } from './extract_export_details';
export { createFieldList } from './create_field_list';
export { getAllowedTypes } from './get_allowed_types';

View file

@ -17,11 +17,8 @@
* under the License.
*/
import { kfetch } from 'ui/kfetch';
import { HttpStart } from 'src/core/public';
export async function logLegacyImport() {
return await kfetch({
method: 'POST',
pathname: '/api/saved_objects/_log_legacy_import',
});
export async function logLegacyImport(http: HttpStart) {
return http.post('/api/saved_objects/_log_legacy_import');
}

View file

@ -25,6 +25,6 @@ describe('getQueryText', () => {
getTermClauses: () => [{ value: 'foo' }, { value: 'bar' }],
getFieldClauses: () => [{ value: 'lala' }, { value: 'lolo' }],
};
expect(parseQuery({ ast })).toEqual({ queryText: 'foo bar', visibleTypes: 'lala' });
expect(parseQuery({ ast } as any)).toEqual({ queryText: 'foo bar', visibleTypes: 'lala' });
});
});

View file

@ -17,9 +17,16 @@
* under the License.
*/
export function parseQuery(query: any) {
let queryText;
let visibleTypes;
import { Query } from '@elastic/eui';
interface ParsedQuery {
queryText?: string;
visibleTypes?: string[];
}
export function parseQuery(query: Query): ParsedQuery {
let queryText: string | undefined;
let visibleTypes: string[] | undefined;
if (query) {
if (query.ast.getTermClauses().length) {
@ -29,7 +36,7 @@ export function parseQuery(query: any) {
.join(' ');
}
if (query.ast.getFieldClauses('type')) {
visibleTypes = query.ast.getFieldClauses('type')[0].value;
visibleTypes = query.ast.getFieldClauses('type')[0].value as string[];
}
}

View file

@ -17,14 +17,10 @@
* under the License.
*/
jest.mock('ui/kfetch', () => ({ kfetch: jest.fn() }));
import { SavedObjectsImportUnknownError } from 'src/core/public';
import { kfetch } from 'ui/kfetch';
import { httpServiceMock } from '../../../../core/public/mocks';
import { resolveImportErrors } from './resolve_import_errors';
const kfetchMock = kfetch as jest.Mock;
function getFormData(form: Map<string, any>) {
const formData: Record<string, any> = {};
for (const [key, val] of form.entries()) {
@ -39,13 +35,20 @@ function getFormData(form: Map<string, any>) {
describe('resolveImportErrors', () => {
const getConflictResolutions = jest.fn();
let httpMock: ReturnType<typeof httpServiceMock.createSetupContract>;
beforeEach(() => {
httpMock = httpServiceMock.createSetupContract();
jest.resetAllMocks();
});
const extractBodyFromCall = (index: number): Map<string, any> => {
return (httpMock.post.mock.calls[index] as any)[1].body;
};
test('works with empty import failures', async () => {
const result = await resolveImportErrors({
http: httpMock,
getConflictResolutions,
state: {
importCount: 0,
@ -62,6 +65,7 @@ Object {
test(`doesn't retry if only unknown failures are passed in`, async () => {
const result = await resolveImportErrors({
http: httpMock,
getConflictResolutions,
state: {
importCount: 0,
@ -98,7 +102,7 @@ Object {
});
test('resolves conflicts', async () => {
kfetchMock.mockResolvedValueOnce({
httpMock.post.mockResolvedValueOnce({
success: true,
successCount: 1,
});
@ -107,6 +111,7 @@ Object {
'a:2': false,
});
const result = await resolveImportErrors({
http: httpMock,
getConflictResolutions,
state: {
importCount: 0,
@ -139,7 +144,8 @@ Object {
"status": "success",
}
`);
const formData = getFormData(kfetchMock.mock.calls[0][0].body);
const formData = getFormData(extractBodyFromCall(0));
expect(formData).toMatchInlineSnapshot(`
Object {
"file": "undefined",
@ -156,12 +162,13 @@ Object {
});
test('resolves missing references', async () => {
kfetchMock.mockResolvedValueOnce({
httpMock.post.mockResolvedValueOnce({
success: true,
successCount: 2,
});
getConflictResolutions.mockResolvedValueOnce({});
const result = await resolveImportErrors({
http: httpMock,
getConflictResolutions,
state: {
importCount: 0,
@ -203,7 +210,7 @@ Object {
"status": "success",
}
`);
const formData = getFormData(kfetchMock.mock.calls[0][0].body);
const formData = getFormData(extractBodyFromCall(0));
expect(formData).toMatchInlineSnapshot(`
Object {
"file": "undefined",
@ -232,6 +239,7 @@ Object {
test(`doesn't resolve missing references if newIndexPatternId isn't defined`, async () => {
getConflictResolutions.mockResolvedValueOnce({});
const result = await resolveImportErrors({
http: httpMock,
getConflictResolutions,
state: {
importCount: 0,
@ -276,7 +284,7 @@ Object {
});
test('handles missing references then conflicts on the same errored objects', async () => {
kfetchMock.mockResolvedValueOnce({
httpMock.post.mockResolvedValueOnce({
success: false,
successCount: 0,
errors: [
@ -289,7 +297,7 @@ Object {
},
],
});
kfetchMock.mockResolvedValueOnce({
httpMock.post.mockResolvedValueOnce({
success: true,
successCount: 1,
});
@ -298,6 +306,7 @@ Object {
'a:1': true,
});
const result = await resolveImportErrors({
http: httpMock,
getConflictResolutions,
state: {
importCount: 0,
@ -334,7 +343,7 @@ Object {
"status": "success",
}
`);
const formData1 = getFormData(kfetchMock.mock.calls[0][0].body);
const formData1 = getFormData(extractBodyFromCall(0));
expect(formData1).toMatchInlineSnapshot(`
Object {
"file": "undefined",
@ -354,7 +363,7 @@ Object {
],
}
`);
const formData2 = getFormData(kfetchMock.mock.calls[1][0].body);
const formData2 = getFormData(extractBodyFromCall(1));
expect(formData2).toMatchInlineSnapshot(`
Object {
"file": "undefined",

View file

@ -17,7 +17,7 @@
* under the License.
*/
import { kfetch } from 'ui/kfetch';
import { HttpStart } from 'src/core/public';
import { FailedImport } from './process_import_response';
interface RetryObject {
@ -27,13 +27,11 @@ interface RetryObject {
replaceReferences?: any[];
}
async function callResolveImportErrorsApi(file: File, retries: any) {
async function callResolveImportErrorsApi(http: HttpStart, file: File, retries: any) {
const formData = new FormData();
formData.append('file', file);
formData.append('retries', JSON.stringify(retries));
return await kfetch({
method: 'POST',
pathname: '/api/saved_objects/_resolve_import_errors',
return http.post<any>('/api/saved_objects/_resolve_import_errors', {
headers: {
// Important to be undefined, it forces proper headers to be set for FormData
'Content-Type': undefined,
@ -100,9 +98,11 @@ function mapImportFailureToRetryObject({
}
export async function resolveImportErrors({
http,
getConflictResolutions,
state,
}: {
http: HttpStart;
getConflictResolutions: (objects: any[]) => Promise<Record<string, boolean>>;
state: { importCount: number; failedImports?: FailedImport[] } & Record<string, any>;
}) {
@ -170,7 +170,7 @@ export async function resolveImportErrors({
}
// Call API
const response = await callResolveImportErrorsApi(file, retries);
const response = await callResolveImportErrorsApi(http, file, retries);
successImportCount += response.successCount;
importFailures = [];
for (const { error, ...obj } of response.errors || []) {

View file

@ -23,11 +23,8 @@ import {
saveObjects,
saveObject,
} from './resolve_saved_objects';
import {
SavedObject,
SavedObjectLoader,
} from '../../../../../../../../plugins/saved_objects/public';
import { IndexPatternsContract } from '../../../../../../../../plugins/data/public';
import { SavedObject, SavedObjectLoader } from '../../../saved_objects/public';
import { IndexPatternsContract } from '../../../data/public';
class SavedObjectNotFound extends Error {
constructor(options: Record<string, any>) {

View file

@ -20,15 +20,8 @@
import { i18n } from '@kbn/i18n';
import { cloneDeep } from 'lodash';
import { OverlayStart, SavedObjectReference } from 'src/core/public';
import {
SavedObject,
SavedObjectLoader,
} from '../../../../../../../../plugins/saved_objects/public';
import {
IndexPatternsContract,
IIndexPattern,
createSearchSource,
} from '../../../../../../../../plugins/data/public';
import { SavedObject, SavedObjectLoader } from '../../../saved_objects/public';
import { IndexPatternsContract, IIndexPattern, createSearchSource } from '../../../data/public';
type SavedObjectsRawDoc = Record<string, any>;
@ -55,7 +48,7 @@ function addJsonFieldToIndexPattern(
target[fieldName] = JSON.parse(sourceString);
} catch (error) {
throw new Error(
i18n.translate('kbn.management.objects.parsingFieldErrorMessage', {
i18n.translate('savedObjectsManagement.parsingFieldErrorMessage', {
defaultMessage:
'Error encountered parsing {fieldName} for index pattern {indexName}: {errorMessage}',
values: {
@ -103,18 +96,21 @@ async function importIndexPattern(
if (!newId) {
// We can override and we want to prompt for confirmation
const isConfirmed = await openConfirm(
i18n.translate('kbn.management.indexPattern.confirmOverwriteLabel', {
i18n.translate('savedObjectsManagement.indexPattern.confirmOverwriteLabel', {
values: { title },
defaultMessage: "Are you sure you want to overwrite '{title}'?",
}),
{
title: i18n.translate('kbn.management.indexPattern.confirmOverwriteTitle', {
title: i18n.translate('savedObjectsManagement.indexPattern.confirmOverwriteTitle', {
defaultMessage: 'Overwrite {type}?',
values: { type },
}),
confirmButtonText: i18n.translate('kbn.management.indexPattern.confirmOverwriteButton', {
defaultMessage: 'Overwrite',
}),
confirmButtonText: i18n.translate(
'savedObjectsManagement.indexPattern.confirmOverwriteButton',
{
defaultMessage: 'Overwrite',
}
),
}
);

View file

@ -17,4 +17,4 @@
* under the License.
*/
export { Relationships } from './relationships';
export { mountManagementSection } from './mount_section';

View file

@ -0,0 +1,211 @@
/*
* 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 React, { useEffect } from 'react';
import ReactDOM from 'react-dom';
import { HashRouter, Switch, Route, useParams, useLocation } from 'react-router-dom';
import { parse } from 'query-string';
import { get } from 'lodash';
import { i18n } from '@kbn/i18n';
import { I18nProvider } from '@kbn/i18n/react';
import { CoreSetup, CoreStart, ChromeBreadcrumb, Capabilities } from 'src/core/public';
import { ManagementAppMountParams } from '../../../management/public';
import { DataPublicPluginStart } from '../../../data/public';
import { StartDependencies, SavedObjectsManagementPluginStart } from '../plugin';
import {
ISavedObjectsManagementServiceRegistry,
SavedObjectsManagementActionServiceStart,
} from '../services';
import { SavedObjectsTable } from './objects_table';
import { SavedObjectEdition } from './object_view';
import { getAllowedTypes } from './../lib';
interface MountParams {
core: CoreSetup<StartDependencies, SavedObjectsManagementPluginStart>;
serviceRegistry: ISavedObjectsManagementServiceRegistry;
mountParams: ManagementAppMountParams;
}
let allowedObjectTypes: string[] | undefined;
export const mountManagementSection = async ({
core,
mountParams,
serviceRegistry,
}: MountParams) => {
const [coreStart, { data }, pluginStart] = await core.getStartServices();
const { element, basePath, setBreadcrumbs } = mountParams;
if (allowedObjectTypes === undefined) {
allowedObjectTypes = await getAllowedTypes(coreStart.http);
}
const capabilities = coreStart.application.capabilities;
ReactDOM.render(
<I18nProvider>
<HashRouter basename={basePath}>
<Switch>
<Route path={'/:service/:id'} exact={true}>
<RedirectToHomeIfUnauthorized capabilities={capabilities}>
<SavedObjectsEditionPage
coreStart={coreStart}
serviceRegistry={serviceRegistry}
setBreadcrumbs={setBreadcrumbs}
/>
</RedirectToHomeIfUnauthorized>
</Route>
<Route path={'/'} exact={false}>
<RedirectToHomeIfUnauthorized capabilities={capabilities}>
<SavedObjectsTablePage
coreStart={coreStart}
dataStart={data}
serviceRegistry={serviceRegistry}
actionRegistry={pluginStart.actions}
allowedTypes={allowedObjectTypes}
setBreadcrumbs={setBreadcrumbs}
/>
</RedirectToHomeIfUnauthorized>
</Route>
</Switch>
</HashRouter>
</I18nProvider>,
element
);
return () => {
ReactDOM.unmountComponentAtNode(element);
};
};
const RedirectToHomeIfUnauthorized: React.FunctionComponent<{
capabilities: Capabilities;
}> = ({ children, capabilities }) => {
const allowed = capabilities?.management?.kibana?.objects ?? false;
if (!allowed) {
window.location.hash = '/home';
return null;
}
return children! as React.ReactElement;
};
const SavedObjectsEditionPage = ({
coreStart,
serviceRegistry,
setBreadcrumbs,
}: {
coreStart: CoreStart;
serviceRegistry: ISavedObjectsManagementServiceRegistry;
setBreadcrumbs: (crumbs: ChromeBreadcrumb[]) => void;
}) => {
const { service: serviceName, id } = useParams<{ service: string; id: string }>();
const capabilities = coreStart.application.capabilities;
const { search } = useLocation();
const query = parse(search);
const service = serviceRegistry.get(serviceName);
useEffect(() => {
setBreadcrumbs([
{
text: i18n.translate('savedObjectsManagement.breadcrumb.index', {
defaultMessage: 'Saved objects',
}),
href: '#/management/kibana/objects',
},
{
text: i18n.translate('savedObjectsManagement.breadcrumb.edit', {
defaultMessage: 'Edit {savedObjectType}',
values: { savedObjectType: service?.service.type ?? 'object' },
}),
},
]);
}, [setBreadcrumbs, service]);
return (
<SavedObjectEdition
id={id}
serviceName={serviceName}
serviceRegistry={serviceRegistry}
savedObjectsClient={coreStart.savedObjects.client}
overlays={coreStart.overlays}
notifications={coreStart.notifications}
capabilities={capabilities}
notFoundType={query.notFound as string}
/>
);
};
const SavedObjectsTablePage = ({
coreStart,
dataStart,
allowedTypes,
serviceRegistry,
actionRegistry,
setBreadcrumbs,
}: {
coreStart: CoreStart;
dataStart: DataPublicPluginStart;
allowedTypes: string[];
serviceRegistry: ISavedObjectsManagementServiceRegistry;
actionRegistry: SavedObjectsManagementActionServiceStart;
setBreadcrumbs: (crumbs: ChromeBreadcrumb[]) => void;
}) => {
const capabilities = coreStart.application.capabilities;
const itemsPerPage = coreStart.uiSettings.get<number>('savedObjects:perPage', 50);
useEffect(() => {
setBreadcrumbs([
{
text: i18n.translate('savedObjectsManagement.breadcrumb.index', {
defaultMessage: 'Saved objects',
}),
href: '#/management/kibana/objects',
},
]);
}, [setBreadcrumbs]);
return (
<SavedObjectsTable
allowedTypes={allowedTypes}
serviceRegistry={serviceRegistry}
actionRegistry={actionRegistry}
savedObjectsClient={coreStart.savedObjects.client}
indexPatterns={dataStart.indexPatterns}
http={coreStart.http}
overlays={coreStart.overlays}
notifications={coreStart.notifications}
applications={coreStart.application}
perPageConfig={itemsPerPage}
goInspectObject={savedObject => {
const { editUrl } = savedObject.meta;
if (editUrl) {
// previously, kbnUrl.change(object.meta.editUrl); was used.
// using direct access to location.hash seems the only option for now,
// as using react-router-dom will prefix the url with the router's basename
// which should be ignored there.
window.location.hash = editUrl;
}
}}
canGoInApp={savedObject => {
const { inAppUrl } = savedObject.meta;
return inAppUrl ? get(capabilities, inAppUrl.uiCapabilitiesPath) : false;
}}
/>
);
};

View file

@ -23,7 +23,7 @@ exports[`Intro component renders correctly 1`] = `
>
<FormattedMessage
defaultMessage="Edit {title}"
id="kbn.management.objects.view.editItemTitle"
id="savedObjectsManagement.view.editItemTitle"
values={
Object {
"title": "search",
@ -85,7 +85,7 @@ exports[`Intro component renders correctly 1`] = `
>
<FormattedMessage
defaultMessage="View {title}"
id="kbn.management.objects.view.viewItemButtonLabel"
id="savedObjectsManagement.view.viewItemButtonLabel"
values={
Object {
"title": "search",
@ -140,7 +140,7 @@ exports[`Intro component renders correctly 1`] = `
>
<FormattedMessage
defaultMessage="Delete {title}"
id="kbn.management.objects.view.deleteItemButtonLabel"
id="savedObjectsManagement.view.deleteItemButtonLabel"
values={
Object {
"title": "search",

View file

@ -8,7 +8,7 @@ exports[`Intro component renders correctly 1`] = `
title={
<FormattedMessage
defaultMessage="Proceed with caution!"
id="kbn.management.objects.view.howToModifyObjectTitle"
id="savedObjectsManagement.view.howToModifyObjectTitle"
values={Object {}}
/>
}
@ -37,7 +37,7 @@ exports[`Intro component renders correctly 1`] = `
>
<FormattedMessage
defaultMessage="Proceed with caution!"
id="kbn.management.objects.view.howToModifyObjectTitle"
id="savedObjectsManagement.view.howToModifyObjectTitle"
values={Object {}}
>
Proceed with caution!
@ -53,7 +53,7 @@ exports[`Intro component renders correctly 1`] = `
<div>
<FormattedMessage
defaultMessage="Modifying objects is for advanced users only. Object properties are not validated and invalid objects could cause errors, data loss, or worse. Unless someone with intimate knowledge of the code told you to be in here, you probably shouldnt be."
id="kbn.management.objects.view.howToModifyObjectDescription"
id="savedObjectsManagement.view.howToModifyObjectDescription"
values={Object {}}
>
Modifying objects is for advanced users only. Object properties are not validated and invalid objects could cause errors, data loss, or worse. Unless someone with intimate knowledge of the code told you to be in here, you probably shouldnt be.

View file

@ -10,7 +10,7 @@ exports[`NotFoundErrors component renders correctly for index-pattern type 1`] =
title={
<FormattedMessage
defaultMessage="There is a problem with this saved object"
id="kbn.management.objects.view.savedObjectProblemErrorMessage"
id="savedObjectsManagement.view.savedObjectProblemErrorMessage"
values={Object {}}
/>
}
@ -39,7 +39,7 @@ exports[`NotFoundErrors component renders correctly for index-pattern type 1`] =
>
<FormattedMessage
defaultMessage="There is a problem with this saved object"
id="kbn.management.objects.view.savedObjectProblemErrorMessage"
id="savedObjectsManagement.view.savedObjectProblemErrorMessage"
values={Object {}}
>
There is a problem with this saved object
@ -55,7 +55,7 @@ exports[`NotFoundErrors component renders correctly for index-pattern type 1`] =
<div>
<FormattedMessage
defaultMessage="The index pattern associated with this object no longer exists."
id="kbn.management.objects.view.indexPatternDoesNotExistErrorMessage"
id="savedObjectsManagement.view.indexPatternDoesNotExistErrorMessage"
values={Object {}}
>
The index pattern associated with this object no longer exists.
@ -64,7 +64,7 @@ exports[`NotFoundErrors component renders correctly for index-pattern type 1`] =
<div>
<FormattedMessage
defaultMessage="If you know what this error means, go ahead and fix it — otherwise click the delete button above."
id="kbn.management.objects.view.howToFixErrorDescription"
id="savedObjectsManagement.view.howToFixErrorDescription"
values={Object {}}
>
If you know what this error means, go ahead and fix it — otherwise click the delete button above.
@ -87,7 +87,7 @@ exports[`NotFoundErrors component renders correctly for index-pattern-field type
title={
<FormattedMessage
defaultMessage="There is a problem with this saved object"
id="kbn.management.objects.view.savedObjectProblemErrorMessage"
id="savedObjectsManagement.view.savedObjectProblemErrorMessage"
values={Object {}}
/>
}
@ -116,7 +116,7 @@ exports[`NotFoundErrors component renders correctly for index-pattern-field type
>
<FormattedMessage
defaultMessage="There is a problem with this saved object"
id="kbn.management.objects.view.savedObjectProblemErrorMessage"
id="savedObjectsManagement.view.savedObjectProblemErrorMessage"
values={Object {}}
>
There is a problem with this saved object
@ -132,7 +132,7 @@ exports[`NotFoundErrors component renders correctly for index-pattern-field type
<div>
<FormattedMessage
defaultMessage="A field associated with this object no longer exists in the index pattern."
id="kbn.management.objects.view.fieldDoesNotExistErrorMessage"
id="savedObjectsManagement.view.fieldDoesNotExistErrorMessage"
values={Object {}}
>
A field associated with this object no longer exists in the index pattern.
@ -141,7 +141,7 @@ exports[`NotFoundErrors component renders correctly for index-pattern-field type
<div>
<FormattedMessage
defaultMessage="If you know what this error means, go ahead and fix it — otherwise click the delete button above."
id="kbn.management.objects.view.howToFixErrorDescription"
id="savedObjectsManagement.view.howToFixErrorDescription"
values={Object {}}
>
If you know what this error means, go ahead and fix it — otherwise click the delete button above.
@ -164,7 +164,7 @@ exports[`NotFoundErrors component renders correctly for search type 1`] = `
title={
<FormattedMessage
defaultMessage="There is a problem with this saved object"
id="kbn.management.objects.view.savedObjectProblemErrorMessage"
id="savedObjectsManagement.view.savedObjectProblemErrorMessage"
values={Object {}}
/>
}
@ -193,7 +193,7 @@ exports[`NotFoundErrors component renders correctly for search type 1`] = `
>
<FormattedMessage
defaultMessage="There is a problem with this saved object"
id="kbn.management.objects.view.savedObjectProblemErrorMessage"
id="savedObjectsManagement.view.savedObjectProblemErrorMessage"
values={Object {}}
>
There is a problem with this saved object
@ -209,7 +209,7 @@ exports[`NotFoundErrors component renders correctly for search type 1`] = `
<div>
<FormattedMessage
defaultMessage="The saved search associated with this object no longer exists."
id="kbn.management.objects.view.savedSearchDoesNotExistErrorMessage"
id="savedObjectsManagement.view.savedSearchDoesNotExistErrorMessage"
values={Object {}}
>
The saved search associated with this object no longer exists.
@ -218,7 +218,7 @@ exports[`NotFoundErrors component renders correctly for search type 1`] = `
<div>
<FormattedMessage
defaultMessage="If you know what this error means, go ahead and fix it — otherwise click the delete button above."
id="kbn.management.objects.view.howToFixErrorDescription"
id="savedObjectsManagement.view.howToFixErrorDescription"
values={Object {}}
>
If you know what this error means, go ahead and fix it — otherwise click the delete button above.
@ -241,7 +241,7 @@ exports[`NotFoundErrors component renders correctly for unknown type 1`] = `
title={
<FormattedMessage
defaultMessage="There is a problem with this saved object"
id="kbn.management.objects.view.savedObjectProblemErrorMessage"
id="savedObjectsManagement.view.savedObjectProblemErrorMessage"
values={Object {}}
/>
}
@ -270,7 +270,7 @@ exports[`NotFoundErrors component renders correctly for unknown type 1`] = `
>
<FormattedMessage
defaultMessage="There is a problem with this saved object"
id="kbn.management.objects.view.savedObjectProblemErrorMessage"
id="savedObjectsManagement.view.savedObjectProblemErrorMessage"
values={Object {}}
>
There is a problem with this saved object
@ -287,7 +287,7 @@ exports[`NotFoundErrors component renders correctly for unknown type 1`] = `
<div>
<FormattedMessage
defaultMessage="If you know what this error means, go ahead and fix it — otherwise click the delete button above."
id="kbn.management.objects.view.howToFixErrorDescription"
id="savedObjectsManagement.view.howToFixErrorDescription"
values={Object {}}
>
If you know what this error means, go ahead and fix it — otherwise click the delete button above.

View file

@ -104,9 +104,9 @@ export class Field extends PureComponent<FieldProps> {
id={this.fieldId}
label={
!!currentValue ? (
<FormattedMessage id="kbn.management.objects.field.onLabel" defaultMessage="On" />
<FormattedMessage id="savedObjectsManagement.field.onLabel" defaultMessage="On" />
) : (
<FormattedMessage id="kbn.management.objects.field.offLabel" defaultMessage="Off" />
<FormattedMessage id="savedObjectsManagement.field.offLabel" defaultMessage="Off" />
)
}
checked={!!currentValue}

View file

@ -29,15 +29,11 @@ import {
import { cloneDeep, set } from 'lodash';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import {
SimpleSavedObject,
SavedObjectsClientContract,
} from '../../../../../../../../../core/public';
import { SavedObjectLoader } from '../../../../../../../../../plugins/saved_objects/public';
import { SimpleSavedObject, SavedObjectsClientContract } from '../../../../../../core/public';
import { SavedObjectLoader } from '../../../../../saved_objects/public';
import { Field } from './field';
import { ObjectField, FieldState, SubmittedFormData } from '../../types';
import { createFieldList } from '../../lib/create_field_list';
import { createFieldList } from '../../../lib';
interface FormProps {
object: SimpleSavedObject;
@ -96,7 +92,7 @@ export class Form extends Component<FormProps, FormState> {
<EuiFlexItem grow={false}>
<EuiButton
fill={true}
aria-label={i18n.translate('kbn.management.objects.view.saveButtonAriaLabel', {
aria-label={i18n.translate('savedObjectsManagement.view.saveButtonAriaLabel', {
defaultMessage: 'Save { title } object',
values: {
title: service.type,
@ -107,7 +103,7 @@ export class Form extends Component<FormProps, FormState> {
data-test-subj="savedObjectEditSave"
>
<FormattedMessage
id="kbn.management.objects.view.saveButtonLabel"
id="savedObjectsManagement.view.saveButtonLabel"
defaultMessage="Save { title } object"
values={{ title: service.type }}
/>
@ -117,14 +113,14 @@ export class Form extends Component<FormProps, FormState> {
<EuiFlexItem grow={false}>
<EuiButtonEmpty
aria-label={i18n.translate('kbn.management.objects.view.cancelButtonAriaLabel', {
aria-label={i18n.translate('savedObjectsManagement.view.cancelButtonAriaLabel', {
defaultMessage: 'Cancel',
})}
onClick={this.onCancel}
data-test-subj="savedObjectEditCancel"
>
<FormattedMessage
id="kbn.management.objects.view.cancelButtonLabel"
id="savedObjectsManagement.view.cancelButtonLabel"
defaultMessage="Cancel"
/>
</EuiButtonEmpty>

View file

@ -52,7 +52,7 @@ export const Header = ({
{canEdit ? (
<h1>
<FormattedMessage
id="kbn.management.objects.view.editItemTitle"
id="savedObjectsManagement.view.editItemTitle"
defaultMessage="Edit {title}"
values={{ title: type }}
/>
@ -60,7 +60,7 @@ export const Header = ({
) : (
<h1>
<FormattedMessage
id="kbn.management.objects.view.viewItemTitle"
id="savedObjectsManagement.view.viewItemTitle"
defaultMessage="View {title}"
values={{ title: type }}
/>
@ -79,7 +79,7 @@ export const Header = ({
data-test-subj="savedObjectEditViewInApp"
>
<FormattedMessage
id="kbn.management.objects.view.viewItemButtonLabel"
id="savedObjectsManagement.view.viewItemButtonLabel"
defaultMessage="View {title}"
values={{ title: type }}
/>
@ -96,7 +96,7 @@ export const Header = ({
data-test-subj="savedObjectEditDelete"
>
<FormattedMessage
id="kbn.management.objects.view.deleteItemButtonLabel"
id="savedObjectsManagement.view.deleteItemButtonLabel"
defaultMessage="Delete {title}"
values={{ title: type }}
/>

View file

@ -26,7 +26,7 @@ export const Intro = () => {
<EuiCallOut
title={
<FormattedMessage
id="kbn.management.objects.view.howToModifyObjectTitle"
id="savedObjectsManagement.view.howToModifyObjectTitle"
defaultMessage="Proceed with caution!"
/>
}
@ -35,7 +35,7 @@ export const Intro = () => {
>
<div>
<FormattedMessage
id="kbn.management.objects.view.howToModifyObjectDescription"
id="savedObjectsManagement.view.howToModifyObjectDescription"
defaultMessage="Modifying objects is for advanced users only. Object properties are not validated and invalid objects could cause errors, data loss, or worse. Unless someone with intimate knowledge of the code told you to be in here, you probably shouldn&rsquo;t be."
/>
</div>

View file

@ -31,21 +31,21 @@ export const NotFoundErrors = ({ type }: NotFoundErrors) => {
case 'search':
return (
<FormattedMessage
id="kbn.management.objects.view.savedSearchDoesNotExistErrorMessage"
id="savedObjectsManagement.view.savedSearchDoesNotExistErrorMessage"
defaultMessage="The saved search associated with this object no longer exists."
/>
);
case 'index-pattern':
return (
<FormattedMessage
id="kbn.management.objects.view.indexPatternDoesNotExistErrorMessage"
id="savedObjectsManagement.view.indexPatternDoesNotExistErrorMessage"
defaultMessage="The index pattern associated with this object no longer exists."
/>
);
case 'index-pattern-field':
return (
<FormattedMessage
id="kbn.management.objects.view.fieldDoesNotExistErrorMessage"
id="savedObjectsManagement.view.fieldDoesNotExistErrorMessage"
defaultMessage="A field associated with this object no longer exists in the index pattern."
/>
);
@ -58,7 +58,7 @@ export const NotFoundErrors = ({ type }: NotFoundErrors) => {
<EuiCallOut
title={
<FormattedMessage
id="kbn.management.objects.view.savedObjectProblemErrorMessage"
id="savedObjectsManagement.view.savedObjectProblemErrorMessage"
defaultMessage="There is a problem with this saved object"
/>
}
@ -68,7 +68,7 @@ export const NotFoundErrors = ({ type }: NotFoundErrors) => {
<div>{getMessage()}</div>
<div>
<FormattedMessage
id="kbn.management.objects.view.howToFixErrorDescription"
id="savedObjectsManagement.view.howToFixErrorDescription"
defaultMessage="If you know what this error means, go ahead and fix it &mdash; otherwise click the delete button above."
/>
</div>

View file

@ -17,4 +17,4 @@
* under the License.
*/
export { Flyout } from './flyout';
export { SavedObjectEdition } from './saved_object_view';

View file

@ -26,16 +26,16 @@ import {
OverlayStart,
NotificationsStart,
SimpleSavedObject,
} from '../../../../../../../core/public';
import { ISavedObjectsManagementRegistry } from '../../saved_object_registry';
import { Header, NotFoundErrors, Intro, Form } from './components/object_view';
import { canViewInApp } from './lib/in_app_url';
import { SubmittedFormData } from './types';
} from '../../../../../core/public';
import { ISavedObjectsManagementServiceRegistry } from '../../services';
import { Header, NotFoundErrors, Intro, Form } from './components';
import { canViewInApp } from '../../lib';
import { SubmittedFormData } from '../types';
interface SavedObjectEditionProps {
id: string;
serviceName: string;
serviceRegistry: ISavedObjectsManagementRegistry;
serviceRegistry: ISavedObjectsManagementServiceRegistry;
capabilities: Capabilities;
overlays: OverlayStart;
notifications: NotificationsStart;
@ -135,17 +135,17 @@ export class SavedObjectEdition extends Component<
const { type, object } = this.state;
const confirmed = await overlays.openConfirm(
i18n.translate('kbn.management.objects.confirmModalOptions.modalDescription', {
i18n.translate('savedObjectsManagement.deleteConfirm.modalDescription', {
defaultMessage: 'This action permanently removes the object from Kibana.',
}),
{
confirmButtonText: i18n.translate(
'kbn.management.objects.confirmModalOptions.deleteButtonLabel',
'savedObjectsManagement.deleteConfirm.modalDeleteButtonLabel',
{
defaultMessage: 'Delete',
}
),
title: i18n.translate('kbn.management.objects.confirmModalOptions.modalTitle', {
title: i18n.translate('savedObjectsManagement.deleteConfirm.modalTitle', {
defaultMessage: `Delete '{title}'?`,
values: {
title: object?.attributes?.title || 'saved Kibana object',

View file

@ -1,19 +1,19 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ObjectsTable delete should show a confirm modal 1`] = `
exports[`SavedObjectsTable delete should show a confirm modal 1`] = `
<EuiConfirmModal
buttonColor="danger"
cancelButtonText={
<FormattedMessage
defaultMessage="Cancel"
id="kbn.management.objects.objectsTable.deleteSavedObjectsConfirmModal.cancelButtonLabel"
id="savedObjectsManagement.objectsTable.deleteSavedObjectsConfirmModal.cancelButtonLabel"
values={Object {}}
/>
}
confirmButtonText={
<FormattedMessage
defaultMessage="Delete"
id="kbn.management.objects.objectsTable.deleteSavedObjectsConfirmModal.deleteButtonLabel"
id="savedObjectsManagement.objectsTable.deleteSavedObjectsConfirmModal.deleteButtonLabel"
values={Object {}}
/>
}
@ -23,7 +23,7 @@ exports[`ObjectsTable delete should show a confirm modal 1`] = `
title={
<FormattedMessage
defaultMessage="Delete saved objects"
id="kbn.management.objects.objectsTable.deleteSavedObjectsConfirmModalTitle"
id="savedObjectsManagement.objectsTable.deleteSavedObjectsConfirmModalTitle"
values={Object {}}
/>
}
@ -31,7 +31,7 @@ exports[`ObjectsTable delete should show a confirm modal 1`] = `
<p>
<FormattedMessage
defaultMessage="This action will delete the following saved objects:"
id="kbn.management.objects.deleteSavedObjectsConfirmModalDescription"
id="savedObjectsManagement.deleteSavedObjectsConfirmModalDescription"
values={Object {}}
/>
</p>
@ -58,12 +58,10 @@ exports[`ObjectsTable delete should show a confirm modal 1`] = `
Array [
Object {
"id": "1",
"title": "Title 1",
"type": "index-pattern",
},
Object {
"id": "3",
"title": "Title 2",
"type": "dashboard",
},
]
@ -76,7 +74,7 @@ exports[`ObjectsTable delete should show a confirm modal 1`] = `
</EuiConfirmModal>
`;
exports[`ObjectsTable export should allow the user to choose when exporting all 1`] = `
exports[`SavedObjectsTable export should allow the user to choose when exporting all 1`] = `
<EuiModal
onClose={[Function]}
>
@ -84,7 +82,7 @@ exports[`ObjectsTable export should allow the user to choose when exporting all
<EuiModalHeaderTitle>
<FormattedMessage
defaultMessage="Export {filteredItemCount, plural, one{# object} other {# objects}}"
id="kbn.management.objects.objectsTable.exportObjectsConfirmModalTitle"
id="savedObjectsManagement.objectsTable.exportObjectsConfirmModalTitle"
values={
Object {
"filteredItemCount": 4,
@ -103,7 +101,7 @@ exports[`ObjectsTable export should allow the user to choose when exporting all
label={
<FormattedMessage
defaultMessage="Select which types to export"
id="kbn.management.objects.objectsTable.exportObjectsConfirmModalDescription"
id="savedObjectsManagement.objectsTable.exportObjectsConfirmModalDescription"
values={Object {}}
/>
}
@ -149,7 +147,7 @@ exports[`ObjectsTable export should allow the user to choose when exporting all
label={
<FormattedMessage
defaultMessage="Include related objects"
id="kbn.management.objects.objectsTable.exportObjectsConfirmModal.includeReferencesDeepLabel"
id="savedObjectsManagement.objectsTable.exportObjectsConfirmModal.includeReferencesDeepLabel"
values={Object {}}
/>
}
@ -173,7 +171,7 @@ exports[`ObjectsTable export should allow the user to choose when exporting all
>
<FormattedMessage
defaultMessage="Cancel"
id="kbn.management.objects.objectsTable.exportObjectsConfirmModal.cancelButtonLabel"
id="savedObjectsManagement.objectsTable.exportObjectsConfirmModal.cancelButtonLabel"
values={Object {}}
/>
</EuiButtonEmpty>
@ -187,7 +185,7 @@ exports[`ObjectsTable export should allow the user to choose when exporting all
>
<FormattedMessage
defaultMessage="Export all"
id="kbn.management.objects.objectsTable.exportObjectsConfirmModal.exportAllButtonLabel"
id="savedObjectsManagement.objectsTable.exportObjectsConfirmModal.exportAllButtonLabel"
values={Object {}}
/>
</EuiButton>
@ -199,23 +197,87 @@ exports[`ObjectsTable export should allow the user to choose when exporting all
</EuiModal>
`;
exports[`ObjectsTable import should show the flyout 1`] = `
exports[`SavedObjectsTable import should show the flyout 1`] = `
<Flyout
allowedTypes={
Array [
"index-pattern",
"visualization",
"dashboard",
"search",
]
}
close={[Function]}
confirmModalPromise={[MockFunction]}
done={[Function]}
http={
Object {
"addLoadingCountSource": [MockFunction],
"anonymousPaths": Object {
"isAnonymous": [MockFunction],
"register": [MockFunction],
},
"basePath": BasePath {
"basePath": "",
"get": [Function],
"prepend": [Function],
"remove": [Function],
"serverBasePath": "",
},
"delete": [MockFunction],
"fetch": [MockFunction],
"get": [MockFunction],
"getLoadingCount$": [MockFunction],
"head": [MockFunction],
"intercept": [MockFunction],
"options": [MockFunction],
"patch": [MockFunction],
"post": [MockFunction],
"put": [MockFunction],
}
}
indexPatterns={
Object {
"clearCache": [MockFunction],
"get": [MockFunction],
"make": [Function],
}
}
overlays={
Object {
"banners": Object {
"add": [MockFunction],
"get$": [MockFunction],
"getComponent": [MockFunction],
"remove": [MockFunction],
"replace": [MockFunction],
},
"openConfirm": [MockFunction],
"openFlyout": [MockFunction],
"openModal": [MockFunction],
}
}
serviceRegistry={
Object {
"all": [MockFunction],
"get": [MockFunction],
"register": [MockFunction],
}
}
newIndexPatternUrl=""
services={Array []}
/>
`;
exports[`ObjectsTable relationships should show the flyout 1`] = `
exports[`SavedObjectsTable relationships should show the flyout 1`] = `
<Relationships
basePath={
BasePath {
"basePath": "",
"get": [Function],
"prepend": [Function],
"remove": [Function],
"serverBasePath": "",
}
}
canGoInApp={[Function]}
close={[Function]}
getRelationships={[Function]}
goInspectObject={[Function]}
@ -237,7 +299,7 @@ exports[`ObjectsTable relationships should show the flyout 1`] = `
/>
`;
exports[`ObjectsTable should render normally 1`] = `
exports[`SavedObjectsTable should render normally 1`] = `
<EuiPageContent
horizontalPosition="center"
>
@ -251,7 +313,23 @@ exports[`ObjectsTable should render normally 1`] = `
size="xs"
/>
<Table
actionRegistry={
Object {
"getAll": [MockFunction],
"has": [MockFunction],
}
}
basePath={
BasePath {
"basePath": "",
"get": [Function],
"prepend": [Function],
"remove": [Function],
"serverBasePath": "",
}
}
canDelete={false}
canGoInApp={[Function]}
filterOptions={
Array [
Object {

View file

@ -18,7 +18,7 @@ exports[`Flyout conflicts should allow conflict resolution 1`] = `
<h2>
<FormattedMessage
defaultMessage="Import saved objects"
id="kbn.management.objects.objectsTable.flyout.importSavedObjectTitle"
id="savedObjectsManagement.objectsTable.flyout.importSavedObjectTitle"
values={Object {}}
/>
</h2>
@ -36,7 +36,7 @@ exports[`Flyout conflicts should allow conflict resolution 1`] = `
title={
<FormattedMessage
defaultMessage="Index Pattern Conflicts"
id="kbn.management.objects.objectsTable.flyout.indexPatternConflictsTitle"
id="savedObjectsManagement.objectsTable.flyout.indexPatternConflictsTitle"
values={Object {}}
/>
}
@ -44,7 +44,7 @@ exports[`Flyout conflicts should allow conflict resolution 1`] = `
<p>
<FormattedMessage
defaultMessage="The following saved objects use index patterns that do not exist. Please select the index patterns you'd like re-associated with them. You can {indexPatternLink} if necessary."
id="kbn.management.objects.objectsTable.flyout.indexPatternConflictsDescription"
id="savedObjectsManagement.objectsTable.flyout.indexPatternConflictsDescription"
values={
Object {
"indexPatternLink": <ForwardRef
@ -52,7 +52,7 @@ exports[`Flyout conflicts should allow conflict resolution 1`] = `
>
<FormattedMessage
defaultMessage="create a new index pattern"
id="kbn.management.objects.objectsTable.flyout.indexPatternConflictsCalloutLinkText"
id="savedObjectsManagement.objectsTable.flyout.indexPatternConflictsCalloutLinkText"
values={Object {}}
/>
</ForwardRef>,
@ -131,7 +131,7 @@ exports[`Flyout conflicts should allow conflict resolution 1`] = `
>
<FormattedMessage
defaultMessage="Cancel"
id="kbn.management.objects.objectsTable.flyout.import.cancelButtonLabel"
id="savedObjectsManagement.objectsTable.flyout.import.cancelButtonLabel"
values={Object {}}
/>
</EuiButtonEmpty>
@ -148,7 +148,7 @@ exports[`Flyout conflicts should allow conflict resolution 1`] = `
>
<FormattedMessage
defaultMessage="Confirm all changes"
id="kbn.management.objects.objectsTable.flyout.importSuccessful.confirmAllChangesButtonLabel"
id="savedObjectsManagement.objectsTable.flyout.importSuccessful.confirmAllChangesButtonLabel"
values={Object {}}
/>
</EuiButton>
@ -164,6 +164,30 @@ exports[`Flyout conflicts should allow conflict resolution 2`] = `
Array [
Object {
"getConflictResolutions": [Function],
"http": Object {
"addLoadingCountSource": [MockFunction],
"anonymousPaths": Object {
"isAnonymous": [MockFunction],
"register": [MockFunction],
},
"basePath": BasePath {
"basePath": "",
"get": [Function],
"prepend": [Function],
"remove": [Function],
"serverBasePath": "",
},
"delete": [MockFunction],
"fetch": [MockFunction],
"get": [MockFunction],
"getLoadingCount$": [MockFunction],
"head": [MockFunction],
"intercept": [MockFunction],
"options": [MockFunction],
"patch": [MockFunction],
"post": [MockFunction],
"put": [MockFunction],
},
"state": Object {
"conflictedIndexPatterns": undefined,
"conflictedSavedObjectsLinkedToSavedSearches": undefined,
@ -243,7 +267,7 @@ exports[`Flyout conflicts should handle errors 1`] = `
title={
<FormattedMessage
defaultMessage="Import failed"
id="kbn.management.objects.objectsTable.flyout.importFailedTitle"
id="savedObjectsManagement.objectsTable.flyout.importFailedTitle"
values={Object {}}
/>
}
@ -251,7 +275,7 @@ exports[`Flyout conflicts should handle errors 1`] = `
<p>
<FormattedMessage
defaultMessage="Failed to import {failedImportCount} of {totalImportCount} objects. Import failed"
id="kbn.management.objects.objectsTable.flyout.importFailedDescription"
id="savedObjectsManagement.objectsTable.flyout.importFailedDescription"
values={
Object {
"failedImportCount": 1,
@ -272,7 +296,7 @@ exports[`Flyout errors should display unsupported type errors properly 1`] = `
title={
<FormattedMessage
defaultMessage="Import failed"
id="kbn.management.objects.objectsTable.flyout.importFailedTitle"
id="savedObjectsManagement.objectsTable.flyout.importFailedTitle"
values={Object {}}
/>
}
@ -280,7 +304,7 @@ exports[`Flyout errors should display unsupported type errors properly 1`] = `
<p>
<FormattedMessage
defaultMessage="Failed to import {failedImportCount} of {totalImportCount} objects. Import failed"
id="kbn.management.objects.objectsTable.flyout.importFailedDescription"
id="savedObjectsManagement.objectsTable.flyout.importFailedDescription"
values={
Object {
"failedImportCount": 1,
@ -313,7 +337,7 @@ exports[`Flyout legacy conflicts should allow conflict resolution 1`] = `
<h2>
<FormattedMessage
defaultMessage="Import saved objects"
id="kbn.management.objects.objectsTable.flyout.importSavedObjectTitle"
id="savedObjectsManagement.objectsTable.flyout.importSavedObjectTitle"
values={Object {}}
/>
</h2>
@ -331,7 +355,7 @@ exports[`Flyout legacy conflicts should allow conflict resolution 1`] = `
title={
<FormattedMessage
defaultMessage="Support for JSON files is going away"
id="kbn.management.objects.objectsTable.flyout.legacyFileUsedTitle"
id="savedObjectsManagement.objectsTable.flyout.legacyFileUsedTitle"
values={Object {}}
/>
}
@ -339,7 +363,7 @@ exports[`Flyout legacy conflicts should allow conflict resolution 1`] = `
<p>
<FormattedMessage
defaultMessage="Use our updated export to generate NDJSON files, and you'll be all set."
id="kbn.management.objects.objectsTable.flyout.legacyFileUsedBody"
id="savedObjectsManagement.objectsTable.flyout.legacyFileUsedBody"
values={Object {}}
/>
</p>
@ -356,7 +380,7 @@ exports[`Flyout legacy conflicts should allow conflict resolution 1`] = `
title={
<FormattedMessage
defaultMessage="Index Pattern Conflicts"
id="kbn.management.objects.objectsTable.flyout.indexPatternConflictsTitle"
id="savedObjectsManagement.objectsTable.flyout.indexPatternConflictsTitle"
values={Object {}}
/>
}
@ -364,7 +388,7 @@ exports[`Flyout legacy conflicts should allow conflict resolution 1`] = `
<p>
<FormattedMessage
defaultMessage="The following saved objects use index patterns that do not exist. Please select the index patterns you'd like re-associated with them. You can {indexPatternLink} if necessary."
id="kbn.management.objects.objectsTable.flyout.indexPatternConflictsDescription"
id="savedObjectsManagement.objectsTable.flyout.indexPatternConflictsDescription"
values={
Object {
"indexPatternLink": <ForwardRef
@ -372,7 +396,7 @@ exports[`Flyout legacy conflicts should allow conflict resolution 1`] = `
>
<FormattedMessage
defaultMessage="create a new index pattern"
id="kbn.management.objects.objectsTable.flyout.indexPatternConflictsCalloutLinkText"
id="savedObjectsManagement.objectsTable.flyout.indexPatternConflictsCalloutLinkText"
values={Object {}}
/>
</ForwardRef>,
@ -462,7 +486,7 @@ exports[`Flyout legacy conflicts should allow conflict resolution 1`] = `
>
<FormattedMessage
defaultMessage="Cancel"
id="kbn.management.objects.objectsTable.flyout.import.cancelButtonLabel"
id="savedObjectsManagement.objectsTable.flyout.import.cancelButtonLabel"
values={Object {}}
/>
</EuiButtonEmpty>
@ -479,7 +503,7 @@ exports[`Flyout legacy conflicts should allow conflict resolution 1`] = `
>
<FormattedMessage
defaultMessage="Confirm all changes"
id="kbn.management.objects.objectsTable.flyout.importSuccessful.confirmAllChangesButtonLabel"
id="savedObjectsManagement.objectsTable.flyout.importSuccessful.confirmAllChangesButtonLabel"
values={Object {}}
/>
</EuiButton>
@ -498,7 +522,7 @@ Array [
title={
<FormattedMessage
defaultMessage="Support for JSON files is going away"
id="kbn.management.objects.objectsTable.flyout.legacyFileUsedTitle"
id="savedObjectsManagement.objectsTable.flyout.legacyFileUsedTitle"
values={Object {}}
/>
}
@ -506,7 +530,7 @@ Array [
<p>
<FormattedMessage
defaultMessage="Use our updated export to generate NDJSON files, and you'll be all set."
id="kbn.management.objects.objectsTable.flyout.legacyFileUsedBody"
id="savedObjectsManagement.objectsTable.flyout.legacyFileUsedBody"
values={Object {}}
/>
</p>
@ -518,7 +542,7 @@ Array [
title={
<FormattedMessage
defaultMessage="Index Pattern Conflicts"
id="kbn.management.objects.objectsTable.flyout.indexPatternConflictsTitle"
id="savedObjectsManagement.objectsTable.flyout.indexPatternConflictsTitle"
values={Object {}}
/>
}
@ -526,7 +550,7 @@ Array [
<p>
<FormattedMessage
defaultMessage="The following saved objects use index patterns that do not exist. Please select the index patterns you'd like re-associated with them. You can {indexPatternLink} if necessary."
id="kbn.management.objects.objectsTable.flyout.indexPatternConflictsDescription"
id="savedObjectsManagement.objectsTable.flyout.indexPatternConflictsDescription"
values={
Object {
"indexPatternLink": <ForwardRef
@ -534,7 +558,7 @@ Array [
>
<FormattedMessage
defaultMessage="create a new index pattern"
id="kbn.management.objects.objectsTable.flyout.indexPatternConflictsCalloutLinkText"
id="savedObjectsManagement.objectsTable.flyout.indexPatternConflictsCalloutLinkText"
values={Object {}}
/>
</ForwardRef>,
@ -548,7 +572,7 @@ Array [
title={
<FormattedMessage
defaultMessage="Sorry, there was an error"
id="kbn.management.objects.objectsTable.flyout.errorCalloutTitle"
id="savedObjectsManagement.objectsTable.flyout.errorCalloutTitle"
values={Object {}}
/>
}
@ -578,7 +602,7 @@ exports[`Flyout should render import step 1`] = `
<h2>
<FormattedMessage
defaultMessage="Import saved objects"
id="kbn.management.objects.objectsTable.flyout.importSavedObjectTitle"
id="savedObjectsManagement.objectsTable.flyout.importSavedObjectTitle"
values={Object {}}
/>
</h2>
@ -595,7 +619,7 @@ exports[`Flyout should render import step 1`] = `
label={
<FormattedMessage
defaultMessage="Please select a file to import"
id="kbn.management.objects.objectsTable.flyout.selectFileToImportFormRowLabel"
id="savedObjectsManagement.objectsTable.flyout.selectFileToImportFormRowLabel"
values={Object {}}
/>
}
@ -607,7 +631,7 @@ exports[`Flyout should render import step 1`] = `
initialPromptText={
<FormattedMessage
defaultMessage="Import"
id="kbn.management.objects.objectsTable.flyout.importPromptText"
id="savedObjectsManagement.objectsTable.flyout.importPromptText"
values={Object {}}
/>
}
@ -628,7 +652,7 @@ exports[`Flyout should render import step 1`] = `
label={
<FormattedMessage
defaultMessage="Automatically overwrite all saved objects?"
id="kbn.management.objects.objectsTable.flyout.overwriteSavedObjectsLabel"
id="savedObjectsManagement.objectsTable.flyout.overwriteSavedObjectsLabel"
values={Object {}}
/>
}
@ -651,7 +675,7 @@ exports[`Flyout should render import step 1`] = `
>
<FormattedMessage
defaultMessage="Cancel"
id="kbn.management.objects.objectsTable.flyout.import.cancelButtonLabel"
id="savedObjectsManagement.objectsTable.flyout.import.cancelButtonLabel"
values={Object {}}
/>
</EuiButtonEmpty>
@ -668,7 +692,7 @@ exports[`Flyout should render import step 1`] = `
>
<FormattedMessage
defaultMessage="Import"
id="kbn.management.objects.objectsTable.flyout.import.confirmButtonLabel"
id="savedObjectsManagement.objectsTable.flyout.import.confirmButtonLabel"
values={Object {}}
/>
</EuiButton>

View file

@ -13,7 +13,7 @@ exports[`Header should render normally 1`] = `
<h1>
<FormattedMessage
defaultMessage="Saved Objects"
id="kbn.management.objects.objectsTable.header.savedObjectsTitle"
id="savedObjectsManagement.objectsTable.header.savedObjectsTitle"
values={Object {}}
/>
</h1>
@ -38,7 +38,7 @@ exports[`Header should render normally 1`] = `
>
<FormattedMessage
defaultMessage="Export {filteredCount, plural, one{# object} other {# objects}}"
id="kbn.management.objects.objectsTable.header.exportButtonLabel"
id="savedObjectsManagement.objectsTable.header.exportButtonLabel"
values={
Object {
"filteredCount": 2,
@ -58,7 +58,7 @@ exports[`Header should render normally 1`] = `
>
<FormattedMessage
defaultMessage="Import"
id="kbn.management.objects.objectsTable.header.importButtonLabel"
id="savedObjectsManagement.objectsTable.header.importButtonLabel"
values={Object {}}
/>
</EuiButtonEmpty>
@ -73,7 +73,7 @@ exports[`Header should render normally 1`] = `
>
<FormattedMessage
defaultMessage="Refresh"
id="kbn.management.objects.objectsTable.header.refreshButtonLabel"
id="savedObjectsManagement.objectsTable.header.refreshButtonLabel"
values={Object {}}
/>
</EuiButtonEmpty>
@ -93,7 +93,7 @@ exports[`Header should render normally 1`] = `
>
<FormattedMessage
defaultMessage="From here you can delete saved objects, such as saved searches. You can also edit the raw data of saved objects. Typically objects are only modified via their associated application, which is probably what you should use instead of this screen."
id="kbn.management.objects.objectsTable.howToDeleteSavedObjectsDescription"
id="savedObjectsManagement.objectsTable.howToDeleteSavedObjectsDescription"
values={Object {}}
/>
</EuiTextColor>

View file

@ -202,7 +202,7 @@ exports[`Relationships should render errors 1`] = `
title={
<FormattedMessage
defaultMessage="Error"
id="kbn.management.objects.objectsTable.relationships.renderErrorMessage"
id="savedObjectsManagement.objectsTable.relationships.renderErrorMessage"
values={Object {}}
/>
}

View file

@ -36,7 +36,7 @@ exports[`Table prevents saved objects from being deleted 1`] = `
>
<FormattedMessage
defaultMessage="Delete"
id="kbn.management.objects.objectsTable.table.deleteButtonLabel"
id="savedObjectsManagement.objectsTable.table.deleteButtonLabel"
values={Object {}}
/>
</EuiButton>,
@ -51,7 +51,7 @@ exports[`Table prevents saved objects from being deleted 1`] = `
>
<FormattedMessage
defaultMessage="Export"
id="kbn.management.objects.objectsTable.table.exportPopoverButtonLabel"
id="savedObjectsManagement.objectsTable.table.exportPopoverButtonLabel"
values={Object {}}
/>
</EuiButton>
@ -72,7 +72,7 @@ exports[`Table prevents saved objects from being deleted 1`] = `
label={
<FormattedMessage
defaultMessage="Options"
id="kbn.management.objects.objectsTable.exportObjectsConfirmModal.exportOptionsLabel"
id="savedObjectsManagement.objectsTable.exportObjectsConfirmModal.exportOptionsLabel"
values={Object {}}
/>
}
@ -83,7 +83,7 @@ exports[`Table prevents saved objects from being deleted 1`] = `
label={
<FormattedMessage
defaultMessage="Include related objects"
id="kbn.management.objects.objectsTable.exportObjectsConfirmModal.includeReferencesDeepLabel"
id="savedObjectsManagement.objectsTable.exportObjectsConfirmModal.includeReferencesDeepLabel"
values={Object {}}
/>
}
@ -106,7 +106,7 @@ exports[`Table prevents saved objects from being deleted 1`] = `
>
<FormattedMessage
defaultMessage="Export"
id="kbn.management.objects.objectsTable.table.exportButtonLabel"
id="savedObjectsManagement.objectsTable.table.exportButtonLabel"
values={Object {}}
/>
</EuiButton>
@ -171,6 +171,7 @@ exports[`Table prevents saved objects from being deleted 1`] = `
items={
Array [
Object {
"attributes": Object {},
"id": "1",
"meta": Object {
"editUrl": "#/management/kibana/index_patterns/1",
@ -181,6 +182,7 @@ exports[`Table prevents saved objects from being deleted 1`] = `
},
"title": "MyIndexPattern*",
},
"references": Array [],
"type": "index-pattern",
},
]
@ -249,7 +251,7 @@ exports[`Table should render normally 1`] = `
>
<FormattedMessage
defaultMessage="Delete"
id="kbn.management.objects.objectsTable.table.deleteButtonLabel"
id="savedObjectsManagement.objectsTable.table.deleteButtonLabel"
values={Object {}}
/>
</EuiButton>,
@ -264,7 +266,7 @@ exports[`Table should render normally 1`] = `
>
<FormattedMessage
defaultMessage="Export"
id="kbn.management.objects.objectsTable.table.exportPopoverButtonLabel"
id="savedObjectsManagement.objectsTable.table.exportPopoverButtonLabel"
values={Object {}}
/>
</EuiButton>
@ -285,7 +287,7 @@ exports[`Table should render normally 1`] = `
label={
<FormattedMessage
defaultMessage="Options"
id="kbn.management.objects.objectsTable.exportObjectsConfirmModal.exportOptionsLabel"
id="savedObjectsManagement.objectsTable.exportObjectsConfirmModal.exportOptionsLabel"
values={Object {}}
/>
}
@ -296,7 +298,7 @@ exports[`Table should render normally 1`] = `
label={
<FormattedMessage
defaultMessage="Include related objects"
id="kbn.management.objects.objectsTable.exportObjectsConfirmModal.includeReferencesDeepLabel"
id="savedObjectsManagement.objectsTable.exportObjectsConfirmModal.includeReferencesDeepLabel"
values={Object {}}
/>
}
@ -319,7 +321,7 @@ exports[`Table should render normally 1`] = `
>
<FormattedMessage
defaultMessage="Export"
id="kbn.management.objects.objectsTable.table.exportButtonLabel"
id="savedObjectsManagement.objectsTable.table.exportButtonLabel"
values={Object {}}
/>
</EuiButton>
@ -384,6 +386,7 @@ exports[`Table should render normally 1`] = `
items={
Array [
Object {
"attributes": Object {},
"id": "1",
"meta": Object {
"editUrl": "#/management/kibana/index_patterns/1",
@ -394,6 +397,7 @@ exports[`Table should render normally 1`] = `
},
"title": "MyIndexPattern*",
},
"references": Array [],
"type": "index-pattern",
},
]

View file

@ -0,0 +1,44 @@
/*
* 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 const importFileMock = jest.fn();
jest.doMock('../../../lib/import_file', () => ({
importFile: importFileMock,
}));
export const resolveImportErrorsMock = jest.fn();
jest.doMock('../../../lib/resolve_import_errors', () => ({
resolveImportErrors: resolveImportErrorsMock,
}));
export const importLegacyFileMock = jest.fn();
jest.doMock('../../../lib/import_legacy_file', () => ({
importLegacyFile: importLegacyFileMock,
}));
export const resolveSavedObjectsMock = jest.fn();
export const resolveSavedSearchesMock = jest.fn();
export const resolveIndexPatternConflictsMock = jest.fn();
export const saveObjectsMock = jest.fn();
jest.doMock('../../../lib/resolve_saved_objects', () => ({
resolveSavedObjects: resolveSavedObjectsMock,
resolveSavedSearches: resolveSavedSearchesMock,
resolveIndexPatternConflicts: resolveIndexPatternConflictsMock,
saveObjects: saveObjectsMock,
}));

View file

@ -17,68 +17,62 @@
* under the License.
*/
import {
importFileMock,
importLegacyFileMock,
resolveImportErrorsMock,
resolveIndexPatternConflictsMock,
resolveSavedObjectsMock,
resolveSavedSearchesMock,
saveObjectsMock,
} from './flyout.test.mocks';
import React from 'react';
import { shallowWithI18nProvider } from 'test_utils/enzyme_helpers';
import { mockManagementPlugin } from '../../../../../../../../../../../../plugins/index_pattern_management/public/mocks';
import { Flyout } from '../flyout';
import { coreMock } from '../../../../../../core/public/mocks';
import { serviceRegistryMock } from '../../../services/service_registry.mock';
import { Flyout, FlyoutProps, FlyoutState } from './flyout';
import { ShallowWrapper } from 'enzyme';
jest.mock('ui/kfetch', () => ({ kfetch: jest.fn() }));
jest.mock('../../../../../lib/import_file', () => ({
importFile: jest.fn(),
}));
jest.mock('../../../../../lib/resolve_import_errors', () => ({
resolveImportErrors: jest.fn(),
}));
jest.mock('ui/chrome', () => ({
addBasePath: () => {},
getInjected: () => ['index-pattern', 'visualization', 'dashboard', 'search'],
}));
jest.mock('../../../../../lib/import_legacy_file', () => ({
importLegacyFile: jest.fn(),
}));
jest.mock('../../../../../lib/resolve_saved_objects', () => ({
resolveSavedObjects: jest.fn(),
resolveSavedSearches: jest.fn(),
resolveIndexPatternConflicts: jest.fn(),
saveObjects: jest.fn(),
}));
jest.mock('../../../../../../../../../../../../plugins/index_pattern_management/public', () => ({
setup: mockManagementPlugin.createSetupContract(),
start: mockManagementPlugin.createStartContract(),
}));
jest.mock('ui/notify', () => ({}));
const defaultProps = {
close: jest.fn(),
done: jest.fn(),
services: [],
newIndexPatternUrl: '',
getConflictResolutions: jest.fn(),
confirmModalPromise: jest.fn(),
indexPatterns: {
getFields: jest.fn().mockImplementation(() => [{ id: '1' }, { id: '2' }]),
},
};
const mockFile = {
const mockFile = ({
name: 'foo.ndjson',
path: '/home/foo.ndjson',
};
const legacyMockFile = {
} as unknown) as File;
const legacyMockFile = ({
name: 'foo.json',
path: '/home/foo.json',
};
} as unknown) as File;
describe('Flyout', () => {
let defaultProps: FlyoutProps;
const shallowRender = (props: FlyoutProps) => {
return (shallowWithI18nProvider(<Flyout {...props} />) as unknown) as ShallowWrapper<
FlyoutProps,
FlyoutState,
Flyout
>;
};
beforeEach(() => {
const { http, overlays } = coreMock.createStart();
defaultProps = {
close: jest.fn(),
done: jest.fn(),
newIndexPatternUrl: '',
indexPatterns: {
getFields: jest.fn().mockImplementation(() => [{ id: '1' }, { id: '2' }]),
} as any,
overlays,
http,
allowedTypes: ['search', 'index-pattern', 'visualization'],
serviceRegistry: serviceRegistryMock.create(),
};
});
it('should render import step', async () => {
const component = shallowWithI18nProvider(<Flyout {...defaultProps} />);
const component = shallowRender(defaultProps);
// Ensure all promises resolve
await new Promise(resolve => process.nextTick(resolve));
@ -89,7 +83,7 @@ describe('Flyout', () => {
});
it('should toggle the overwrite all control', async () => {
const component = shallowWithI18nProvider(<Flyout {...defaultProps} />);
const component = shallowRender(defaultProps);
// Ensure all promises resolve
await new Promise(resolve => process.nextTick(resolve));
@ -102,7 +96,7 @@ describe('Flyout', () => {
});
it('should allow picking a file', async () => {
const component = shallowWithI18nProvider(<Flyout {...defaultProps} />);
const component = shallowRender(defaultProps);
// Ensure all promises resolve
await new Promise(resolve => process.nextTick(resolve));
@ -115,7 +109,7 @@ describe('Flyout', () => {
});
it('should allow removing a file', async () => {
const component = shallowWithI18nProvider(<Flyout {...defaultProps} />);
const component = shallowRender(defaultProps);
// Ensure all promises resolve
await Promise.resolve();
@ -130,22 +124,21 @@ describe('Flyout', () => {
});
it('should handle invalid files', async () => {
const { importLegacyFile } = require('../../../../../lib/import_legacy_file');
const component = shallowWithI18nProvider(<Flyout {...defaultProps} />);
const component = shallowRender(defaultProps);
// Ensure all promises resolve
await new Promise(resolve => process.nextTick(resolve));
// Ensure the state changes are reflected
component.update();
importLegacyFile.mockImplementation(() => {
importLegacyFileMock.mockImplementation(() => {
throw new Error('foobar');
});
await component.instance().legacyImport();
expect(component.state('error')).toBe('The file could not be processed.');
importLegacyFile.mockImplementation(() => ({
importLegacyFileMock.mockImplementation(() => ({
invalid: true,
}));
@ -156,11 +149,8 @@ describe('Flyout', () => {
});
describe('conflicts', () => {
const { importFile } = require('../../../../../lib/import_file');
const { resolveImportErrors } = require('../../../../../lib/resolve_import_errors');
beforeEach(() => {
importFile.mockImplementation(() => ({
importFileMock.mockImplementation(() => ({
success: false,
successCount: 0,
errors: [
@ -180,7 +170,7 @@ describe('Flyout', () => {
},
],
}));
resolveImportErrors.mockImplementation(() => ({
resolveImportErrorsMock.mockImplementation(() => ({
status: 'success',
importCount: 1,
failedImports: [],
@ -188,7 +178,7 @@ describe('Flyout', () => {
});
it('should figure out unmatchedReferences', async () => {
const component = shallowWithI18nProvider(<Flyout {...defaultProps} />);
const component = shallowRender(defaultProps);
// Ensure all promises resolve
await new Promise(resolve => process.nextTick(resolve));
@ -198,7 +188,7 @@ describe('Flyout', () => {
component.setState({ file: mockFile, isLegacyFile: false });
await component.instance().import();
expect(importFile).toHaveBeenCalledWith(mockFile, true);
expect(importFileMock).toHaveBeenCalledWith(defaultProps.http, mockFile, true);
expect(component.state()).toMatchObject({
conflictedIndexPatterns: undefined,
conflictedSavedObjectsLinkedToSavedSearches: undefined,
@ -223,7 +213,7 @@ describe('Flyout', () => {
});
it('should allow conflict resolution', async () => {
const component = shallowWithI18nProvider(<Flyout {...defaultProps} />);
const component = shallowRender(defaultProps);
// Ensure all promises resolve
await new Promise(resolve => process.nextTick(resolve));
@ -239,7 +229,7 @@ describe('Flyout', () => {
// Ensure we can change the resolution
component.instance().onIndexChanged('MyIndexPattern*', { target: { value: '2' } });
expect(component.state('unmatchedReferences')[0].newIndexPatternId).toBe('2');
expect(component.state('unmatchedReferences')![0].newIndexPatternId).toBe('2');
// Let's resolve now
await component
@ -247,18 +237,18 @@ describe('Flyout', () => {
.simulate('click');
// Ensure all promises resolve
await new Promise(resolve => process.nextTick(resolve));
expect(resolveImportErrors).toMatchSnapshot();
expect(resolveImportErrorsMock).toMatchSnapshot();
});
it('should handle errors', async () => {
const component = shallowWithI18nProvider(<Flyout {...defaultProps} />);
const component = shallowRender(defaultProps);
// Ensure all promises resolve
await new Promise(resolve => process.nextTick(resolve));
// Ensure the state changes are reflected
component.update();
resolveImportErrors.mockImplementation(() => ({
resolveImportErrorsMock.mockImplementation(() => ({
status: 'success',
importCount: 0,
failedImports: [
@ -303,18 +293,15 @@ 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 = shallowWithI18nProvider(<Flyout {...defaultProps} />);
const component = shallowRender(defaultProps);
// Ensure all promises resolve
await Promise.resolve();
// Ensure the state changes are reflected
component.update();
importFile.mockImplementation(() => ({
importFileMock.mockImplementation(() => ({
success: false,
successCount: 0,
errors: [
@ -328,7 +315,7 @@ describe('Flyout', () => {
},
],
}));
resolveImportErrors.mockImplementation(() => ({
resolveImportErrorsMock.mockImplementation(() => ({
status: 'success',
importCount: 0,
failedImports: [
@ -372,14 +359,6 @@ describe('Flyout', () => {
});
describe('legacy conflicts', () => {
const { importLegacyFile } = require('../../../../../lib/import_legacy_file');
const {
resolveSavedObjects,
resolveSavedSearches,
resolveIndexPatternConflicts,
saveObjects,
} = require('../../../../../lib/resolve_saved_objects');
const mockData = [
{
_id: '1',
@ -406,7 +385,7 @@ describe('Flyout', () => {
},
obj: {
searchSource: {
getOwnField: field => {
getOwnField: (field: string) => {
if (field === 'index') {
return 'MyIndexPattern*';
}
@ -426,8 +405,8 @@ describe('Flyout', () => {
const mockConflictedSearchDocs = [3];
beforeEach(() => {
importLegacyFile.mockImplementation(() => mockData);
resolveSavedObjects.mockImplementation(() => ({
importLegacyFileMock.mockImplementation(() => mockData);
resolveSavedObjectsMock.mockImplementation(() => ({
conflictedIndexPatterns: mockConflictedIndexPatterns,
conflictedSavedObjectsLinkedToSavedSearches: mockConflictedSavedObjectsLinkedToSavedSearches,
conflictedSearchDocs: mockConflictedSearchDocs,
@ -437,7 +416,7 @@ describe('Flyout', () => {
});
it('should figure out unmatchedReferences', async () => {
const component = shallowWithI18nProvider(<Flyout {...defaultProps} />);
const component = shallowRender(defaultProps);
// Ensure all promises resolve
await new Promise(resolve => process.nextTick(resolve));
@ -447,14 +426,14 @@ describe('Flyout', () => {
component.setState({ file: legacyMockFile, isLegacyFile: true });
await component.instance().legacyImport();
expect(importLegacyFile).toHaveBeenCalledWith(legacyMockFile);
expect(importLegacyFileMock).toHaveBeenCalledWith(legacyMockFile);
// Remove the last element from data since it should be filtered out
expect(resolveSavedObjects).toHaveBeenCalledWith(
expect(resolveSavedObjectsMock).toHaveBeenCalledWith(
mockData.slice(0, 2).map(doc => ({ ...doc, _migrationVersion: {} })),
true,
defaultProps.services,
defaultProps.serviceRegistry.all().map(s => s.service),
defaultProps.indexPatterns,
defaultProps.confirmModalPromise
defaultProps.overlays.openConfirm
);
expect(component.state()).toMatchObject({
@ -492,7 +471,7 @@ describe('Flyout', () => {
});
it('should allow conflict resolution', async () => {
const component = shallowWithI18nProvider(<Flyout {...defaultProps} />);
const component = shallowRender(defaultProps);
// Ensure all promises resolve
await new Promise(resolve => process.nextTick(resolve));
@ -508,7 +487,7 @@ describe('Flyout', () => {
// Ensure we can change the resolution
component.instance().onIndexChanged('MyIndexPattern*', { target: { value: '2' } });
expect(component.state('unmatchedReferences')[0].newIndexPatternId).toBe('2');
expect(component.state('unmatchedReferences')![0].newIndexPatternId).toBe('2');
// Let's resolve now
await component
@ -516,33 +495,33 @@ describe('Flyout', () => {
.simulate('click');
// Ensure all promises resolve
await new Promise(resolve => process.nextTick(resolve));
expect(resolveIndexPatternConflicts).toHaveBeenCalledWith(
expect(resolveIndexPatternConflictsMock).toHaveBeenCalledWith(
component.instance().resolutions,
mockConflictedIndexPatterns,
true,
defaultProps.indexPatterns
);
expect(saveObjects).toHaveBeenCalledWith(
expect(saveObjectsMock).toHaveBeenCalledWith(
mockConflictedSavedObjectsLinkedToSavedSearches,
true
);
expect(resolveSavedSearches).toHaveBeenCalledWith(
expect(resolveSavedSearchesMock).toHaveBeenCalledWith(
mockConflictedSearchDocs,
defaultProps.services,
defaultProps.serviceRegistry.all().map(s => s.service),
defaultProps.indexPatterns,
true
);
});
it('should handle errors', async () => {
const component = shallowWithI18nProvider(<Flyout {...defaultProps} />);
const component = shallowRender(defaultProps);
// Ensure all promises resolve
await new Promise(resolve => process.nextTick(resolve));
// Ensure the state changes are reflected
component.update();
resolveIndexPatternConflicts.mockImplementation(() => {
resolveIndexPatternConflictsMock.mockImplementation(() => {
throw new Error('foobar');
});

View file

@ -18,7 +18,6 @@
*/
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import { take, get as getField } from 'lodash';
import {
EuiFlyout,
@ -32,6 +31,7 @@ import {
EuiForm,
EuiFormRow,
EuiSwitch,
// @ts-ignore
EuiFilePicker,
EuiInMemoryTable,
EuiSelect,
@ -47,34 +47,62 @@ import {
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { OverlayStart, HttpStart } from 'src/core/public';
import { IndexPatternsContract, IIndexPattern } from '../../../../../data/public';
import {
importFile,
importLegacyFile,
resolveImportErrors,
logLegacyImport,
getDefaultTitle,
} from '../../../../lib';
import { processImportResponse } from '../../../../lib/process_import_response';
processImportResponse,
ProcessedImportResponse,
} from '../../../lib';
import {
resolveSavedObjects,
resolveSavedSearches,
resolveIndexPatternConflicts,
saveObjects,
} from '../../../../lib/resolve_saved_objects';
import { POSSIBLE_TYPES } from '../../objects_table';
} from '../../../lib/resolve_saved_objects';
import { ISavedObjectsManagementServiceRegistry } from '../../../services';
export class Flyout extends Component {
static propTypes = {
close: PropTypes.func.isRequired,
done: PropTypes.func.isRequired,
services: PropTypes.array.isRequired,
newIndexPatternUrl: PropTypes.string.isRequired,
indexPatterns: PropTypes.object.isRequired,
confirmModalPromise: PropTypes.func.isRequired,
};
export interface FlyoutProps {
serviceRegistry: ISavedObjectsManagementServiceRegistry;
allowedTypes: string[];
close: () => void;
done: () => void;
newIndexPatternUrl: string;
indexPatterns: IndexPatternsContract;
overlays: OverlayStart;
http: HttpStart;
}
constructor(props) {
export interface FlyoutState {
conflictedIndexPatterns?: any[];
conflictedSavedObjectsLinkedToSavedSearches?: any[];
conflictedSearchDocs?: any[];
unmatchedReferences?: ProcessedImportResponse['unmatchedReferences'];
failedImports?: ProcessedImportResponse['failedImports'];
conflictingRecord?: ConflictingRecord;
error?: string;
file?: File;
importCount: number;
indexPatterns?: IIndexPattern[];
isOverwriteAllChecked: boolean;
loadingMessage?: string;
isLegacyFile: boolean;
status: string;
}
interface ConflictingRecord {
id: string;
type: string;
title: string;
done: (success: boolean) => void;
}
export class Flyout extends Component<FlyoutProps, FlyoutState> {
constructor(props: FlyoutProps) {
super(props);
this.state = {
@ -100,7 +128,7 @@ export class Flyout extends Component {
fetchIndexPatterns = async () => {
const indexPatterns = await this.props.indexPatterns.getFields(['id', 'title']);
this.setState({ indexPatterns });
this.setState({ indexPatterns } as any);
};
changeOverwriteAll = () => {
@ -109,11 +137,12 @@ export class Flyout extends Component {
}));
};
setImportFile = ([file]) => {
if (!file) {
setImportFile = (files: FileList | null) => {
if (!files || !files[0]) {
this.setState({ file: undefined, isLegacyFile: false });
return;
}
const file = files[0];
this.setState({
file,
isLegacyFile: /\.json$/i.test(file.name) || file.type === 'application/json',
@ -126,30 +155,29 @@ export class Flyout extends Component {
* Does the initial import of a file, resolveImportErrors then handles errors and retries
*/
import = async () => {
const { http } = this.props;
const { file, isOverwriteAllChecked } = this.state;
this.setState({ status: 'loading', error: undefined });
// Import the file
let response;
try {
response = await importFile(file, isOverwriteAllChecked);
const response = await importFile(http, file!, isOverwriteAllChecked);
this.setState(processImportResponse(response), () => {
// Resolve import errors right away if there's no index patterns to match
// This will ask about overwriting each object, etc
if (this.state.unmatchedReferences?.length === 0) {
this.resolveImportErrors();
}
});
} catch (e) {
this.setState({
status: 'error',
error: i18n.translate('kbn.management.objects.objectsTable.flyout.importFileErrorMessage', {
error: i18n.translate('savedObjectsManagement.objectsTable.flyout.importFileErrorMessage', {
defaultMessage: 'The file could not be processed.',
}),
});
return;
}
this.setState(processImportResponse(response), () => {
// Resolve import errors right away if there's no index patterns to match
// This will ask about overwriting each object, etc
if (this.state.unmatchedReferences.length === 0) {
this.resolveImportErrors();
}
});
};
/**
@ -160,10 +188,10 @@ export class Flyout extends Component {
* @param {array} objects List of objects to request the user if they wish to overwrite it
* @return {Promise<array>} An object with the key being "type:id" and value the resolution chosen by the user
*/
getConflictResolutions = async objects => {
const resolutions = {};
getConflictResolutions = async (objects: any[]) => {
const resolutions: Record<string, boolean> = {};
for (const { type, id, title } of objects) {
const overwrite = await new Promise(resolve => {
const overwrite = await new Promise<boolean>(resolve => {
this.setState({
conflictingRecord: {
id,
@ -193,6 +221,7 @@ export class Flyout extends Component {
try {
const updatedState = await resolveImportErrors({
http: this.props.http,
state: this.state,
getConflictResolutions: this.getConflictResolutions,
});
@ -201,7 +230,7 @@ export class Flyout extends Component {
this.setState({
status: 'error',
error: i18n.translate(
'kbn.management.objects.objectsTable.flyout.resolveImportErrorsFileErrorMessage',
'savedObjectsManagement.objectsTable.flyout.resolveImportErrorsFileErrorMessage',
{ defaultMessage: 'The file could not be processed.' }
),
});
@ -209,22 +238,22 @@ export class Flyout extends Component {
};
legacyImport = async () => {
const { services, indexPatterns, confirmModalPromise } = this.props;
const { serviceRegistry, indexPatterns, overlays, http, allowedTypes } = this.props;
const { file, isOverwriteAllChecked } = this.state;
this.setState({ status: 'loading', error: undefined });
// Log warning on server, don't wait for response
logLegacyImport();
logLegacyImport(http);
let contents;
try {
contents = await importLegacyFile(file);
contents = await importLegacyFile(file!);
} catch (e) {
this.setState({
status: 'error',
error: i18n.translate(
'kbn.management.objects.objectsTable.flyout.importLegacyFileErrorMessage',
'savedObjectsManagement.objectsTable.flyout.importLegacyFileErrorMessage',
{ defaultMessage: 'The file could not be processed.' }
),
});
@ -235,7 +264,7 @@ export class Flyout extends Component {
this.setState({
status: 'error',
error: i18n.translate(
'kbn.management.objects.objectsTable.flyout.invalidFormatOfImportedFileErrorMessage',
'savedObjectsManagement.objectsTable.flyout.invalidFormatOfImportedFileErrorMessage',
{ defaultMessage: 'Saved objects file format is invalid and cannot be imported.' }
),
});
@ -243,7 +272,7 @@ export class Flyout extends Component {
}
contents = contents
.filter(content => POSSIBLE_TYPES.includes(content._type))
.filter(content => allowedTypes.includes(content._type))
.map(doc => ({
...doc,
// The server assumes that documents with no migrationVersion are up to date.
@ -263,18 +292,18 @@ export class Flyout extends Component {
} = await resolveSavedObjects(
contents,
isOverwriteAllChecked,
services,
serviceRegistry.all().map(e => e.service),
indexPatterns,
confirmModalPromise
overlays.openConfirm
);
const byId = {};
const byId: Record<string, any[]> = {};
conflictedIndexPatterns
.map(({ doc, obj }) => {
return { doc, obj: obj._serialize() };
})
.forEach(({ doc, obj }) =>
obj.references.forEach(ref => {
obj.references.forEach((ref: Record<string, any>) => {
byId[ref.id] = byId[ref.id] != null ? byId[ref.id].concat({ doc, obj }) : [{ doc, obj }];
})
);
@ -291,7 +320,7 @@ export class Flyout extends Component {
});
return accum;
},
[]
[] as any[]
);
this.setState({
@ -305,12 +334,12 @@ export class Flyout extends Component {
});
};
get hasUnmatchedReferences() {
public get hasUnmatchedReferences() {
return this.state.unmatchedReferences && this.state.unmatchedReferences.length > 0;
}
get resolutions() {
return this.state.unmatchedReferences.reduce(
public get resolutions() {
return this.state.unmatchedReferences!.reduce(
(accum, { existingIndexPatternId, newIndexPatternId }) => {
if (newIndexPatternId) {
accum.push({
@ -320,7 +349,7 @@ export class Flyout extends Component {
}
return accum;
},
[]
[] as Array<{ oldId: string; newId: string }>
);
}
@ -333,7 +362,7 @@ export class Flyout extends Component {
failedImports,
} = this.state;
const { services, indexPatterns } = this.props;
const { serviceRegistry, indexPatterns } = this.props;
this.setState({
error: undefined,
@ -350,48 +379,48 @@ export class Flyout extends Component {
// Do not Promise.all these calls as the order matters
this.setState({
loadingMessage: i18n.translate(
'kbn.management.objects.objectsTable.flyout.confirmLegacyImport.resolvingConflictsLoadingMessage',
'savedObjectsManagement.objectsTable.flyout.confirmLegacyImport.resolvingConflictsLoadingMessage',
{ defaultMessage: 'Resolving conflicts…' }
),
});
if (resolutions.length) {
importCount += await resolveIndexPatternConflicts(
resolutions,
conflictedIndexPatterns,
conflictedIndexPatterns!,
isOverwriteAllChecked,
this.props.indexPatterns
indexPatterns
);
}
this.setState({
loadingMessage: i18n.translate(
'kbn.management.objects.objectsTable.flyout.confirmLegacyImport.savingConflictsLoadingMessage',
'savedObjectsManagement.objectsTable.flyout.confirmLegacyImport.savingConflictsLoadingMessage',
{ defaultMessage: 'Saving conflicts…' }
),
});
importCount += await saveObjects(
conflictedSavedObjectsLinkedToSavedSearches,
conflictedSavedObjectsLinkedToSavedSearches!,
isOverwriteAllChecked
);
this.setState({
loadingMessage: i18n.translate(
'kbn.management.objects.objectsTable.flyout.confirmLegacyImport.savedSearchAreLinkedProperlyLoadingMessage',
'savedObjectsManagement.objectsTable.flyout.confirmLegacyImport.savedSearchAreLinkedProperlyLoadingMessage',
{ defaultMessage: 'Ensure saved searches are linked properly…' }
),
});
importCount += await resolveSavedSearches(
conflictedSearchDocs,
services,
conflictedSearchDocs!,
serviceRegistry.all().map(e => e.service),
indexPatterns,
isOverwriteAllChecked
);
this.setState({
loadingMessage: i18n.translate(
'kbn.management.objects.objectsTable.flyout.confirmLegacyImport.retryingFailedObjectsLoadingMessage',
'savedObjectsManagement.objectsTable.flyout.confirmLegacyImport.retryingFailedObjectsLoadingMessage',
{ defaultMessage: 'Retrying failed objects…' }
),
});
importCount += await saveObjects(
failedImports.map(({ obj }) => obj),
failedImports!.map(({ obj }) => obj) as any[],
isOverwriteAllChecked
);
} catch (e) {
@ -407,26 +436,26 @@ export class Flyout extends Component {
this.setState({ status: 'success', importCount });
};
onIndexChanged = (id, e) => {
onIndexChanged = (id: string, e: any) => {
const value = e.target.value;
this.setState(state => {
const conflictIndex = state.unmatchedReferences.findIndex(
const conflictIndex = state.unmatchedReferences?.findIndex(
conflict => conflict.existingIndexPatternId === id
);
if (conflictIndex === -1) {
if (conflictIndex === undefined || conflictIndex === -1) {
return state;
}
return {
unmatchedReferences: [
...state.unmatchedReferences.slice(0, conflictIndex),
...state.unmatchedReferences!.slice(0, conflictIndex),
{
...state.unmatchedReferences[conflictIndex],
...state.unmatchedReferences![conflictIndex],
newIndexPatternId: value,
},
...state.unmatchedReferences.slice(conflictIndex + 1),
...state.unmatchedReferences!.slice(conflictIndex + 1),
],
};
} as any;
});
};
@ -441,11 +470,11 @@ export class Flyout extends Component {
{
field: 'existingIndexPatternId',
name: i18n.translate(
'kbn.management.objects.objectsTable.flyout.renderConflicts.columnIdName',
'savedObjectsManagement.objectsTable.flyout.renderConflicts.columnIdName',
{ defaultMessage: 'ID' }
),
description: i18n.translate(
'kbn.management.objects.objectsTable.flyout.renderConflicts.columnIdDescription',
'savedObjectsManagement.objectsTable.flyout.renderConflicts.columnIdDescription',
{ defaultMessage: 'ID of the index pattern' }
),
sortable: true,
@ -453,28 +482,28 @@ export class Flyout extends Component {
{
field: 'list',
name: i18n.translate(
'kbn.management.objects.objectsTable.flyout.renderConflicts.columnCountName',
'savedObjectsManagement.objectsTable.flyout.renderConflicts.columnCountName',
{ defaultMessage: 'Count' }
),
description: i18n.translate(
'kbn.management.objects.objectsTable.flyout.renderConflicts.columnCountDescription',
'savedObjectsManagement.objectsTable.flyout.renderConflicts.columnCountDescription',
{ defaultMessage: 'How many affected objects' }
),
render: list => {
render: (list: any[]) => {
return <Fragment>{list.length}</Fragment>;
},
},
{
field: 'list',
name: i18n.translate(
'kbn.management.objects.objectsTable.flyout.renderConflicts.columnSampleOfAffectedObjectsName',
'savedObjectsManagement.objectsTable.flyout.renderConflicts.columnSampleOfAffectedObjectsName',
{ defaultMessage: 'Sample of affected objects' }
),
description: i18n.translate(
'kbn.management.objects.objectsTable.flyout.renderConflicts.columnSampleOfAffectedObjectsDescription',
'savedObjectsManagement.objectsTable.flyout.renderConflicts.columnSampleOfAffectedObjectsDescription',
{ defaultMessage: 'Sample of affected objects' }
),
render: list => {
render: (list: any[]) => {
return (
<ul style={{ listStyle: 'none' }}>
{take(list, 3).map((obj, key) => (
@ -487,15 +516,18 @@ export class Flyout extends Component {
{
field: 'existingIndexPatternId',
name: i18n.translate(
'kbn.management.objects.objectsTable.flyout.renderConflicts.columnNewIndexPatternName',
'savedObjectsManagement.objectsTable.flyout.renderConflicts.columnNewIndexPatternName',
{ defaultMessage: 'New index pattern' }
),
render: id => {
const options = this.state.indexPatterns.map(indexPattern => ({
text: indexPattern.title,
value: indexPattern.id,
['data-test-subj']: `indexPatternOption-${indexPattern.title}`,
}));
render: (id: string) => {
const options = this.state.indexPatterns!.map(
indexPattern =>
({
text: indexPattern.title,
value: indexPattern.id,
'data-test-subj': `indexPatternOption-${indexPattern.title}`,
} as { text: string; value: string; 'data-test-subj'?: string })
);
options.unshift({
text: '-- Skip Import --',
@ -518,7 +550,11 @@ export class Flyout extends Component {
};
return (
<EuiInMemoryTable items={unmatchedReferences} columns={columns} pagination={pagination} />
<EuiInMemoryTable
items={unmatchedReferences as any[]}
columns={columns}
pagination={pagination}
/>
);
}
@ -534,7 +570,7 @@ export class Flyout extends Component {
<EuiCallOut
title={
<FormattedMessage
id="kbn.management.objects.objectsTable.flyout.errorCalloutTitle"
id="savedObjectsManagement.objectsTable.flyout.errorCalloutTitle"
defaultMessage="Sorry, there was an error"
/>
}
@ -581,7 +617,7 @@ export class Flyout extends Component {
data-test-subj="importSavedObjectsFailedWarning"
title={
<FormattedMessage
id="kbn.management.objects.objectsTable.flyout.importFailedTitle"
id="savedObjectsManagement.objectsTable.flyout.importFailedTitle"
defaultMessage="Import failed"
/>
}
@ -590,7 +626,7 @@ export class Flyout extends Component {
>
<p>
<FormattedMessage
id="kbn.management.objects.objectsTable.flyout.importFailedDescription"
id="savedObjectsManagement.objectsTable.flyout.importFailedDescription"
defaultMessage="Failed to import {failedImportCount} of {totalImportCount} objects. Import failed"
values={{
failedImportCount: failedImports.length,
@ -604,7 +640,7 @@ export class Flyout extends Component {
if (error.type === 'missing_references') {
return error.references.map(reference => {
return i18n.translate(
'kbn.management.objects.objectsTable.flyout.importFailedMissingReference',
'savedObjectsManagement.objectsTable.flyout.importFailedMissingReference',
{
defaultMessage: '{type} [id={id}] could not locate {refType} [id={refId}]',
values: {
@ -618,7 +654,7 @@ export class Flyout extends Component {
});
} else if (error.type === 'unsupported_type') {
return i18n.translate(
'kbn.management.objects.objectsTable.flyout.importFailedUnsupportedType',
'savedObjectsManagement.objectsTable.flyout.importFailedUnsupportedType',
{
defaultMessage: '{type} [id={id}] unsupported type',
values: {
@ -628,7 +664,7 @@ export class Flyout extends Component {
}
);
}
return getField(error, 'body.message', error.message || '');
return getField(error, 'body.message', (error as any).message ?? '');
})
.join(' ')}
</p>
@ -643,7 +679,7 @@ export class Flyout extends Component {
data-test-subj="importSavedObjectsSuccessNoneImported"
title={
<FormattedMessage
id="kbn.management.objects.objectsTable.flyout.importSuccessfulCallout.noObjectsImportedTitle"
id="savedObjectsManagement.objectsTable.flyout.importSuccessfulCallout.noObjectsImportedTitle"
defaultMessage="No objects imported"
/>
}
@ -657,7 +693,7 @@ export class Flyout extends Component {
data-test-subj="importSavedObjectsSuccess"
title={
<FormattedMessage
id="kbn.management.objects.objectsTable.flyout.importSuccessfulTitle"
id="savedObjectsManagement.objectsTable.flyout.importSuccessfulTitle"
defaultMessage="Import successful"
/>
}
@ -666,7 +702,7 @@ export class Flyout extends Component {
>
<p>
<FormattedMessage
id="kbn.management.objects.objectsTable.flyout.importSuccessfulDescription"
id="savedObjectsManagement.objectsTable.flyout.importSuccessfulDescription"
defaultMessage="Successfully imported {importCount} objects."
values={{ importCount }}
/>
@ -684,7 +720,7 @@ export class Flyout extends Component {
<EuiFormRow
label={
<FormattedMessage
id="kbn.management.objects.objectsTable.flyout.selectFileToImportFormRowLabel"
id="savedObjectsManagement.objectsTable.flyout.selectFileToImportFormRowLabel"
defaultMessage="Please select a file to import"
/>
}
@ -692,7 +728,7 @@ export class Flyout extends Component {
<EuiFilePicker
initialPromptText={
<FormattedMessage
id="kbn.management.objects.objectsTable.flyout.importPromptText"
id="savedObjectsManagement.objectsTable.flyout.importPromptText"
defaultMessage="Import"
/>
}
@ -704,7 +740,7 @@ export class Flyout extends Component {
name="overwriteAll"
label={
<FormattedMessage
id="kbn.management.objects.objectsTable.flyout.overwriteSavedObjectsLabel"
id="savedObjectsManagement.objectsTable.flyout.overwriteSavedObjectsLabel"
defaultMessage="Automatically overwrite all saved objects?"
/>
}
@ -727,7 +763,7 @@ export class Flyout extends Component {
confirmButton = (
<EuiButton onClick={done} size="s" fill data-test-subj="importSavedObjectsDoneBtn">
<FormattedMessage
id="kbn.management.objects.objectsTable.flyout.importSuccessful.confirmButtonLabel"
id="savedObjectsManagement.objectsTable.flyout.importSuccessful.confirmButtonLabel"
defaultMessage="Done"
/>
</EuiButton>
@ -742,7 +778,7 @@ export class Flyout extends Component {
data-test-subj="importSavedObjectsConfirmBtn"
>
<FormattedMessage
id="kbn.management.objects.objectsTable.flyout.importSuccessful.confirmAllChangesButtonLabel"
id="savedObjectsManagement.objectsTable.flyout.importSuccessful.confirmAllChangesButtonLabel"
defaultMessage="Confirm all changes"
/>
</EuiButton>
@ -757,7 +793,7 @@ export class Flyout extends Component {
data-test-subj="importSavedObjectsImportBtn"
>
<FormattedMessage
id="kbn.management.objects.objectsTable.flyout.import.confirmButtonLabel"
id="savedObjectsManagement.objectsTable.flyout.import.confirmButtonLabel"
defaultMessage="Import"
/>
</EuiButton>
@ -769,7 +805,7 @@ export class Flyout extends Component {
<EuiFlexItem grow={false}>
<EuiButtonEmpty onClick={close} size="s">
<FormattedMessage
id="kbn.management.objects.objectsTable.flyout.import.cancelButtonLabel"
id="savedObjectsManagement.objectsTable.flyout.import.cancelButtonLabel"
defaultMessage="Cancel"
/>
</EuiButtonEmpty>
@ -791,7 +827,7 @@ export class Flyout extends Component {
data-test-subj="importSavedObjectsLegacyWarning"
title={
<FormattedMessage
id="kbn.management.objects.objectsTable.flyout.legacyFileUsedTitle"
id="savedObjectsManagement.objectsTable.flyout.legacyFileUsedTitle"
defaultMessage="Support for JSON files is going away"
/>
}
@ -800,7 +836,7 @@ export class Flyout extends Component {
>
<p>
<FormattedMessage
id="kbn.management.objects.objectsTable.flyout.legacyFileUsedBody"
id="savedObjectsManagement.objectsTable.flyout.legacyFileUsedBody"
defaultMessage="Use our updated export to generate NDJSON files, and you'll be all set."
/>
</p>
@ -815,7 +851,7 @@ export class Flyout extends Component {
data-test-subj="importSavedObjectsConflictsWarning"
title={
<FormattedMessage
id="kbn.management.objects.objectsTable.flyout.indexPatternConflictsTitle"
id="savedObjectsManagement.objectsTable.flyout.indexPatternConflictsTitle"
defaultMessage="Index Pattern Conflicts"
/>
}
@ -824,7 +860,7 @@ export class Flyout extends Component {
>
<p>
<FormattedMessage
id="kbn.management.objects.objectsTable.flyout.indexPatternConflictsDescription"
id="savedObjectsManagement.objectsTable.flyout.indexPatternConflictsDescription"
defaultMessage="The following saved objects use index patterns that do not exist.
Please select the index patterns you'd like re-associated with
them. You can {indexPatternLink} if necessary."
@ -832,7 +868,7 @@ export class Flyout extends Component {
indexPatternLink: (
<EuiLink href={this.props.newIndexPatternUrl}>
<FormattedMessage
id="kbn.management.objects.objectsTable.flyout.indexPatternConflictsCalloutLinkText"
id="savedObjectsManagement.objectsTable.flyout.indexPatternConflictsCalloutLinkText"
defaultMessage="create a new index pattern"
/>
</EuiLink>
@ -867,11 +903,11 @@ export class Flyout extends Component {
}
overwriteConfirmed() {
this.state.conflictingRecord.done(true);
this.state.conflictingRecord!.done(true);
}
overwriteSkipped() {
this.state.conflictingRecord.done(false);
this.state.conflictingRecord!.done(false);
}
render() {
@ -883,18 +919,18 @@ export class Flyout extends Component {
<EuiOverlayMask>
<EuiConfirmModal
title={i18n.translate(
'kbn.management.objects.objectsTable.flyout.confirmOverwriteTitle',
'savedObjectsManagement.objectsTable.flyout.confirmOverwriteTitle',
{
defaultMessage: 'Overwrite {type}?',
values: { type: this.state.conflictingRecord.type },
}
)}
cancelButtonText={i18n.translate(
'kbn.management.objects.objectsTable.flyout.confirmOverwriteCancelButtonText',
'savedObjectsManagement.objectsTable.flyout.confirmOverwriteCancelButtonText',
{ defaultMessage: 'Cancel' }
)}
confirmButtonText={i18n.translate(
'kbn.management.objects.objectsTable.flyout.confirmOverwriteOverwriteButtonText',
'savedObjectsManagement.objectsTable.flyout.confirmOverwriteOverwriteButtonText',
{ defaultMessage: 'Overwrite' }
)}
buttonColor="danger"
@ -904,7 +940,7 @@ export class Flyout extends Component {
>
<p>
<FormattedMessage
id="kbn.management.objects.objectsTable.flyout.confirmOverwriteBody"
id="savedObjectsManagement.objectsTable.flyout.confirmOverwriteBody"
defaultMessage="Are you sure you want to overwrite {title}?"
values={{
title:
@ -924,7 +960,7 @@ export class Flyout extends Component {
<EuiTitle size="m">
<h2>
<FormattedMessage
id="kbn.management.objects.objectsTable.flyout.importSavedObjectTitle"
id="savedObjectsManagement.objectsTable.flyout.importSavedObjectTitle"
defaultMessage="Import saved objects"
/>
</h2>

View file

@ -19,8 +19,7 @@
import React from 'react';
import { shallow } from 'enzyme';
import { Header } from '../header';
import { Header } from './header';
describe('Header', () => {
it('should render normally', () => {

View file

@ -18,8 +18,6 @@
*/
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import {
EuiSpacer,
EuiTitle,
@ -31,14 +29,24 @@ import {
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
export const Header = ({ onExportAll, onImport, onRefresh, filteredCount }) => (
export const Header = ({
onExportAll,
onImport,
onRefresh,
filteredCount,
}: {
onExportAll: () => void;
onImport: () => void;
onRefresh: () => void;
filteredCount: number;
}) => (
<Fragment>
<EuiFlexGroup justifyContent="spaceBetween" alignItems="baseline">
<EuiFlexItem grow={false}>
<EuiTitle>
<h1>
<FormattedMessage
id="kbn.management.objects.objectsTable.header.savedObjectsTitle"
id="savedObjectsManagement.objectsTable.header.savedObjectsTitle"
defaultMessage="Saved Objects"
/>
</h1>
@ -55,7 +63,7 @@ export const Header = ({ onExportAll, onImport, onRefresh, filteredCount }) => (
onClick={onExportAll}
>
<FormattedMessage
id="kbn.management.objects.objectsTable.header.exportButtonLabel"
id="savedObjectsManagement.objectsTable.header.exportButtonLabel"
defaultMessage="Export {filteredCount, plural, one{# object} other {# objects}}"
values={{
filteredCount,
@ -71,7 +79,7 @@ export const Header = ({ onExportAll, onImport, onRefresh, filteredCount }) => (
onClick={onImport}
>
<FormattedMessage
id="kbn.management.objects.objectsTable.header.importButtonLabel"
id="savedObjectsManagement.objectsTable.header.importButtonLabel"
defaultMessage="Import"
/>
</EuiButtonEmpty>
@ -79,7 +87,7 @@ export const Header = ({ onExportAll, onImport, onRefresh, filteredCount }) => (
<EuiFlexItem grow={false}>
<EuiButtonEmpty size="s" iconType="refresh" onClick={onRefresh}>
<FormattedMessage
id="kbn.management.objects.objectsTable.header.refreshButtonLabel"
id="savedObjectsManagement.objectsTable.header.refreshButtonLabel"
defaultMessage="Refresh"
/>
</EuiButtonEmpty>
@ -92,7 +100,7 @@ export const Header = ({ onExportAll, onImport, onRefresh, filteredCount }) => (
<p>
<EuiTextColor color="subdued">
<FormattedMessage
id="kbn.management.objects.objectsTable.howToDeleteSavedObjectsDescription"
id="savedObjectsManagement.objectsTable.howToDeleteSavedObjectsDescription"
defaultMessage="From here you can delete saved objects, such as saved searches.
You can also edit the raw data of saved objects.
Typically objects are only modified via their associated application,
@ -104,10 +112,3 @@ export const Header = ({ onExportAll, onImport, onRefresh, filteredCount }) => (
<EuiSpacer size="m" />
</Fragment>
);
Header.propTypes = {
onExportAll: PropTypes.func.isRequired,
onImport: PropTypes.func.isRequired,
onRefresh: PropTypes.func.isRequired,
filteredCount: PropTypes.number.isRequired,
};

View file

@ -0,0 +1,23 @@
/*
* 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 { Header } from './header';
export { Table } from './table';
export { Flyout } from './flyout';
export { Relationships } from './relationships';

View file

@ -19,27 +19,23 @@
import React from 'react';
import { shallowWithI18nProvider } from 'test_utils/enzyme_helpers';
import { httpServiceMock } from '../../../../../../core/public/mocks';
import { Relationships, RelationshipsProps } from './relationships';
jest.mock('ui/kfetch', () => ({ kfetch: jest.fn() }));
jest.mock('ui/chrome', () => ({
addBasePath: () => '',
}));
jest.mock('../../../../../lib/fetch_export_by_type_and_search', () => ({
jest.mock('../../../lib/fetch_export_by_type_and_search', () => ({
fetchExportByTypeAndSearch: jest.fn(),
}));
jest.mock('../../../../../lib/fetch_export_objects', () => ({
jest.mock('../../../lib/fetch_export_objects', () => ({
fetchExportObjects: jest.fn(),
}));
import { Relationships } from '../relationships';
describe('Relationships', () => {
it('should render index patterns normally', async () => {
const props = {
const props: RelationshipsProps = {
goInspectObject: () => {},
canGoInApp: () => true,
basePath: httpServiceMock.createSetupContract().basePath,
getRelationships: jest.fn().mockImplementation(() => [
{
type: 'search',
@ -73,6 +69,8 @@ describe('Relationships', () => {
savedObject: {
id: '1',
type: 'index-pattern',
attributes: {},
references: [],
meta: {
title: 'MyIndexPattern*',
icon: 'indexPatternApp',
@ -101,8 +99,10 @@ describe('Relationships', () => {
});
it('should render searches normally', async () => {
const props = {
const props: RelationshipsProps = {
goInspectObject: () => {},
canGoInApp: () => true,
basePath: httpServiceMock.createSetupContract().basePath,
getRelationships: jest.fn().mockImplementation(() => [
{
type: 'index-pattern',
@ -136,6 +136,8 @@ describe('Relationships', () => {
savedObject: {
id: '1',
type: 'search',
attributes: {},
references: [],
meta: {
title: 'MySearch',
icon: 'search',
@ -164,8 +166,10 @@ describe('Relationships', () => {
});
it('should render visualizations normally', async () => {
const props = {
const props: RelationshipsProps = {
goInspectObject: () => {},
canGoInApp: () => true,
basePath: httpServiceMock.createSetupContract().basePath,
getRelationships: jest.fn().mockImplementation(() => [
{
type: 'dashboard',
@ -199,6 +203,8 @@ describe('Relationships', () => {
savedObject: {
id: '1',
type: 'visualization',
attributes: {},
references: [],
meta: {
title: 'MyViz',
icon: 'visualizeApp',
@ -227,8 +233,10 @@ describe('Relationships', () => {
});
it('should render dashboards normally', async () => {
const props = {
const props: RelationshipsProps = {
goInspectObject: () => {},
canGoInApp: () => true,
basePath: httpServiceMock.createSetupContract().basePath,
getRelationships: jest.fn().mockImplementation(() => [
{
type: 'visualization',
@ -262,6 +270,8 @@ describe('Relationships', () => {
savedObject: {
id: '1',
type: 'dashboard',
attributes: {},
references: [],
meta: {
title: 'MyDashboard',
icon: 'dashboardApp',
@ -290,14 +300,18 @@ describe('Relationships', () => {
});
it('should render errors', async () => {
const props = {
const props: RelationshipsProps = {
goInspectObject: () => {},
canGoInApp: () => true,
basePath: httpServiceMock.createSetupContract().basePath,
getRelationships: jest.fn().mockImplementation(() => {
throw new Error('foo');
}),
savedObject: {
id: '1',
type: 'dashboard',
attributes: {},
references: [],
meta: {
title: 'MyDashboard',
icon: 'dashboardApp',

View file

@ -18,8 +18,6 @@
*/
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import {
EuiTitle,
EuiFlyout,
@ -34,25 +32,34 @@ import {
EuiText,
EuiSpacer,
} from '@elastic/eui';
import chrome from 'ui/chrome';
import { FilterConfig } from '@elastic/eui/src/components/search_bar/filters/filters';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { getDefaultTitle, getSavedObjectLabel } from '../../../../lib';
import { IBasePath } from 'src/core/public';
import { getDefaultTitle, getSavedObjectLabel } from '../../../lib';
import { SavedObjectWithMetadata, SavedObjectRelation } from '../../../types';
export class Relationships extends Component {
static propTypes = {
getRelationships: PropTypes.func.isRequired,
savedObject: PropTypes.object.isRequired,
close: PropTypes.func.isRequired,
goInspectObject: PropTypes.func.isRequired,
canGoInApp: PropTypes.func.isRequired,
};
export interface RelationshipsProps {
basePath: IBasePath;
getRelationships: (type: string, id: string) => Promise<SavedObjectRelation[]>;
savedObject: SavedObjectWithMetadata;
close: () => void;
goInspectObject: (obj: SavedObjectWithMetadata) => void;
canGoInApp: (obj: SavedObjectWithMetadata) => boolean;
}
constructor(props) {
export interface RelationshipsState {
relationships: SavedObjectRelation[];
isLoading: boolean;
error?: string;
}
export class Relationships extends Component<RelationshipsProps, RelationshipsState> {
constructor(props: RelationshipsProps) {
super(props);
this.state = {
relationships: undefined,
relationships: [],
isLoading: false,
error: undefined,
};
@ -62,7 +69,7 @@ export class Relationships extends Component {
this.getRelationshipData();
}
UNSAFE_componentWillReceiveProps(nextProps) {
UNSAFE_componentWillReceiveProps(nextProps: RelationshipsProps) {
if (nextProps.savedObject.id !== this.props.savedObject.id) {
this.getRelationshipData();
}
@ -92,7 +99,7 @@ export class Relationships extends Component {
<EuiCallOut
title={
<FormattedMessage
id="kbn.management.objects.objectsTable.relationships.renderErrorMessage"
id="savedObjectsManagement.objectsTable.relationships.renderErrorMessage"
defaultMessage="Error"
/>
}
@ -104,7 +111,7 @@ export class Relationships extends Component {
}
renderRelationships() {
const { goInspectObject, savedObject } = this.props;
const { goInspectObject, savedObject, basePath } = this.props;
const { relationships, isLoading, error } = this.state;
if (error) {
@ -118,17 +125,17 @@ export class Relationships extends Component {
const columns = [
{
field: 'type',
name: i18n.translate('kbn.management.objects.objectsTable.relationships.columnTypeName', {
name: i18n.translate('savedObjectsManagement.objectsTable.relationships.columnTypeName', {
defaultMessage: 'Type',
}),
width: '50px',
align: 'center',
description: i18n.translate(
'kbn.management.objects.objectsTable.relationships.columnTypeDescription',
'savedObjectsManagement.objectsTable.relationships.columnTypeDescription',
{ defaultMessage: 'Type of the saved object' }
),
sortable: false,
render: (type, object) => {
render: (type: string, object: SavedObjectWithMetadata) => {
return (
<EuiToolTip position="top" content={getSavedObjectLabel(type)}>
<EuiIcon
@ -144,19 +151,19 @@ export class Relationships extends Component {
{
field: 'relationship',
name: i18n.translate(
'kbn.management.objects.objectsTable.relationships.columnRelationshipName',
'savedObjectsManagement.objectsTable.relationships.columnRelationshipName',
{ defaultMessage: 'Direct relationship' }
),
dataType: 'string',
sortable: false,
width: '125px',
'data-test-subj': 'directRelationship',
render: relationship => {
render: (relationship: string) => {
if (relationship === 'parent') {
return (
<EuiText size="s">
<FormattedMessage
id="kbn.management.objects.objectsTable.relationships.columnRelationship.parentAsValue"
id="savedObjectsManagement.objectsTable.relationships.columnRelationship.parentAsValue"
defaultMessage="Parent"
/>
</EuiText>
@ -166,7 +173,7 @@ export class Relationships extends Component {
return (
<EuiText size="s">
<FormattedMessage
id="kbn.management.objects.objectsTable.relationships.columnRelationship.childAsValue"
id="savedObjectsManagement.objectsTable.relationships.columnRelationship.childAsValue"
defaultMessage="Child"
/>
</EuiText>
@ -176,17 +183,17 @@ export class Relationships extends Component {
},
{
field: 'meta.title',
name: i18n.translate('kbn.management.objects.objectsTable.relationships.columnTitleName', {
name: i18n.translate('savedObjectsManagement.objectsTable.relationships.columnTitleName', {
defaultMessage: 'Title',
}),
description: i18n.translate(
'kbn.management.objects.objectsTable.relationships.columnTitleDescription',
'savedObjectsManagement.objectsTable.relationships.columnTitleDescription',
{ defaultMessage: 'Title of the saved object' }
),
dataType: 'string',
sortable: false,
render: (title, object) => {
const { path } = object.meta.inAppUrl || {};
render: (title: string, object: SavedObjectWithMetadata) => {
const { path = '' } = object.meta.inAppUrl || {};
const canGoInApp = this.props.canGoInApp(object);
if (!canGoInApp) {
return (
@ -196,7 +203,7 @@ export class Relationships extends Component {
);
}
return (
<EuiLink href={chrome.addBasePath(path)} data-test-subj="relationshipsTitle">
<EuiLink href={basePath.prepend(path)} data-test-subj="relationshipsTitle">
{title || getDefaultTitle(object)}
</EuiLink>
);
@ -204,24 +211,24 @@ export class Relationships extends Component {
},
{
name: i18n.translate(
'kbn.management.objects.objectsTable.relationships.columnActionsName',
'savedObjectsManagement.objectsTable.relationships.columnActionsName',
{ defaultMessage: 'Actions' }
),
actions: [
{
name: i18n.translate(
'kbn.management.objects.objectsTable.relationships.columnActions.inspectActionName',
'savedObjectsManagement.objectsTable.relationships.columnActions.inspectActionName',
{ defaultMessage: 'Inspect' }
),
description: i18n.translate(
'kbn.management.objects.objectsTable.relationships.columnActions.inspectActionDescription',
'savedObjectsManagement.objectsTable.relationships.columnActions.inspectActionDescription',
{ defaultMessage: 'Inspect this saved object' }
),
type: 'icon',
icon: 'inspect',
'data-test-subj': 'relationshipsTableAction-inspect',
onClick: object => goInspectObject(object),
available: object => !!object.meta.editUrl,
onClick: (object: SavedObjectWithMetadata) => goInspectObject(object),
available: (object: SavedObjectWithMetadata) => !!object.meta.editUrl,
},
],
},
@ -244,7 +251,7 @@ export class Relationships extends Component {
type: 'field_value_selection',
field: 'relationship',
name: i18n.translate(
'kbn.management.objects.objectsTable.relationships.search.filters.relationship.name',
'savedObjectsManagement.objectsTable.relationships.search.filters.relationship.name',
{ defaultMessage: 'Direct relationship' }
),
multiSelect: 'or',
@ -253,7 +260,7 @@ export class Relationships extends Component {
value: 'parent',
name: 'parent',
view: i18n.translate(
'kbn.management.objects.objectsTable.relationships.search.filters.relationship.parentAsValue.view',
'savedObjectsManagement.objectsTable.relationships.search.filters.relationship.parentAsValue.view',
{ defaultMessage: 'Parent' }
),
},
@ -261,7 +268,7 @@ export class Relationships extends Component {
value: 'child',
name: 'child',
view: i18n.translate(
'kbn.management.objects.objectsTable.relationships.search.filters.relationship.childAsValue.view',
'savedObjectsManagement.objectsTable.relationships.search.filters.relationship.childAsValue.view',
{ defaultMessage: 'Child' }
),
},
@ -271,13 +278,13 @@ export class Relationships extends Component {
type: 'field_value_selection',
field: 'type',
name: i18n.translate(
'kbn.management.objects.objectsTable.relationships.search.filters.type.name',
'savedObjectsManagement.objectsTable.relationships.search.filters.type.name',
{ defaultMessage: 'Type' }
),
multiSelect: 'or',
options: [...filterTypesMap.values()],
},
],
] as FilterConfig[],
};
return (
@ -285,7 +292,7 @@ export class Relationships extends Component {
<EuiCallOut>
<p>
{i18n.translate(
'kbn.management.objects.objectsTable.relationships.relationshipsTitle',
'savedObjectsManagement.objectsTable.relationships.relationshipsTitle',
{
defaultMessage:
'Here are the saved objects related to {title}. ' +
@ -301,7 +308,7 @@ export class Relationships extends Component {
<EuiSpacer />
<EuiInMemoryTable
items={relationships}
columns={columns}
columns={columns as any}
pagination={true}
search={search}
rowProps={() => ({

View file

@ -19,27 +19,22 @@
import React from 'react';
import { shallowWithI18nProvider, mountWithI18nProvider } from 'test_utils/enzyme_helpers';
// @ts-ignore
import { findTestSubject } from '@elastic/eui/lib/test';
import { keyCodes } from '@elastic/eui/lib/services';
import { npSetup as mockNpSetup } from '../../../../../../../../../../../ui/public/new_platform/__mocks__';
import { keyCodes } from '@elastic/eui';
import { httpServiceMock } from '../../../../../../core/public/mocks';
import { actionServiceMock } from '../../../services/action_service.mock';
import { Table, TableProps } from './table';
jest.mock('ui/kfetch', () => ({ kfetch: jest.fn() }));
jest.mock('ui/chrome', () => ({
addBasePath: () => '',
}));
jest.mock('ui/new_platform', () => ({
npSetup: mockNpSetup,
}));
import { Table } from '../table';
const defaultProps = {
const defaultProps: TableProps = {
basePath: httpServiceMock.createSetupContract().basePath,
actionRegistry: actionServiceMock.createStart(),
selectedSavedObjects: [
{
id: '1',
type: 'index-pattern',
attributes: {},
references: [],
meta: {
title: `MyIndexPattern*`,
icon: 'indexPatternApp',
@ -58,13 +53,15 @@ const defaultProps = {
onDelete: () => {},
onExport: () => {},
goInspectObject: () => {},
canGoInApp: () => {},
canGoInApp: () => true,
pageIndex: 1,
pageSize: 2,
items: [
{
id: '1',
type: 'index-pattern',
attributes: {},
references: [],
meta: {
title: `MyIndexPattern*`,
icon: 'indexPatternApp',
@ -120,7 +117,7 @@ describe('Table', () => {
{ type: 'visualization' },
{ type: 'search' },
{ type: 'index-pattern' },
];
] as any;
const customizedProps = { ...defaultProps, selectedSavedObjects, canDelete: false };
const component = shallowWithI18nProvider(<Table {...customizedProps} />);

View file

@ -17,12 +17,10 @@
* under the License.
*/
import chrome from 'ui/chrome';
import { npSetup } from 'ui/new_platform';
import { IBasePath } from 'src/core/public';
import React, { PureComponent, Fragment } from 'react';
import PropTypes from 'prop-types';
import {
// @ts-ignore
EuiSearchBar,
EuiBasicTable,
EuiButton,
@ -35,54 +33,64 @@ import {
EuiSwitch,
EuiFormRow,
EuiText,
EuiTableFieldDataColumnType,
EuiTableActionsColumnType,
} from '@elastic/eui';
import { getDefaultTitle, getSavedObjectLabel } from '../../../../lib';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { getDefaultTitle, getSavedObjectLabel } from '../../../lib';
import { SavedObjectWithMetadata } from '../../../types';
import {
SavedObjectsManagementActionServiceStart,
SavedObjectsManagementAction,
} from '../../../services';
export class Table extends PureComponent {
static propTypes = {
selectedSavedObjects: PropTypes.array.isRequired,
selectionConfig: PropTypes.shape({
selectable: PropTypes.func,
selectableMessage: PropTypes.func,
onSelectionChange: PropTypes.func.isRequired,
}).isRequired,
filterOptions: PropTypes.array.isRequired,
canDelete: PropTypes.bool.isRequired,
onDelete: PropTypes.func.isRequired,
onExport: PropTypes.func.isRequired,
goInspectObject: PropTypes.func.isRequired,
pageIndex: PropTypes.number.isRequired,
pageSize: PropTypes.number.isRequired,
items: PropTypes.array.isRequired,
itemId: PropTypes.oneOfType([
PropTypes.string, // the name of the item id property
PropTypes.func, // (item) => string
]),
totalItemCount: PropTypes.number.isRequired,
onQueryChange: PropTypes.func.isRequired,
onTableChange: PropTypes.func.isRequired,
isSearching: PropTypes.bool.isRequired,
onShowRelationships: PropTypes.func.isRequired,
export interface TableProps {
basePath: IBasePath;
actionRegistry: SavedObjectsManagementActionServiceStart;
selectedSavedObjects: SavedObjectWithMetadata[];
selectionConfig: {
onSelectionChange: (selection: SavedObjectWithMetadata[]) => void;
};
filterOptions: any[];
canDelete: boolean;
onDelete: () => void;
onExport: (includeReferencesDeep: boolean) => void;
goInspectObject: (obj: SavedObjectWithMetadata) => void;
pageIndex: number;
pageSize: number;
items: SavedObjectWithMetadata[];
itemId: string | (() => string);
totalItemCount: number;
onQueryChange: (query: any) => void;
onTableChange: (table: any) => void;
isSearching: boolean;
onShowRelationships: (object: SavedObjectWithMetadata) => void;
canGoInApp: (obj: SavedObjectWithMetadata) => boolean;
}
state = {
interface TableState {
isSearchTextValid: boolean;
parseErrorMessage: any;
isExportPopoverOpen: boolean;
isIncludeReferencesDeepChecked: boolean;
activeAction?: SavedObjectsManagementAction;
}
export class Table extends PureComponent<TableProps, TableState> {
state: TableState = {
isSearchTextValid: true,
parseErrorMessage: null,
isExportPopoverOpen: false,
isIncludeReferencesDeepChecked: true,
activeAction: null,
activeAction: undefined,
};
constructor(props) {
constructor(props: TableProps) {
super(props);
this.extraActions = npSetup.plugins.savedObjectsManagement.actionRegistry.getAll();
}
onChange = ({ query, error }) => {
onChange = ({ query, error }: any) => {
if (error) {
this.setState({
isSearchTextValid: false,
@ -136,12 +144,14 @@ export class Table extends PureComponent {
onTableChange,
goInspectObject,
onShowRelationships,
basePath,
actionRegistry,
} = this.props;
const pagination = {
pageIndex: pageIndex,
pageSize: pageSize,
totalItemCount: totalItemCount,
pageIndex,
pageSize,
totalItemCount,
pageSizeOptions: [5, 10, 20, 50],
};
@ -149,7 +159,7 @@ export class Table extends PureComponent {
{
type: 'field_value_selection',
field: 'type',
name: i18n.translate('kbn.management.objects.objectsTable.table.typeFilterName', {
name: i18n.translate('savedObjectsManagement.objectsTable.table.typeFilterName', {
defaultMessage: 'Type',
}),
multiSelect: 'or',
@ -168,18 +178,18 @@ export class Table extends PureComponent {
const columns = [
{
field: 'type',
name: i18n.translate('kbn.management.objects.objectsTable.table.columnTypeName', {
name: i18n.translate('savedObjectsManagement.objectsTable.table.columnTypeName', {
defaultMessage: 'Type',
}),
width: '50px',
align: 'center',
description: i18n.translate(
'kbn.management.objects.objectsTable.table.columnTypeDescription',
'savedObjectsManagement.objectsTable.table.columnTypeDescription',
{ defaultMessage: 'Type of the saved object' }
),
sortable: false,
'data-test-subj': 'savedObjectsTableRowType',
render: (type, object) => {
render: (type: string, object: SavedObjectWithMetadata) => {
return (
<EuiToolTip position="top" content={getSavedObjectLabel(type)}>
<EuiIcon
@ -191,42 +201,42 @@ export class Table extends PureComponent {
</EuiToolTip>
);
},
},
} as EuiTableFieldDataColumnType<SavedObjectWithMetadata<any>>,
{
field: 'meta.title',
name: i18n.translate('kbn.management.objects.objectsTable.table.columnTitleName', {
name: i18n.translate('savedObjectsManagement.objectsTable.table.columnTitleName', {
defaultMessage: 'Title',
}),
description: i18n.translate(
'kbn.management.objects.objectsTable.table.columnTitleDescription',
'savedObjectsManagement.objectsTable.table.columnTitleDescription',
{ defaultMessage: 'Title of the saved object' }
),
dataType: 'string',
sortable: false,
'data-test-subj': 'savedObjectsTableRowTitle',
render: (title, object) => {
const { path } = object.meta.inAppUrl || {};
render: (title: string, object: SavedObjectWithMetadata) => {
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>
<EuiLink href={basePath.prepend(path)}>{title || getDefaultTitle(object)}</EuiLink>
);
},
},
} as EuiTableFieldDataColumnType<SavedObjectWithMetadata<any>>,
{
name: i18n.translate('kbn.management.objects.objectsTable.table.columnActionsName', {
name: i18n.translate('savedObjectsManagement.objectsTable.table.columnActionsName', {
defaultMessage: 'Actions',
}),
actions: [
{
name: i18n.translate(
'kbn.management.objects.objectsTable.table.columnActions.inspectActionName',
'savedObjectsManagement.objectsTable.table.columnActions.inspectActionName',
{ defaultMessage: 'Inspect' }
),
description: i18n.translate(
'kbn.management.objects.objectsTable.table.columnActions.inspectActionDescription',
'savedObjectsManagement.objectsTable.table.columnActions.inspectActionDescription',
{ defaultMessage: 'Inspect this saved object' }
),
type: 'icon',
@ -237,11 +247,11 @@ export class Table extends PureComponent {
},
{
name: i18n.translate(
'kbn.management.objects.objectsTable.table.columnActions.viewRelationshipsActionName',
'savedObjectsManagement.objectsTable.table.columnActions.viewRelationshipsActionName',
{ defaultMessage: 'Relationships' }
),
description: i18n.translate(
'kbn.management.objects.objectsTable.table.columnActions.viewRelationshipsActionDescription',
'savedObjectsManagement.objectsTable.table.columnActions.viewRelationshipsActionDescription',
{
defaultMessage:
'View the relationships this saved object has to other saved objects',
@ -252,33 +262,35 @@ export class Table extends PureComponent {
onClick: object => onShowRelationships(object),
'data-test-subj': 'savedObjectsTableAction-relationships',
},
...this.extraActions.map(action => {
...actionRegistry.getAll().map(action => {
return {
...action.euiAction,
'data-test-subj': `savedObjectsTableAction-${action.id}`,
onClick: object => {
onClick: (object: SavedObjectWithMetadata) => {
this.setState({
activeAction: action,
});
action.registerOnFinishCallback(() => {
this.setState({
activeAction: null,
activeAction: undefined,
});
});
action.euiAction.onClick(object);
if (action.euiAction.onClick) {
action.euiAction.onClick(object as any);
}
},
};
}),
],
},
} as EuiTableActionsColumnType<SavedObjectWithMetadata>,
];
let queryParseError;
if (!this.state.isSearchTextValid) {
const parseErrorMsg = i18n.translate(
'kbn.management.objects.objectsTable.searchBar.unableToParseQueryErrorMessage',
'savedObjectsManagement.objectsTable.searchBar.unableToParseQueryErrorMessage',
{ defaultMessage: 'Unable to parse query' }
);
queryParseError = (
@ -294,20 +306,20 @@ export class Table extends PureComponent {
isDisabled={selectedSavedObjects.length === 0}
>
<FormattedMessage
id="kbn.management.objects.objectsTable.table.exportPopoverButtonLabel"
id="savedObjectsManagement.objectsTable.table.exportPopoverButtonLabel"
defaultMessage="Export"
/>
</EuiButton>
);
const activeActionContents = this.state.activeAction ? this.state.activeAction.render() : null;
const activeActionContents = this.state.activeAction?.render() ?? null;
return (
<Fragment>
{activeActionContents}
<EuiSearchBar
box={{ 'data-test-subj': 'savedObjectSearchBar' }}
filters={filters}
filters={filters as any}
onChange={this.onChange}
toolsRight={[
<EuiButton
@ -319,14 +331,14 @@ export class Table extends PureComponent {
title={
this.props.canDelete
? undefined
: i18n.translate('kbn.management.objects.objectsTable.table.deleteButtonTitle', {
: i18n.translate('savedObjectsManagement.objectsTable.table.deleteButtonTitle', {
defaultMessage: 'Unable to delete saved objects',
})
}
data-test-subj="savedObjectsManagementDelete"
>
<FormattedMessage
id="kbn.management.objects.objectsTable.table.deleteButtonLabel"
id="savedObjectsManagement.objectsTable.table.deleteButtonLabel"
defaultMessage="Delete"
/>
</EuiButton>,
@ -339,7 +351,7 @@ export class Table extends PureComponent {
<EuiFormRow
label={
<FormattedMessage
id="kbn.management.objects.objectsTable.exportObjectsConfirmModal.exportOptionsLabel"
id="savedObjectsManagement.objectsTable.exportObjectsConfirmModal.exportOptionsLabel"
defaultMessage="Options"
/>
}
@ -348,7 +360,7 @@ export class Table extends PureComponent {
name="includeReferencesDeep"
label={
<FormattedMessage
id="kbn.management.objects.objectsTable.exportObjectsConfirmModal.includeReferencesDeepLabel"
id="savedObjectsManagement.objectsTable.exportObjectsConfirmModal.includeReferencesDeepLabel"
defaultMessage="Include related objects"
/>
}
@ -359,7 +371,7 @@ export class Table extends PureComponent {
<EuiFormRow>
<EuiButton key="exportSO" iconType="exportAction" onClick={this.onExportClick} fill>
<FormattedMessage
id="kbn.management.objects.objectsTable.table.exportButtonLabel"
id="savedObjectsManagement.objectsTable.table.exportButtonLabel"
defaultMessage="Export"
/>
</EuiButton>
@ -374,7 +386,7 @@ export class Table extends PureComponent {
loading={isSearching}
itemId={itemId}
items={items}
columns={columns}
columns={columns as any}
pagination={pagination}
selection={selection}
onChange={onTableChange}

View file

@ -17,4 +17,4 @@
* under the License.
*/
export { Table } from './table';
export { SavedObjectsTable } from './saved_objects_table';

View file

@ -0,0 +1,67 @@
/*
* 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 const saveAsMock = jest.fn();
jest.doMock('@elastic/filesaver', () => ({
saveAs: saveAsMock,
}));
jest.doMock('lodash', () => ({
...jest.requireActual('lodash'),
debounce: (func: Function) => {
function debounced(this: any, ...args: any[]) {
return func.apply(this, args);
}
return debounced;
},
}));
export const findObjectsMock = jest.fn();
jest.doMock('../../lib/find_objects', () => ({
findObjects: findObjectsMock,
}));
export const fetchExportObjectsMock = jest.fn();
jest.doMock('../../lib/fetch_export_objects', () => ({
fetchExportObjects: fetchExportObjectsMock,
}));
export const fetchExportByTypeAndSearchMock = jest.fn();
jest.doMock('../../lib/fetch_export_by_type_and_search', () => ({
fetchExportByTypeAndSearch: fetchExportByTypeAndSearchMock,
}));
export const extractExportDetailsMock = jest.fn();
jest.doMock('../../lib/extract_export_details', () => ({
extractExportDetails: extractExportDetailsMock,
}));
jest.doMock('./components/header', () => ({
Header: () => 'Header',
}));
export const getSavedObjectCountsMock = jest.fn();
jest.doMock('../../lib/get_saved_object_counts', () => ({
getSavedObjectCounts: getSavedObjectCountsMock,
}));
export const getRelationshipsMock = jest.fn();
jest.doMock('../../lib/get_relationships', () => ({
getRelationships: getRelationshipsMock,
}));

View file

@ -17,69 +17,39 @@
* under the License.
*/
import {
extractExportDetailsMock,
fetchExportByTypeAndSearchMock,
fetchExportObjectsMock,
findObjectsMock,
getRelationshipsMock,
getSavedObjectCountsMock,
saveAsMock,
} from './saved_objects_table.test.mocks';
import React from 'react';
import { shallowWithI18nProvider } from 'test_utils/enzyme_helpers';
import { mockManagementPlugin } from '../../../../../../../../../../plugins/index_pattern_management/public/mocks';
import { Query } from '@elastic/eui';
import { ShallowWrapper } from 'enzyme';
import { shallowWithI18nProvider } from 'test_utils/enzyme_helpers';
import {
httpServiceMock,
overlayServiceMock,
notificationServiceMock,
savedObjectsServiceMock,
applicationServiceMock,
} from '../../../../../core/public/mocks';
import { dataPluginMock } from '../../../../data/public/mocks';
import { serviceRegistryMock } from '../../services/service_registry.mock';
import { actionServiceMock } from '../../services/action_service.mock';
import {
SavedObjectsTable,
SavedObjectsTableProps,
SavedObjectsTableState,
} from './saved_objects_table';
import { Flyout, Relationships } from './components';
import { SavedObjectWithMetadata } from '../../types';
import { ObjectsTable, POSSIBLE_TYPES } from '../objects_table';
import { Flyout } from '../components/flyout/';
import { Relationships } from '../components/relationships/';
import { findObjects } from '../../../lib';
import { extractExportDetails } from '../../../lib/extract_export_details';
jest.mock('ui/kfetch', () => ({ kfetch: jest.fn() }));
jest.mock('../../../../../../../../../../plugins/index_pattern_management/public', () => ({
setup: mockManagementPlugin.createSetupContract(),
start: mockManagementPlugin.createStartContract(),
}));
jest.mock('../../../lib/find_objects', () => ({
findObjects: jest.fn(),
}));
jest.mock('../components/header', () => ({
Header: () => 'Header',
}));
jest.mock('ui/chrome', () => ({
addBasePath: () => '',
getInjected: () => ['index-pattern', 'visualization', 'dashboard', 'search'],
}));
jest.mock('../../../lib/fetch_export_objects', () => ({
fetchExportObjects: jest.fn(),
}));
jest.mock('../../../lib/fetch_export_by_type_and_search', () => ({
fetchExportByTypeAndSearch: jest.fn(),
}));
jest.mock('../../../lib/extract_export_details', () => ({
extractExportDetails: jest.fn(),
}));
jest.mock('../../../lib/get_saved_object_counts', () => ({
getSavedObjectCounts: jest.fn().mockImplementation(() => {
return {
'index-pattern': 0,
visualization: 0,
dashboard: 0,
search: 0,
};
}),
}));
jest.mock('@elastic/filesaver', () => ({
saveAs: jest.fn(),
}));
jest.mock('../../../lib/get_relationships', () => ({
getRelationships: jest.fn(),
}));
jest.mock('ui/notify', () => ({}));
const allowedTypes = ['index-pattern', 'visualization', 'dashboard', 'search'];
const allSavedObjects = [
{
@ -112,122 +82,128 @@ const allSavedObjects = [
},
];
const $http = () => {};
$http.post = jest.fn().mockImplementation(() => []);
const defaultProps = {
goInspectObject: () => {},
confirmModalPromise: jest.fn(),
savedObjectsClient: {
find: jest.fn(),
bulkGet: jest.fn(),
},
indexPatterns: {
clearCache: jest.fn(),
},
$http,
basePath: '',
newIndexPatternUrl: '',
kbnIndex: '',
services: [],
uiCapabilities: {
savedObjectsManagement: {
read: true,
edit: false,
delete: false,
},
},
canDelete: true,
};
describe('SavedObjectsTable', () => {
let defaultProps: SavedObjectsTableProps;
let http: ReturnType<typeof httpServiceMock.createStartContract>;
let overlays: ReturnType<typeof overlayServiceMock.createStartContract>;
let notifications: ReturnType<typeof notificationServiceMock.createStartContract>;
let savedObjects: ReturnType<typeof savedObjectsServiceMock.createStartContract>;
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',
},
},
},
],
}));
});
const shallowRender = (overrides: Partial<SavedObjectsTableProps> = {}) => {
return (shallowWithI18nProvider(
<SavedObjectsTable {...defaultProps} {...overrides} />
) as unknown) as ShallowWrapper<
SavedObjectsTableProps,
SavedObjectsTableState,
SavedObjectsTable
>;
};
let addDangerMock;
let addSuccessMock;
let addWarningMock;
describe('ObjectsTable', () => {
beforeEach(() => {
defaultProps.savedObjectsClient.find.mockClear();
extractExportDetails.mockReset();
// mock _.debounce to fire immediately with no internal timer
require('lodash').debounce = func => {
function debounced(...args) {
return func.apply(this, args);
}
return debounced;
extractExportDetailsMock.mockReset();
http = httpServiceMock.createStartContract();
overlays = overlayServiceMock.createStartContract();
notifications = notificationServiceMock.createStartContract();
savedObjects = savedObjectsServiceMock.createStartContract();
const applications = applicationServiceMock.createStartContract();
applications.capabilities = {
navLinks: {},
management: {},
catalogue: {},
savedObjectsManagement: {
read: true,
edit: false,
delete: false,
},
};
addDangerMock = jest.fn();
addSuccessMock = jest.fn();
addWarningMock = jest.fn();
require('ui/notify').toastNotifications = {
addDanger: addDangerMock,
addSuccess: addSuccessMock,
addWarning: addWarningMock,
http.post.mockResolvedValue([]);
getSavedObjectCountsMock.mockReturnValue({
'index-pattern': 0,
visualization: 0,
dashboard: 0,
search: 0,
});
defaultProps = {
allowedTypes,
serviceRegistry: serviceRegistryMock.create(),
actionRegistry: actionServiceMock.createStart(),
savedObjectsClient: savedObjects.client,
indexPatterns: dataPluginMock.createStartContract().indexPatterns,
http,
overlays,
notifications,
applications,
perPageConfig: 15,
goInspectObject: () => {},
canGoInApp: () => true,
};
findObjectsMock.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',
},
},
},
],
}));
});
it('should render normally', async () => {
const component = shallowWithI18nProvider(
<ObjectsTable {...defaultProps} perPageConfig={15} />
);
const component = shallowRender({ perPageConfig: 15 });
// Ensure all promises resolve
await new Promise(resolve => process.nextTick(resolve));
@ -238,19 +214,17 @@ describe('ObjectsTable', () => {
});
it('should add danger toast when find fails', async () => {
findObjects.mockImplementation(() => {
findObjectsMock.mockImplementation(() => {
throw new Error('Simulated find error');
});
const component = shallowWithI18nProvider(
<ObjectsTable {...defaultProps} perPageConfig={15} />
);
const component = shallowRender({ perPageConfig: 15 });
// Ensure all promises resolve
await new Promise(resolve => process.nextTick(resolve));
// Ensure the state changes are reflected
component.update();
expect(addDangerMock).toHaveBeenCalled();
expect(notifications.toasts.addDanger).toHaveBeenCalled();
});
describe('export', () => {
@ -258,7 +232,7 @@ describe('ObjectsTable', () => {
const mockSelectedSavedObjects = [
{ id: '1', type: 'index-pattern' },
{ id: '3', type: 'dashboard' },
];
] as SavedObjectWithMetadata[];
const mockSavedObjects = mockSelectedSavedObjects.map(obj => ({
_id: obj.id,
@ -272,11 +246,7 @@ describe('ObjectsTable', () => {
})),
};
const { fetchExportObjects } = require('../../../lib/fetch_export_objects');
const component = shallowWithI18nProvider(
<ObjectsTable {...defaultProps} savedObjectsClient={mockSavedObjectsClient} />
);
const component = shallowRender({ savedObjectsClient: mockSavedObjectsClient });
// Ensure all promises resolve
await new Promise(resolve => process.nextTick(resolve));
@ -288,8 +258,8 @@ describe('ObjectsTable', () => {
await component.instance().onExport(true);
expect(fetchExportObjects).toHaveBeenCalledWith(mockSelectedSavedObjects, true);
expect(addSuccessMock).toHaveBeenCalledWith({
expect(fetchExportObjectsMock).toHaveBeenCalledWith(http, mockSelectedSavedObjects, true);
expect(notifications.toasts.addSuccess).toHaveBeenCalledWith({
title: 'Your file is downloading in the background',
});
});
@ -298,7 +268,7 @@ describe('ObjectsTable', () => {
const mockSelectedSavedObjects = [
{ id: '1', type: 'index-pattern' },
{ id: '3', type: 'dashboard' },
];
] as SavedObjectWithMetadata[];
const mockSavedObjects = mockSelectedSavedObjects.map(obj => ({
_id: obj.id,
@ -312,16 +282,13 @@ describe('ObjectsTable', () => {
})),
};
const { fetchExportObjects } = require('../../../lib/fetch_export_objects');
extractExportDetails.mockImplementation(() => ({
extractExportDetailsMock.mockImplementation(() => ({
exportedCount: 2,
missingRefCount: 1,
missingReferences: [{ id: '7', type: 'visualisation' }],
}));
const component = shallowWithI18nProvider(
<ObjectsTable {...defaultProps} savedObjectsClient={mockSavedObjectsClient} />
);
const component = shallowRender({ savedObjectsClient: mockSavedObjectsClient });
// Ensure all promises resolve
await new Promise(resolve => process.nextTick(resolve));
@ -333,8 +300,8 @@ describe('ObjectsTable', () => {
await component.instance().onExport(true);
expect(fetchExportObjects).toHaveBeenCalledWith(mockSelectedSavedObjects, true);
expect(addWarningMock).toHaveBeenCalledWith({
expect(fetchExportObjectsMock).toHaveBeenCalledWith(http, mockSelectedSavedObjects, true);
expect(notifications.toasts.addWarning).toHaveBeenCalledWith({
title:
'Your file is downloading in the background. ' +
'Some related objects could not be found. ' +
@ -343,25 +310,21 @@ describe('ObjectsTable', () => {
});
it('should allow the user to choose when exporting all', async () => {
const component = shallowWithI18nProvider(<ObjectsTable {...defaultProps} />);
const component = shallowRender();
// Ensure all promises resolve
await new Promise(resolve => process.nextTick(resolve));
// Ensure the state changes are reflected
component.update();
component.find('Header').prop('onExportAll')();
(component.find('Header') as any).prop('onExportAll')();
component.update();
expect(component.find('EuiModal')).toMatchSnapshot();
});
it('should export all', async () => {
const {
fetchExportByTypeAndSearch,
} = require('../../../lib/fetch_export_by_type_and_search');
const { saveAs } = require('@elastic/filesaver');
const component = shallowWithI18nProvider(<ObjectsTable {...defaultProps} />);
const component = shallowRender();
// Ensure all promises resolve
await new Promise(resolve => process.nextTick(resolve));
@ -370,23 +333,24 @@ describe('ObjectsTable', () => {
// Set up mocks
const blob = new Blob([JSON.stringify(allSavedObjects)], { type: 'application/ndjson' });
fetchExportByTypeAndSearch.mockImplementation(() => blob);
fetchExportByTypeAndSearchMock.mockImplementation(() => blob);
await component.instance().onExportAll();
expect(fetchExportByTypeAndSearch).toHaveBeenCalledWith(POSSIBLE_TYPES, undefined, true);
expect(saveAs).toHaveBeenCalledWith(blob, 'export.ndjson');
expect(addSuccessMock).toHaveBeenCalledWith({
expect(fetchExportByTypeAndSearchMock).toHaveBeenCalledWith(
http,
allowedTypes,
undefined,
true
);
expect(saveAsMock).toHaveBeenCalledWith(blob, 'export.ndjson');
expect(notifications.toasts.addSuccess).toHaveBeenCalledWith({
title: 'Your file is downloading in the background',
});
});
it('should export all, accounting for the current search criteria', async () => {
const {
fetchExportByTypeAndSearch,
} = require('../../../lib/fetch_export_by_type_and_search');
const { saveAs } = require('@elastic/filesaver');
const component = shallowWithI18nProvider(<ObjectsTable {...defaultProps} />);
const component = shallowRender();
component.instance().onQueryChange({
query: Query.parse('test'),
@ -399,13 +363,18 @@ describe('ObjectsTable', () => {
// Set up mocks
const blob = new Blob([JSON.stringify(allSavedObjects)], { type: 'application/ndjson' });
fetchExportByTypeAndSearch.mockImplementation(() => blob);
fetchExportByTypeAndSearchMock.mockImplementation(() => blob);
await component.instance().onExportAll();
expect(fetchExportByTypeAndSearch).toHaveBeenCalledWith(POSSIBLE_TYPES, 'test*', true);
expect(saveAs).toHaveBeenCalledWith(blob, 'export.ndjson');
expect(addSuccessMock).toHaveBeenCalledWith({
expect(fetchExportByTypeAndSearchMock).toHaveBeenCalledWith(
http,
allowedTypes,
'test*',
true
);
expect(saveAsMock).toHaveBeenCalledWith(blob, 'export.ndjson');
expect(notifications.toasts.addSuccess).toHaveBeenCalledWith({
title: 'Your file is downloading in the background',
});
});
@ -413,7 +382,7 @@ describe('ObjectsTable', () => {
describe('import', () => {
it('should show the flyout', async () => {
const component = shallowWithI18nProvider(<ObjectsTable {...defaultProps} />);
const component = shallowRender();
// Ensure all promises resolve
await new Promise(resolve => process.nextTick(resolve));
@ -427,7 +396,7 @@ describe('ObjectsTable', () => {
});
it('should hide the flyout', async () => {
const component = shallowWithI18nProvider(<ObjectsTable {...defaultProps} />);
const component = shallowRender();
// Ensure all promises resolve
await new Promise(resolve => process.nextTick(resolve));
@ -443,9 +412,7 @@ describe('ObjectsTable', () => {
describe('relationships', () => {
it('should fetch relationships', async () => {
const { getRelationships } = require('../../../lib/get_relationships');
const component = shallowWithI18nProvider(<ObjectsTable {...defaultProps} />);
const component = shallowRender();
// Ensure all promises resolve
await new Promise(resolve => process.nextTick(resolve));
@ -454,17 +421,11 @@ describe('ObjectsTable', () => {
await component.instance().getRelationships('search', '1');
const savedObjectTypes = ['index-pattern', 'visualization', 'dashboard', 'search'];
expect(getRelationships).toHaveBeenCalledWith(
'search',
'1',
savedObjectTypes,
defaultProps.$http,
defaultProps.basePath
);
expect(getRelationshipsMock).toHaveBeenCalledWith(http, 'search', '1', savedObjectTypes);
});
it('should show the flyout', async () => {
const component = shallowWithI18nProvider(<ObjectsTable {...defaultProps} />);
const component = shallowRender();
// Ensure all promises resolve
await new Promise(resolve => process.nextTick(resolve));
@ -483,7 +444,7 @@ describe('ObjectsTable', () => {
uiCapabilitiesPath: 'discover.show',
},
},
});
} as SavedObjectWithMetadata);
component.update();
expect(component.find(Relationships)).toMatchSnapshot();
@ -503,7 +464,7 @@ describe('ObjectsTable', () => {
});
it('should hide the flyout', async () => {
const component = shallowWithI18nProvider(<ObjectsTable {...defaultProps} />);
const component = shallowRender();
// Ensure all promises resolve
await new Promise(resolve => process.nextTick(resolve));
@ -522,12 +483,12 @@ describe('ObjectsTable', () => {
describe('delete', () => {
it('should show a confirm modal', async () => {
const component = shallowWithI18nProvider(<ObjectsTable {...defaultProps} />);
const component = shallowRender();
const mockSelectedSavedObjects = [
{ id: '1', type: 'index-pattern', title: 'Title 1' },
{ id: '3', type: 'dashboard', title: 'Title 2' },
];
{ id: '1', type: 'index-pattern' },
{ id: '3', type: 'dashboard' },
] as SavedObjectWithMetadata[];
// Ensure all promises resolve
await new Promise(resolve => process.nextTick(resolve));
@ -546,7 +507,7 @@ describe('ObjectsTable', () => {
const mockSelectedSavedObjects = [
{ id: '1', type: 'index-pattern' },
{ id: '3', type: 'dashboard' },
];
] as SavedObjectWithMetadata[];
const mockSavedObjects = mockSelectedSavedObjects.map(obj => ({
id: obj.id,
@ -562,9 +523,7 @@ describe('ObjectsTable', () => {
delete: jest.fn(),
};
const component = shallowWithI18nProvider(
<ObjectsTable {...defaultProps} savedObjectsClient={mockSavedObjectsClient} />
);
const component = shallowRender({ savedObjectsClient: mockSavedObjectsClient });
// Ensure all promises resolve
await new Promise(resolve => process.nextTick(resolve));

View file

@ -17,17 +17,10 @@
* under the License.
*/
import chrome from 'ui/chrome';
import { saveAs } from '@elastic/filesaver';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { debounce } from 'lodash';
import { Header } from './components/header';
import { Flyout } from './components/flyout';
import { Relationships } from './components/relationships';
import { Table } from './components/table';
import { toastNotifications } from 'ui/notify';
// @ts-ignore
import { saveAs } from '@elastic/filesaver';
import {
EuiSpacer,
Query,
@ -54,7 +47,15 @@ import {
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import {
SavedObjectsClientContract,
SavedObjectsFindOptions,
HttpStart,
OverlayStart,
NotificationsStart,
ApplicationStart,
} from 'src/core/public';
import { IndexPatternsContract } from '../../../../data/public';
import {
parseQuery,
getSavedObjectCounts,
@ -63,39 +64,72 @@ import {
fetchExportObjects,
fetchExportByTypeAndSearch,
findObjects,
extractExportDetails,
SavedObjectsExportResultDetails,
} from '../../lib';
import { extractExportDetails } from '../../lib/extract_export_details';
import { SavedObjectWithMetadata } from '../../types';
import {
ISavedObjectsManagementServiceRegistry,
SavedObjectsManagementActionServiceStart,
} from '../../services';
import { Header, Table, Flyout, Relationships } from './components';
export const POSSIBLE_TYPES = chrome.getInjected('importAndExportableTypes');
interface ExportAllOption {
id: string;
label: string;
}
export class ObjectsTable extends Component {
static propTypes = {
savedObjectsClient: PropTypes.object.isRequired,
indexPatterns: PropTypes.object.isRequired,
$http: PropTypes.func.isRequired,
basePath: PropTypes.string.isRequired,
perPageConfig: PropTypes.number,
newIndexPatternUrl: PropTypes.string.isRequired,
confirmModalPromise: PropTypes.func.isRequired,
services: PropTypes.array.isRequired,
uiCapabilities: PropTypes.object.isRequired,
goInspectObject: PropTypes.func.isRequired,
canGoInApp: PropTypes.func.isRequired,
};
export interface SavedObjectsTableProps {
allowedTypes: string[];
serviceRegistry: ISavedObjectsManagementServiceRegistry;
actionRegistry: SavedObjectsManagementActionServiceStart;
savedObjectsClient: SavedObjectsClientContract;
indexPatterns: IndexPatternsContract;
http: HttpStart;
overlays: OverlayStart;
notifications: NotificationsStart;
applications: ApplicationStart;
perPageConfig: number;
goInspectObject: (obj: SavedObjectWithMetadata) => void;
canGoInApp: (obj: SavedObjectWithMetadata) => boolean;
}
constructor(props) {
export interface SavedObjectsTableState {
totalCount: number;
page: number;
perPage: number;
savedObjects: SavedObjectWithMetadata[];
savedObjectCounts: Record<string, number>;
activeQuery: Query;
selectedSavedObjects: SavedObjectWithMetadata[];
isShowingImportFlyout: boolean;
isSearching: boolean;
filteredItemCount: number;
isShowingRelationships: boolean;
relationshipObject?: SavedObjectWithMetadata;
isShowingDeleteConfirmModal: boolean;
isShowingExportAllOptionsModal: boolean;
isDeleting: boolean;
exportAllOptions: ExportAllOption[];
exportAllSelectedOptions: Record<string, boolean>;
isIncludeReferencesDeepChecked: boolean;
}
export class SavedObjectsTable extends Component<SavedObjectsTableProps, SavedObjectsTableState> {
private _isMounted = false;
constructor(props: SavedObjectsTableProps) {
super(props);
this.savedObjectTypes = POSSIBLE_TYPES;
this.state = {
totalCount: 0,
page: 0,
perPage: props.perPageConfig || 50,
savedObjects: [],
savedObjectCounts: this.savedObjectTypes.reduce((typeToCountMap, type) => {
savedObjectCounts: props.allowedTypes.reduce((typeToCountMap, type) => {
typeToCountMap[type] = 0;
return typeToCountMap;
}, {}),
}, {} as Record<string, number>),
activeQuery: Query.parse(''),
selectedSavedObjects: [],
isShowingImportFlyout: false,
@ -124,21 +158,20 @@ export class ObjectsTable extends Component {
}
fetchCounts = async () => {
const { allowedTypes } = this.props;
const { queryText, visibleTypes } = parseQuery(this.state.activeQuery);
const filteredTypes = this.savedObjectTypes.filter(
type => !visibleTypes || visibleTypes.includes(type)
);
const filteredTypes = allowedTypes.filter(type => !visibleTypes || visibleTypes.includes(type));
// These are the saved objects visible in the table.
const filteredSavedObjectCounts = await getSavedObjectCounts(
this.props.$http,
this.props.http,
filteredTypes,
queryText
);
const exportAllOptions = [];
const exportAllSelectedOptions = {};
const exportAllOptions: ExportAllOption[] = [];
const exportAllSelectedOptions: Record<string, boolean> = {};
Object.keys(filteredSavedObjectCounts).forEach(id => {
// Add this type as a bulk-export option.
@ -147,17 +180,13 @@ export class ObjectsTable extends Component {
label: `${id} (${filteredSavedObjectCounts[id] || 0})`,
});
// Select it by defayult.
// Select it by default.
exportAllSelectedOptions[id] = true;
});
// Fetch all the saved objects that exist so we can accurately populate the counts within
// the table filter dropdown.
const savedObjectCounts = await getSavedObjectCounts(
this.props.$http,
this.savedObjectTypes,
queryText
);
const savedObjectCounts = await getSavedObjectCounts(this.props.http, allowedTypes, queryText);
this.setState(state => ({
...state,
@ -178,66 +207,64 @@ export class ObjectsTable extends Component {
debouncedFetch = debounce(async () => {
const { activeQuery: query, page, perPage } = this.state;
const { notifications, http, allowedTypes } = this.props;
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 = {
const findOptions: SavedObjectsFindOptions = {
search: queryText ? `${queryText}*` : undefined,
perPage,
page: page + 1,
fields: ['id'],
type: this.savedObjectTypes.filter(type => !visibleTypes || visibleTypes.includes(type)),
type: allowedTypes.filter(type => !visibleTypes || visibleTypes.includes(type)),
};
if (findOptions.type.length > 1) {
findOptions.sortField = 'type';
}
let resp;
try {
resp = await findObjects(findOptions);
const resp = await findObjects(http, findOptions);
if (!this._isMounted) {
return;
}
this.setState(({ activeQuery }) => {
// ignore results for old requests
if (activeQuery.text !== query.text) {
return null;
}
return {
savedObjects: resp.savedObjects,
filteredItemCount: resp.total,
isSearching: false,
};
});
} catch (error) {
if (this._isMounted) {
this.setState({
isSearching: false,
});
}
toastNotifications.addDanger({
notifications.toasts.addDanger({
title: i18n.translate(
'kbn.management.objects.objectsTable.unableFindSavedObjectsNotificationMessage',
'savedObjectsManagement.objectsTable.unableFindSavedObjectsNotificationMessage',
{ defaultMessage: 'Unable find saved objects' }
),
text: `${error}`,
});
return;
}
if (!this._isMounted) {
return;
}
this.setState(({ activeQuery }) => {
// ignore results for old requests
if (activeQuery.text !== query.text) {
return {};
}
return {
savedObjects: resp.savedObjects,
filteredItemCount: resp.total,
isSearching: false,
};
});
}, 300);
refreshData = async () => {
await Promise.all([this.fetchSavedObjects(), this.fetchCounts()]);
};
onSelectionChanged = selection => {
onSelectionChanged = (selection: SavedObjectWithMetadata[]) => {
this.setState({ selectedSavedObjects: selection });
};
onQueryChange = ({ query }) => {
onQueryChange = ({ query }: { query: Query }) => {
// TODO: Use isSameQuery to compare new query with state.activeQuery to avoid re-fetching the
// same data we already have.
this.setState(
@ -253,7 +280,7 @@ export class ObjectsTable extends Component {
);
};
onTableChange = async table => {
onTableChange = async (table: any) => {
const { index: page, size: perPage } = table.page || {};
this.setState(
@ -266,7 +293,7 @@ export class ObjectsTable extends Component {
);
};
onShowRelationships = object => {
onShowRelationships = (object: SavedObjectWithMetadata) => {
this.setState({
isShowingRelationships: true,
relationshipObject: object,
@ -280,16 +307,17 @@ export class ObjectsTable extends Component {
});
};
onExport = async includeReferencesDeep => {
onExport = async (includeReferencesDeep: boolean) => {
const { selectedSavedObjects } = this.state;
const { notifications, http } = this.props;
const objectsToExport = selectedSavedObjects.map(obj => ({ id: obj.id, type: obj.type }));
let blob;
try {
blob = await fetchExportObjects(objectsToExport, includeReferencesDeep);
blob = await fetchExportObjects(http, objectsToExport, includeReferencesDeep);
} catch (e) {
toastNotifications.addDanger({
title: i18n.translate('kbn.management.objects.objectsTable.export.dangerNotification', {
notifications.toasts.addDanger({
title: i18n.translate('savedObjectsManagement.objectsTable.export.dangerNotification', {
defaultMessage: 'Unable to generate export',
}),
});
@ -304,24 +332,26 @@ export class ObjectsTable extends Component {
onExportAll = async () => {
const { exportAllSelectedOptions, isIncludeReferencesDeepChecked, activeQuery } = this.state;
const { notifications, http } = this.props;
const { queryText } = parseQuery(activeQuery);
const exportTypes = Object.entries(exportAllSelectedOptions).reduce((accum, [id, selected]) => {
if (selected) {
accum.push(id);
}
return accum;
}, []);
}, [] as string[]);
let blob;
try {
blob = await fetchExportByTypeAndSearch(
http,
exportTypes,
queryText ? `${queryText}*` : undefined,
isIncludeReferencesDeepChecked
);
} catch (e) {
toastNotifications.addDanger({
title: i18n.translate('kbn.management.objects.objectsTable.export.dangerNotification', {
notifications.toasts.addDanger({
title: i18n.translate('savedObjectsManagement.objectsTable.export.dangerNotification', {
defaultMessage: 'Unable to generate export',
}),
});
@ -335,11 +365,12 @@ export class ObjectsTable extends Component {
this.setState({ isShowingExportAllOptionsModal: false });
};
showExportSuccessMessage = exportDetails => {
showExportSuccessMessage = (exportDetails: SavedObjectsExportResultDetails | undefined) => {
const { notifications } = this.props;
if (exportDetails && exportDetails.missingReferences.length > 0) {
toastNotifications.addWarning({
notifications.toasts.addWarning({
title: i18n.translate(
'kbn.management.objects.objectsTable.export.successWithMissingRefsNotification',
'savedObjectsManagement.objectsTable.export.successWithMissingRefsNotification',
{
defaultMessage:
'Your file is downloading in the background. ' +
@ -349,8 +380,8 @@ export class ObjectsTable extends Component {
),
});
} else {
toastNotifications.addSuccess({
title: i18n.translate('kbn.management.objects.objectsTable.export.successNotification', {
notifications.toasts.addSuccess({
title: i18n.translate('savedObjectsManagement.objectsTable.export.successNotification', {
defaultMessage: 'Your file is downloading in the background',
}),
});
@ -412,30 +443,30 @@ export class ObjectsTable extends Component {
});
};
getRelationships = async (type, id) => {
return await getRelationships(
type,
id,
this.savedObjectTypes,
this.props.$http,
this.props.basePath
);
getRelationships = async (type: string, id: string) => {
const { allowedTypes, http } = this.props;
return await getRelationships(http, type, id, allowedTypes);
};
renderFlyout() {
if (!this.state.isShowingImportFlyout) {
return null;
}
const { applications } = this.props;
const newIndexPatternUrl = applications.getUrlForApp('kibana', {
path: '#/management/kibana/index_pattern',
});
return (
<Flyout
close={this.hideImportFlyout}
done={this.finishImport}
services={this.props.services}
http={this.props.http}
serviceRegistry={this.props.serviceRegistry}
indexPatterns={this.props.indexPatterns}
newIndexPatternUrl={this.props.newIndexPatternUrl}
savedObjectTypes={this.props.savedObjectTypes}
confirmModalPromise={this.props.confirmModalPromise}
newIndexPatternUrl={newIndexPatternUrl}
allowedTypes={this.props.allowedTypes}
overlays={this.props.overlays}
/>
);
}
@ -447,10 +478,10 @@ export class ObjectsTable extends Component {
return (
<Relationships
savedObject={this.state.relationshipObject}
basePath={this.props.http.basePath}
savedObject={this.state.relationshipObject!}
getRelationships={this.getRelationships}
close={this.onHideRelationships}
getDashboardUrl={this.props.getDashboardUrl}
goInspectObject={this.props.goInspectObject}
canGoInApp={this.props.canGoInApp}
/>
@ -482,7 +513,7 @@ export class ObjectsTable extends Component {
<EuiConfirmModal
title={
<FormattedMessage
id="kbn.management.objects.objectsTable.deleteSavedObjectsConfirmModalTitle"
id="savedObjectsManagement.objectsTable.deleteSavedObjectsConfirmModalTitle"
defaultMessage="Delete saved objects"
/>
}
@ -491,19 +522,19 @@ export class ObjectsTable extends Component {
buttonColor="danger"
cancelButtonText={
<FormattedMessage
id="kbn.management.objects.objectsTable.deleteSavedObjectsConfirmModal.cancelButtonLabel"
id="savedObjectsManagement.objectsTable.deleteSavedObjectsConfirmModal.cancelButtonLabel"
defaultMessage="Cancel"
/>
}
confirmButtonText={
isDeleting ? (
<FormattedMessage
id="kbn.management.objects.objectsTable.deleteSavedObjectsConfirmModal.deleteProcessButtonLabel"
id="savedObjectsManagement.objectsTable.deleteSavedObjectsConfirmModal.deleteProcessButtonLabel"
defaultMessage="Deleting…"
/>
) : (
<FormattedMessage
id="kbn.management.objects.objectsTable.deleteSavedObjectsConfirmModal.deleteButtonLabel"
id="savedObjectsManagement.objectsTable.deleteSavedObjectsConfirmModal.deleteButtonLabel"
defaultMessage="Delete"
/>
)
@ -512,7 +543,7 @@ export class ObjectsTable extends Component {
>
<p>
<FormattedMessage
id="kbn.management.objects.deleteSavedObjectsConfirmModalDescription"
id="savedObjectsManagement.deleteSavedObjectsConfirmModalDescription"
defaultMessage="This action will delete the following saved objects:"
/>
</p>
@ -522,7 +553,7 @@ export class ObjectsTable extends Component {
{
field: 'type',
name: i18n.translate(
'kbn.management.objects.objectsTable.deleteSavedObjectsConfirmModal.typeColumnName',
'savedObjectsManagement.objectsTable.deleteSavedObjectsConfirmModal.typeColumnName',
{ defaultMessage: 'Type' }
),
width: '50px',
@ -535,14 +566,14 @@ export class ObjectsTable extends Component {
{
field: 'id',
name: i18n.translate(
'kbn.management.objects.objectsTable.deleteSavedObjectsConfirmModal.idColumnName',
'savedObjectsManagement.objectsTable.deleteSavedObjectsConfirmModal.idColumnName',
{ defaultMessage: 'Id' }
),
},
{
field: 'meta.title',
name: i18n.translate(
'kbn.management.objects.objectsTable.deleteSavedObjectsConfirmModal.titleColumnName',
'savedObjectsManagement.objectsTable.deleteSavedObjectsConfirmModal.titleColumnName',
{ defaultMessage: 'Title' }
),
},
@ -586,7 +617,7 @@ export class ObjectsTable extends Component {
<EuiModalHeader>
<EuiModalHeaderTitle>
<FormattedMessage
id="kbn.management.objects.objectsTable.exportObjectsConfirmModalTitle"
id="savedObjectsManagement.objectsTable.exportObjectsConfirmModalTitle"
defaultMessage="Export {filteredItemCount, plural, one{# object} other {# objects}}"
values={{
filteredItemCount,
@ -598,7 +629,7 @@ export class ObjectsTable extends Component {
<EuiFormRow
label={
<FormattedMessage
id="kbn.management.objects.objectsTable.exportObjectsConfirmModalDescription"
id="savedObjectsManagement.objectsTable.exportObjectsConfirmModalDescription"
defaultMessage="Select which types to export"
/>
}
@ -626,7 +657,7 @@ export class ObjectsTable extends Component {
name="includeReferencesDeep"
label={
<FormattedMessage
id="kbn.management.objects.objectsTable.exportObjectsConfirmModal.includeReferencesDeepLabel"
id="savedObjectsManagement.objectsTable.exportObjectsConfirmModal.includeReferencesDeepLabel"
defaultMessage="Include related objects"
/>
}
@ -641,7 +672,7 @@ export class ObjectsTable extends Component {
<EuiFlexItem grow={false}>
<EuiButtonEmpty onClick={this.closeExportAllModal}>
<FormattedMessage
id="kbn.management.objects.objectsTable.exportObjectsConfirmModal.cancelButtonLabel"
id="savedObjectsManagement.objectsTable.exportObjectsConfirmModal.cancelButtonLabel"
defaultMessage="Cancel"
/>
</EuiButtonEmpty>
@ -649,7 +680,7 @@ export class ObjectsTable extends Component {
<EuiFlexItem grow={false}>
<EuiButton fill onClick={this.onExportAll}>
<FormattedMessage
id="kbn.management.objects.objectsTable.exportObjectsConfirmModal.exportAllButtonLabel"
id="savedObjectsManagement.objectsTable.exportObjectsConfirmModal.exportAllButtonLabel"
defaultMessage="Export all"
/>
</EuiButton>
@ -673,12 +704,13 @@ export class ObjectsTable extends Component {
isSearching,
savedObjectCounts,
} = this.state;
const { http, allowedTypes, applications } = this.props;
const selectionConfig = {
onSelectionChange: this.onSelectionChanged,
};
const filterOptions = this.savedObjectTypes.map(type => ({
const filterOptions = allowedTypes.map(type => ({
value: type,
name: type,
view: `${type} (${savedObjectCounts[type] || 0})`,
@ -698,14 +730,16 @@ export class ObjectsTable extends Component {
/>
<EuiSpacer size="xs" />
<Table
basePath={http.basePath}
itemId={'id'}
actionRegistry={this.props.actionRegistry}
selectionConfig={selectionConfig}
selectedSavedObjects={selectedSavedObjects}
onQueryChange={this.onQueryChange}
onTableChange={this.onTableChange}
filterOptions={filterOptions}
onExport={this.onExport}
canDelete={this.props.uiCapabilities.savedObjectsManagement.delete}
canDelete={applications.capabilities.savedObjectsManagement.delete as boolean}
onDelete={this.onDelete}
goInspectObject={this.props.goInspectObject}
pageIndex={page}

View file

@ -0,0 +1,38 @@
/*
* 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 { SavedObjectReference } from '../../../../core/types';
export interface ObjectField {
type: FieldType;
name: string;
value: any;
}
export type FieldType = 'text' | 'number' | 'boolean' | 'array' | 'json';
export interface FieldState {
value?: any;
invalid?: boolean;
}
export interface SubmittedFormData {
attributes: any;
references: SavedObjectReference[];
}

View file

@ -17,23 +17,27 @@
* under the License.
*/
import { actionRegistryMock } from './services/action_registry.mock';
import { actionServiceMock } from './services/action_service.mock';
import { serviceRegistryMock } from './services/service_registry.mock';
import { SavedObjectsManagementPluginSetup, SavedObjectsManagementPluginStart } from './plugin';
const createSetupContractMock = (): jest.Mocked<SavedObjectsManagementPluginSetup> => {
const mock = {
actionRegistry: actionRegistryMock.create(),
actions: actionServiceMock.createSetup(),
serviceRegistry: serviceRegistryMock.create(),
};
return mock;
};
const createStartContractMock = (): jest.Mocked<SavedObjectsManagementPluginStart> => {
const mock = {};
const mock = {
actions: actionServiceMock.createStart(),
};
return mock;
};
export const savedObjectsManagementPluginMock = {
createActionRegistry: actionRegistryMock.create,
createServiceRegistry: serviceRegistryMock.create,
createSetupContract: createSetupContractMock,
createStartContract: createStartContractMock,
};

View file

@ -20,6 +20,9 @@
import { coreMock } from '../../../core/public/mocks';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { homePluginMock } from '../../home/public/mocks';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { managementPluginMock } from '../../management/public/mocks';
import { dataPluginMock } from '../../data/public/mocks';
import { SavedObjectsManagementPlugin } from './plugin';
describe('SavedObjectsManagementPlugin', () => {
@ -31,10 +34,13 @@ describe('SavedObjectsManagementPlugin', () => {
describe('#setup', () => {
it('registers the saved_objects feature to the home plugin', async () => {
const coreSetup = coreMock.createSetup();
const coreSetup = coreMock.createSetup({
pluginStartDeps: { data: dataPluginMock.createStartContract() },
});
const homeSetup = homePluginMock.createSetupContract();
const managementSetup = managementPluginMock.createSetupContract();
await plugin.setup(coreSetup, { home: homeSetup });
await plugin.setup(coreSetup, { home: homeSetup, management: managementSetup });
expect(homeSetup.featureCatalogue.register).toHaveBeenCalledTimes(1);
expect(homeSetup.featureCatalogue.register).toHaveBeenCalledWith(

View file

@ -19,37 +19,59 @@
import { i18n } from '@kbn/i18n';
import { CoreSetup, CoreStart, Plugin } from 'src/core/public';
import { ManagementSetup } from '../../management/public';
import { DataPublicPluginStart } from '../../data/public';
import { DashboardStart } from '../../dashboard/public';
import { DiscoverStart } from '../../discover/public';
import { HomePublicPluginSetup, FeatureCatalogueCategory } from '../../home/public';
import { VisualizationsStart } from '../../visualizations/public';
import {
SavedObjectsManagementActionRegistry,
ISavedObjectsManagementActionRegistry,
SavedObjectsManagementActionService,
SavedObjectsManagementActionServiceSetup,
SavedObjectsManagementActionServiceStart,
SavedObjectsManagementServiceRegistry,
ISavedObjectsManagementServiceRegistry,
} from './services';
import { registerServices } from './register_services';
export interface SavedObjectsManagementPluginSetup {
actionRegistry: ISavedObjectsManagementActionRegistry;
actions: SavedObjectsManagementActionServiceSetup;
serviceRegistry: ISavedObjectsManagementServiceRegistry;
}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface SavedObjectsManagementPluginStart {}
export interface SavedObjectsManagementPluginStart {
actions: SavedObjectsManagementActionServiceStart;
}
export interface SetupDependencies {
management: ManagementSetup;
home: HomePublicPluginSetup;
}
export interface StartDependencies {
data: DataPublicPluginStart;
dashboard?: DashboardStart;
visualizations?: VisualizationsStart;
discover?: DiscoverStart;
}
export class SavedObjectsManagementPlugin
implements
Plugin<
SavedObjectsManagementPluginSetup,
SavedObjectsManagementPluginStart,
SetupDependencies,
{}
StartDependencies
> {
private actionRegistry = new SavedObjectsManagementActionRegistry();
private actionService = new SavedObjectsManagementActionService();
private serviceRegistry = new SavedObjectsManagementServiceRegistry();
public setup(
core: CoreSetup<{}>,
{ home }: SetupDependencies
core: CoreSetup<StartDependencies, SavedObjectsManagementPluginStart>,
{ home, management }: SetupDependencies
): SavedObjectsManagementPluginSetup {
const actionSetup = this.actionService.setup();
home.featureCatalogue.register({
id: 'saved_objects',
title: i18n.translate('savedObjectsManagement.objects.savedObjectsTitle', {
@ -65,12 +87,39 @@ export class SavedObjectsManagementPlugin
category: FeatureCatalogueCategory.ADMIN,
});
const kibanaSection = management.sections.getSection('kibana');
if (!kibanaSection) {
throw new Error('`kibana` management section not found.');
}
kibanaSection.registerApp({
id: 'objects',
title: i18n.translate('savedObjectsManagement.managementSectionLabel', {
defaultMessage: 'Saved Objects',
}),
order: 10,
mount: async mountParams => {
const { mountManagementSection } = await import('./management_section');
return mountManagementSection({
core,
serviceRegistry: this.serviceRegistry,
mountParams,
});
},
});
// depends on `getStartServices`, should not be awaited
registerServices(this.serviceRegistry, core.getStartServices);
return {
actionRegistry: this.actionRegistry,
actions: actionSetup,
serviceRegistry: this.serviceRegistry,
};
}
public start(core: CoreStart) {
return {};
const actionStart = this.actionService.start();
return {
actions: actionStart,
};
}
}

View file

@ -0,0 +1,59 @@
/*
* 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 { StartServicesAccessor } from '../../../core/public';
import { SavedObjectsManagementPluginStart, StartDependencies } from './plugin';
import { ISavedObjectsManagementServiceRegistry } from './services';
export const registerServices = async (
registry: ISavedObjectsManagementServiceRegistry,
getStartServices: StartServicesAccessor<StartDependencies, SavedObjectsManagementPluginStart>
) => {
const [coreStart, { dashboard, data, visualizations, discover }] = await getStartServices();
if (dashboard) {
registry.register({
id: 'savedDashboards',
title: 'dashboards',
service: dashboard.getSavedDashboardLoader(),
});
}
if (visualizations) {
registry.register({
id: 'savedVisualizations',
title: 'visualizations',
service: visualizations.savedVisualizationsLoader,
});
}
if (discover) {
registry.register({
id: 'savedSearches',
title: 'searches',
service: discover.savedSearches.createLoader({
savedObjectsClient: coreStart.savedObjects.client,
indexPatterns: data.indexPatterns,
search: data.search,
chrome: coreStart.chrome,
overlays: coreStart.overlays,
}),
});
}
};

View file

@ -0,0 +1,57 @@
/*
* 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 {
SavedObjectsManagementActionService,
SavedObjectsManagementActionServiceSetup,
SavedObjectsManagementActionServiceStart,
} from './action_service';
const createSetupMock = (): jest.Mocked<SavedObjectsManagementActionServiceSetup> => {
const mock = {
register: jest.fn(),
};
return mock;
};
const createStartMock = (): jest.Mocked<SavedObjectsManagementActionServiceStart> => {
const mock = {
has: jest.fn(),
getAll: jest.fn(),
};
mock.has.mockReturnValue(true);
mock.getAll.mockReturnValue([]);
return mock;
};
const createServiceMock = (): jest.Mocked<PublicMethodsOf<SavedObjectsManagementActionService>> => {
const mock = {
setup: jest.fn().mockReturnValue(createSetupMock()),
start: jest.fn().mockReturnValue(createStartMock()),
};
return mock;
};
export const actionServiceMock = {
create: createServiceMock,
createSetup: createSetupMock,
createStart: createStartMock,
};

View file

@ -17,8 +17,11 @@
* under the License.
*/
import { SavedObjectsManagementActionRegistry } from './action_registry';
import { SavedObjectsManagementAction } from './action_types';
import {
SavedObjectsManagementActionService,
SavedObjectsManagementActionServiceSetup,
} from './action_service';
import { SavedObjectsManagementAction } from './types';
class DummyAction extends SavedObjectsManagementAction {
constructor(public id: string) {
@ -36,27 +39,30 @@ class DummyAction extends SavedObjectsManagementAction {
}
describe('SavedObjectsManagementActionRegistry', () => {
let registry: SavedObjectsManagementActionRegistry;
let service: SavedObjectsManagementActionService;
let setup: SavedObjectsManagementActionServiceSetup;
const createAction = (id: string): SavedObjectsManagementAction => {
return new DummyAction(id);
};
beforeEach(() => {
registry = new SavedObjectsManagementActionRegistry();
service = new SavedObjectsManagementActionService();
setup = service.setup();
});
describe('#register', () => {
it('allows actions to be registered and retrieved', () => {
const action = createAction('foo');
registry.register(action);
expect(registry.getAll()).toContain(action);
setup.register(action);
const start = service.start();
expect(start.getAll()).toContain(action);
});
it('does not allow actions with duplicate ids to be registered', () => {
const action = createAction('my-action');
registry.register(action);
expect(() => registry.register(action)).toThrowErrorMatchingInlineSnapshot(
setup.register(action);
expect(() => setup.register(action)).toThrowErrorMatchingInlineSnapshot(
`"Saved Objects Management Action with id 'my-action' already exists"`
);
});
@ -65,12 +71,14 @@ describe('SavedObjectsManagementActionRegistry', () => {
describe('#has', () => {
it('returns true when an action with a matching ID exists', () => {
const action = createAction('existing-action');
registry.register(action);
expect(registry.has('existing-action')).toEqual(true);
setup.register(action);
const start = service.start();
expect(start.has('existing-action')).toEqual(true);
});
it(`returns false when an action doesn't exist`, () => {
expect(registry.has('missing-action')).toEqual(false);
const start = service.start();
expect(start.has('missing-action')).toEqual(false);
});
});
});

View file

@ -17,36 +17,44 @@
* under the License.
*/
import { SavedObjectsManagementAction } from './action_types';
export type ISavedObjectsManagementActionRegistry = PublicMethodsOf<
SavedObjectsManagementActionRegistry
>;
export class SavedObjectsManagementActionRegistry {
private readonly actions = new Map<string, SavedObjectsManagementAction>();
import { SavedObjectsManagementAction } from './types';
export interface SavedObjectsManagementActionServiceSetup {
/**
* register given action in the registry.
*/
register(action: SavedObjectsManagementAction) {
if (this.actions.has(action.id)) {
throw new Error(`Saved Objects Management Action with id '${action.id}' already exists`);
}
this.actions.set(action.id, action);
}
register: (action: SavedObjectsManagementAction) => void;
}
export interface SavedObjectsManagementActionServiceStart {
/**
* return true if the registry contains given action, false otherwise.
*/
has(actionId: string) {
return this.actions.has(actionId);
}
has: (actionId: string) => boolean;
/**
* return all {@link SavedObjectsManagementAction | actions} currently registered.
*/
getAll() {
return [...this.actions.values()];
getAll: () => SavedObjectsManagementAction[];
}
export class SavedObjectsManagementActionService {
private readonly actions = new Map<string, SavedObjectsManagementAction>();
setup(): SavedObjectsManagementActionServiceSetup {
return {
register: action => {
if (this.actions.has(action.id)) {
throw new Error(`Saved Objects Management Action with id '${action.id}' already exists`);
}
this.actions.set(action.id, action);
},
};
}
start(): SavedObjectsManagementActionServiceStart {
return {
has: actionId => this.actions.has(actionId),
getAll: () => [...this.actions.values()],
};
}
}

View file

@ -18,7 +18,13 @@
*/
export {
SavedObjectsManagementActionRegistry,
ISavedObjectsManagementActionRegistry,
} from './action_registry';
export { SavedObjectsManagementAction, SavedObjectsManagementRecord } from './action_types';
SavedObjectsManagementActionService,
SavedObjectsManagementActionServiceStart,
SavedObjectsManagementActionServiceSetup,
} from './action_service';
export {
SavedObjectsManagementServiceRegistry,
ISavedObjectsManagementServiceRegistry,
SavedObjectsManagementServiceRegistryEntry,
} from './service_registry';
export { SavedObjectsManagementAction, SavedObjectsManagementRecord } from './types';

View file

@ -17,21 +17,20 @@
* under the License.
*/
import { ISavedObjectsManagementActionRegistry } from './action_registry';
import { ISavedObjectsManagementServiceRegistry } from './service_registry';
const createRegistryMock = (): jest.Mocked<ISavedObjectsManagementActionRegistry> => {
const createRegistryMock = (): jest.Mocked<ISavedObjectsManagementServiceRegistry> => {
const mock = {
register: jest.fn(),
has: jest.fn(),
getAll: jest.fn(),
all: jest.fn(),
get: jest.fn(),
};
mock.has.mockReturnValue(true);
mock.getAll.mockReturnValue([]);
mock.all.mockReturnValue([]);
return mock;
};
export const actionRegistryMock = {
export const serviceRegistryMock = {
create: createRegistryMock,
};

View file

@ -0,0 +1,49 @@
/*
* 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 { SavedObjectLoader } from '../../../saved_objects/public';
export interface SavedObjectsManagementServiceRegistryEntry {
id: string;
service: SavedObjectLoader;
title: string;
}
export type ISavedObjectsManagementServiceRegistry = PublicMethodsOf<
SavedObjectsManagementServiceRegistry
>;
export class SavedObjectsManagementServiceRegistry {
private readonly registry = new Map<string, SavedObjectsManagementServiceRegistryEntry>();
public register(entry: SavedObjectsManagementServiceRegistryEntry) {
if (this.registry.has(entry.id)) {
throw new Error('');
}
this.registry.set(entry.id, entry);
}
public all(): SavedObjectsManagementServiceRegistryEntry[] {
return [...this.registry.values()];
}
public get(id: string): SavedObjectsManagementServiceRegistryEntry | undefined {
return this.registry.get(id);
}
}

Some files were not shown because too many files have changed in this diff Show more