Skip to content
Closed
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
31 changes: 30 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 { 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,24 @@
value={component.selectedVersion}
disabled={!component.isSelected || providerDisabled}
aria-label={`${component.name} version`}
onChange={handleVersionChange}

Check warning on line 141 in src/components/ComponentsSelection/ComponentsSelection.tsx

View workflow job for this annotation

GitHub Actions / build / build-and-test

Callbacks must be listed after all other props
valueState={component.isSelected && !component.selectedVersion ? 'Negative' : 'None'}
valueStateMessage={
component.isSelected && !component.selectedVersion ? (
<span>{t('ComponentsSelection.chooseVersion')}</span>
) : undefined
}
>
{!component.selectedVersion && (
<Option

Check failure on line 150 in src/components/ComponentsSelection/ComponentsSelection.tsx

View workflow job for this annotation

GitHub Actions / build / build-and-test

Replace `⏎··························key="__placeholder"⏎··························data-version=""⏎··························data-name={component.name}⏎··························selected⏎························` with `·key="__placeholder"·data-version=""·data-name={component.name}·selected`
key="__placeholder"
data-version=""
data-name={component.name}
selected
>
{t('ComponentsSelection.chooseVersion')}
</Option>
)}
{component.versions.map((version) => (
<Option
key={version}
Expand All @@ -155,7 +177,14 @@
</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
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect, useRef } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import { ComponentsSelection } from './ComponentsSelection.tsx';

import IllustratedError from '../Shared/IllustratedError.tsx';
Expand All @@ -9,10 +9,12 @@
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 @@ -33,10 +35,13 @@
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 = managedControlPlaneTemplate?.spec?.spec?.components?.defaultComponents ?? [];

Check warning on line 44 in src/components/ComponentsSelection/ComponentsSelectionContainer.tsx

View workflow job for this annotation

GitHub Actions / build / build-and-test

The 'defaultComponents' logical expression could make the dependencies of useEffect Hook (at line 119) change on every render. To fix this, wrap the initialization of 'defaultComponents' in its own useMemo() Hook

Check warning on line 44 in src/components/ComponentsSelection/ComponentsSelectionContainer.tsx

View workflow job for this annotation

GitHub Actions / build / build-and-test

The 'defaultComponents' logical expression could make the dependencies of useEffect Hook (at line 99) change on every render. To fix this, wrap the initialization of 'defaultComponents' in its own useMemo() Hook

Check warning on line 44 in src/components/ComponentsSelection/ComponentsSelectionContainer.tsx

View workflow job for this annotation

GitHub Actions / build / build-and-test

The 'defaultComponents' logical expression could make the dependencies of useEffect Hook (at line 75) change on every render. To fix this, wrap the initialization of 'defaultComponents' in its own useMemo() Hook

useEffect(() => {
if (
Expand All @@ -50,19 +55,68 @@
const newComponentsList = availableManagedComponentsListData.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 : '')

Check failure on line 61 in src/components/ComponentsSelection/ComponentsSelectionContainer.tsx

View workflow job for this annotation

GitHub Actions / build / build-and-test

Replace `(templateVersion·&&·versions.includes(templateVersion)·?·templateVersion·:·'')` with `templateVersion·&&·versions.includes(templateVersion)⏎············?·templateVersion⏎············:·''`
: (versions[0] ?? '');
return {
name: item.metadata.name,
versions,
selectedVersion: versions[0] ?? '',
isSelected: false,
selectedVersion,
isSelected: !!template,
documentationUrl: '',
};
})
.filter((component) => !removeComponents.find((item) => item === component.name));

setComponentsList(newComponentsList);
initialized.current = true;
}, [availableManagedComponentsListData, setComponentsList]);
}, [availableManagedComponentsListData, setComponentsList, defaultComponents]);

useEffect(() => {
const items = availableManagedComponentsListData?.items ?? [];
if (items.length === 0 || !defaultComponents.length) {
setTemplateDefaultsError(null);
return;
}

const errs: string[] = [];
defaultComponents.forEach((dc: any) => {

Check failure on line 85 in src/components/ComponentsSelection/ComponentsSelectionContainer.tsx

View workflow job for this annotation

GitHub Actions / build / build-and-test

Unexpected any. Specify a different type
if (!dc?.name) return;
const item = items.find((it) => it.metadata.name === dc.name);
if (!item) {
errs.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)) {
errs.push(`Component "${dc.name}" version "${dc.version}" from template is not available.`);
}
});

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

useEffect(() => {
if (!initialized.current) return;
if (!defaultComponents?.length) return;
if (!componentsList?.length) return;

const anySelected = componentsList.some((c) => c.isSelected);
if (anySelected) return;

const updated = componentsList.map((c) => {
const template = defaultComponents.find((dc) => dc.name === c.name);
if (!template) return c;
const templateVersion = template.version;
const selectedVersion =
templateVersion && Array.isArray(c.versions) && c.versions.includes(templateVersion) ? templateVersion : '';
return { ...c, isSelected: true, selectedVersion };
});

setComponentsList(updated);
}, [defaultComponents, componentsList, setComponentsList]);

if (isLoading) {
return <Loading />;
Expand All @@ -77,5 +131,11 @@
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/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,

Check failure on line 19 in src/components/ControlPlanes/ControlPlanesListMenu.tsx

View workflow job for this annotation

GitHub Actions / build / build-and-test

Delete `·`
}) => {
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 @@
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 @@
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 { 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 @@

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,22 @@
return null;
}

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

return (workspace.spec.members ?? []).filter((member: any) => {

Check failure on line 76 in src/components/ControlPlanes/List/ControlPlaneListWorkspaceGridTile.tsx

View workflow job for this annotation

GitHub Actions / build / build-and-test

Unexpected any. Specify a different type
const memberNamespace = member?.namespace ?? fallbackNamespace;
const memberName = String(member?.name ?? '').trim().toLowerCase();

Check failure on line 78 in src/components/ControlPlanes/List/ControlPlaneListWorkspaceGridTile.tsx

View workflow job for this annotation

GitHub Actions / build / build-and-test

Replace `.trim()` with `⏎········.trim()⏎········`
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 +115,7 @@

<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 +125,7 @@
<ControlPlanesListMenu
setDialogDeleteWsIsOpen={setDialogDeleteWsIsOpen}
setIsCreateManagedControlPlaneWizardOpen={setIsCreateManagedControlPlaneWizardOpen}
setInitialTemplateName={setInitialTemplateName}
/>
</FlexBox>
</div>
Expand Down Expand Up @@ -168,6 +186,7 @@
setIsOpen={setIsCreateManagedControlPlaneWizardOpen}
projectName={projectNamespace}
workspaceName={workspaceName}
initialTemplateName={initialTemplateName}
/>
</>
);
Expand Down
Loading
Loading