mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Saved Objects Management] Encapsulate saved objects deletion behind an API endpoint (#148602)
This commit is contained in:
parent
2f7b933fbc
commit
091b15e52d
16 changed files with 323 additions and 170 deletions
|
@ -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),
|
||||
}
|
||||
);
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -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,
|
||||
}));
|
||||
|
|
|
@ -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.',
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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,
|
||||
}));
|
||||
|
|
|
@ -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.'),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -83,7 +83,6 @@ const SavedObjectsTablePage = ({
|
|||
actionRegistry={actionRegistry}
|
||||
columnRegistry={columnRegistry}
|
||||
taggingApi={taggingApi}
|
||||
savedObjectsClient={coreStart.savedObjects.client}
|
||||
dataViews={dataViewsApi}
|
||||
search={dataStart.search}
|
||||
http={coreStart.http}
|
||||
|
|
|
@ -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 });
|
||||
})
|
||||
);
|
||||
};
|
|
@ -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',
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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",
|
||||
],
|
||||
|
|
|
@ -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);
|
||||
}));
|
||||
});
|
||||
}
|
|
@ -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'));
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue