231 lines
6.4 KiB
TypeScript
231 lines
6.4 KiB
TypeScript
import { useEffect } from 'react'
|
|
import { Copy, ShieldCheck } from 'lucide-react'
|
|
import { Button } from '@/components/ui/button'
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from '@/components/ui/dialog'
|
|
import type { McpStatus } from '../hooks/useMcpStatus'
|
|
import { createTranslator, type AppLocale } from '../lib/i18n'
|
|
|
|
interface McpSetupDialogProps {
|
|
open: boolean
|
|
status: McpStatus
|
|
busyAction: 'connect' | 'disconnect' | null
|
|
manualConfigError?: string | null
|
|
manualConfigLoading?: boolean
|
|
manualConfigSnippet?: string | null
|
|
locale?: AppLocale
|
|
onClose: () => void
|
|
onConnect: () => void
|
|
onCopyManualConfig?: () => void
|
|
onDisconnect: () => void
|
|
onLoadManualConfig?: () => void
|
|
}
|
|
|
|
interface ManualMcpConfigSectionProps {
|
|
error?: string | null
|
|
loading: boolean
|
|
locale?: AppLocale
|
|
onCopy?: () => void
|
|
snippet?: string | null
|
|
}
|
|
|
|
interface McpSetupActionsProps {
|
|
buttonsDisabled: boolean
|
|
connectBusy: boolean
|
|
disconnectBusy: boolean
|
|
locale?: AppLocale
|
|
primaryLabel: string
|
|
secondaryLabel: string | null
|
|
onClose: () => void
|
|
onConnect: () => void
|
|
onDisconnect: () => void
|
|
}
|
|
|
|
function isConnected(status: McpStatus): boolean {
|
|
return status === 'installed'
|
|
}
|
|
|
|
function actionCopy(status: McpStatus, t: ReturnType<typeof createTranslator>) {
|
|
if (isConnected(status)) {
|
|
return {
|
|
description: t('mcp.setup.connectedDescription'),
|
|
primaryLabel: t('mcp.setup.reconnect'),
|
|
secondaryLabel: t('mcp.setup.disconnect'),
|
|
title: t('mcp.setup.manageTitle'),
|
|
}
|
|
}
|
|
|
|
return {
|
|
description: t('mcp.setup.setupDescription'),
|
|
primaryLabel: t('mcp.setup.connect'),
|
|
secondaryLabel: null,
|
|
title: t('mcp.setup.setupTitle'),
|
|
}
|
|
}
|
|
|
|
function manualConfigText(
|
|
{ error, loading, snippet }: ManualMcpConfigSectionProps,
|
|
t: ReturnType<typeof createTranslator>,
|
|
): string {
|
|
if (loading) return t('mcp.setup.manual.loading')
|
|
return error ?? snippet ?? t('mcp.setup.manual.unavailable')
|
|
}
|
|
|
|
function ManualMcpConfigSection(props: ManualMcpConfigSectionProps) {
|
|
const t = createTranslator(props.locale)
|
|
|
|
return (
|
|
<div className="space-y-2">
|
|
<div className="flex items-center justify-between gap-2">
|
|
<p className="m-0 text-sm font-medium text-foreground">{t('mcp.setup.manual.title')}</p>
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={props.onCopy}
|
|
disabled={!props.onCopy || props.loading}
|
|
data-testid="mcp-copy-config"
|
|
>
|
|
<Copy size={14} />
|
|
{t('mcp.setup.manual.copy')}
|
|
</Button>
|
|
</div>
|
|
<pre
|
|
tabIndex={0}
|
|
className="max-h-48 overflow-auto rounded-md border border-border bg-background px-3 py-3 font-mono text-xs leading-5 text-foreground"
|
|
data-testid="mcp-config-snippet"
|
|
>
|
|
{manualConfigText(props, t)}
|
|
</pre>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function McpSetupActions({
|
|
buttonsDisabled,
|
|
connectBusy,
|
|
disconnectBusy,
|
|
locale = 'en',
|
|
primaryLabel,
|
|
secondaryLabel,
|
|
onClose,
|
|
onConnect,
|
|
onDisconnect,
|
|
}: McpSetupActionsProps) {
|
|
const t = createTranslator(locale)
|
|
|
|
return (
|
|
<DialogFooter className="flex-row items-center justify-end gap-2 sm:justify-end">
|
|
<Button type="button" variant="outline" onClick={onClose} disabled={buttonsDisabled}>
|
|
{t('common.cancel')}
|
|
</Button>
|
|
{secondaryLabel ? (
|
|
<Button
|
|
type="button"
|
|
variant="destructive"
|
|
onClick={onDisconnect}
|
|
disabled={buttonsDisabled}
|
|
data-testid="mcp-setup-disconnect"
|
|
>
|
|
{disconnectBusy ? t('mcp.setup.disconnecting') : secondaryLabel}
|
|
</Button>
|
|
) : null}
|
|
<Button
|
|
type="button"
|
|
autoFocus
|
|
onClick={onConnect}
|
|
disabled={buttonsDisabled}
|
|
data-testid="mcp-setup-connect"
|
|
>
|
|
{connectBusy ? t('mcp.setup.connecting') : primaryLabel}
|
|
</Button>
|
|
</DialogFooter>
|
|
)
|
|
}
|
|
|
|
export function McpSetupDialog({
|
|
open,
|
|
status,
|
|
busyAction,
|
|
manualConfigError,
|
|
manualConfigLoading = false,
|
|
manualConfigSnippet,
|
|
locale = 'en',
|
|
onClose,
|
|
onConnect,
|
|
onCopyManualConfig,
|
|
onDisconnect,
|
|
onLoadManualConfig,
|
|
}: McpSetupDialogProps) {
|
|
const t = createTranslator(locale)
|
|
const copy = actionCopy(status, t)
|
|
const connectBusy = busyAction === 'connect'
|
|
const disconnectBusy = busyAction === 'disconnect'
|
|
const buttonsDisabled = busyAction !== null || status === 'checking'
|
|
|
|
useEffect(() => {
|
|
if (open) onLoadManualConfig?.()
|
|
}, [open, onLoadManualConfig])
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={(next) => { if (!next) onClose() }}>
|
|
<DialogContent showCloseButton={false} className="sm:max-w-[520px]" data-testid="mcp-setup-dialog">
|
|
<DialogHeader>
|
|
<DialogTitle className="flex items-center gap-2">
|
|
<ShieldCheck size={18} />
|
|
{copy.title}
|
|
</DialogTitle>
|
|
<DialogDescription>{copy.description}</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div className="space-y-3 text-sm leading-6 text-muted-foreground">
|
|
<p>
|
|
{t('mcp.setup.nodeRequirement')}
|
|
</p>
|
|
<p>
|
|
{t('mcp.setup.writeEntryDescription', { entry: 'tolaria' })}
|
|
</p>
|
|
<div className="rounded-md border border-border bg-muted/30 px-3 py-3 font-mono text-xs text-foreground">
|
|
<div>~/.claude.json</div>
|
|
<div>~/.claude/mcp.json</div>
|
|
<div>~/.gemini/settings.json</div>
|
|
<div>~/.cursor/mcp.json</div>
|
|
<div>~/.config/mcp/mcp.json</div>
|
|
</div>
|
|
<ManualMcpConfigSection
|
|
error={manualConfigError}
|
|
loading={manualConfigLoading}
|
|
locale={locale}
|
|
onCopy={onCopyManualConfig}
|
|
snippet={manualConfigSnippet}
|
|
/>
|
|
<p>
|
|
{t('mcp.setup.clientPathsDescription')}
|
|
</p>
|
|
<p>
|
|
{t('mcp.setup.geminiGuidanceDescription')}
|
|
</p>
|
|
</div>
|
|
|
|
<McpSetupActions
|
|
buttonsDisabled={buttonsDisabled}
|
|
connectBusy={connectBusy}
|
|
disconnectBusy={disconnectBusy}
|
|
locale={locale}
|
|
primaryLabel={copy.primaryLabel}
|
|
secondaryLabel={copy.secondaryLabel}
|
|
onClose={onClose}
|
|
onConnect={onConnect}
|
|
onDisconnect={onDisconnect}
|
|
/>
|
|
</DialogContent>
|
|
</Dialog>
|
|
)
|
|
}
|