From 6ce3e65821bb202a2983aec73599b084bf61ee11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Goral?= Date: Thu, 27 Nov 2025 12:26:58 +0100 Subject: [PATCH 01/15] fix --- src/lib/api/types/crate/controlPlanes.ts | 15 +++++++++- src/spaces/mcp/pages/McpPage.tsx | 38 +++++++++++++++++++++++- 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/src/lib/api/types/crate/controlPlanes.ts b/src/lib/api/types/crate/controlPlanes.ts index 1176614e..57721ea4 100644 --- a/src/lib/api/types/crate/controlPlanes.ts +++ b/src/lib/api/types/crate/controlPlanes.ts @@ -20,6 +20,9 @@ export interface ControlPlaneType { enableSystemIdentityProvider?: boolean; }; components: ControlPlaneComponentsType; + authorization?: { + roleBindings?: RoleBinding[]; + }; } | undefined; status: ControlPlaneStatusType | undefined; @@ -85,6 +88,16 @@ export const ControlPlane = ( ): Resource => { return { path: `/apis/core.openmcp.cloud/v1alpha1/namespaces/project-${projectName}--ws-${workspaceName}/managedcontrolplanes/${controlPlaneName}`, - jq: '{ spec: .spec | {components}, metadata: .metadata | {name, namespace, creationTimestamp, annotations}, status: { conditions: [.status.conditions[] | {type: .type, status: .status, message: .message, reason: .reason, lastTransitionTime: .lastTransitionTime}], access: .status.components.authentication.access, status: .status.status }}', + jq: '{ spec: .spec | {components, authorization: {roleBindings: .authorization.roleBindings}}, metadata: .metadata | {name, namespace, creationTimestamp, annotations}, status: { conditions: [.status.conditions[] | {type: .type, status: .status, message: .message, reason: .reason, lastTransitionTime: .lastTransitionTime}], access: .status.components.authentication.access, status: .status.status }}', }; }; + +export interface RoleBinding { + role: string; + subjects: Subject[]; +} + +export interface Subject { + kind: string; + name: string; +} diff --git a/src/spaces/mcp/pages/McpPage.tsx b/src/spaces/mcp/pages/McpPage.tsx index b33e3bde..f8759771 100644 --- a/src/spaces/mcp/pages/McpPage.tsx +++ b/src/spaces/mcp/pages/McpPage.tsx @@ -24,7 +24,8 @@ import { Providers } from '../../../components/ControlPlane/Providers.tsx'; import ComponentList from '../../../components/ControlPlane/ComponentList.tsx'; import MCPHealthPopoverButton from '../../../components/ControlPlane/MCPHealthPopoverButton.tsx'; import { useApiResource } from '../../../lib/api/useApiResource.ts'; - +import { MembersAvatarView } from '../../../components/ControlPlanes/List/MembersAvatarView.tsx'; +import { Member } from '../../../lib/api/types/shared/members.ts'; import { YamlViewButton } from '../../../components/Yaml/YamlViewButton.tsx'; import { Landscapers } from '../../../components/ControlPlane/Landscapers.tsx'; import { AuthProviderMcp } from '../auth/AuthContextMcp.tsx'; @@ -44,6 +45,37 @@ import { ComponentsDashboard } from '../components/ComponentsDashboard/Component export type McpPageSectionId = 'overview' | 'crossplane' | 'flux' | 'landscapers'; +// Helper function to convert roleBindings to Members +function convertRoleBindingsToMembers( + roleBindings?: { role: string; subjects: { kind: string; name: string }[] }[], +): Member[] { + if (!roleBindings) return []; + + const memberMap = new Map(); + + for (const binding of roleBindings) { + for (const subject of binding.subjects) { + const key = `${subject.kind}-${subject.name}`; + if (memberMap.has(key)) { + // Add role to existing member + const member = memberMap.get(key)!; + if (!member.roles.includes(binding.role)) { + member.roles.push(binding.role); + } + } else { + // Create new member + memberMap.set(key, { + kind: subject.kind, + name: subject.name, + roles: [binding.role], + }); + } + } + } + + return Array.from(memberMap.values()); +} + export default function McpPage() { const { projectName, workspaceName, controlPlaneName } = useParams(); const { t } = useTranslation(); @@ -85,6 +117,9 @@ export default function McpPage() { const isComponentInstalledFlux = !!mcp.spec?.components.flux; const isComponentInstalledLandscaper = !!mcp.spec?.components.landscaper; + // Convert roleBindings to members for MembersAvatarView + const members = convertRoleBindingsToMembers(mcp.spec?.authorization?.roleBindings); + return ( + Date: Fri, 28 Nov 2025 08:54:20 +0100 Subject: [PATCH 02/15] refactor --- .../ControlPlanes/List/MembersAvatarView.tsx | 5 ++- .../McpMembersAvatarView.tsx | 14 ++++++ src/components/Members/MemberTable.tsx | 11 +++-- src/hooks/useConvertRoleBindingsToMembers.ts | 31 +++++++++++++ src/spaces/mcp/pages/McpPage.tsx | 45 ++++--------------- 5 files changed, 64 insertions(+), 42 deletions(-) create mode 100644 src/components/ControlPlanes/McpMembersAvatarView/McpMembersAvatarView.tsx create mode 100644 src/hooks/useConvertRoleBindingsToMembers.ts diff --git a/src/components/ControlPlanes/List/MembersAvatarView.tsx b/src/components/ControlPlanes/List/MembersAvatarView.tsx index 94e7b30b..6ac0c427 100644 --- a/src/components/ControlPlanes/List/MembersAvatarView.tsx +++ b/src/components/ControlPlanes/List/MembersAvatarView.tsx @@ -10,9 +10,10 @@ interface Props { project?: string; workspace?: string; members: Member[]; + hideNamespaceColumn?: boolean; } -export function MembersAvatarView({ members, project, workspace }: Props) { +export function MembersAvatarView({ members, project, workspace, hideNamespaceColumn = false }: Props) { const openerId = useId(); const [popoverIsOpen, setPopoverIsOpen] = useState(false); const avatars = []; @@ -44,7 +45,7 @@ export function MembersAvatarView({ members, project, workspace }: Props) { setPopoverIsOpen(false); }} > - + ); diff --git a/src/components/ControlPlanes/McpMembersAvatarView/McpMembersAvatarView.tsx b/src/components/ControlPlanes/McpMembersAvatarView/McpMembersAvatarView.tsx new file mode 100644 index 00000000..ea9ae4af --- /dev/null +++ b/src/components/ControlPlanes/McpMembersAvatarView/McpMembersAvatarView.tsx @@ -0,0 +1,14 @@ +import { MembersAvatarView } from '../List/MembersAvatarView.tsx'; +import { useConvertRoleBindingsToMembers } from '../../../hooks/useConvertRoleBindingsToMembers.ts'; + +interface Props { + project?: string; + workspace?: string; + roleBindings?: { role: string; subjects: { kind: string; name: string }[] }[]; +} + +export function McpMembersAvatarView({ roleBindings, project, workspace }: Props) { + const members = useConvertRoleBindingsToMembers(roleBindings); + + return ; +} diff --git a/src/components/Members/MemberTable.tsx b/src/components/Members/MemberTable.tsx index 1a62131f..5f8cc0b4 100644 --- a/src/components/Members/MemberTable.tsx +++ b/src/components/Members/MemberTable.tsx @@ -20,6 +20,7 @@ type MemberTableProps = { onEditMember?: (member: Member) => void; isValidationError?: boolean; requireAtLeastOneMember: boolean; + hideNamespaceColumn?: boolean; }; export const MemberTable: FC = ({ @@ -28,6 +29,7 @@ export const MemberTable: FC = ({ onEditMember, isValidationError = false, requireAtLeastOneMember, + hideNamespaceColumn = false, }) => { const { t } = useTranslation(); @@ -56,11 +58,14 @@ export const MemberTable: FC = ({ accessor: 'role', width: 105, }, - { + ]; + + if (!hideNamespaceColumn) { + columns.push({ Header: t('MemberTable.columnNamespaceHeader'), accessor: 'namespace', - }, - ]; + }); + } if (onEditMember && onDeleteMember) { columns.push({ diff --git a/src/hooks/useConvertRoleBindingsToMembers.ts b/src/hooks/useConvertRoleBindingsToMembers.ts new file mode 100644 index 00000000..5d3a8e79 --- /dev/null +++ b/src/hooks/useConvertRoleBindingsToMembers.ts @@ -0,0 +1,31 @@ +import { Member } from '../lib/api/types/shared/members'; + +export function useConvertRoleBindingsToMembers( + roleBindings?: { role: string; subjects: { kind: string; name: string }[] }[], +): Member[] { + if (!roleBindings) return []; + + const memberMap = new Map(); + + for (const binding of roleBindings) { + for (const subject of binding.subjects) { + const key = `${subject.kind}-${subject.name}`; + if (memberMap.has(key)) { + // Add role to existing member + const member = memberMap.get(key)!; + if (!member.roles.includes(binding.role)) { + member.roles.push(binding.role); + } + } else { + // Create new member + memberMap.set(key, { + kind: subject.kind, + name: subject.name, + roles: [binding.role], + }); + } + } + } + + return Array.from(memberMap.values()); +} diff --git a/src/spaces/mcp/pages/McpPage.tsx b/src/spaces/mcp/pages/McpPage.tsx index f8759771..5cd5b769 100644 --- a/src/spaces/mcp/pages/McpPage.tsx +++ b/src/spaces/mcp/pages/McpPage.tsx @@ -24,8 +24,7 @@ import { Providers } from '../../../components/ControlPlane/Providers.tsx'; import ComponentList from '../../../components/ControlPlane/ComponentList.tsx'; import MCPHealthPopoverButton from '../../../components/ControlPlane/MCPHealthPopoverButton.tsx'; import { useApiResource } from '../../../lib/api/useApiResource.ts'; -import { MembersAvatarView } from '../../../components/ControlPlanes/List/MembersAvatarView.tsx'; -import { Member } from '../../../lib/api/types/shared/members.ts'; +import { McpMembersAvatarView } from '../../../components/ControlPlanes/McpMembersAvatarView/McpMembersAvatarView.tsx'; import { YamlViewButton } from '../../../components/Yaml/YamlViewButton.tsx'; import { Landscapers } from '../../../components/ControlPlane/Landscapers.tsx'; import { AuthProviderMcp } from '../auth/AuthContextMcp.tsx'; @@ -45,37 +44,6 @@ import { ComponentsDashboard } from '../components/ComponentsDashboard/Component export type McpPageSectionId = 'overview' | 'crossplane' | 'flux' | 'landscapers'; -// Helper function to convert roleBindings to Members -function convertRoleBindingsToMembers( - roleBindings?: { role: string; subjects: { kind: string; name: string }[] }[], -): Member[] { - if (!roleBindings) return []; - - const memberMap = new Map(); - - for (const binding of roleBindings) { - for (const subject of binding.subjects) { - const key = `${subject.kind}-${subject.name}`; - if (memberMap.has(key)) { - // Add role to existing member - const member = memberMap.get(key)!; - if (!member.roles.includes(binding.role)) { - member.roles.push(binding.role); - } - } else { - // Create new member - memberMap.set(key, { - kind: subject.kind, - name: subject.name, - roles: [binding.role], - }); - } - } - } - - return Array.from(memberMap.values()); -} - export default function McpPage() { const { projectName, workspaceName, controlPlaneName } = useParams(); const { t } = useTranslation(); @@ -117,9 +85,6 @@ export default function McpPage() { const isComponentInstalledFlux = !!mcp.spec?.components.flux; const isComponentInstalledLandscaper = !!mcp.spec?.components.landscaper; - // Convert roleBindings to members for MembersAvatarView - const members = convertRoleBindingsToMembers(mcp.spec?.authorization?.roleBindings); - return ( - + + + Date: Tue, 2 Dec 2025 13:58:12 +0100 Subject: [PATCH 03/15] refactor --- .../McpMembersAvatarView.module.css | 5 +++++ .../McpMembersAvatarView.tsx | 14 ++++++++++++-- src/spaces/mcp/pages/McpPage.tsx | 16 +++++++++------- 3 files changed, 26 insertions(+), 9 deletions(-) create mode 100644 src/components/ControlPlanes/McpMembersAvatarView/McpMembersAvatarView.module.css diff --git a/src/components/ControlPlanes/McpMembersAvatarView/McpMembersAvatarView.module.css b/src/components/ControlPlanes/McpMembersAvatarView/McpMembersAvatarView.module.css new file mode 100644 index 00000000..ed74e9a3 --- /dev/null +++ b/src/components/ControlPlanes/McpMembersAvatarView/McpMembersAvatarView.module.css @@ -0,0 +1,5 @@ +.membersTitle { + margin-left: 5px; + font-weight: bold; + margin-bottom: 0.5rem; +} diff --git a/src/components/ControlPlanes/McpMembersAvatarView/McpMembersAvatarView.tsx b/src/components/ControlPlanes/McpMembersAvatarView/McpMembersAvatarView.tsx index ea9ae4af..c644a5cd 100644 --- a/src/components/ControlPlanes/McpMembersAvatarView/McpMembersAvatarView.tsx +++ b/src/components/ControlPlanes/McpMembersAvatarView/McpMembersAvatarView.tsx @@ -1,5 +1,8 @@ import { MembersAvatarView } from '../List/MembersAvatarView.tsx'; import { useConvertRoleBindingsToMembers } from '../../../hooks/useConvertRoleBindingsToMembers.ts'; +import { FlexBox, Text } from '@ui5/webcomponents-react'; +import { useTranslation } from 'react-i18next'; +import styles from './McpMembersAvatarView.module.css'; interface Props { project?: string; @@ -9,6 +12,13 @@ interface Props { export function McpMembersAvatarView({ roleBindings, project, workspace }: Props) { const members = useConvertRoleBindingsToMembers(roleBindings); - - return ; + const { t } = useTranslation(); + return ( + + + {t('common.members')} ({members.length}): + + + + ); } diff --git a/src/spaces/mcp/pages/McpPage.tsx b/src/spaces/mcp/pages/McpPage.tsx index 5cd5b769..93632bc9 100644 --- a/src/spaces/mcp/pages/McpPage.tsx +++ b/src/spaces/mcp/pages/McpPage.tsx @@ -1,5 +1,6 @@ import { BusyIndicator, + FlexBox, ObjectPage, ObjectPageHeader, ObjectPageSection, @@ -105,12 +106,6 @@ export default function McpPage() { //TODO: actionBar should use Toolbar and ToolbarButton for consistent design actionsBar={
- - - + + + + } onSelectedSectionChange={() => setSelectedSectionId(undefined)} From 79ff133b71947135341f19b6d767709d78472d80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Goral?= Date: Tue, 2 Dec 2025 14:28:23 +0100 Subject: [PATCH 04/15] refactor --- .../ControlPlane/MCPHealthPopoverButton.tsx | 23 ++++++++++++++++++- .../Helper/AnimatedHoverTextButton.module.css | 11 +++++++++ .../Helper/AnimatedHoverTextButton.tsx | 21 +++++++++-------- src/spaces/mcp/pages/McpPage.tsx | 14 +++++------ 4 files changed, 51 insertions(+), 18 deletions(-) create mode 100644 src/components/Helper/AnimatedHoverTextButton.module.css diff --git a/src/components/ControlPlane/MCPHealthPopoverButton.tsx b/src/components/ControlPlane/MCPHealthPopoverButton.tsx index 389c7623..82de4d90 100644 --- a/src/components/ControlPlane/MCPHealthPopoverButton.tsx +++ b/src/components/ControlPlane/MCPHealthPopoverButton.tsx @@ -30,9 +30,16 @@ type MCPHealthPopoverButtonProps = { projectName: string; workspaceName: string; mcpName: string; + large?: boolean; }; -const MCPHealthPopoverButton = ({ mcpStatus, projectName, workspaceName, mcpName }: MCPHealthPopoverButtonProps) => { +const MCPHealthPopoverButton = ({ + mcpStatus, + projectName, + workspaceName, + mcpName, + large = false, +}: MCPHealthPopoverButtonProps) => { const popoverRef = useRef(null); const [open, setOpen] = useState(false); const { githubIssuesSupportTicket } = useLink(); @@ -143,6 +150,7 @@ const MCPHealthPopoverButton = ({ mcpStatus, projectName, workspaceName, mcpName @@ -181,6 +189,19 @@ const StatusTable = ({ status, tableColumns, githubIssuesLink }: StatusTableProp ); }; +export const getClassNameForOverallStatus = (status: ReadyStatus | undefined): string => { + switch (status) { + case ReadyStatus.Ready: + return 'ready'; + case ReadyStatus.NotReady: + return 'not-ready'; + case ReadyStatus.InDeletion: + return 'deleting'; + default: + return ''; + } +}; + const getIconForOverallStatus = (status: ReadyStatus | undefined): JSX.Element => { switch (status) { case ReadyStatus.Ready: diff --git a/src/components/Helper/AnimatedHoverTextButton.module.css b/src/components/Helper/AnimatedHoverTextButton.module.css new file mode 100644 index 00000000..a1a20e2a --- /dev/null +++ b/src/components/Helper/AnimatedHoverTextButton.module.css @@ -0,0 +1,11 @@ +.ready { + color: var(--sapPositiveColor); +} + +.not-ready { + color: var(--sapNegativeColor); +} + +.deleting { + color: var(--sapCriticalColor); +} diff --git a/src/components/Helper/AnimatedHoverTextButton.tsx b/src/components/Helper/AnimatedHoverTextButton.tsx index f68f7b1e..e0b27a26 100644 --- a/src/components/Helper/AnimatedHoverTextButton.tsx +++ b/src/components/Helper/AnimatedHoverTextButton.tsx @@ -4,29 +4,30 @@ import { JSX, useId, useState } from 'react'; import ButtonDesign from '@ui5/webcomponents/dist/types/ButtonDesign.js'; import type { Ui5CustomEvent } from '@ui5/webcomponents-react-base'; import type { ButtonClickEventDetail } from '@ui5/webcomponents/dist/Button.js'; - +import styles from './AnimatedHoverTextButton.module.css'; +import { getClassNameForOverallStatus } from '../ControlPlane/MCPHealthPopoverButton.tsx'; +import { ReadyStatus } from '../../lib/api/types/crate/controlPlanes.ts'; type HoverTextButtonProps = { id?: string; text: string; icon: JSX.Element; onClick: (event: Ui5CustomEvent) => void; + large?: boolean; }; -export const AnimatedHoverTextButton = ({ id, text, icon, onClick }: HoverTextButtonProps) => { +export const AnimatedHoverTextButton = ({ id, text, icon, onClick, large = false }: HoverTextButtonProps) => { const [hover, setHover] = useState(false); const generatedId = useId(); id ??= generatedId; return ( - diff --git a/src/spaces/mcp/pages/McpPage.tsx b/src/spaces/mcp/pages/McpPage.tsx index 62348617..678de89e 100644 --- a/src/spaces/mcp/pages/McpPage.tsx +++ b/src/spaces/mcp/pages/McpPage.tsx @@ -106,13 +106,6 @@ export default function McpPage() { //TODO: actionBar should use Toolbar and ToolbarButton for consistent design actionsBar={
- - + Date: Tue, 2 Dec 2025 14:51:13 +0100 Subject: [PATCH 05/15] fix --- .../ControlPlane/MCPHealthPopoverButton.tsx | 6 +- .../Helper/AnimatedHoverTextButton.module.css | 8 +++ .../Helper/AnimatedHoverTextButton.tsx | 55 +++++++++++++++---- 3 files changed, 54 insertions(+), 15 deletions(-) diff --git a/src/components/ControlPlane/MCPHealthPopoverButton.tsx b/src/components/ControlPlane/MCPHealthPopoverButton.tsx index 82de4d90..67a50465 100644 --- a/src/components/ControlPlane/MCPHealthPopoverButton.tsx +++ b/src/components/ControlPlane/MCPHealthPopoverButton.tsx @@ -205,11 +205,11 @@ export const getClassNameForOverallStatus = (status: ReadyStatus | undefined): s const getIconForOverallStatus = (status: ReadyStatus | undefined): JSX.Element => { switch (status) { case ReadyStatus.Ready: - return ; + return ; case ReadyStatus.NotReady: - return ; + return ; case ReadyStatus.InDeletion: - return ; + return ; default: return <>; } diff --git a/src/components/Helper/AnimatedHoverTextButton.module.css b/src/components/Helper/AnimatedHoverTextButton.module.css index a1a20e2a..edb20d31 100644 --- a/src/components/Helper/AnimatedHoverTextButton.module.css +++ b/src/components/Helper/AnimatedHoverTextButton.module.css @@ -1,3 +1,7 @@ +.text { + margin-right: 0.5rem; +} + .ready { color: var(--sapPositiveColor); } @@ -9,3 +13,7 @@ .deleting { color: var(--sapCriticalColor); } + +.large { + font-size: 1.25rem; +} diff --git a/src/components/Helper/AnimatedHoverTextButton.tsx b/src/components/Helper/AnimatedHoverTextButton.tsx index e0b27a26..03407d07 100644 --- a/src/components/Helper/AnimatedHoverTextButton.tsx +++ b/src/components/Helper/AnimatedHoverTextButton.tsx @@ -1,17 +1,20 @@ -import { Button, ButtonDomRef, FlexBox, FlexBoxAlignItems, Text } from '@ui5/webcomponents-react'; +import { Button, ButtonDomRef, Link, LinkDomRef, FlexBox, FlexBoxAlignItems, Text } from '@ui5/webcomponents-react'; import '@ui5/webcomponents-icons/dist/copy'; import { JSX, useId, useState } from 'react'; -import ButtonDesign from '@ui5/webcomponents/dist/types/ButtonDesign.js'; import type { Ui5CustomEvent } from '@ui5/webcomponents-react-base'; import type { ButtonClickEventDetail } from '@ui5/webcomponents/dist/Button.js'; +import type { LinkClickEventDetail } from '@ui5/webcomponents/dist/Link.js'; import styles from './AnimatedHoverTextButton.module.css'; import { getClassNameForOverallStatus } from '../ControlPlane/MCPHealthPopoverButton.tsx'; import { ReadyStatus } from '../../lib/api/types/crate/controlPlanes.ts'; +import cx from 'clsx'; type HoverTextButtonProps = { id?: string; text: string; icon: JSX.Element; - onClick: (event: Ui5CustomEvent) => void; + onClick: ( + event: Ui5CustomEvent | Ui5CustomEvent, + ) => void; large?: boolean; }; export const AnimatedHoverTextButton = ({ id, text, icon, onClick, large = false }: HoverTextButtonProps) => { @@ -20,16 +23,44 @@ export const AnimatedHoverTextButton = ({ id, text, icon, onClick, large = false const generatedId = useId(); id ??= generatedId; + const content = ( + + {hover || large ? ( + + {text} + + ) : null} + {icon} + + ); + + if (large) { + return ( + setHover(false)} + onMouseOver={() => setHover(true)} + > + {content} + + ); + } + return ( - ); }; From d19f369a1190a14732de4d64c8295092dcb0f054 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Goral?= Date: Tue, 2 Dec 2025 14:53:36 +0100 Subject: [PATCH 06/15] Update MCPHealthPopoverButton.tsx --- src/components/ControlPlane/MCPHealthPopoverButton.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/ControlPlane/MCPHealthPopoverButton.tsx b/src/components/ControlPlane/MCPHealthPopoverButton.tsx index 67a50465..a8038d51 100644 --- a/src/components/ControlPlane/MCPHealthPopoverButton.tsx +++ b/src/components/ControlPlane/MCPHealthPopoverButton.tsx @@ -7,12 +7,14 @@ import { Button, PopoverDomRef, ButtonDomRef, + LinkDomRef, } from '@ui5/webcomponents-react'; import { AnalyticalTableColumnDefinition } from '@ui5/webcomponents-react/wrappers'; import PopoverPlacement from '@ui5/webcomponents/dist/types/PopoverPlacement.js'; import '@ui5/webcomponents-icons/dist/copy'; import { JSX, useRef, useState } from 'react'; import type { ButtonClickEventDetail } from '@ui5/webcomponents/dist/Button.js'; +import type { LinkClickEventDetail } from '@ui5/webcomponents/dist/Link.js'; import { ControlPlaneStatusType, ReadyStatus, @@ -45,7 +47,9 @@ const MCPHealthPopoverButton = ({ const { githubIssuesSupportTicket } = useLink(); const { t } = useTranslation(); - const handleOpenerClick = (event: Ui5CustomEvent) => { + const handleOpenerClick = ( + event: Ui5CustomEvent | Ui5CustomEvent, + ) => { if (popoverRef.current) { (popoverRef.current as unknown as { opener: EventTarget | null }).opener = event.target; setOpen((prev) => !prev); From c32b9ef9b15262289a6ce556d662bb36b2d0de7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Goral?= Date: Tue, 2 Dec 2025 15:05:45 +0100 Subject: [PATCH 07/15] fix --- public/locales/en.json | 1 + .../Helper/AnimatedHoverTextButton.module.css | 5 +++++ .../Helper/AnimatedHoverTextButton.tsx | 2 +- src/spaces/mcp/pages/McpPage.tsx | 18 +++++++++++------- 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/public/locales/en.json b/public/locales/en.json index 0e145c5c..b3b8ed93 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -369,6 +369,7 @@ }, "common": { "all": "All", + "status": "Status", "documentation": "Documentation", "close": "Close", "cannotLoadData": "Cannot load data", diff --git a/src/components/Helper/AnimatedHoverTextButton.module.css b/src/components/Helper/AnimatedHoverTextButton.module.css index edb20d31..7314cc0b 100644 --- a/src/components/Helper/AnimatedHoverTextButton.module.css +++ b/src/components/Helper/AnimatedHoverTextButton.module.css @@ -2,6 +2,11 @@ margin-right: 0.5rem; } +.link:focus-within, +.link:focus-visible { + background-color: transparent; +} + .ready { color: var(--sapPositiveColor); } diff --git a/src/components/Helper/AnimatedHoverTextButton.tsx b/src/components/Helper/AnimatedHoverTextButton.tsx index 03407d07..ea284ea9 100644 --- a/src/components/Helper/AnimatedHoverTextButton.tsx +++ b/src/components/Helper/AnimatedHoverTextButton.tsx @@ -42,7 +42,7 @@ export const AnimatedHoverTextButton = ({ id, text, icon, onClick, large = false return ( setHover(false)} onMouseOver={() => setHover(true)} diff --git a/src/spaces/mcp/pages/McpPage.tsx b/src/spaces/mcp/pages/McpPage.tsx index 678de89e..dafefb9e 100644 --- a/src/spaces/mcp/pages/McpPage.tsx +++ b/src/spaces/mcp/pages/McpPage.tsx @@ -6,6 +6,7 @@ import { ObjectPageSection, ObjectPageSubSection, ObjectPageTitle, + Text, } from '@ui5/webcomponents-react'; import { useParams } from 'react-router-dom'; import CopyKubeconfigButton from '../../../components/ControlPlanes/CopyKubeconfigButton.tsx'; @@ -133,13 +134,16 @@ export default function McpPage() { - + + {t('common.status')}: + + Date: Tue, 2 Dec 2025 15:10:47 +0100 Subject: [PATCH 08/15] fix --- .../ControlPlane/McpStatusSection.tsx | 28 +++++++++++++++++++ src/spaces/mcp/pages/McpPage.tsx | 18 +++++------- 2 files changed, 35 insertions(+), 11 deletions(-) create mode 100644 src/components/ControlPlane/McpStatusSection.tsx diff --git a/src/components/ControlPlane/McpStatusSection.tsx b/src/components/ControlPlane/McpStatusSection.tsx new file mode 100644 index 00000000..315b08db --- /dev/null +++ b/src/components/ControlPlane/McpStatusSection.tsx @@ -0,0 +1,28 @@ +import { FlexBox, Text } from '@ui5/webcomponents-react'; +import { useTranslation } from 'react-i18next'; +import MCPHealthPopoverButton from './MCPHealthPopoverButton.tsx'; +import { ControlPlaneStatusType } from '../../lib/api/types/crate/controlPlanes.ts'; + +interface McpStatusSectionProps { + mcpStatus: ControlPlaneStatusType | undefined; + projectName: string; + workspaceName: string; + mcpName: string; +} + +export function McpStatusSection({ mcpStatus, projectName, workspaceName, mcpName }: McpStatusSectionProps) { + const { t } = useTranslation(); + + return ( + + {t('common.status')}: + + + ); +} diff --git a/src/spaces/mcp/pages/McpPage.tsx b/src/spaces/mcp/pages/McpPage.tsx index dafefb9e..05740686 100644 --- a/src/spaces/mcp/pages/McpPage.tsx +++ b/src/spaces/mcp/pages/McpPage.tsx @@ -24,7 +24,6 @@ import { ManagedResources } from '../../../components/ControlPlane/ManagedResour import { ProvidersConfig } from '../../../components/ControlPlane/ProvidersConfig.tsx'; import { Providers } from '../../../components/ControlPlane/Providers.tsx'; import ComponentList from '../../../components/ControlPlane/ComponentList.tsx'; -import MCPHealthPopoverButton from '../../../components/ControlPlane/MCPHealthPopoverButton.tsx'; import { useApiResource } from '../../../lib/api/useApiResource.ts'; import { McpMembersAvatarView } from '../../../components/ControlPlanes/McpMembersAvatarView/McpMembersAvatarView.tsx'; import { YamlViewButton } from '../../../components/Yaml/YamlViewButton.tsx'; @@ -43,6 +42,7 @@ import { GitRepositories } from '../../../components/ControlPlane/GitRepositorie import { Kustomizations } from '../../../components/ControlPlane/Kustomizations.tsx'; import { McpHeader } from '../components/McpHeader/McpHeader.tsx'; import { ComponentsDashboard } from '../components/ComponentsDashboard/ComponentsDashboard.tsx'; +import { McpStatusSection } from '../../../components/ControlPlane/McpStatusSection.tsx'; export type McpPageSectionId = 'overview' | 'crossplane' | 'flux' | 'landscapers'; @@ -134,16 +134,12 @@ export default function McpPage() { - - {t('common.status')}: - - + Date: Tue, 2 Dec 2025 15:19:29 +0100 Subject: [PATCH 09/15] fix --- src/components/ControlPlane/McpStatusSection.module.css | 4 ++++ src/components/ControlPlane/McpStatusSection.tsx | 3 ++- src/spaces/mcp/pages/McpPage.tsx | 1 - 3 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 src/components/ControlPlane/McpStatusSection.module.css diff --git a/src/components/ControlPlane/McpStatusSection.module.css b/src/components/ControlPlane/McpStatusSection.module.css new file mode 100644 index 00000000..03576e73 --- /dev/null +++ b/src/components/ControlPlane/McpStatusSection.module.css @@ -0,0 +1,4 @@ +.statusLabel { + font-weight: bold; + margin-bottom: 1rem; +} diff --git a/src/components/ControlPlane/McpStatusSection.tsx b/src/components/ControlPlane/McpStatusSection.tsx index 315b08db..a6d70b95 100644 --- a/src/components/ControlPlane/McpStatusSection.tsx +++ b/src/components/ControlPlane/McpStatusSection.tsx @@ -2,6 +2,7 @@ import { FlexBox, Text } from '@ui5/webcomponents-react'; import { useTranslation } from 'react-i18next'; import MCPHealthPopoverButton from './MCPHealthPopoverButton.tsx'; import { ControlPlaneStatusType } from '../../lib/api/types/crate/controlPlanes.ts'; +import styles from './McpStatusSection.module.css'; interface McpStatusSectionProps { mcpStatus: ControlPlaneStatusType | undefined; @@ -15,7 +16,7 @@ export function McpStatusSection({ mcpStatus, projectName, workspaceName, mcpNam return ( - {t('common.status')}: + {t('common.status')}: Date: Tue, 2 Dec 2025 16:34:04 +0100 Subject: [PATCH 10/15] fix --- .../McpMembersAvatarView/McpMembersAvatarView.tsx | 4 ++-- .../convertRoleBindingsToMembers.ts} | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) rename src/{hooks/useConvertRoleBindingsToMembers.ts => utils/convertRoleBindingsToMembers.ts} (87%) diff --git a/src/components/ControlPlanes/McpMembersAvatarView/McpMembersAvatarView.tsx b/src/components/ControlPlanes/McpMembersAvatarView/McpMembersAvatarView.tsx index c644a5cd..63a04081 100644 --- a/src/components/ControlPlanes/McpMembersAvatarView/McpMembersAvatarView.tsx +++ b/src/components/ControlPlanes/McpMembersAvatarView/McpMembersAvatarView.tsx @@ -1,5 +1,5 @@ import { MembersAvatarView } from '../List/MembersAvatarView.tsx'; -import { useConvertRoleBindingsToMembers } from '../../../hooks/useConvertRoleBindingsToMembers.ts'; +import { convertRoleBindingsToMembers } from '../../../utils/convertRoleBindingsToMembers.ts'; import { FlexBox, Text } from '@ui5/webcomponents-react'; import { useTranslation } from 'react-i18next'; import styles from './McpMembersAvatarView.module.css'; @@ -11,7 +11,7 @@ interface Props { } export function McpMembersAvatarView({ roleBindings, project, workspace }: Props) { - const members = useConvertRoleBindingsToMembers(roleBindings); + const members = convertRoleBindingsToMembers(roleBindings); const { t } = useTranslation(); return ( diff --git a/src/hooks/useConvertRoleBindingsToMembers.ts b/src/utils/convertRoleBindingsToMembers.ts similarity index 87% rename from src/hooks/useConvertRoleBindingsToMembers.ts rename to src/utils/convertRoleBindingsToMembers.ts index 5d3a8e79..320e752f 100644 --- a/src/hooks/useConvertRoleBindingsToMembers.ts +++ b/src/utils/convertRoleBindingsToMembers.ts @@ -1,6 +1,6 @@ -import { Member } from '../lib/api/types/shared/members'; +import { Member } from '../lib/api/types/shared/members.ts'; -export function useConvertRoleBindingsToMembers( +export function convertRoleBindingsToMembers( roleBindings?: { role: string; subjects: { kind: string; name: string }[] }[], ): Member[] { if (!roleBindings) return []; From e5b2a01d73b825adada011f17283ceb986583226 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Goral?= Date: Tue, 2 Dec 2025 16:39:37 +0100 Subject: [PATCH 11/15] Update AnimatedHoverTextButton.tsx --- src/components/Helper/AnimatedHoverTextButton.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Helper/AnimatedHoverTextButton.tsx b/src/components/Helper/AnimatedHoverTextButton.tsx index ea284ea9..71685eca 100644 --- a/src/components/Helper/AnimatedHoverTextButton.tsx +++ b/src/components/Helper/AnimatedHoverTextButton.tsx @@ -42,7 +42,7 @@ export const AnimatedHoverTextButton = ({ id, text, icon, onClick, large = false return ( setHover(false)} onMouseOver={() => setHover(true)} From de7944621e2b9f2b688fee058ba16c1e2f971ba2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Goral?= Date: Tue, 2 Dec 2025 16:40:45 +0100 Subject: [PATCH 12/15] Update McpPage.tsx --- src/spaces/mcp/pages/McpPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spaces/mcp/pages/McpPage.tsx b/src/spaces/mcp/pages/McpPage.tsx index 5c615cba..df786e28 100644 --- a/src/spaces/mcp/pages/McpPage.tsx +++ b/src/spaces/mcp/pages/McpPage.tsx @@ -136,7 +136,7 @@ export default function McpPage() { Date: Thu, 4 Dec 2025 13:46:09 +0100 Subject: [PATCH 13/15] fix --- .../ControlPlane/McpStatusSection.module.css | 1 + .../Helper/AnimatedHoverTextButton.tsx | 17 ++++++++--------- src/spaces/mcp/pages/McpPage.tsx | 5 +++-- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/components/ControlPlane/McpStatusSection.module.css b/src/components/ControlPlane/McpStatusSection.module.css index 03576e73..9a8a8d26 100644 --- a/src/components/ControlPlane/McpStatusSection.module.css +++ b/src/components/ControlPlane/McpStatusSection.module.css @@ -1,4 +1,5 @@ .statusLabel { font-weight: bold; margin-bottom: 1rem; + margin-left: 0.6rem; } diff --git a/src/components/Helper/AnimatedHoverTextButton.tsx b/src/components/Helper/AnimatedHoverTextButton.tsx index 71685eca..9fc4154f 100644 --- a/src/components/Helper/AnimatedHoverTextButton.tsx +++ b/src/components/Helper/AnimatedHoverTextButton.tsx @@ -1,9 +1,9 @@ -import { Button, ButtonDomRef, Link, LinkDomRef, FlexBox, FlexBoxAlignItems, Text } from '@ui5/webcomponents-react'; +import { Button, ButtonDomRef, FlexBox, FlexBoxAlignItems } from '@ui5/webcomponents-react'; import '@ui5/webcomponents-icons/dist/copy'; import { JSX, useId, useState } from 'react'; import type { Ui5CustomEvent } from '@ui5/webcomponents-react-base'; import type { ButtonClickEventDetail } from '@ui5/webcomponents/dist/Button.js'; -import type { LinkClickEventDetail } from '@ui5/webcomponents/dist/Link.js'; + import styles from './AnimatedHoverTextButton.module.css'; import { getClassNameForOverallStatus } from '../ControlPlane/MCPHealthPopoverButton.tsx'; import { ReadyStatus } from '../../lib/api/types/crate/controlPlanes.ts'; @@ -12,9 +12,7 @@ type HoverTextButtonProps = { id?: string; text: string; icon: JSX.Element; - onClick: ( - event: Ui5CustomEvent | Ui5CustomEvent, - ) => void; + onClick: (event: Ui5CustomEvent) => void; large?: boolean; }; export const AnimatedHoverTextButton = ({ id, text, icon, onClick, large = false }: HoverTextButtonProps) => { @@ -26,13 +24,13 @@ export const AnimatedHoverTextButton = ({ id, text, icon, onClick, large = false const content = ( {hover || large ? ( - {text} - + ) : null} {icon} @@ -40,15 +38,16 @@ export const AnimatedHoverTextButton = ({ id, text, icon, onClick, large = false if (large) { return ( - setHover(false)} onMouseOver={() => setHover(true)} > {content} - + ); } diff --git a/src/spaces/mcp/pages/McpPage.tsx b/src/spaces/mcp/pages/McpPage.tsx index b0e0eba6..ca5e7f8f 100644 --- a/src/spaces/mcp/pages/McpPage.tsx +++ b/src/spaces/mcp/pages/McpPage.tsx @@ -1,5 +1,6 @@ import { - BusyIndicator, FlexBox, + BusyIndicator, + FlexBox, ObjectPage, ObjectPageHeader, ObjectPageSection, @@ -22,7 +23,7 @@ import { ManagedResources } from '../../../components/ControlPlane/ManagedResour import { ProvidersConfig } from '../../../components/ControlPlane/ProvidersConfig.tsx'; import { Providers } from '../../../components/ControlPlane/Providers.tsx'; import ComponentList from '../../../components/ControlPlane/ComponentList.tsx'; -import MCPHealthPopoverButton from '../../../components/ControlPlane/MCPHealthPopoverButton.tsx'; + import { useApiResource } from '../../../lib/api/useApiResource.ts'; import { YamlViewButton } from '../../../components/Yaml/YamlViewButton.tsx'; From 0f68884d06fba21652c1192d9405f5d32b21c1d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Goral?= Date: Thu, 4 Dec 2025 13:56:14 +0100 Subject: [PATCH 14/15] fix --- .../ControlPlane/MCPHealthPopoverButton.tsx | 20 ++--- src/components/ControlPlane/statusUtils.ts | 14 ++++ .../Helper/AnimatedHoverTextButton.tsx | 75 ++++++++++--------- 3 files changed, 60 insertions(+), 49 deletions(-) create mode 100644 src/components/ControlPlane/statusUtils.ts diff --git a/src/components/ControlPlane/MCPHealthPopoverButton.tsx b/src/components/ControlPlane/MCPHealthPopoverButton.tsx index a8038d51..6d319cf7 100644 --- a/src/components/ControlPlane/MCPHealthPopoverButton.tsx +++ b/src/components/ControlPlane/MCPHealthPopoverButton.tsx @@ -43,6 +43,7 @@ const MCPHealthPopoverButton = ({ large = false, }: MCPHealthPopoverButtonProps) => { const popoverRef = useRef(null); + const buttonRef = useRef(null); const [open, setOpen] = useState(false); const { githubIssuesSupportTicket } = useLink(); const { t } = useTranslation(); @@ -51,7 +52,8 @@ const MCPHealthPopoverButton = ({ event: Ui5CustomEvent | Ui5CustomEvent, ) => { if (popoverRef.current) { - (popoverRef.current as unknown as { opener: EventTarget | null }).opener = event.target; + // Prefer explicit button ref as opener (works reliably); fall back to event.target + (popoverRef.current as unknown as { opener: EventTarget | null }).opener = buttonRef.current ?? event.target; setOpen((prev) => !prev); } }; @@ -152,12 +154,13 @@ const MCPHealthPopoverButton = ({ return (
- + setOpen(false)}> { - switch (status) { - case ReadyStatus.Ready: - return 'ready'; - case ReadyStatus.NotReady: - return 'not-ready'; - case ReadyStatus.InDeletion: - return 'deleting'; - default: - return ''; - } -}; - const getIconForOverallStatus = (status: ReadyStatus | undefined): JSX.Element => { switch (status) { case ReadyStatus.Ready: diff --git a/src/components/ControlPlane/statusUtils.ts b/src/components/ControlPlane/statusUtils.ts new file mode 100644 index 00000000..714669c6 --- /dev/null +++ b/src/components/ControlPlane/statusUtils.ts @@ -0,0 +1,14 @@ +import { ReadyStatus } from '../../lib/api/types/crate/controlPlanes'; + +export const getClassNameForOverallStatus = (status: ReadyStatus | undefined): string => { + switch (status) { + case ReadyStatus.Ready: + return 'ready'; + case ReadyStatus.NotReady: + return 'not-ready'; + case ReadyStatus.InDeletion: + return 'deleting'; + default: + return ''; + } +}; diff --git a/src/components/Helper/AnimatedHoverTextButton.tsx b/src/components/Helper/AnimatedHoverTextButton.tsx index 9fc4154f..d0b0509c 100644 --- a/src/components/Helper/AnimatedHoverTextButton.tsx +++ b/src/components/Helper/AnimatedHoverTextButton.tsx @@ -1,11 +1,11 @@ import { Button, ButtonDomRef, FlexBox, FlexBoxAlignItems } from '@ui5/webcomponents-react'; import '@ui5/webcomponents-icons/dist/copy'; -import { JSX, useId, useState } from 'react'; +import { JSX, useId, useState, forwardRef } from 'react'; import type { Ui5CustomEvent } from '@ui5/webcomponents-react-base'; import type { ButtonClickEventDetail } from '@ui5/webcomponents/dist/Button.js'; import styles from './AnimatedHoverTextButton.module.css'; -import { getClassNameForOverallStatus } from '../ControlPlane/MCPHealthPopoverButton.tsx'; +import { getClassNameForOverallStatus } from '../ControlPlane/statusUtils'; import { ReadyStatus } from '../../lib/api/types/crate/controlPlanes.ts'; import cx from 'clsx'; type HoverTextButtonProps = { @@ -15,33 +15,50 @@ type HoverTextButtonProps = { onClick: (event: Ui5CustomEvent) => void; large?: boolean; }; -export const AnimatedHoverTextButton = ({ id, text, icon, onClick, large = false }: HoverTextButtonProps) => { - const [hover, setHover] = useState(false); - const generatedId = useId(); - id ??= generatedId; +export const AnimatedHoverTextButton = forwardRef( + ({ id, text, icon, onClick, large = false }: HoverTextButtonProps, ref) => { + const [hover, setHover] = useState(false); - const content = ( - - {hover || large ? ( - + {hover || large ? ( + + {text} + + ) : null} + {icon} + + ); + + if (large) { + return ( + + ); + } - if (large) { return ( ); - } + }, +); - return ( - - ); -}; +AnimatedHoverTextButton.displayName = 'AnimatedHoverTextButton'; From 9a9719af223ecc0e286a365dfbd7ad627dfd2d12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Goral?= Date: Fri, 5 Dec 2025 09:11:45 +0100 Subject: [PATCH 15/15] Update McpStatusSection.module.css --- src/components/ControlPlane/McpStatusSection.module.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ControlPlane/McpStatusSection.module.css b/src/components/ControlPlane/McpStatusSection.module.css index 9a8a8d26..3c8e1493 100644 --- a/src/components/ControlPlane/McpStatusSection.module.css +++ b/src/components/ControlPlane/McpStatusSection.module.css @@ -1,5 +1,5 @@ .statusLabel { font-weight: bold; - margin-bottom: 1rem; + margin-bottom: 0.75rem; margin-left: 0.6rem; }