一键导入
connection-page
Auto-loads when creating or editing a plugin Connections.tsx page. Provides the exact standardized pattern used by all 32 plugins.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Auto-loads when creating or editing a plugin Connections.tsx page. Provides the exact standardized pattern used by all 32 plugins.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Auto-loads when editing dashboard CSS, styling, or UI components. Provides the dark theme CSS variables, component patterns, and design conventions.
Deployment checklist and commands for Node2Flow services. Auto-loads when deploying gateway, dashboard, or platform-worker.
Auto-loads when creating a new MCP plugin. Provides complete file templates for backend (gateway) and frontend (dashboard) plugin files with all required patterns.
| name | connection-page |
| description | Auto-loads when creating or editing a plugin Connections.tsx page. Provides the exact standardized pattern used by all 32 plugins. |
All 32 plugins use this identical Connections.tsx structure. Only the form fields and plugin-specific text change.
1. MCP Endpoint card (top) — Copy URL button
2. Header (title + Add button) — Add button hidden when empty
3. 2FA Alert (conditional) — Shield icon + enable link
4. Error Alert (conditional)
5. Blue Info Alert — Connection key explanation
6. Empty State OR Table — Ternary switch
7. Add Connection Dialog
8. API Key Display Dialog
9. Edit Name (Sheet on mobile, Dialog on desktop)
10. Delete Confirmation AlertDialog
11. Revoke Key AlertDialog
12. Generate Key AlertDialog
All from @node2flow/dashboard-core (single import):
import { getConnections, useConnection, useSudoContext, type Connection, Field, FieldLabel,
FieldDescription, InputGroup, InputGroupInput, InputGroupAddon, Button, Card, CardContent,
Alert, AlertTitle, AlertDescription, Badge, Table, TableHeader, TableBody, TableHead,
TableRow, TableCell, Dialog, DialogContent, DialogHeader, DialogTitle, AlertDialog,
AlertDialogContent, AlertDialogHeader, AlertDialogTitle, AlertDialogDescription,
AlertDialogFooter, AlertDialogCancel, AlertDialogAction, Item, ItemMedia, ItemContent,
ItemTitle, ItemDescription, ItemActions, DropdownMenu, DropdownMenuTrigger,
DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, Empty, EmptyHeader,
EmptyMedia, EmptyTitle, EmptyDescription, EmptyContent, useIsMobile, Sheet, SheetContent,
SheetHeader, SheetTitle, SheetFooter, SheetClose } from '@node2flow/dashboard-core';
import { createConnection, updateConnection, deleteConnection } from '../../lib/gateway-api';
import { getApiKeys, createApiKey, deleteApiKey } from '../../lib/platform-api';
import type { ApiKeyInfo } from '../../lib/platform-api';
const mcpUrl = `${import.meta.env.VITE_GATEWAY_URL || 'https://mcp.node2flow.net'}/mcp`;
<Item>
<ItemMedia><img src="/logos/xyz.svg?v=2" alt="XYZ" className="h-10 w-10" /></ItemMedia>
<ItemContent>
<ItemTitle>MCP Endpoint</ItemTitle>
<ItemDescription><code className="font-mono text-xs text-foreground break-all">{mcpUrl}</code></ItemDescription>
</ItemContent>
<ItemActions>
<Button variant="outline" size="sm" onClick={() => { navigator.clipboard.writeText(mcpUrl); ... }}>
{copiedMcp ? <><Check /> Copied</> : <><Copy /> Copy URL</>}
</Button>
</ItemActions>
</Item>
{connections.length > 0 && (
<Button variant="outline" onClick={() => setShowAddModal(true)}>
<Plus className="h-4 w-4 mr-2" /> Add Connection
</Button>
)}
<Empty>
<EmptyHeader>
<EmptyMedia variant="icon"><PluginSpecificIcon /></EmptyMedia>
<EmptyTitle>No connections yet</EmptyTitle>
<EmptyDescription>Add your XYZ credentials to get started.</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<Button variant="outline" onClick={() => setShowAddModal(true)}>
<Plus className="h-4 w-4 mr-2" />Add Connection
</Button>
<p className="text-xs text-muted-foreground">
By connecting, you agree to our <Link to="/terms">Terms</Link> and <Link to="/privacy">Privacy Policy</Link>
</p>
</EmptyContent>
</Empty>
{isMobile ? (
<Sheet open={!!editTarget} onOpenChange={(open) => !open && setEditTarget(null)}>
<SheetContent side="bottom">...</SheetContent>
</Sheet>
) : (
<Dialog open={!!editTarget} onOpenChange={(open) => !open && setEditTarget(null)}>
<DialogContent>...</DialogContent>
</Dialog>
)}
<div className={`w-2.5 h-2.5 rounded-full mx-auto ${conn.status === 'active' ? 'bg-green-400' : 'bg-muted-foreground'}`} />
<div className="space-y-6 px-4 lg:px-6">
Only these parts differ between plugins:
| Part | Changes |
|---|---|
| Logo path | /logos/xyz.svg |
| Plugin ID | 'xyz' in getConnections() / createConnection() |
| Page title | "XYZ Connections" |
| Form fields | Match createClient() config keys |
| Empty state icon | Plugin-specific Lucide icon |
| Empty state description | Plugin-specific text |
| Info alert text | How to get credentials |
| Add dialog fields | Field + InputGroup for each config key |
{/* Text field */}
<Field>
<FieldLabel>API Key</FieldLabel>
<InputGroup>
<InputGroupAddon><Key /></InputGroupAddon>
<InputGroupInput type="password" placeholder="sk-xxx" value={formApiKey}
onChange={(e) => setFormApiKey(e.target.value)} required />
</InputGroup>
<FieldDescription>From Settings → API Keys</FieldDescription>
</Field>
{/* Optional field (no required attr) */}
<Field>
<FieldLabel>Account ID <span className="text-muted-foreground font-normal">(optional)</span></FieldLabel>
<InputGroup>
<InputGroupAddon><Hash /></InputGroupAddon>
<InputGroupInput type="text" value={formAccountId}
onChange={(e) => setFormAccountId(e.target.value)} />
</InputGroup>
</Field>
All Connections.tsx code MUST pass ESLint with 0 errors.
// ✅ CORRECT — async function INSIDE useEffect, no top-level setState
useEffect(() => {
async function fetchData() {
try {
setError('');
const [connRes, keyRes] = await Promise.all([
getConnections('xyz'),
getApiKeys(),
]);
if (connRes.success && connRes.data) setConnections(connRes.data.connections);
if (keyRes.success && keyRes.data) setApiKeys(keyRes.data.api_keys);
} catch {
setError('Failed to load data');
} finally {
setLoading(false);
}
}
fetchData();
}, []);
useMemo/useCallback after early returns (if (loading) return)cd apps/dashboard && npx eslint src/plugins/xyz/Connections.tsx