Skip to content
Open
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
9 changes: 9 additions & 0 deletions public/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -507,5 +507,14 @@
"addMembersButton0": "Add members",
"addMembersButton1": "Add member",
"addMembersButtonN": "Add {{count}} members"
},
"mcp": {
"authorization": {
"accessDenied": {
"title": "Access Denied",
"details": "You are not authorized to see this Managed Control Plane."
},
"backToWorkspaces": "Back to Workspaces"
}
}
}
23 changes: 12 additions & 11 deletions src/components/ControlPlanes/ControlPlaneCard/ControlPlaneCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,20 +100,21 @@ export const ControlPlaneCard = ({
resourceName={controlPlane.metadata.name}
resourceType={'managedcontrolplanes'}
/>
{showWarningBecauseOfDisabledSystemIdentityProvider && (
<Infobox size="sm" variant="warning">
{showWarningBecauseOfDisabledSystemIdentityProvider ? (
<Infobox size="sm" variant="warning" noMargin>
{t('ConnectButton.unsupportedIdP')}
</Infobox>
) : (
<ConnectButton
disabled={!isConnectButtonEnabled}
controlPlaneName={name}
projectName={projectName}
workspaceName={workspace.metadata.name ?? ''}
namespace={controlPlane.status?.access?.namespace ?? ''}
secretName={controlPlane.status?.access?.name ?? ''}
secretKey={controlPlane.status?.access?.key ?? ''}
/>
)}
<ConnectButton
disabled={!isConnectButtonEnabled}
controlPlaneName={name}
projectName={projectName}
workspaceName={workspace.metadata.name ?? ''}
namespace={controlPlane.status?.access?.namespace ?? ''}
secretName={controlPlane.status?.access?.name ?? ''}
secretKey={controlPlane.status?.access?.key ?? ''}
/>
</FlexBox>
</FlexBox>
</FlexBox>
Expand Down
12 changes: 12 additions & 0 deletions src/components/Ui/Center/Center.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
.wrapper {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
width: 100%;
height: 100%;
}

.textAlignCenter {
text-align: center;
}
19 changes: 19 additions & 0 deletions src/components/Ui/Center/Center.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { PropsWithChildren, ReactNode, CSSProperties } from 'react';
import cx from 'clsx';
import styles from './Center.module.css';

export type CenterProps = PropsWithChildren<{
className?: string;
style?: CSSProperties;
textAlignCenter?: boolean;
}>;

export const Center = ({ children, className, style, textAlignCenter = true }: CenterProps): ReactNode => {
const classes = cx(styles.wrapper, { [styles.textAlignCenter]: textAlignCenter }, className);

return (
<div className={classes} style={style}>
{children}
</div>
);
};
14 changes: 12 additions & 2 deletions src/components/Ui/Infobox/Infobox.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@
margin-right: 1rem;
}

.icon-sm {
width: 1.25rem;
height: 1.25rem;
}

.content {
flex-grow: 1;
padding-right: 0.5rem;
Expand All @@ -29,8 +34,9 @@
}

.size-sm {
padding: 0.75rem 1rem;
font-size: 0.875rem;
padding: 0.5rem 0.875rem;
font-size: 0.75rem;
border-radius: 0.5rem;
}

.size-md {
Expand Down Expand Up @@ -67,3 +73,7 @@
color: var(--sapBackgroundColor);
line-height: 1.2rem;
}

.no-margin {
margin-bottom: 0;
}
5 changes: 4 additions & 1 deletion src/components/Ui/Infobox/Infobox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ interface LabelProps {
fullWidth?: boolean;
className?: string;
icon?: string;
noMargin?: boolean;
}

const variantIcons = {
Expand All @@ -29,6 +30,7 @@ export const Infobox: React.FC<LabelProps> = ({
fullWidth = false,
className,
icon,
noMargin = false,
}) => {
const infoboxClasses = cx(
styles.infobox,
Expand All @@ -41,6 +43,7 @@ export const Infobox: React.FC<LabelProps> = ({
[styles['variant-warning']]: variant === 'warning',
[styles['variant-danger']]: variant === 'danger',
[styles['full-width']]: fullWidth,
[styles['no-margin']]: noMargin,
},
className,
);
Expand All @@ -49,7 +52,7 @@ export const Infobox: React.FC<LabelProps> = ({

return (
<div className={infoboxClasses} id={id}>
{iconName && <Icon name={iconName} className={styles.icon} />}
{iconName && <Icon name={iconName} className={cx(styles.icon, { [styles['icon-sm']]: size === 'sm' })} />}
<div className={styles.content}>{children}</div>
</div>
);
Expand Down
2 changes: 1 addition & 1 deletion src/components/Ui/NotFoundBanner/NotFoundBanner.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@
.button {
margin-inline: auto;
margin-block: 2rem;
}
}
31 changes: 17 additions & 14 deletions src/components/Ui/NotFoundBanner/NotFoundBanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Trans, useTranslation } from 'react-i18next';
import styles from './NotFoundBanner.module.css';
import { Button } from '@ui5/webcomponents-react';
import { useNavigate } from 'react-router-dom';
import { Center } from '../Center/Center.tsx';

export interface NotFoundBannerProps {
entityType: string;
Expand All @@ -14,19 +15,21 @@ export function NotFoundBanner({ entityType }: NotFoundBannerProps) {
const navigate = useNavigate();

return (
<IllustratedBanner
illustrationName={IllustrationMessageType.PageNotFound}
title={t('NotFoundBanner.titleMessage', { entityType })}
subtitle={
<div className={styles.subtitleContainer}>
<span>
<Trans i18nKey="NotFoundBanner.subtitleMessage" values={{ entityType }} />
</span>
<Button className={styles.button} onClick={() => navigate('/')}>
{t('NotFoundBanner.navigateHome')}
</Button>
</div>
}
/>
<Center>
<IllustratedBanner
illustrationName={IllustrationMessageType.PageNotFound}
title={t('NotFoundBanner.titleMessage', { entityType })}
subtitle={
<div className={styles.subtitleContainer}>
<span>
<Trans i18nKey="NotFoundBanner.subtitleMessage" values={{ entityType }} />
</span>
<Button className={styles.button} onClick={() => navigate('/')}>
{t('NotFoundBanner.navigateHome')}
</Button>
</div>
}
/>
</Center>
);
}
5 changes: 5 additions & 0 deletions src/lib/api/types/crossplane/CRDList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,8 @@ export type CRDResponse = {
export const CRDRequest: Resource<CRDResponse> = {
path: '/apis/apiextensions.k8s.io/v1/customresourcedefinitions',
};

export const CRDRequestAuthCheck: Resource<CRDResponse> = {
path: '/apis/apiextensions.k8s.io/v1/customresourcedefinitions',
jq: '{kind: .kind}',
};
1 change: 0 additions & 1 deletion src/lib/shared/McpContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ interface Mcp {
project: string;
workspace: string;
name: string;

secretNamespace?: string;
secretName?: string;
secretKey?: string;
Expand Down
56 changes: 56 additions & 0 deletions src/spaces/mcp/authorization/ManagedControlPlaneAuthorization.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { ReactNode } from 'react';
import { useTranslation } from 'react-i18next';

import IllustratedError from '../../../components/Shared/IllustratedError.tsx';
import { BusyIndicator, Button } from '@ui5/webcomponents-react';
import { ControlPlaneType } from '../../../lib/api/types/crate/controlPlanes.ts';
import { generatePath, useNavigate, useParams } from 'react-router-dom';
import { Routes } from '../../../Routes.ts';

import { Center } from '../../../components/Ui/Center/Center.tsx';
import { CRDRequestAuthCheck } from '../../../lib/api/types/crossplane/CRDList.ts';
import { useApiResource } from '../../../lib/api/useApiResource.ts';

export interface ManagedControlPlaneAuthorizationProps {
mcp: ControlPlaneType;
Copy link

Copilot AI Nov 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The mcp prop is defined in the interface but never used in the component. If it's not needed for the current implementation, it should be removed from the interface. If it will be needed in the future, consider adding a comment explaining its intended use.

Suggested change
mcp: ControlPlaneType;

Copilot uses AI. Check for mistakes.
children: ReactNode;
}
export const ManagedControlPlaneAuthorization = ({ children }: ManagedControlPlaneAuthorizationProps) => {
const { t } = useTranslation();
const navigate = useNavigate();
const { projectName, workspaceName } = useParams();
const onBack = () => {
if (workspaceName) {
navigate(
generatePath(Routes.Project, {
projectName: projectName ?? '',
}),
);
}
};
Comment on lines +22 to +30
Copy link

Copilot AI Nov 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The onBack function only navigates when workspaceName is truthy, but the button is always rendered. This means the button will do nothing when workspaceName is falsy. Consider either:

  1. Hiding the button when navigation is not possible
  2. Always navigating (remove the conditional), or
  3. Providing a fallback navigation target

Copilot uses AI. Check for mistakes.

// Check if user has access to CRDs in the MCP's cluster
const { error, isLoading } = useApiResource(CRDRequestAuthCheck);
if (isLoading) {
return (
<Center>
<BusyIndicator active />
</Center>
);
}
const isUserNotAuthorized = error?.status === 403 || error?.status === 401;
if (isUserNotAuthorized)
return (
<Center>
<IllustratedError
title={t('mcp.authorization.accessDenied.title')}
details={t('mcp.authorization.accessDenied.details')}
/>
<Button design={'Default'} icon={'navigation-left-arrow'} onClick={onBack}>
{t('mcp.authorization.backToWorkspaces')}
</Button>
</Center>
);

return <>{children}</>;
};
Loading
Loading