feat: optimize web ui
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user