mirror of
https://github.com/Crosstalk-Solutions/project-nomad.git
synced 2026-10-17 05:14:19 +02:00
Users want things they already run to appear on the dashboard: a NAS, a router UI, a printer, a Pi, a container they started themselves. Adds a tile they address themselves, with a name, one URL field, an optional description and an icon from the curated set. Chosen over adopting containers from the Docker daemon, which is what community PR #1239 proposed. Adoption only reaches containers on this Docker host and has to guess: it took the lowest published host port and assumed http, which picks the wrong port for anything publishing several (Portainer 8000 against 9443, Jellyfin 8096 against 8920) and breaks entirely for HTTPS. A tile the user addresses has no guessing in it, covers the whole LAN rather than one Docker host, and has no lifecycle that can break. One URL field rather than separate host, port and path, because people paste URLs and it gets https and sub-paths for free. A bare "192.168.1.50:8080" gains http automatically, which is what most LAN devices need. normalizeCustomUrl and getServiceLink already did exactly this work for the custom app URL override, so both are reused rather than duplicated. Stored as a service row with no container behind it: is_link_tile marks it, custom_url carries the destination, and the service_name is namespaced nomad_link_* so it can never collide with a curated entry. The seeder only iterates its own DEFAULT_SERVICES and never prunes unknown rows, so a reseed cannot touch or remove a tile. The tile is deliberately styled differently from a managed app, outlined and dashed with an external-link marker rather than filled, because if it looked the same users would expect Start, Stop and Update and file bugs when those controls are not there. Removing a tile arms a confirm first, since it is user-entered data with no undo. Icons come from a fixed 36 shared by the picker and the server-side validator, so the two cannot disagree about what is selectable. DynamicIcon renders nothing at all for a name it does not know, which would leave a silent hole in a tile. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F8Mgg1vd2eKSs8StZiuyMV
73 lines
1.9 KiB
TypeScript
73 lines
1.9 KiB
TypeScript
/**
|
|
* The icons offered when creating a dashboard link tile: a curated 36, laid out
|
|
* six across and six down.
|
|
*
|
|
* Every name here must exist in the `DynamicIcon` registry (`inertia/lib/icons.ts`).
|
|
* `DynamicIcon` renders nothing at all for a name it does not know, so an icon
|
|
* that is not in that registry produces a tile with a silent hole where its icon
|
|
* should be. Shared by the picker and the server-side validator so the two can
|
|
* never disagree about what is selectable.
|
|
*
|
|
* Chosen for "a thing on my network": storage, media, documents, tooling,
|
|
* infrastructure. Not a general icon browser. The point of a fixed set is that
|
|
* the user picks from something that fits on one screen.
|
|
*/
|
|
export const LINK_TILE_ICONS = [
|
|
// Infrastructure
|
|
'IconServer',
|
|
'IconDatabase',
|
|
'IconWorld',
|
|
'IconWifi',
|
|
'IconAntenna',
|
|
'IconBroadcast',
|
|
|
|
// Machines and containers
|
|
'IconBrandDocker',
|
|
'IconCpu',
|
|
'IconHome',
|
|
'IconBox',
|
|
'IconFolderOpen',
|
|
'IconMovie',
|
|
|
|
// Reading and reference
|
|
'IconBooks',
|
|
'IconBook',
|
|
'IconLibrary',
|
|
'IconMap',
|
|
'IconNotes',
|
|
'IconFileDescription',
|
|
|
|
// Tools and admin
|
|
'IconCode',
|
|
'IconTool',
|
|
'IconSettings',
|
|
'IconShieldCheck',
|
|
'IconShieldLock',
|
|
'IconRobot',
|
|
|
|
// Subjects
|
|
'IconBrain',
|
|
'IconPlant',
|
|
'IconChefHat',
|
|
'IconSchool',
|
|
'IconStethoscope',
|
|
'IconSearch',
|
|
|
|
// Transfer and misc
|
|
'IconCloudDownload',
|
|
'IconCloudUpload',
|
|
'IconLogs',
|
|
'IconPlayerPlay',
|
|
'IconExternalLink',
|
|
'IconWand',
|
|
] as const
|
|
|
|
export type LinkTileIconName = (typeof LINK_TILE_ICONS)[number]
|
|
|
|
/** The icon a tile gets when none was chosen, or when a stored name is no longer offered. */
|
|
export const DEFAULT_LINK_TILE_ICON: LinkTileIconName = 'IconExternalLink'
|
|
|
|
export function isLinkTileIcon(value: unknown): value is LinkTileIconName {
|
|
return typeof value === 'string' && (LINK_TILE_ICONS as readonly string[]).includes(value)
|
|
}
|