Skip to content

Commit 6986bbd

Browse files
Managed resources deletion
1 parent e0faf2a commit 6986bbd

File tree

11 files changed

+397
-54
lines changed

11 files changed

+397
-54
lines changed

public/locales/en.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,13 @@
3737
"tableHeaderName": "Name",
3838
"tableHeaderCreated": "Created",
3939
"tableHeaderSynced": "Synced",
40-
"tableHeaderReady": "Ready"
40+
"tableHeaderReady": "Ready",
41+
"tableHeaderDelete": "Delete",
42+
"deleteAction": "Delete resource",
43+
"deleteDialogTitle": "Delete resource",
44+
"advancedOptions": "Advanced options",
45+
"forceDeletion": "Force deletion",
46+
"forceWarningLine": "Force deletion removes finalizers. Related resources may not be deleted and cleanup may be skipped."
4147
},
4248
"ProvidersConfig": {
4349
"headerProviderConfigs": "Provider Configs",

src/components/ControlPlane/ManagedResources.tsx

Lines changed: 105 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,13 @@ import {
33
AnalyticalTable,
44
AnalyticalTableColumnDefinition,
55
AnalyticalTableScaleWidthMode,
6+
Button,
67
Title,
8+
Menu,
9+
MenuItem,
10+
MenuDomRef,
711
} from '@ui5/webcomponents-react';
8-
import { useApiResource } from '../../lib/api/useApiResource';
12+
import { useApiResource, useCRDItemsMapping } from '../../lib/api/useApiResource';
913
import { ManagedResourcesRequest } from '../../lib/api/types/crossplane/listManagedResources';
1014
import { formatDateAsTimeAgo } from '../../utils/i18n/timeAgo';
1115
import IllustratedError from '../Shared/IllustratedError';
@@ -14,10 +18,37 @@ import '@ui5/webcomponents-icons/dist/sys-cancel-2';
1418
import { resourcesInterval } from '../../lib/shared/constants';
1519

1620
import { YamlViewButton } from '../Yaml/YamlViewButton.tsx';
17-
import { useMemo } from 'react';
21+
import { FC, useMemo, useRef, useState } from 'react';
1822
import StatusFilter from '../Shared/StatusFilter/StatusFilter.tsx';
1923
import { ResourceStatusCell } from '../Shared/ResourceStatusCell.tsx';
2024
import { Resource } from '../../utils/removeManagedFieldsAndFilterData.ts';
25+
import { ManagedResourceItem } from '../../lib/shared/types.ts';
26+
import { ManagedResourceDeleteDialog } from '../Dialogs/ManagedResourceDeleteDialog.tsx';
27+
28+
const getItemKey = (item: ManagedResourceItem): string => `${item.kind}-${item.metadata.name}`;
29+
30+
const RowActionsMenu: FC<{
31+
item: ManagedResourceItem;
32+
onOpen: (item: ManagedResourceItem) => void;
33+
isDeleting: boolean;
34+
}> = ({ item, onOpen, isDeleting }) => {
35+
const { t } = useTranslation();
36+
const popoverRef = useRef<MenuDomRef>(null);
37+
38+
return (
39+
<>
40+
<Button icon="overflow" icon-end disabled={isDeleting} onClick={() => onOpen(item)} />
41+
<Menu
42+
ref={popoverRef}
43+
onItemClick={() => {
44+
onOpen(item);
45+
}}
46+
>
47+
<MenuItem text={t('ManagedResources.deleteAction')} icon="delete" />
48+
</Menu>
49+
</>
50+
);
51+
};
2152

2253
interface CellData<T> {
2354
cell: {
@@ -43,13 +74,30 @@ type ResourceRow = {
4374

4475
export function ManagedResources() {
4576
const { t } = useTranslation();
77+
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
78+
const [selectedItem, setSelectedItem] = useState<ManagedResourceItem | null>(null);
79+
const [deletingItems, setDeletingItems] = useState<Set<string>>(new Set());
4680

4781
const {
4882
data: managedResources,
4983
error,
5084
isLoading,
5185
} = useApiResource(ManagedResourcesRequest, {
52-
refreshInterval: resourcesInterval, // Resources are quite expensive to fetch, so we refresh every 30 seconds
86+
refreshInterval: resourcesInterval,
87+
});
88+
89+
const openDeleteDialog = (item: ManagedResourceItem) => {
90+
setSelectedItem(item);
91+
setDeleteDialogOpen(true);
92+
};
93+
94+
const handleDeleteStart = (item: ManagedResourceItem) => {
95+
const itemKey = getItemKey(item);
96+
setDeletingItems((prev) => new Set(prev.add(itemKey)));
97+
};
98+
99+
const { data: kindMapping } = useCRDItemsMapping({
100+
refreshInterval: resourcesInterval,
53101
});
54102

55103
const columns: AnalyticalTableColumnDefinition[] = useMemo(
@@ -106,20 +154,39 @@ export function ManagedResources() {
106154
width: 75,
107155
accessor: 'yaml',
108156
disableFilters: true,
109-
Cell: (cellData: CellData<ResourceRow>) =>
110-
cellData.cell.row.original?.item ? (
157+
Cell: (cellData: CellData<ResourceRow>) => {
158+
return cellData.cell.row.original?.item ? (
111159
<YamlViewButton variant="resource" resource={cellData.cell.row.original?.item as Resource} />
112-
) : undefined,
160+
) : undefined;
161+
},
162+
},
163+
{
164+
Header: ' ',
165+
hAlign: 'Center',
166+
width: 60,
167+
disableFilters: true,
168+
Cell: (cellData: CellData<ResourceRow>) => {
169+
const item = cellData.cell.row.original?.item as ManagedResourceItem;
170+
const itemKey = item ? getItemKey(item) : '';
171+
const isDeleting = deletingItems.has(itemKey);
172+
173+
return cellData.cell.row.original?.item ? (
174+
<RowActionsMenu item={item} isDeleting={isDeleting} onOpen={openDeleteDialog} />
175+
) : undefined;
176+
},
113177
},
114178
],
115-
[t],
179+
[t, deletingItems],
116180
);
117181

118182
const rows: ResourceRow[] =
119183
managedResources
120184
?.filter((managedResource) => managedResource.items)
121185
.flatMap((managedResource) =>
122186
managedResource.items?.map((item) => {
187+
const itemKey = getItemKey(item);
188+
const isDeleting = deletingItems.has(itemKey);
189+
123190
const conditionSynced = item.status?.conditions?.find((condition) => condition.type === 'Synced');
124191
const conditionReady = item.status?.conditions?.find((condition) => condition.type === 'Ready');
125192

@@ -134,6 +201,7 @@ export function ManagedResources() {
134201
item: item,
135202
conditionSyncedMessage: conditionSynced?.message ?? conditionSynced?.reason ?? '',
136203
conditionReadyMessage: conditionReady?.message ?? conditionReady?.reason ?? '',
204+
style: isDeleting ? { opacity: 0.5, pointerEvents: 'none' } : undefined,
137205
};
138206
}),
139207
) ?? [];
@@ -145,28 +213,36 @@ export function ManagedResources() {
145213
{error && <IllustratedError details={error.message} />}
146214

147215
{!error && (
148-
<AnalyticalTable
149-
columns={columns}
150-
data={rows}
151-
minRows={1}
152-
groupBy={['kind']}
153-
scaleWidthMode={AnalyticalTableScaleWidthMode.Smart}
154-
loading={isLoading}
155-
filterable
156-
// Prevent the table from resetting when the data changes
157-
retainColumnWidth
158-
reactTableOptions={{
159-
autoResetHiddenColumns: false,
160-
autoResetPage: false,
161-
autoResetExpanded: false,
162-
autoResetGroupBy: false,
163-
autoResetSelectedRows: false,
164-
autoResetSortBy: false,
165-
autoResetFilters: false,
166-
autoResetRowState: false,
167-
autoResetResize: false,
168-
}}
169-
/>
216+
<>
217+
<AnalyticalTable
218+
columns={columns}
219+
data={rows}
220+
minRows={1}
221+
groupBy={['kind']}
222+
scaleWidthMode={AnalyticalTableScaleWidthMode.Smart}
223+
loading={isLoading}
224+
filterable
225+
retainColumnWidth
226+
reactTableOptions={{
227+
autoResetHiddenColumns: false,
228+
autoResetPage: false,
229+
autoResetExpanded: false,
230+
autoResetGroupBy: false,
231+
autoResetSelectedRows: false,
232+
autoResetSortBy: false,
233+
autoResetFilters: false,
234+
autoResetRowState: false,
235+
autoResetResize: false,
236+
}}
237+
/>
238+
<ManagedResourceDeleteDialog
239+
kindMapping={kindMapping}
240+
open={deleteDialogOpen}
241+
item={selectedItem}
242+
onClose={() => setDeleteDialogOpen(false)}
243+
onDeleteStart={handleDeleteStart}
244+
/>
245+
</>
170246
)}
171247
</>
172248
);

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
}

src/components/Dialogs/DeleteConfirmationDialog.module.css renamed to src/components/Dialogs/DeleteConfirmationForm.module.css

File renamed without changes.
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)