diff --git a/public/locales/en.json b/public/locales/en.json index f6fd2513..ad122a5c 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -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" + } } } diff --git a/src/components/ControlPlanes/ControlPlaneCard/ControlPlaneCard.tsx b/src/components/ControlPlanes/ControlPlaneCard/ControlPlaneCard.tsx index 0a9e11e0..98a9bffe 100644 --- a/src/components/ControlPlanes/ControlPlaneCard/ControlPlaneCard.tsx +++ b/src/components/ControlPlanes/ControlPlaneCard/ControlPlaneCard.tsx @@ -100,20 +100,21 @@ export const ControlPlaneCard = ({ resourceName={controlPlane.metadata.name} resourceType={'managedcontrolplanes'} /> - {showWarningBecauseOfDisabledSystemIdentityProvider && ( - + {showWarningBecauseOfDisabledSystemIdentityProvider ? ( + {t('ConnectButton.unsupportedIdP')} + ) : ( + )} - diff --git a/src/components/Ui/Center/Center.module.css b/src/components/Ui/Center/Center.module.css new file mode 100644 index 00000000..fcb286fa --- /dev/null +++ b/src/components/Ui/Center/Center.module.css @@ -0,0 +1,12 @@ +.wrapper { + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + width: 100%; + height: 100%; +} + +.textAlignCenter { + text-align: center; +} diff --git a/src/components/Ui/Center/Center.tsx b/src/components/Ui/Center/Center.tsx new file mode 100644 index 00000000..46fd8e01 --- /dev/null +++ b/src/components/Ui/Center/Center.tsx @@ -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 ( +
+ {children} +
+ ); +}; diff --git a/src/components/Ui/Infobox/Infobox.module.css b/src/components/Ui/Infobox/Infobox.module.css index 6d1b8a0d..5f5312b5 100644 --- a/src/components/Ui/Infobox/Infobox.module.css +++ b/src/components/Ui/Infobox/Infobox.module.css @@ -17,6 +17,11 @@ margin-right: 1rem; } +.icon-sm { + width: 1.25rem; + height: 1.25rem; +} + .content { flex-grow: 1; padding-right: 0.5rem; @@ -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 { @@ -67,3 +73,7 @@ color: var(--sapBackgroundColor); line-height: 1.2rem; } + +.no-margin { + margin-bottom: 0; +} diff --git a/src/components/Ui/Infobox/Infobox.tsx b/src/components/Ui/Infobox/Infobox.tsx index e0eb4e3f..743e7bf7 100644 --- a/src/components/Ui/Infobox/Infobox.tsx +++ b/src/components/Ui/Infobox/Infobox.tsx @@ -12,6 +12,7 @@ interface LabelProps { fullWidth?: boolean; className?: string; icon?: string; + noMargin?: boolean; } const variantIcons = { @@ -29,6 +30,7 @@ export const Infobox: React.FC = ({ fullWidth = false, className, icon, + noMargin = false, }) => { const infoboxClasses = cx( styles.infobox, @@ -41,6 +43,7 @@ export const Infobox: React.FC = ({ [styles['variant-warning']]: variant === 'warning', [styles['variant-danger']]: variant === 'danger', [styles['full-width']]: fullWidth, + [styles['no-margin']]: noMargin, }, className, ); @@ -49,7 +52,7 @@ export const Infobox: React.FC = ({ return (
- {iconName && } + {iconName && }
{children}
); diff --git a/src/components/Ui/NotFoundBanner/NotFoundBanner.module.css b/src/components/Ui/NotFoundBanner/NotFoundBanner.module.css index f017a659..0fbd6379 100644 --- a/src/components/Ui/NotFoundBanner/NotFoundBanner.module.css +++ b/src/components/Ui/NotFoundBanner/NotFoundBanner.module.css @@ -6,4 +6,4 @@ .button { margin-inline: auto; margin-block: 2rem; -} \ No newline at end of file +} diff --git a/src/components/Ui/NotFoundBanner/NotFoundBanner.tsx b/src/components/Ui/NotFoundBanner/NotFoundBanner.tsx index 8e2e7741..4eff5a38 100644 --- a/src/components/Ui/NotFoundBanner/NotFoundBanner.tsx +++ b/src/components/Ui/NotFoundBanner/NotFoundBanner.tsx @@ -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; @@ -14,19 +15,21 @@ export function NotFoundBanner({ entityType }: NotFoundBannerProps) { const navigate = useNavigate(); return ( - - - - - - - } - /> +
+ + + + + + + } + /> +
); } diff --git a/src/lib/api/types/crossplane/CRDList.ts b/src/lib/api/types/crossplane/CRDList.ts index 9cbd3259..5a7d9510 100644 --- a/src/lib/api/types/crossplane/CRDList.ts +++ b/src/lib/api/types/crossplane/CRDList.ts @@ -40,3 +40,8 @@ export type CRDResponse = { export const CRDRequest: Resource = { path: '/apis/apiextensions.k8s.io/v1/customresourcedefinitions', }; + +export const CRDRequestAuthCheck: Resource = { + path: '/apis/apiextensions.k8s.io/v1/customresourcedefinitions', + jq: '{kind: .kind}', +}; diff --git a/src/lib/shared/McpContext.tsx b/src/lib/shared/McpContext.tsx index 8c46a06d..267b6ee1 100644 --- a/src/lib/shared/McpContext.tsx +++ b/src/lib/shared/McpContext.tsx @@ -10,7 +10,6 @@ interface Mcp { project: string; workspace: string; name: string; - secretNamespace?: string; secretName?: string; secretKey?: string; diff --git a/src/spaces/mcp/authorization/ManagedControlPlaneAuthorization.tsx b/src/spaces/mcp/authorization/ManagedControlPlaneAuthorization.tsx new file mode 100644 index 00000000..f4f5359f --- /dev/null +++ b/src/spaces/mcp/authorization/ManagedControlPlaneAuthorization.tsx @@ -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; + 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 ?? '', + }), + ); + } + }; + + // Check if user has access to CRDs in the MCP's cluster + const { error, isLoading } = useApiResource(CRDRequestAuthCheck); + if (isLoading) { + return ( +
+ +
+ ); + } + const isUserNotAuthorized = error?.status === 403 || error?.status === 401; + if (isUserNotAuthorized) + return ( +
+ + +
+ ); + + return <>{children}; +}; diff --git a/src/spaces/mcp/pages/McpPage.tsx b/src/spaces/mcp/pages/McpPage.tsx index b33e3bde..dcfef20b 100644 --- a/src/spaces/mcp/pages/McpPage.tsx +++ b/src/spaces/mcp/pages/McpPage.tsx @@ -41,6 +41,8 @@ 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 { ManagedControlPlaneAuthorization } from '../authorization/ManagedControlPlaneAuthorization.tsx'; +import { Center } from '../../../components/Ui/Center/Center.tsx'; export type McpPageSectionId = 'overview' | 'crossplane' | 'flux' | 'landscapers'; @@ -52,11 +54,13 @@ export default function McpPage() { undefined | WizardStepType >(undefined); const [selectedSectionId, setSelectedSectionId] = useState('overview'); + const { data: mcp, error, isLoading, } = useApiResource(ControlPlaneResource(projectName, workspaceName, controlPlaneName)); + const displayName = mcp?.metadata?.annotations && typeof mcp.metadata.annotations === 'object' ? (mcp.metadata.annotations as Record)[DISPLAY_NAME_ANNOTATION] @@ -69,8 +73,13 @@ export default function McpPage() { setIsEditManagedControlPlaneWizardOpen(false); setEditManagedControlPlaneWizardSection(undefined); }; + if (isLoading) { - return ; + return ( +
+ +
+ ); } if (!projectName || !workspaceName || !controlPlaneName || isNotFoundError(error)) { @@ -84,7 +93,6 @@ export default function McpPage() { const isComponentInstalledCrossplane = !!mcp.spec?.components.crossplane; const isComponentInstalledFlux = !!mcp.spec?.components.flux; const isComponentInstalledLandscaper = !!mcp.spec?.components.landscaper; - return ( - } - //TODO: actionBar should use Toolbar and ToolbarButton for consistent design - actionsBar={ -
- - - - - -
- } - /> - } - selectedSectionId={selectedSectionId} - headerArea={ - - - - } - onSelectedSectionChange={() => setSelectedSectionId(undefined)} - > - - - setSelectedSectionId(sectionId)} + + } + //TODO: actionBar should use Toolbar and ToolbarButton for consistent design + actionsBar={ +
+ + + + + +
+ } /> -
- - - - - - -
- - {isComponentInstalledCrossplane && ( - - - + } + selectedSectionId={selectedSectionId} + headerArea={ + + + + } + onSelectedSectionChange={() => setSelectedSectionId(undefined)} + > + + + setSelectedSectionId(sectionId)} + /> - - + + - + - )} - {isComponentInstalledFlux && ( - - - - - + + + + + + + + + + + )} + + {isComponentInstalledFlux && ( + + + + + + + + + )} + + {isComponentInstalledLandscaper && ( + - - - - )} - - {isComponentInstalledLandscaper && ( - - - - )} -
+ + + )} + +