Files FIles Files
This commit is contained in:
@@ -1,39 +1,22 @@
|
||||
import { useState } from 'react';
|
||||
import { Copy, Check, Server, Wrench, Loader2 } from 'lucide-react';
|
||||
import { Server, Wrench, Loader2 } from 'lucide-react';
|
||||
import { useGlobal } from 'hooks/useGlobal';
|
||||
import { usePanelChannel } from 'hooks/usePanelChannel';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import { CommandBlock } from '@/components/CommandBlock';
|
||||
import { useResources, getResourceCategory, type Resource, type PingResult } from '@/state/useResources';
|
||||
|
||||
const CopyCommand = ({ command }: { command: string }) => {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const copy = () => {
|
||||
navigator.clipboard.writeText(command);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 1500);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-1 mt-1">
|
||||
<code className="flex-1 bg-duck-dark/5 rounded px-2 py-1 text-xs text-duck-dark/70">{command}</code>
|
||||
<button
|
||||
type="button"
|
||||
onClick={copy}
|
||||
className="shrink-0 p-1 rounded hover:bg-duck-dark/10 cursor-pointer transition-colors"
|
||||
>
|
||||
{copied ? <Check className="h-3.5 w-3.5 text-green-600" /> : <Copy className="h-3.5 w-3.5 text-duck-dark/50" />}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const CommandRow = ({ label, command }: { label: string; command: string }) => (
|
||||
<div className="text-xs text-duck-dark/50">
|
||||
{label}:
|
||||
<CopyCommand command={command} />
|
||||
</div>
|
||||
);
|
||||
import { RUN_COMMAND_CHANNEL, type RunCommandState } from './run-command-channel';
|
||||
|
||||
type ConnectionSectionProps = {
|
||||
resource: Resource;
|
||||
@@ -147,7 +130,12 @@ const ConnectionSection = ({ resource }: ConnectionSectionProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
const LocalAvailabilitySection = ({ resource }: { resource: Resource }) => (
|
||||
type LocalAvailabilitySectionProps = {
|
||||
resource: Resource;
|
||||
onRun: (command: string) => void;
|
||||
};
|
||||
|
||||
const LocalAvailabilitySection = ({ resource, onRun }: LocalAvailabilitySectionProps) => (
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-duck-dark mb-3">Local Availability</h3>
|
||||
<div className="mb-3">
|
||||
@@ -159,60 +147,203 @@ const LocalAvailabilitySection = ({ resource }: { resource: Resource }) => (
|
||||
<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 className="flex flex-col gap-3">
|
||||
{resource.installCommand && <CommandBlock label="Install" command={resource.installCommand} onRun={onRun} />}
|
||||
{resource.uninstallCommand && (
|
||||
<CommandBlock label="Uninstall" command={resource.uninstallCommand} onRun={onRun} />
|
||||
)}
|
||||
{resource.manageCommand && <CommandBlock label="Manage" command={resource.manageCommand} onRun={onRun} />}
|
||||
{resource.verifyCommand && <CommandBlock label="Verify" command={resource.verifyCommand} onRun={onRun} />}
|
||||
{resource.updateCommand && <CommandBlock label="Update" command={resource.updateCommand} onRun={onRun} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const Resources = () => {
|
||||
const { resources } = useResources();
|
||||
const [selectedId] = useGlobal<string | null>('RESOURCE_SELECTED', null);
|
||||
type CatalogCardProps = {
|
||||
resource: Resource;
|
||||
onSelect: (id: string) => void;
|
||||
};
|
||||
|
||||
const resource = resources?.find((r: Resource) => r.id === selectedId);
|
||||
|
||||
if (!resource) {
|
||||
return (
|
||||
<div className="h-full flex items-center justify-center">
|
||||
<p className="text-sm text-duck-dark/30">Select a resource to view details</p>
|
||||
const CatalogCard = ({ resource: r, onSelect }: CatalogCardProps) => {
|
||||
const category = getResourceCategory(r);
|
||||
return (
|
||||
<button
|
||||
onClick={() => onSelect(r.id)}
|
||||
className="flex flex-col gap-2 p-4 rounded-lg border border-duck-dark/10 text-left cursor-pointer transition-colors hover:border-duck-teal/30 hover:bg-duck-teal/5"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{r.port ? (
|
||||
<Server className="h-3.5 w-3.5 text-duck-teal shrink-0" />
|
||||
) : (
|
||||
<Wrench className="h-3.5 w-3.5 text-duck-dark/40 shrink-0" />
|
||||
)}
|
||||
<span className="text-sm font-medium text-duck-dark truncate">{r.name}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
<p className="text-xs text-duck-dark/40 line-clamp-1">{r.subtitle}</p>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-[10px] bg-duck-dark/5 text-duck-dark/40 rounded-full px-1.5 py-0.5">{r.type}</span>
|
||||
<span
|
||||
className={`text-[10px] rounded-full px-1.5 py-0.5 ${category === 'api-based' ? 'bg-duck-teal/10 text-duck-teal' : 'bg-duck-dark/5 text-duck-dark/40'}`}
|
||||
>
|
||||
{category === 'api-based' ? 'API' : 'CLI'}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
const category = getResourceCategory(resource);
|
||||
const ResourceCatalog = () => {
|
||||
const { resources, isLoading } = useResources();
|
||||
const [, setSelectedId] = useGlobal<string | null>('RESOURCE_SELECTED', null);
|
||||
const [, setShowCatalog] = useGlobal<boolean>('RESOURCE_CATALOG', false);
|
||||
const [search, setSearch] = useState('');
|
||||
|
||||
const query = search.toLowerCase();
|
||||
const filtered =
|
||||
resources?.filter(
|
||||
(r: Resource) =>
|
||||
!r.installed &&
|
||||
(r.name.toLowerCase().includes(query) ||
|
||||
r.subtitle.toLowerCase().includes(query) ||
|
||||
r.description.toLowerCase().includes(query)),
|
||||
) ?? [];
|
||||
|
||||
const apiBased = filtered.filter((r: Resource) => getResourceCategory(r) === 'api-based');
|
||||
const localCli = filtered.filter((r: Resource) => getResourceCategory(r) === 'local-cli');
|
||||
|
||||
const handleSelect = (id: string) => {
|
||||
setShowCatalog(false);
|
||||
setSelectedId(id);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full overflow-y-auto p-6">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
{resource.port ? (
|
||||
<Server className="h-4 w-4 text-duck-teal shrink-0" />
|
||||
) : (
|
||||
<Wrench className="h-4 w-4 text-duck-dark/40 shrink-0" />
|
||||
)}
|
||||
<h2 className="text-lg font-bold text-duck-dark">{resource.name}</h2>
|
||||
<span className="text-sm text-duck-dark/40">{resource.subtitle}</span>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-duck-dark/70 mb-6">{resource.description}</p>
|
||||
|
||||
{category === 'api-based' && <ConnectionSection key={resource.id} resource={resource} />}
|
||||
<LocalAvailabilitySection resource={resource} />
|
||||
<h2 className="text-lg font-bold text-duck-dark mb-1">Resource Catalog</h2>
|
||||
<p className="text-sm text-duck-dark/40 mb-4">All available resources. Select one to configure.</p>
|
||||
<Input
|
||||
placeholder="Search resources..."
|
||||
value={search}
|
||||
onChange={(ev) => setSearch(ev.target.value)}
|
||||
className="h-8 text-xs mb-4 max-w-xs"
|
||||
/>
|
||||
{isLoading && <p className="text-xs text-duck-dark/50">Loading...</p>}
|
||||
{apiBased.length > 0 && (
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Server className="h-3.5 w-3.5 text-duck-teal" />
|
||||
<span className="text-xs font-semibold uppercase tracking-wider text-duck-dark/40">API Based</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 xl:grid-cols-3 gap-3">
|
||||
{apiBased.map((r: Resource) => (
|
||||
<CatalogCard key={r.id} resource={r} onSelect={handleSelect} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{localCli.length > 0 && (
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Wrench className="h-3.5 w-3.5 text-duck-dark/40" />
|
||||
<span className="text-xs font-semibold uppercase tracking-wider text-duck-dark/40">Local CLI</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 xl:grid-cols-3 gap-3">
|
||||
{localCli.map((r: Resource) => (
|
||||
<CatalogCard key={r.id} resource={r} onSelect={handleSelect} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ResourceDetail = ({ resource }: { resource: Resource }) => {
|
||||
const category = getResourceCategory(resource);
|
||||
const [, setRunCommand] = usePanelChannel<RunCommandState>(RUN_COMMAND_CHANNEL, null);
|
||||
const [confirmCommand, setConfirmCommand] = useState<string | null>(null);
|
||||
|
||||
const handleRun = (command: string) => {
|
||||
const isSudo = command.trimStart().startsWith('sudo');
|
||||
if (isSudo) {
|
||||
setConfirmCommand(command);
|
||||
} else {
|
||||
setRunCommand({ command });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="h-full overflow-y-auto p-6">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
{resource.port ? (
|
||||
<Server className="h-4 w-4 text-duck-teal shrink-0" />
|
||||
) : (
|
||||
<Wrench className="h-4 w-4 text-duck-dark/40 shrink-0" />
|
||||
)}
|
||||
<h2 className="text-lg font-bold text-duck-dark">{resource.name}</h2>
|
||||
<span className="text-sm text-duck-dark/40">{resource.subtitle}</span>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-duck-dark/70 mb-6">{resource.description}</p>
|
||||
|
||||
{category === 'api-based' && <ConnectionSection key={resource.id} resource={resource} />}
|
||||
<LocalAvailabilitySection resource={resource} onRun={handleRun} />
|
||||
</div>
|
||||
|
||||
<AlertDialog open={!!confirmCommand} onOpenChange={(open) => !open && setConfirmCommand(null)}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Run with elevated privileges</AlertDialogTitle>
|
||||
<AlertDialogDescription asChild>
|
||||
<div>
|
||||
You are about to run a command with elevated privileges (sudo)
|
||||
<br />
|
||||
<span className="text-red-500">
|
||||
in the host machine.
|
||||
<br />
|
||||
ARE YOU SURE?
|
||||
</span>
|
||||
</div>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<code className="text-sm font-mono bg-[#1a1a2e] text-[#e0e0e0] rounded-lg px-3 py-2 break-all">{confirmCommand}</code>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel className="cursor-pointer">Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
className="cursor-pointer bg-duck-yellow hover:bg-duck-yellow/90 text-duck-teal font-bold"
|
||||
onClick={() => {
|
||||
if (confirmCommand) setRunCommand({ command: confirmCommand });
|
||||
setConfirmCommand(null);
|
||||
}}
|
||||
>
|
||||
Run
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const Resources = () => {
|
||||
const { resources } = useResources();
|
||||
const [selectedId] = useGlobal<string | null>('RESOURCE_SELECTED', null);
|
||||
const [showCatalog] = useGlobal<boolean>('RESOURCE_CATALOG', false);
|
||||
|
||||
const resource = selectedId ? resources?.find((r: Resource) => r.id === selectedId) : null;
|
||||
|
||||
if (showCatalog || !resource) return <ResourceCatalog />;
|
||||
return <ResourceDetail resource={resource} />;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user