954 lines
64 KiB
Plaintext
954 lines
64 KiB
Plaintext
|
|
<!DOCTYPE html>
|
|||
|
|
<html lang="nl">
|
|||
|
|
<head>
|
|||
|
|
<meta charset="UTF-8">
|
|||
|
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|||
|
|
<title><%= diagram ? diagram.name : 'Nieuw Diagram' %> — Mek-Tech Architectuur</title>
|
|||
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
|
|||
|
|
<link rel="stylesheet" href="/css/style.css">
|
|||
|
|
<link rel="stylesheet" href="/css/neon.css">
|
|||
|
|
<link rel="stylesheet" href="/css/dynamic.css">
|
|||
|
|
<link rel="stylesheet" href="/css/arch-editor.css">
|
|||
|
|
<link rel="icon" href="/img/favicon.svg" type="image/svg+xml">
|
|||
|
|
<script src="https://unpkg.com/lucide@latest"></script>
|
|||
|
|
</head>
|
|||
|
|
<body class="arch-editor<%= (typeof embed !== 'undefined' && embed) ? ' embed-mode' : '' %><%= (typeof rack !== 'undefined' && rack && !(typeof embed !== 'undefined' && embed)) ? ' has-rack-ws-nav' : '' %>">
|
|||
|
|
<% if (typeof rack !== 'undefined' && rack && !(typeof embed !== 'undefined' && embed)) { %>
|
|||
|
|
<%- include('partials/rack-workspace-nav', { rack, flowDiagram, landscapeDiagram, workspaceTab: typeof workspaceTab !== 'undefined' ? workspaceTab : 'flow' }) %>
|
|||
|
|
<% } %>
|
|||
|
|
<header class="arch-topbar">
|
|||
|
|
<a href="/architecture" class="brand">
|
|||
|
|
<img src="/img/logo.svg" alt="Mek-Tech">
|
|||
|
|
<span>Architectuur Designer<em>v2</em></span>
|
|||
|
|
</a>
|
|||
|
|
<div class="arch-topbar-actions">
|
|||
|
|
<a href="/architecture"><i data-lucide="layers" style="width:14px;height:14px"></i> Diagrammen</a>
|
|||
|
|
<a href="/"><i data-lucide="layout-dashboard" style="width:14px;height:14px"></i> Dashboard</a>
|
|||
|
|
<a href="/auth/logout" style="color:#ff6b8a"><i data-lucide="log-out" style="width:14px;height:14px"></i></a>
|
|||
|
|
</div>
|
|||
|
|
</header>
|
|||
|
|
|
|||
|
|
<% if (typeof rack !== 'undefined' && rack) { %>
|
|||
|
|
<div class="rack-context-bar" style="background:linear-gradient(90deg,rgba(0,212,255,.12),rgba(168,85,247,.08));border-bottom:1px solid rgba(0,212,255,.25);padding:.55rem 1rem;display:flex;align-items:center;justify-content:space-between;gap:1rem;flex-wrap:wrap">
|
|||
|
|
<div style="font-size:.75rem;color:#fff">
|
|||
|
|
<strong style="color:var(--accent)">Rack:</strong> <%= rack.name %> · <%= rack.client_name %>
|
|||
|
|
<span style="color:rgba(255,255,255,.7);margin-left:.5rem"><%= (typeof rackDevices !== 'undefined' ? rackDevices.length : 0) %> devices gekoppeld</span>
|
|||
|
|
</div>
|
|||
|
|
<div style="display:flex;gap:.35rem;flex-wrap:wrap">
|
|||
|
|
<a href="/racks/<%= rack.id %>" class="btn btn-pri" style="font-size:.65rem;padding:.35rem .75rem">← Terug naar Rack</a>
|
|||
|
|
<a href="/racks/<%= rack.id %>/studio" class="btn btn-sec" style="font-size:.65rem;padding:.35rem .75rem">Studio</a>
|
|||
|
|
<a href="/racks/<%= rack.id %>/report" class="btn btn-sec" style="font-size:.65rem;padding:.35rem .75rem">Rapport</a>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<% } %>
|
|||
|
|
|
|||
|
|
<div class="editor-layout">
|
|||
|
|
<div class="editor-sidebar">
|
|||
|
|
<div class="palette-header">
|
|||
|
|
<h3>Componenten</h3>
|
|||
|
|
<div class="palette-search"><input id="paletteSearch" placeholder="Zoek component..." oninput="filterPalette(this.value)"></div>
|
|||
|
|
<div class="palette-cats" id="paletteCats">
|
|||
|
|
<button class="pc-btn active" data-cat="" onclick="filterPaletteCat('')">Alles</button>
|
|||
|
|
<button class="pc-btn" data-cat="Data Bronnen" onclick="filterPaletteCat(this.dataset.cat)">Bron</button>
|
|||
|
|
<button class="pc-btn" data-cat="Ingest" onclick="filterPaletteCat(this.dataset.cat)">Pipeline</button>
|
|||
|
|
<button class="pc-btn" data-cat="Opslag" onclick="filterPaletteCat(this.dataset.cat)">Storage</button>
|
|||
|
|
<button class="pc-btn" data-cat="Microservices" onclick="filterPaletteCat(this.dataset.cat)">Apps</button>
|
|||
|
|
<button class="pc-btn" data-cat="API" onclick="filterPaletteCat(this.dataset.cat)">API</button>
|
|||
|
|
<button class="pc-btn" data-cat="Databases" onclick="filterPaletteCat(this.dataset.cat)">DB</button>
|
|||
|
|
<button class="pc-btn" data-cat="CI/CD" onclick="filterPaletteCat(this.dataset.cat)">DevOps</button>
|
|||
|
|
<button class="pc-btn" data-cat="Observability" onclick="filterPaletteCat(this.dataset.cat)">Observe</button>
|
|||
|
|
</div>
|
|||
|
|
<% if (typeof rack !== 'undefined' && rack) { %>
|
|||
|
|
<div class="rack-flow-filters" style="margin-top:.55rem;padding-top:.55rem;border-top:1px solid var(--border)">
|
|||
|
|
<div style="font-size:.58rem;color:var(--text-muted);text-transform:uppercase;letter-spacing:.04em;margin-bottom:.35rem;font-weight:700">Flow filters</div>
|
|||
|
|
<div class="palette-cats" style="margin-bottom:.4rem">
|
|||
|
|
<button type="button" class="pc-btn layer-btn active" data-layer="" onclick="filterPaletteLayer('')">Alle lagen</button>
|
|||
|
|
<button type="button" class="pc-btn layer-btn" data-layer="data" onclick="filterPaletteLayer('data')">Data</button>
|
|||
|
|
<button type="button" class="pc-btn layer-btn" data-layer="app" onclick="filterPaletteLayer('app')">App</button>
|
|||
|
|
<button type="button" class="pc-btn layer-btn" data-layer="integration" onclick="filterPaletteLayer('integration')">Integratie</button>
|
|||
|
|
<button type="button" class="pc-btn layer-btn" data-layer="infra" onclick="filterPaletteLayer('infra')">Infra</button>
|
|||
|
|
</div>
|
|||
|
|
<select id="flowProtocolDefault" style="width:100%;padding:.3rem .45rem;font-size:.68rem;background:var(--bg);border:1px solid var(--border);border-radius:6px;color:var(--text);margin-bottom:.4rem" onchange="setDefaultFlowProtocol(this.value)">
|
|||
|
|
<option value="">Standaard protocol: geen</option>
|
|||
|
|
<option value="sync">Sync (REST/RPC)</option>
|
|||
|
|
<option value="async">Async (message)</option>
|
|||
|
|
<option value="batch">Batch / ETL</option>
|
|||
|
|
<option value="stream">Stream / realtime</option>
|
|||
|
|
<option value="event">Event-driven</option>
|
|||
|
|
</select>
|
|||
|
|
<button type="button" class="btn btn-sec" style="width:100%;font-size:.65rem;padding:.35rem" onclick="importRackDevices()">
|
|||
|
|
<i data-lucide="download" style="width:12px;height:12px"></i> Import rack devices (<%= typeof rackDevices !== 'undefined' ? rackDevices.length : 0 %>)
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
<% } %>
|
|||
|
|
</div>
|
|||
|
|
<div class="palette-scroll" id="paletteScroll">
|
|||
|
|
<div class="cat-label">Data Bronnen</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="source-db"><span class="p-icon" style="background:#d2992233;color:#d29922">DB</span> Database</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="source-api"><span class="p-icon" style="background:#d2992233;color:#d29922">API</span> API Service</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="source-iot"><span class="p-icon" style="background:#d2992233;color:#d29922">IoT</span> IoT Sensor</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="source-stream"><span class="p-icon" style="background:#d2992233;color:#d29922">ST</span> Streaming</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="source-sap"><span class="p-icon" style="background:#0070d133;color:#0070d1">SAP</span> SAP HANA</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="source-oracle"><span class="p-icon" style="background:#f8000033;color:#f80000">OR</span> Oracle DB</div>
|
|||
|
|
|
|||
|
|
<div class="cat-label">Ingest & Pipeline</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="ingest-kafka"><span class="p-icon" style="background:#58a6ff33;color:#00d4ff">K</span> Kafka</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="ingest-airbyte"><span class="p-icon" style="background:#58a6ff33;color:#00d4ff">AB</span> Airbyte</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="ingest-flink"><span class="p-icon" style="background:#58a6ff33;color:#00d4ff">SF</span> Spark/Flink</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="ingest-cdc"><span class="p-icon" style="background:#58a6ff33;color:#00d4ff">CDC</span> Debezium/CDC</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="ingest-nifi"><span class="p-icon" style="background:#58a6ff33;color:#00d4ff">NF</span> NiFi</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="ingest-dms"><span class="p-icon" style="background:#58a6ff33;color:#00d4ff">DMS</span> AWS DMS</div>
|
|||
|
|
|
|||
|
|
<div class="cat-label">Opslag & Storage</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="storage-minio"><span class="p-icon" style="background:#3fb95033;color:#3fb950">M</span> MinIO / S3</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="storage-iceberg"><span class="p-icon" style="background:#3fb95033;color:#3fb950">IC</span> Iceberg</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="storage-warehouse"><span class="p-icon" style="background:#3fb95033;color:#3fb950">DW</span> Data Warehouse</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="storage-lake"><span class="p-icon" style="background:#3fb95033;color:#3fb950">LH</span> Data Lakehouse</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="storage-nimble"><span class="p-icon" style="background:#ff7b0033;color:#ff7b00">HN</span> HPE Nimble</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="storage-3par"><span class="p-icon" style="background:#ff7b0033;color:#ff7b00">3P</span> HPE 3PAR</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="storage-powerstore"><span class="p-icon" style="background:#0076a833;color:#0076a8">PS</span> Dell PowerStore</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="storage-powerscale"><span class="p-icon" style="background:#0076a833;color:#0076a8">PSC</span> Dell PowerScale</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="storage-netapp"><span class="p-icon" style="background:#00a1df33;color:#00a1df">NA</span> NetApp AFF/FAS</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="storage-pure"><span class="p-icon" style="background:#4a90d933;color:#4a90d9">PU</span> Pure Storage</div>
|
|||
|
|
|
|||
|
|
<div class="cat-label">Compute / Servers</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="hw-hpe-dl"><span class="p-icon" style="background:#ff7b0033;color:#ff7b00">HP</span> HPE ProLiant DL</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="hw-hpe-ml"><span class="p-icon" style="background:#ff7b0033;color:#ff7b00">HPM</span> HPE ProLiant ML</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="hw-hpe-synergy"><span class="p-icon" style="background:#ff7b0033;color:#ff7b00">SYN</span> HPE Synergy</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="hw-dell-pe"><span class="p-icon" style="background:#0076a833;color:#0076a8">DE</span> Dell PowerEdge</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="hw-dell-mx"><span class="p-icon" style="background:#0076a833;color:#0076a8">MX</span> Dell PowerEdge MX</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="hw-lenovo-ts"><span class="p-icon" style="background:#e41f3333;color:#e41f33">LE</span> Lenovo ThinkSystem</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="hw-lenovo-ta"><span class="p-icon" style="background:#e41f3333;color:#e41f33">TA</span> Lenovo ThinkAgile</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="hw-supermicro"><span class="p-icon" style="background:#e6a81733;color:#e6a817">SM</span> Supermicro SYS</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="hw-cisco-ucs"><span class="p-icon" style="background:#00529a33;color:#00529a">UCS</span> Cisco UCS</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="compute-vmware"><span class="p-icon" style="background:#a855f733;color:#a855f7">VM</span> VMware vSphere</div>
|
|||
|
|
|
|||
|
|
<div class="cat-label">Netwerk</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="net-catalyst"><span class="p-icon" style="background:#34d39933;color:#34d399">CAT</span> Cisco Catalyst</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="net-nexus"><span class="p-icon" style="background:#34d39933;color:#34d399">NX</span> Cisco Nexus</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="net-junper-ex"><span class="p-icon" style="background:#34d39933;color:#34d399">JEX</span> Juniper EX</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="net-junper-qfx"><span class="p-icon" style="background:#34d39933;color:#34d399">JQ</span> Juniper QFX</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="net-junper-srx"><span class="p-icon" style="background:#34d39933;color:#34d399">SRX</span> Juniper SRX</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="net-arista"><span class="p-icon" style="background:#34d39933;color:#34d399">AR</span> Arista EOS</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="net-fortinet"><span class="p-icon" style="background:#34d39933;color:#34d399">FG</span> Fortinet FortiGate</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="net-f5"><span class="p-icon" style="background:#34d39933;color:#34d399">F5</span> F5 BIG-IP</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="net-paloalto"><span class="p-icon" style="background:#34d39933;color:#34d399">PA</span> Palo Alto</div>
|
|||
|
|
|
|||
|
|
<div class="cat-label">Compute / Data Platform</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="compute-spark"><span class="p-icon" style="background:#a855f733;color:#a855f7">SP</span> Apache Spark</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="compute-trino"><span class="p-icon" style="background:#a855f733;color:#a855f7">TR</span> Trino / Presto</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="compute-k8s"><span class="p-icon" style="background:#a855f733;color:#a855f7">K8</span> Kubernetes</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="compute-dbt"><span class="p-icon" style="background:#a855f733;color:#a855f7">DBT</span> dbt</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="compute-hadoop"><span class="p-icon" style="background:#a855f733;color:#a855f7">HD</span> Hadoop Cluster</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="compute-sap-hana"><span class="p-icon" style="background:#0070d133;color:#0070d1">H</span> SAP HANA Platform</div>
|
|||
|
|
|
|||
|
|
<div class="cat-label">AI / ML</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="ai-ollama"><span class="p-icon" style="background:#fb718533;color:#fb7185">O</span> Ollama / vLLM</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="ai-mlflow"><span class="p-icon" style="background:#fb718533;color:#fb7185">MF</span> MLflow</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="ai-vector"><span class="p-icon" style="background:#fb718533;color:#fb7185">VS</span> Vector Store</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="ai-model"><span class="p-icon" style="background:#fb718533;color:#fb7185">MS</span> Model Serving</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="ai-nvidia"><span class="p-icon" style="background:#76b90033;color:#76b900">NV</span> NVIDIA DGX</div>
|
|||
|
|
|
|||
|
|
<div class="cat-label">Microservices & Apps</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="app-nodejs"><span class="p-icon" style="background:#22c55e33;color:#22c55e">NJS</span> Node.js App</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="app-python"><span class="p-icon" style="background:#22c55e33;color:#22c55e">PY</span> Python App</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="app-java"><span class="p-icon" style="background:#22c55e33;color:#22c55e">JV</span> Java / Spring Boot</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="app-go"><span class="p-icon" style="background:#22c55e33;color:#22c55e">GO</span> Golang App</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="app-dotnet"><span class="p-icon" style="background:#22c55e33;color:#22c55e">DN</span> .NET App</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="app-react"><span class="p-icon" style="background:#22c55e33;color:#22c55e">RE</span> React / Frontend</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="app-microservice"><span class="p-icon" style="background:#22c55e33;color:#22c55e">μS</span> Microservice</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="app-container"><span class="p-icon" style="background:#22c55e33;color:#22c55e">CT</span> Container / Docker</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="app-serverless"><span class="p-icon" style="background:#22c55e33;color:#22c55e">λ</span> Serverless Function</div>
|
|||
|
|
|
|||
|
|
<div class="cat-label">API & Gateway</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="api-gateway"><span class="p-icon" style="background:#38bdf833;color:#38bdf8">GW</span> API Gateway</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="api-rest"><span class="p-icon" style="background:#38bdf833;color:#38bdf8">REST</span> REST API</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="api-graphql"><span class="p-icon" style="background:#38bdf833;color:#38bdf8">GQL</span> GraphQL API</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="api-grpc"><span class="p-icon" style="background:#38bdf833;color:#38bdf8">gRPC</span> gRPC Service</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="api-ingress"><span class="p-icon" style="background:#38bdf833;color:#38bdf8">IN</span> K8s Ingress</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="api-reverse-proxy"><span class="p-icon" style="background:#38bdf833;color:#38bdf8">RP</span> Reverse Proxy</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="api-service-mesh"><span class="p-icon" style="background:#38bdf833;color:#38bdf8">SM</span> Service Mesh</div>
|
|||
|
|
|
|||
|
|
<div class="cat-label">Messaging & Queue</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="queue-kafka"><span class="p-icon" style="background:#38bdf833;color:#38bdf8">KQ</span> Kafka / Confluent</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="queue-rabbit"><span class="p-icon" style="background:#38bdf833;color:#38bdf8">RM</span> RabbitMQ</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="queue-nats"><span class="p-icon" style="background:#38bdf833;color:#38bdf8">N</span> NATS / Pulsar</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="queue-redis"><span class="p-icon" style="background:#38bdf833;color:#38bdf8">RD</span> Redis Pub/Sub</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="queue-mqtt"><span class="p-icon" style="background:#38bdf833;color:#38bdf8">MQ</span> MQTT Broker</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="queue-sqs"><span class="p-icon" style="background:#38bdf833;color:#38bdf8">SQS</span> AWS SQS / SNS</div>
|
|||
|
|
|
|||
|
|
<div class="cat-label">Databases & Cache</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="db-postgres"><span class="p-icon" style="background:#d2992233;color:#d29922">PG</span> PostgreSQL</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="db-mysql"><span class="p-icon" style="background:#d2992233;color:#d29922">MY</span> MySQL / MariaDB</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="db-mongodb"><span class="p-icon" style="background:#d2992233;color:#d29922">MD</span> MongoDB</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="db-redis"><span class="p-icon" style="background:#d2992233;color:#d29922">RE</span> Redis Cache</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="db-elastic"><span class="p-icon" style="background:#d2992233;color:#d29922">ES</span> Elasticsearch</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="db-cassandra"><span class="p-icon" style="background:#d2992233;color:#d29922">CA</span> Cassandra / Scylla</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="db-clickhouse"><span class="p-icon" style="background:#d2992233;color:#d29922">CH</span> ClickHouse</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="db-dynamodb"><span class="p-icon" style="background:#d2992233;color:#d29922">DY</span> DynamoDB / Cosmos</div>
|
|||
|
|
|
|||
|
|
<div class="cat-label">CI/CD & DevOps</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="devops-github"><span class="p-icon" style="background:#a855f733;color:#a855f7">GH</span> GitHub Actions</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="devops-gitlab"><span class="p-icon" style="background:#a855f733;color:#a855f7">GL</span> GitLab CI/CD</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="devops-jenkins"><span class="p-icon" style="background:#a855f733;color:#a855f7">JK</span> Jenkins</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="devops-argocd"><span class="p-icon" style="background:#a855f733;color:#a855f7">AR</span> ArgoCD</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="devops-terraform"><span class="p-icon" style="background:#a855f733;color:#a855f7">TF</span> Terraform</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="devops-docker"><span class="p-icon" style="background:#a855f733;color:#a855f7">DK</span> Docker Registry</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="devops-helm"><span class="p-icon" style="background:#a855f733;color:#a855f7">HL</span> Helm Charts</div>
|
|||
|
|
|
|||
|
|
<div class="cat-label">Observability</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="obs-prometheus"><span class="p-icon" style="background:#f472b633;color:#f472b6">PR</span> Prometheus</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="obs-grafana"><span class="p-icon" style="background:#f472b633;color:#f472b6">GF</span> Grafana</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="obs-loki"><span class="p-icon" style="background:#f472b633;color:#f472b6">LO</span> Loki / Logs</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="obs-tempo"><span class="p-icon" style="background:#f472b633;color:#f472b6">TP</span> Tempo / Tracing</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="obs-datadog"><span class="p-icon" style="background:#f472b633;color:#f472b6">DD</span> Datadog</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="obs-opentelemetry"><span class="p-icon" style="background:#f472b633;color:#f472b6">OT</span> OpenTelemetry</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="obs-elk"><span class="p-icon" style="background:#f472b633;color:#f472b6">ELK</span> ELK Stack</div>
|
|||
|
|
|
|||
|
|
<div class="cat-label">Security</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="security-iam"><span class="p-icon" style="background:#f472b633;color:#f472b6">IAM</span> IAM / Auth</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="security-vault"><span class="p-icon" style="background:#f472b633;color:#f472b6">V</span> HashiCorp Vault</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="security-grafana"><span class="p-icon" style="background:#f472b633;color:#f472b6">G</span> Grafana</div>
|
|||
|
|
|
|||
|
|
<div class="cat-label">Infrastructure</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="infra-rack"><span class="p-icon" style="background:#6e768133;color:#6e7681">RK</span> Server Rack</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="infra-apc-ups"><span class="p-icon" style="background:#6e768133;color:#6e7681">UPS</span> APC UPS</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="infra-pdu"><span class="p-icon" style="background:#6e768133;color:#6e7681">PDU</span> PDU / Stroom</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="infra-cooling"><span class="p-icon" style="background:#6e768133;color:#6e7681">AC</span> Koeling / AC</div>
|
|||
|
|
<div class="palette-item" draggable="true" data-type="infra-patch-panel"><span class="p-icon" style="background:#6e768133;color:#6e7681">PP</span> Patch Panel</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="node-count" id="nodeCount">0 componenten · 0 connecties</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="editor-main">
|
|||
|
|
<div class="editor-toolbar">
|
|||
|
|
<div class="toolbar-group meta">
|
|||
|
|
<input class="ed-name" id="diagramName" placeholder="Diagram naam..." value="<%= diagram ? diagram.name : '' %>">
|
|||
|
|
<select id="diagramType">
|
|||
|
|
<option value="data_architecture" <%= (diagram && diagram.diagram_type === 'data_architecture') || (!diagram && defaultDiagramType === 'data_architecture') ? 'selected' : '' %>>Data Architectuur</option>
|
|||
|
|
<option value="network_topology" <%= (diagram && diagram.diagram_type === 'network_topology') || (!diagram && defaultDiagramType === 'network_topology') ? 'selected' : '' %>>Netwerk Topologie</option>
|
|||
|
|
<option value="system_architecture" <%= (diagram && diagram.diagram_type === 'system_architecture') || (!diagram && defaultDiagramType === 'system_architecture') ? 'selected' : '' %>>Systeem Architectuur</option>
|
|||
|
|
<option value="application_flow" <%= (diagram && diagram.diagram_type === 'application_flow') || (!diagram && defaultDiagramType === 'application_flow') ? 'selected' : '' %>>Applicatie Flow</option>
|
|||
|
|
<option value="application_landscape" <%= (diagram && diagram.diagram_type === 'application_landscape') || (!diagram && defaultDiagramType === 'application_landscape') ? 'selected' : '' %>>Applicatie Landschap</option>
|
|||
|
|
</select>
|
|||
|
|
<select id="diagramClient">
|
|||
|
|
<option value="">Cliënt...</option>
|
|||
|
|
<% clients.forEach(function(c) { %>
|
|||
|
|
<option value="<%= c.id %>" <%= (diagram && diagram.client_id == c.id) || clientId == c.id ? 'selected' : '' %>><%= c.name %></option>
|
|||
|
|
<% }) %>
|
|||
|
|
</select>
|
|||
|
|
</div>
|
|||
|
|
<div class="tb-sep"></div>
|
|||
|
|
<div class="toolbar-group">
|
|||
|
|
<button class="btn btn-sec btn-icon" onclick="undoAction()" title="Ongedaan maken">↩</button>
|
|||
|
|
<button class="btn btn-sec btn-icon" onclick="redoAction()" title="Opnieuw">↪</button>
|
|||
|
|
</div>
|
|||
|
|
<div class="tb-sep"></div>
|
|||
|
|
<div class="toolbar-group flow-toolbar" id="defaultFlowToolbar">
|
|||
|
|
<span class="flow-label">Nieuwe lijnen:</span>
|
|||
|
|
<button type="button" class="flow-chip" data-flow="off" onclick="setDefaultFlowMode('off')" title="Statische lijnen">Uit</button>
|
|||
|
|
<button type="button" class="flow-chip" data-flow="flow" onclick="setDefaultFlowMode('flow')" title="Stromende stippellijn">Flow</button>
|
|||
|
|
<button type="button" class="flow-chip" data-flow="pulse" onclick="setDefaultFlowMode('pulse')" title="Pulserende gloed">Puls</button>
|
|||
|
|
<button type="button" class="flow-chip active" data-flow="both" onclick="setDefaultFlowMode('both')" title="Flow + puls">Beide</button>
|
|||
|
|
</div>
|
|||
|
|
<div class="tb-sep"></div>
|
|||
|
|
<div class="toolbar-group">
|
|||
|
|
<button class="btn btn-pri" onclick="saveDiagram()">Opslaan</button>
|
|||
|
|
<button class="btn btn-sec" onclick="exportJSON()">JSON</button>
|
|||
|
|
<button class="btn btn-sec btn-icon" onclick="fitToScreen()" title="Passend maken">⊞</button>
|
|||
|
|
<button class="btn btn-sec btn-icon" onclick="toggleGrid()" title="Raster">⊟</button>
|
|||
|
|
<button class="btn btn-sec btn-danger" onclick="clearCanvas()" title="Wissen">× Wis</button>
|
|||
|
|
</div>
|
|||
|
|
<div class="tb-sep"></div>
|
|||
|
|
<a href="<%= (typeof rack !== 'undefined' && rack) ? '/racks/' + rack.id + '/report' : '/architecture/report/' + (diagram ? diagram.id : 0) %>" class="btn btn-sec" title="Rapport genereren">📄 Rapport</a>
|
|||
|
|
<span class="status-msg" id="statusMsg"></span>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="canvas-wrap" id="canvasWrap">
|
|||
|
|
<div class="canvas-hint" id="canvasHint">
|
|||
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><path d="M17.5 14v7M14 17.5h7"/></svg>
|
|||
|
|
<p>Sleep componenten hierheen</p>
|
|||
|
|
<small>Verbind nodes via de poorten aan de zijkanten</small>
|
|||
|
|
</div>
|
|||
|
|
<div class="viewport" id="viewport">
|
|||
|
|
<svg class="connections" id="svgConnections">
|
|||
|
|
<defs>
|
|||
|
|
<marker id="arrowhead" markerWidth="10" markerHeight="7" refX="10" refY="3.5" orient="auto"><polygon points="0 0,10 3.5,0 7" fill="var(--accent)"/></marker>
|
|||
|
|
<marker id="arrowhead-glow" markerWidth="14" markerHeight="10" refX="12" refY="5" orient="auto"><polygon points="0 0,12 5,0 10" fill="#00d4ff"/></marker>
|
|||
|
|
</defs>
|
|||
|
|
</svg>
|
|||
|
|
<div id="canvas" style="position:absolute;top:0;left:0;width:100%;height:100%"></div>
|
|||
|
|
</div>
|
|||
|
|
<div class="zoom-indicator" id="zoomIndicator">100%</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="props-panel" id="propsPanel">
|
|||
|
|
<h3 id="propsTitle">Eigenschappen</h3>
|
|||
|
|
<div class="props-body">
|
|||
|
|
<div id="propsNode">
|
|||
|
|
<div class="pg"><label>Label</label><input id="pLabel" onchange="updateProp('label',this.value)"></div>
|
|||
|
|
<div class="pg"><label>Subtitel</label><input id="pSub" onchange="updateProp('sub',this.value)"></div>
|
|||
|
|
<div class="pg"><label>IP Adres</label><input id="pIp" onchange="updateProp('ip',this.value)" placeholder="10.0.0.1/24"></div>
|
|||
|
|
<div class="props-divider"></div>
|
|||
|
|
<div class="pg"><label>Vendor</label><select id="pVendor" onchange="updateProp('vendor',this.value)"></select></div>
|
|||
|
|
<div class="pg"><label>Model</label><input id="pModel" onchange="updateProp('model',this.value)" placeholder="ProLiant DL380 Gen11"></div>
|
|||
|
|
<div class="pg"><label>Type</label><select id="pType" onchange="updateProp('type',this.value)"></select></div>
|
|||
|
|
<div class="pg"><label>Kleur</label><select id="pColor" onchange="updateProp('css',this.value)">
|
|||
|
|
<option value="node-source">Oranje (Bron)</option><option value="node-ingest">Blauw (Ingest)</option>
|
|||
|
|
<option value="node-storage">Groen (Opslag)</option><option value="node-compute">Paars (Compute)</option>
|
|||
|
|
<option value="node-ai">Rood (AI/ML)</option><option value="node-queue">Cyaan (Queue)</option>
|
|||
|
|
<option value="node-network">Mint (Netwerk)</option><option value="node-security">Roze (Security)</option>
|
|||
|
|
<option value="node-vendor">Oranje (Vendor)</option><option value="node-sap">Blauw (SAP)</option>
|
|||
|
|
<option value="node-db">Rood (Database)</option><option value="node-hw">Oranje (Hardware)</option>
|
|||
|
|
<option value="node-hybrid">Cyaan (Hybrid)</option>
|
|||
|
|
</select></div>
|
|||
|
|
<div class="pg"><label>Breedte</label><input id="pWidth" type="number" min="100" max="500" onchange="updateProp('w',+this.value)"></div>
|
|||
|
|
<div class="props-divider"></div>
|
|||
|
|
<div class="pg"><label>Specs / Config</label><textarea id="pSpecs" onchange="updateProp('specs',this.value)" placeholder="CPU: Xeon 16C RAM: 512GB Opslag: 4x 3.84TB NVMe Poorten: 4x 25GbE"></textarea></div>
|
|||
|
|
<div class="pg"><label>Notities</label><textarea id="pNotes" onchange="updateProp('notes',this.value)" placeholder="Extra informatie..."></textarea></div>
|
|||
|
|
<div class="props-divider"></div>
|
|||
|
|
<button class="btn-delete-node" onclick="deleteSelected()">Verwijder node</button>
|
|||
|
|
</div>
|
|||
|
|
<div id="propsEdge" style="display:none">
|
|||
|
|
<div class="pg"><label>Connectie</label><div id="pEdgeRoute" class="edge-route-text">—</div></div>
|
|||
|
|
<div class="pg"><label>Flow protocol</label>
|
|||
|
|
<select id="pEdgeProtocol" onchange="setEdgeProtocol(this.value)">
|
|||
|
|
<option value="">—</option>
|
|||
|
|
<option value="sync">Sync</option>
|
|||
|
|
<option value="async">Async</option>
|
|||
|
|
<option value="batch">Batch</option>
|
|||
|
|
<option value="stream">Stream</option>
|
|||
|
|
<option value="event">Event</option>
|
|||
|
|
</select>
|
|||
|
|
</div>
|
|||
|
|
<div class="pg"><label>Animatie</label>
|
|||
|
|
<div class="flow-chips" id="edgeFlowChips">
|
|||
|
|
<button type="button" class="flow-chip" data-flow="off" onclick="setEdgeFlow('off')">Uit</button>
|
|||
|
|
<button type="button" class="flow-chip" data-flow="flow" onclick="setEdgeFlow('flow')">Flow</button>
|
|||
|
|
<button type="button" class="flow-chip" data-flow="pulse" onclick="setEdgeFlow('pulse')">Puls</button>
|
|||
|
|
<button type="button" class="flow-chip" data-flow="both" onclick="setEdgeFlow('both')">Beide</button>
|
|||
|
|
</div>
|
|||
|
|
<p class="flow-hint">Klik een lijn op het canvas om de animatie per connectie aan te passen.</p>
|
|||
|
|
</div>
|
|||
|
|
<div class="props-divider"></div>
|
|||
|
|
<button class="btn-delete-node" onclick="deleteSelectedEdge()">Verwijder connectie</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div id="diagramMeta" data-id="<%= diagram ? diagram.id : '' %>" data-rack-id="<%= (typeof rack !== 'undefined' && rack) ? rack.id : (diagram && diagram.rack_id ? diagram.rack_id : '') %>" style="display:none"></div>
|
|||
|
|
|
|||
|
|
<script>
|
|||
|
|
const DIAGRAM_ID = '<%= diagram ? diagram.id : '' %>';
|
|||
|
|
const RACK_ID = '<%= (typeof rack !== "undefined" && rack) ? rack.id : (diagram && diagram.rack_id ? diagram.rack_id : "") %>';
|
|||
|
|
const RACK_DEVICES = <%- JSON.stringify(typeof rackDevices !== 'undefined' ? rackDevices : []) %>;
|
|||
|
|
let defaultFlowProtocol = '';
|
|||
|
|
let _paletteLayer = '';
|
|||
|
|
let nodes = [], edges = [], selectedNodeId = null, selectedEdgeIdx = null;
|
|||
|
|
let defaultFlowMode = 'both';
|
|||
|
|
let dragNodeId = null, dragOffX = 0, dragOffY = 0;
|
|||
|
|
let isPanning = false, panStartX, panStartY, panOffX = 0, panOffY = 0;
|
|||
|
|
let connectingFrom = null, tempLine = null, zoomLevel = 1, showGrid = true;
|
|||
|
|
let nodeIdCounter = 0, undoStack = [], redoStack = [], isDirty = false;
|
|||
|
|
const wrap = document.getElementById('canvasWrap'), viewport = document.getElementById('viewport');
|
|||
|
|
const canvas = document.getElementById('canvas'), svg = document.getElementById('svgConnections');
|
|||
|
|
const propsEl = document.getElementById('propsPanel');
|
|||
|
|
const VENDORS = ['','HPE','Dell','Lenovo','Super micro','Cisco','Juniper','Arista','NetApp','Pure Storage','Fortinet','F5','Palo Alto','NVIDIA','APC','Supermicro','ORACLE','Custom'];
|
|||
|
|
|
|||
|
|
const NT = {
|
|||
|
|
'source-db':{l:'Database',s:'OLTP / Bron',c:'node-source',w:140,h:44},
|
|||
|
|
'source-api':{l:'API Service',s:'REST / gRPC',c:'node-source',w:140,h:44},
|
|||
|
|
'source-iot':{l:'IoT Sensor',s:'Device Data',c:'node-source',w:140,h:44},
|
|||
|
|
'source-stream':{l:'Streaming',s:'Real-time Data',c:'node-source',w:140,h:44},
|
|||
|
|
'source-sap':{l:'SAP HANA',s:'Enterprise Data',c:'node-sap',w:140,h:44},
|
|||
|
|
'source-oracle':{l:'Oracle DB',s:'Enterprise DB',c:'node-db',w:140,h:44},
|
|||
|
|
'ingest-kafka':{l:'Kafka',s:'Event Streaming',c:'node-ingest',w:130,h:44},
|
|||
|
|
'ingest-airbyte':{l:'Airbyte',s:'Data Integration',c:'node-ingest',w:130,h:44},
|
|||
|
|
'ingest-flink':{l:'Spark / Flink',s:'Stream Processing',c:'node-ingest',w:140,h:44},
|
|||
|
|
'ingest-cdc':{l:'Debezium / CDC',s:'Change Data Capture',c:'node-ingest',w:150,h:44},
|
|||
|
|
'ingest-nifi':{l:'Apache NiFi',s:'Data Flow',c:'node-ingest',w:130,h:44},
|
|||
|
|
'ingest-dms':{l:'AWS DMS',s:'Migration Service',c:'node-ingest',w:140,h:44},
|
|||
|
|
'storage-minio':{l:'MinIO / S3',s:'Object Storage',c:'node-storage',w:140,h:44},
|
|||
|
|
'storage-iceberg':{l:'Iceberg',s:'Table Format',c:'node-storage',w:130,h:44},
|
|||
|
|
'storage-warehouse':{l:'Data Warehouse',s:'Analytics',c:'node-storage',w:150,h:44},
|
|||
|
|
'storage-lake':{l:'Data Lakehouse',s:'Unified Analytics',c:'node-storage',w:155,h:44},
|
|||
|
|
'storage-nimble':{l:'HPE Nimble',s:'All-Flash Storage',c:'node-vendor',w:150,h:44},
|
|||
|
|
'storage-3par':{l:'HPE 3PAR',s:'Enterprise Storage',c:'node-vendor',w:140,h:44},
|
|||
|
|
'storage-powerstore':{l:'Dell PowerStore',s:'All-Flash',c:'node-hw',w:155,h:44},
|
|||
|
|
'storage-powerscale':{l:'Dell PowerScale',s:'Scale-Out NAS',c:'node-hw',w:160,h:44},
|
|||
|
|
'storage-netapp':{l:'NetApp AFF/FAS',s:'Hybrid Cloud',c:'node-vendor',w:155,h:44},
|
|||
|
|
'storage-pure':{l:'Pure Storage',s:'All-Flash Array',c:'node-vendor',w:155,h:44},
|
|||
|
|
'hw-hpe-dl':{l:'HPE ProLiant DL',s:'Rack Server',c:'node-hw',w:160,h:48},
|
|||
|
|
'hw-hpe-ml':{l:'HPE ProLiant ML',s:'Tower Server',c:'node-hw',w:160,h:48},
|
|||
|
|
'hw-hpe-synergy':{l:'HPE Synergy',s:'Composable Infra',c:'node-hw',w:160,h:48},
|
|||
|
|
'hw-dell-pe':{l:'Dell PowerEdge',s:'Rack Server',c:'node-hw',w:160,h:48},
|
|||
|
|
'hw-dell-mx':{l:'Dell PowerEdge MX',s:'Modular Infra',c:'node-hw',w:170,h:48},
|
|||
|
|
'hw-lenovo-ts':{l:'Lenovo ThinkSystem',s:'Enterprise Server',c:'node-hw',w:170,h:48},
|
|||
|
|
'hw-lenovo-ta':{l:'Lenovo ThinkAgile',s:'HCI Platform',c:'node-hw',w:170,h:48},
|
|||
|
|
'hw-supermicro':{l:'Supermicro SYS',s:'High-Density Server',c:'node-hw',w:160,h:48},
|
|||
|
|
'hw-cisco-ucs':{l:'Cisco UCS',s:'Unified Computing',c:'node-hw',w:155,h:48},
|
|||
|
|
'compute-vmware':{l:'VMware vSphere',s:'Virtualization',c:'node-compute',w:155,h:44},
|
|||
|
|
'net-catalyst':{l:'Cisco Catalyst',s:'Access / Core Switch',c:'node-network',w:155,h:44},
|
|||
|
|
'net-nexus':{l:'Cisco Nexus',s:'Datacenter Switch',c:'node-network',w:155,h:44},
|
|||
|
|
'net-junper-ex':{l:'Juniper EX',s:'Ethernet Switch',c:'node-network',w:150,h:44},
|
|||
|
|
'net-junper-qfx':{l:'Juniper QFX',s:'Spine / Leaf',c:'node-network',w:150,h:44},
|
|||
|
|
'net-junper-srx':{l:'Juniper SRX',s:'Security Gateway',c:'node-network',w:150,h:44},
|
|||
|
|
'net-arista':{l:'Arista EOS',s:'Datacenter Switch',c:'node-network',w:150,h:44},
|
|||
|
|
'net-fortinet':{l:'Fortinet FortiGate',s:'Next-Gen Firewall',c:'node-network',w:165,h:44},
|
|||
|
|
'net-f5':{l:'F5 BIG-IP',s:'Load Balancer / ADC',c:'node-network',w:160,h:44},
|
|||
|
|
'net-paloalto':{l:'Palo Alto',s:'Next-Gen Firewall',c:'node-network',w:155,h:44},
|
|||
|
|
'compute-spark':{l:'Apache Spark',s:'Distributed Compute',c:'node-compute',w:145,h:44},
|
|||
|
|
'compute-trino':{l:'Trino / Presto',s:'SQL Query Engine',c:'node-compute',w:150,h:44},
|
|||
|
|
'compute-k8s':{l:'Kubernetes',s:'Container Orchestr.',c:'node-compute',w:145,h:44},
|
|||
|
|
'compute-dbt':{l:'dbt',s:'Data Transform',c:'node-compute',w:130,h:44},
|
|||
|
|
'compute-hadoop':{l:'Hadoop Cluster',s:'Big Data Platform',c:'node-compute',w:155,h:44},
|
|||
|
|
'compute-sap-hana':{l:'SAP HANA',s:'In-Memory Platform',c:'node-sap',w:155,h:44},
|
|||
|
|
'ai-ollama':{l:'Ollama / vLLM',s:'LLM Serving',c:'node-ai',w:145,h:44},
|
|||
|
|
'ai-mlflow':{l:'MLflow',s:'ML Lifecycle',c:'node-ai',w:140,h:44},
|
|||
|
|
'ai-vector':{l:'Vector Store',s:'Embeddings',c:'node-ai',w:140,h:44},
|
|||
|
|
'ai-model':{l:'Model Serving',s:'Inference',c:'node-ai',w:145,h:44},
|
|||
|
|
'ai-nvidia':{l:'NVIDIA DGX',s:'AI Supercomputer',c:'node-ai',w:150,h:44},
|
|||
|
|
'queue-kafka':{l:'Kafka / Confluent',s:'Message Queue',c:'node-queue',w:150,h:44},
|
|||
|
|
'queue-rabbit':{l:'RabbitMQ',s:'Message Broker',c:'node-queue',w:140,h:44},
|
|||
|
|
'queue-nats':{l:'NATS / Pulsar',s:'Event Bus',c:'node-queue',w:150,h:44},
|
|||
|
|
'security-iam':{l:'IAM / Auth',s:'Identity & Access',c:'node-security',w:140,h:44},
|
|||
|
|
'security-vault':{l:'HashiCorp Vault',s:'Secrets Mgmt',c:'node-security',w:155,h:44},
|
|||
|
|
'security-grafana':{l:'Grafana',s:'Observability',c:'node-security',w:140,h:44},
|
|||
|
|
'infra-rack':{l:'Server Rack',s:'42U Rack Enclosure',c:'node-vendor',w:160,h:52},
|
|||
|
|
'infra-apc-ups':{l:'APC UPS',s:'Uninterruptible Power',c:'node-vendor',w:150,h:44},
|
|||
|
|
'infra-pdu':{l:'PDU',s:'Power Distribution',c:'node-vendor',w:140,h:42},
|
|||
|
|
'infra-cooling':{l:'Koeling / AC',s:'Rack Cooling',c:'node-vendor',w:140,h:42},
|
|||
|
|
'infra-patch-panel':{l:'Patch Panel',s:'48-port Keystone',c:'node-vendor',w:150,h:40},
|
|||
|
|
|
|||
|
|
// Software & Apps
|
|||
|
|
'app-nodejs':{l:'Node.js App',s:'JavaScript Runtime',c:'node-compute',w:145,h:44},
|
|||
|
|
'app-python':{l:'Python App',s:'Python Runtime',c:'node-compute',w:145,h:44},
|
|||
|
|
'app-java':{l:'Java / Spring Boot',s:'JVM Runtime',c:'node-compute',w:155,h:44},
|
|||
|
|
'app-go':{l:'Golang App',s:'Go Runtime',c:'node-compute',w:140,h:44},
|
|||
|
|
'app-dotnet':{l:'.NET App',s:'.NET Runtime',c:'node-compute',w:140,h:44},
|
|||
|
|
'app-react':{l:'React / Frontend',s:'Web Frontend',c:'node-compute',w:150,h:44},
|
|||
|
|
'app-microservice':{l:'Microservice',s:'Service Component',c:'node-compute',w:150,h:44},
|
|||
|
|
'app-container':{l:'Container / Docker',s:'Container Runtime',c:'node-compute',w:160,h:44},
|
|||
|
|
'app-serverless':{l:'Serverless Function',s:'FaaS / Lambda',c:'node-compute',w:160,h:44},
|
|||
|
|
|
|||
|
|
// API & Gateway
|
|||
|
|
'api-gateway':{l:'API Gateway',s:'Traffic Management',c:'node-queue',w:145,h:44},
|
|||
|
|
'api-rest':{l:'REST API',s:'HTTP / JSON',c:'node-queue',w:135,h:44},
|
|||
|
|
'api-graphql':{l:'GraphQL API',s:'Query Language',c:'node-queue',w:145,h:44},
|
|||
|
|
'api-grpc':{l:'gRPC Service',s:'RPC Framework',c:'node-queue',w:145,h:44},
|
|||
|
|
'api-ingress':{l:'K8s Ingress',s:'External Access',c:'node-queue',w:140,h:44},
|
|||
|
|
'api-reverse-proxy':{l:'Reverse Proxy',s:'Load Balancing',c:'node-queue',w:145,h:44},
|
|||
|
|
'api-service-mesh':{l:'Service Mesh',s:'mTLS / Observability',c:'node-queue',w:155,h:44},
|
|||
|
|
|
|||
|
|
// More messaging
|
|||
|
|
'queue-redis':{l:'Redis Pub/Sub',s:'In-Memory Queue',c:'node-queue',w:145,h:44},
|
|||
|
|
'queue-mqtt':{l:'MQTT Broker',s:'IoT Protocol',c:'node-queue',w:140,h:44},
|
|||
|
|
'queue-sqs':{l:'AWS SQS / SNS',s:'Cloud Queue',c:'node-queue',w:145,h:44},
|
|||
|
|
|
|||
|
|
// Databases & Cache
|
|||
|
|
'db-postgres':{l:'PostgreSQL',s:'Relational DB',c:'node-db',w:145,h:44},
|
|||
|
|
'db-mysql':{l:'MySQL / MariaDB',s:'Relational DB',c:'node-db',w:150,h:44},
|
|||
|
|
'db-mongodb':{l:'MongoDB',s:'Document DB',c:'node-db',w:145,h:44},
|
|||
|
|
'db-redis':{l:'Redis Cache',s:'In-Memory Cache',c:'node-db',w:140,h:44},
|
|||
|
|
'db-elastic':{l:'Elasticsearch',s:'Search & Analytics',c:'node-db',w:155,h:44},
|
|||
|
|
'db-cassandra':{l:'Cassandra / Scylla',s:'Wide-Column DB',c:'node-db',w:160,h:44},
|
|||
|
|
'db-clickhouse':{l:'ClickHouse',s:'OLAP Database',c:'node-db',w:145,h:44},
|
|||
|
|
'db-dynamodb':{l:'DynamoDB / Cosmos',s:'NoSQL Cloud DB',c:'node-db',w:160,h:44},
|
|||
|
|
|
|||
|
|
// CI/CD & DevOps
|
|||
|
|
'devops-github':{l:'GitHub Actions',s:'CI/CD Pipeline',c:'node-queue',w:145,h:44},
|
|||
|
|
'devops-gitlab':{l:'GitLab CI/CD',s:'CI/CD Pipeline',c:'node-queue',w:145,h:44},
|
|||
|
|
'devops-jenkins':{l:'Jenkins',s:'Automation Server',c:'node-queue',w:135,h:44},
|
|||
|
|
'devops-argocd':{l:'ArgoCD',s:'GitOps Deployment',c:'node-queue',w:150,h:44},
|
|||
|
|
'devops-terraform':{l:'Terraform',s:'Infrastructure as Code',c:'node-queue',w:160,h:44},
|
|||
|
|
'devops-docker':{l:'Docker Registry',s:'Container Registry',c:'node-queue',w:155,h:44},
|
|||
|
|
'devops-helm':{l:'Helm Charts',s:'K8s Package Manager',c:'node-queue',w:155,h:44},
|
|||
|
|
|
|||
|
|
// Observability
|
|||
|
|
'obs-prometheus':{l:'Prometheus',s:'Metrics Collection',c:'node-security',w:150,h:44},
|
|||
|
|
'obs-grafana':{l:'Grafana',s:'Dashboard & Viz',c:'node-security',w:145,h:44},
|
|||
|
|
'obs-loki':{l:'Loki / Logs',s:'Log Aggregation',c:'node-security',w:145,h:44},
|
|||
|
|
'obs-tempo':{l:'Tempo / Tracing',s:'Distributed Tracing',c:'node-security',w:155,h:44},
|
|||
|
|
'obs-datadog':{l:'Datadog',s:'Monitoring SaaS',c:'node-security',w:145,h:44},
|
|||
|
|
'obs-opentelemetry':{l:'OpenTelemetry',s:'Telemetry Standard',c:'node-security',w:160,h:44},
|
|||
|
|
'obs-elk':{l:'ELK Stack',s:'Log Analysis',c:'node-security',w:140,h:44}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
function snap(v){return Math.round(v/20)*20}
|
|||
|
|
|
|||
|
|
function addNode(type,x,y,id,opt){
|
|||
|
|
const d=NT[type];if(!d)return null;
|
|||
|
|
const nid=id||(++nodeIdCounter);
|
|||
|
|
const el=document.createElement('div');
|
|||
|
|
el.className='canvas-node '+(opt&&opt.css?opt.css:d.c);
|
|||
|
|
el.dataset.id=nid;
|
|||
|
|
el.style.left=snap(x)+'px';el.style.top=snap(y)+'px';el.style.width=(opt&&opt.w?opt.w:d.w)+'px';
|
|||
|
|
const label=(opt&&opt.label)||d.l;
|
|||
|
|
const sub=(opt&&opt.sub)||d.s;
|
|||
|
|
const ip=(opt&&opt.ip)||'';
|
|||
|
|
const metaLine=ip?'<div class="node-meta">'+ip+'</div>':'';
|
|||
|
|
el.innerHTML='<div class="node-remove" onclick="removeNode('+nid+')">×</div>'+
|
|||
|
|
'<div class="node-port in" data-node="'+nid+'" data-port="in"></div>'+
|
|||
|
|
'<div class="node-label">'+label+'</div><div class="node-sub">'+sub+'</div>'+metaLine+
|
|||
|
|
'<div class="node-port out" data-node="'+nid+'" data-port="out"></div>';
|
|||
|
|
el.addEventListener('mousedown',e=>{
|
|||
|
|
if(e.target.closest('.node-remove')||e.target.closest('.node-port'))return;
|
|||
|
|
e.stopPropagation();selectNode(nid);
|
|||
|
|
const r=el.getBoundingClientRect();dragNodeId=nid;
|
|||
|
|
dragOffX=e.clientX-r.left;dragOffY=e.clientY-r.top;
|
|||
|
|
el.style.zIndex='20';el.style.cursor='grabbing';saveState();
|
|||
|
|
});
|
|||
|
|
el.querySelectorAll('.node-port').forEach(p=>{
|
|||
|
|
p.addEventListener('mousedown',e=>{
|
|||
|
|
e.stopPropagation();e.preventDefault();
|
|||
|
|
if(p.dataset.port==='out'){
|
|||
|
|
connectingFrom={nodeId:parseInt(p.dataset.node),el:p};
|
|||
|
|
const r=p.getBoundingClientRect(),wr=viewport.getBoundingClientRect();
|
|||
|
|
tempLine=document.createElementNS('http://www.w3.org/2000/svg','line');
|
|||
|
|
tempLine.setAttribute('stroke','#00d4ff');tempLine.setAttribute('stroke-width','2.5');
|
|||
|
|
tempLine.setAttribute('stroke-dasharray','8,6');tempLine.setAttribute('opacity','0.85');
|
|||
|
|
if(defaultFlowMode==='flow'||defaultFlowMode==='both')tempLine.classList.add('conn-anim-flow');
|
|||
|
|
if(defaultFlowMode==='pulse'||defaultFlowMode==='both')tempLine.classList.add('conn-anim-pulse');
|
|||
|
|
tempLine.setAttribute('x1',(r.left-wr.left)/zoomLevel);tempLine.setAttribute('y1',(r.top-wr.top+5)/zoomLevel);
|
|||
|
|
tempLine.setAttribute('x2',(e.clientX-wr.left)/zoomLevel);tempLine.setAttribute('y2',(e.clientY-wr.top)/zoomLevel);
|
|||
|
|
svg.appendChild(tempLine);
|
|||
|
|
}});
|
|||
|
|
});
|
|||
|
|
canvas.appendChild(el);
|
|||
|
|
const nd={id:nid,type,x:snap(x),y:snap(y),label,sub,css:opt&&opt.css?opt.css:d.c,w:opt&&opt.w?opt.w:d.w,h:opt&&opt.h?opt.h:d.h,ip:ip||'',vendor:(opt&&opt.vendor)||'',model:(opt&&opt.model)||'',specs:(opt&&opt.specs)||'',notes:(opt&&opt.notes)||'',rack_device_id:opt&&opt.rack_device_id?opt.rack_device_id:null,layer:opt&&opt.layer?opt.layer:inferLayerFromType(type)};
|
|||
|
|
nodes.push(nd);updateStats();return nid;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function inferLayerFromType(type){
|
|||
|
|
if(!type)return 'other';
|
|||
|
|
if(type.startsWith('source-')||type.startsWith('ingest-')||type.startsWith('storage-')||type.startsWith('db-'))return 'data';
|
|||
|
|
if(type.startsWith('app-'))return 'app';
|
|||
|
|
if(type.startsWith('api-')||type.startsWith('queue-'))return 'integration';
|
|||
|
|
if(type.startsWith('hw-')||type.startsWith('net-')||type.startsWith('infra-'))return 'infra';
|
|||
|
|
return 'other';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function removeNode(id){
|
|||
|
|
const el=canvas.querySelector('.canvas-node[data-id="'+id+'"]');
|
|||
|
|
if(el)el.remove();
|
|||
|
|
nodes=nodes.filter(n=>n.id!==id);
|
|||
|
|
edges=edges.filter(e=>e.from!==id&&e.to!==id);
|
|||
|
|
renderEdges();
|
|||
|
|
if(selectedNodeId===id){selectedNodeId=null;if(selectedEdgeIdx===null)propsEl.classList.remove('open');}
|
|||
|
|
updateStats();saveState();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function normalizeEdge(e){return{from:e.from,to:e.to,flow:e.flow||'both',protocol:e.protocol||''};}
|
|||
|
|
|
|||
|
|
function setDefaultFlowProtocol(p){defaultFlowProtocol=p||'';}
|
|||
|
|
|
|||
|
|
function setEdgeProtocol(protocol){
|
|||
|
|
if(selectedEdgeIdx===null)return;
|
|||
|
|
edges[selectedEdgeIdx].protocol=protocol;
|
|||
|
|
saveState();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function setDefaultFlowMode(mode){
|
|||
|
|
defaultFlowMode=mode;
|
|||
|
|
document.querySelectorAll('#defaultFlowToolbar .flow-chip').forEach(b=>b.classList.toggle('active',b.dataset.flow===mode));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function setEdgeFlow(mode){
|
|||
|
|
if(selectedEdgeIdx===null)return;
|
|||
|
|
edges[selectedEdgeIdx].flow=mode;
|
|||
|
|
document.querySelectorAll('#edgeFlowChips .flow-chip').forEach(b=>b.classList.toggle('active',b.dataset.flow===mode));
|
|||
|
|
renderEdges();
|
|||
|
|
saveState();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function selectEdge(idx){
|
|||
|
|
selectedEdgeIdx=idx;
|
|||
|
|
selectedNodeId=null;
|
|||
|
|
canvas.querySelectorAll('.canvas-node').forEach(n=>n.classList.remove('selected'));
|
|||
|
|
svg.querySelectorAll('.conn-group').forEach(g=>g.classList.remove('selected'));
|
|||
|
|
const g=svg.querySelector('.conn-group[data-idx="'+idx+'"]');
|
|||
|
|
if(g)g.classList.add('selected');
|
|||
|
|
showEdgeProps(idx);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function showEdgeProps(idx){
|
|||
|
|
const e=edges[idx];if(!e)return;
|
|||
|
|
const from=nodes.find(n=>n.id===e.from),to=nodes.find(n=>n.id===e.to);
|
|||
|
|
document.getElementById('propsTitle').textContent='Connectie';
|
|||
|
|
document.getElementById('propsNode').style.display='none';
|
|||
|
|
document.getElementById('propsEdge').style.display='block';
|
|||
|
|
document.getElementById('pEdgeRoute').textContent=(from?from.label:'?')+' → '+(to?to.label:'?');
|
|||
|
|
document.getElementById('pEdgeProtocol').value=e.protocol||'';
|
|||
|
|
document.querySelectorAll('#edgeFlowChips .flow-chip').forEach(b=>b.classList.toggle('active',b.dataset.flow===(e.flow||'both')));
|
|||
|
|
propsEl.classList.add('open');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function deselectAll(){
|
|||
|
|
selectedNodeId=null;
|
|||
|
|
selectedEdgeIdx=null;
|
|||
|
|
canvas.querySelectorAll('.canvas-node').forEach(n=>n.classList.remove('selected'));
|
|||
|
|
svg.querySelectorAll('.conn-group').forEach(g=>g.classList.remove('selected'));
|
|||
|
|
propsEl.classList.remove('open');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function deleteSelectedEdge(){
|
|||
|
|
if(selectedEdgeIdx===null)return;
|
|||
|
|
edges.splice(selectedEdgeIdx,1);
|
|||
|
|
selectedEdgeIdx=null;
|
|||
|
|
propsEl.classList.remove('open');
|
|||
|
|
renderEdges();
|
|||
|
|
updateStats();
|
|||
|
|
saveState();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function nodeLabel(id){const n=nodes.find(nn=>nn.id===id);return n?n.label:'Node '+id;}
|
|||
|
|
|
|||
|
|
function selectNode(id){
|
|||
|
|
selectedEdgeIdx=null;
|
|||
|
|
svg.querySelectorAll('.conn-group').forEach(g=>g.classList.remove('selected'));
|
|||
|
|
canvas.querySelectorAll('.canvas-node').forEach(n=>n.classList.remove('selected'));
|
|||
|
|
const el=canvas.querySelector('.canvas-node[data-id="'+id+'"]');
|
|||
|
|
if(el)el.classList.add('selected');
|
|||
|
|
selectedNodeId=id;showProps(id);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function showProps(id){
|
|||
|
|
const n=nodes.find(nn=>nn.id===id);if(!n)return;
|
|||
|
|
document.getElementById('propsTitle').textContent='Eigenschappen';
|
|||
|
|
document.getElementById('propsNode').style.display='block';
|
|||
|
|
document.getElementById('propsEdge').style.display='none';
|
|||
|
|
propsEl.classList.add('open');
|
|||
|
|
document.getElementById('pLabel').value=n.label||'';
|
|||
|
|
document.getElementById('pSub').value=n.sub||'';
|
|||
|
|
document.getElementById('pIp').value=n.ip||'';
|
|||
|
|
document.getElementById('pWidth').value=n.w||140;
|
|||
|
|
document.getElementById('pColor').value=n.css||'node-source';
|
|||
|
|
document.getElementById('pSpecs').value=n.specs||'';
|
|||
|
|
document.getElementById('pNotes').value=n.notes||'';
|
|||
|
|
const vSel=document.getElementById('pVendor');
|
|||
|
|
vSel.innerHTML='';VENDORS.forEach(v=>{const o=document.createElement('option');o.value=v;o.textContent=v||'— Vendor —';if(v===(n.vendor||''))o.selected=true;vSel.appendChild(o);});
|
|||
|
|
const mInp=document.getElementById('pModel');mInp.value=n.model||'';
|
|||
|
|
const tSel=document.getElementById('pType');
|
|||
|
|
tSel.innerHTML='';Object.keys(NT).forEach(k=>{const o=document.createElement('option');o.value=k;o.textContent=NT[k].l;if(k===n.type)o.selected=true;tSel.appendChild(o);});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function updateProp(field,value){
|
|||
|
|
const n=nodes.find(nn=>nn.id===selectedNodeId);if(!n)return;
|
|||
|
|
const el=canvas.querySelector('.canvas-node[data-id="'+selectedNodeId+'"]');if(!el)return;
|
|||
|
|
if(field==='label'){n.label=value;el.querySelector('.node-label').textContent=value;if(selectedEdgeIdx!==null)showEdgeProps(selectedEdgeIdx);}
|
|||
|
|
else if(field==='sub'){n.sub=value;el.querySelector('.node-sub').textContent=value;}
|
|||
|
|
else if(field==='ip'){n.ip=value;let m=el.querySelector('.node-meta');if(m){m.textContent=value;}else if(value){const d=document.createElement('div');d.className='node-meta';d.textContent=value;el.querySelector('.node-sub').after(d);}}
|
|||
|
|
else if(field==='w'){n.w=value;el.style.width=value+'px';}
|
|||
|
|
else if(field==='css'){n.css=value;el.className='canvas-node '+value;}
|
|||
|
|
else if(field==='vendor'){n.vendor=value;}
|
|||
|
|
else if(field==='model'){n.model=value;}
|
|||
|
|
else if(field==='specs'){n.specs=value;}
|
|||
|
|
else if(field==='notes'){n.notes=value;}
|
|||
|
|
else if(field==='type'){
|
|||
|
|
const d=NT[value];if(d){n.type=value;n.css=d.c;n.w=d.w;n.h=d.h;
|
|||
|
|
el.style.width=d.w+'px';el.className='canvas-node '+d.c;
|
|||
|
|
el.querySelector('.node-label').textContent=d.l;el.querySelector('.node-sub').textContent=d.s;
|
|||
|
|
document.getElementById('pLabel').value=d.l;document.getElementById('pSub').value=d.s;
|
|||
|
|
document.getElementById('pWidth').value=d.w;document.getElementById('pColor').value=d.c;
|
|||
|
|
}}renderEdges();saveState();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function deleteSelected(){
|
|||
|
|
if(selectedEdgeIdx!==null){deleteSelectedEdge();return;}
|
|||
|
|
if(selectedNodeId)removeNode(selectedNodeId);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function addEdge(from,to){
|
|||
|
|
if(from===to)return;
|
|||
|
|
if(edges.some(e=>e.from===from&&e.to===to))return;
|
|||
|
|
edges.push(normalizeEdge({from,to,flow:defaultFlowMode,protocol:defaultFlowProtocol}));
|
|||
|
|
renderEdges();
|
|||
|
|
selectEdge(edges.length-1);
|
|||
|
|
updateStats();
|
|||
|
|
saveState();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function renderEdges(){
|
|||
|
|
svg.querySelectorAll('.conn-group').forEach(g=>g.remove());
|
|||
|
|
const wr=viewport.getBoundingClientRect();
|
|||
|
|
edges.forEach((e,idx)=>{
|
|||
|
|
e=normalizeEdge(e);edges[idx]=e;
|
|||
|
|
const flow=e.flow;
|
|||
|
|
const fEl=canvas.querySelector('.canvas-node[data-id="'+e.from+'"]');
|
|||
|
|
const tEl=canvas.querySelector('.canvas-node[data-id="'+e.to+'"]');
|
|||
|
|
if(!fEl||!tEl)return;
|
|||
|
|
const fr=fEl.getBoundingClientRect(),tr=tEl.getBoundingClientRect();
|
|||
|
|
const x1=(fr.right-wr.left-8)/zoomLevel,y1=(fr.top+fr.height/2-wr.top)/zoomLevel;
|
|||
|
|
const x2=(tr.left-wr.left+8)/zoomLevel,y2=(tr.top+tr.height/2-wr.top)/zoomLevel;
|
|||
|
|
const dx=Math.abs(x2-x1),cp=Math.max(50,dx*0.4);
|
|||
|
|
const d='M '+x1+' '+y1+' C '+(x1+cp)+' '+y1+', '+(x2-cp)+' '+y2+', '+x2+' '+y2;
|
|||
|
|
const g=document.createElementNS('http://www.w3.org/2000/svg','g');
|
|||
|
|
g.classList.add('conn-group');
|
|||
|
|
g.dataset.idx=idx;
|
|||
|
|
if(selectedEdgeIdx===idx)g.classList.add('selected');
|
|||
|
|
const glow=document.createElementNS('http://www.w3.org/2000/svg','path');
|
|||
|
|
glow.setAttribute('d',d);glow.setAttribute('stroke','var(--accent)');glow.setAttribute('stroke-width','6');
|
|||
|
|
glow.setAttribute('fill','none');glow.setAttribute('opacity','0.12');
|
|||
|
|
glow.style.pointerEvents='none';
|
|||
|
|
if(flow==='pulse'||flow==='both')glow.classList.add('conn-glow-pulse');
|
|||
|
|
g.appendChild(glow);
|
|||
|
|
const main=document.createElementNS('http://www.w3.org/2000/svg','path');
|
|||
|
|
main.setAttribute('d',d);main.setAttribute('stroke','var(--accent)');main.setAttribute('stroke-width','2.5');
|
|||
|
|
main.setAttribute('fill','none');main.setAttribute('stroke-linecap','round');
|
|||
|
|
main.classList.add('conn-path');main.dataset.idx=idx;
|
|||
|
|
main.addEventListener('click',ev=>{ev.stopPropagation();selectEdge(idx);});
|
|||
|
|
main.addEventListener('dblclick',ev=>{ev.stopPropagation();edges.splice(idx,1);selectedEdgeIdx=null;propsEl.classList.remove('open');renderEdges();updateStats();saveState();});
|
|||
|
|
main.addEventListener('mouseenter',()=>glow.setAttribute('opacity',flow==='off'?'0.2':'0.35'));
|
|||
|
|
main.addEventListener('mouseleave',()=>glow.setAttribute('opacity',flow==='off'?'0.12':'0.18'));
|
|||
|
|
g.appendChild(main);
|
|||
|
|
if(flow==='flow'||flow==='both'){
|
|||
|
|
const flow1=document.createElementNS('http://www.w3.org/2000/svg','path');
|
|||
|
|
flow1.setAttribute('d',d);flow1.setAttribute('stroke','#00d4ff');flow1.setAttribute('stroke-width','2');
|
|||
|
|
flow1.setAttribute('fill','none');flow1.setAttribute('stroke-dasharray','10,14');
|
|||
|
|
flow1.setAttribute('opacity','0.85');flow1.classList.add('conn-anim-flow');
|
|||
|
|
flow1.style.pointerEvents='none';
|
|||
|
|
g.appendChild(flow1);
|
|||
|
|
const flow2=document.createElementNS('http://www.w3.org/2000/svg','path');
|
|||
|
|
flow2.setAttribute('d',d);flow2.setAttribute('stroke','#66e5ff');flow2.setAttribute('stroke-width','1.5');
|
|||
|
|
flow2.setAttribute('fill','none');flow2.setAttribute('stroke-dasharray','6,18');
|
|||
|
|
flow2.setAttribute('opacity','0.55');flow2.classList.add('conn-anim-flow','conn-anim-flow-delay');
|
|||
|
|
flow2.style.pointerEvents='none';
|
|||
|
|
g.appendChild(flow2);
|
|||
|
|
}
|
|||
|
|
if(flow==='pulse'||flow==='both'){
|
|||
|
|
const pulse=document.createElementNS('http://www.w3.org/2000/svg','path');
|
|||
|
|
pulse.setAttribute('d',d);pulse.setAttribute('stroke','#00d4ff');pulse.setAttribute('stroke-width','3');
|
|||
|
|
pulse.setAttribute('fill','none');pulse.setAttribute('opacity','0.35');
|
|||
|
|
pulse.classList.add('conn-anim-pulse');
|
|||
|
|
pulse.style.pointerEvents='none';
|
|||
|
|
g.appendChild(pulse);
|
|||
|
|
}
|
|||
|
|
const angle=Math.atan2(y2-y1,x2-x1);
|
|||
|
|
const arrow=document.createElementNS('http://www.w3.org/2000/svg','polygon');
|
|||
|
|
arrow.setAttribute('points',(x2-5)+','+(y2-3)+' '+(x2-9*Math.cos(angle-0.35))+','+(y2-9*Math.sin(angle-0.35))+' '+(x2-9*Math.cos(angle+0.35))+','+(y2-9*Math.sin(angle+0.35)));
|
|||
|
|
arrow.setAttribute('fill',flow==='off'?'var(--accent)':'#00d4ff');
|
|||
|
|
if(flow==='pulse'||flow==='both')arrow.classList.add('conn-anim-pulse');
|
|||
|
|
arrow.style.pointerEvents='none';
|
|||
|
|
g.appendChild(arrow);
|
|||
|
|
svg.appendChild(g);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function saveState(){undoStack.push(JSON.stringify({nodes,edges}));if(undoStack.length>50)undoStack.shift();redoStack=[];isDirty=true;}
|
|||
|
|
function undoAction(){if(undoStack.length<2)return;redoStack.push(undoStack.pop());loadState(JSON.parse(undoStack[undoStack.length-1]));}
|
|||
|
|
function redoAction(){if(redoStack.length===0)return;undoStack.push(redoStack.pop());loadState(JSON.parse(undoStack[undoStack.length-1]));}
|
|||
|
|
function loadState(st){
|
|||
|
|
canvas.querySelectorAll('.canvas-node').forEach(el=>el.remove());svg.querySelectorAll('.conn-group').forEach(g=>g.remove());
|
|||
|
|
nodes=[];edges=(st.edges||[]).map(normalizeEdge);
|
|||
|
|
(st.nodes||[]).forEach(n=>addNode(n.type,n.x,n.y,n.id,n));
|
|||
|
|
renderEdges();updateStats();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
document.addEventListener('mousemove',e=>{
|
|||
|
|
if(dragNodeId!==null){
|
|||
|
|
const el=canvas.querySelector('.canvas-node[data-id="'+dragNodeId+'"]');if(!el)return;
|
|||
|
|
const wr=viewport.getBoundingClientRect();
|
|||
|
|
let x=(e.clientX-wr.left-dragOffX)/zoomLevel,y=(e.clientY-wr.top-dragOffY)/zoomLevel;
|
|||
|
|
x=snap(Math.max(0,x));y=snap(Math.max(0,y));
|
|||
|
|
el.style.left=x+'px';el.style.top=y+'px';
|
|||
|
|
const n=nodes.find(nn=>nn.id===dragNodeId);if(n){n.x=x;n.y=y;}
|
|||
|
|
renderEdges();
|
|||
|
|
}
|
|||
|
|
if(tempLine){const wr=viewport.getBoundingClientRect();tempLine.setAttribute('x2',(e.clientX-wr.left)/zoomLevel);tempLine.setAttribute('y2',(e.clientY-wr.top)/zoomLevel);}
|
|||
|
|
if(isPanning){const dx=e.clientX-panStartX,dy=e.clientY-panStartY;panOffX+=dx;panOffY+=dy;panStartX=e.clientX;panStartY=e.clientY;viewport.style.transform='translate('+panOffX+'px,'+panOffY+'px) scale('+zoomLevel+')';}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
document.addEventListener('mouseup',e=>{
|
|||
|
|
if(dragNodeId!==null){const el=canvas.querySelector('.canvas-node[data-id="'+dragNodeId+'"]');if(el)el.style.cursor='move';dragNodeId=null;}
|
|||
|
|
if(tempLine){tempLine.remove();tempLine=null;const target=e.target.closest('.node-port.in');if(target&&connectingFrom)addEdge(connectingFrom.nodeId,parseInt(target.dataset.node));connectingFrom=null;}
|
|||
|
|
if(isPanning){isPanning=false;wrap.style.cursor='grab';}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
wrap.addEventListener('mousedown',e=>{
|
|||
|
|
if(e.target.closest('.canvas-node'))return;
|
|||
|
|
if(e.target===wrap||e.target===viewport||e.target===canvas||e.target===svg){
|
|||
|
|
deselectAll();
|
|||
|
|
isPanning=true;panStartX=e.clientX;panStartY=e.clientY;wrap.style.cursor='grabbing';
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
wrap.addEventListener('wheel',e=>{
|
|||
|
|
e.preventDefault();
|
|||
|
|
const delta=e.deltaY>0?.9:1.1,newZoom=Math.max(.2,Math.min(3,zoomLevel*delta));
|
|||
|
|
const mx=e.clientX,my=e.clientY,wr=wrap.getBoundingClientRect();
|
|||
|
|
const vx=(mx-wr.left-panOffX)/zoomLevel,vy=(my-wr.top-panOffY)/zoomLevel;
|
|||
|
|
zoomLevel=newZoom;panOffX=mx-wr.left-vx*zoomLevel;panOffY=my-wr.top-vy*zoomLevel;
|
|||
|
|
viewport.style.transform='translate('+panOffX+'px,'+panOffY+'px) scale('+zoomLevel+')';
|
|||
|
|
document.getElementById('zoomIndicator').textContent=Math.round(zoomLevel*100)+'%';
|
|||
|
|
},{passive:false});
|
|||
|
|
|
|||
|
|
document.querySelectorAll('.palette-item[draggable]').forEach(item=>{
|
|||
|
|
item.addEventListener('dragstart',e=>{e.dataTransfer.setData('text/plain',item.dataset.type);e.dataTransfer.effectAllowed='copy';});
|
|||
|
|
});
|
|||
|
|
canvas.addEventListener('dragover',e=>{e.preventDefault();e.dataTransfer.dropEffect='copy';});
|
|||
|
|
canvas.addEventListener('drop',e=>{
|
|||
|
|
e.preventDefault();
|
|||
|
|
const type=e.dataTransfer.getData('text/plain');if(!type||!NT[type])return;
|
|||
|
|
const wr=viewport.getBoundingClientRect();
|
|||
|
|
let x=(e.clientX-wr.left)/zoomLevel-70,y=(e.clientY-wr.top)/zoomLevel-24;
|
|||
|
|
x=snap(Math.max(0,x));y=snap(Math.max(0,y));
|
|||
|
|
addNode(type,x,y);saveState();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
function updateStats(){
|
|||
|
|
document.getElementById('nodeCount').textContent=nodes.length+' componenten · '+edges.length+' connecties';
|
|||
|
|
const hint=document.getElementById('canvasHint');
|
|||
|
|
if(hint)hint.classList.toggle('hidden',nodes.length>0);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function filterPalette(q){
|
|||
|
|
document.getElementById('paletteSearch').value=q;
|
|||
|
|
applyPaletteFilters();
|
|||
|
|
document.querySelectorAll('.pc-btn:not(.layer-btn)').forEach(b=>b.classList.toggle('active',!b.dataset.cat));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var _paletteCat='';
|
|||
|
|
function filterPaletteCat(cat){
|
|||
|
|
_paletteCat=cat;
|
|||
|
|
document.querySelectorAll('.pc-btn:not(.layer-btn)').forEach(b=>b.classList.toggle('active',b.dataset.cat===cat));
|
|||
|
|
document.getElementById('paletteSearch').value='';
|
|||
|
|
applyPaletteFilters();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const LAYER_CATS={
|
|||
|
|
data:['data bron','ingest','opslag','database','compute / data'],
|
|||
|
|
app:['microservice','apps','ai / ml'],
|
|||
|
|
integration:['api','messaging','queue'],
|
|||
|
|
infra:['compute / server','netwerk','infrastructure','security','observability','ci/cd']
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
function inferLayerFromCat(cat){
|
|||
|
|
const c=(cat||'').toLowerCase();
|
|||
|
|
for(const [layer,keys] of Object.entries(LAYER_CATS)){
|
|||
|
|
if(keys.some(k=>c.includes(k)))return layer;
|
|||
|
|
}
|
|||
|
|
return 'other';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function applyPaletteFilters(){
|
|||
|
|
const q=(document.getElementById('paletteSearch').value||'').toLowerCase();
|
|||
|
|
document.querySelectorAll('.cat-label').forEach(el=>{
|
|||
|
|
const layer=inferLayerFromCat(el.textContent);
|
|||
|
|
let showCat=!_paletteLayer||_paletteLayer===layer||layer==='other';
|
|||
|
|
if(_paletteCat&&!el.textContent.trim().toLowerCase().includes(_paletteCat.toLowerCase()))showCat=false;
|
|||
|
|
el.style.display=showCat?'block':'none';
|
|||
|
|
let sib=el.nextElementSibling;
|
|||
|
|
while(sib&&sib.classList.contains('palette-item')){
|
|||
|
|
let showItem=showCat;
|
|||
|
|
if(q&&!sib.textContent.toLowerCase().includes(q))showItem=false;
|
|||
|
|
sib.style.display=showItem?'flex':'none';
|
|||
|
|
sib=sib.nextElementSibling;
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function filterPaletteLayer(layer){
|
|||
|
|
_paletteLayer=layer;
|
|||
|
|
document.querySelectorAll('.layer-btn').forEach(b=>b.classList.toggle('active',b.dataset.layer===layer));
|
|||
|
|
applyPaletteFilters();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function importRackDevices(){
|
|||
|
|
if(!RACK_ID){setStatus('Geen rack gekoppeld','err');return;}
|
|||
|
|
let devs=RACK_DEVICES;
|
|||
|
|
if(!devs.length){
|
|||
|
|
try{
|
|||
|
|
const r=await fetch('/racks/'+RACK_ID+'/devices.json');
|
|||
|
|
const d=await r.json();
|
|||
|
|
devs=d.devices||[];
|
|||
|
|
}catch(e){setStatus('Import mislukt','err');return;}
|
|||
|
|
}
|
|||
|
|
if(!devs.length){setStatus('Geen rack devices','err');return;}
|
|||
|
|
const typeMap={server:'hw-dell-pe',switch:'net-catalyst',storage:'storage-netapp',firewall:'net-fortinet',router:'net-junper-srx',loadbalancer:'net-f5',san:'storage-pure',pdu:'infra-pdu',ups:'infra-apc-ups'};
|
|||
|
|
let added=0;
|
|||
|
|
devs.forEach(function(dev,i){
|
|||
|
|
if(nodes.some(n=>n.rack_device_id===dev.id))return;
|
|||
|
|
const type=typeMap[dev.device_type]||'hw-dell-pe';
|
|||
|
|
addNode(type,60+(i%3)*190,60+Math.floor(i/3)*95,null,{
|
|||
|
|
label:dev.name,sub:dev.model||dev.device_type,vendor:dev.manufacturer||'',model:dev.model||'',ip:dev.mgmt_ip||'',specs:dev.specs||'',rack_device_id:dev.id,layer:'infra'
|
|||
|
|
});
|
|||
|
|
added++;
|
|||
|
|
});
|
|||
|
|
saveState();
|
|||
|
|
setStatus(added?added+' rack devices geïmporteerd':'Alle devices al aanwezig','ok');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function setStatus(msg,type){const el=document.getElementById('statusMsg');el.textContent=msg;el.className='status-msg '+(type||'');clearTimeout(el._timer);el._timer=setTimeout(()=>{el.textContent='';el.className='status-msg';},3000);}
|
|||
|
|
|
|||
|
|
async function saveDiagram(){
|
|||
|
|
const name=document.getElementById('diagramName').value;
|
|||
|
|
const clientId=document.getElementById('diagramClient').value;
|
|||
|
|
const diagramType=document.getElementById('diagramType').value;
|
|||
|
|
if(!name){setStatus('Naam is verplicht','err');return;}
|
|||
|
|
if(!clientId){setStatus('Selecteer een cliënt','err');return;}
|
|||
|
|
const payload={id:DIAGRAM_ID||null,client_id:clientId,name,description:diagramType,diagram_type:diagramType,nodes:JSON.stringify(nodes),edges:JSON.stringify(edges),rack_id:RACK_ID||null};
|
|||
|
|
try{
|
|||
|
|
const r=await fetch('/architecture/save',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)});
|
|||
|
|
const d=await r.json();setStatus('Opgeslagen ✓','ok');isDirty=false;
|
|||
|
|
if(d.id){
|
|||
|
|
document.getElementById('diagramMeta').dataset.id=d.id;
|
|||
|
|
if(RACK_ID){
|
|||
|
|
window.history.replaceState(null,'','/racks/'+RACK_ID+'/diagram/'+d.id);
|
|||
|
|
}else if(!DIAGRAM_ID){
|
|||
|
|
window.history.replaceState(null,'','/architecture/'+d.id);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}catch(e){setStatus('Fout bij opslaan','err');}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function exportJSON(){
|
|||
|
|
const data={nodes,edges};const blob=new Blob([JSON.stringify(data,null,2)],{type:'application/json'});
|
|||
|
|
const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download=(document.getElementById('diagramName').value||'diagram')+'.json';a.click();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function fitToScreen(){
|
|||
|
|
if(nodes.length===0)return;
|
|||
|
|
const minX=Math.min(...nodes.map(n=>n.x)),minY=Math.min(...nodes.map(n=>n.y));
|
|||
|
|
const maxX=Math.max(...nodes.map(n=>n.x+(n.w||140))),maxY=Math.max(...nodes.map(n=>n.y+(n.h||48)));
|
|||
|
|
const wr=wrap.getBoundingClientRect();
|
|||
|
|
const w=maxX-minX+100,h=maxY-minY+100,z=Math.min(wr.width/w,(wr.height-60)/h,1.5);
|
|||
|
|
zoomLevel=Math.max(.2,z);panOffX=-minX*zoomLevel+50;panOffY=-minY*zoomLevel+30;
|
|||
|
|
viewport.style.transform='translate('+panOffX+'px,'+panOffY+'px) scale('+zoomLevel+')';
|
|||
|
|
document.getElementById('zoomIndicator').textContent=Math.round(zoomLevel*100)+'%';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const GRID_ON='radial-gradient(ellipse 60% 50% at 50% 50%, rgba(0,212,255,0.04) 0%, transparent 70%),linear-gradient(rgba(0,212,255,0.045) 1px,transparent 1px),linear-gradient(90deg,rgba(0,212,255,0.045) 1px,transparent 1px)';
|
|||
|
|
const GRID_OFF='radial-gradient(ellipse 60% 50% at 50% 50%, rgba(0,212,255,0.04) 0%, transparent 70%)';
|
|||
|
|
function toggleGrid(){showGrid=!showGrid;wrap.style.backgroundImage=showGrid?GRID_ON:GRID_OFF;wrap.style.backgroundSize=showGrid?'100% 100%, 24px 24px, 24px 24px':'100% 100%';}
|
|||
|
|
|
|||
|
|
function clearCanvas(){if(nodes.length===0)return;if(!confirm('Wis alle componenten en connecties?'))return;canvas.querySelectorAll('.canvas-node').forEach(el=>el.remove());svg.querySelectorAll('.conn-group').forEach(g=>g.remove());nodes=[];edges=[];selectedNodeId=null;selectedEdgeIdx=null;propsEl.classList.remove('open');updateStats();saveState();}
|
|||
|
|
|
|||
|
|
window.addEventListener('keydown',e=>{
|
|||
|
|
if(e.target.tagName==='INPUT'||e.target.tagName==='TEXTAREA'||e.target.tagName==='SELECT')return;
|
|||
|
|
if(e.key==='Delete'||e.key==='Backspace'){deleteSelected();e.preventDefault();}
|
|||
|
|
if((e.ctrlKey||e.metaKey)&&e.key==='z'){e.shiftKey?redoAction():undoAction();e.preventDefault();}
|
|||
|
|
if((e.ctrlKey||e.metaKey)&&e.key==='s'){saveDiagram();e.preventDefault();}
|
|||
|
|
if((e.ctrlKey||e.metaKey)&&e.key==='f'){fitToScreen();e.preventDefault();}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
if(DIAGRAM_ID){
|
|||
|
|
fetch('/architecture/render/'+DIAGRAM_ID).then(r=>r.json()).then(data=>{
|
|||
|
|
if(data.nodes)data.nodes.forEach(n=>addNode(n.type,n.x,n.y,n.id,n));
|
|||
|
|
if(data.edges)edges=data.edges.map(normalizeEdge);
|
|||
|
|
renderEdges();updateStats();saveState();setTimeout(fitToScreen,100);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
updateStats();saveState();
|
|||
|
|
setDefaultFlowMode('both');
|
|||
|
|
if(RACK_ID && !document.getElementById('diagramName').value){
|
|||
|
|
const dt=document.getElementById('diagramType').value;
|
|||
|
|
document.getElementById('diagramName').value=(dt==='application_landscape'?'Applicatie Landschap':'Applicatie Flow')+' — Rack '+RACK_ID;
|
|||
|
|
}
|
|||
|
|
if(typeof lucide!=='undefined')lucide.createIcons();
|
|||
|
|
</script>
|
|||
|
|
<script src="/js/theme.js"></script></body>
|
|||
|
|
</html>
|