Skip to content

Commit d7e58eb

Browse files
PR changes
1 parent 6f9a53a commit d7e58eb

File tree

6 files changed

+120
-90
lines changed

6 files changed

+120
-90
lines changed

public/locales/en.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,9 @@
4343
"deleteDialogTitle": "Delete resource",
4444
"advancedOptions": "Advanced options",
4545
"forceDeletion": "Force deletion",
46-
"forceWarningLine": "Force deletion removes finalizers. Related resources may not be deleted and cleanup may be skipped."
46+
"forceWarningLine": "Force deletion removes finalizers. Related resources may not be deleted and cleanup may be skipped.",
47+
"deleteStarted": "Deleting {{resourceName}} initialized",
48+
"actionColumnHeader": " "
4749
},
4850
"ProvidersConfig": {
4951
"headerProviderConfigs": "Provider Configs",

src/components/ControlPlane/ManagedResources.module.css

Lines changed: 0 additions & 4 deletions
This file was deleted.

src/components/ControlPlane/ManagedResources.tsx

Lines changed: 53 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
AnalyticalTableScaleWidthMode,
66
Title,
77
} from '@ui5/webcomponents-react';
8-
import { useApiResource, useCRDItemsMapping } from '../../lib/api/useApiResource';
8+
import { useApiResource, useApiResourceMutation } from '../../lib/api/useApiResource';
99
import { ManagedResourcesRequest } from '../../lib/api/types/crossplane/listManagedResources';
1010
import { formatDateAsTimeAgo } from '../../utils/i18n/timeAgo';
1111
import IllustratedError from '../Shared/IllustratedError';
@@ -19,9 +19,13 @@ import { Resource } from '../../utils/removeManagedFieldsAndFilterData.ts';
1919
import { ManagedResourceItem } from '../../lib/shared/types.ts';
2020
import { ManagedResourceDeleteDialog } from '../Dialogs/ManagedResourceDeleteDialog.tsx';
2121
import { RowActionsMenu } from './ManagedResourcesActionMenu.tsx';
22-
import styles from './ManagedResources.module.css';
23-
24-
const getItemKey = (item: ManagedResourceItem): string => `${item.kind}-${item.metadata.name}`;
22+
import { useToast } from '../../context/ToastContext.tsx';
23+
import {
24+
DeleteManagedResourceType,
25+
DeleteMCPManagedResource,
26+
PatchResourceForForceDeletion,
27+
} from '../../lib/api/types/crate/deleteResource';
28+
import { useResourcePluralNames } from '../../hooks/useResourcePluralNames';
2529

