[Saved Objects Management] Encapsulate saved objects deletion behind an API endpoint (#148602)

This commit is contained in:
Michael Dokolin 2023-01-19 15:05:07 +01:00 committed by GitHub
parent 2f7b933fbc
commit 091b15e52d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 323 additions and 170 deletions

View file

@ -0,0 +1,29 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { HttpStart } from '@kbn/core/public';
import { SavedObjectError, SavedObjectTypeIdTuple } from '@kbn/core-saved-objects-common';
interface SavedObjectDeleteStatus {
id: string;
success: boolean;
type: string;
error?: SavedObjectError;
}
export function bulkDeleteObjects(
http: HttpStart,
objects: SavedObjectTypeIdTuple[]
): Promise<SavedObjectDeleteStatus[]> {
return http.post<SavedObjectDeleteStatus[]>(
'/internal/kibana/management/saved_objects/_bulk_delete',
{
body: JSON.stringify(objects),
}
);
}

View file

@ -18,6 +18,7 @@ export type { ProcessedImportResponse, FailedImport } from './process_import_res
export { processImportResponse } from './process_import_response';
export { getDefaultTitle } from './get_default_title';
export { findObjects } from './find_objects';
export { bulkDeleteObjects } from './bulk_delete_objects';
export { bulkGetObjects } from './bulk_get_objects';
export type { SavedObjectsExportResultDetails } from './extract_export_details';
export { extractExportDetails } from './extract_export_details';

View file

@ -23,3 +23,8 @@ export const bulkGetObjectsMock = jest.fn();
jest.doMock('../../lib/bulk_get_objects', () => ({
bulkGetObjects: bulkGetObjectsMock,
}));
export const bulkDeleteObjectsMock = jest.fn();
jest.doMock('../../lib/bulk_delete_objects', () => ({
bulkDeleteObjects: bulkDeleteObjectsMock,
}));

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import { bulkGetObjectsMock } from './saved_object_view.test.mocks';
import { bulkDeleteObjectsMock, bulkGetObjectsMock } from './saved_object_view.test.mocks';
import React from 'react';
import { ShallowWrapper } from 'enzyme';
@ -16,13 +16,13 @@ import {
httpServiceMock,
overlayServiceMock,
notificationServiceMock,
savedObjectsServiceMock,
applicationServiceMock,
uiSettingsServiceMock,
scopedHistoryMock,
docLinksServiceMock,
} from '@kbn/core/public/mocks';
import type { SavedObjectWithMetadata } from '../../types';
import {
SavedObjectEdition,
SavedObjectEditionProps,
@ -36,7 +36,6 @@ describe('SavedObjectEdition', () => {
let http: ReturnType<typeof httpServiceMock.createStartContract>;
let overlays: ReturnType<typeof overlayServiceMock.createStartContract>;
let notifications: ReturnType<typeof notificationServiceMock.createStartContract>;
let savedObjects: ReturnType<typeof savedObjectsServiceMock.createStartContract>;
let uiSettings: ReturnType<typeof uiSettingsServiceMock.createStartContract>;
let history: ReturnType<typeof scopedHistoryMock.create>;
let applications: ReturnType<typeof applicationServiceMock.createStartContract>;
@ -56,7 +55,6 @@ describe('SavedObjectEdition', () => {
http = httpServiceMock.createStartContract();
overlays = overlayServiceMock.createStartContract();
notifications = notificationServiceMock.createStartContract();
savedObjects = savedObjectsServiceMock.createStartContract();
uiSettings = uiSettingsServiceMock.createStartContract();
history = scopedHistoryMock.create();
docLinks = docLinksServiceMock.createStartContract();
@ -81,35 +79,32 @@ describe('SavedObjectEdition', () => {
capabilities: applications.capabilities,
overlays,
notifications,
savedObjectsClient: savedObjects.client,
history,
uiSettings,
docLinks: docLinks.links,
};
bulkGetObjectsMock.mockImplementation(() => [{}]);
bulkDeleteObjectsMock.mockResolvedValue([{}]);
});
it('should render normally', async () => {
bulkGetObjectsMock.mockImplementation(() =>
Promise.resolve([
{
id: '1',
type: 'dashboard',
attributes: {
title: `MyDashboard*`,
},
meta: {
title: `MyDashboard*`,
icon: 'dashboardApp',
inAppUrl: {
path: '/app/dashboards#/view/1',
uiCapabilitiesPath: 'management.kibana.dashboard',
},
bulkGetObjectsMock.mockResolvedValue([
{
id: '1',
type: 'dashboard',
attributes: {
title: `MyDashboard*`,
},
meta: {
title: `MyDashboard*`,
icon: 'dashboardApp',
inAppUrl: {
path: '/app/dashboards#/view/1',
uiCapabilitiesPath: 'management.kibana.dashboard',
},
},
])
);
} as SavedObjectWithMetadata,
]);
const component = shallowRender();
// Ensure all promises resolve
await resolvePromises();
@ -119,15 +114,15 @@ describe('SavedObjectEdition', () => {
});
it('should add danger toast when bulk get fails', async () => {
bulkGetObjectsMock.mockImplementation(() =>
Promise.resolve([
{
error: {
message: 'Not found',
},
bulkGetObjectsMock.mockResolvedValue([
{
error: {
error: '',
message: 'Not found',
statusCode: 404,
},
])
);
} as SavedObjectWithMetadata,
]);
const component = shallowRender({ notFoundType: 'does_not_exist' });
await resolvePromises();
@ -165,8 +160,8 @@ describe('SavedObjectEdition', () => {
},
hiddenType: false,
},
};
bulkGetObjectsMock.mockImplementation(() => Promise.resolve([savedObjectItem]));
} as SavedObjectWithMetadata;
bulkGetObjectsMock.mockResolvedValue([savedObjectItem]);
applications.capabilities = {
navLinks: {},
management: {},
@ -232,14 +227,9 @@ describe('SavedObjectEdition', () => {
},
hiddenType: false,
},
};
} as SavedObjectWithMetadata;
it('should display a confirmation message on deleting the saved object', async () => {
bulkGetObjectsMock.mockImplementation(() => Promise.resolve([savedObjectItem]));
const mockSavedObjectsClient = {
...defaultProps.savedObjectsClient,
delete: jest.fn().mockImplementation(() => ({})),
};
beforeEach(() => {
applications.capabilities = {
navLinks: {},
management: {},
@ -250,13 +240,13 @@ describe('SavedObjectEdition', () => {
delete: true,
},
};
overlays.openConfirm.mockResolvedValue(false);
const component = shallowRender({
capabilities: applications.capabilities,
savedObjectsClient: mockSavedObjectsClient,
overlays,
});
});
it('should display a confirmation message on deleting the saved object', async () => {
bulkGetObjectsMock.mockResolvedValue([savedObjectItem]);
overlays.openConfirm.mockResolvedValue(false);
const component = shallowRender();
await resolvePromises();
component.update();
@ -272,28 +262,10 @@ describe('SavedObjectEdition', () => {
});
it('should route back if action is confirm and user accepted', async () => {
bulkGetObjectsMock.mockImplementation(() => Promise.resolve([savedObjectItem]));
const mockSavedObjectsClient = {
...defaultProps.savedObjectsClient,
delete: jest.fn().mockImplementation(() => ({})),
};
applications.capabilities = {
navLinks: {},
management: {},
catalogue: {},
savedObjectsManagement: {
read: true,
edit: false,
delete: true,
},
};
bulkGetObjectsMock.mockResolvedValue([savedObjectItem]);
overlays.openConfirm.mockResolvedValue(true);
const component = shallowRender({
capabilities: applications.capabilities,
savedObjectsClient: mockSavedObjectsClient,
overlays,
});
const component = shallowRender();
await resolvePromises();
component.update();
@ -303,27 +275,34 @@ describe('SavedObjectEdition', () => {
});
it('should not enable delete if the saved object is hidden', async () => {
bulkGetObjectsMock.mockImplementation(() =>
Promise.resolve([{ ...savedObjectItem, meta: { hiddenType: true } }])
);
applications.capabilities = {
navLinks: {},
management: {},
catalogue: {},
savedObjectsManagement: {
read: true,
edit: false,
delete: true,
},
};
const component = shallowRender({
capabilities: applications.capabilities,
});
bulkGetObjectsMock.mockResolvedValue([{ ...savedObjectItem, meta: { hiddenType: true } }]);
const component = shallowRender();
await resolvePromises();
component.update();
expect(component.find('Header').prop('canDelete')).toBe(false);
});
it('should show a danger toast when bulk deletion fails', async () => {
bulkGetObjectsMock.mockResolvedValue([savedObjectItem]);
bulkDeleteObjectsMock.mockResolvedValue([
{
error: { message: 'Something went wrong.' },
success: false,
},
]);
const component = shallowRender();
await resolvePromises();
component.update();
await component.instance().delete();
expect(notifications.toasts.addDanger).toHaveBeenCalledWith(
expect.objectContaining({
text: 'Something went wrong.',
})
);
});
});
});

View file

@ -13,7 +13,6 @@ import { get } from 'lodash';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import {
Capabilities,
SavedObjectsClientContract,
OverlayStart,
NotificationsStart,
ScopedHistory,
@ -22,7 +21,7 @@ import {
DocLinksStart,
} from '@kbn/core/public';
import { Header, Inspect, NotFoundErrors } from './components';
import { bulkGetObjects } from '../../lib/bulk_get_objects';
import { bulkDeleteObjects, bulkGetObjects } from '../../lib';
import { SavedObjectWithMetadata } from '../../types';
import './saved_object_view.scss';
export interface SavedObjectEditionProps {
@ -33,7 +32,6 @@ export interface SavedObjectEditionProps {
overlays: OverlayStart;
notifications: NotificationsStart;
notFoundType?: string;
savedObjectsClient: SavedObjectsClientContract;
history: ScopedHistory;
uiSettings: IUiSettingsClient;
docLinks: DocLinksStart['links'];
@ -129,7 +127,7 @@ export class SavedObjectEdition extends Component<
}
async delete() {
const { id, savedObjectsClient, overlays, notifications } = this.props;
const { http, id, overlays, notifications } = this.props;
const { type, object } = this.state;
const confirmed = await overlays.openConfirm(
@ -146,17 +144,37 @@ export class SavedObjectEdition extends Component<
title: i18n.translate('savedObjectsManagement.deleteConfirm.modalTitle', {
defaultMessage: `Delete '{title}'?`,
values: {
title: object?.attributes?.title || 'saved Kibana object',
title: object?.meta?.title || 'saved Kibana object',
},
}),
buttonColor: 'danger',
}
);
if (confirmed) {
await savedObjectsClient.delete(type, id);
notifications.toasts.addSuccess(`Deleted '${object!.attributes.title}' ${type} object`);
this.redirectToListing();
if (!confirmed) {
return;
}
const [{ success, error }] = await bulkDeleteObjects(http, [{ id, type }]);
if (!success) {
notifications.toasts.addDanger({
title: i18n.translate(
'savedObjectsManagement.objectView.unableDeleteSavedObjectNotificationMessage',
{
defaultMessage: `Failed to delete '{title}' {type} object`,
values: {
type,
title: object?.meta?.title,
},
}
),
text: error?.message,
});
return;
}
notifications.toasts.addSuccess(`Deleted '${object?.meta?.title}' ${type} object`);
this.redirectToListing();
}
redirectToListing() {

View file

@ -58,3 +58,13 @@ export const getRelationshipsMock = jest.fn();
jest.doMock('../../lib/get_relationships', () => ({
getRelationships: getRelationshipsMock,
}));
export const bulkGetObjectsMock = jest.fn();
jest.doMock('../../lib/bulk_get_objects', () => ({
bulkGetObjects: bulkGetObjectsMock,
}));
export const bulkDeleteObjectsMock = jest.fn();
jest.doMock('../../lib/bulk_delete_objects', () => ({
bulkDeleteObjects: bulkDeleteObjectsMock,
}));

View file

@ -7,6 +7,8 @@
*/
import {
bulkDeleteObjectsMock,
bulkGetObjectsMock,
extractExportDetailsMock,
fetchExportByTypeAndSearchMock,
fetchExportObjectsMock,
@ -17,6 +19,7 @@ import {
} from './saved_objects_table.test.mocks';
import React from 'react';
import { pick } from 'lodash';
import { Query } from '@elastic/eui';
import { ShallowWrapper } from 'enzyme';
import { shallowWithI18nProvider } from '@kbn/test-jest-helpers';
@ -24,7 +27,6 @@ import {
httpServiceMock,
overlayServiceMock,
notificationServiceMock,
savedObjectsServiceMock,
applicationServiceMock,
} from '@kbn/core/public/mocks';
import { dataPluginMock } from '@kbn/data-plugin/public/mocks';
@ -85,7 +87,6 @@ describe('SavedObjectsTable', () => {
let http: ReturnType<typeof httpServiceMock.createStartContract>;
let overlays: ReturnType<typeof overlayServiceMock.createStartContract>;
let notifications: ReturnType<typeof notificationServiceMock.createStartContract>;
let savedObjects: ReturnType<typeof savedObjectsServiceMock.createStartContract>;
let search: ReturnType<typeof dataPluginMock.createStartContract>['search'];
const shallowRender = (overrides: Partial<SavedObjectsTableProps> = {}) => {
@ -104,7 +105,6 @@ describe('SavedObjectsTable', () => {
http = httpServiceMock.createStartContract();
overlays = overlayServiceMock.createStartContract();
notifications = notificationServiceMock.createStartContract();
savedObjects = savedObjectsServiceMock.createStartContract();
search = dataPluginMock.createStartContract().search;
const applications = applicationServiceMock.createStartContract();
@ -132,7 +132,6 @@ describe('SavedObjectsTable', () => {
allowedTypes,
actionRegistry: actionServiceMock.createStart(),
columnRegistry: columnServiceMock.createStart(),
savedObjectsClient: savedObjects.client,
dataViews: dataViewPluginMocks.createStartContract(),
http,
overlays,
@ -236,15 +235,9 @@ describe('SavedObjectsTable', () => {
_id: obj.id,
_source: {},
}));
bulkGetObjectsMock.mockResolvedValue(mockSavedObjects);
const mockSavedObjectsClient = {
...defaultProps.savedObjectsClient,
bulkGet: jest.fn().mockImplementation(() => ({
savedObjects: mockSavedObjects,
})),
};
const component = shallowRender({ savedObjectsClient: mockSavedObjectsClient });
const component = shallowRender();
// Ensure all promises resolve
await new Promise((resolve) => process.nextTick(resolve));
@ -272,13 +265,7 @@ describe('SavedObjectsTable', () => {
_id: obj.id,
_source: {},
}));
const mockSavedObjectsClient = {
...defaultProps.savedObjectsClient,
bulkGet: jest.fn().mockImplementation(() => ({
savedObjects: mockSavedObjects,
})),
};
bulkGetObjectsMock.mockResolvedValue(mockSavedObjects);
extractExportDetailsMock.mockImplementation(() => ({
exportedCount: 2,
@ -288,7 +275,7 @@ describe('SavedObjectsTable', () => {
excludedObjects: [],
}));
const component = shallowRender({ savedObjectsClient: mockSavedObjectsClient });
const component = shallowRender();
// Ensure all promises resolve
await new Promise((resolve) => process.nextTick(resolve));
@ -319,13 +306,7 @@ describe('SavedObjectsTable', () => {
_id: obj.id,
_source: {},
}));
const mockSavedObjectsClient = {
...defaultProps.savedObjectsClient,
bulkGet: jest.fn().mockImplementation(() => ({
savedObjects: mockSavedObjects,
})),
};
bulkGetObjectsMock.mockResolvedValue(mockSavedObjects);
extractExportDetailsMock.mockImplementation(() => ({
exportedCount: 2,
@ -335,7 +316,7 @@ describe('SavedObjectsTable', () => {
excludedObjects: [{ id: '7', type: 'visualisation' }],
}));
const component = shallowRender({ savedObjectsClient: mockSavedObjectsClient });
const component = shallowRender();
// Ensure all promises resolve
await new Promise((resolve) => process.nextTick(resolve));
@ -553,6 +534,7 @@ describe('SavedObjectsTable', () => {
const mockSelectedSavedObjects = [
{ id: '1', type: 'index-pattern', meta: {} },
{ id: '3', type: 'dashboard', meta: {} },
{ id: '4', type: 'dashboard', meta: { hiddenType: false } },
] as SavedObjectWithMetadata[];
const mockSavedObjects = mockSelectedSavedObjects.map((obj) => ({
@ -560,16 +542,13 @@ describe('SavedObjectsTable', () => {
type: obj.type,
source: {},
}));
bulkGetObjectsMock.mockResolvedValue(mockSavedObjects);
bulkDeleteObjectsMock.mockResolvedValueOnce([
{ id: '1', type: 'index-pattern', success: true },
{ id: '3', type: 'dashboard', success: true },
]);
const mockSavedObjectsClient = {
...defaultProps.savedObjectsClient,
bulkGet: jest.fn().mockImplementation(() => ({
savedObjects: mockSavedObjects,
})),
delete: jest.fn(),
};
const component = shallowRender({ savedObjectsClient: mockSavedObjectsClient });
const component = shallowRender();
// Ensure all promises resolve
await new Promise((resolve) => process.nextTick(resolve));
@ -582,23 +561,20 @@ describe('SavedObjectsTable', () => {
await component.instance().delete();
expect(defaultProps.dataViews.clearCache).toHaveBeenCalled();
expect(mockSavedObjectsClient.delete).toHaveBeenCalledWith(
mockSavedObjects[0].type,
mockSavedObjects[0].id,
{ force: true }
);
expect(mockSavedObjectsClient.delete).toHaveBeenCalledWith(
mockSavedObjects[1].type,
mockSavedObjects[1].id,
{ force: true }
expect(bulkDeleteObjectsMock).toHaveBeenCalledWith(
expect.anything(),
expect.arrayContaining([
expect.objectContaining(pick(mockSavedObjects[0], 'id', 'type')),
expect.objectContaining(pick(mockSavedObjects[1], 'id', 'type')),
])
);
expect(component.state('selectedSavedObjects').length).toBe(0);
});
it('should not delete hidden selected objects', async () => {
it('should show a notification when deletion failed', async () => {
const mockSelectedSavedObjects = [
{ id: '1', type: 'index-pattern', meta: {} },
{ id: '3', type: 'hidden-type', meta: { hiddenType: true } },
{ id: '3', type: 'hidden-type', meta: {} },
] as SavedObjectWithMetadata[];
const mockSavedObjects = mockSelectedSavedObjects.map((obj) => ({
@ -606,16 +582,18 @@ describe('SavedObjectsTable', () => {
type: obj.type,
source: {},
}));
bulkGetObjectsMock.mockResolvedValue(mockSavedObjects);
bulkDeleteObjectsMock.mockResolvedValueOnce([
{ id: '1', type: 'index-pattern', success: true },
{
id: '3',
type: 'hidden-type',
success: false,
error: { message: 'Something went wrong.' },
},
]);
const mockSavedObjectsClient = {
...defaultProps.savedObjectsClient,
bulkGet: jest.fn().mockImplementation(() => ({
savedObjects: mockSavedObjects,
})),
delete: jest.fn(),
};
const component = shallowRender({ savedObjectsClient: mockSavedObjectsClient });
const component = shallowRender();
// Ensure all promises resolve
await new Promise((resolve) => process.nextTick(resolve));
@ -628,10 +606,11 @@ describe('SavedObjectsTable', () => {
await component.instance().delete();
expect(defaultProps.dataViews.clearCache).toHaveBeenCalled();
expect(mockSavedObjectsClient.delete).toHaveBeenCalledTimes(1);
expect(mockSavedObjectsClient.delete).toHaveBeenCalledWith('index-pattern', '1', {
force: true,
});
expect(notifications.toasts.addInfo).toHaveBeenCalledWith(
expect.objectContaining({
title: expect.stringContaining('1 object.'),
})
);
});
});
});

View file

@ -7,18 +7,12 @@
*/
import React, { Component } from 'react';
import { debounce } from 'lodash';
import { debounce, matches } from 'lodash';
// @ts-expect-error
import { saveAs } from '@elastic/filesaver';
import { EuiSpacer, Query, CriteriaWithPagination } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import {
SavedObjectsClientContract,
HttpStart,
OverlayStart,
NotificationsStart,
ApplicationStart,
} from '@kbn/core/public';
import { HttpStart, OverlayStart, NotificationsStart, ApplicationStart } from '@kbn/core/public';
import type { SavedObjectsFindOptions } from '@kbn/core-saved-objects-api-server';
import { RedirectAppLinks } from '@kbn/kibana-react-plugin/public';
import { SavedObjectsTaggingApi } from '@kbn/saved-objects-tagging-oss-plugin/public';
@ -32,6 +26,7 @@ import {
fetchExportObjects,
fetchExportByTypeAndSearch,
findObjects,
bulkDeleteObjects,
bulkGetObjects,
extractExportDetails,
SavedObjectsExportResultDetails,
@ -60,7 +55,6 @@ export interface SavedObjectsTableProps {
allowedTypes: SavedObjectManagementTypeInfo[];
actionRegistry: SavedObjectsManagementActionServiceStart;
columnRegistry: SavedObjectsManagementColumnServiceStart;
savedObjectsClient: SavedObjectsClientContract;
dataViews: DataViewsContract;
taggingApi?: SavedObjectsTaggingApi;
http: HttpStart;
@ -507,7 +501,7 @@ export class SavedObjectsTable extends Component<SavedObjectsTableProps, SavedOb
};
delete = async () => {
const { savedObjectsClient } = this.props;
const { http, notifications } = this.props;
const { selectedSavedObjects, isDeleting } = this.state;
if (isDeleting) {
@ -521,14 +515,27 @@ export class SavedObjectsTable extends Component<SavedObjectsTableProps, SavedOb
await this.props.dataViews.clearCache();
}
const deletes = selectedSavedObjects
.filter((object) => !object.meta.hiddenType)
.map((object) => savedObjectsClient.delete(object.type, object.id, { force: true }));
await Promise.all(deletes);
const deleteStatus = await bulkDeleteObjects(
http,
selectedSavedObjects
.filter((object) => !object.meta.hiddenType)
.map(({ id, type }) => ({ id, type }))
);
notifications.toasts.addInfo({
title: i18n.translate('savedObjectsManagement.objectsTable.delete.successNotification', {
defaultMessage: `Successfully deleted {count, plural, one {# object} other {# objects}}.`,
values: {
count: deleteStatus.filter(({ success }) => !!success).length,
},
}),
});
// Unset this
this.setState({
selectedSavedObjects: [],
selectedSavedObjects: selectedSavedObjects.filter(({ id, type }) =>
deleteStatus.some(matches({ id, type, success: false }))
),
});
// Fetching all data

View file

@ -57,7 +57,6 @@ const SavedObjectsEditionPage = ({
id={id}
savedObjectType={type}
http={coreStart.http}
savedObjectsClient={coreStart.savedObjects.client}
overlays={coreStart.overlays}
notifications={coreStart.notifications}
capabilities={capabilities}

View file

@ -83,7 +83,6 @@ const SavedObjectsTablePage = ({
actionRegistry={actionRegistry}
columnRegistry={columnRegistry}
taggingApi={taggingApi}
savedObjectsClient={coreStart.savedObjects.client}
dataViews={dataViewsApi}
search={dataStart.search}
http={coreStart.http}

View file

@ -0,0 +1,35 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { schema } from '@kbn/config-schema';
import { IRouter } from '@kbn/core/server';
export const registerBulkDeleteRoute = (router: IRouter) => {
router.post(
{
path: '/internal/kibana/management/saved_objects/_bulk_delete',
validate: {
body: schema.arrayOf(
schema.object({
type: schema.string(),
id: schema.string(),
})
),
},
},
router.handleLegacyErrors(async (context, req, res) => {
const { getClient } = (await context.core).savedObjects;
const objects = req.body;
const client = getClient();
const response = await client.bulkDelete(objects, { force: true });
return res.ok({ body: response.statuses });
})
);
};

View file

@ -24,7 +24,7 @@ describe('registerRoutes', () => {
expect(httpSetup.createRouter).toHaveBeenCalledTimes(1);
expect(router.get).toHaveBeenCalledTimes(3);
expect(router.post).toHaveBeenCalledTimes(2);
expect(router.post).toHaveBeenCalledTimes(3);
expect(router.get).toHaveBeenCalledWith(
expect.objectContaining({
@ -32,6 +32,12 @@ describe('registerRoutes', () => {
}),
expect.any(Function)
);
expect(router.post).toHaveBeenCalledWith(
expect.objectContaining({
path: '/internal/kibana/management/saved_objects/_bulk_delete',
}),
expect.any(Function)
);
expect(router.post).toHaveBeenCalledWith(
expect.objectContaining({
path: '/api/kibana/management/saved_objects/_bulk_get',

View file

@ -9,6 +9,7 @@
import { HttpServiceSetup } from '@kbn/core/server';
import { ISavedObjectsManagement } from '../services';
import { registerFindRoute } from './find';
import { registerBulkDeleteRoute } from './bulk_delete';
import { registerBulkGetRoute } from './bulk_get';
import { registerScrollForCountRoute } from './scroll_count';
import { registerRelationshipsRoute } from './relationships';
@ -22,6 +23,7 @@ interface RegisterRouteOptions {
export function registerRoutes({ http, managementServicePromise }: RegisterRouteOptions) {
const router = http.createRouter();
registerFindRoute(router, managementServicePromise);
registerBulkDeleteRoute(router);
registerBulkGetRoute(router, managementServicePromise);
registerScrollForCountRoute(router);
registerRelationshipsRoute(router, managementServicePromise);

View file

@ -22,6 +22,7 @@
"@kbn/i18n-react",
"@kbn/test-jest-helpers",
"@kbn/core-saved-objects-api-server",
"@kbn/core-saved-objects-common",
"@kbn/monaco",
"@kbn/config-schema",
],

View file

@ -0,0 +1,82 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import expect from '@kbn/expect';
import type { Response } from 'supertest';
import type { FtrProviderContext } from '../../ftr_provider_context';
export default function ({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
const kibanaServer = getService('kibanaServer');
describe('_bulk_delete', () => {
const endpoint = '/internal/kibana/management/saved_objects/_bulk_delete';
const validObject = { type: 'visualization', id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab' };
const invalidObject = { type: 'wigwags', id: 'foo' };
beforeEach(() =>
kibanaServer.importExport.load(
'test/api_integration/fixtures/kbn_archiver/saved_objects/basic.json'
)
);
afterEach(() =>
kibanaServer.importExport.unload(
'test/api_integration/fixtures/kbn_archiver/saved_objects/basic.json'
)
);
function expectSuccess(index: number, { body }: Response) {
const { type, id, error } = body[index];
expect(type).to.eql(validObject.type);
expect(id).to.eql(validObject.id);
expect(error).to.equal(undefined);
}
function expectBadRequest(index: number, { body }: Response) {
const { type, id, error } = body[index];
expect(type).to.eql(invalidObject.type);
expect(id).to.eql(invalidObject.id);
expect(error).to.eql({
message: `Unsupported saved object type: '${invalidObject.type}': Bad Request`,
statusCode: 400,
error: 'Bad Request',
});
}
it('should return 200 for an existing object', async () =>
await supertest
.post(endpoint)
.send([validObject])
.expect(200)
.then((response: Response) => {
expect(response.body).to.have.length(1);
expectSuccess(0, response);
}));
it('should return error for invalid object type', async () =>
await supertest
.post(endpoint)
.send([invalidObject])
.expect(200)
.then((response: Response) => {
expect(response.body).to.have.length(1);
expectBadRequest(0, response);
}));
it('should return mix of successes and errors', async () =>
await supertest
.post(endpoint)
.send([validObject, invalidObject])
.expect(200)
.then((response: Response) => {
expect(response.body).to.have.length(2);
expectSuccess(0, response);
expectBadRequest(1, response);
}));
});
}

View file

@ -11,6 +11,7 @@ import { FtrProviderContext } from '../../ftr_provider_context';
export default function ({ loadTestFile }: FtrProviderContext) {
describe('saved objects management apis', () => {
loadTestFile(require.resolve('./find'));
loadTestFile(require.resolve('./bulk_delete'));
loadTestFile(require.resolve('./bulk_get'));
loadTestFile(require.resolve('./relationships'));
loadTestFile(require.resolve('./scroll_count'));