Resources tuneup

This commit is contained in:
2026-02-20 12:35:47 +00:00
parent 8a637c7788
commit f48093424c
5 changed files with 2104 additions and 40 deletions
@@ -1,5 +1,6 @@
import { useState } from 'react';
import { Server, Wrench, Loader2 } from 'lucide-react';
import { Server, Wrench, Loader2, RefreshCw, Trash2 } from 'lucide-react';
import { toast } from 'sonner';
import { useGlobal } from 'hooks/useGlobal';
import { usePanelChannel } from 'hooks/usePanelChannel';
import { Button } from '@/components/ui/button';
@@ -14,7 +15,6 @@ import {
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog';
import { CommandBlock } from '@/components/CommandBlock';
import { useResources, getResourceCategory, type Resource, type PingResult } from '@/state/useResources';
import { RUN_COMMAND_CHANNEL, type RunCommandState } from './run-command-channel';
@@ -135,29 +135,133 @@ type LocalAvailabilitySectionProps = {
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">
type ResourceAction = 'install' | 'uninstall' | 'verify' | 'update' | 'manage';
const LocalAvailabilitySection = ({ resource, onRun }: LocalAvailabilitySectionProps) => {
const { runCommand } = useResources();
const [runningAction, setRunningAction] = useState<ResourceAction | null>(null);
const isSudo = (cmd: string) => cmd.trimStart().startsWith('sudo');
const handleAction = async (action: ResourceAction, command: string) => {
if (isSudo(command)) {
onRun(command);
return;
}
setRunningAction(action);
try {
const result = await runCommand(resource.id, action);
if (result.exitCode === 0) {
toast.success('Command completed successfully');
} else {
toast.error(result.output || `Command failed (exit code ${result.exitCode})`, { duration: 8000 });
}
} catch {
toast.error('Failed to run command');
} finally {
setRunningAction(null);
}
};
return (
<div className="flex flex-col gap-2">
{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>
<>
<div>
<span className="text-xs bg-green-100 text-green-700 rounded-full px-2 py-0.5">Installed</span>
</div>
{resource.version && (
<div className="flex items-center gap-2">
{resource.updateCommand && (
<Button
variant="outline"
size="sm"
className="h-6 text-xs cursor-pointer"
onClick={() => handleAction('update', resource.updateCommand!)}
disabled={!!runningAction}
>
<RefreshCw className={`h-3 w-3 mr-1 ${runningAction === 'update' ? 'animate-spin' : ''}`} />
Update
</Button>
)}
<span className="text-xs text-duck-dark/50 dark:text-foreground/50">{resource.version}</span>
</div>
)}
<div className="flex items-center gap-1.5">
{!resource.version && resource.updateCommand && (
<Button
variant="outline"
size="sm"
className="h-6 text-xs cursor-pointer"
onClick={() => handleAction('update', resource.updateCommand!)}
disabled={!!runningAction}
>
<RefreshCw className={`h-3 w-3 mr-1 ${runningAction === 'update' ? 'animate-spin' : ''}`} />
Update
</Button>
)}
{resource.verifyCommand && (
<Button
variant="outline"
size="sm"
className="h-6 text-xs cursor-pointer"
onClick={() => handleAction('verify', resource.verifyCommand!)}
disabled={!!runningAction}
>
{runningAction === 'verify' && <Loader2 className="h-3 w-3 mr-1 animate-spin" />}
Verify
</Button>
)}
{resource.manageCommand && (
<Button
variant="outline"
size="sm"
className="h-6 text-xs cursor-pointer"
onClick={() => handleAction('manage', resource.manageCommand!)}
disabled={!!runningAction}
>
{runningAction === 'manage' && <Loader2 className="h-3 w-3 mr-1 animate-spin" />}
Manage
</Button>
)}
</div>
{resource.uninstallCommand && (
<div className="flex items-center mt-1.5">
<Button
variant="outline"
size="sm"
className="h-6 text-xs cursor-pointer text-red-500 hover:text-red-600 border-red-500/30 hover:border-red-500/50 hover:bg-red-500/5"
onClick={() => handleAction('uninstall', resource.uninstallCommand!)}
disabled={!!runningAction}
>
{runningAction === 'uninstall' ? (
<Loader2 className="h-3 w-3 mr-1 animate-spin" />
) : (
<Trash2 className="h-3 w-3 mr-1" />
)}
Uninstall
</Button>
</div>
)}
</>
) : (
<span className="text-xs bg-duck-dark/5 text-duck-dark/40 rounded-full px-2 py-0.5">Not installed</span>
<div className="flex items-center gap-3">
<span className="text-xs bg-duck-dark/5 text-duck-dark/40 rounded-full px-2 py-0.5">Not installed</span>
{resource.installCommand && (
<Button
size="sm"
className="h-6 text-xs bg-duck-teal text-duck-yellow hover:bg-duck-teal/90 cursor-pointer"
onClick={() => handleAction('install', resource.installCommand!)}
disabled={runningAction === 'install'}
>
{runningAction === 'install' ? 'Installing...' : 'Install'}
</Button>
)}
</div>
)}
</div>
<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>
);
);
};
type CatalogCardProps = {
resource: Resource;
@@ -306,18 +410,12 @@ const ResourceDetail = ({ resource }: { resource: Resource }) => {
<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>
<p className="text-sm text-duck-dark/70 dark:text-foreground/70">
For this operation the script must be run with elevated privileges (sudo) on the host machine.
<br />
Not to worry, though, we wrote it and battle tested it ourselves.
</p>
<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>
+7 -1
View File
@@ -58,5 +58,11 @@ export const useResources = () => {
return client.post<PingResult>(`/server-settings/resources/${id}/ping`, { url });
};
return { resources, isLoading, saveConnectionConfig, pingResource };
const runCommand = async (id: string, action: string) => {
const result = await client.post<{ exitCode: number; output: string }>(`/server-settings/resources/${id}/run`, { action });
queryClient.invalidateQueries({ queryKey: RESOURCES_KEY });
return result;
};
return { resources, isLoading, saveConnectionConfig, pingResource, runCommand };
};