Skip to content

Commit 3e9ad97

Browse files
committed
Merge branch 'main' into ft/tab-navigation
2 parents fc5315c + d272e62 commit 3e9ad97

18 files changed

+503
-489
lines changed

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ RUN npm prune --omit=dev
1818

1919

2020
# PRODUCTION STAGE
21-
FROM gcr.io/distroless/nodejs24-debian12@sha256:e6af293a96996c2a6d1e8a440600518ec614571a91356a38c55f316ff1ee9924 AS production
21+
FROM gcr.io/distroless/nodejs24-debian12@sha256:b8be532fe81829b3803c832989cba7f42de62b2fbc8c7599cae30f378882ea12 AS production
2222
WORKDIR /usr/src/app
2323

2424
# Copy built files

package-lock.json

Lines changed: 155 additions & 209 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@
3434
"@fastify/static": "8.2.0",
3535
"@fastify/vite": "8.2.0",
3636
"@hookform/resolvers": "5.2.2",
37-
"@sentry/node": "10.13.0",
38-
"@sentry/react": "10.13.0",
37+
"@sentry/node": "10.14.0",
38+
"@sentry/react": "10.14.0",
3939
"@sentry/vite-plugin": "4.3.0",
4040
"@ui5/webcomponents": "2.14.0",
4141
"@ui5/webcomponents-fiori": "2.14.0",
@@ -47,7 +47,7 @@
4747
"dagre": "0.8.5",
4848
"diff": "^8.0.2",
4949
"dotenv": "17.2.2",
50-
"fastify": "5.6.0",
50+
"fastify": "5.6.1",
5151
"fastify-plugin": "5.0.1",
5252
"graphql": "16.11.0",
5353
"graphql-config": "5.1.5",
@@ -95,7 +95,7 @@
9595
"prettier": "3.6.2",
9696
"tsx": "4.20.5",
9797
"typescript": "5.9.2",
98-
"typescript-eslint": "8.44.0",
98+
"typescript-eslint": "8.44.1",
9999
"vite": "7.1.7",
100100
"vitest": "3.2.4"
101101
}

