Files
homarr/packages/widget-sdk/examples/rack.json
T

36 lines
5.9 KiB
JSON

{
"$schema": "homarr-widget-package-v3",
"manifest": {
"id": "homarr.reference.rack",
"version": "1.0.0",
"sdkVersion": "1",
"name": "Interactive server rack",
"description": "A keyboard-accessible SVG rack backed by real Beszel inventory, with selectable systems and host-native detail views.",
"entrypoints": {
"tile": "widget.tsx",
"advanced": "widget.tsx",
"server": "server.ts"
},
"handlers": {
"systems": {
"kind": "query",
"permission": "view"
}
}
},
"files": {
"server.ts": "import type { NativeResult, SystemRow } from \"./beszel-types\";\nimport { callWidgetIntegration, defineWidgetServer } from \"@homarr/widget-sdk/server\";\nexport default defineWidgetServer({ systems: (_input, context) => callWidgetIntegration<NativeResult<SystemRow[]>>(context, {connection:\"beszel\",capability:\"beszel.systems\"}) });",
"widget.tsx": "import type { SystemRow, NativeResult } from \"./beszel-types\";\nimport { Alert, Button, Group, Stack, Text } from \"@mantine/core\";\nimport { useWidgetHost, useWidgetQuery, useWidgetServices, useWidgetState } from \"@homarr/widget-sdk\";\nimport \"./rack.css\";\nexport default function Rack() {\n const host = useWidgetHost();\n const services = useWidgetServices();\n const query = useWidgetQuery<NativeResult<SystemRow[]>>(\"systems\", {}, {refetchInterval: 15000});\n const [selected, setSelected] = useWidgetState(\"selected\", \"\");\n const systems = query.data?.data ?? [];\n const rows = host.displayMode === \"advanced\" ? systems : systems.slice(0, 8);\n const inspect = (system:SystemRow) => { setSelected(system.id); services.openDetail({title: system.name, content: <Stack><Text>{system.hostname} \u00b7 {system.osName}</Text><Text>CPU: {system.cpu.toFixed(1)}% \u00b7 Memory: {system.memory.toFixed(1)}% \u00b7 Disk: {system.disk.toFixed(1)}%</Text><Text>{system.cpuModel} \u00b7 {system.cores} cores</Text><Text>Uptime: {Math.floor(system.uptime / 3600)} hours</Text></Stack>}); };\n return <Stack className=\"reference-rack\" p=\"sm\" h=\"100%\" style={{overflow:\"auto\"}}>\n <Group justify=\"space-between\"><Text fw={600}>Server rack</Text><Text size=\"xs\" c=\"dimmed\">{systems.length} systems</Text></Group>\n {query.error && <Alert color=\"red\">{query.error.message}<Button size=\"xs\" variant=\"subtle\" onClick={() => void query.refetch()}>Retry</Button></Alert>}\n <svg viewBox={`0 0 420 ${Math.max(90, rows.length * 64 + 24)}`} role=\"group\" aria-label=\"Beszel server rack\" style={{width:\"100%\", flexShrink:0}}>\n <rect x=\"1\" y=\"1\" width=\"418\" height={Math.max(88, rows.length * 64 + 22)} rx=\"8\" fill=\"var(--mantine-color-default)\" stroke=\"var(--mantine-color-default-border)\" />\n {rows.map((system, index) => <g key={system.id} role=\"button\" tabIndex={0} aria-label={`Inspect ${system.name}, ${system.status}`} onClick={() => inspect(system)} onKeyDown={(event) => { if (event.key === \"Enter\" || event.key === \" \") { event.preventDefault(); inspect(system); } }} className=\"reference-rack__unit\">\n <rect x=\"14\" y={12 + index * 64} width=\"392\" height=\"54\" rx=\"4\" fill=\"var(--mantine-color-body)\" stroke={selected === system.id ? \"var(--mantine-color-blue-6)\" : \"var(--mantine-color-default-border)\"} />\n <circle cx=\"31\" cy={39 + index * 64} r=\"5\" fill={system.status === \"up\" ? \"var(--mantine-color-teal-6)\" : \"var(--mantine-color-red-6)\"} />\n <text x=\"47\" y={34 + index * 64} fill=\"var(--mantine-color-text)\" fontSize=\"14\">{system.name.slice(0, 35)}</text>\n <text x=\"47\" y={53 + index * 64} fill=\"var(--mantine-color-dimmed)\" fontSize=\"11\">CPU {system.cpu.toFixed(1)}% \u00b7 RAM {system.memory.toFixed(1)}% \u00b7 {system.status}</text>\n <rect x=\"345\" y={26 + index * 64} width=\"44\" height=\"24\" rx=\"3\" fill=\"var(--mantine-color-dark-5)\" />\n {[0,1,2,3].map((slot) => <path key={slot} d={`M${350 + slot * 10},${30 + index * 64}v16`} stroke=\"var(--mantine-color-gray-6)\" />)}\n </g>)}\n </svg>\n {!query.isPending && !query.error && !systems.length && <Text c=\"dimmed\">No Beszel systems are available.</Text>}\n </Stack>;\n}",
"beszel-types.ts": "// Public result fields used from the Homarr Beszel capability adapter.\nexport interface SystemRow {\n id:string; name:string; status:\"up\"|\"down\"|\"paused\"|\"pending\";\n cpu:number; memory:number; disk:number; gpu:number; netBytes:number; uptime:number;\n hostname:string; cpuModel:string; cores:number; osName:string; agentVersion:string;\n}\nexport interface AttributedSystem extends SystemRow {connection:string;rowKey:string}\nexport interface NativeResult<T> {data:T;timestamp?:string}\nexport interface SystemStats {cpu:number;mu:number;mb:number;du:number;dio?:[number,number];dr?:number;dw?:number;b?:[number,number];ns?:number;nr?:number}\nexport interface ContainerStats {n:string;c:number;m:number;b?:[number,number];ns?:number;nr?:number}\nexport interface StatsRecord<T> {id:string;created:string;stats:T}\nexport type SystemRecord = StatsRecord<SystemStats>;\nexport type ContainerRecord = StatsRecord<ContainerStats[]>;\nexport interface StatsResult {systemStats:SystemRecord[];containerStats:ContainerRecord[]}\nexport type LiveStatsEvent = {type:\"system_stats\";record:SystemRecord}|{type:\"container_stats\";record:ContainerRecord};\nexport interface StatsInput {connection?:string;systemId:string;timePeriod?:string;includeDocker?:boolean}\n",
"rack.css": ".reference-rack__unit { cursor: pointer; outline: none; }\n.reference-rack__unit:focus-visible > rect:first-child { stroke: var(--mantine-color-blue-6); stroke-width: 3; }\n.reference-rack__unit:hover > rect:first-child { stroke: var(--mantine-color-blue-5); }\n"
},
"dependencies": {},
"connections": {
"beszel": {
"kind": "integration",
"label": "Beszel integration"
}
},
"options": {}
}