feat: add custom icon picker to node create/edit modal

- Add 65+ icons across 7 categories (Infrastructure, Media, Monitoring,
  Storage, Security, Automation, Dev & Containers, Communications) covering
  popular self-hosted apps: Home Assistant, Jellyfin, Plex, Grafana, Portainer,
  Pi-hole, Vaultwarden, Gitea, Nextcloud, Node-RED, Frigate, etc.
- New nodeIcons.ts utility with ICON_REGISTRY, ICON_MAP and resolveNodeIcon()
- Inline icon picker in NodeModal: collapsible panel with search + grid grouped
  by category; click to select, click again or Reset to revert to type default
- BaseNode uses resolveNodeIcon() so custom icon renders live on canvas
- Add custom_icon field to NodeData type, NodeBase/NodeUpdate schemas, Node ORM
  model, and database.py idempotent ALTER TABLE migration
This commit is contained in:
Pouzor
2026-03-07 23:01:30 +01:00
parent 1a72f9fa20
commit d98bfba506
7 changed files with 222 additions and 4 deletions
@@ -1,7 +1,9 @@
import { createElement } from 'react'
import { Handle, Position, type NodeProps, type Node } from '@xyflow/react'
import { type LucideIcon } from 'lucide-react'
import type { NodeData, NodeStatus } from '@/types'
import { resolveNodeColors } from '@/utils/nodeColors'
import { resolveNodeIcon } from '@/utils/nodeIcons'
const STATUS_COLORS: Record<NodeStatus, string> = {
online: '#39d353',
@@ -14,7 +16,8 @@ interface BaseNodeProps extends NodeProps<Node<NodeData>> {
icon: LucideIcon
}
export function BaseNode({ data, selected, icon: Icon }: BaseNodeProps) {
export function BaseNode({ data, selected, icon: typeIcon }: BaseNodeProps) {
const resolvedIcon = resolveNodeIcon(typeIcon, data.custom_icon)
const colors = resolveNodeColors(data)
const statusColor = STATUS_COLORS[data.status]
const isOnline = data.status === 'online'
@@ -42,7 +45,7 @@ export function BaseNode({ data, selected, icon: Icon }: BaseNodeProps) {
className="flex items-center justify-center w-7 h-7 rounded-md shrink-0"
style={{ color: isOnline ? colors.icon : '#8b949e', background: '#161b22' }}
>
<Icon size={15} />
{createElement(resolvedIcon, { size: 15 })}
</div>
{/* Details */}