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 };
};
+43 -7
View File
@@ -198,20 +198,22 @@ function buildConnectionConfig(
return { url: `http://127.0.0.1:${port ?? resource.port}` };
}
async function loadResources(): Promise<Resource[]> {
async function parseResources() {
const dir = getResourcesDir();
if (!existsSync(dir)) return [];
const [files, config] = await Promise.all([readdir(dir), readConfig()]);
const files = await readdir(dir);
const serviceFiles = files.filter((f) => f.startsWith('SERVICE_') && f.endsWith('.md'));
const parsed = await Promise.all(
return Promise.all(
serviceFiles.map(async (filename) => {
const content = await Bun.file(`${dir}/${filename}`).text();
return parseResourceFile(filename, content);
}),
);
}
const resources = await Promise.all(
async function loadResources(): Promise<Resource[]> {
const [parsed, config] = await Promise.all([parseResources(), readConfig()]);
return Promise.all(
parsed.map(async (r) => {
const configUrl = config[r.id]?.url;
const status = await checkResourceStatus({ resource: r, configUrl });
@@ -219,8 +221,6 @@ async function loadResources(): Promise<Resource[]> {
return { ...r, ...status, connectionConfig };
}),
);
return resources;
}
export const resourcesRouter = createRouter();
@@ -290,6 +290,42 @@ resourcesRouter.post('/error-log', async (ctx) => {
return ctx.json({ filePath });
});
resourcesRouter.post('/:id/run', async (ctx) => {
const id = ctx.req.param('id');
const { action } = await ctx.req.json<{ action: string }>();
const parsed = await parseResources();
const resource = parsed.find((r) => r.id === id);
if (!resource) return ctx.json({ error: 'Resource not found' }, 404);
const commands: Record<string, string | null> = {
install: resource.installCommand,
uninstall: resource.uninstallCommand,
verify: resource.verifyCommand,
update: resource.updateCommand,
manage: resource.manageCommand,
};
const command = commands[action];
if (!command) return ctx.json({ error: `No ${action} command for this resource` }, 400);
if (command.trimStart().startsWith('sudo')) {
return ctx.json({ error: 'Sudo commands must run in terminal' }, 400);
}
try {
const proc = Bun.spawn(['sh', '-c', command], { stdout: 'pipe', stderr: 'pipe' });
const [stdout, stderr] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
]);
await proc.exited;
return ctx.json({ exitCode: proc.exitCode, output: (stdout + stderr).trim() });
} catch {
return ctx.json({ exitCode: 1, output: 'Failed to execute command' });
}
});
resourcesRouter.get('/:id', async (ctx) => {
const resources = await loadResources();
const resource = resources.find((r) => r.id === ctx.req.param('id'));