misc: added permission guards in the UI

This commit is contained in:
Sheen Capadngan
2025-12-17 15:59:06 +08:00
parent 404c8b7de2
commit 902b16134f
12 changed files with 234 additions and 86 deletions

View File

@@ -16,5 +16,6 @@ export {
ProjectPermissionPkiSyncActions,
ProjectPermissionPkiTemplateActions,
ProjectPermissionSshHostActions,
ProjectPermissionMcpEndpointActions,
ProjectPermissionSub
} from "./types";

View File

@@ -27,6 +27,7 @@ export {
ProjectPermissionPkiSyncActions,
ProjectPermissionPkiTemplateActions,
ProjectPermissionSshHostActions,
ProjectPermissionMcpEndpointActions,
ProjectPermissionSub,
useProjectPermission
} from "./ProjectPermissionContext";

View File

@@ -10,6 +10,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate, useParams } from "@tanstack/react-router";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
Button,
ContentLoader,
@@ -20,6 +21,7 @@ import {
DropdownMenuTrigger,
EmptyState
} from "@app/components/v2";
import { ProjectPermissionMcpEndpointActions, ProjectPermissionSub } from "@app/context";
import { useDeleteAiMcpEndpoint, useGetAiMcpEndpointById } from "@app/hooks/api";
import { EditMCPEndpointModal } from "../MCPPage/components/MCPEndpointsTab/EditMCPEndpointModal";
@@ -98,7 +100,7 @@ const PageContent = () => {
<button
type="button"
onClick={handleBack}
className="mb-4 flex items-center gap-1 text-sm text-bunker-300 hover:text-primary-400"
className="mb-4 flex w-fit items-center gap-1 text-sm text-bunker-300 hover:text-primary-400"
>
<FontAwesomeIcon icon={faChevronLeft} className="text-xs" />
MCP Endpoints
@@ -121,13 +123,34 @@ const PageContent = () => {
<FontAwesomeIcon icon={faEllipsisV} />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setIsEditModalOpen(true)}>
Edit Endpoint
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setIsDeleteModalOpen(true)} className="text-red-500">
Delete Endpoint
</DropdownMenuItem>
<DropdownMenuContent align="end" sideOffset={6}>
<ProjectPermissionCan
I={ProjectPermissionMcpEndpointActions.Edit}
a={ProjectPermissionSub.McpEndpoints}
>
{(isAllowed) => (
<DropdownMenuItem
onClick={() => setIsEditModalOpen(true)}
isDisabled={!isAllowed}
>
Edit Endpoint
</DropdownMenuItem>
)}
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionMcpEndpointActions.Delete}
a={ProjectPermissionSub.McpEndpoints}
>
{(isAllowed) => (
<DropdownMenuItem
onClick={() => setIsDeleteModalOpen(true)}
className="text-red-500"
isDisabled={!isAllowed}
>
Delete Endpoint
</DropdownMenuItem>
)}
</ProjectPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</div>

View File

