diff --git a/public/locales/en.json b/public/locales/en.json index 9142a1f4..50164e06 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -417,19 +417,21 @@ "back": "Back", "cancel": "Cancel", "update": "Update", - "applyChanges": "Apply changes" + "applyChanges": "Apply changes", + "edit": "Edit" }, "yaml": { "YAML": "File", "showOnlyImportant": "Show only important fields", "panelTitle": "YAML", - "editorTitle": "YAML Editor", "applySuccess2": "The Managed Control Plane will reconcile this resource shortly.", "applySuccess": "Update submitted ", "diffConfirmTitle": "Review changes", "diffConfirmMessage": "Are you sure that you want to apply these changes?", "diffNo": "No, go back", - "diffYes": "Yes" + "diffYes": "Yes", + "validationErrors": "Validation errors", + "fluxManaged": "This resource is managed by Flux and cannot be edited here" }, "createMCP": { "dialogTitle": "Create Managed Control Plane", diff --git a/src/components/ControlPlane/ActionsMenu.tsx b/src/components/ControlPlane/ActionsMenu.tsx index 7a911df0..a9beff20 100644 --- a/src/components/ControlPlane/ActionsMenu.tsx +++ b/src/components/ControlPlane/ActionsMenu.tsx @@ -9,6 +9,7 @@ export type ActionItem = { icon?: string; disabled?: boolean; onClick: (item: T) => void; + tooltip?: string; }; export type ActionsMenuProps = { @@ -45,7 +46,14 @@ export function ActionsMenu({ item, actions, buttonIcon = 'overflow' }: Actio }} > {actions.map((a) => ( - + ))} diff --git a/src/components/ControlPlane/GitRepositories.tsx b/src/components/ControlPlane/GitRepositories.tsx index d33865b5..451899cf 100644 --- a/src/components/ControlPlane/GitRepositories.tsx +++ b/src/components/ControlPlane/GitRepositories.tsx @@ -1,5 +1,12 @@ import ConfiguredAnalyticstable from '../Shared/ConfiguredAnalyticsTable.tsx'; -import { AnalyticalTableColumnDefinition, Panel, Title, Toolbar, ToolbarSpacer } from '@ui5/webcomponents-react'; +import { + AnalyticalTableColumnDefinition, + Panel, + Title, + Toolbar, + ToolbarSpacer, + Button, +} from '@ui5/webcomponents-react'; import IllustratedError from '../Shared/IllustratedError.tsx'; import { useApiResource } from '../../lib/api/useApiResource'; import { FluxRequest } from '../../lib/api/types/flux/listGitRepo'; @@ -17,6 +24,7 @@ import { useHandleResourcePatch } from '../../lib/api/types/crossplane/useHandle import { ErrorDialog, ErrorDialogHandle } from '../Shared/ErrorMessageBox.tsx'; import type { GitReposResponse } from '../../lib/api/types/flux/listGitRepo'; import { ActionsMenu, type ActionItem } from './ActionsMenu'; +import { useHasMcpAdminRights } from '../../spaces/mcp/auth/useHasMcpAdminRights.ts'; export type GitRepoItem = GitReposResponse['items'][0] & { apiVersion?: string; @@ -39,7 +47,6 @@ export function GitRepositories() { readyMessage: string; revision?: string; }; - const openEditPanel = useCallback( (item: GitRepoItem) => { const identityKey = `${item.kind}:${item.metadata.namespace ?? ''}:${item.metadata.name}`; @@ -56,6 +63,7 @@ export function GitRepositories() { }, [openInAside, handlePatch], ); + const hasMCPAdminRights = useHasMcpAdminRights(); const columns = useMemo( () => @@ -96,7 +104,28 @@ export function GitRepositories() { width: 75, accessor: 'yaml', disableFilters: true, - Cell: ({ row }) => , + Cell: ({ row }) => { + const item = row.original?.item; + return item ? ( + { + openEditPanel(item); + }} + > + {t('buttons.edit')} + + ) : undefined + } + /> + ) : undefined; + }, }, { Header: t('ManagedResources.actionColumnHeader'), @@ -113,13 +142,14 @@ export function GitRepositories() { text: t('ManagedResources.editAction', 'Edit'), icon: 'edit', onClick: openEditPanel, + disabled: !hasMCPAdminRights, }, ]; return ; }, }, ] as AnalyticalTableColumnDefinition[], - [t, openEditPanel], + [t, hasMCPAdminRights, openEditPanel], ); if (error) { diff --git a/src/components/ControlPlane/Kustomizations.tsx b/src/components/ControlPlane/Kustomizations.tsx index aeb9fec1..fb0528d1 100644 --- a/src/components/ControlPlane/Kustomizations.tsx +++ b/src/components/ControlPlane/Kustomizations.tsx @@ -1,5 +1,12 @@ import ConfiguredAnalyticstable from '../Shared/ConfiguredAnalyticsTable.tsx'; -import { AnalyticalTableColumnDefinition, Panel, Title, Toolbar, ToolbarSpacer } from '@ui5/webcomponents-react'; +import { + AnalyticalTableColumnDefinition, + Panel, + Title, + Toolbar, + ToolbarSpacer, + Button, +} from '@ui5/webcomponents-react'; import IllustratedError from '../Shared/IllustratedError.tsx'; import { useApiResource } from '../../lib/api/useApiResource'; import { FluxKustomization } from '../../lib/api/types/flux/listKustomization'; @@ -17,6 +24,7 @@ import { useHandleResourcePatch } from '../../lib/api/types/crossplane/useHandle import { ErrorDialog, ErrorDialogHandle } from '../Shared/ErrorMessageBox.tsx'; import type { KustomizationsResponse } from '../../lib/api/types/flux/listKustomization'; import { ActionsMenu, type ActionItem } from './ActionsMenu'; +import { useHasMcpAdminRights } from '../../spaces/mcp/auth/useHasMcpAdminRights.ts'; export type KustomizationItem = KustomizationsResponse['items'][0] & { apiVersion?: string; @@ -55,7 +63,7 @@ export function Kustomizations() { }, [openInAside, handlePatch], ); - + const hasMCPAdminRights = useHasMcpAdminRights(); const columns = useMemo( () => [ @@ -91,7 +99,28 @@ export function Kustomizations() { width: 75, accessor: 'yaml', disableFilters: true, - Cell: ({ row }) => , + Cell: ({ row }) => { + const item = row.original?.item; + return item ? ( + { + openEditPanel(item); + }} + > + {t('buttons.edit')} + + ) : undefined + } + /> + ) : undefined; + }, }, { Header: t('ManagedResources.actionColumnHeader'), @@ -108,13 +137,14 @@ export function Kustomizations() { text: t('ManagedResources.editAction', 'Edit'), icon: 'edit', onClick: openEditPanel, + disabled: !hasMCPAdminRights, }, ]; return ; }, }, ] as AnalyticalTableColumnDefinition[], - [t, openEditPanel], + [t, openEditPanel, hasMCPAdminRights], ); if (error) { diff --git a/src/components/ControlPlane/ManagedResources.tsx b/src/components/ControlPlane/ManagedResources.tsx index bebc8c54..48d77ec6 100644 --- a/src/components/ControlPlane/ManagedResources.tsx +++ b/src/components/ControlPlane/ManagedResources.tsx @@ -4,6 +4,7 @@ import { AnalyticalTable, AnalyticalTableColumnDefinition, AnalyticalTableScaleWidthMode, + Button, Panel, Title, Toolbar, @@ -35,6 +36,7 @@ import { YamlSidePanel } from '../Yaml/YamlSidePanel.tsx'; import { ErrorDialog, ErrorDialogHandle } from '../Shared/ErrorMessageBox.tsx'; import { APIError } from '../../lib/api/error.ts'; import { useHandleResourcePatch } from '../../lib/api/types/crossplane/useHandleResourcePatch.ts'; +import { useHasMcpAdminRights } from '../../spaces/mcp/auth/useHasMcpAdminRights.ts'; interface StatusFilterColumn { filterValue?: string; @@ -54,6 +56,19 @@ type ResourceRow = { conditionSyncedMessage: string; }; +/** + * Checks if a resource is managed by Flux based on the kustomize.toolkit.fluxcd.io/name label + */ +const isResourceFluxManaged = (item: ManagedResourceItem | undefined): boolean => { + if (!item) return false; + + const fluxLabelValue = (item?.metadata?.labels as unknown as Record | undefined)?.[ + 'kustomize.toolkit.fluxcd.io/name' + ]; + + return fluxLabelValue != null && (typeof fluxLabelValue !== 'string' || fluxLabelValue.trim() !== ''); +}; + export function ManagedResources() { const { t } = useTranslation(); const toast = useToast(); @@ -105,7 +120,7 @@ export function ManagedResources() { }, [openInAside, handlePatch], ); - + const hasMCPAdminRights = useHasMcpAdminRights(); const columns = useMemo( () => [ @@ -167,8 +182,27 @@ export function ManagedResources() { disableFilters: true, Cell: ({ row }) => { const { original } = row; + const isFluxManaged = isResourceFluxManaged(original?.item); return original?.item ? ( - + { + openEditPanel(original?.item); + }} + > + {t('buttons.edit')} + + ) : undefined + } + /> ) : undefined; }, }, @@ -182,12 +216,7 @@ export function ManagedResources() { const item = original?.item; if (!item) return undefined; - // Flux-managed check for disabling Edit - const fluxLabelValue = (item?.metadata?.labels as unknown as Record | undefined)?.[ - 'kustomize.toolkit.fluxcd.io/name' - ]; - const isFluxManaged = - typeof fluxLabelValue === 'string' ? fluxLabelValue.trim() !== '' : fluxLabelValue != null; + const isFluxManaged = isResourceFluxManaged(item); const actions: ActionItem[] = [ { @@ -196,12 +225,15 @@ export function ManagedResources() { icon: 'edit', disabled: isFluxManaged, onClick: openEditPanel, + tooltip: isFluxManaged && hasMCPAdminRights ? t('yaml.fluxManaged') : undefined, }, + { key: 'delete', text: t('ManagedResources.deleteAction'), icon: 'delete', onClick: openDeleteDialog, + disabled: !hasMCPAdminRights, }, ]; @@ -209,7 +241,7 @@ export function ManagedResources() { }, }, ] as AnalyticalTableColumnDefinition[], - [t, openEditPanel, openDeleteDialog], + [t, openEditPanel, openDeleteDialog, hasMCPAdminRights], ); const rows: ResourceRow[] = diff --git a/src/components/ControlPlane/ProvidersConfig.tsx b/src/components/ControlPlane/ProvidersConfig.tsx index 68d3705f..76a4e6a5 100644 --- a/src/components/ControlPlane/ProvidersConfig.tsx +++ b/src/components/ControlPlane/ProvidersConfig.tsx @@ -3,6 +3,7 @@ import { AnalyticalTable, AnalyticalTableColumnDefinition, AnalyticalTableScaleWidthMode, + Button, Panel, Title, Toolbar, @@ -23,6 +24,7 @@ import { useSplitter } from '../Splitter/SplitterContext.tsx'; import { YamlSidePanel } from '../Yaml/YamlSidePanel.tsx'; import { useHandleResourcePatch } from '../../lib/api/types/crossplane/useHandleResourcePatch.ts'; import { ErrorDialog, ErrorDialogHandle } from '../Shared/ErrorMessageBox.tsx'; +import { useHasMcpAdminRights } from '../../spaces/mcp/auth/useHasMcpAdminRights.ts'; type Rows = { parent: string; @@ -74,7 +76,7 @@ export function ProvidersConfig() { }, [openInAside, handlePatch], ); - + const hasMCPAdminRights = useHasMcpAdminRights(); const columns = useMemo( () => [ @@ -102,7 +104,25 @@ export function ProvidersConfig() { disableFilters: true, Cell: ({ row }) => { const item = row.original?.resource; - return item ? : undefined; + return item ? ( + { + openEditPanel(item); + }} + > + {t('buttons.edit')} + + ) : undefined + } + /> + ) : undefined; }, }, { @@ -120,13 +140,14 @@ export function ProvidersConfig() { text: t('ManagedResources.editAction', 'Edit'), icon: 'edit', onClick: openEditPanel, + disabled: !hasMCPAdminRights, }, ]; return ; }, }, ] as AnalyticalTableColumnDefinition[], - [t, openEditPanel], + [t, openEditPanel, hasMCPAdminRights], ); return ( diff --git a/src/components/Wizards/CreateManagedControlPlane/SummarizeStep.tsx b/src/components/Wizards/CreateManagedControlPlane/SummarizeStep.tsx index ef665630..5311740d 100644 --- a/src/components/Wizards/CreateManagedControlPlane/SummarizeStep.tsx +++ b/src/components/Wizards/CreateManagedControlPlane/SummarizeStep.tsx @@ -7,13 +7,13 @@ import { CreateManagedControlPlane, } from '../../../lib/api/types/crate/createManagedControlPlane.ts'; -import YamlPanel from '../../Yaml/YamlPanel.tsx'; import { idpPrefix } from '../../../utils/idpPrefix.ts'; import { UseFormWatch } from 'react-hook-form'; import { CreateDialogProps } from '../../Dialogs/CreateWorkspaceDialogContainer.tsx'; import styles from './SummarizeStep.module.css'; import { YamlDiff } from './YamlDiff.tsx'; +import YamlSummarize from './YamlSummarize.tsx'; interface SummarizeStepProps { watch: UseFormWatch; projectName: string; @@ -94,7 +94,7 @@ export const SummarizeStep: React.FC = ({ )} /> ) : ( - + )} diff --git a/src/components/Yaml/YamlPanel.module.css b/src/components/Wizards/CreateManagedControlPlane/YamlSummarize.module.css similarity index 100% rename from src/components/Yaml/YamlPanel.module.css rename to src/components/Wizards/CreateManagedControlPlane/YamlSummarize.module.css diff --git a/src/components/Yaml/YamlPanel.tsx b/src/components/Wizards/CreateManagedControlPlane/YamlSummarize.tsx similarity index 76% rename from src/components/Yaml/YamlPanel.tsx rename to src/components/Wizards/CreateManagedControlPlane/YamlSummarize.tsx index b72855d8..e95344be 100644 --- a/src/components/Yaml/YamlPanel.tsx +++ b/src/components/Wizards/CreateManagedControlPlane/YamlSummarize.tsx @@ -1,16 +1,17 @@ import { FC } from 'react'; import { Button, FlexBox } from '@ui5/webcomponents-react'; -import styles from './YamlPanel.module.css'; -import { useCopyToClipboard } from '../../hooks/useCopyToClipboard.ts'; +import styles from './YamlSummarize.module.css'; import { useTranslation } from 'react-i18next'; -import { SHOW_DOWNLOAD_BUTTON } from './YamlSidePanel.tsx'; -import { YamlViewer } from './YamlViewer.tsx'; +import { useCopyToClipboard } from '../../../hooks/useCopyToClipboard.ts'; +import { SHOW_DOWNLOAD_BUTTON } from '../../Yaml/YamlSidePanel.tsx'; +import { YamlViewer } from '../../Yaml/YamlViewer.tsx'; + type YamlPanelProps = { yamlString: string; filename: string; }; -const YamlPanel: FC = ({ yamlString, filename }) => { +const YamlSummarize: FC = ({ yamlString, filename }) => { const { t } = useTranslation(); const { copyToClipboard } = useCopyToClipboard(); const downloadYaml = () => { @@ -37,9 +38,9 @@ const YamlPanel: FC = ({ yamlString, filename }) => { )} - + ); }; -export default YamlPanel; +export default YamlSummarize; diff --git a/src/components/Yaml/YamlSidePanel.module.css b/src/components/Yaml/YamlSidePanel.module.css index 49ec6095..595e94bc 100644 --- a/src/components/Yaml/YamlSidePanel.module.css +++ b/src/components/Yaml/YamlSidePanel.module.css @@ -26,10 +26,10 @@ position: sticky; top: 0; height: 6rem; - padding: 1rem; - padding-bottom: 1.5rem; + padding: 1rem 1rem 1.5rem; z-index: 1; background: var(--sapBackgroundColor); + border-radius: 1rem; } .stickyHeaderInner { diff --git a/src/components/Yaml/YamlSidePanel.tsx b/src/components/Yaml/YamlSidePanel.tsx index 5d67c873..10604d48 100644 --- a/src/components/Yaml/YamlSidePanel.tsx +++ b/src/components/Yaml/YamlSidePanel.tsx @@ -13,7 +13,7 @@ import IllustrationMessageType from '@ui5/webcomponents-fiori/dist/types/Illustr import { useTranslation } from 'react-i18next'; import { YamlViewer } from './YamlViewer.tsx'; import { useSplitter } from '../Splitter/SplitterContext.tsx'; -import { useMemo, useState, useCallback } from 'react'; +import { useMemo, useState, useCallback, JSX } from 'react'; import { stringify } from 'yaml'; import { convertToResourceConfig } from '../../utils/convertToResourceConfig.ts'; import { removeManagedFieldsAndFilterData, Resource } from '../../utils/removeManagedFieldsAndFilterData.ts'; @@ -29,8 +29,9 @@ export interface YamlSidePanelProps { filename: string; onApply?: (parsed: unknown, yaml: string) => void | boolean | Promise; isEdit?: boolean; + toolbarContent?: JSX.Element; } -export function YamlSidePanel({ resource, filename, onApply, isEdit }: YamlSidePanelProps) { +export function YamlSidePanel({ resource, filename, onApply, isEdit, toolbarContent }: YamlSidePanelProps) { const [showOnlyImportantData, setShowOnlyImportantData] = useState(true); const [mode, setMode] = useState<'edit' | 'review' | 'success'>('edit'); const [editedYaml, setEditedYaml] = useState(null); @@ -85,7 +86,7 @@ export function YamlSidePanel({ resource, filename, onApply, isEdit }: YamlSideP fixed header={ - {t('yaml.panelTitle')} + {toolbarContent ?? {t('yaml.panelTitle')}} {!isEdit && ( diff --git a/src/components/Yaml/YamlViewButton.tsx b/src/components/Yaml/YamlViewButton.tsx index 3d0e4edb..ad086c11 100644 --- a/src/components/Yaml/YamlViewButton.tsx +++ b/src/components/Yaml/YamlViewButton.tsx @@ -7,10 +7,12 @@ import { YamlIcon } from './YamlIcon.tsx'; import { useSplitter } from '../Splitter/SplitterContext.tsx'; import { YamlSidePanel } from './YamlSidePanel.tsx'; import { YamlSidePanelWithLoader } from './YamlSidePanelWithLoader.tsx'; +import { JSX } from 'react'; export interface YamlViewButtonResourceProps { variant: 'resource'; resource: Resource; + toolbarContent?: JSX.Element; } export interface YamlViewButtonLoaderProps { variant: 'loader'; @@ -20,26 +22,27 @@ export interface YamlViewButtonLoaderProps { } export type YamlViewButtonProps = YamlViewButtonResourceProps | YamlViewButtonLoaderProps; -export function YamlViewButton(props: YamlViewButtonProps) { +export function YamlViewButton({ variant, ...props }: YamlViewButtonProps) { const { t } = useTranslation(); const { openInAside } = useSplitter(); const openSplitterSidePanel = () => { - switch (props.variant) { + switch (variant) { case 'resource': { - const { resource } = props; + const { resource, toolbarContent } = props as YamlViewButtonResourceProps; openInAside( , ); break; } case 'loader': { - const { workspaceName, resourceType, resourceName } = props; + const { workspaceName, resourceType, resourceName } = props as YamlViewButtonLoaderProps; openInAside( , 'language'> & { isEdit?: boolean; @@ -34,8 +35,25 @@ export const YamlEditor = (props: YamlEditorProps) => { ...(options as monaco.editor.IStandaloneEditorConstructionOptions), readOnly: isEdit ? false : (options?.readOnly ?? true), minimap: { enabled: false }, - wordWrap: 'on' as const, scrollBeyondLastLine: false, + tabSize: 2, + insertSpaces: true, + detectIndentation: false, + wordWrap: 'on', + folding: true, + foldingStrategy: 'indentation', + quickSuggestions: { + other: true, + comments: true, + strings: true, + }, + suggestOnTriggerCharacters: true, + glyphMargin: true, + formatOnPaste: true, + formatOnType: true, + fontSize: 13, + lineHeight: 20, + renderWhitespace: 'boundary', }), [options, isEdit], ); @@ -80,17 +98,15 @@ export const YamlEditor = (props: YamlEditorProps) => { const showValidationErrors = isEdit && applyAttempted && validationErrors.length > 0; return ( -
+
{isEdit && ( - {t('yaml.editorTitle')} - - )} -
+
{ />
{showValidationErrors && ( - -
    + +
      {validationErrors.map((err, idx) => ( -
    • +
    • {err}
    • ))} diff --git a/src/lib/api/types/crate/controlPlanes.ts b/src/lib/api/types/crate/controlPlanes.ts index 1176614e..7ce612f9 100644 --- a/src/lib/api/types/crate/controlPlanes.ts +++ b/src/lib/api/types/crate/controlPlanes.ts @@ -12,6 +12,20 @@ export interface Metadata { }; } +export interface Subject { + kind: string; + name: string; +} + +export interface RoleBinding { + role: string; + subjects: Subject[]; +} + +export interface Authorization { + roleBindings: RoleBinding[]; +} + export interface ControlPlaneType { metadata: Metadata; spec: @@ -19,6 +33,7 @@ export interface ControlPlaneType { authentication: { enableSystemIdentityProvider?: boolean; }; + authorization?: Authorization; components: ControlPlaneComponentsType; } | undefined; @@ -85,6 +100,6 @@ 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}, 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 }}', }; }; diff --git a/src/lib/monaco.ts b/src/lib/monaco.ts index e17d5939..0febd19a 100644 --- a/src/lib/monaco.ts +++ b/src/lib/monaco.ts @@ -7,6 +7,7 @@ import 'monaco-editor/esm/vs/basic-languages/yaml/yaml.contribution.js'; import EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'; import YamlWorker from 'monaco-yaml/yaml.worker?worker'; +import { configureMonacoYaml } from 'monaco-yaml'; // Use ESM monaco to avoid loading AMD loader from CDN loader.config({ monaco }); @@ -88,4 +89,11 @@ export const configureMonaco = () => { rules: [], colors: GITHUB_DARK_EDITOR_COLORS, }); + + configureMonacoYaml(monaco, { + hover: true, + completion: true, + validate: true, + format: true, + }); }; diff --git a/src/lib/shared/McpContext.tsx b/src/lib/shared/McpContext.tsx index 8c46a06d..4c6fcebe 100644 --- a/src/lib/shared/McpContext.tsx +++ b/src/lib/shared/McpContext.tsx @@ -1,5 +1,5 @@ import { createContext, ReactNode, useContext } from 'react'; -import { ControlPlane as ManagedControlPlaneResource } from '../api/types/crate/controlPlanes.ts'; +import { ControlPlane as ManagedControlPlaneResource, RoleBinding } from '../api/types/crate/controlPlanes.ts'; import { ApiConfigProvider } from '../../components/Shared/k8s'; import { useApiResource } from '../api/useApiResource.ts'; import { GetKubeconfig } from '../api/types/crate/getKubeconfig.ts'; @@ -15,6 +15,7 @@ interface Mcp { secretName?: string; secretKey?: string; kubeconfig?: string; + roleBindings?: RoleBinding[]; } interface Props { @@ -44,6 +45,7 @@ export const McpContextProvider = ({ children, context }: Props) => { return <>; } context.kubeconfig = kubeconfig.data; + context.roleBindings = mcp.data?.spec?.authorization?.roleBindings; return {children}; }; diff --git a/src/spaces/mcp/auth/useHasMcpAdminRights.ts b/src/spaces/mcp/auth/useHasMcpAdminRights.ts new file mode 100644 index 00000000..46fbc6f9 --- /dev/null +++ b/src/spaces/mcp/auth/useHasMcpAdminRights.ts @@ -0,0 +1,20 @@ +import { useAuthOnboarding } from '../../onboarding/auth/AuthContextOnboarding.tsx'; +import { useMcp } from '../../../lib/shared/McpContext.tsx'; + +export function useHasMcpAdminRights(): boolean { + const auth = useAuthOnboarding(); + const mcp = useMcp(); + const userEmail = auth.user?.email; + const mcpUsers = mcp.roleBindings ?? []; + + if (!userEmail) { + return false; + } + + const matchingRoleBinding = mcpUsers.find( + (roleBinding) => + Array.isArray(roleBinding.subjects) && roleBinding.subjects.some((subject) => subject?.name?.includes(userEmail)), + ); + + return matchingRoleBinding?.role === 'admin'; +} diff --git a/src/spaces/mcp/pages/McpPage.tsx b/src/spaces/mcp/pages/McpPage.tsx index b33e3bde..01ea3f8a 100644 --- a/src/spaces/mcp/pages/McpPage.tsx +++ b/src/spaces/mcp/pages/McpPage.tsx @@ -28,6 +28,7 @@ import { useApiResource } from '../../../lib/api/useApiResource.ts'; import { YamlViewButton } from '../../../components/Yaml/YamlViewButton.tsx'; import { Landscapers } from '../../../components/ControlPlane/Landscapers.tsx'; import { AuthProviderMcp } from '../auth/AuthContextMcp.tsx'; +import { useHasMcpAdminRights } from '../auth/useHasMcpAdminRights.ts'; import { isNotFoundError } from '../../../lib/api/error.ts'; import { NotFoundBanner } from '../../../components/Ui/NotFoundBanner/NotFoundBanner.tsx'; import Graph from '../../../components/Graphs/Graph.tsx';