Skip to content

Commit 0662013

Browse files
feat: deleting managed resources (#301)
Co-authored-by: Łukasz Goral <lukasz.goral@sap.com>
1 parent 56a7b66 commit 0662013

14 files changed

+460
-62
lines changed

public/locales/en.json

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,15 @@
3434
"tableHeaderName": "Name",
3535
"tableHeaderCreated": "Created",
3636
"tableHeaderSynced": "Synced",
37-
"tableHeaderReady": "Ready"
37+
"tableHeaderReady": "Ready",
38+
"tableHeaderDelete": "Delete",
39+
"deleteAction": "Delete resource",
40+
"deleteDialogTitle": "Delete resource",
41+
"advancedOptions": "Advanced options",
42+
"forceDeletion": "Force deletion",
43+
"forceWarningLine": "Force deletion removes finalizers. Related resources may not be deleted and cleanup may be skipped.",
44+
"deleteStarted": "Deleting {{resourceName}} initialized",
45+
"actionColumnHeader": " "
3846
},
3947
"ProvidersConfig": {
4048
"tableHeaderProvider": "Provider",

src/components/ControlPlane/ManagedResources.tsx

Lines changed: 103 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,28 @@ import {
88
Toolbar,
99
ToolbarSpacer,
1010
} from '@ui5/webcomponents-react';
11-
import { useApiResource } from '../../lib/api/useApiResource';
11+
import { useApiResource, useApiResourceMutation } from '../../lib/api/useApiResource';
1212
import { ManagedResourcesRequest } from '../../lib/api/types/crossplane/listManagedResources';
1313
import { formatDateAsTimeAgo } from '../../utils/i18n/timeAgo';
1414
import IllustratedError from '../Shared/IllustratedError';
15-
import '@ui5/webcomponents-icons/dist/sys-enter-2';
16-
import '@ui5/webcomponents-icons/dist/sys-cancel-2';
1715
import { resourcesInterval } from '../../lib/shared/constants';
1816

1917
import { YamlViewButton } from '../Yaml/YamlViewButton.tsx';
20-
import { useMemo } from 'react';
18+
import { useMemo, useState } from 'react';
2119
import StatusFilter from '../Shared/StatusFilter/StatusFilter.tsx';
2220
import { ResourceStatusCell } from '../Shared/ResourceStatusCell.tsx';
2321
import { Resource } from '../../utils/removeManagedFieldsAndFilterData.ts';
22+
import { ManagedResourceItem } from '../../lib/shared/types.ts';
23+
import { ManagedResourceDeleteDialog } from '../Dialogs/ManagedResourceDeleteDialog.tsx';
24+
import { RowActionsMenu } from './ManagedResourcesActionMenu.tsx';
25+
import { useToast } from '../../context/ToastContext.tsx';
26+
import {
27+
DeleteManagedResourceType,
28+
DeleteMCPManagedResource,
29+
PatchResourceForForceDeletion,
30+
PatchResourceForForceDeletionBody,
31+
} from '../../lib/api/types/crate/deleteResource';
32+
import { useResourcePluralNames } from '../../hooks/useResourcePluralNames';
2433

2534
interface CellData<T> {
2635
cell: {
@@ -46,15 +55,32 @@ type ResourceRow = {
4655

4756
export function ManagedResources() {
4857
const { t } = useTranslation();
58+
const toast = useToast();
59+
const [pendingDeleteItem, setPendingDeleteItem] = useState<ManagedResourceItem | null>(null);
4960

5061
const {
5162
data: managedResources,
5263
error,
5364
isLoading,
5465
} = useApiResource(ManagedResourcesRequest, {
55-
refreshInterval: resourcesInterval, // Resources are quite expensive to fetch, so we refresh every 30 seconds
66+
refreshInterval: resourcesInterval,
5667
});
5768

69+
const { getPluralKind, isLoading: isLoadingPluralNames, error: pluralNamesError } = useResourcePluralNames();
70+
71+
const resourceName = pendingDeleteItem?.metadata?.name ?? '';
72+
const apiVersion = pendingDeleteItem?.apiVersion ?? '';
73+
const pluralKind = pendingDeleteItem ? getPluralKind(pendingDeleteItem.kind) : '';
74+
const namespace = pendingDeleteItem?.metadata?.namespace ?? '';
75+
76+
const { trigger: deleteTrigger } = useApiResourceMutation<DeleteManagedResourceType>(
77+
DeleteMCPManagedResource(apiVersion, pluralKind, resourceName, namespace),
78+
);
79+
80+
const { trigger: patchTrigger } = useApiResourceMutation<DeleteManagedResourceType>(
81+
PatchResourceForForceDeletion(apiVersion, pluralKind, resourceName, namespace),
82+
);
83+
5884
const columns: AnalyticalTableColumnDefinition[] = useMemo(
5985
() => [
6086
{
@@ -109,10 +135,24 @@ export function ManagedResources() {
109135
width: 75,
110136
accessor: 'yaml',
111137
disableFilters: true,
112-
Cell: (cellData: CellData<ResourceRow>) =>
113-
cellData.cell.row.original?.item ? (
138+
Cell: (cellData: CellData<ResourceRow>) => {
139+
return cellData.cell.row.original?.item ? (
114140
<YamlViewButton variant="resource" resource={cellData.cell.row.original?.item as Resource} />
115-
) : undefined,
141+
) : undefined;
142+
},
143+
},
144+
{
145+
Header: t('ManagedResources.actionColumnHeader'),
146+
hAlign: 'Center',
147+
width: 60,
148+
disableFilters: true,
149+
Cell: (cellData: CellData<ResourceRow>) => {
150+
const item = cellData.cell.row.original?.item as ManagedResourceItem;
151+
152+
return cellData.cell.row.original?.item ? (
153+
<RowActionsMenu item={item} onOpen={openDeleteDialog} />
154+
) : undefined;
155+
},
116156
},
117157
],
118158
[t],
@@ -141,11 +181,34 @@ export function ManagedResources() {
141181
}),
142182
) ?? [];
143183

184+
const openDeleteDialog = (item: ManagedResourceItem) => {
185+
setPendingDeleteItem(item);
186+
};
187+
188+
const handleDeletionConfirmed = async (item: ManagedResourceItem, force: boolean) => {
189+
toast.show(t('ManagedResources.deleteStarted', { resourceName: item.metadata.name }));
190+
191+
try {
192+
await deleteTrigger();
193+
194+
if (force) {
195+
await patchTrigger(PatchResourceForForceDeletionBody);
196+
}
197+
} catch (_) {
198+
// Ignore errors - will be handled by the mutation hook
199+
} finally {
200+
setPendingDeleteItem(null);
201+
}
202+
};
203+
204+
const combinedError = error || pluralNamesError;
205+
const combinedLoading = isLoading || isLoadingPluralNames;
206+
144207
return (
145208
<>
146-
{error && <IllustratedError details={error.message} />}
209+
{combinedError && <IllustratedError details={combinedError.message} />}
147210

148-
{!error && (
211+
{!combinedError && (
149212
<Panel
150213
fixed
151214
header={
@@ -155,28 +218,36 @@ export function ManagedResources() {
155218
</Toolbar>
156219
}
157220
>
158-
<AnalyticalTable
159-
columns={columns}
160-
data={rows}
161-
minRows={1}
162-
groupBy={['kind']}
163-
scaleWidthMode={AnalyticalTableScaleWidthMode.Smart}
164-
loading={isLoading}
165-
filterable
166-
// Prevent the table from resetting when the data changes
167-
retainColumnWidth
168-
reactTableOptions={{
169-
autoResetHiddenColumns: false,
170-
autoResetPage: false,
171-
autoResetExpanded: false,
172-
autoResetGroupBy: false,
173-
autoResetSelectedRows: false,
174-
autoResetSortBy: false,
175-
autoResetFilters: false,
176-
autoResetRowState: false,
177-
autoResetResize: false,
178-
}}
179-
/>
221+
<>
222+
<AnalyticalTable
223+
columns={columns}
224+
data={rows}
225+
minRows={1}
226+
groupBy={['kind']}
227+
scaleWidthMode={AnalyticalTableScaleWidthMode.Smart}
228+
loading={combinedLoading}
229+
filterable
230+
retainColumnWidth
231+
reactTableOptions={{
232+
autoResetHiddenColumns: false,
233+
autoResetPage: false,
234+
autoResetExpanded: false,
235+
autoResetGroupBy: false,
236+
autoResetSelectedRows: false,
237+
autoResetSortBy: false,
238+
autoResetFilters: false,
239+
autoResetRowState: false,
240+
autoResetResize: false,
241+
}}
242+
/>
243+
244+
<ManagedResourceDeleteDialog
245+
open={!!pendingDeleteItem}
246+
item={pendingDeleteItem}
247+
onClose={() => setPendingDeleteItem(null)}
248+
onDeletionConfirmed={handleDeletionConfirmed}
249+
/>
250+
</>
180251
</Panel>
181252
)}
182253
</>
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { FC, useRef, useState } from 'react';
2+
import { Button, Menu, MenuItem, MenuDomRef } from '@ui5/webcomponents-react';
3+
import { useTranslation } from 'react-i18next';
4+
import { ManagedResourceItem } from '../../lib/shared/types';
5+
import type { ButtonClickEventDetail } from '@ui5/webcomponents/dist/Button.js';
6+
import type { Ui5CustomEvent, ButtonDomRef } from '@ui5/webcomponents-react';
7+
8+
interface RowActionsMenuProps {
9+
item: ManagedResourceItem;
10+
onOpen: (item: ManagedResourceItem) => void;
11+
}
12+
13+
export const RowActionsMenu: FC<RowActionsMenuProps> = ({ item, onOpen }) => {
14+
const { t } = useTranslation();
15+
const popoverRef = useRef<MenuDomRef>(null);
16+
const [open, setOpen] = useState(false);
17+
18+
const handleOpenerClick = (e: Ui5CustomEvent<ButtonDomRef, ButtonClickEventDetail>) => {
19+
if (popoverRef.current && e.currentTarget) {
20+
popoverRef.current.opener = e.currentTarget as unknown as HTMLElement;
21+
setOpen((prev) => !prev);
22+
}
23+
};
24+
25+
return (
26+
<>
27+
<Button icon="overflow" design="Transparent" onClick={handleOpenerClick} />
28+
<Menu
29+
ref={popoverRef}
30+
open={open}
31+
onItemClick={(event) => {
32+
const element = event.detail.item as HTMLElement;
33+
const action = element.dataset.action;
34+
if (action === 'delete') {
35+
onOpen(item);
36+
}
37+
setOpen(false);
38+
}}
39+
>
40+
<MenuItem text={t('ManagedResources.deleteAction')} icon="delete" data-action="delete" />
41+
</Menu>
42+
</>
43+
);
44+
};

src/components/Dialogs/DeleteConfirmationDialog.cy.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,15 +42,15 @@ describe('DeleteConfirmationDialog', () => {
4242
it('should enable Delete button when correct resource name is typed', () => {
4343
mountDialog();
4444

45-
cy.get('ui5-input[id*="mcp-name-input"]').find(' input[id*="inner"]').type('test-resource', { force: true });
45+
cy.get('ui5-input[id*="delete-confirm-input"]').find(' input[id*="inner"]').type('test-resource', { force: true });
4646

4747
cy.get('ui5-button').contains('Delete').should('not.have.attr', 'disabled');
4848
});
4949

5050
it('should keep Delete button disabled when incorrect name is typed', () => {
5151
mountDialog();
5252

53-
cy.get('ui5-input[id*="mcp-name-input"]').find(' input[id*="inner"]').type('wrong-name', { force: true });
53+
cy.get('ui5-input[id*="delete-confirm-input"]').find(' input[id*="inner"]').type('wrong-name', { force: true });
5454

5555
cy.get('ui5-button').contains('Delete').should('have.attr', 'disabled');
5656
});
@@ -68,7 +68,7 @@ describe('DeleteConfirmationDialog', () => {
6868
it('should call onDeletionConfirmed and setIsOpen when Delete is confirmed', () => {
6969
mountDialog();
7070

71-
cy.get('ui5-input[id*="mcp-name-input"]').find(' input[id*="inner"]').type('test-resource');
71+
cy.get('ui5-input[id*="delete-confirm-input"]').find(' input[id*="inner"]').type('test-resource');
7272

7373
cy.get('ui5-button').contains('Delete').click();
7474

@@ -82,15 +82,15 @@ describe('DeleteConfirmationDialog', () => {
8282
mountDialog();
8383

8484
// Type something
85-
cy.get('ui5-input[id*="mcp-name-input"]').find(' input[id*="inner"]').type('test-resource', { force: true });
85+
cy.get('ui5-input[id*="delete-confirm-input"]').find(' input[id*="inner"]').type('test-resource', { force: true });
8686

8787
// Close dialog
8888
cy.get('ui5-button').contains('Cancel').click();
8989

9090
// Reopen dialog
9191
mountDialog();
9292

93-
cy.get('ui5-input[id*="mcp-name-input"]').find(' input[id*="inner"]').should('have.value', '');
93+
cy.get('ui5-input[id*="delete-confirm-input"]').find(' input[id*="inner"]').should('have.value', '');
9494
});
9595

9696
it('should display correct resource name in all labels', () => {

src/components/Dialogs/DeleteConfirmationDialog.tsx

Lines changed: 10 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { ReactNode, useState } from 'react';
2-
import { Bar, Button, Dialog, Input, InputDomRef, Label } from '@ui5/webcomponents-react';
2+
import { Bar, Button, Dialog, InputDomRef } from '@ui5/webcomponents-react';
33
import ButtonDesign from '@ui5/webcomponents/dist/types/ButtonDesign.js';
4-
import { Trans, useTranslation } from 'react-i18next';
4+
import { useTranslation } from 'react-i18next';
55

6-
import styles from './DeleteConfirmationDialog.module.css';
76
import type { Ui5CustomEvent } from '@ui5/webcomponents-react-base';
7+
import { DeleteConfirmationForm } from './DeleteConfirmationForm.tsx';
88

99
interface DeleteConfirmationDialogProps {
1010
isOpen: boolean;
@@ -67,26 +67,13 @@ export function DeleteConfirmationDialog({
6767
/>
6868
}
6969
>
70-
<div className={styles.dialogContent}>
71-
<span className={styles.message}>
72-
<Trans
73-
i18nKey="DeleteConfirmationDialog.deleteMessage"
74-
values={{ resourceName }}
75-
components={{
76-
b: <b />,
77-
}}
78-
/>
79-
</span>
80-
<Label className={styles.confirmLabel} for="mcp-name-input">
81-
{t('DeleteConfirmationDialog.deleteConfirmation', { resourceName })}
82-
</Label>
83-
<Input
84-
id="mcp-name-input"
85-
value={confirmationText}
86-
className={styles.confirmationInput}
87-
onInput={onConfirmationInputChange}
88-
/>
89-
</div>
70+
<DeleteConfirmationForm
71+
resourceName={resourceName}
72+
confirmationText={confirmationText}
73+
deleteMessageKey="DeleteConfirmationDialog.deleteMessage"
74+
deleteConfirmationLabel={t('DeleteConfirmationDialog.deleteConfirmation', { resourceName })}
75+
onConfirmationInputChange={onConfirmationInputChange}
76+
/>
9077
</Dialog>
9178
);
9279
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { Trans } from 'react-i18next';
2+
import { Input, InputDomRef, Label, Ui5CustomEvent } from '@ui5/webcomponents-react';
3+
import styles from './DeleteConfirmationForm.module.css';
4+
5+
interface DeleteConfirmationFormProps {
6+
resourceName: string;
7+
confirmationText: string;
8+
onConfirmationInputChange: (event: Ui5CustomEvent<InputDomRef>) => void;
9+
deleteMessageKey: string;
10+
deleteConfirmationLabel: string;
11+
}
12+
13+
export function DeleteConfirmationForm({
14+
resourceName,
15+
confirmationText,
16+
onConfirmationInputChange,
17+
deleteMessageKey,
18+
deleteConfirmationLabel,
19+
}: DeleteConfirmationFormProps) {
20+
return (
21+
<div className={styles.dialogContent}>
22+
<span className={styles.message}>
23+
<Trans i18nKey={deleteMessageKey} values={{ resourceName }} components={{ b: <b /> }} />
24+
</span>
25+
<Label className={styles.confirmLabel} for="delete-confirm-input">
26+
{deleteConfirmationLabel}
27+
</Label>
28+
<Input
29+
id="delete-confirm-input"
30+
value={confirmationText}
31+
className={styles.confirmationInput}
32+
onInput={onConfirmationInputChange}
33+
/>
34+
</div>
35+
);
36+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
.dialog {
2+
width: 520px;
3+
}
4+
5+
.content {
6+
gap: 0.75rem;
7+
}
8+
9+
.advancedOptionsContent {
10+
gap: 0.5rem;
11+
}
12+
13+
.actions {
14+
gap: 0.5rem;
15+
}

0 commit comments

Comments
 (0)