Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions public/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@
"menuDownload": "Download",
"menuCopy": "Copy to clipboard"
},
"ComponentsSelection": {
"chooseVersion": "Please select version"
},
"IllustratedBanner": {
"titleMessage": "No Managed Control Planes found",
"subtitleMessage": "Get started by creating your first Managed Control Plane.",
Expand Down
26 changes: 25 additions & 1 deletion src/components/ComponentsSelection/ComponentsSelection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,19 @@ import { Infobox } from '../Ui/Infobox/Infobox.tsx';
import { useTranslation } from 'react-i18next';
import { ComponentsListItem } from '../../lib/api/types/crate/createManagedControlPlane.ts';
import { getSelectedComponents } from './ComponentsSelectionContainer.tsx';
import IllustratedError from '../Shared/IllustratedError.tsx';

export interface ComponentsSelectionProps {
componentsList: ComponentsListItem[];
setComponentsList: (components: ComponentsListItem[]) => void;
templateDefaultsError?: string;
}

export const ComponentsSelection: React.FC<ComponentsSelectionProps> = ({ componentsList, setComponentsList }) => {
export const ComponentsSelection: React.FC<ComponentsSelectionProps> = ({
componentsList,
setComponentsList,
templateDefaultsError,
}) => {
const [searchTerm, setSearchTerm] = useState('');
const { t } = useTranslation();

Expand Down Expand Up @@ -132,8 +138,19 @@ export const ComponentsSelection: React.FC<ComponentsSelectionProps> = ({ compon
value={component.selectedVersion}
disabled={!component.isSelected || providerDisabled}
aria-label={`${component.name} version`}
valueState={component.isSelected && !component.selectedVersion ? 'Negative' : 'None'}
valueStateMessage={
component.isSelected && !component.selectedVersion ? (
<span>{t('ComponentsSelection.chooseVersion')}</span>
) : undefined
}
onChange={handleVersionChange}
>
{!component.selectedVersion && (
<Option key="__placeholder" data-version="" data-name={component.name} selected>
{t('ComponentsSelection.chooseVersion')}
</Option>
)}
{component.versions.map((version) => (
<Option
key={version}
Expand All @@ -155,7 +172,14 @@ export const ComponentsSelection: React.FC<ComponentsSelectionProps> = ({ compon
</Infobox>
)}
</div>

<div data-layout-span="XL4 L4 M4 S4">
{templateDefaultsError ? (
<div style={{ marginBottom: 8 }}>
<IllustratedError title={templateDefaultsError} compact />
</div>
) : null}

{selectedComponents.length > 0 ? (
<List headerText={t('componentsSelection.selectedComponents')}>
{selectedComponents.map((component) => (
Expand Down
97 changes: 74 additions & 23 deletions src/components/ComponentsSelection/ComponentsSelectionContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect, useRef } from 'react';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { ComponentsSelection } from './ComponentsSelection.tsx';

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

export interface ComponentsSelectionProps {
componentsList: ComponentsListItem[];
setComponentsList: (components: ComponentsListItem[]) => void;
managedControlPlaneTemplate?: ManagedControlPlaneTemplate;
}

/**
Expand All @@ -30,39 +32,82 @@ export const getSelectedComponents = (components: ComponentsListItem[]) => {
});
};

type TemplateDefaultComponent = {
name: string;
version: string;
removable?: boolean;
versionChangeable?: boolean;
};

export const ComponentsSelectionContainer: React.FC<ComponentsSelectionProps> = ({
setComponentsList,
componentsList,
managedControlPlaneTemplate,
}) => {
const { data: availableManagedComponentsListData, error, isLoading } = useApiResource(ListManagedComponents());
const { t } = useTranslation();
const initialized = useRef(false);
const [templateDefaultsError, setTemplateDefaultsError] = useState<string | null>(null);
const defaultComponents = useMemo<TemplateDefaultComponent[]>(
() => managedControlPlaneTemplate?.spec?.spec?.components?.defaultComponents ?? [],
[managedControlPlaneTemplate],
);

useEffect(() => {
if (
initialized.current ||
!availableManagedComponentsListData?.items ||
availableManagedComponentsListData.items.length === 0
) {
const items = availableManagedComponentsListData?.items ?? [];

if (!items.length) {
if (!initialized.current) return;
setTemplateDefaultsError(null);
return;
}

if (!initialized.current) {
const newComponentsList = items
.map((item) => {
const versions = sortVersions(item.status.versions);
const template = defaultComponents.find((dc) => dc.name === item.metadata.name);
const templateVersion = template?.version;
const selectedVersion = template
? templateVersion && versions.includes(templateVersion)
? templateVersion
: ''
: (versions[0] ?? '');
return {
name: item.metadata.name,
versions,
selectedVersion,
isSelected: !!template,
documentationUrl: '',
};
})
.filter((component) => !removeComponents.find((item) => item === component.name));

setComponentsList(newComponentsList);
initialized.current = true;
}

if (!defaultComponents.length) {
setTemplateDefaultsError(null);
return;
}

const newComponentsList = availableManagedComponentsListData.items
.map((item) => {
const versions = sortVersions(item.status.versions);
return {
name: item.metadata.name,
versions,
selectedVersion: versions[0] ?? '',
isSelected: false,
documentationUrl: '',
};
})
.filter((component) => !removeComponents.find((item) => item === component.name));

setComponentsList(newComponentsList);
initialized.current = true;
}, [availableManagedComponentsListData, setComponentsList]);
const errors: string[] = [];
defaultComponents.forEach((dc: TemplateDefaultComponent) => {
if (!dc?.name) return;
const item = items.find((it) => it.metadata.name === dc.name);
if (!item) {
errors.push(`Component "${dc.name}" from template is not available.`);
return;
}
const versions: string[] = Array.isArray(item.status?.versions) ? item.status.versions : [];
if (dc.version && !versions.includes(dc.version)) {
errors.push(`Component "${dc.name}" version "${dc.version}" from template is not available.`);
}
});

setTemplateDefaultsError(errors.length ? errors.join('\n') : null);
}, [availableManagedComponentsListData, defaultComponents, setComponentsList]);

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

return <ComponentsSelection componentsList={componentsList} setComponentsList={setComponentsList} />;
return (
<ComponentsSelection
componentsList={componentsList}
setComponentsList={setComponentsList}
templateDefaultsError={templateDefaultsError || undefined}
/>
);
};
27 changes: 25 additions & 2 deletions src/components/ControlPlanes/ControlPlanesListMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,27 @@ import '@ui5/webcomponents-icons/dist/copy';
import '@ui5/webcomponents-icons/dist/accept';

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

type ControlPlanesListMenuProps = {
setDialogDeleteWsIsOpen: Dispatch<SetStateAction<boolean>>;
setIsCreateManagedControlPlaneWizardOpen: Dispatch<SetStateAction<boolean>>;
setInitialTemplateName: Dispatch<SetStateAction<string | undefined>>;
};

export const ControlPlanesListMenu: FC<ControlPlanesListMenuProps> = ({
setDialogDeleteWsIsOpen,
setIsCreateManagedControlPlaneWizardOpen,
setInitialTemplateName,
}) => {
const popoverRef = useRef<MenuDomRef>(null);
const [open, setOpen] = useState(false);

const { t } = useTranslation();

// Here we will pass template list from OnboardingAPI
const allTemplates: ManagedControlPlaneTemplate[] = [];

const handleOpenerClick = (e: Ui5CustomEvent<ButtonDomRef, ButtonClickEventDetail>) => {
if (popoverRef.current && e.currentTarget) {
popoverRef.current.opener = e.currentTarget as HTMLElement;
Expand All @@ -34,14 +40,20 @@ export const ControlPlanesListMenu: FC<ControlPlanesListMenuProps> = ({
ref={popoverRef}
open={open}
onItemClick={(event) => {
const action = (event.detail.item as HTMLElement).dataset.action;
const item = event.detail.item as HTMLElement;
const action = item.dataset.action;
if (action === 'newManagedControlPlane') {
setInitialTemplateName(undefined);
setIsCreateManagedControlPlaneWizardOpen(true);
}
if (action === 'newManagedControlPlaneWithTemplate') {
const tplName = item.dataset.templateName || undefined;
setInitialTemplateName(tplName);
setIsCreateManagedControlPlaneWizardOpen(true);
}
if (action === 'deleteWorkspace') {
setDialogDeleteWsIsOpen(true);
}

setOpen(false);
}}
>
Expand All @@ -51,6 +63,17 @@ export const ControlPlanesListMenu: FC<ControlPlanesListMenuProps> = ({
data-action="newManagedControlPlane"
icon="add"
/>
{allTemplates.map((tpl) => (
<MenuItem
key={`tpl-${tpl.metadata.name}`}
text={tpl.metadata.name}
title={tpl.metadata.descriptionText || ''}
data-action="newManagedControlPlaneWithTemplate"
data-template-name={tpl.metadata.name}
icon="document-text"
/>
))}

<MenuItem
key={'delete'}
text={t('ControlPlaneListToolbar.deleteWorkspace')}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import '@ui5/webcomponents-icons/dist/delete';
import { CopyButton } from '../../Shared/CopyButton.tsx';
import { ControlPlaneCard } from '../ControlPlaneCard/ControlPlaneCard.tsx';
import { ListWorkspacesType, isWorkspaceReady } from '../../../lib/api/types/crate/listWorkspaces.ts';
import { useState } from 'react';
import { useMemo, useState } from 'react';
import { MembersAvatarView } from './MembersAvatarView.tsx';
import { DeleteWorkspaceResource, DeleteWorkspaceType } from '../../../lib/api/types/crate/deleteWorkspace.ts';
import { useApiResourceMutation, useApiResource } from '../../../lib/api/useApiResource.ts';
Expand All @@ -32,6 +32,7 @@ interface Props {

export function ControlPlaneListWorkspaceGridTile({ projectName, workspace }: Props) {
const [isCreateManagedControlPlaneWizardOpen, setIsCreateManagedControlPlaneWizardOpen] = useState(false);
const [initialTemplateName, setInitialTemplateName] = useState<string | undefined>(undefined);
const workspaceName = workspace.metadata.name;
const workspaceDisplayName = workspace.metadata.annotations?.[DISPLAY_NAME_ANNOTATION] || '';
const showDisplayName = workspaceDisplayName.length > 0;
Expand Down Expand Up @@ -68,6 +69,24 @@ export function ControlPlaneListWorkspaceGridTile({ projectName, workspace }: Pr
return null;
}

const uniqueMembers = useMemo(() => {
const seenKeys = new Set<string>();
const fallbackNamespace = workspace.status?.namespace ?? '';

return (workspace.spec.members ?? []).filter((member: { name?: string; namespace?: string }) => {
const memberNamespace = member?.namespace ?? fallbackNamespace;
const memberName = String(member?.name ?? '')
.trim()
.toLowerCase();
if (!memberName) return false;

const dedupeKey = `${memberNamespace}::${memberName}`;
if (seenKeys.has(dedupeKey)) return false;
seenKeys.add(dedupeKey);
return true;
});
}, [workspace.spec.members, workspace.status?.namespace]);

return (
<>
<ObjectPageSection
Expand Down Expand Up @@ -98,7 +117,7 @@ export function ControlPlaneListWorkspaceGridTile({ projectName, workspace }: Pr

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

<MembersAvatarView members={workspace.spec.members} project={projectName} workspace={workspaceName} />
<MembersAvatarView members={uniqueMembers} project={projectName} workspace={workspaceName} />
<FlexBox justifyContent={'SpaceBetween'} gap={10}>
<YamlViewButtonWithLoader
workspaceName={workspace.metadata.namespace}
Expand All @@ -108,6 +127,7 @@ export function ControlPlaneListWorkspaceGridTile({ projectName, workspace }: Pr
<ControlPlanesListMenu
setDialogDeleteWsIsOpen={setDialogDeleteWsIsOpen}
setIsCreateManagedControlPlaneWizardOpen={setIsCreateManagedControlPlaneWizardOpen}
setInitialTemplateName={setInitialTemplateName}
/>
</FlexBox>
</div>
Expand Down Expand Up @@ -168,6 +188,7 @@ export function ControlPlaneListWorkspaceGridTile({ projectName, workspace }: Pr
setIsOpen={setIsCreateManagedControlPlaneWizardOpen}
projectName={projectNamespace}
workspaceName={workspaceName}
initialTemplateName={initialTemplateName}
/>
</>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
.affixRow {
display: flex;
gap: 8px;
align-items: center;
}

.input {
width: 100%;
margin-bottom: 2rem;
Expand Down
Loading
Loading