@@ -3,7 +3,9 @@ import { faCheck, faPencil, faServer, faTimes } from "@fortawesome/free-solid-sv
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import { Checkbox, IconButton, Spinner } from "@app/components/v2";
import { ProjectPermissionMcpEndpointActions, ProjectPermissionSub } from "@app/context";
import { useListAiMcpServers, useUpdateAiMcpEndpoint } from "@app/hooks/api";
type Props = {
@@ -96,14 +98,25 @@ export const MCPEndpointConnectedServersSection = ({ endpointId, projectId, serv
</IconButton>
</div>
) : (
<IconButton
ariaLabel="Edit connected servers"
variant="plain"
size="sm"
onClick={handleStartEdit}
<ProjectPermissionCan
I={ProjectPermissionMcpEndpointActions.Edit}
a={ProjectPermissionSub.McpEndpoints}
>
<FontAwesomeIcon icon={faPencil} className="text-bunker-300 hover:text-mineshaft-100" />
</IconButton>
{(isAllowed) => (
<IconButton
ariaLabel="Edit connected servers"
variant="plain"
size="sm"
onClick={handleStartEdit}
isDisabled={!isAllowed}
>
<FontAwesomeIcon
icon={faPencil}
className="text-bunker-300 hover:text-mineshaft-100"
/>
</IconButton>
)}
</ProjectPermissionCan>
)}
</div>
<div className="space-y-2">

View File

@@ -4,6 +4,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { format } from "date-fns";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
GenericFieldLabel,
IconButton,
@@ -13,6 +14,7 @@ import {
Switch,
Tooltip
} from "@app/components/v2";
import { ProjectPermissionMcpEndpointActions, ProjectPermissionSub } from "@app/context";
import { TAiMcpEndpointWithServerIds, useUpdateAiMcpEndpoint } from "@app/hooks/api";
type Props = {
@@ -122,14 +124,22 @@ export const MCPEndpointDetailsSection = ({ endpoint, onEdit }: Props) => {
<div className="flex w-full flex-col gap-3 rounded-lg border border-mineshaft-600 bg-mineshaft-900 px-4 py-3">
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-2">
<h3 className="text-lg font-medium text-mineshaft-100">Details</h3>
<IconButton
variant="plain"
colorSchema="secondary"
ariaLabel="Edit endpoint details"
onClick={onEdit}
<ProjectPermissionCan
I={ProjectPermissionMcpEndpointActions.Edit}
a={ProjectPermissionSub.McpEndpoints}
>
<FontAwesomeIcon icon={faEdit} />
</IconButton>
{(isAllowed) => (
<IconButton
variant="plain"
colorSchema="secondary"
ariaLabel="Edit endpoint details"
onClick={onEdit}
isDisabled={!isAllowed}
>
<FontAwesomeIcon icon={faEdit} />
</IconButton>
)}
</ProjectPermissionCan>
</div>
<div className="space-y-3">
<GenericFieldLabel label="Name">{endpoint.name}</GenericFieldLabel>

View File

@@ -10,6 +10,11 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import { Input, Switch, Tooltip } from "@app/components/v2";
import {
ProjectPermissionMcpEndpointActions,
ProjectPermissionSub,
useProjectPermission
} from "@app/context";
import {
TAiMcpEndpointToolConfig,
useDisableEndpointTool,
@@ -33,6 +38,7 @@ type ServerToolsSectionProps = {
toolConfigs: TAiMcpEndpointToolConfig[];
onToolToggle: (serverToolId: string, isEnabled: boolean) => void;
isUpdating: boolean;
canEdit: boolean;
};
const ServerToolsSection = ({
@@ -42,7 +48,8 @@ const ServerToolsSection = ({
searchQuery,
toolConfigs,
onToolToggle,
isUpdating
isUpdating,
canEdit
}: ServerToolsSectionProps) => {
const [isExpanded, setIsExpanded] = useState(false);
@@ -126,7 +133,7 @@ const ServerToolsSection = ({
id={`tool-${tool.id}`}
isChecked={isToolEnabled(tool.id)}
onCheckedChange={(checked) => onToolToggle(tool.id, checked)}
isDisabled={isUpdating}
isDisabled={isUpdating || !canEdit}
/>
</div>
))}
@@ -146,6 +153,12 @@ const ServerToolsSection = ({
export const MCPEndpointToolSelectionSection = ({ endpointId, projectId, serverIds }: Props) => {
const [searchQuery, setSearchQuery] = useState("");
const { permission } = useProjectPermission();
const canEdit = permission.can(
ProjectPermissionMcpEndpointActions.Edit,
ProjectPermissionSub.McpEndpoints
);
const { data: serversData } = useListAiMcpServers({ projectId });
const { data: toolConfigs = [] } = useListEndpointTools({ endpointId });
const enableTool = useEnableEndpointTool();
@@ -212,6 +225,7 @@ export const MCPEndpointToolSelectionSection = ({ endpointId, projectId, serverI
toolConfigs={toolConfigs}
onToolToggle={handleToolToggle}
isUpdating={enableTool.isPending || disableTool.isPending}
canEdit={canEdit}
/>
))
)}

View File

@@ -4,6 +4,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate, useParams } from "@tanstack/react-router";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
DropdownMenu,
DropdownMenuContent,
@@ -13,6 +14,7 @@ import {
Tooltip,
Tr
} from "@app/components/v2";
import { ProjectPermissionMcpEndpointActions, ProjectPermissionSub } from "@app/context";
import { useToggle } from "@app/hooks";
import { TAiMcpEndpoint } from "@app/hooks/api";
@@ -113,25 +115,41 @@ export const MCPEndpointRow = ({ endpoint, onEditEndpoint, onDeleteEndpoint }: P
>
Copy Endpoint ID
</DropdownMenuItem>
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
onEditEndpoint(endpoint);
}}
icon={<FontAwesomeIcon icon={faEdit} className="w-3" />}
<ProjectPermissionCan
I={ProjectPermissionMcpEndpointActions.Edit}
a={ProjectPermissionSub.McpEndpoints}
>
Edit Endpoint
</DropdownMenuItem>
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
onDeleteEndpoint(endpoint);
}}
icon={<FontAwesomeIcon icon={faTrash} className="w-3" />}
className="text-red-500 hover:text-red-400"
{(isAllowed) => (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
onEditEndpoint(endpoint);
}}
icon={<FontAwesomeIcon icon={faEdit} className="w-3" />}
isDisabled={!isAllowed}
>
Edit Endpoint
</DropdownMenuItem>
)}
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionMcpEndpointActions.Delete}
a={ProjectPermissionSub.McpEndpoints}
>
Delete Endpoint
</DropdownMenuItem>
{(isAllowed) => (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
onDeleteEndpoint(endpoint);
}}
icon={<FontAwesomeIcon icon={faTrash} className="w-3" />}
className="text-red-500 hover:text-red-400"
isDisabled={!isAllowed}
>
Delete Endpoint
</DropdownMenuItem>
)}
</ProjectPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</Td>

