resource settings page
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
import { useState } from 'react';
|
||||
import { Copy, Check, Server, Wrench } from 'lucide-react';
|
||||
import { Copy, Check, Server, Wrench, Loader2 } from 'lucide-react';
|
||||
import { useGlobal } from 'hooks/useGlobal';
|
||||
import { useResources, type Resource } from '@/state/useResources';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { useResources, getResourceCategory, type Resource, type PingResult } from '@/state/useResources';
|
||||
|
||||
const CopyCommand = ({ command }: { command: string }) => {
|
||||
const [copied, setCopied] = useState(false);
|
||||
@@ -33,6 +35,140 @@ const CommandRow = ({ label, command }: { label: string; command: string }) => (
|
||||
</div>
|
||||
);
|
||||
|
||||
type ConnectionSectionProps = {
|
||||
resource: Resource;
|
||||
};
|
||||
|
||||
const ConnectionSection = ({ resource }: ConnectionSectionProps) => {
|
||||
const { saveConnectionConfig, pingResource } = useResources();
|
||||
const [url, setUrl] = useState(resource.connectionConfig?.url ?? '');
|
||||
const [apiKey, setApiKey] = useState(resource.connectionConfig?.credentials?.apiKey ?? '');
|
||||
const [username, setUsername] = useState(resource.connectionConfig?.credentials?.username ?? '');
|
||||
const [password, setPassword] = useState(resource.connectionConfig?.credentials?.password ?? '');
|
||||
const [pinging, setPinging] = useState(false);
|
||||
const [pingResult, setPingResult] = useState<PingResult | null>(null);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
const handlePing = async () => {
|
||||
setPinging(true);
|
||||
setPingResult(null);
|
||||
try {
|
||||
const result = await pingResource(resource.id, url);
|
||||
setPingResult(result);
|
||||
} catch {
|
||||
setPingResult({ reachable: false, latencyMs: null });
|
||||
} finally {
|
||||
setPinging(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
setSaving(true);
|
||||
try {
|
||||
const credentials =
|
||||
apiKey || username || password
|
||||
? { apiKey: apiKey || undefined, username: username || undefined, password: password || undefined }
|
||||
: undefined;
|
||||
await saveConnectionConfig(resource.id, { url, credentials });
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const hasCredentials = !!(
|
||||
resource.connectionConfig?.credentials?.apiKey || resource.connectionConfig?.credentials?.username
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="mb-6">
|
||||
<h3 className="text-sm font-semibold text-duck-dark mb-3">Connection</h3>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div>
|
||||
<label className="text-xs text-duck-dark/50 mb-1 block">Base URL</label>
|
||||
<Input
|
||||
value={url}
|
||||
onChange={(ev) => setUrl(ev.target.value)}
|
||||
placeholder="http://127.0.0.1:64202"
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
</div>
|
||||
{(hasCredentials || apiKey) && (
|
||||
<div>
|
||||
<label className="text-xs text-duck-dark/50 mb-1 block">API Key</label>
|
||||
<Input
|
||||
value={apiKey}
|
||||
onChange={(ev) => setApiKey(ev.target.value)}
|
||||
placeholder="Optional"
|
||||
type="password"
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{(hasCredentials || username || password) && (
|
||||
<div className="flex gap-2">
|
||||
<div className="flex-1">
|
||||
<label className="text-xs text-duck-dark/50 mb-1 block">Username</label>
|
||||
<Input
|
||||
value={username}
|
||||
onChange={(ev) => setUsername(ev.target.value)}
|
||||
placeholder="Optional"
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<label className="text-xs text-duck-dark/50 mb-1 block">Password</label>
|
||||
<Input
|
||||
value={password}
|
||||
onChange={(ev) => setPassword(ev.target.value)}
|
||||
placeholder="Optional"
|
||||
type="password"
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" size="sm" onClick={handlePing} disabled={pinging || !url} className="text-xs">
|
||||
{pinging && <Loader2 className="h-3 w-3 animate-spin mr-1" />}
|
||||
Test Connection
|
||||
</Button>
|
||||
<Button size="sm" onClick={handleSave} disabled={saving || !url} className="text-xs">
|
||||
{saving && <Loader2 className="h-3 w-3 animate-spin mr-1" />}
|
||||
Save
|
||||
</Button>
|
||||
{pingResult && (
|
||||
<span className={`text-xs ${pingResult.reachable ? 'text-green-600' : 'text-red-500'}`}>
|
||||
{pingResult.reachable ? `Reachable (${pingResult.latencyMs}ms)` : 'Unreachable'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const LocalAvailabilitySection = ({ resource }: { resource: Resource }) => (
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-duck-dark mb-3">Local Availability</h3>
|
||||
<div className="mb-3">
|
||||
{resource.installed ? (
|
||||
<span className="text-xs bg-green-100 text-green-700 rounded-full px-2 py-0.5">
|
||||
Installed{resource.version ? ` (${resource.version})` : ''}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-xs bg-duck-dark/5 text-duck-dark/40 rounded-full px-2 py-0.5">Not installed</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
{resource.installCommand && <CommandRow label="Install" command={resource.installCommand} />}
|
||||
{resource.uninstallCommand && <CommandRow label="Uninstall" command={resource.uninstallCommand} />}
|
||||
{resource.manageCommand && <CommandRow label="Manage" command={resource.manageCommand} />}
|
||||
{resource.verifyCommand && <CommandRow label="Verify" command={resource.verifyCommand} />}
|
||||
{resource.updateCommand && <CommandRow label="Update" command={resource.updateCommand} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const Resources = () => {
|
||||
const { resources } = useResources();
|
||||
const [selectedId] = useGlobal<string | null>('RESOURCE_SELECTED', null);
|
||||
@@ -47,6 +183,8 @@ export const Resources = () => {
|
||||
);
|
||||
}
|
||||
|
||||
const category = getResourceCategory(resource);
|
||||
|
||||
return (
|
||||
<div className="h-full overflow-y-auto p-6">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
@@ -61,27 +199,20 @@ export const Resources = () => {
|
||||
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<span className="text-xs bg-duck-dark/5 text-duck-dark/50 rounded-full px-2 py-0.5">{resource.type}</span>
|
||||
<span
|
||||
className={`text-xs rounded-full px-2 py-0.5 ${category === 'api-based' ? 'bg-duck-teal/10 text-duck-teal' : 'bg-duck-dark/5 text-duck-dark/50'}`}
|
||||
>
|
||||
{category === 'api-based' ? 'API Based' : 'Local CLI'}
|
||||
</span>
|
||||
{resource.port && (
|
||||
<span className="text-xs bg-duck-teal/10 text-duck-teal rounded-full px-2 py-0.5">:{resource.port}</span>
|
||||
)}
|
||||
{resource.installed ? (
|
||||
<span className="text-xs bg-green-100 text-green-700 rounded-full px-2 py-0.5">
|
||||
{resource.version ?? 'Running'}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-xs bg-duck-dark/5 text-duck-dark/40 rounded-full px-2 py-0.5">Not installed</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-duck-dark/70 mb-6">{resource.description}</p>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
{resource.installCommand && <CommandRow label="Install" command={resource.installCommand} />}
|
||||
{resource.uninstallCommand && <CommandRow label="Uninstall" command={resource.uninstallCommand} />}
|
||||
{resource.manageCommand && <CommandRow label="Manage" command={resource.manageCommand} />}
|
||||
{resource.verifyCommand && <CommandRow label="Verify" command={resource.verifyCommand} />}
|
||||
{resource.updateCommand && <CommandRow label="Update" command={resource.updateCommand} />}
|
||||
</div>
|
||||
{category === 'api-based' && <ConnectionSection key={resource.id} resource={resource} />}
|
||||
<LocalAvailabilitySection resource={resource} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user