public/locales/en.json

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,9 @@
5555
},
5656
"ControlPlaneCard": {
5757
"deleteConfirmationDialog": "MCP deletion triggered. The list will refresh automatically once completed.",
58-
"editMCP": "Edit Managed Control Plane",
59-
"deleteMCP": "Delete Managed Control Plane"
58+
"editMCP": "Edit ManagedControlPlane",
59+
"duplicateMCP": "Duplicate ManagedControlPlane",
60+
"deleteMCP": "Delete ManagedControlPlane"
6061

6162
},
6263
"ControlPlaneListAllWorkspaces": {
@@ -378,18 +379,23 @@
378379
"createMCP": {
379380
"dialogTitle": "Create Managed Control Plane",
380381
"titleText": "Managed Control Plane Created Successfully!",
381-
"subtitleText": "Your Managed Control Plane is being set up. It will be ready to use in just a few minutes. You can safely close this window."
382+
"subtitleText": "Your Managed Control Plane is being set up. It will be ready to use in just a few minutes. You can safely close this window.",
383+
"copySuffix": "-copy"
382384
},
383385
"editMCP": {
384386
"dialogTitle": "Edit Managed Control Plane",
385387
"titleText": "Managed Control Plane Updated Successfully!",
386-
"subtitleText": "Your Managed Control Plane is being updated. It will be ready to use in just a few minutes. You can safely close this window."
388+
"subtitleText": "Your Managed Control Plane is being updated. It will be ready to use in just a few minutes. You can safely close this window.",
389+
"editComponents": "Edit components",
390+
"duplicatingMCPInfo1": "Duplicating a <span>ManagedControlPlane</span> will only create a <span>ManagedControlPlane</span> with the same configuration. ",
391+
"duplicatingMCPInfo2": "<b>It will NOT copy the managed resources inside</b>"
387392
},
388393
"componentsSelection": {
389394
"selectComponents": "Select Components",
390395
"selectedComponents": "Selected Components",
391396
"pleaseSelectComponents": "Choose the components you want to add to your Managed Control Plane.",
392-
"cannotLoad": "Cannot load components list"
397+
"cannotLoad": "Cannot load components list",
398+
"noComponentsFound": "No components found matching your search."
393399
},
394400
"Hints": {
395401
"CrossplaneHint": {

src/components/ComponentsSelection/ComponentsSelection.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ export const ComponentsSelection: React.FC<ComponentsSelectionProps> = ({
101101
/>
102102

103103
<Grid>
104-
<div data-layout-span="XL8 L8 M8 S8">
104+
<div data-layout-span="XL7 L7 M7 S7">
105105
{searchResults.length > 0 ? (
106106
searchResults.map((component) => {
107107
const providerDisabled = isProviderDisabled(component);
@@ -167,13 +167,13 @@ export const ComponentsSelection: React.FC<ComponentsSelectionProps> = ({
167167
);
168168
})
169169
) : (
170-
<Infobox fullWidth variant="success">
171-
<Text>{t('componentsSelection.pleaseSelectComponents')}</Text>
170+
<Infobox fullWidth variant="normal" icon="search">
171+
<Text>{t('componentsSelection.noComponentsFound')}</Text>
172172
</Infobox>
173173
)}
174174
</div>
175175

176-
<div data-layout-span="XL4 L4 M4 S4">
176+
<div data-layout-span="XL5 L5 M5 S5">
177177
{templateDefaultsError ? (
178178
<div style={{ marginBottom: 8 }}>
179179
<IllustratedError title={templateDefaultsError} compact />
@@ -191,7 +191,7 @@ export const ComponentsSelection: React.FC<ComponentsSelectionProps> = ({
191191
))}
192192
</List>
193193
) : (
194-
<Infobox fullWidth variant="success">
194+
<Infobox variant="success" icon="add">
195195
<Text>{t('componentsSelection.pleaseSelectComponents')}</Text>
196196
</Infobox>
197197
)}

src/components/ComponentsSelection/ComponentsSelectionContainer.tsx

Lines changed: 8 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,16 @@
1-
import React, { useEffect, useMemo, useState } from 'react';
1+
import React from 'react';
22
import { ComponentsSelection } from './ComponentsSelection.tsx';
3-
43
import IllustratedError from '../Shared/IllustratedError.tsx';
5-
import { sortVersions } from '../../utils/componentsVersions.ts';
6-
7-
import { ListManagedComponents } from '../../lib/api/types/crate/listManagedComponents.ts';
8-
import { useApiResource } from '../../lib/api/useApiResource.ts';
94
import Loading from '../Shared/Loading.tsx';
10-
import { ComponentsListItem, removeComponents } from '../../lib/api/types/crate/createManagedControlPlane.ts';
5+
import { ComponentsListItem } from '../../lib/api/types/crate/createManagedControlPlane.ts';
116
import { useTranslation } from 'react-i18next';
12-
import { ManagedControlPlaneTemplate } from '../../lib/api/types/templates/mcpTemplate.ts';
137

148
export interface ComponentsSelectionProps {
159
componentsList: ComponentsListItem[];
1610
setComponentsList: (components: ComponentsListItem[]) => void;
17-
setInitialComponentsList: (components: ComponentsListItem[]) => void;
18-
managedControlPlaneTemplate?: ManagedControlPlaneTemplate;
19-
initialSelection?: Record<string, { isSelected: boolean; version: string }>;
20-
isOnMcpPage?: boolean;
21-
initializedComponents: React.RefObject<boolean>;
11+
isLoading: boolean;
12+
error: unknown;
13+
templateDefaultsError?: string;
2214
}
2315

2416
/**
@@ -34,124 +26,15 @@ export const getSelectedComponents = (components: ComponentsListItem[]) => {
3426
});
3527
};
3628

37-
type TemplateDefaultComponent = {
38-
name: string;
39-
version: string;
40-
removable?: boolean;
41-
versionChangeable?: boolean;
42-
};
43-
4429
export const ComponentsSelectionContainer: React.FC<ComponentsSelectionProps> = ({
4530
setComponentsList,
4631
componentsList,
47-
managedControlPlaneTemplate,
48-
initialSelection,
49-
isOnMcpPage,
50-
setInitialComponentsList,
51-
initializedComponents,
32+
isLoading,
33+
error,
34+
templateDefaultsError,
5235
}) => {
53-
const {
54-
data: availableManagedComponentsListData,
55-
error,
56-
isLoading,
57-
} = useApiResource(ListManagedComponents(), undefined, !!isOnMcpPage);
5836
const { t } = useTranslation();
5937

60-
const [templateDefaultsError, setTemplateDefaultsError] = useState<string | null>(null);
61-
const defaultComponents = useMemo<TemplateDefaultComponent[]>(
62-
() => managedControlPlaneTemplate?.spec?.spec?.components?.defaultComponents ?? [],
63-
[managedControlPlaneTemplate],
64-
);
65-
66-
useEffect(() => {
67-
if (
68-
initializedComponents.current ||
69-
!availableManagedComponentsListData?.items ||
70-
availableManagedComponentsListData.items.length === 0
71-
) {
72-
return;
73-
}
74-
75-
const newComponentsList = availableManagedComponentsListData.items
76-
.map((item) => {
77-
const versions = sortVersions(item.status?.versions ?? []);
78-
const template = defaultComponents.find((dc) => dc.name === (item.metadata?.name ?? ''));
79-
const templateVersion = template?.version;
80-
let selectedVersion = template
81-
? templateVersion && versions.includes(templateVersion)
82-
? templateVersion
83-
: ''
84-
: (versions[0] ?? '');
85-
let isSelected = !!template;
86-
87-
const initSel = initialSelection?.[item.metadata?.name ?? ''];
88-
if (initSel) {
89-
// Override selection and version from initial selection if provided
90-
isSelected = Boolean(initSel.isSelected);
91-
selectedVersion = initSel.version && versions.includes(initSel.version) ? initSel.version : '';
92-
}
93-
return {
94-
name: item.metadata?.name ?? '',
95-
versions,
96-
selectedVersion,
97-
isSelected,
98-
documentationUrl: '',
99-
};
100-
})
101-
.filter((component) => !removeComponents.find((item) => item === component.name));
102-
setInitialComponentsList(newComponentsList);
103-
setComponentsList(newComponentsList);
104-
initializedComponents.current = true;
105-
// eslint-disable-next-line react-hooks/exhaustive-deps
106-
}, [setComponentsList, defaultComponents, initialSelection, availableManagedComponentsListData?.items]);
107-
108-
useEffect(() => {
109-
const items = availableManagedComponentsListData?.items ?? [];
110-
if (items.length === 0 || !defaultComponents.length) {
111-
setTemplateDefaultsError(null);
112-
return;
113-
}
114-
115-
const errors: string[] = [];
116-
defaultComponents.forEach((dc: TemplateDefaultComponent) => {
117-
if (!dc?.name) return;
118-
const item = items.find((it) => it.metadata?.name === dc.name);
119-
if (!item) {
120-
errors.push(`Component "${dc.name}" from template is not available.`);
121-
return;
122-
}
123-
const versions: string[] = Array.isArray(item.status?.versions) ? (item.status?.versions as string[]) : [];
124-
if (dc.version && !versions.includes(dc.version)) {
125-
errors.push(`Component "${dc.name}" version "${dc.version}" from template is not available.`);
126-
}
127-
});
128-
129-
setTemplateDefaultsError(errors.length ? errors.join('\n') : null);
130-
}, [availableManagedComponentsListData, defaultComponents]);
131-
132-
useEffect(() => {
133-
if (!initializedComponents.current) return;
134-
if (!defaultComponents?.length) return;
135-
if (!componentsList?.length) return;
136-
// If initialSelection is provided, do not auto-apply template defaults
137-
if (initialSelection && Object.keys(initialSelection).length > 0) return;
138-
139-
const anySelected = componentsList.some((c) => c.isSelected);
140-
if (anySelected) return;
141-
142-
const updated = componentsList.map((c) => {
143-
const template = defaultComponents.find((dc) => dc.name === c.name);
144-
if (!template) return c;
145-
const templateVersion = template.version;
146-
const selectedVersion =
147-
templateVersion && Array.isArray(c.versions) && c.versions.includes(templateVersion) ? templateVersion : '';
148-
return { ...c, isSelected: true, selectedVersion };
149-
});
150-
151-
setComponentsList(updated);
152-
// eslint-disable-next-line react-hooks/exhaustive-deps
153-
}, [defaultComponents, componentsList, setComponentsList, initialSelection]);
154-
15538
if (isLoading) {
15639
return <Loading />;
15740
}

src/components/ControlPlane/ComponentList.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@ import {
44
Panel,
55
Title,
66
Toolbar,
7+
ToolbarButton,
78
ToolbarSpacer,
89
} from '@ui5/webcomponents-react';
910
import { ControlPlaneType } from '../../lib/api/types/crate/controlPlanes';
1011
import { useTranslation } from 'react-i18next';
1112

12-
export default function ComponentList({ mcp }: { mcp: ControlPlaneType }) {
13+
export default function ComponentList({ mcp, onEditClick }: { mcp: ControlPlaneType; onEditClick: () => void }) {
1314
const { t } = useTranslation();
1415

1516
const data = [
@@ -53,6 +54,7 @@ export default function ComponentList({ mcp }: { mcp: ControlPlaneType }) {
5354
<Toolbar>
5455
<Title>{t('common.itemsCount', { count: data.length })}</Title>
5556
<ToolbarSpacer />
57+
<ToolbarButton tooltip={t('editMCP.editComponents')} design="Transparent" icon="edit" onClick={onEditClick} />
5658
</Toolbar>
5759
}
5860
>

src/components/ControlPlanes/ControlPlaneCard/ControlPlaneCard.tsx

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,22 @@ interface Props {
3838
projectName: string;
3939
}
4040

41-
export function ControlPlaneCard({ controlPlane, workspace, projectName }: Props) {
41+
type MCPWizardState = {
42+
isOpen: boolean;
43+
mode?: 'edit' | 'duplicate';
44+
};
45+
export const ControlPlaneCard = ({ controlPlane, workspace, projectName }: Props) => {
4246
const [dialogDeleteMcpIsOpen, setDialogDeleteMcpIsOpen] = useState(false);
4347
const toast = useToast();
4448
const { t } = useTranslation();
45-
const [isEditManagedControlPlaneWizardOpen, setIsEditManagedControlPlaneWizardOpen] = useState(false);
49+
const [managedControlPlaneWizardState, setManagedControlPlaneWizardState] = useState<MCPWizardState>({
50+
isOpen: false,
51+
mode: undefined,
52+
});
53+
54+
const handleIsManagedControlPlaneWizardOpen = (isOpen: boolean, mode?: 'edit' | 'duplicate') => {
55+
setManagedControlPlaneWizardState({ isOpen, mode });
56+
};
4657
const { trigger: patchTrigger } = useApiResourceMutation<DeleteMCPType>(
4758
PatchMCPResourceForDeletion(controlPlane.metadata.namespace, controlPlane.metadata.name),
4859
);
@@ -85,7 +96,7 @@ export function ControlPlaneCard({ controlPlane, workspace, projectName }: Props
8596
<ControlPlaneCardMenu
8697
setDialogDeleteMcpIsOpen={setDialogDeleteMcpIsOpen}
8798
isDeleteMcpButtonDisabled={controlPlane.status?.status === ReadyStatus.InDeletion}
88-
setIsEditManagedControlPlaneWizardOpen={setIsEditManagedControlPlaneWizardOpen}
99+
setIsEditManagedControlPlaneWizardOpen={handleIsManagedControlPlaneWizardOpen}
89100
/>
90101
<FlexBox direction="Row" justifyContent="SpaceBetween" alignItems="Center" gap={10}>
91102
<YamlViewButtonWithLoader
@@ -130,11 +141,12 @@ export function ControlPlaneCard({ controlPlane, workspace, projectName }: Props
130141
}}
131142
/>
132143
<EditManagedControlPlaneWizardDataLoader
133-
isOpen={isEditManagedControlPlaneWizardOpen}
134-
setIsOpen={setIsEditManagedControlPlaneWizardOpen}
144+
isOpen={managedControlPlaneWizardState.isOpen}
145+
setIsOpen={(isOpen) => handleIsManagedControlPlaneWizardOpen(isOpen)}
135146
workspaceName={namespace}
136147
resourceName={name}
148+
mode={managedControlPlaneWizardState.mode}
137149
/>
138150
</>
139151
);
140-
}
152+
};

src/components/ControlPlanes/ControlPlaneCard/ControlPlaneCardMenu.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { useTranslation } from 'react-i18next';
99
type ControlPlanesListMenuProps = {
1010
setDialogDeleteMcpIsOpen: Dispatch<SetStateAction<boolean>>;
1111
isDeleteMcpButtonDisabled: boolean;
12-
setIsEditManagedControlPlaneWizardOpen: Dispatch<SetStateAction<boolean>>;
12+
setIsEditManagedControlPlaneWizardOpen: (isOpen: boolean, mode?: 'edit' | 'duplicate') => void;
1313
};
1414

1515
export const ControlPlaneCardMenu: FC<ControlPlanesListMenuProps> = ({
@@ -34,7 +34,10 @@ export const ControlPlaneCardMenu: FC<ControlPlanesListMenuProps> = ({
3434
onItemClick={(event) => {
3535
const action = (event.detail.item as HTMLElement).dataset.action;
3636
if (action === 'editMcp') {
37-
setIsEditManagedControlPlaneWizardOpen(true);
37+
setIsEditManagedControlPlaneWizardOpen(true, 'edit');
38+
}
39+
if (action === 'duplicateMcp') {
40+
setIsEditManagedControlPlaneWizardOpen(true, 'duplicate');
3841
}
3942
if (action === 'deleteMcp') {
4043
setDialogDeleteMcpIsOpen(true);
@@ -53,6 +56,7 @@ export const ControlPlaneCardMenu: FC<ControlPlanesListMenuProps> = ({
5356
icon="delete"
5457
disabled={isDeleteMcpButtonDisabled}
5558
/>
59+
<MenuItem key={'duplicate'} text={t('ControlPlaneCard.duplicateMCP')} data-action="duplicateMcp" icon="copy" />
5660
<MenuItem
5761
key={'edit'}
5862
text={t('ControlPlaneCard.editMCP')}

0 commit comments

Comments
 (0)