View File

@@ -3,7 +3,9 @@ import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import { Button, DeleteActionModal } from "@app/components/v2";
import { ProjectPermissionMcpEndpointActions, ProjectPermissionSub } from "@app/context";
import { TAiMcpEndpoint, useDeleteAiMcpEndpoint } from "@app/hooks/api";
import { AddMCPEndpointModal } from "./AddMCPEndpointModal";
@@ -63,14 +65,22 @@ export const MCPEndpointsTab = () => {
</p>
</div>
<Button
colorSchema="primary"
type="button"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={handleCreateEndpoint}
<ProjectPermissionCan
I={ProjectPermissionMcpEndpointActions.Create}
a={ProjectPermissionSub.McpEndpoints}
>
Create Endpoint
</Button>
{(isAllowed) => (
<Button
colorSchema="primary"
type="button"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={handleCreateEndpoint}
isDisabled={!isAllowed}
>
Create Endpoint
</Button>
)}
</ProjectPermissionCan>
</div>
<MCPEndpointList

View File

@@ -4,6 +4,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate, useParams } from "@tanstack/react-router";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
DropdownMenu,
DropdownMenuContent,
@@ -13,6 +14,7 @@ import {
Tooltip,
Tr
} from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { useToggle } from "@app/hooks";
import { AiMcpServerStatus, TAiMcpServer } from "@app/hooks/api";
@@ -97,25 +99,41 @@ export const MCPServerRow = ({ server, onEditServer, onDeleteServer }: Props) =>
>
Copy Server ID
</DropdownMenuItem>
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
onEditServer(server);
}}
icon={<FontAwesomeIcon icon={faEdit} className="w-3" />}
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.McpServers}
>
Edit Server
</DropdownMenuItem>
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
onDeleteServer(server);
}}
icon={<FontAwesomeIcon icon={faTrash} className="w-3" />}
className="text-red-500 hover:text-red-400"
{(isAllowed) => (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
onEditServer(server);
}}
icon={<FontAwesomeIcon icon={faEdit} className="w-3" />}
isDisabled={!isAllowed}
>
Edit Server
</DropdownMenuItem>
)}
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.McpServers}
>
Delete Server
</DropdownMenuItem>
{(isAllowed) => (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
onDeleteServer(server);
}}
icon={<FontAwesomeIcon icon={faTrash} className="w-3" />}
className="text-red-500 hover:text-red-400"
isDisabled={!isAllowed}
>
Delete Server
</DropdownMenuItem>
)}
</ProjectPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</Td>

