Files
ADS3-Design-System/src/components/organisms/ApiSettings/ApiSettings.tsx
Richie ce1efd1c13
Some checks failed
Publish package to GitHub Packages / publish (push) Has been cancelled
Add shared SDC app shell (SdcTopBar + ApiSettings + unified credentials)
Workstream D: a shared top bar for the SDC tool suite — app name (left), API
settings cog, and the suite app directory (grid) — composed on the existing
TopBar. Adds an ApiSettings dialog and sdc_api_key/sdc_api_endpoint credential
helpers (shared once across all tools, with legacy-key migration). lucide-react
becomes a peer dependency. Bump to 0.2.0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 08:44:00 +10:00

147 lines
4.4 KiB
TypeScript

import { useState, useEffect } from 'react'
import {
Dialog,
DialogHeader,
DialogTitle,
DialogDescription,
DialogContent,
DialogFooter,
} from '@/components/molecules/Dialog'
import { Button } from '@/components/atoms/Button'
import { Input } from '@/components/atoms/Input'
import { Alert } from '@/components/molecules/Alert'
import { Badge } from '@/components/atoms/Badge'
import {
getEndpoint,
getApiKey,
saveCredentials,
testConnection,
} from '@/lib/credentials'
export interface ApiSettingsProps {
open: boolean
onClose: () => void
/** Called after credentials are saved (e.g. to refresh a "no API key" badge). */
onSaved?: () => void
}
/**
* Shared API-settings dialog for the SDC tool suite. Reads/writes the suite-wide
* `sdc_api_endpoint` + `sdc_api_key` so the key is entered once across all tools.
*/
export function ApiSettings({ open, onClose, onSaved }: ApiSettingsProps) {
const [endpoint, setEndpoint] = useState('')
const [apiKey, setApiKey] = useState('')
const [status, setStatus] = useState<{ type: 'success' | 'error' | 'info'; message: string } | null>(null)
const [testing, setTesting] = useState(false)
const [models, setModels] = useState<string[]>([])
useEffect(() => {
if (open) {
setEndpoint(getEndpoint())
setApiKey(getApiKey())
setStatus(null)
setModels([])
}
}, [open])
function handleSave() {
if (!endpoint.trim()) {
setStatus({ type: 'error', message: 'Please enter a gateway endpoint.' })
return
}
if (!apiKey.trim()) {
setStatus({ type: 'error', message: 'Please enter an API key.' })
return
}
saveCredentials(endpoint, apiKey)
onSaved?.()
setStatus({ type: 'success', message: 'Credentials saved. They apply across all SDC tools.' })
}
async function handleTest() {
if (!endpoint.trim() || !apiKey.trim()) {
setStatus({ type: 'error', message: 'Enter an endpoint and key first.' })
return
}
saveCredentials(endpoint, apiKey)
onSaved?.()
setTesting(true)
setStatus({ type: 'info', message: 'Testing connection…' })
setModels([])
try {
const result = await testConnection(endpoint, apiKey)
setModels(result.models)
setStatus({
type: 'success',
message: 'Connected. ' + result.models.length + ' model(s) available.',
})
} catch (e) {
setStatus({ type: 'error', message: (e as Error).message })
} finally {
setTesting(false)
}
}
return (
<Dialog open={open} onClose={onClose} size="lg">
<DialogHeader onClose={onClose}>
<DialogTitle>API Settings</DialogTitle>
<DialogDescription>
Configure your gateway connection. This is shared across all SDC tools.
</DialogDescription>
</DialogHeader>
<DialogContent>
<div className="space-y-4">
<Input
variant="stacked"
label="Gateway Endpoint"
description="The base URL of your LiteLLM gateway"
type="url"
placeholder="https://your-gateway.example.com"
value={endpoint}
onChange={(e) => setEndpoint(e.target.value)}
/>
<Input
variant="stacked"
label="API Key"
type="password"
placeholder="Paste your API key here"
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
/>
{status && (
<Alert
variant={status.type === 'success' ? 'success' : status.type === 'error' ? 'error' : 'info'}
title={status.type === 'success' ? 'Success' : status.type === 'error' ? 'Error' : ''}
onClose={() => setStatus(null)}
>
{status.message}
</Alert>
)}
{models.length > 0 && (
<div className="space-y-2">
<p className="text-small font-semibold text-text-secondary">Available Models</p>
<div className="flex flex-wrap gap-1.5">
{models.map((m) => (
<Badge key={m} variant="info-light">{m}</Badge>
))}
</div>
</div>
)}
</div>
</DialogContent>
<DialogFooter>
<Button variant="secondary" onClick={handleTest} loading={testing}>
Test Connection
</Button>
<Button variant="primary" onClick={handleSave}>
Save
</Button>
</DialogFooter>
</Dialog>
)
}