diff --git a/frontend/src/lib/Buttons.ts b/frontend/src/lib/Buttons.ts
index ccc4ea776fc..ace2c205b47 100644
--- a/frontend/src/lib/Buttons.ts
+++ b/frontend/src/lib/Buttons.ts
@@ -148,7 +148,7 @@ export default class Buttons {
// or recurring run config.
public delete(
getSelectedIds: () => string[],
- resourceName: 'pipeline' | 'recurring run config' | 'pipeline version',
+ resourceName: 'pipeline' | 'recurring run config' | 'pipeline version' | 'run',
callback: (selectedIds: string[], success: boolean) => void,
useCurrentResource: boolean,
): Buttons {
@@ -158,6 +158,8 @@ export default class Buttons {
? this._deletePipeline(getSelectedIds(), useCurrentResource, callback)
: resourceName === 'pipeline version'
? this._deletePipelineVersion(getSelectedIds(), useCurrentResource, callback)
+ : resourceName === 'run'
+ ? this._deleteRun(getSelectedIds(), useCurrentResource, callback)
: this._deleteRecurringRun(getSelectedIds()[0], useCurrentResource, callback),
disabled: !useCurrentResource,
disabledTitle: useCurrentResource
@@ -497,6 +499,22 @@ export default class Buttons {
);
}
+ private _deleteRun(
+ ids: string[],
+ useCurrentResource: boolean,
+ callback: (_: string[], success: boolean) => void,
+ ): void {
+ this._dialogActionHandler(
+ ids,
+ 'Do you want to delete the selected runs? This action cannot be undone.',
+ useCurrentResource,
+ id => Apis.runServiceApi.deleteRun(id),
+ callback,
+ 'Delete',
+ 'run',
+ );
+ }
+
private _dialogActionHandler(
selectedIds: string[],
content: string,
diff --git a/frontend/src/pages/Archive.test.tsx b/frontend/src/pages/Archive.test.tsx
index f10ff0e1f55..39c104e4c9b 100644
--- a/frontend/src/pages/Archive.test.tsx
+++ b/frontend/src/pages/Archive.test.tsx
@@ -21,11 +21,15 @@ import { PageProps } from './Page';
import { RunStorageState } from '../apis/run';
import { ShallowWrapper, shallow } from 'enzyme';
import { ButtonKeys } from '../lib/Buttons';
+import { Apis } from '../lib/Apis';
describe('Archive', () => {
const updateBannerSpy = jest.fn();
const updateToolbarSpy = jest.fn();
const historyPushSpy = jest.fn();
+ const deleteRunSpy = jest.spyOn(Apis.runServiceApi, 'deleteRun');
+ const updateDialogSpy = jest.fn();
+ const updateSnackbarSpy = jest.fn();
let tree: ShallowWrapper;
function generateProps(): PageProps {
@@ -35,9 +39,9 @@ describe('Archive', () => {
{} as any,
historyPushSpy,
updateBannerSpy,
- null,
+ updateDialogSpy,
updateToolbarSpy,
- null,
+ updateSnackbarSpy,
);
}
@@ -45,6 +49,9 @@ describe('Archive', () => {
updateBannerSpy.mockClear();
updateToolbarSpy.mockClear();
historyPushSpy.mockClear();
+ deleteRunSpy.mockClear();
+ updateDialogSpy.mockClear();
+ updateSnackbarSpy.mockClear();
});
afterEach(() => tree.unmount());
@@ -60,17 +67,29 @@ describe('Archive', () => {
expect(updateBannerSpy).toHaveBeenCalledWith({});
});
- it('only enables restore button when at least one run is selected', () => {
+ it('enables restore and delete button when at least one run is selected', () => {
tree = shallow();
TestUtils.flushPromises();
tree.update();
expect(TestUtils.getToolbarButton(updateToolbarSpy, ButtonKeys.RESTORE).disabled).toBeTruthy();
+ expect(
+ TestUtils.getToolbarButton(updateToolbarSpy, ButtonKeys.DELETE_RUN).disabled,
+ ).toBeTruthy();
tree.find('RunList').simulate('selectionChange', ['run1']);
expect(TestUtils.getToolbarButton(updateToolbarSpy, ButtonKeys.RESTORE).disabled).toBeFalsy();
+ expect(
+ TestUtils.getToolbarButton(updateToolbarSpy, ButtonKeys.DELETE_RUN).disabled,
+ ).toBeFalsy();
tree.find('RunList').simulate('selectionChange', ['run1', 'run2']);
expect(TestUtils.getToolbarButton(updateToolbarSpy, ButtonKeys.RESTORE).disabled).toBeFalsy();
+ expect(
+ TestUtils.getToolbarButton(updateToolbarSpy, ButtonKeys.DELETE_RUN).disabled,
+ ).toBeFalsy();
tree.find('RunList').simulate('selectionChange', []);
expect(TestUtils.getToolbarButton(updateToolbarSpy, ButtonKeys.RESTORE).disabled).toBeTruthy();
+ expect(
+ TestUtils.getToolbarButton(updateToolbarSpy, ButtonKeys.DELETE_RUN).disabled,
+ ).toBeTruthy();
});
it('refreshes the run list when refresh button is clicked', async () => {
@@ -85,4 +104,64 @@ describe('Archive', () => {
tree = shallow();
expect(tree.find('RunList').prop('storageState')).toBe(RunStorageState.ARCHIVED.toString());
});
+
+ it('cancells deletion when Cancel is clicked', async () => {
+ tree = shallow();
+
+ // Click delete button to delete selected ids.
+ const deleteBtn = (tree.instance() as Archive).getInitialToolbarState().actions[
+ ButtonKeys.DELETE_RUN
+ ];
+ await deleteBtn!.action();
+
+ // Dialog pops up to confirm the deletion.
+ expect(updateDialogSpy).toHaveBeenCalledTimes(1);
+ expect(updateDialogSpy).toHaveBeenLastCalledWith(
+ expect.objectContaining({
+ content: 'Do you want to delete the selected runs? This action cannot be undone.',
+ }),
+ );
+
+ // Cancel deletion.
+ const call = updateDialogSpy.mock.calls[0][0];
+ const cancelBtn = call.buttons.find((b: any) => b.text === 'Cancel');
+ await cancelBtn.onClick();
+ expect(deleteRunSpy).not.toHaveBeenCalled();
+ });
+
+ it('deletes selected ids when Confirm is clicked', async () => {
+ tree = shallow();
+ tree.setState({ selectedIds: ['id1', 'id2', 'id3'] });
+
+ // Mock the behavior where the deletion of id1 fails, the deletion of id2 and id3 succeed.
+ TestUtils.makeErrorResponseOnce(deleteRunSpy, 'woops');
+ deleteRunSpy.mockImplementation(() => Promise.resolve({}));
+
+ // Click delete button to delete selected ids.
+ const deleteBtn = (tree.instance() as Archive).getInitialToolbarState().actions[
+ ButtonKeys.DELETE_RUN
+ ];
+ await deleteBtn!.action();
+
+ // Dialog pops up to confirm the deletion.
+ expect(updateDialogSpy).toHaveBeenCalledTimes(1);
+ expect(updateDialogSpy).toHaveBeenLastCalledWith(
+ expect.objectContaining({
+ content: 'Do you want to delete the selected runs? This action cannot be undone.',
+ }),
+ );
+
+ // Confirm.
+ const call = updateDialogSpy.mock.calls[0][0];
+ const confirmBtn = call.buttons.find((b: any) => b.text === 'Delete');
+ await confirmBtn.onClick();
+ await deleteRunSpy;
+ await TestUtils.flushPromises();
+ tree.update();
+ expect(deleteRunSpy).toHaveBeenCalledTimes(3);
+ expect(deleteRunSpy).toHaveBeenCalledWith('id1');
+ expect(deleteRunSpy).toHaveBeenCalledWith('id2');
+ expect(deleteRunSpy).toHaveBeenCalledWith('id3');
+ expect(tree.state('selectedIds')).toEqual(['id1']); // id1 is left over since its deletion failed.
+ });
});
diff --git a/frontend/src/pages/Archive.tsx b/frontend/src/pages/Archive.tsx
index bdfe2ff9654..10c6557509f 100644
--- a/frontend/src/pages/Archive.tsx
+++ b/frontend/src/pages/Archive.tsx
@@ -44,6 +44,12 @@ export default class Archive extends Page<{}, ArchiveState> {
actions: buttons
.restore(() => this.state.selectedIds, false, this._selectionChanged.bind(this))
.refresh(this.refresh.bind(this))
+ .delete(
+ () => this.state.selectedIds,
+ 'run',
+ this._selectionChanged.bind(this),
+ false /* useCurrentResource */,
+ )
.getToolbarActionMap(),
breadcrumbs: [],
pageTitle: 'Archive',
@@ -76,6 +82,7 @@ export default class Archive extends Page<{}, ArchiveState> {
private _selectionChanged(selectedIds: string[]): void {
const toolbarActions = this.props.toolbarProps.actions;
toolbarActions[ButtonKeys.RESTORE].disabled = !selectedIds.length;
+ toolbarActions[ButtonKeys.DELETE_RUN].disabled = !selectedIds.length;
this.props.updateToolbar({
actions: toolbarActions,
breadcrumbs: this.props.toolbarProps.breadcrumbs,
diff --git a/frontend/src/pages/__snapshots__/Archive.test.tsx.snap b/frontend/src/pages/__snapshots__/Archive.test.tsx.snap
index c7e1b82f852..23f34bfc98c 100644
--- a/frontend/src/pages/__snapshots__/Archive.test.tsx.snap
+++ b/frontend/src/pages/__snapshots__/Archive.test.tsx.snap
@@ -19,6 +19,14 @@ exports[`Archive renders archived runs 1`] = `
toolbarProps={
Object {
"actions": Object {
+ "deleteRun": Object {
+ "action": [Function],
+ "disabled": true,
+ "disabledTitle": "Select at least one run to delete",
+ "id": "deleteBtn",
+ "title": "Delete",
+ "tooltip": "Delete",
+ },
"refresh": Object {
"action": [Function],
"id": "refreshBtn",
@@ -39,8 +47,8 @@ exports[`Archive renders archived runs 1`] = `
}
}
updateBanner={[MockFunction]}
- updateDialog={null}
- updateSnackbar={null}
+ updateDialog={[MockFunction]}
+ updateSnackbar={[MockFunction]}
updateToolbar={[MockFunction]}
/>