feat: optimize web ui

This commit is contained in:
2026-04-02 18:22:14 +08:00
parent ccf76fea95
commit 143b1e8c4b
24 changed files with 1833 additions and 246 deletions

View File

@@ -1,21 +1,23 @@
import { useState } from 'react';
import { useMutation } from '@tanstack/react-query';
import { apiFetch } from '../../lib/api';
import ConfirmDialog from '../../components/ConfirmDialog';
type Project = { id: string; name: string };
export default function McpIntegration({ project }: { project: Project }) {
const [apiKey, setApiKey] = useState<string | null>(null);
const [showRotateConfirm, setShowRotateConfirm] = useState(false);
const [copied, setCopied] = useState<string | null>(null);
const mcpHost = window.location.hostname;
const mcpUrl = `http://${mcpHost}:3001/mcp/${project.id}`;
const rotateMutation = useMutation({
mutationFn: () => apiFetch<{ apiKey: string }>(`/projects/${project.id}/api-key/rotate`, { method: 'POST' }),
onSuccess: (data) => setApiKey(data.apiKey),
onSuccess: (data) => { setApiKey(data.apiKey); setShowRotateConfirm(false); },
});
const serverName = project.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '');
const configSnippet = JSON.stringify({
mcpServers: {
[serverName]: {
@@ -26,52 +28,105 @@ export default function McpIntegration({ project }: { project: Project }) {
},
}, null, 2);
const copyText = (text: string, key: string) => {
navigator.clipboard.writeText(text);
setCopied(key);
setTimeout(() => setCopied(null), 2000);
};
return (
<div className="space-y-6 max-w-2xl">
<div>
<h3 className="font-medium mb-2">MCP Service URL</h3>
<div className="max-w-2xl space-y-8">
{/* MCP URL */}
<section>
<p className="section-title">MCP Service URL</p>
<p className="section-desc mb-3">Connect your LLM client to this endpoint.</p>
<div className="flex items-center gap-2">
<code className="flex-1 px-3 py-2 bg-gray-100 rounded text-sm font-mono">{mcpUrl}</code>
<button onClick={() => navigator.clipboard.writeText(mcpUrl)} className="px-3 py-2 text-sm bg-gray-200 rounded hover:bg-gray-300">Copy</button>
<code className="flex-1 px-3.5 py-2.5 rounded-lg bg-bg-tertiary border border-border-muted text-[13px] font-mono text-text-primary truncate">{mcpUrl}</code>
<button onClick={() => copyText(mcpUrl, 'url')} className="btn-outline shrink-0">
{copied === 'url' ? (
<><svg className="w-3.5 h-3.5 text-success" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}><path d="M5 13l4 4L19 7" /></svg> Copied</>
) : (
<><svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}><path d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" /></svg> Copy</>
)}
</button>
</div>
</div>
<div>
<h3 className="font-medium mb-2">API Key</h3>
</section>
{/* API Key */}
<section>
<p className="section-title">API Key</p>
<p className="section-desc mb-3">Used to authenticate MCP requests. Each project has its own key.</p>
{apiKey ? (
<div className="p-3 bg-yellow-50 border border-yellow-200 rounded">
<p className="text-xs text-yellow-700 mb-1">Save this key it won't be shown again.</p>
<code className="text-sm break-all">{apiKey}</code>
<div className="p-4 rounded-lg bg-warning-muted border border-warning/20 space-y-2">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<svg className="w-4 h-4 text-warning" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}><path d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z" /></svg>
<p className="text-xs font-medium text-warning">Save this key it won't be shown again.</p>
</div>
<button onClick={() => copyText(apiKey, 'key')} className="text-xs font-medium text-warning hover:underline">
{copied === 'key' ? 'Copied!' : 'Copy'}
</button>
</div>
<code className="block text-xs break-all text-text-primary font-mono bg-bg-primary/50 rounded p-2">{apiKey}</code>
</div>
) : (
<p className="text-sm text-gray-500">API key is hidden. Rotate to generate a new one.</p>
<div className="flex items-center gap-3 px-3.5 py-2.5 rounded-lg bg-bg-tertiary border border-border-muted">
<svg className="w-4 h-4 text-text-muted shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}><path d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" /></svg>
<p className="text-[13px] text-text-muted">API key is hidden. Rotate to generate a new one.</p>
</div>
)}
<button onClick={() => { if (confirm('This will invalidate the current API key. Continue?')) rotateMutation.mutate(); }}
className="mt-2 px-3 py-1 text-sm bg-orange-100 text-orange-700 rounded hover:bg-orange-200">Rotate API Key</button>
</div>
<div>
<h3 className="font-medium mb-2">Configuration for Claude Code / Cursor</h3>
<button onClick={() => setShowRotateConfirm(true)} className="btn-outline mt-3">
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
Rotate API Key
</button>
</section>
{/* Config snippet */}
<section>
<p className="section-title">Configuration for Claude Code / Cursor</p>
<p className="section-desc mb-3">Add this to your MCP client configuration.</p>
<div className="relative">
<pre className="bg-gray-900 text-green-400 p-4 rounded text-sm overflow-auto">{configSnippet}</pre>
<button onClick={() => navigator.clipboard.writeText(configSnippet)} className="absolute top-2 right-2 px-2 py-1 text-xs bg-gray-700 text-white rounded hover:bg-gray-600">Copy</button>
<pre className="code-block text-xs">{configSnippet}</pre>
<button onClick={() => copyText(configSnippet, 'config')} className="copy-btn absolute top-2.5 right-2.5">
{copied === 'config' ? 'Copied!' : 'Copy'}
</button>
</div>
</div>
<div>
<h3 className="font-medium mb-2">Available Tools</h3>
<div className="space-y-2 text-sm">
</section>
{/* Available tools */}
<section>
<p className="section-title">Available MCP Tools</p>
<p className="section-desc mb-3">5 tools for progressive drill-down, designed for minimal token usage.</p>
<div className="space-y-1.5 stagger-children">
{[
{ name: 'get_project_overview', desc: 'Get project name, version, base URL, and module summary. Call this first.' },
{ name: 'list_modules', desc: 'List all modules with descriptions and endpoint counts.' },
{ name: 'list_endpoints', desc: 'List endpoints in a module. Provide moduleId.' },
{ name: 'get_endpoint_detail', desc: 'Get full endpoint details: parameters, request body, responses.' },
{ name: 'search_endpoints', desc: 'Search by keyword across all endpoints. Optional moduleId filter.' },
{ name: 'get_project_overview', desc: 'Get project name, version, base URL, and module summary. Call this first.', num: '1' },
{ name: 'list_modules', desc: 'List all modules with descriptions and endpoint counts.', num: '2' },
{ name: 'list_endpoints', desc: 'List endpoints in a module. Provide moduleId.', num: '3' },
{ name: 'get_endpoint_detail', desc: 'Get full endpoint details: parameters, request body, responses.', num: '4' },
{ name: 'search_endpoints', desc: 'Search by keyword across all endpoints. Optional moduleId filter.', num: '5' },
].map((t) => (
<div key={t.name} className="p-3 bg-gray-50 rounded">
<code className="font-medium">{t.name}</code>
<p className="text-gray-500 mt-1">{t.desc}</p>
<div key={t.name} className="card px-4 py-3 flex items-start gap-3">
<span className="w-5 h-5 rounded-full bg-accent-muted text-accent text-[10px] font-bold flex items-center justify-center shrink-0 mt-0.5">{t.num}</span>
<div className="min-w-0">
<code className="text-[13px] font-mono font-medium text-accent">{t.name}</code>
<p className="text-xs text-text-muted mt-0.5 leading-relaxed">{t.desc}</p>
</div>
</div>
))}
</div>
</div>
</section>
<ConfirmDialog
open={showRotateConfirm}
onCancel={() => setShowRotateConfirm(false)}
onConfirm={() => rotateMutation.mutate()}
title="Rotate API Key"
description="This will invalidate the current API key immediately. Any MCP clients using the old key will stop working."
confirmText="Rotate Key"
variant="warning"
/>
</div>
);
}