mirror of
https://github.com/jcreek/OpenNetworkDiagram.git
synced 2026-07-12 18:43:44 +00:00
feat(#5): add icon presets, dark mode theme store, and icon-aware graph rendering
This commit is contained in:
+28
@@ -1,2 +1,30 @@
|
|||||||
@import 'tailwindcss';
|
@import 'tailwindcss';
|
||||||
@plugin '@tailwindcss/typography';
|
@plugin '@tailwindcss/typography';
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--app-bg: #e2e8f0;
|
||||||
|
--panel-bg: #f8fafc;
|
||||||
|
--panel-border: #cbd5e1;
|
||||||
|
--panel-contrast: #0f172a;
|
||||||
|
--muted-text: #334155;
|
||||||
|
--chip-bg: #e2e8f0;
|
||||||
|
--chip-text: #0f172a;
|
||||||
|
--modal-overlay: rgba(15, 23, 42, 0.58);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme='dark'] {
|
||||||
|
--app-bg: #0b1220;
|
||||||
|
--panel-bg: #111827;
|
||||||
|
--panel-border: #334155;
|
||||||
|
--panel-contrast: #e2e8f0;
|
||||||
|
--muted-text: #94a3b8;
|
||||||
|
--chip-bg: #1e293b;
|
||||||
|
--chip-text: #e2e8f0;
|
||||||
|
--modal-overlay: rgba(2, 6, 23, 0.78);
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
background: var(--app-bg);
|
||||||
|
color: var(--panel-contrast);
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
export interface IconDefinition {
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ICONS: IconDefinition[] = [
|
||||||
|
{ key: 'server', label: 'Server', path: '/icons/server.svg' },
|
||||||
|
{ key: 'router', label: 'Router', path: '/icons/router.svg' },
|
||||||
|
{ key: 'switch', label: 'Switch', path: '/icons/switch.svg' },
|
||||||
|
{ key: 'storage', label: 'Storage', path: '/icons/storage.svg' },
|
||||||
|
{ key: 'desktop', label: 'Desktop', path: '/icons/desktop.svg' },
|
||||||
|
{ key: 'cloud', label: 'Cloud', path: '/icons/cloud.svg' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const ICON_BY_KEY = new Map(ICONS.map((icon) => [icon.key, icon]));
|
||||||
|
|
||||||
|
export function listIconDefinitions(): IconDefinition[] {
|
||||||
|
return ICONS;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveIconPath(iconKey?: string): string | undefined {
|
||||||
|
if (!iconKey) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return ICON_BY_KEY.get(iconKey)?.path;
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { ConnectedPortRef, NetworkData, Port } from '../types';
|
import type { ConnectedPortRef, NetworkData, Port } from '../types';
|
||||||
|
import { resolveIconPath } from '../config/iconRegistry';
|
||||||
import type { GraphEdgeElement, GraphNodeElement, GraphTransformResult } from './types';
|
import type { GraphEdgeElement, GraphNodeElement, GraphTransformResult } from './types';
|
||||||
|
|
||||||
interface EndpointOwner {
|
interface EndpointOwner {
|
||||||
@@ -8,8 +9,12 @@ interface EndpointOwner {
|
|||||||
ports: Map<string, Port>;
|
ports: Map<string, Port>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function toOwnerKey(kind: EndpointOwner['kind'], name: string): string {
|
function normalizeOwnerName(name: string): string {
|
||||||
return `${kind}:${name}`;
|
return name.trim().toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function toOwnerLookupKey(kind: EndpointOwner['kind'], name: string): string {
|
||||||
|
return `${kind}:${normalizeOwnerName(name)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function toMachineNodeId(name: string): string {
|
function toMachineNodeId(name: string): string {
|
||||||
@@ -75,7 +80,7 @@ export default function transformNetworkDataToGraph(data: NetworkData): GraphTra
|
|||||||
vmCount: number;
|
vmCount: number;
|
||||||
}
|
}
|
||||||
> = {};
|
> = {};
|
||||||
const ownersByKey = new Map<string, EndpointOwner>();
|
const ownersByLookup = new Map<string, EndpointOwner>();
|
||||||
const seenWarnings = new Set<string>();
|
const seenWarnings = new Set<string>();
|
||||||
|
|
||||||
const warn = (message: string) => {
|
const warn = (message: string) => {
|
||||||
@@ -86,14 +91,14 @@ export default function transformNetworkDataToGraph(data: NetworkData): GraphTra
|
|||||||
};
|
};
|
||||||
|
|
||||||
const addOwner = (owner: EndpointOwner) => {
|
const addOwner = (owner: EndpointOwner) => {
|
||||||
const ownerKey = toOwnerKey(owner.kind, owner.name);
|
const ownerLookupKey = toOwnerLookupKey(owner.kind, owner.name);
|
||||||
if (ownersByKey.has(ownerKey)) {
|
if (ownersByLookup.has(ownerLookupKey)) {
|
||||||
warn(
|
warn(
|
||||||
`Duplicate ${owner.kind} name "${owner.name}" found. First occurrence will be used for link resolution.`
|
`Duplicate ${owner.kind} name "${owner.name}" found. First occurrence will be used for link resolution.`
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ownersByKey.set(ownerKey, owner);
|
ownersByLookup.set(ownerLookupKey, owner);
|
||||||
};
|
};
|
||||||
|
|
||||||
let hostingEdgeCounter = 0;
|
let hostingEdgeCounter = 0;
|
||||||
@@ -110,24 +115,27 @@ export default function transformNetworkDataToGraph(data: NetworkData): GraphTra
|
|||||||
|
|
||||||
nodes.push({
|
nodes.push({
|
||||||
data: {
|
data: {
|
||||||
id: machineNodeId,
|
id: machineNodeId,
|
||||||
label: machine.machineName,
|
label: machine.machineName,
|
||||||
rawName: machine.machineName,
|
rawName: machine.machineName,
|
||||||
kind: 'machine',
|
kind: 'machine',
|
||||||
|
iconKey: machine.iconKey,
|
||||||
|
iconUrl: resolveIconPath(machine.iconKey),
|
||||||
nodeWidth: 200,
|
nodeWidth: 200,
|
||||||
nodeHeight: 110,
|
nodeHeight: 110,
|
||||||
details: {
|
details: {
|
||||||
type: 'machine',
|
type: 'machine',
|
||||||
name: machine.machineName,
|
name: machine.machineName,
|
||||||
ip: machine.ipAddress,
|
iconKey: machine.iconKey,
|
||||||
role: machine.role,
|
ip: machine.ipAddress,
|
||||||
os: machine.operatingSystem,
|
role: machine.role,
|
||||||
cpu: machine.hardware?.cpu ?? 'Unknown',
|
os: machine.operatingSystem,
|
||||||
ram: machine.hardware?.ram ?? 'Unknown',
|
cpu: machine.hardware?.cpu ?? 'Unknown',
|
||||||
gpu: machine.hardware?.gpu ?? undefined,
|
ram: machine.hardware?.ram ?? 'Unknown',
|
||||||
ports: machinePortsList,
|
gpu: machine.hardware?.gpu ?? undefined,
|
||||||
vmCount: machineVms.length,
|
ports: machinePortsList,
|
||||||
vms: machineVms
|
vmCount: machineVms.length,
|
||||||
|
vms: machineVms
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -148,11 +156,14 @@ export default function transformNetworkDataToGraph(data: NetworkData): GraphTra
|
|||||||
rawName: vm.name,
|
rawName: vm.name,
|
||||||
kind: 'vm',
|
kind: 'vm',
|
||||||
hostMachineId: machineNodeId,
|
hostMachineId: machineNodeId,
|
||||||
|
iconKey: vm.iconKey,
|
||||||
|
iconUrl: resolveIconPath(vm.iconKey),
|
||||||
nodeWidth: 140,
|
nodeWidth: 140,
|
||||||
nodeHeight: 66,
|
nodeHeight: 66,
|
||||||
details: {
|
details: {
|
||||||
type: 'vm',
|
type: 'vm',
|
||||||
name: vm.name,
|
name: vm.name,
|
||||||
|
iconKey: vm.iconKey,
|
||||||
ip: vm.ipAddress,
|
ip: vm.ipAddress,
|
||||||
role: vm.role,
|
role: vm.role,
|
||||||
hostName: machine.machineName
|
hostName: machine.machineName
|
||||||
@@ -199,11 +210,14 @@ export default function transformNetworkDataToGraph(data: NetworkData): GraphTra
|
|||||||
label: device.name,
|
label: device.name,
|
||||||
rawName: device.name,
|
rawName: device.name,
|
||||||
kind: 'device',
|
kind: 'device',
|
||||||
|
iconKey: device.iconKey,
|
||||||
|
iconUrl: resolveIconPath(device.iconKey),
|
||||||
nodeWidth: 176,
|
nodeWidth: 176,
|
||||||
nodeHeight: 116,
|
nodeHeight: 116,
|
||||||
details: {
|
details: {
|
||||||
type: 'device',
|
type: 'device',
|
||||||
name: device.name,
|
name: device.name,
|
||||||
|
iconKey: device.iconKey,
|
||||||
ip: device.ipAddress,
|
ip: device.ipAddress,
|
||||||
deviceType: device.type,
|
deviceType: device.type,
|
||||||
notes: device.notes,
|
notes: device.notes,
|
||||||
@@ -228,15 +242,17 @@ export default function transformNetworkDataToGraph(data: NetworkData): GraphTra
|
|||||||
const seenPhysicalConnections = new Set<string>();
|
const seenPhysicalConnections = new Set<string>();
|
||||||
let physicalEdgeCounter = 0;
|
let physicalEdgeCounter = 0;
|
||||||
|
|
||||||
for (const owner of ownersByKey.values()) {
|
for (const owner of ownersByLookup.values()) {
|
||||||
for (const port of owner.ports.values()) {
|
for (const port of owner.ports.values()) {
|
||||||
const connectedTo = parseConnectedTo(port.connectedTo);
|
const connectedTo = parseConnectedTo(port.connectedTo);
|
||||||
if (!connectedTo) {
|
if (!connectedTo) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const machineTargetOwner = ownersByKey.get(toOwnerKey('machine', connectedTo.device));
|
const machineTargetOwner = ownersByLookup.get(
|
||||||
const deviceTargetOwner = ownersByKey.get(toOwnerKey('device', connectedTo.device));
|
toOwnerLookupKey('machine', connectedTo.device)
|
||||||
|
);
|
||||||
|
const deviceTargetOwner = ownersByLookup.get(toOwnerLookupKey('device', connectedTo.device));
|
||||||
if (machineTargetOwner && deviceTargetOwner) {
|
if (machineTargetOwner && deviceTargetOwner) {
|
||||||
warn(
|
warn(
|
||||||
`Link from "${owner.name}:${port.portName}" references ambiguous device "${connectedTo.device}" (matches both machine and device names).`
|
`Link from "${owner.name}:${port.portName}" references ambiguous device "${connectedTo.device}" (matches both machine and device names).`
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export interface PortDetails {
|
|||||||
export interface MachineDetails {
|
export interface MachineDetails {
|
||||||
type: 'machine';
|
type: 'machine';
|
||||||
name: string;
|
name: string;
|
||||||
|
iconKey?: string;
|
||||||
ip: string;
|
ip: string;
|
||||||
role: string;
|
role: string;
|
||||||
os: string;
|
os: string;
|
||||||
@@ -31,6 +32,7 @@ export interface MachineDetails {
|
|||||||
export interface DeviceDetails {
|
export interface DeviceDetails {
|
||||||
type: 'device';
|
type: 'device';
|
||||||
name: string;
|
name: string;
|
||||||
|
iconKey?: string;
|
||||||
ip: string;
|
ip: string;
|
||||||
deviceType: string;
|
deviceType: string;
|
||||||
notes?: string;
|
notes?: string;
|
||||||
@@ -40,6 +42,7 @@ export interface DeviceDetails {
|
|||||||
export interface VmDetails {
|
export interface VmDetails {
|
||||||
type: 'vm';
|
type: 'vm';
|
||||||
name: string;
|
name: string;
|
||||||
|
iconKey?: string;
|
||||||
ip: string;
|
ip: string;
|
||||||
role: string;
|
role: string;
|
||||||
hostName: string;
|
hostName: string;
|
||||||
@@ -55,6 +58,8 @@ export interface GraphNodeData {
|
|||||||
hostMachineId?: string;
|
hostMachineId?: string;
|
||||||
nodeWidth?: number;
|
nodeWidth?: number;
|
||||||
nodeHeight?: number;
|
nodeHeight?: number;
|
||||||
|
iconUrl?: string;
|
||||||
|
iconKey?: string;
|
||||||
details?: GraphNodeDetails;
|
details?: GraphNodeDetails;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { writable } from 'svelte/store';
|
||||||
|
|
||||||
|
export type ThemeMode = 'light' | 'dark';
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'ond-theme';
|
||||||
|
|
||||||
|
function getInitialTheme(): ThemeMode {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return 'light';
|
||||||
|
}
|
||||||
|
const stored = window.localStorage.getItem(STORAGE_KEY);
|
||||||
|
if (stored === 'light' || stored === 'dark') {
|
||||||
|
return stored;
|
||||||
|
}
|
||||||
|
return 'light';
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyTheme(theme: ThemeMode) {
|
||||||
|
if (typeof document === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
document.documentElement.setAttribute('data-theme', theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createThemeStore() {
|
||||||
|
const store = writable<ThemeMode>('light');
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe: store.subscribe,
|
||||||
|
initialize() {
|
||||||
|
const theme = getInitialTheme();
|
||||||
|
store.set(theme);
|
||||||
|
applyTheme(theme);
|
||||||
|
},
|
||||||
|
toggle() {
|
||||||
|
store.update((current) => {
|
||||||
|
const next = current === 'dark' ? 'light' : 'dark';
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.localStorage.setItem(STORAGE_KEY, next);
|
||||||
|
}
|
||||||
|
applyTheme(next);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
set(theme: ThemeMode) {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.localStorage.setItem(STORAGE_KEY, theme);
|
||||||
|
}
|
||||||
|
applyTheme(theme);
|
||||||
|
store.set(theme);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const themeMode = createThemeStore();
|
||||||
@@ -21,6 +21,7 @@ export interface VM {
|
|||||||
name: string;
|
name: string;
|
||||||
role: string;
|
role: string;
|
||||||
ipAddress: string;
|
ipAddress: string;
|
||||||
|
iconKey?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Software {
|
export interface Software {
|
||||||
@@ -32,6 +33,7 @@ export interface Machine {
|
|||||||
ipAddress: string;
|
ipAddress: string;
|
||||||
role: string;
|
role: string;
|
||||||
operatingSystem: string;
|
operatingSystem: string;
|
||||||
|
iconKey?: string;
|
||||||
software: Software;
|
software: Software;
|
||||||
hardware: Hardware;
|
hardware: Hardware;
|
||||||
ports?: Port[];
|
ports?: Port[];
|
||||||
@@ -41,6 +43,7 @@ export interface NetworkDevice {
|
|||||||
name: string;
|
name: string;
|
||||||
ipAddress: string;
|
ipAddress: string;
|
||||||
type: string;
|
type: string;
|
||||||
|
iconKey?: string;
|
||||||
notes?: string;
|
notes?: string;
|
||||||
ports?: Port[];
|
ports?: Port[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
|
||||||
|
<path d="M24 46h24a10 10 0 0 0 2-19 14 14 0 0 0-27-2 9 9 0 0 0 1 21z" fill="#0ea5e9"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 169 B |
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
|
||||||
|
<rect x="10" y="10" width="44" height="30" rx="4" fill="#7c3aed"/>
|
||||||
|
<rect x="24" y="42" width="16" height="6" rx="2" fill="#c4b5fd"/>
|
||||||
|
<rect x="18" y="50" width="28" height="4" rx="2" fill="#a78bfa"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 285 B |
@@ -0,0 +1,7 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
|
||||||
|
<rect x="9" y="24" width="46" height="20" rx="5" fill="#2563eb"/>
|
||||||
|
<path d="M18 20c4-4 8-6 14-6s10 2 14 6" stroke="#93c5fd" stroke-width="3" stroke-linecap="round"/>
|
||||||
|
<path d="M22 24c3-3 6-4 10-4s7 1 10 4" stroke="#bfdbfe" stroke-width="2.5" stroke-linecap="round"/>
|
||||||
|
<circle cx="20" cy="34" r="2" fill="#dbeafe"/>
|
||||||
|
<circle cx="28" cy="34" r="2" fill="#dbeafe"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 449 B |
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
|
||||||
|
<rect x="10" y="10" width="44" height="18" rx="4" fill="#0ea5e9"/>
|
||||||
|
<rect x="10" y="36" width="44" height="18" rx="4" fill="#0284c7"/>
|
||||||
|
<circle cx="18" cy="19" r="2" fill="#e0f2fe"/>
|
||||||
|
<circle cx="18" cy="45" r="2" fill="#e0f2fe"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 316 B |
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
|
||||||
|
<rect x="12" y="10" width="40" height="44" rx="6" fill="#16a34a"/>
|
||||||
|
<rect x="18" y="18" width="28" height="8" rx="2" fill="#dcfce7"/>
|
||||||
|
<rect x="18" y="30" width="28" height="8" rx="2" fill="#dcfce7"/>
|
||||||
|
<rect x="18" y="42" width="28" height="8" rx="2" fill="#dcfce7"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 353 B |
@@ -0,0 +1,11 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
|
||||||
|
<rect x="8" y="18" width="48" height="28" rx="6" fill="#f59e0b"/>
|
||||||
|
<rect x="14" y="26" width="8" height="4" rx="1" fill="#fff7ed"/>
|
||||||
|
<rect x="24" y="26" width="8" height="4" rx="1" fill="#fff7ed"/>
|
||||||
|
<rect x="34" y="26" width="8" height="4" rx="1" fill="#fff7ed"/>
|
||||||
|
<rect x="44" y="26" width="6" height="4" rx="1" fill="#fff7ed"/>
|
||||||
|
<rect x="14" y="34" width="8" height="4" rx="1" fill="#fff7ed"/>
|
||||||
|
<rect x="24" y="34" width="8" height="4" rx="1" fill="#fff7ed"/>
|
||||||
|
<rect x="34" y="34" width="8" height="4" rx="1" fill="#fff7ed"/>
|
||||||
|
<rect x="44" y="34" width="6" height="4" rx="1" fill="#fff7ed"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 684 B |
Reference in New Issue
Block a user