Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 8 commits into from
Jan 19, 2023
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({
dokmic marked this conversation as resolved.
Show resolved Hide resolved
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