Skip to content

Commit 37793d8

Browse files
Templates loaded from file onboardingAPI TBD
1 parent 7ce45e3 commit 37793d8

File tree

13 files changed

+808
-40
lines changed

13 files changed

+808
-40
lines changed

public/locales/en.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,9 @@
8787
"menuDownload": "Download",
8888
"menuCopy": "Copy to clipboard"
8989
},
90+
"ComponentsSelection": {
91+
"chooseVersion": "Please select version"
92+
},
9093
"IllustratedBanner": {
9194
"titleMessage": "No Managed Control Planes found",
9295
"subtitleMessage": "Get started by creating your first Managed Control Plane.",

src/components/ComponentsSelection/ComponentsSelection.tsx

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,19 @@ import { Infobox } from '../Ui/Infobox/Infobox.tsx';
2222
import { useTranslation } from 'react-i18next';
2323
import { ComponentsListItem } from '../../lib/api/types/crate/createManagedControlPlane.ts';
2424
import { getSelectedComponents } from './ComponentsSelectionContainer.tsx';
25+
import IllustratedError from '../Shared/IllustratedError.tsx';
2526

2627
export interface ComponentsSelectionProps {
2728
componentsList: ComponentsListItem[];
2829
setComponentsList: (components: ComponentsListItem[]) => void;
30+
templateDefaultsError?: string;
2931
}
3032

31-
export const ComponentsSelection: React.FC<ComponentsSelectionProps> = ({ componentsList, setComponentsList }) => {
33+
export const ComponentsSelection: React.FC<ComponentsSelectionProps> = ({
34+
componentsList,
35+
setComponentsList,
36+
templateDefaultsError,
37+
}) => {
3238
const [searchTerm, setSearchTerm] = useState('');
3339
const { t } = useTranslation();
3440

@@ -133,7 +139,23 @@ export const ComponentsSelection: React.FC<ComponentsSelectionProps> = ({ compon
133139
disabled={!component.isSelected || providerDisabled}
134140
aria-label={`${component.name} version`}
135141
onChange={handleVersionChange}
142+
valueState={component.isSelected && !component.selectedVersion ? 'Negative' : 'None'}
143+
valueStateMessage={
144+
component.isSelected && !component.selectedVersion ? (
145+
<span>{t('ComponentsSelection.chooseVersion')}</span>
146+
) : undefined
147+
}
136148
>
149+
{!component.selectedVersion && (
150+
<Option
151+
key="__placeholder"
152+
data-version=""
153+
data-name={component.name}
154+
selected
155+
>
156+
{t('ComponentsSelection.chooseVersion')}
157+
</Option>
158+
)}
137159
{component.versions.map((version) => (
138160
<Option
139161
key={version}
@@ -155,7 +177,14 @@ export const ComponentsSelection: React.FC<ComponentsSelectionProps> = ({ compon
155177
</Infobox>
156178
)}
157179
</div>
180+
158181
<div data-layout-span="XL4 L4 M4 S4">
182+
{templateDefaultsError ? (
183+
<div style={{ marginBottom: 8 }}>
184+
<IllustratedError title={templateDefaultsError} compact />
185+
</div>
186+
) : null}
187+
159188
{selectedComponents.length > 0 ? (
160189
<List headerText={t('componentsSelection.selectedComponents')}>
161190
{selectedComponents.map((component) => (

src/components/ComponentsSelection/ComponentsSelectionContainer.tsx

Lines changed: 65 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useEffect, useRef } from 'react';
1+
import React, { useEffect, useRef, useState } from 'react';
22
import { ComponentsSelection } from './ComponentsSelection.tsx';
33

44
import IllustratedError from '../Shared/IllustratedError.tsx';
@@ -9,10 +9,12 @@ import { useApiResource } from '../../lib/api/useApiResource.ts';
99
import Loading from '../Shared/Loading.tsx';
1010
import { ComponentsListItem, removeComponents } from '../../lib/api/types/crate/createManagedControlPlane.ts';
1111
import { useTranslation } from 'react-i18next';
12+
import { ManagedControlPlaneTemplate } from '../../lib/api/types/templates/mcpTemplate.ts';
1213

1314
export interface ComponentsSelectionProps {
1415
componentsList: ComponentsListItem[];
1516
setComponentsList: (components: ComponentsListItem[]) => void;
17+
managedControlPlaneTemplate?: ManagedControlPlaneTemplate;
1618
}
1719

1820
/**
@@ -33,10 +35,13 @@ export const getSelectedComponents = (components: ComponentsListItem[]) => {
3335
export const ComponentsSelectionContainer: React.FC<ComponentsSelectionProps> = ({
3436
setComponentsList,
3537
componentsList,
38+
managedControlPlaneTemplate,
3639
}) => {
3740
const { data: availableManagedComponentsListData, error, isLoading } = useApiResource(ListManagedComponents());
3841
const { t } = useTranslation();
3942
const initialized = useRef(false);
43+
const [templateDefaultsError, setTemplateDefaultsError] = useState<string | null>(null);
44+
const defaultComponents = managedControlPlaneTemplate?.spec?.spec?.components?.defaultComponents ?? [];
4045

4146
useEffect(() => {
4247
if (
@@ -50,19 +55,68 @@ export const ComponentsSelectionContainer: React.FC<ComponentsSelectionProps> =
5055
const newComponentsList = availableManagedComponentsListData.items
5156
.map((item) => {
5257
const versions = sortVersions(item.status.versions);
58+
const template = defaultComponents.find((dc) => dc.name === item.metadata.name);
59+
const templateVersion = template?.version;
60+
const selectedVersion = template
61+
? (templateVersion && versions.includes(templateVersion) ? templateVersion : '')
62+
: (versions[0] ?? '');
5363
return {
5464
name: item.metadata.name,
5565
versions,
56-
selectedVersion: versions[0] ?? '',
57-
isSelected: false,
66+
selectedVersion,
67+
isSelected: !!template,
5868
documentationUrl: '',
5969
};
6070
})
6171
.filter((component) => !removeComponents.find((item) => item === component.name));
6272

6373
setComponentsList(newComponentsList);
6474
initialized.current = true;
65-
}, [availableManagedComponentsListData, setComponentsList]);
75+
}, [availableManagedComponentsListData, setComponentsList, defaultComponents]);
76+
77+
useEffect(() => {
78+
const items = availableManagedComponentsListData?.items ?? [];
79+
if (items.length === 0 || !defaultComponents.length) {
80+
setTemplateDefaultsError(null);
81+
return;
82+
}
83+
84+
const errs: string[] = [];
85+
defaultComponents.forEach((dc: any) => {
86+
if (!dc?.name) return;
87+
const item = items.find((it) => it.metadata.name === dc.name);
88+
if (!item) {
89+
errs.push(`Component "${dc.name}" from template is not available.`);
90+
return;
91+
}
92+
const versions: string[] = Array.isArray(item.status?.versions) ? item.status.versions : [];
93+
if (dc.version && !versions.includes(dc.version)) {
94+
errs.push(`Component "${dc.name}" version "${dc.version}" from template is not available.`);
95+
}
96+
});
97+
98+
setTemplateDefaultsError(errs.length ? errs.join('\n') : null);
99+
}, [availableManagedComponentsListData, defaultComponents]);
100+
101+
useEffect(() => {
102+
if (!initialized.current) return;
103+
if (!defaultComponents?.length) return;
104+
if (!componentsList?.length) return;
105+
106+
const anySelected = componentsList.some((c) => c.isSelected);
107+
if (anySelected) return;
108+
109+
const updated = componentsList.map((c) => {
110+
const template = defaultComponents.find((dc) => dc.name === c.name);
111+
if (!template) return c;
112+
const templateVersion = template.version;
113+
const selectedVersion =
114+
templateVersion && Array.isArray(c.versions) && c.versions.includes(templateVersion) ? templateVersion : '';
115+
return { ...c, isSelected: true, selectedVersion };
116+
});
117+
118+
setComponentsList(updated);
119+
}, [defaultComponents, componentsList, setComponentsList]);
66120

67121
if (isLoading) {
68122
return <Loading />;
@@ -77,5 +131,11 @@ export const ComponentsSelectionContainer: React.FC<ComponentsSelectionProps> =
77131
return <IllustratedError title={t('componentsSelection.cannotLoad')} compact={true} />;
78132
}
79133

80-
return <ComponentsSelection componentsList={componentsList} setComponentsList={setComponentsList} />;
134+
return (
135+
<ComponentsSelection
136+
componentsList={componentsList}
137+
setComponentsList={setComponentsList}
138+
templateDefaultsError={templateDefaultsError || undefined}
139+
/>
140+
);
81141
};

src/components/ControlPlanes/ControlPlanesListMenu.tsx

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,27 @@ import '@ui5/webcomponents-icons/dist/copy';
55
import '@ui5/webcomponents-icons/dist/accept';
66

77
import { useTranslation } from 'react-i18next';
8+
import { ManagedControlPlaneTemplate } from '../../lib/api/types/templates/mcpTemplate.ts';
89

910
type ControlPlanesListMenuProps = {
1011
setDialogDeleteWsIsOpen: Dispatch<SetStateAction<boolean>>;
1112
setIsCreateManagedControlPlaneWizardOpen: Dispatch<SetStateAction<boolean>>;
13+
setInitialTemplateName: Dispatch<SetStateAction<string | undefined>>;
1214
};
1315

1416
export const ControlPlanesListMenu: FC<ControlPlanesListMenuProps> = ({
1517
setDialogDeleteWsIsOpen,
1618
setIsCreateManagedControlPlaneWizardOpen,
19+
setInitialTemplateName,
1720
}) => {
1821
const popoverRef = useRef<MenuDomRef>(null);
1922
const [open, setOpen] = useState(false);
2023

2124
const { t } = useTranslation();
2225

26+
// Here we will pass template list from OnboardingAPI
27+
const allTemplates: ManagedControlPlaneTemplate[] = [];
28+
2329
const handleOpenerClick = (e: Ui5CustomEvent<ButtonDomRef, ButtonClickEventDetail>) => {
2430
if (popoverRef.current && e.currentTarget) {
2531
popoverRef.current.opener = e.currentTarget as HTMLElement;
@@ -34,14 +40,20 @@ export const ControlPlanesListMenu: FC<ControlPlanesListMenuProps> = ({
3440
ref={popoverRef}
3541
open={open}
3642
onItemClick={(event) => {
37-
const action = (event.detail.item as HTMLElement).dataset.action;
43+
const item = event.detail.item as HTMLElement;
44+
const action = item.dataset.action;
3845
if (action === 'newManagedControlPlane') {
46+
setInitialTemplateName(undefined);
47+
setIsCreateManagedControlPlaneWizardOpen(true);
48+
}
49+
if (action === 'newManagedControlPlaneWithTemplate') {
50+
const tplName = item.dataset.templateName || undefined;
51+
setInitialTemplateName(tplName);
3952
setIsCreateManagedControlPlaneWizardOpen(true);
4053
}
4154
if (action === 'deleteWorkspace') {
4255
setDialogDeleteWsIsOpen(true);
4356
}
44-
4557
setOpen(false);
4658
}}
4759
>
@@ -51,6 +63,17 @@ export const ControlPlanesListMenu: FC<ControlPlanesListMenuProps> = ({
5163
data-action="newManagedControlPlane"
5264
icon="add"
5365
/>
66+
{allTemplates.map((tpl) => (
67+
<MenuItem
68+
key={`tpl-${tpl.metadata.name}`}
69+
text={tpl.metadata.name}
70+
title={tpl.metadata.descriptionText || ''}
71+
data-action="newManagedControlPlaneWithTemplate"
72+
data-template-name={tpl.metadata.name}
73+
icon="document-text"
74+
/>
75+
))}
76+
5477
<MenuItem
5578
key={'delete'}
5679
text={t('ControlPlaneListToolbar.deleteWorkspace')}

src/components/ControlPlanes/List/ControlPlaneListWorkspaceGridTile.tsx

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import '@ui5/webcomponents-icons/dist/delete';
55
import { CopyButton } from '../../Shared/CopyButton.tsx';
66
import { ControlPlaneCard } from '../ControlPlaneCard/ControlPlaneCard.tsx';
77
import { ListWorkspacesType, isWorkspaceReady } from '../../../lib/api/types/crate/listWorkspaces.ts';
8-
import { useState } from 'react';
8+
import { useMemo, useState } from 'react';
99
import { MembersAvatarView } from './MembersAvatarView.tsx';
1010
import { DeleteWorkspaceResource, DeleteWorkspaceType } from '../../../lib/api/types/crate/deleteWorkspace.ts';
1111
import { useApiResourceMutation, useApiResource } from '../../../lib/api/useApiResource.ts';
@@ -32,6 +32,7 @@ interface Props {
3232

3333
export function ControlPlaneListWorkspaceGridTile({ projectName, workspace }: Props) {
3434
const [isCreateManagedControlPlaneWizardOpen, setIsCreateManagedControlPlaneWizardOpen] = useState(false);
35+
const [initialTemplateName, setInitialTemplateName] = useState<string | undefined>(undefined);
3536
const workspaceName = workspace.metadata.name;
3637
const workspaceDisplayName = workspace.metadata.annotations?.[DISPLAY_NAME_ANNOTATION] || '';
3738
const showDisplayName = workspaceDisplayName.length > 0;
@@ -68,6 +69,22 @@ export function ControlPlaneListWorkspaceGridTile({ projectName, workspace }: Pr
6869
return null;
6970
}
7071

72+
const uniqueMembers = useMemo(() => {
73+
const seenKeys = new Set<string>();
74+
const fallbackNamespace = workspace.status?.namespace ?? '';
75+
76+
return (workspace.spec.members ?? []).filter((member: any) => {
77+
const memberNamespace = member?.namespace ?? fallbackNamespace;
78+
const memberName = String(member?.name ?? '').trim().toLowerCase();
79+
if (!memberName) return false;
80+
81+
const dedupeKey = `${memberNamespace}::${memberName}`;
82+
if (seenKeys.has(dedupeKey)) return false;
83+
seenKeys.add(dedupeKey);
84+
return true;
85+
});
86+
}, [workspace.spec.members, workspace.status?.namespace]);
87+
7188
return (
7289
<>
7390
<ObjectPageSection
@@ -98,7 +115,7 @@ export function ControlPlaneListWorkspaceGridTile({ projectName, workspace }: Pr
98115

99116
<CopyButton text={workspace.status?.namespace || '-'} style={{ justifyContent: 'start' }} />
100117

101-
<MembersAvatarView members={workspace.spec.members} project={projectName} workspace={workspaceName} />
118+
<MembersAvatarView members={uniqueMembers} project={projectName} workspace={workspaceName} />
102119
<FlexBox justifyContent={'SpaceBetween'} gap={10}>
103120
<YamlViewButtonWithLoader
104121
workspaceName={workspace.metadata.namespace}
@@ -108,6 +125,7 @@ export function ControlPlaneListWorkspaceGridTile({ projectName, workspace }: Pr
108125
<ControlPlanesListMenu
109126
setDialogDeleteWsIsOpen={setDialogDeleteWsIsOpen}
110127
setIsCreateManagedControlPlaneWizardOpen={setIsCreateManagedControlPlaneWizardOpen}
128+
setInitialTemplateName={setInitialTemplateName}
111129
/>
112130
</FlexBox>
113131
</div>
@@ -168,6 +186,7 @@ export function ControlPlaneListWorkspaceGridTile({ projectName, workspace }: Pr
168186
setIsOpen={setIsCreateManagedControlPlaneWizardOpen}
169187
projectName={projectNamespace}
170188
workspaceName={workspaceName}
189+
initialTemplateName={initialTemplateName}
171190
/>
172191
</>
173192
);

0 commit comments

Comments
 (0)