Skip to content

Commit

Permalink
[Saved Objects Management] Encapsulate saved objects deletion behind …
Browse files Browse the repository at this point in the history
…an API endpoint (#148602)
  • Loading branch information
dokmic authored Jan 19, 2023
1 parent 2f7b933 commit 091b15e
Show file tree
Hide file tree
Showing 16 changed files with 322 additions and 169 deletions.
Original file line number Diff line number Diff line change
@@ -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),
}
);
}
1 change: 1 addition & 0 deletions src/plugins/saved_objects_management/public/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}));
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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,
Expand All @@ -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>;
Expand All @@ -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();
Expand All @@ -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();
Expand All @@ -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();
Expand Down Expand Up @@ -165,8 +160,8 @@ describe('SavedObjectEdition', () => {
},
hiddenType: false,
},
};
bulkGetObjectsMock.mockImplementation(() => Promise.resolve([savedObjectItem]));
} as SavedObjectWithMetadata;
bulkGetObjectsMock.mockResolvedValue([savedObjectItem]);
applications.capabilities = {
navLinks: {},
management: {},
Expand Down Expand Up @@ -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: {},
Expand All @@ -250,13 +240,13 @@ describe('SavedObjectEdition', () => {
delete: true,
},
};
});

it('should display a confirmation message on deleting the saved object', async () => {
bulkGetObjectsMock.mockResolvedValue([savedObjectItem]);
overlays.openConfirm.mockResolvedValue(false);
const component = shallowRender({
capabilities: applications.capabilities,
savedObjectsClient: mockSavedObjectsClient,
overlays,
});

const component = shallowRender();
await resolvePromises();

component.update();
Expand All @@ -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();
Expand All @@ -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.',
})
);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import { get } from 'lodash';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import {
Capabilities,
SavedObjectsClientContract,
OverlayStart,
NotificationsStart,
ScopedHistory,
Expand All @@ -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 {
Expand All @@ -33,7 +32,6 @@ export interface SavedObjectEditionProps {
overlays: OverlayStart;
notifications: NotificationsStart;
notFoundType?: string;
savedObjectsClient: SavedObjectsClientContract;
history: ScopedHistory;
uiSettings: IUiSettingsClient;
docLinks: DocLinksStart['links'];
Expand Down Expand Up @@ -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(
Expand All @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}));
Loading

0 comments on commit 091b15e

Please sign in to comment.