resources

This commit is contained in:
pastilhas
2026-02-18 19:57:46 +00:00
parent 485ae4c3d7
commit 8b97f8a38a
8 changed files with 341 additions and 15 deletions
@@ -1,28 +1,59 @@
import { Box, AppWindow } from 'lucide-react';
const sidebarItems = [
{ id: 'applications', label: 'Applications', icon: AppWindow },
] as const;
import { useState } from 'react';
import { Box, Circle, Server, Wrench } from 'lucide-react';
import { useGlobal } from 'hooks/useGlobal';
import { Input } from '@/components/ui/input';
import { useResources, type Resource } from '@/state/useResources';
export const ResourceSidebar = () => {
const { resources, isLoading } = useResources();
const [selectedId, setSelectedId] = useGlobal<string | null>('RESOURCE_SELECTED', null);
const [search, setSearch] = useState('');
const query = search.toLowerCase();
const filtered = resources?.filter(
(r: Resource) =>
r.name.toLowerCase().includes(query) ||
r.subtitle.toLowerCase().includes(query) ||
r.description.toLowerCase().includes(query),
);
return (
<div className="flex flex-col h-full overflow-y-auto">
<div className="p-3 pb-0">
<div className="p-3 pb-2">
<div className="flex items-center gap-2.5 rounded-lg px-3 py-2 text-sm font-medium bg-duck-teal/15 text-duck-teal">
<Box className="h-4 w-4" />
Resources
</div>
</div>
<div className="flex flex-col gap-0.5 px-3 pt-3">
{sidebarItems.map((item) => {
const Icon = item.icon;
<div className="px-3 pb-2">
<Input placeholder="Search..." value={search} onChange={(ev) => setSearch(ev.target.value)} className="h-8 text-xs" />
</div>
<div className="flex flex-col gap-0.5 px-3 overflow-y-auto flex-1">
{isLoading && <p className="text-xs text-duck-dark/50 px-3 py-2">Loading...</p>}
{filtered?.map((r: Resource) => {
const isActive = selectedId === r.id;
return (
<button
key={item.id}
className="flex items-center gap-2.5 py-2 px-3 rounded-lg text-sm font-medium bg-duck-teal/10 text-duck-dark cursor-default"
key={r.id}
onClick={() => setSelectedId(r.id)}
className={`flex items-start gap-2.5 py-2 px-3 rounded-lg text-left cursor-pointer transition-colors ${
isActive ? 'bg-duck-teal/10 text-duck-dark' : 'text-duck-dark/70 hover:bg-duck-dark/5 hover:text-duck-dark'
}`}
>
<Icon className="h-4 w-4 shrink-0" />
<span className="flex-1 text-left">{item.label}</span>
{r.port ? (
<Server className="h-3.5 w-3.5 text-duck-teal shrink-0 mt-0.5" />
) : (
<Wrench className="h-3.5 w-3.5 text-duck-dark/40 shrink-0 mt-0.5" />
)}
<div className="min-w-0 flex-1">
<div className="text-sm font-medium truncate">{r.name}</div>
<div className="text-xs text-duck-dark/40 truncate">{r.subtitle}</div>
</div>
<Circle
className={`h-2 w-2 shrink-0 mt-1.5 ${
r.installed ? 'fill-green-500 text-green-500' : 'fill-duck-dark/20 text-duck-dark/20'
}`}
/>
</button>
);
})}
@@ -0,0 +1,87 @@
import { useState } from 'react';
import { Copy, Check, Server, Wrench } from 'lucide-react';
import { useGlobal } from 'hooks/useGlobal';
import { useResources, type Resource } 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>
);
export const Resources = () => {
const { resources } = useResources();
const [selectedId] = useGlobal<string | null>('RESOURCE_SELECTED', null);
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>
</div>
);
}
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>
{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>
</div>
);
};
@@ -2,7 +2,7 @@ import { useMemo } from 'react';
import type { LayoutNode, PanelComponents } from '@/components/Workspace';
import { WorkspaceLayout } from '@/components/Workspace';
import { appRegistry } from '../../Workspaces/app-registry';
import { Applications } from './Applications';
import { Resources } from './Resources';
import { ResourceSidebar } from './ResourceSidebar';
const layout: LayoutNode = {
@@ -19,7 +19,7 @@ export const ResourceSettings = () => {
const panelComponents: PanelComponents = useMemo(
() => ({
'resources-left': ResourceSidebar,
'resources-right': Applications,
'resources-right': Resources,
}),
[],
);
@@ -0,0 +1,31 @@
import { useQuery } from '@tanstack/react-query';
import { useClient } from 'hooks/useClient';
export type Resource = {
id: string;
name: string;
subtitle: string;
type: string;
port: string | null;
description: string;
installCommand: string | null;
uninstallCommand: string | null;
manageCommand: string | null;
verifyCommand: string | null;
updateCommand: string | null;
installed: boolean;
version: string | null;
};
const RESOURCES_KEY = ['RESOURCES'];
export const useResources = () => {
const client = useClient();
const { data: resources, isLoading } = useQuery({
queryKey: RESOURCES_KEY,
queryFn: () => client.get<Resource[]>('/server-settings/resources'),
});
return { resources, isLoading };
};