View File

@@ -3,7 +3,9 @@ import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import { Button, DeleteActionModal } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { TAiMcpServer, useDeleteAiMcpServer } from "@app/hooks/api";
import { AddMCPServerModal } from "./AddMCPServerModal";
@@ -63,14 +65,22 @@ export const MCPServersTab = () => {
</p>
</div>
<Button
colorSchema="primary"
type="button"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={handleAddServer}
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={ProjectPermissionSub.McpServers}
>
Add MCP Server
</Button>
{(isAllowed) => (
<Button
colorSchema="primary"
type="button"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={handleAddServer}
isDisabled={!isAllowed}
>
Add MCP Server
</Button>
)}
</ProjectPermissionCan>
</div>
<MCPServerList onEditServer={handleEditServer} onDeleteServer={handleDeleteServer} />

View File

@@ -5,6 +5,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate, useParams } from "@tanstack/react-router";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
Button,
ContentLoader,
@@ -15,6 +16,7 @@ import {
DropdownMenuTrigger,
EmptyState
} from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { useDeleteAiMcpServer, useGetAiMcpServerById } from "@app/hooks/api";
import { EditMCPServerModal } from "../MCPPage/components/MCPServersTab/EditMCPServerModal";
@@ -118,12 +120,33 @@ const PageContent = () => {
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setIsEditModalOpen(true)}>
Edit Server
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setIsDeleteModalOpen(true)} className="text-red-500">
Delete Server
</DropdownMenuItem>
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.McpServers}
>
{(isAllowed) => (
<DropdownMenuItem
onClick={() => setIsEditModalOpen(true)}
isDisabled={!isAllowed}
>
Edit Server
</DropdownMenuItem>
)}
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.McpServers}
>
{(isAllowed) => (
<DropdownMenuItem
onClick={() => setIsDeleteModalOpen(true)}
className="text-red-500"
isDisabled={!isAllowed}
>
Delete Server
</DropdownMenuItem>
)}
</ProjectPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</div>

View File

@@ -2,7 +2,9 @@ import { faEdit } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { format } from "date-fns";
import { ProjectPermissionCan } from "@app/components/permissions";
import { GenericFieldLabel, IconButton } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { AiMcpServerStatus, TAiMcpServer } from "@app/hooks/api";
type Props = {
@@ -33,14 +35,19 @@ export const MCPServerDetailsSection = ({ server, onEdit }: Props) => {
<div className="flex w-full flex-col gap-3 rounded-lg border border-mineshaft-600 bg-mineshaft-900 px-4 py-3">
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-2">
<h3 className="text-lg font-medium text-mineshaft-100">Details</h3>
<IconButton
variant="plain"
colorSchema="secondary"
ariaLabel="Edit server details"
onClick={onEdit}
>
<FontAwesomeIcon icon={faEdit} />
</IconButton>
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.McpServers}>
{(isAllowed) => (
<IconButton
variant="plain"
colorSchema="secondary"
ariaLabel="Edit server details"
onClick={onEdit}
isDisabled={!isAllowed}
>
<FontAwesomeIcon icon={faEdit} />
</IconButton>
)}
</ProjectPermissionCan>
</div>
<div className="space-y-3">
<GenericFieldLabel label="Name">{server.name}</GenericFieldLabel>