2630
interface CellData<T> {
2731
cell: {
@@ -47,9 +51,8 @@ type ResourceRow = {
4751

4852
export function ManagedResources() {
4953
const { t } = useTranslation();
50-
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
51-
const [selectedItem, setSelectedItem] = useState<ManagedResourceItem | null>(null);
52-
const [deletingItems, setDeletingItems] = useState<Set<string>>(new Set());
54+
const toast = useToast();
55+
const [pendingDeleteItem, setPendingDeleteItem] = useState<ManagedResourceItem | null>(null);
5356

5457
const {
5558
data: managedResources,
@@ -59,19 +62,20 @@ export function ManagedResources() {
5962
refreshInterval: resourcesInterval,
6063
});
6164

62-
const openDeleteDialog = (item: ManagedResourceItem) => {
63-
setSelectedItem(item);
64-
setDeleteDialogOpen(true);
65-
};
65+
const { getPluralKind, isLoading: isLoadingPluralNames, error: pluralNamesError } = useResourcePluralNames();
6666

67-
const handleDeleteStart = (item: ManagedResourceItem) => {
68-
const itemKey = getItemKey(item);
69-
setDeletingItems((prev) => new Set(prev.add(itemKey)));
70-
};
67+
const resourceName = pendingDeleteItem?.metadata?.name ?? '';
68+
const apiVersion = pendingDeleteItem?.apiVersion ?? '';
69+
const pluralKind = pendingDeleteItem ? getPluralKind(pendingDeleteItem.kind) : '';
70+
const namespace = pendingDeleteItem?.metadata?.namespace ?? '';
7171

72-
const { data: kindMapping } = useCRDItemsMapping({
73-
refreshInterval: resourcesInterval,
74-
});
72+
const { trigger: deleteTrigger } = useApiResourceMutation<DeleteManagedResourceType>(
73+
DeleteMCPManagedResource(apiVersion, pluralKind, resourceName, namespace),
74+
);
75+
76+
const { trigger: patchTrigger } = useApiResourceMutation<undefined>(
77+
PatchResourceForForceDeletion(apiVersion, pluralKind, resourceName, namespace),
78+
);
7579

7680
const columns: AnalyticalTableColumnDefinition[] = useMemo(
7781
() => [
@@ -134,32 +138,27 @@ export function ManagedResources() {
134138
},
135139
},
136140
{
137-
Header: ' ',
141+
Header: t('ManagedResources.actionColumnHeader'),
138142
hAlign: 'Center',
139143
width: 60,
140144
disableFilters: true,
141145
Cell: (cellData: CellData<ResourceRow>) => {
142146
const item = cellData.cell.row.original?.item as ManagedResourceItem;
143-
const itemKey = item ? getItemKey(item) : '';
144-
const isDeleting = deletingItems.has(itemKey);
145147

146148
return cellData.cell.row.original?.item ? (
147-
<RowActionsMenu item={item} isDeleting={isDeleting} onOpen={openDeleteDialog} />
149+
<RowActionsMenu item={item} onOpen={openDeleteDialog} />
148150
) : undefined;
149151
},
150152
},
151153
],
152-
[t, deletingItems],
154+
[t],
153155
);
154156

155157
const rows: ResourceRow[] =
156158
managedResources
157159
?.filter((managedResource) => managedResource.items)
158160
.flatMap((managedResource) =>
159161
managedResource.items?.map((item) => {
160-
const itemKey = getItemKey(item);
161-
const isDeleting = deletingItems.has(itemKey);
162-
163162
const conditionSynced = item.status?.conditions?.find((condition) => condition.type === 'Synced');
164163
const conditionReady = item.status?.conditions?.find((condition) => condition.type === 'Ready');
165164

@@ -174,26 +173,46 @@ export function ManagedResources() {
174173
item: item,
175174
conditionSyncedMessage: conditionSynced?.message ?? conditionSynced?.reason ?? '',
176175
conditionReadyMessage: conditionReady?.message ?? conditionReady?.reason ?? '',
177-
className: isDeleting ? styles.deletingRow : undefined,
178176
};
179177
}),
180178
) ?? [];
181179

180+
const openDeleteDialog = (item: ManagedResourceItem) => {
181+
setPendingDeleteItem(item);
182+
};
183+
184+
const handleDeletionConfirmed = async (item: ManagedResourceItem, force: boolean) => {
185+
toast.show(t('ManagedResources.deleteStarted', { resourceName: item.metadata.name }));
186+
187+
try {
188+
await deleteTrigger();
189+
190+
if (force) {
191+
await patchTrigger();
192+
}
193+
} catch (_) {
194+
// Ignore errors - will be handled by the mutation hook
195+
}
196+
};
197+
198+
const combinedError = error || pluralNamesError;
199+
const combinedLoading = isLoading || isLoadingPluralNames;
200+
182201
return (
183202
<>
184203
<Title level="H4">{t('ManagedResources.header')}</Title>
185204

186-
{error && <IllustratedError details={error.message} />}
205+
{combinedError && <IllustratedError details={combinedError.message} />}
187206

188-
{!error && (
207+
{!combinedError && (
189208
<>
190209
<AnalyticalTable
191210
columns={columns}
192211
data={rows}
193212
minRows={1}
194213
groupBy={['kind']}
195214
scaleWidthMode={AnalyticalTableScaleWidthMode.Smart}
196-
loading={isLoading}
215+
loading={combinedLoading}
197216
filterable
198217
retainColumnWidth
199218
reactTableOptions={{
@@ -209,11 +228,10 @@ export function ManagedResources() {
209228
}}
210229
/>
211230
<ManagedResourceDeleteDialog
212-
kindMapping={kindMapping}
213-
open={deleteDialogOpen}
214-
item={selectedItem}
215-
onClose={() => setDeleteDialogOpen(false)}
216-
onDeleteStart={handleDeleteStart}
231+
open={!!pendingDeleteItem}
232+
item={pendingDeleteItem}
233+
onClose={() => setPendingDeleteItem(null)}
234+
onDeletionConfirmed={handleDeletionConfirmed}
217235
/>
218236
</>
219237
)}

src/components/ControlPlane/ManagedResourcesActionMenu.tsx

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,43 @@
1-
import { FC, useRef } from 'react';
1+
import { FC, useRef, useState } from 'react';
22
import { Button, Menu, MenuItem, MenuDomRef } from '@ui5/webcomponents-react';
33
import { useTranslation } from 'react-i18next';
44
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';
57

68
interface RowActionsMenuProps {
79
item: ManagedResourceItem;
810
onOpen: (item: ManagedResourceItem) => void;
9-
isDeleting: boolean;
1011
}
1112

12-
export const RowActionsMenu: FC<RowActionsMenuProps> = ({ item, onOpen, isDeleting }) => {
13+
export const RowActionsMenu: FC<RowActionsMenuProps> = ({ item, onOpen }) => {
1314
const { t } = useTranslation();
1415
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+
};
1524

1625
return (
1726
<>
18-
<Button icon="overflow" icon-end disabled={isDeleting} onClick={() => onOpen(item)} />
27+
<Button icon="overflow" design="Transparent" onClick={handleOpenerClick} />
1928
<Menu
2029
ref={popoverRef}
21-
onItemClick={() => {
22-
onOpen(item);
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);
2338
}}
2439
>
25-
<MenuItem text={t('ManagedResources.deleteAction')} icon="delete" />
40+
<MenuItem text={t('ManagedResources.deleteAction')} icon="delete" data-action="delete" />
2641
</Menu>
2742
</>
2843
);

src/components/Dialogs/ManagedResourceDeleteDialog.tsx

Lines changed: 16 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { FC, useState, useMemo, useEffect } from 'react';
1+
import { FC, useState, useEffect } from 'react';
22
import {
33
Button,
44
CheckBox,
@@ -11,26 +11,19 @@ import {
1111
} from '@ui5/webcomponents-react';
1212
import { useTranslation } from 'react-i18next';
1313
import { ManagedResourceItem } from '../../lib/shared/types';
14-
import { useApiResourceMutation } from '../../lib/api/useApiResource';
15-
import {
16-
DeleteManagedResourceType,
17-
DeleteMCPManagedResource,
18-
PatchResourceForForceDeletion,
19-
} from '../../lib/api/types/crate/deleteResource';
2014
import ButtonDesign from '@ui5/webcomponents/dist/types/ButtonDesign.js';
2115
import { DeleteConfirmationForm } from './DeleteConfirmationForm.tsx';
2216
import styles from './ManagedResourceDeleteDialog.module.css';
23-
import { getPluralKind } from '../Helper/getPluralKind.ts';
2417

2518
type Props = {
26-
kindMapping: Record<string, string>;
2719
open: boolean;
2820
onClose: () => void;
2921
item: ManagedResourceItem | null;
30-
onDeleteStart: (item: ManagedResourceItem) => void;
22+
onDeletionConfirmed?: (item: ManagedResourceItem, force: boolean) => void;
23+
onCanceled?: () => void;
3124
};
3225

33-
export const ManagedResourceDeleteDialog: FC<Props> = ({ kindMapping, open, onClose, item, onDeleteStart }) => {
26+
export const ManagedResourceDeleteDialog: FC<Props> = ({ open, onClose, item, onDeletionConfirmed, onCanceled }) => {
3427
const { t } = useTranslation();
3528
const [forceDeletion, setForceDeletion] = useState(false);
3629
const [advancedCollapsed, setAdvancedCollapsed] = useState(true);
@@ -44,59 +37,39 @@ export const ManagedResourceDeleteDialog: FC<Props> = ({ kindMapping, open, onCl
4437
}
4538
}, [open]);
4639

47-
const { apiVersion, resourceName, pluralKind, namespace } = useMemo(
48-
() => ({
49-
namespace: item?.metadata?.namespace ?? '',
50-
apiVersion: item?.apiVersion ?? '',
51-
resourceName: item?.metadata?.name ?? '',
52-
pluralKind: item ? getPluralKind(item, kindMapping) : '',
53-
}),
54-
[item, kindMapping],
55-
);
40+
const resourceName = item?.metadata?.name ?? '';
5641

5742
const onConfirmationInputChange = (event: Ui5CustomEvent<InputDomRef>) => {
5843
setConfirmationText(event.target.value);
5944
};
6045

6146
const isConfirmed = confirmationText === resourceName;
6247

63-
const { trigger: deleteTrigger } = useApiResourceMutation<DeleteManagedResourceType>(
64-
DeleteMCPManagedResource(apiVersion, pluralKind, resourceName, namespace),
65-
);
66-
67-
const { trigger: patchTrigger } = useApiResourceMutation<undefined>(
68-
PatchResourceForForceDeletion(apiVersion, pluralKind, resourceName, namespace),
69-
);
70-
7148
const handleForceDeletionChange = () => {
7249
setForceDeletion(!forceDeletion);
7350
if (!forceDeletion) setAdvancedCollapsed(false);
7451
};
7552

76-
const handleDelete = async () => {
77-
if (!item) return;
78-
79-
onDeleteStart(item);
80-
81-
try {
82-
await deleteTrigger();
83-
84-
if (forceDeletion) {
85-
await patchTrigger();
86-
}
87-
} catch (_) {
88-
// Ignore errors - item can be deleted before patch and it's ok.
53+
const handleDelete = () => {
54+
if (item && onDeletionConfirmed) {
55+
onDeletionConfirmed(item, forceDeletion);
8956
}
57+
onClose();
58+
};
9059

60+
const handleCancel = () => {
9161
onClose();
62+
if (onCanceled) {
63+
onCanceled();
64+
}
9265
};
9366

9467
return (
9568
<Dialog
9669
open={open}
9770
headerText={t('ManagedResources.deleteDialogTitle')}
9871
className={styles.dialog}
99-
onClose={onClose}
72+
onClose={handleCancel}
10073
>
10174
<FlexBox direction="Column" className={styles.content}>
10275
<DeleteConfirmationForm
@@ -126,7 +99,7 @@ export const ManagedResourceDeleteDialog: FC<Props> = ({ kindMapping, open, onCl
12699
</Panel>
127100

128101
<FlexBox justifyContent="End" className={styles.actions}>
129-
<Button design="Transparent" onClick={onClose}>
102+
<Button design="Transparent" onClick={handleCancel}>
130103
{t('buttons.cancel')}
131104
</Button>
132105
<Button design={ButtonDesign.Negative} disabled={!isConfirmed} onClick={handleDelete}>
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { useMemo } from 'react';
2+
import { useCRDItemsMapping } from '../lib/api/useApiResource';
3+
import { resourcesInterval } from '../lib/shared/constants';
4+
5+
export const useResourcePluralNames = () => {
6+
const {
7+
data: kindMapping,
8+
isLoading,
9+
error,
10+
} = useCRDItemsMapping({
11+
refreshInterval: resourcesInterval,
12+
});
13+
14+
const getPluralKind = useMemo(() => {
15+
return (singularKind: string): string => {
16+
if (!kindMapping) return '';
17+
return kindMapping[singularKind.toLowerCase()] ?? '';
18+
};
19+
}, [kindMapping]);
20+
21+
return {
22+
getPluralKind,
23+
isLoading,
24+
error,
25+
};
26+
};

0 commit comments

Comments
 (0)