mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-10 03:28:53 +00:00
feat: Add MCP server approval workflow UI (mock)
This commit implements a mock/demo version of the MCP server approval workflow UI to demonstrate the admin approval process. Changes: - Created approvals page with pending/approved/rejected tabs - Added MCPApprovalTable component with view/approve/reject actions - Added ApprovalDetailModal for viewing full request details - Created mock data with 10 sample MCP approval requests - Updated navigation to add "Approvals" menu item with badge count Features: - Admin-only access control - Tab-based view of requests by status - Approve/reject actions with reason capture - Detailed view of all request metadata - Badge showing pending request count in navigation Tech stack: - Next.js App Router (dashboard route) - Ant Design components (Table, Modal, Tabs, Badge) - TypeScript with proper type definitions - Client-side state management (mock data) Note: This is a prototype using mock data. Backend integration and database schema will be added in future PRs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
f8ef5a80e7
commit
393a9472e7
5 changed files with 797 additions and 1 deletions
149
ui/litellm-dashboard/src/app/(dashboard)/approvals/page.tsx
Normal file
149
ui/litellm-dashboard/src/app/(dashboard)/approvals/page.tsx
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Card, Tabs, Badge, message } from "antd";
|
||||
import { CheckCircleOutlined, CloseCircleOutlined, ClockCircleOutlined } from "@ant-design/icons";
|
||||
import MCPApprovalTable from "@/components/approvals/MCPApprovalTable";
|
||||
import { isAdminRole } from "@/components/utils/roles";
|
||||
import { mockMCPApprovalRequests, MCPApprovalRequest } from "@/components/approvals/mockData";
|
||||
|
||||
export default function ApprovalsPage() {
|
||||
const [userRole, setUserRole] = useState<string | null>(null);
|
||||
const [approvalRequests, setApprovalRequests] = useState<MCPApprovalRequest[]>(mockMCPApprovalRequests);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
// Get user role from localStorage/session
|
||||
const role = localStorage.getItem("user_role") || null;
|
||||
setUserRole(role);
|
||||
setLoading(false);
|
||||
}, []);
|
||||
|
||||
// Check if user is admin
|
||||
if (loading) {
|
||||
return (
|
||||
<div style={{ padding: "24px", textAlign: "center" }}>
|
||||
Loading...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isAdminRole(userRole)) {
|
||||
return (
|
||||
<div style={{ padding: "24px" }}>
|
||||
<Card>
|
||||
<div style={{ textAlign: "center", padding: "40px" }}>
|
||||
<CloseCircleOutlined style={{ fontSize: 48, color: "#ff4d4f", marginBottom: 16 }} />
|
||||
<h2>Access Denied</h2>
|
||||
<p>Admin role required to access this page.</p>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const pendingCount = approvalRequests.filter(r => r.status === "pending").length;
|
||||
const approvedCount = approvalRequests.filter(r => r.status === "approved").length;
|
||||
const rejectedCount = approvalRequests.filter(r => r.status === "rejected").length;
|
||||
|
||||
const handleApprove = (approvalId: string) => {
|
||||
setApprovalRequests(prev =>
|
||||
prev.map(req =>
|
||||
req.approval_id === approvalId
|
||||
? {
|
||||
...req,
|
||||
status: "approved" as const,
|
||||
reviewed_at: new Date().toISOString(),
|
||||
reviewed_by: "current-admin"
|
||||
}
|
||||
: req
|
||||
)
|
||||
);
|
||||
message.success("MCP server request approved successfully");
|
||||
};
|
||||
|
||||
const handleReject = (approvalId: string, reason: string) => {
|
||||
setApprovalRequests(prev =>
|
||||
prev.map(req =>
|
||||
req.approval_id === approvalId
|
||||
? {
|
||||
...req,
|
||||
status: "rejected" as const,
|
||||
rejection_reason: reason,
|
||||
reviewed_at: new Date().toISOString(),
|
||||
reviewed_by: "current-admin"
|
||||
}
|
||||
: req
|
||||
)
|
||||
);
|
||||
message.success("MCP server request rejected");
|
||||
};
|
||||
|
||||
const tabItems = [
|
||||
{
|
||||
key: "pending",
|
||||
label: (
|
||||
<span>
|
||||
<ClockCircleOutlined /> Pending{" "}
|
||||
{pendingCount > 0 && <Badge count={pendingCount} style={{ marginLeft: 8 }} />}
|
||||
</span>
|
||||
),
|
||||
children: (
|
||||
<MCPApprovalTable
|
||||
requests={approvalRequests.filter(r => r.status === "pending")}
|
||||
onApprove={handleApprove}
|
||||
onReject={handleReject}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "approved",
|
||||
label: (
|
||||
<span>
|
||||
<CheckCircleOutlined /> Approved{" "}
|
||||
{approvedCount > 0 && <Badge count={approvedCount} style={{ marginLeft: 8, backgroundColor: "#52c41a" }} />}
|
||||
</span>
|
||||
),
|
||||
children: (
|
||||
<MCPApprovalTable
|
||||
requests={approvalRequests.filter(r => r.status === "approved")}
|
||||
onApprove={handleApprove}
|
||||
onReject={handleReject}
|
||||
readOnly={true}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "rejected",
|
||||
label: (
|
||||
<span>
|
||||
<CloseCircleOutlined /> Rejected{" "}
|
||||
{rejectedCount > 0 && <Badge count={rejectedCount} style={{ marginLeft: 8, backgroundColor: "#ff4d4f" }} />}
|
||||
</span>
|
||||
),
|
||||
children: (
|
||||
<MCPApprovalTable
|
||||
requests={approvalRequests.filter(r => r.status === "rejected")}
|
||||
onApprove={handleApprove}
|
||||
onReject={handleReject}
|
||||
readOnly={true}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{ padding: "24px" }}>
|
||||
<Card
|
||||
title="MCP Server Approvals"
|
||||
extra={
|
||||
<div style={{ fontSize: 14, color: "#888" }}>
|
||||
{pendingCount} pending approval{pendingCount !== 1 ? "s" : ""}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Tabs items={tabItems} defaultActiveKey="pending" />
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,152 @@
|
|||
import { Modal, Descriptions, Tag, Button, Space, Divider } from "antd";
|
||||
import { MCPApprovalRequest } from "./mockData";
|
||||
|
||||
interface ApprovalDetailModalProps {
|
||||
request: MCPApprovalRequest;
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
onApprove?: (approvalId: string) => void;
|
||||
onReject?: (approvalId: string) => void;
|
||||
}
|
||||
|
||||
export default function ApprovalDetailModal({
|
||||
request,
|
||||
visible,
|
||||
onClose,
|
||||
onApprove,
|
||||
onReject
|
||||
}: ApprovalDetailModalProps) {
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case "pending":
|
||||
return "gold";
|
||||
case "approved":
|
||||
return "green";
|
||||
case "rejected":
|
||||
return "red";
|
||||
default:
|
||||
return "default";
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={`MCP Server Request: ${request.server_name}`}
|
||||
open={visible}
|
||||
onCancel={onClose}
|
||||
width={800}
|
||||
footer={
|
||||
<Space>
|
||||
<Button onClick={onClose}>Close</Button>
|
||||
{onApprove && request.status === "pending" && (
|
||||
<>
|
||||
<Button
|
||||
danger
|
||||
onClick={() => {
|
||||
onReject?.(request.approval_id);
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
Reject
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
onApprove(request.approval_id);
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
Approve
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Descriptions column={1} bordered>
|
||||
<Descriptions.Item label="Status">
|
||||
<Tag color={getStatusColor(request.status)}>
|
||||
{request.status.toUpperCase()}
|
||||
</Tag>
|
||||
</Descriptions.Item>
|
||||
|
||||
<Descriptions.Item label="Server Name">{request.server_name}</Descriptions.Item>
|
||||
<Descriptions.Item label="Alias">{request.alias || "-"}</Descriptions.Item>
|
||||
<Descriptions.Item label="Description">{request.description || "-"}</Descriptions.Item>
|
||||
<Descriptions.Item label="URL">{request.url}</Descriptions.Item>
|
||||
<Descriptions.Item label="Transport">{request.transport}</Descriptions.Item>
|
||||
<Descriptions.Item label="Auth Type">{request.auth_type || "None"}</Descriptions.Item>
|
||||
|
||||
<Descriptions.Item label="Access Groups">
|
||||
{request.mcp_access_groups.length > 0 ? (
|
||||
request.mcp_access_groups.map(group => (
|
||||
<Tag key={group} color="blue" style={{ marginBottom: 4 }}>
|
||||
{group}
|
||||
</Tag>
|
||||
))
|
||||
) : (
|
||||
"-"
|
||||
)}
|
||||
</Descriptions.Item>
|
||||
|
||||
<Descriptions.Item label="Allowed Tools">
|
||||
{request.allowed_tools.length > 0 ? (
|
||||
<div style={{ display: "flex", flexWrap: "wrap", gap: 4 }}>
|
||||
{request.allowed_tools.map(tool => (
|
||||
<Tag key={tool} style={{ marginBottom: 4 }}>
|
||||
{tool}
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
"-"
|
||||
)}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
|
||||
<Divider>Requester Information</Divider>
|
||||
|
||||
<Descriptions column={1} bordered>
|
||||
<Descriptions.Item label="Requester Email">{request.requester_email}</Descriptions.Item>
|
||||
<Descriptions.Item label="Team">{request.team_name || "-"}</Descriptions.Item>
|
||||
<Descriptions.Item label="Requested Date">
|
||||
{new Date(request.created_at).toLocaleString()}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
|
||||
{request.request_metadata && (
|
||||
<>
|
||||
<Divider>Request Metadata</Divider>
|
||||
<Descriptions column={1} bordered>
|
||||
{request.request_metadata.business_justification && (
|
||||
<Descriptions.Item label="Business Justification">
|
||||
{request.request_metadata.business_justification}
|
||||
</Descriptions.Item>
|
||||
)}
|
||||
{request.request_metadata.expected_usage && (
|
||||
<Descriptions.Item label="Expected Usage">
|
||||
{request.request_metadata.expected_usage}
|
||||
</Descriptions.Item>
|
||||
)}
|
||||
</Descriptions>
|
||||
</>
|
||||
)}
|
||||
|
||||
{request.status !== "pending" && (
|
||||
<>
|
||||
<Divider>Review Information</Divider>
|
||||
<Descriptions column={1} bordered>
|
||||
<Descriptions.Item label="Reviewed At">
|
||||
{request.reviewed_at ? new Date(request.reviewed_at).toLocaleString() : "-"}
|
||||
</Descriptions.Item>
|
||||
{request.rejection_reason && (
|
||||
<Descriptions.Item label="Rejection Reason">
|
||||
<div style={{ color: "#ff4d4f" }}>{request.rejection_reason}</div>
|
||||
</Descriptions.Item>
|
||||
)}
|
||||
</Descriptions>
|
||||
</>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,231 @@
|
|||
import { useState } from "react";
|
||||
import { Table, Button, Space, Tag, Tooltip, Modal, Input, Badge } from "antd";
|
||||
import { EyeOutlined, CheckOutlined, CloseOutlined } from "@ant-design/icons";
|
||||
import type { ColumnsType } from "antd/es/table";
|
||||
import { MCPApprovalRequest } from "./mockData";
|
||||
import ApprovalDetailModal from "./ApprovalDetailModal";
|
||||
|
||||
interface MCPApprovalTableProps {
|
||||
requests: MCPApprovalRequest[];
|
||||
onApprove: (approvalId: string) => void;
|
||||
onReject: (approvalId: string, reason: string) => void;
|
||||
readOnly?: boolean;
|
||||
}
|
||||
|
||||
export default function MCPApprovalTable({
|
||||
requests,
|
||||
onApprove,
|
||||
onReject,
|
||||
readOnly = false
|
||||
}: MCPApprovalTableProps) {
|
||||
const [selectedRequest, setSelectedRequest] = useState<MCPApprovalRequest | null>(null);
|
||||
const [showDetailModal, setShowDetailModal] = useState(false);
|
||||
const [showRejectModal, setShowRejectModal] = useState(false);
|
||||
const [rejectReason, setRejectReason] = useState("");
|
||||
const [rejectingId, setRejectingId] = useState<string | null>(null);
|
||||
|
||||
const handleViewDetails = (record: MCPApprovalRequest) => {
|
||||
setSelectedRequest(record);
|
||||
setShowDetailModal(true);
|
||||
};
|
||||
|
||||
const handleApproveClick = (approvalId: string) => {
|
||||
onApprove(approvalId);
|
||||
};
|
||||
|
||||
const handleRejectClick = (approvalId: string) => {
|
||||
setRejectingId(approvalId);
|
||||
setShowRejectModal(true);
|
||||
};
|
||||
|
||||
const handleRejectConfirm = () => {
|
||||
if (rejectingId && rejectReason.trim()) {
|
||||
onReject(rejectingId, rejectReason);
|
||||
setShowRejectModal(false);
|
||||
setRejectReason("");
|
||||
setRejectingId(null);
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case "pending":
|
||||
return "gold";
|
||||
case "approved":
|
||||
return "green";
|
||||
case "rejected":
|
||||
return "red";
|
||||
default:
|
||||
return "default";
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusText = (status: string) => {
|
||||
switch (status) {
|
||||
case "pending":
|
||||
return "Pending";
|
||||
case "approved":
|
||||
return "Approved";
|
||||
case "rejected":
|
||||
return "Rejected";
|
||||
default:
|
||||
return status;
|
||||
}
|
||||
};
|
||||
|
||||
const columns: ColumnsType<MCPApprovalRequest> = [
|
||||
{
|
||||
title: "Status",
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
width: 120,
|
||||
render: (status: string) => (
|
||||
<Tag color={getStatusColor(status)}>{getStatusText(status)}</Tag>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: "Server Name",
|
||||
dataIndex: "server_name",
|
||||
key: "server_name",
|
||||
width: 200,
|
||||
render: (text: string, record: MCPApprovalRequest) => (
|
||||
<div>
|
||||
<div style={{ fontWeight: 500 }}>{text}</div>
|
||||
{record.alias && (
|
||||
<div style={{ fontSize: 12, color: "#888" }}>Alias: {record.alias}</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: "Description",
|
||||
dataIndex: "description",
|
||||
key: "description",
|
||||
ellipsis: true,
|
||||
render: (text: string) => (
|
||||
<Tooltip title={text}>
|
||||
<span>{text || "-"}</span>
|
||||
</Tooltip>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: "Requested By",
|
||||
key: "requester",
|
||||
width: 200,
|
||||
render: (record: MCPApprovalRequest) => (
|
||||
<div>
|
||||
<div>{record.requester_email}</div>
|
||||
{record.team_name && (
|
||||
<Badge
|
||||
count={record.team_name}
|
||||
style={{ backgroundColor: "#1890ff", marginTop: 4 }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: "Requested Date",
|
||||
dataIndex: "created_at",
|
||||
key: "created_at",
|
||||
width: 150,
|
||||
render: (date: string) => new Date(date).toLocaleDateString()
|
||||
},
|
||||
{
|
||||
title: "Access Groups",
|
||||
dataIndex: "mcp_access_groups",
|
||||
key: "mcp_access_groups",
|
||||
width: 150,
|
||||
render: (groups: string[]) => (
|
||||
<Tooltip title={groups.join(", ")}>
|
||||
<Badge count={groups.length} style={{ backgroundColor: "#52c41a" }} />
|
||||
</Tooltip>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: "Actions",
|
||||
key: "actions",
|
||||
width: 200,
|
||||
fixed: "right",
|
||||
render: (record: MCPApprovalRequest) => (
|
||||
<Space size="small">
|
||||
<Button
|
||||
size="small"
|
||||
icon={<EyeOutlined />}
|
||||
onClick={() => handleViewDetails(record)}
|
||||
>
|
||||
View
|
||||
</Button>
|
||||
{!readOnly && record.status === "pending" && (
|
||||
<>
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
icon={<CheckOutlined />}
|
||||
onClick={() => handleApproveClick(record.approval_id)}
|
||||
>
|
||||
Approve
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
danger
|
||||
icon={<CloseOutlined />}
|
||||
onClick={() => handleRejectClick(record.approval_id)}
|
||||
>
|
||||
Reject
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={requests}
|
||||
rowKey="approval_id"
|
||||
pagination={{ pageSize: 10 }}
|
||||
scroll={{ x: 1200 }}
|
||||
/>
|
||||
|
||||
{selectedRequest && (
|
||||
<ApprovalDetailModal
|
||||
request={selectedRequest}
|
||||
visible={showDetailModal}
|
||||
onClose={() => setShowDetailModal(false)}
|
||||
onApprove={!readOnly ? handleApproveClick : undefined}
|
||||
onReject={!readOnly ? handleRejectClick : undefined}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Modal
|
||||
title="Reject MCP Server Request"
|
||||
open={showRejectModal}
|
||||
onOk={handleRejectConfirm}
|
||||
onCancel={() => {
|
||||
setShowRejectModal(false);
|
||||
setRejectReason("");
|
||||
}}
|
||||
okText="Confirm Rejection"
|
||||
okButtonProps={{ danger: true, disabled: !rejectReason.trim() }}
|
||||
>
|
||||
<div style={{ marginBottom: 8, fontWeight: 500 }}>
|
||||
Rejection Reason <span style={{ color: "#ff4d4f" }}>*</span>
|
||||
</div>
|
||||
<Input.TextArea
|
||||
rows={4}
|
||||
placeholder="Explain why this request is being rejected..."
|
||||
value={rejectReason}
|
||||
onChange={(e) => setRejectReason(e.target.value)}
|
||||
required
|
||||
/>
|
||||
<div style={{ marginTop: 8, fontSize: 12, color: "#888" }}>
|
||||
The requester will be notified with this reason.
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
252
ui/litellm-dashboard/src/components/approvals/mockData.ts
Normal file
252
ui/litellm-dashboard/src/components/approvals/mockData.ts
Normal file
|
|
@ -0,0 +1,252 @@
|
|||
// Mock data for MCP approval requests
|
||||
// This file contains sample data for demonstrating the approval workflow
|
||||
|
||||
export interface MCPApprovalRequest {
|
||||
approval_id: string;
|
||||
server_name: string;
|
||||
alias?: string;
|
||||
description?: string;
|
||||
url: string;
|
||||
transport: string;
|
||||
auth_type?: string;
|
||||
mcp_access_groups: string[];
|
||||
allowed_tools: string[];
|
||||
status: 'pending' | 'approved' | 'rejected';
|
||||
requested_by: string;
|
||||
requested_by_team_id?: string;
|
||||
team_name?: string;
|
||||
requester_email: string;
|
||||
created_at: string;
|
||||
reviewed_by?: string;
|
||||
reviewed_at?: string;
|
||||
rejection_reason?: string;
|
||||
request_metadata?: {
|
||||
business_justification?: string;
|
||||
expected_usage?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const mockMCPApprovalRequests: MCPApprovalRequest[] = [
|
||||
{
|
||||
approval_id: "req-001",
|
||||
server_name: "team-engineering-mcp",
|
||||
alias: "eng-mcp",
|
||||
description: "Engineering team's MCP server for code analysis and automated review",
|
||||
url: "https://mcp.engineering.example.com",
|
||||
transport: "sse",
|
||||
auth_type: "bearer",
|
||||
mcp_access_groups: ["engineering", "backend-team"],
|
||||
allowed_tools: ["code-review", "lint-check", "test-runner", "coverage-analysis"],
|
||||
status: "pending",
|
||||
requested_by: "user-123",
|
||||
requested_by_team_id: "team-eng",
|
||||
team_name: "Engineering Team",
|
||||
requester_email: "john.smith@example.com",
|
||||
created_at: "2026-02-15T10:30:00Z",
|
||||
request_metadata: {
|
||||
business_justification: "Need automated code review for PR workflows and continuous integration. Will help reduce manual code review time by ~40%.",
|
||||
expected_usage: "~500 requests per day across 50 engineers"
|
||||
}
|
||||
},
|
||||
{
|
||||
approval_id: "req-002",
|
||||
server_name: "data-science-ml-mcp",
|
||||
alias: "ds-ml",
|
||||
description: "Data Science team's MCP server for ML model training and evaluation",
|
||||
url: "https://mcp.datascience.example.com",
|
||||
transport: "sse",
|
||||
auth_type: "oauth2",
|
||||
mcp_access_groups: ["data-science", "ml-team"],
|
||||
allowed_tools: ["train-model", "evaluate-model", "feature-engineering", "data-viz"],
|
||||
status: "pending",
|
||||
requested_by: "user-456",
|
||||
requested_by_team_id: "team-ds",
|
||||
team_name: "Data Science Team",
|
||||
requester_email: "sarah.jones@example.com",
|
||||
created_at: "2026-02-16T14:20:00Z",
|
||||
request_metadata: {
|
||||
business_justification: "Required for automated ML pipeline. Will enable faster model iteration and A/B testing.",
|
||||
expected_usage: "~200 training jobs per week, ~1000 evaluation requests per day"
|
||||
}
|
||||
},
|
||||
{
|
||||
approval_id: "req-003",
|
||||
server_name: "security-audit-mcp",
|
||||
alias: "sec-audit",
|
||||
description: "Security team's MCP server for vulnerability scanning and compliance checks",
|
||||
url: "https://mcp.security.example.com",
|
||||
transport: "sse",
|
||||
auth_type: "bearer",
|
||||
mcp_access_groups: ["security", "compliance-team"],
|
||||
allowed_tools: ["vulnerability-scan", "dependency-check", "secrets-detection", "compliance-audit"],
|
||||
status: "pending",
|
||||
requested_by: "user-789",
|
||||
requested_by_team_id: "team-sec",
|
||||
team_name: "Security Team",
|
||||
requester_email: "mike.chen@example.com",
|
||||
created_at: "2026-02-17T09:15:00Z",
|
||||
request_metadata: {
|
||||
business_justification: "Critical for SOC 2 compliance. Automated security scanning for all production deployments.",
|
||||
expected_usage: "~100 scans per day, triggered on every deployment"
|
||||
}
|
||||
},
|
||||
{
|
||||
approval_id: "req-004",
|
||||
server_name: "devops-infrastructure-mcp",
|
||||
alias: "devops-infra",
|
||||
description: "DevOps team's MCP server for infrastructure provisioning and monitoring",
|
||||
url: "https://mcp.devops.example.com",
|
||||
transport: "stdio",
|
||||
auth_type: "api_key",
|
||||
mcp_access_groups: ["devops", "sre-team"],
|
||||
allowed_tools: ["provision-server", "deploy-service", "health-check", "log-analysis"],
|
||||
status: "approved",
|
||||
requested_by: "user-111",
|
||||
requested_by_team_id: "team-devops",
|
||||
team_name: "DevOps Team",
|
||||
requester_email: "alex.rodriguez@example.com",
|
||||
created_at: "2026-02-10T11:00:00Z",
|
||||
reviewed_by: "admin-001",
|
||||
reviewed_at: "2026-02-11T15:30:00Z",
|
||||
request_metadata: {
|
||||
business_justification: "Essential for automated infrastructure management. Reduces manual toil and improves reliability.",
|
||||
expected_usage: "~300 infrastructure operations per day"
|
||||
}
|
||||
},
|
||||
{
|
||||
approval_id: "req-005",
|
||||
server_name: "product-analytics-mcp",
|
||||
alias: "prod-analytics",
|
||||
description: "Product team's MCP server for user analytics and metrics tracking",
|
||||
url: "https://mcp.product.example.com",
|
||||
transport: "sse",
|
||||
auth_type: "bearer",
|
||||
mcp_access_groups: ["product", "analytics-team"],
|
||||
allowed_tools: ["track-event", "query-metrics", "generate-report", "create-dashboard"],
|
||||
status: "approved",
|
||||
requested_by: "user-222",
|
||||
requested_by_team_id: "team-product",
|
||||
team_name: "Product Team",
|
||||
requester_email: "lisa.thompson@example.com",
|
||||
created_at: "2026-02-12T13:45:00Z",
|
||||
reviewed_by: "admin-002",
|
||||
reviewed_at: "2026-02-13T10:00:00Z",
|
||||
request_metadata: {
|
||||
business_justification: "Required for product analytics and user behavior tracking. Enables data-driven product decisions.",
|
||||
expected_usage: "~1000 events per minute, ~50 report generations per day"
|
||||
}
|
||||
},
|
||||
{
|
||||
approval_id: "req-006",
|
||||
server_name: "marketing-automation-mcp",
|
||||
alias: "marketing-auto",
|
||||
description: "Marketing team's MCP server for campaign automation and email marketing",
|
||||
url: "https://mcp.marketing.example.com",
|
||||
transport: "sse",
|
||||
auth_type: "oauth2",
|
||||
mcp_access_groups: ["marketing"],
|
||||
allowed_tools: ["send-campaign", "segment-users", "track-conversion", "ab-test"],
|
||||
status: "rejected",
|
||||
requested_by: "user-333",
|
||||
requested_by_team_id: "team-marketing",
|
||||
team_name: "Marketing Team",
|
||||
requester_email: "david.kim@example.com",
|
||||
created_at: "2026-02-14T16:00:00Z",
|
||||
reviewed_by: "admin-001",
|
||||
reviewed_at: "2026-02-15T09:20:00Z",
|
||||
rejection_reason: "Security concerns: OAuth2 configuration incomplete. Missing required scopes and callback URLs. Please resubmit with proper authentication setup.",
|
||||
request_metadata: {
|
||||
business_justification: "Need automated marketing campaigns for user engagement and retention.",
|
||||
expected_usage: "~10 campaigns per week, ~100K emails per month"
|
||||
}
|
||||
},
|
||||
{
|
||||
approval_id: "req-007",
|
||||
server_name: "customer-support-mcp",
|
||||
alias: "support",
|
||||
description: "Customer Support team's MCP server for ticket management and chatbot",
|
||||
url: "https://mcp.support.example.com",
|
||||
transport: "sse",
|
||||
auth_type: "bearer",
|
||||
mcp_access_groups: ["support", "customer-success"],
|
||||
allowed_tools: ["create-ticket", "update-ticket", "search-knowledge-base", "chatbot-response"],
|
||||
status: "pending",
|
||||
requested_by: "user-444",
|
||||
requested_by_team_id: "team-support",
|
||||
team_name: "Customer Support Team",
|
||||
requester_email: "emma.wilson@example.com",
|
||||
created_at: "2026-02-18T08:00:00Z",
|
||||
request_metadata: {
|
||||
business_justification: "Enhance customer support response times. Automated ticket routing and AI-powered responses.",
|
||||
expected_usage: "~500 tickets per day, ~2000 chatbot interactions per day"
|
||||
}
|
||||
},
|
||||
{
|
||||
approval_id: "req-008",
|
||||
server_name: "finance-reporting-mcp",
|
||||
alias: "fin-reports",
|
||||
description: "Finance team's MCP server for financial reporting and analysis",
|
||||
url: "https://mcp.finance.example.com",
|
||||
transport: "stdio",
|
||||
auth_type: "mtls",
|
||||
mcp_access_groups: ["finance", "accounting"],
|
||||
allowed_tools: ["generate-financial-report", "calculate-metrics", "budget-analysis", "forecast"],
|
||||
status: "rejected",
|
||||
requested_by: "user-555",
|
||||
requested_by_team_id: "team-finance",
|
||||
team_name: "Finance Team",
|
||||
requester_email: "robert.anderson@example.com",
|
||||
created_at: "2026-02-13T10:30:00Z",
|
||||
reviewed_by: "admin-002",
|
||||
reviewed_at: "2026-02-14T11:45:00Z",
|
||||
rejection_reason: "Insufficient access controls. Finance data requires additional encryption and audit logging. Please add detailed access policies and data retention policies.",
|
||||
request_metadata: {
|
||||
business_justification: "Automate monthly financial reports and forecasting. Reduce manual work by 60%.",
|
||||
expected_usage: "~50 reports per month, daily metric calculations"
|
||||
}
|
||||
},
|
||||
{
|
||||
approval_id: "req-009",
|
||||
server_name: "hr-recruitment-mcp",
|
||||
alias: "hr-recruit",
|
||||
description: "HR team's MCP server for recruitment automation and candidate screening",
|
||||
url: "https://mcp.hr.example.com",
|
||||
transport: "sse",
|
||||
auth_type: "bearer",
|
||||
mcp_access_groups: ["hr", "recruitment-team"],
|
||||
allowed_tools: ["parse-resume", "screen-candidate", "schedule-interview", "generate-offer"],
|
||||
status: "pending",
|
||||
requested_by: "user-666",
|
||||
requested_by_team_id: "team-hr",
|
||||
team_name: "HR Team",
|
||||
requester_email: "jessica.brown@example.com",
|
||||
created_at: "2026-02-17T15:30:00Z",
|
||||
request_metadata: {
|
||||
business_justification: "Streamline recruitment process. Automated resume screening and interview scheduling to reduce time-to-hire.",
|
||||
expected_usage: "~200 applications per week, ~50 interviews per month"
|
||||
}
|
||||
},
|
||||
{
|
||||
approval_id: "req-010",
|
||||
server_name: "legal-contract-mcp",
|
||||
alias: "legal-docs",
|
||||
description: "Legal team's MCP server for contract management and document review",
|
||||
url: "https://mcp.legal.example.com",
|
||||
transport: "sse",
|
||||
auth_type: "mtls",
|
||||
mcp_access_groups: ["legal"],
|
||||
allowed_tools: ["review-contract", "extract-terms", "compliance-check", "redline-document"],
|
||||
status: "approved",
|
||||
requested_by: "user-777",
|
||||
requested_by_team_id": "team-legal",
|
||||
team_name: "Legal Team",
|
||||
requester_email: "william.martinez@example.com",
|
||||
created_at: "2026-02-11T09:00:00Z",
|
||||
reviewed_by: "admin-001",
|
||||
reviewed_at: "2026-02-12T14:15:00Z",
|
||||
request_metadata: {
|
||||
business_justification: "Automated contract review and compliance checking. Speeds up legal review process by 50%.",
|
||||
expected_usage: "~30 contracts per week, ongoing compliance monitoring"
|
||||
}
|
||||
}
|
||||
];
|
||||
|
|
@ -27,7 +27,7 @@ import {
|
|||
UserOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import type { MenuProps } from "antd";
|
||||
import { ConfigProvider, Layout, Menu } from "antd";
|
||||
import { Badge, ConfigProvider, Layout, Menu } from "antd";
|
||||
import { useMemo } from "react";
|
||||
import { all_admin_roles, internalUserRoles, isAdminRole, rolesWithWriteAccess } from "../utils/roles";
|
||||
import NewBadge from "./common_components/NewBadge";
|
||||
|
|
@ -117,6 +117,18 @@ const menuGroups: MenuGroup[] = [
|
|||
icon: <AuditOutlined />,
|
||||
roles: all_admin_roles,
|
||||
},
|
||||
{
|
||||
key: "approvals",
|
||||
page: "approvals",
|
||||
label: (
|
||||
<span className="flex items-center gap-4">
|
||||
Approvals
|
||||
<Badge count={5} style={{ backgroundColor: "#faad14" }} />
|
||||
</span>
|
||||
),
|
||||
icon: <AuditOutlined />,
|
||||
roles: all_admin_roles,
|
||||
},
|
||||
{
|
||||
key: "tools",
|
||||
page: "tools",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue