feat: opt web ux

This commit is contained in:
2026-04-02 22:10:24 +08:00
parent 143b1e8c4b
commit 35511eb877
16 changed files with 1251 additions and 383 deletions

View File

@@ -1,20 +1,19 @@
import { useState } from 'react';
import { useMutation } from '@tanstack/react-query';
import { useQuery } from '@tanstack/react-query';
import { apiFetch } from '../../lib/api';
import ConfirmDialog from '../../components/ConfirmDialog';
import { useLayoutContext } from '../Layout';
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 { onOpenSettings } = useLayoutContext();
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); setShowRotateConfirm(false); },
const { data: keyStatus } = useQuery({
queryKey: ['api-key-status'],
queryFn: () => apiFetch<{ hasKey: boolean; prefix: string | null }>('/auth/api-key/status'),
});
const serverName = project.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '');
@@ -23,7 +22,7 @@ export default function McpIntegration({ project }: { project: Project }) {
[serverName]: {
type: 'http',
url: mcpUrl,
headers: { Authorization: `Bearer ${apiKey || '<your-api-key>'}` },
headers: { Authorization: 'Bearer <your-api-key>' },
},
},
}, null, 2);
@@ -52,37 +51,6 @@ export default function McpIntegration({ project }: { project: Project }) {
</div>
</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-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>
) : (
<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={() => 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>
@@ -93,6 +61,30 @@ export default function McpIntegration({ project }: { project: Project }) {
{copied === 'config' ? 'Copied!' : 'Copy'}
</button>
</div>
{/* API Key guidance */}
{keyStatus && (
<div className="mt-3">
{keyStatus.hasKey ? (
<div className="flex items-center gap-2 px-3.5 py-2.5 rounded-lg bg-bg-tertiary border border-border-muted">
<svg className="w-4 h-4 text-success shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}><path d="M5 13l4 4L19 7" /></svg>
<p className="text-[13px] text-text-secondary">
API key generated. Copy it from{' '}
<button onClick={onOpenSettings} className="text-accent hover:underline font-medium">Settings</button>
{' '}and replace <code className="text-xs font-mono bg-bg-inset px-1 py-0.5 rounded">&lt;your-api-key&gt;</code> above.
</p>
</div>
) : (
<div className="flex items-center gap-3 p-3.5 rounded-lg bg-warning-muted border border-warning/20">
<svg className="w-4 h-4 text-warning shrink-0" 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-[13px] text-text-secondary flex-1">You need to generate an API key before using MCP.</p>
<button onClick={onOpenSettings} className="btn-primary shrink-0 text-[13px] py-1.5 px-3">
Open Settings
</button>
</div>
)}
</div>
)}
</section>
{/* Available tools */}
@@ -117,16 +109,6 @@ export default function McpIntegration({ project }: { project: Project }) {
))}
</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>
);
}