feature: added a Container Mode toggle to the Virtualization group of node types
This commit is contained in:
+11
-6
@@ -33,6 +33,7 @@ import type { NodeData, EdgeData } from '@/types'
|
||||
|
||||
const STANDALONE = import.meta.env.VITE_STANDALONE === 'true'
|
||||
const STANDALONE_STORAGE_KEY = 'homelable_canvas'
|
||||
const CONTAINER_MODE_TYPES = new Set<NodeData['type']>(['proxmox', 'vm', 'lxc', 'docker_host'])
|
||||
|
||||
export default function App() {
|
||||
const { loadCanvas, markSaved, markUnsaved, selectedNodeId, selectedNodeIds, addNode, updateNode, deleteNode, onConnect, updateEdge, deleteEdge, setProxmoxContainerMode, setNodeZIndex, editingGroupRectId, setEditingGroupRectId, nodes, edges, snapshotHistory, undo, redo, copySelectedNodes, pasteNodes } = useCanvasStore()
|
||||
@@ -102,8 +103,8 @@ export default function App() {
|
||||
// Build a map of proxmox container mode to know if children should be nested
|
||||
const proxmoxContainerMap = new Map<string, boolean>(
|
||||
(apiNodes as ApiNode[])
|
||||
.filter((n) => n.type === 'proxmox' || n.type === 'group')
|
||||
.map((n) => [n.id, n.type === 'group' ? true : n.container_mode !== false])
|
||||
.filter((n) => n.type === 'group' || n.container_mode === true)
|
||||
.map((n) => [n.id, true])
|
||||
)
|
||||
const rfNodes = (apiNodes as ApiNode[]).map((n) => deserializeApiNode(n, proxmoxContainerMap))
|
||||
const rfEdges = (apiEdges as ApiEdge[]).map(deserializeApiEdge)
|
||||
@@ -241,8 +242,8 @@ export default function App() {
|
||||
snapshotHistory()
|
||||
const existingNode = nodes.find((n) => n.id === editNodeId)
|
||||
updateNode(editNodeId, data)
|
||||
// If proxmox container_mode changed, apply structural changes (children parentId, node dimensions)
|
||||
if (data.type === 'proxmox' && typeof data.container_mode === 'boolean') {
|
||||
// If container_mode changed, apply structural changes (children parentId, node dimensions)
|
||||
if (typeof data.container_mode === 'boolean') {
|
||||
setProxmoxContainerMode(editNodeId, data.container_mode)
|
||||
}
|
||||
// Sync virtual edge when parent_id changes on an LXC/VM node
|
||||
@@ -421,7 +422,9 @@ export default function App() {
|
||||
onClose={() => setAddNodeOpen(false)}
|
||||
onSubmit={handleAddNode}
|
||||
title="Add Node"
|
||||
proxmoxNodes={nodes.filter((n) => n.type === 'proxmox').map((n) => ({ id: n.id, label: n.data.label }))}
|
||||
parentContainerNodes={nodes
|
||||
.filter((n) => CONTAINER_MODE_TYPES.has(n.data.type) && n.data.container_mode)
|
||||
.map((n) => ({ id: n.id, label: n.data.label }))}
|
||||
/>
|
||||
|
||||
{/* key forces re-mount when editing a different node, resetting form state */}
|
||||
@@ -432,7 +435,9 @@ export default function App() {
|
||||
onSubmit={handleUpdateNode}
|
||||
initial={editNode?.data}
|
||||
title="Edit Node"
|
||||
proxmoxNodes={nodes.filter((n) => n.type === 'proxmox').map((n) => ({ id: n.id, label: n.data.label }))}
|
||||
parentContainerNodes={nodes
|
||||
.filter((n) => n.id !== editNodeId && CONTAINER_MODE_TYPES.has(n.data.type) && n.data.container_mode)
|
||||
.map((n) => ({ id: n.id, label: n.data.label }))}
|
||||
/>
|
||||
|
||||
<EdgeModal
|
||||
|
||||
@@ -18,7 +18,7 @@ export const nodeTypes = {
|
||||
printer: PrinterNode,
|
||||
computer: ComputerNode,
|
||||
cpl: CplNode,
|
||||
docker: DockerNode,
|
||||
docker_host: DockerNode,
|
||||
generic: GenericNode,
|
||||
groupRect: GroupRectNode,
|
||||
group: GroupNode,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createElement, useState } from 'react'
|
||||
import { Fragment, createElement, useState } from 'react'
|
||||
import { RotateCcw, ChevronDown } from 'lucide-react'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
@@ -11,12 +11,13 @@ import { ICON_REGISTRY, ICON_CATEGORIES, NODE_TYPE_DEFAULT_ICONS } from '@/utils
|
||||
|
||||
const NODE_TYPE_GROUPS: { label: string; types: NodeType[] }[] = [
|
||||
{ label: 'Hardware', types: ['isp', 'router', 'switch', 'server', 'nas', 'ap', 'printer'] },
|
||||
{ label: 'Virtualization', types: ['proxmox', 'vm', 'lxc', 'docker'] },
|
||||
{ label: 'Virtualization', types: ['proxmox', 'vm', 'lxc', 'docker_host'] },
|
||||
{ label: 'IoT', types: ['iot', 'camera', 'cpl'] },
|
||||
{ label: 'Generic', types: ['computer', 'generic', 'groupRect'] },
|
||||
]
|
||||
|
||||
const CHECK_METHODS: CheckMethod[] = ['none', 'ping', 'http', 'https', 'tcp', 'ssh', 'prometheus', 'health']
|
||||
const CONTAINER_MODE_TYPES: NodeType[] = ['proxmox', 'vm', 'lxc', 'docker_host']
|
||||
|
||||
const DEFAULT_DATA: Partial<NodeData> = {
|
||||
type: 'server',
|
||||
@@ -26,7 +27,7 @@ const DEFAULT_DATA: Partial<NodeData> = {
|
||||
status: 'unknown',
|
||||
check_method: 'ping',
|
||||
services: [],
|
||||
container_mode: true,
|
||||
container_mode: false,
|
||||
custom_colors: undefined,
|
||||
custom_icon: undefined,
|
||||
}
|
||||
@@ -37,14 +38,12 @@ interface NodeModalProps {
|
||||
onSubmit: (data: Partial<NodeData>) => void
|
||||
initial?: Partial<NodeData>
|
||||
title?: string
|
||||
proxmoxNodes?: { id: string; label: string }[]
|
||||
parentContainerNodes?: { id: string; label: string }[]
|
||||
}
|
||||
|
||||
const CHILD_TYPES: NodeType[] = ['vm', 'lxc']
|
||||
|
||||
// NodeModal is always mounted with a key that changes on open/edit, so useState
|
||||
// initial value is enough — no need for a reset effect.
|
||||
export function NodeModal({ open, onClose, onSubmit, initial, title = 'Add Node', proxmoxNodes = [] }: NodeModalProps) {
|
||||
// initial value is enough - no need for a reset effect.
|
||||
export function NodeModal({ open, onClose, onSubmit, initial, title = 'Add Node', parentContainerNodes = [] }: NodeModalProps) {
|
||||
const [form, setForm] = useState<Partial<NodeData>>({ ...DEFAULT_DATA, ...initial })
|
||||
const [iconSearch, setIconSearch] = useState('')
|
||||
const [iconPickerOpen, setIconPickerOpen] = useState(false)
|
||||
@@ -60,7 +59,12 @@ export function NodeModal({ open, onClose, onSubmit, initial, title = 'Add Node'
|
||||
return
|
||||
}
|
||||
setLabelError(false)
|
||||
onSubmit(form)
|
||||
const selectedType = (form.type ?? 'generic') as NodeType
|
||||
const canUseContainerMode = CONTAINER_MODE_TYPES.includes(selectedType)
|
||||
onSubmit({
|
||||
...form,
|
||||
container_mode: canUseContainerMode ? !!form.container_mode : false,
|
||||
})
|
||||
onClose()
|
||||
}
|
||||
|
||||
@@ -82,9 +86,9 @@ export function NodeModal({ open, onClose, onSubmit, initial, title = 'Add Node'
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-[#21262d] border-[#30363d]">
|
||||
{NODE_TYPE_GROUPS.map((group, i) => (
|
||||
<>
|
||||
{i > 0 && <SelectSeparator key={`sep-${group.label}`} className="bg-[#30363d]" />}
|
||||
<SelectGroup key={group.label}>
|
||||
<Fragment key={group.label}>
|
||||
{i > 0 && <SelectSeparator className="bg-[#30363d]" />}
|
||||
<SelectGroup>
|
||||
<SelectLabel className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground/50 px-2 py-1">
|
||||
{group.label}
|
||||
</SelectLabel>
|
||||
@@ -94,7 +98,7 @@ export function NodeModal({ open, onClose, onSubmit, initial, title = 'Add Node'
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</>
|
||||
</Fragment>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
@@ -134,7 +138,7 @@ export function NodeModal({ open, onClose, onSubmit, initial, title = 'Add Node'
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Inline icon picker — full width, shown below the type+icon row */}
|
||||
{/* Inline icon picker - full width, shown below the type+icon row */}
|
||||
{iconPickerOpen && (
|
||||
<div className="flex flex-col gap-2 p-2.5 rounded-md bg-[#0d1117] border border-[#30363d] col-span-2">
|
||||
<Input
|
||||
@@ -244,10 +248,10 @@ export function NodeModal({ open, onClose, onSubmit, initial, title = 'Add Node'
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Parent Proxmox (VM / LXC only) */}
|
||||
{CHILD_TYPES.includes(form.type as NodeType) && proxmoxNodes.length > 0 && (
|
||||
{/* Parent container */}
|
||||
{form.type !== 'groupRect' && form.type !== 'group' && parentContainerNodes.length > 0 && (
|
||||
<div className="flex flex-col gap-1.5 col-span-2">
|
||||
<Label className="text-xs text-muted-foreground">Parent Proxmox</Label>
|
||||
<Label className="text-xs text-muted-foreground">Parent Container</Label>
|
||||
<Select
|
||||
value={form.parent_id ?? 'none'}
|
||||
onValueChange={(v) => set('parent_id', v === 'none' ? undefined : v)}
|
||||
@@ -257,7 +261,7 @@ export function NodeModal({ open, onClose, onSubmit, initial, title = 'Add Node'
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-[#21262d] border-[#30363d]">
|
||||
<SelectItem value="none" className="text-sm">None (standalone)</SelectItem>
|
||||
{proxmoxNodes.map((n) => (
|
||||
{parentContainerNodes.map((n) => (
|
||||
<SelectItem key={n.id} value={n.id} className="text-sm">{n.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
@@ -265,12 +269,12 @@ export function NodeModal({ open, onClose, onSubmit, initial, title = 'Add Node'
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Container mode (proxmox only) */}
|
||||
{form.type === 'proxmox' && (
|
||||
{/* Container mode */}
|
||||
{CONTAINER_MODE_TYPES.includes((form.type ?? 'generic') as NodeType) && (
|
||||
<div className="flex items-center justify-between col-span-2 py-1">
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<Label className="text-xs text-muted-foreground">Container Mode</Label>
|
||||
<span className="text-[10px] text-muted-foreground/60">Show VM/LXC nodes nested inside</span>
|
||||
<span className="text-[10px] text-muted-foreground/60">Allow other nodes to nest inside this node</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
@@ -344,10 +348,10 @@ export function NodeModal({ open, onClose, onSubmit, initial, title = 'Add Node'
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-[#21262d] border-[#30363d]">
|
||||
<SelectItem value="1" className="text-sm">1 — center</SelectItem>
|
||||
<SelectItem value="2" className="text-sm">2 — left / right</SelectItem>
|
||||
<SelectItem value="3" className="text-sm">3 — left / center / right</SelectItem>
|
||||
<SelectItem value="4" className="text-sm">4 — evenly spaced</SelectItem>
|
||||
<SelectItem value="1" className="text-sm">1 - center</SelectItem>
|
||||
<SelectItem value="2" className="text-sm">2 - left / right</SelectItem>
|
||||
<SelectItem value="3" className="text-sm">3 - left / center / right</SelectItem>
|
||||
<SelectItem value="4" className="text-sm">4 - evenly spaced</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
@@ -219,15 +219,20 @@ describe('NodeModal', () => {
|
||||
expect(screen.queryByTitle('Router')).toBeNull()
|
||||
})
|
||||
|
||||
// ── Container mode (proxmox only) ─────────────────────────────────────
|
||||
// ── Container mode ─────────────────────────────────────────────────────
|
||||
|
||||
it('shows Container Mode toggle for proxmox type', () => {
|
||||
renderModal({ initial: { ...BASE, type: 'proxmox' } })
|
||||
expect(screen.getByText('Container Mode')).toBeDefined()
|
||||
})
|
||||
|
||||
it('hides Container Mode for non-proxmox types', () => {
|
||||
renderModal({ initial: BASE })
|
||||
it('hides Container Mode for server type', () => {
|
||||
renderModal({ initial: { ...BASE, type: 'server' } })
|
||||
expect(screen.queryByText('Container Mode')).toBeNull()
|
||||
})
|
||||
|
||||
it('hides Container Mode for groupRect type', () => {
|
||||
renderModal({ initial: { ...BASE, type: 'groupRect' } })
|
||||
expect(screen.queryByText('Container Mode')).toBeNull()
|
||||
})
|
||||
|
||||
@@ -238,33 +243,25 @@ describe('NodeModal', () => {
|
||||
expect((onSubmit.mock.calls[0][0] as Partial<NodeData>).container_mode).toBe(false)
|
||||
})
|
||||
|
||||
// ── Parent Proxmox (vm / lxc only) ───────────────────────────────────
|
||||
// ── Parent container ──────────────────────────────────────────────────
|
||||
|
||||
it('shows Parent Proxmox for vm with proxmoxNodes', () => {
|
||||
it('shows Parent Container when options are provided', () => {
|
||||
renderModal({
|
||||
initial: { ...BASE, type: 'vm' },
|
||||
proxmoxNodes: [{ id: 'px1', label: 'PVE-01' }],
|
||||
initial: { ...BASE, type: 'server' },
|
||||
parentContainerNodes: [{ id: 'c1', label: 'Container 01' }],
|
||||
})
|
||||
expect(screen.getByText('Parent Proxmox')).toBeDefined()
|
||||
expect(screen.getByText('PVE-01')).toBeDefined()
|
||||
expect(screen.getByText('Parent Container')).toBeDefined()
|
||||
expect(screen.getByText('Container 01')).toBeDefined()
|
||||
})
|
||||
|
||||
it('shows Parent Proxmox for lxc with proxmoxNodes', () => {
|
||||
renderModal({
|
||||
initial: { ...BASE, type: 'lxc' },
|
||||
proxmoxNodes: [{ id: 'px1', label: 'PVE-01' }],
|
||||
})
|
||||
expect(screen.getByText('Parent Proxmox')).toBeDefined()
|
||||
it('hides Parent Container for groupRect type', () => {
|
||||
renderModal({ initial: { ...BASE, type: 'groupRect' }, parentContainerNodes: [{ id: 'c1', label: 'Container 01' }] })
|
||||
expect(screen.queryByText('Parent Container')).toBeNull()
|
||||
})
|
||||
|
||||
it('hides Parent Proxmox for server type', () => {
|
||||
renderModal({ initial: BASE, proxmoxNodes: [{ id: 'px1', label: 'PVE-01' }] })
|
||||
expect(screen.queryByText('Parent Proxmox')).toBeNull()
|
||||
})
|
||||
|
||||
it('hides Parent Proxmox for vm when no proxmoxNodes', () => {
|
||||
renderModal({ initial: { ...BASE, type: 'vm' } })
|
||||
expect(screen.queryByText('Parent Proxmox')).toBeNull()
|
||||
it('hides Parent Container when no container options are available', () => {
|
||||
renderModal({ initial: { ...BASE, type: 'server' } })
|
||||
expect(screen.queryByText('Parent Container')).toBeNull()
|
||||
})
|
||||
|
||||
// ── Appearance ────────────────────────────────────────────────────────
|
||||
|
||||
@@ -49,6 +49,21 @@ describe('canvasStore', () => {
|
||||
expect(hasUnsavedChanges).toBe(true)
|
||||
})
|
||||
|
||||
it('addNode nests under parent only when parent is in container mode', () => {
|
||||
const parent = { ...makeNode('p1', { container_mode: false }), position: { x: 100, y: 100 } }
|
||||
const child = { ...makeNode('c1', { parent_id: 'p1' }), position: { x: 150, y: 180 } }
|
||||
useCanvasStore.getState().addNode(parent)
|
||||
useCanvasStore.getState().addNode(child)
|
||||
const childNode = useCanvasStore.getState().nodes.find((n) => n.id === 'c1')
|
||||
expect(childNode?.parentId).toBeUndefined()
|
||||
|
||||
useCanvasStore.getState().updateNode('p1', { container_mode: true })
|
||||
useCanvasStore.getState().setProxmoxContainerMode('p1', true)
|
||||
const nested = useCanvasStore.getState().nodes.find((n) => n.id === 'c1')
|
||||
expect(nested?.parentId).toBe('p1')
|
||||
expect(nested?.extent).toBe('parent')
|
||||
})
|
||||
|
||||
it('updateNode updates data fields', () => {
|
||||
useCanvasStore.getState().addNode(makeNode('n1', { label: 'old' }))
|
||||
useCanvasStore.getState().updateNode('n1', { label: 'new', ip: '10.0.0.1' })
|
||||
|
||||
@@ -172,8 +172,18 @@ export const useCanvasStore = create<CanvasState>((set) => ({
|
||||
|
||||
addNode: (node) =>
|
||||
set((state) => {
|
||||
const enriched = node.data.parent_id
|
||||
? { ...node, parentId: node.data.parent_id, extent: 'parent' as const }
|
||||
const parent = node.data.parent_id ? state.nodes.find((n) => n.id === node.data.parent_id) : null
|
||||
const shouldNestInParent = !!(parent?.data.container_mode)
|
||||
const enriched = node.data.parent_id && shouldNestInParent
|
||||
? {
|
||||
...node,
|
||||
parentId: node.data.parent_id,
|
||||
extent: 'parent' as const,
|
||||
position: {
|
||||
x: Math.max(10, node.position.x - parent.position.x),
|
||||
y: Math.max(10, node.position.y - parent.position.y),
|
||||
},
|
||||
}
|
||||
: node
|
||||
// Parents must come before children in the array (React Flow requirement)
|
||||
const withoutNew = state.nodes.filter((n) => n.id !== node.id)
|
||||
@@ -283,14 +293,38 @@ export const useCanvasStore = create<CanvasState>((set) => ({
|
||||
|
||||
setProxmoxContainerMode: (proxmoxId, enabled) =>
|
||||
set((state) => {
|
||||
const parentNode = state.nodes.find((n) => n.id === proxmoxId)
|
||||
let nodes = state.nodes.map((n) => {
|
||||
if (n.id === proxmoxId) {
|
||||
const withMode = { ...n, data: { ...n.data, container_mode: enabled } }
|
||||
if (n.data.type !== 'proxmox') return withMode
|
||||
return enabled
|
||||
? { ...withMode, width: 300, height: 200 }
|
||||
? { ...withMode, width: n.width ?? 300, height: n.height ?? 200 }
|
||||
: { ...withMode, width: undefined, height: undefined }
|
||||
}
|
||||
if (n.data.parent_id === proxmoxId) {
|
||||
if (enabled && parentNode) {
|
||||
return {
|
||||
...n,
|
||||
parentId: proxmoxId,
|
||||
extent: 'parent' as const,
|
||||
position: {
|
||||
x: Math.max(10, n.position.x - parentNode.position.x),
|
||||
y: Math.max(10, n.position.y - parentNode.position.y),
|
||||
},
|
||||
}
|
||||
}
|
||||
if (!enabled && parentNode) {
|
||||
return {
|
||||
...n,
|
||||
parentId: undefined,
|
||||
extent: undefined,
|
||||
position: {
|
||||
x: parentNode.position.x + n.position.x,
|
||||
y: parentNode.position.y + n.position.y,
|
||||
},
|
||||
}
|
||||
}
|
||||
return enabled
|
||||
? { ...n, parentId: proxmoxId, extent: 'parent' as const }
|
||||
: { ...n, parentId: undefined, extent: undefined }
|
||||
|
||||
@@ -13,7 +13,7 @@ export type NodeType =
|
||||
| 'printer'
|
||||
| 'computer'
|
||||
| 'cpl'
|
||||
| 'docker'
|
||||
| 'docker_host'
|
||||
| 'generic'
|
||||
| 'groupRect'
|
||||
| 'group'
|
||||
@@ -126,7 +126,7 @@ export const NODE_TYPE_LABELS: Record<NodeType, string> = {
|
||||
printer: 'Printer',
|
||||
computer: 'Computer',
|
||||
cpl: 'CPL / Powerline',
|
||||
docker: 'Docker Host',
|
||||
docker_host: 'Docker Host',
|
||||
generic: 'Generic Device',
|
||||
groupRect: 'Group Rectangle',
|
||||
group: 'Node Group',
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { NodeType, EdgeType, NodeStatus } from '@/types'
|
||||
|
||||
const NODE_TYPES: NodeType[] = [
|
||||
'isp', 'router', 'switch', 'server', 'proxmox', 'vm', 'lxc',
|
||||
'nas', 'iot', 'ap', 'camera', 'printer', 'computer', 'cpl', 'docker', 'generic', 'groupRect',
|
||||
'nas', 'iot', 'ap', 'camera', 'printer', 'computer', 'cpl', 'docker_host', 'generic', 'groupRect',
|
||||
]
|
||||
const EDGE_TYPES: EdgeType[] = ['ethernet', 'wifi', 'iot', 'vlan', 'virtual', 'cluster']
|
||||
const STATUS_TYPES: NodeStatus[] = ['online', 'offline', 'pending', 'unknown']
|
||||
@@ -84,7 +84,7 @@ describe('THEMES', () => {
|
||||
expect(d.nodeAccents.server.border).toBe('#a855f7')
|
||||
expect(d.nodeAccents.isp.border).toBe('#00d4ff')
|
||||
expect(d.nodeAccents.proxmox.border).toBe('#ff6e00')
|
||||
expect(d.nodeAccents.docker.border).toBe('#2496ED')
|
||||
expect(d.nodeAccents.docker_host.border).toBe('#2496ED')
|
||||
expect(d.nodeCardBackground).toBe('#21262d')
|
||||
expect(d.nodeIconBackground).toBe('#161b22')
|
||||
expect(d.canvasBackground).toBe('#0d1117')
|
||||
|
||||
@@ -134,6 +134,7 @@ export function deserializeApiNode(
|
||||
n: ApiNode,
|
||||
proxmoxContainerMap: Map<string, boolean>,
|
||||
): Node<NodeData> {
|
||||
const normalizedType = n.type === 'docker' ? 'docker_host' : n.type
|
||||
if (n.type === 'groupRect') {
|
||||
const w = (n.custom_colors?.width as number | undefined) ?? 360
|
||||
const h = (n.custom_colors?.height as number | undefined) ?? 240
|
||||
@@ -152,15 +153,15 @@ export function deserializeApiNode(
|
||||
const parentIsContainer = n.parent_id ? (proxmoxContainerMap.get(n.parent_id) ?? false) : false
|
||||
return {
|
||||
id: n.id,
|
||||
type: n.type,
|
||||
type: normalizedType,
|
||||
position: { x: n.pos_x, y: n.pos_y },
|
||||
data: n as unknown as NodeData,
|
||||
data: { ...n, type: normalizedType } as unknown as NodeData,
|
||||
...(n.parent_id && parentIsContainer ? { parentId: n.parent_id, extent: 'parent' as const } : {}),
|
||||
...(n.type === 'proxmox' && n.container_mode !== false
|
||||
...(normalizedType === 'proxmox' && n.container_mode !== false
|
||||
? { width: n.width ?? 300, height: n.height ?? 200 }
|
||||
: {}),
|
||||
...(n.width && n.type !== 'proxmox' ? { width: n.width } : {}),
|
||||
...(n.height && n.type !== 'proxmox' ? { height: n.height } : {}),
|
||||
...(n.width && normalizedType !== 'proxmox' ? { width: n.width } : {}),
|
||||
...(n.height && normalizedType !== 'proxmox' ? { height: n.height } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -132,7 +132,7 @@ export const NODE_TYPE_DEFAULT_ICONS: Record<NodeType, LucideIcon> = {
|
||||
printer: Printer,
|
||||
computer: Monitor,
|
||||
cpl: PlugZap,
|
||||
docker: Anchor,
|
||||
docker_host: Anchor,
|
||||
generic: Circle,
|
||||
group: Circle,
|
||||
groupRect: Circle,
|
||||
|
||||
@@ -56,7 +56,7 @@ export const THEMES: Record<ThemeId, ThemePreset> = {
|
||||
printer: { border: '#8b949e', icon: '#8b949e' },
|
||||
computer: { border: '#a855f7', icon: '#a855f7' },
|
||||
cpl: { border: '#e3b341', icon: '#e3b341' },
|
||||
docker: { border: '#2496ED', icon: '#2496ED' },
|
||||
docker_host: { border: '#2496ED', icon: '#2496ED' },
|
||||
generic: { border: '#8b949e', icon: '#8b949e' },
|
||||
groupRect:{ border: '#00d4ff', icon: '#00d4ff' },
|
||||
group: { border: '#00d4ff', icon: '#00d4ff' },
|
||||
@@ -111,7 +111,7 @@ export const THEMES: Record<ThemeId, ThemePreset> = {
|
||||
printer: { border: '#94a3b8', icon: '#94a3b8' },
|
||||
computer: { border: '#c084fc', icon: '#c084fc' },
|
||||
cpl: { border: '#fbbf24', icon: '#fbbf24' },
|
||||
docker: { border: '#2496ED', icon: '#2496ED' },
|
||||
docker_host: { border: '#2496ED', icon: '#2496ED' },
|
||||
generic: { border: '#94a3b8', icon: '#94a3b8' },
|
||||
groupRect:{ border: '#22d3ee', icon: '#22d3ee' },
|
||||
group: { border: '#22d3ee', icon: '#22d3ee' },
|
||||
@@ -166,7 +166,7 @@ export const THEMES: Record<ThemeId, ThemePreset> = {
|
||||
printer: { border: '#6b7280', icon: '#6b7280' },
|
||||
computer: { border: '#7c3aed', icon: '#7c3aed' },
|
||||
cpl: { border: '#b45309', icon: '#b45309' },
|
||||
docker: { border: '#2496ED', icon: '#2496ED' },
|
||||
docker_host: { border: '#2496ED', icon: '#2496ED' },
|
||||
generic: { border: '#6b7280', icon: '#6b7280' },
|
||||
groupRect:{ border: '#0284c7', icon: '#0284c7' },
|
||||
group: { border: '#0284c7', icon: '#0284c7' },
|
||||
@@ -221,7 +221,7 @@ export const THEMES: Record<ThemeId, ThemePreset> = {
|
||||
printer: { border: '#8888ff', icon: '#8888ff' },
|
||||
computer: { border: '#ff00ff', icon: '#ff00ff' },
|
||||
cpl: { border: '#ffff00', icon: '#ffff00' },
|
||||
docker: { border: '#00aaff', icon: '#00aaff' },
|
||||
docker_host: { border: '#00aaff', icon: '#00aaff' },
|
||||
generic: { border: '#8888ff', icon: '#8888ff' },
|
||||
groupRect:{ border: '#00ffff', icon: '#00ffff' },
|
||||
group: { border: '#00ffff', icon: '#00ffff' },
|
||||
@@ -276,7 +276,7 @@ export const THEMES: Record<ThemeId, ThemePreset> = {
|
||||
printer: { border: '#005500', icon: '#005500' },
|
||||
computer: { border: '#008822', icon: '#008822' },
|
||||
cpl: { border: '#66ff33', icon: '#66ff33' },
|
||||
docker: { border: '#00cc88', icon: '#00cc88' },
|
||||
docker_host: { border: '#00cc88', icon: '#00cc88' },
|
||||
generic: { border: '#006600', icon: '#006600' },
|
||||
groupRect:{ border: '#00ff41', icon: '#00ff41' },
|
||||
group: { border: '#00ff41', icon: '#00ff41' },
|
||||
|
||||
Reference in New Issue
Block a user