Add shared SDC app shell (SdcTopBar + ApiSettings + unified credentials)
Some checks failed
Publish package to GitHub Packages / publish (push) Has been cancelled
Some checks failed
Publish package to GitHub Packages / publish (push) Has been cancelled
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>
This commit is contained in:
146
src/components/organisms/ApiSettings/ApiSettings.tsx
Normal file
146
src/components/organisms/ApiSettings/ApiSettings.tsx
Normal file
@@ -0,0 +1,146 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user