resources
This commit is contained in:
+44
-13
@@ -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,
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user