Skip to content

Commit ab25411

Browse files
Merge branch 'main' into fix/visualise-resources-on-graph-by-name-and-version
2 parents 5eb969d + 0662013 commit ab25411

17 files changed

+522
-69
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/ComponentsSelection/ComponentsSelection.module.css

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,37 @@
33
background: var(--sapBackgroundColor);
44
border-bottom: 1px solid var(--sapList_BorderColor);
55
}
6+
7+
.row:hover {
8+
background: var(--sapList_Hover_Background);
9+
}
10+
11+
.row.providerRow {
12+
position: relative;
13+
padding-inline-start: calc(1rem + 16px);
14+
background: color-mix(in srgb, var(--sapBackgroundColor) 98%, #000 2%);
15+
}
16+
17+
@media (prefers-color-scheme: dark) {
18+
.row.providerRow {
19+
background: color-mix(in srgb, var(--sapBackgroundColor) 97%, #fff 3%);
20+
}
21+
}
22+
23+
.row.providerRow:hover {
24+
background: var(--sapList_Hover_Background);
25+
}
26+
27+
.row.providerRow::before {
28+
content: "";
29+
position: absolute;
30+
inset-block: 6px;
31+
inset-inline-start: 1.6rem;
32+
width: 2px;
33+
background: var(--sapList_BorderColor);
34+
}
35+
36+
.providerRow :global(ui5-checkbox) {
37+
transform: scale(0.9);
38+
transform-origin: left center;
39+
}

src/components/ComponentsSelection/ComponentsSelection.tsx

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,31 @@ export const ComponentsSelection: React.FC<ComponentsSelectionProps> = ({
4040

4141
const selectedComponents = useMemo(() => getSelectedComponents(componentsList), [componentsList]);
4242

43+
const isProvider = useCallback((componentName: string) => {
44+
return componentName.includes('provider') && componentName !== 'crossplane';
45+
}, []);
46+
4347
const searchResults = useMemo(() => {
4448
const lowerSearch = searchTerm.toLowerCase();
45-
return componentsList.filter(({ name }) => name.toLowerCase().includes(lowerSearch));
46-
}, [componentsList, searchTerm]);
49+
const filtered = componentsList.filter(({ name }) => name.toLowerCase().includes(lowerSearch));
50+
51+
// Sort components: crossplane first, then providers, then rest
52+
return filtered.sort((a, b) => {
53+
const isCrossplaneA = a.name === 'crossplane';
54+
const isCrossplaneB = b.name === 'crossplane';
55+
56+
if (isCrossplaneA && !isCrossplaneB) return -1;
57+
if (isCrossplaneB && !isCrossplaneA) return 1;
58+
59+
const isProviderA = isProvider(a.name);
60+
const isProviderB = isProvider(b.name);
61+
62+
if (isProviderA && !isProviderB) return -1;
63+
if (isProviderB && !isProviderA) return 1;
64+
65+
return a.name.localeCompare(b.name);
66+
});
67+
}, [componentsList, searchTerm, isProvider]);
4768

4869
const handleSelectionChange = useCallback(
4970
(e: Ui5CustomEvent<CheckBoxDomRef, { checked: boolean }>) => {
@@ -105,10 +126,12 @@ export const ComponentsSelection: React.FC<ComponentsSelectionProps> = ({
105126
{searchResults.length > 0 ? (
106127
searchResults.map((component) => {
107128
const providerDisabled = isProviderDisabled(component);
129+
const isProviderComponent = isProvider(component.name);
130+
108131
return (
109132
<FlexBox
110133
key={component.name}
111-
className={styles.row}
134+
className={`${styles.row} ${isProviderComponent ? styles.providerRow : ''}`}
112135
gap={10}
113136
justifyContent="SpaceBetween"
114137
data-testid={`component-row-${component.name}`}
@@ -120,6 +143,7 @@ export const ComponentsSelection: React.FC<ComponentsSelectionProps> = ({
120143
checked={component.isSelected}
121144
disabled={providerDisabled}
122145
aria-label={component.name}
146+
className={isProviderComponent ? styles.checkBox : ''}
123147
onChange={handleSelectionChange}
124148
/>
125149
<FlexBox gap={10} justifyContent="SpaceBetween" alignItems="Baseline">

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', () => {

0 commit comments

Comments
 (0)