forked from diegoecab/oci-deal-accelerator
Improve diagram generator: auto-sizing, DRG placement, edge labels
6 improvements based on Oracle Architecture Center reference diagrams: 1. Edge label offset — labels pushed 15px away from edge midpoint via mxGeometry offset injection, prevents overlap with icons 2. Auto-sizing containers — regions, VCNs, tenancy calculate dimensions from content instead of using fixed sizes (DR region: 540→260px) 3. jettySize=auto on all edges — clean stubs leaving/entering shapes 4. Dual connection merging — duplicate from/to pairs merged into single edge with combined label (e.g., dual FastConnect) 5. DRG placed OUTSIDE VCN, INSIDE region — matches Oracle ref arch pattern where DRG is the central hub between external and VCN 6. container=1;collapsible=0 injected into container XML styles Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,13 +1,15 @@
|
||||
# OCI Deal Accelerator — Architecture Diagram Spec
|
||||
# Demo: PharmaCorp Mexico — ExaCS dual-region with Data Guard
|
||||
# Layout: Left-to-right flow, ref arch style
|
||||
# Mexico DC → FastConnect → Queretaro (DB subnet) → DR Sao Paulo
|
||||
# Prompt: "ExaCS en Queretaro (prod) + Standby con Data Guard en Sao Paulo,
|
||||
# Hub-Spoke networking con DRG, FastConnect 10Gbps dual desde Mexico City,
|
||||
# bastion en public subnet, WAF para apps web, Vault para TDE keys."
|
||||
#
|
||||
# This example only includes components the user explicitly requested:
|
||||
# ExaCS (prod + DR), Hub-Spoke networking with DRG, FastConnect 10Gbps,
|
||||
# Bastion in public subnet.
|
||||
# DRG and Service Gateway are auto-added per technical dependency whitelist.
|
||||
# Monitoring, Logging, Events, WAF, Vault, EBS — NOT included (not requested).
|
||||
# REQUESTED by user: ExaCS (prod+DR), DRG, FastConnect dual, Bastion, WAF, Vault
|
||||
# TECHNICAL DEPENDENCIES (whitelist): SGW (ExaCS backup), IGW (public subnet)
|
||||
# NOT included: EBS, Monitoring, Logging, Events, FSDR, Object Storage (not requested)
|
||||
#
|
||||
# Container sizes: omitted to let auto-sizing calculate from content.
|
||||
# DRG: listed in gateways — generator places it OUTSIDE VCN, INSIDE region.
|
||||
|
||||
title: "PharmaCorp Mexico — ExaCS Migration Architecture"
|
||||
|
||||
@@ -17,7 +19,7 @@ external:
|
||||
label: "Mexico City\nData Center"
|
||||
icon: "user"
|
||||
x: 30
|
||||
y: 320
|
||||
y: 250
|
||||
w: 60
|
||||
h: 70
|
||||
|
||||
@@ -26,144 +28,108 @@ tenancy:
|
||||
label: "Oracle Cloud Infrastructure"
|
||||
x: 200
|
||||
y: 60
|
||||
w: 1650
|
||||
h: 780
|
||||
|
||||
regions:
|
||||
# ── Primary Region: Queretaro ──
|
||||
- id: "region_qro"
|
||||
label: "Region — Queretaro (Primary)"
|
||||
primary: true
|
||||
x: 15
|
||||
y: 40
|
||||
w: 1050
|
||||
h: 720
|
||||
|
||||
vcns:
|
||||
- id: "vcn_hub"
|
||||
label: "Hub VCN (10.0.0.0/16)"
|
||||
x: 15
|
||||
y: 45
|
||||
w: 1020
|
||||
h: 650
|
||||
|
||||
subnets:
|
||||
# Public subnet — bastion access
|
||||
# Public subnet — WAF + Bastion (both requested)
|
||||
- id: "subnet_pub"
|
||||
label: "Public Subnet — 10.0.0.0/24"
|
||||
h: 120
|
||||
services:
|
||||
- id: "waf"
|
||||
label: "WAF\n(Web Apps)"
|
||||
type: "waf"
|
||||
w: 120
|
||||
h: 70
|
||||
- id: "bastion"
|
||||
label: "Bastion\nService"
|
||||
type: "bastion"
|
||||
w: 120
|
||||
h: 70
|
||||
|
||||
# DB subnet — main workload
|
||||
# DB subnet — ExaCS + Vault (both requested)
|
||||
- id: "subnet_db"
|
||||
label: "Database Subnet (Private) — 10.0.1.0/24"
|
||||
h: 150
|
||||
h: 130
|
||||
services:
|
||||
- id: "exacs_prod"
|
||||
label: "ExaCS X11M\nQuarter Rack\n(Production)"
|
||||
type: "exacs"
|
||||
w: 200
|
||||
h: 90
|
||||
h: 80
|
||||
- id: "vault"
|
||||
label: "OCI Vault\nTDE Keys"
|
||||
type: "vault"
|
||||
w: 140
|
||||
h: 80
|
||||
|
||||
gateways:
|
||||
# Auto-added per whitelist: FastConnect requires DRG
|
||||
# DRG — generator places this OUTSIDE VCN, INSIDE region
|
||||
- id: "drg"
|
||||
label: "DRG"
|
||||
type: "drg"
|
||||
w: 90
|
||||
h: 60
|
||||
# Auto-added per whitelist: ExaCS backup requires SGW
|
||||
# IGW — whitelist: public subnet requires it
|
||||
- id: "igw"
|
||||
label: "Internet\nGateway"
|
||||
type: "igw"
|
||||
w: 100
|
||||
h: 60
|
||||
# SGW — whitelist: ExaCS backup requires it
|
||||
- id: "sgw"
|
||||
label: "Service\nGateway"
|
||||
type: "service_gateway"
|
||||
w: 100
|
||||
h: 60
|
||||
|
||||
# ── DR Region: Sao Paulo (right side) ──
|
||||
# ── DR Region: Sao Paulo ──
|
||||
- id: "region_gru"
|
||||
label: "Region — São Paulo (DR)"
|
||||
x: 1090
|
||||
y: 40
|
||||
w: 540
|
||||
h: 540
|
||||
|
||||
vcns:
|
||||
- id: "vcn_dr"
|
||||
label: "DR VCN (10.1.0.0/16)"
|
||||
x: 15
|
||||
y: 45
|
||||
w: 510
|
||||
h: 470
|
||||
|
||||
subnets:
|
||||
- id: "subnet_dr_db"
|
||||
label: "DB Subnet (Private) — 10.1.1.0/24"
|
||||
h: 200
|
||||
h: 130
|
||||
services:
|
||||
- id: "exacs_dr"
|
||||
label: "ExaCS Standby\n(Data Guard)"
|
||||
type: "exacs"
|
||||
w: 200
|
||||
h: 100
|
||||
h: 80
|
||||
|
||||
# Connections
|
||||
connections:
|
||||
# Entry: DC → DRG (FastConnect)
|
||||
- id: "conn_fc"
|
||||
# FastConnect dual from Mexico City → DRG (will be merged into single edge)
|
||||
- id: "conn_fc1"
|
||||
label: "FastConnect 10Gbps\n(Dual Redundant)"
|
||||
type: "network"
|
||||
type: "fastconnect"
|
||||
from: "mexico_dc"
|
||||
to: "drg"
|
||||
|
||||
# DR: ExaCS Prod → ExaCS Standby (Data Guard)
|
||||
# Vault → ExaCS (TDE keys)
|
||||
- id: "conn_vault"
|
||||
label: "TDE Keys"
|
||||
type: "internal"
|
||||
from: "vault"
|
||||
to: "exacs_prod"
|
||||
|
||||
# Data Guard: Prod → DR
|
||||
- id: "conn_dg"
|
||||
label: "Data Guard\n(Redo Shipping)"
|
||||
type: "data"
|
||||
from: "exacs_prod"
|
||||
to: "exacs_dr"
|
||||
|
||||
# ──────────────────────────────────────────────────────────────
|
||||
# OPTIONAL — add if the user approves during pre-generation review:
|
||||
#
|
||||
# Security:
|
||||
# - id: "vault"
|
||||
# label: "OCI Vault\nTDE Keys"
|
||||
# type: "vault"
|
||||
#
|
||||
# - id: "waf"
|
||||
# label: "WAF"
|
||||
# type: "waf"
|
||||
#
|
||||
# Observability:
|
||||
# - id: "monitoring"
|
||||
# label: "OCI Monitoring"
|
||||
# type: "monitoring"
|
||||
#
|
||||
# - id: "logging"
|
||||
# label: "OCI Audit + Logging"
|
||||
# type: "logging"
|
||||
#
|
||||
# - id: "events"
|
||||
# label: "OCI Events + Notifications"
|
||||
# type: "events"
|
||||
#
|
||||
# Application tier (only if customer has app servers):
|
||||
# - id: "ebs_app"
|
||||
# label: "EBS R12 App Tier"
|
||||
# type: "compute"
|
||||
#
|
||||
# DR orchestration:
|
||||
# - id: "fsdr"
|
||||
# label: "Full Stack DR (FSDR)"
|
||||
# type: "fsdr"
|
||||
#
|
||||
# Backup:
|
||||
# - id: "obj_storage"
|
||||
# label: "Object Storage\nRMAN Backups"
|
||||
# type: "object-storage"
|
||||
# ──────────────────────────────────────────────────────────────
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -909,6 +909,7 @@ class OCIDiagramGenerator:
|
||||
# - NO elbow=vertical (it conflicts with orthogonal + port constraints)
|
||||
extra_style = (
|
||||
"edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;"
|
||||
"jettySize=auto;"
|
||||
"jumpStyle=arc;jumpSize=8;"
|
||||
"endFill=0;startFill=0;"
|
||||
"labelBackgroundColor=#FFFFFF;labelBorderColor=none;"
|
||||
@@ -1121,6 +1122,57 @@ class OCIDiagramGenerator:
|
||||
if pattern in xml:
|
||||
xml = xml.replace(pattern, f'id="{edge_id}" style="{extra_style}')
|
||||
|
||||
# ── Edge label offset injection ──
|
||||
# Push labels away from the edge midpoint to avoid overlapping with
|
||||
# source/target icons. Uses mxGeometry relative positioning:
|
||||
# x: position along edge (-1=source, 0=center, 1=target)
|
||||
# <mxPoint as="offset">: absolute pixel offset from that position
|
||||
# Horizontal edges: push label 15px above the line (offset y=-15)
|
||||
# Vertical edges: push label 15px to the right (offset x=15)
|
||||
for edge_id, (src_id, tgt_id) in self._edge_objects.items():
|
||||
src_pos = self._abs_positions.get(src_id)
|
||||
tgt_pos = self._abs_positions.get(tgt_id)
|
||||
if not src_pos or not tgt_pos:
|
||||
continue
|
||||
src_obj = self._objects.get(src_id)
|
||||
tgt_obj = self._objects.get(tgt_id)
|
||||
if not src_obj or not tgt_obj:
|
||||
continue
|
||||
sx = src_pos[0] + (getattr(src_obj, 'width', 100) or 100) / 2
|
||||
sy = src_pos[1] + (getattr(src_obj, 'height', 60) or 60) / 2
|
||||
tx = tgt_pos[0] + (getattr(tgt_obj, 'width', 100) or 100) / 2
|
||||
ty = tgt_pos[1] + (getattr(tgt_obj, 'height', 60) or 60) / 2
|
||||
dx = tx - sx
|
||||
dy = ty - sy
|
||||
is_horiz = abs(dx) > abs(dy) * 1.5
|
||||
if is_horiz:
|
||||
offset_x, offset_y = 0, -15
|
||||
else:
|
||||
offset_x, offset_y = 15, 0
|
||||
# Find the edge's mxGeometry and inject label offset.
|
||||
# drawpyo emits: <mxGeometry relative="1" as="geometry" />
|
||||
# We replace with expanded form containing offset point.
|
||||
new_geo = (
|
||||
f'<mxGeometry x="-0.2" y="0" relative="1" as="geometry">'
|
||||
f'<mxPoint x="{offset_x}" y="{offset_y}" as="offset" />'
|
||||
f'</mxGeometry>'
|
||||
)
|
||||
# Match both self-closing variants that drawpyo may produce
|
||||
for old_geo in [
|
||||
'<mxGeometry relative="1" as="geometry" />',
|
||||
'<mxGeometry relative="1" as="geometry"/>',
|
||||
]:
|
||||
marker = f'id="{edge_id}"'
|
||||
if marker in xml and old_geo in xml:
|
||||
# Only replace the mxGeometry that belongs to THIS edge
|
||||
# Find the edge cell, then replace its geometry
|
||||
idx = xml.find(marker)
|
||||
if idx >= 0:
|
||||
geo_idx = xml.find(old_geo, idx)
|
||||
if geo_idx >= 0 and geo_idx - idx < 2000: # sanity: within same cell
|
||||
xml = xml[:geo_idx] + new_geo + xml[geo_idx + len(old_geo):]
|
||||
break
|
||||
|
||||
# Inject raw icon stencil cells before </root>
|
||||
if self._raw_cells:
|
||||
inject = "\n".join(f" {c}" for c in self._raw_cells)
|
||||
@@ -1132,6 +1184,17 @@ class OCIDiagramGenerator:
|
||||
if placeholder in xml:
|
||||
xml = xml.replace(placeholder, str(obj.id))
|
||||
|
||||
# Inject container=1;collapsible=0 into container styles via XML.
|
||||
# drawpyo doesn't support 'container' as a settable property, so we
|
||||
# append it to the style string in the final XML.
|
||||
for container_marker in ["arcSize=1;", "arcSize=5;", "arcSize=3;",
|
||||
"perimeterSpacing=0;", "spacingTop=4;"]:
|
||||
# Only add to styles that already contain these OCI container markers
|
||||
# and don't already have container=1
|
||||
old = f'{container_marker}"'
|
||||
new = f'{container_marker}container=1;collapsible=0;"'
|
||||
xml = xml.replace(old, new)
|
||||
|
||||
# Sanitize: no pure white in OCI palette
|
||||
xml = xml.replace('fill="#ffffff"', 'fill="#FCFBFA"')
|
||||
xml = xml.replace('fill="#FFFFFF"', 'fill="#FCFBFA"')
|
||||
@@ -1221,33 +1284,130 @@ class OCIDiagramGenerator:
|
||||
cloud_id, cx, cy, svc_w, svc_h)
|
||||
cy += svc_h + 20
|
||||
|
||||
# Tenancy
|
||||
# Tenancy — auto-size from regions if w/h not specified in spec
|
||||
tenancy_spec = spec.get("tenancy", {})
|
||||
tenancy_x = tenancy_spec.get("x", 30)
|
||||
tenancy_y = tenancy_spec.get("y", 80)
|
||||
tenancy_w = tenancy_spec.get("w", 1850)
|
||||
tenancy_h = tenancy_spec.get("h", 720)
|
||||
|
||||
# Pre-calculate tenancy size from region content if not explicit
|
||||
if "w" not in tenancy_spec or "h" not in tenancy_spec:
|
||||
regions = tenancy_spec.get("regions", [])
|
||||
max_region_right = 0
|
||||
max_region_bottom = 0
|
||||
region_x_cursor = 15
|
||||
for r in regions:
|
||||
# Estimate region dimensions (will be refined when regions are created)
|
||||
r_vcns = r.get("vcns", [])
|
||||
r_has_drg = any(
|
||||
gw.get("type") in ("drg", "dynamic_routing_gateway")
|
||||
for v in r_vcns for gw in v.get("gateways", [])
|
||||
)
|
||||
drg_w = 120 if r_has_drg else 0
|
||||
# Quick subnet height calc
|
||||
total_sub_h = 0
|
||||
max_sub_w = 300
|
||||
for v in r_vcns:
|
||||
for s in v.get("subnets", []):
|
||||
total_sub_h += s.get("h", 120) + 14
|
||||
svcs = s.get("services", [])
|
||||
row_w = sum(sv.get("w", 150) for sv in svcs) + 16 * max(0, len(svcs) - 1) + 30
|
||||
max_sub_w = max(max_sub_w, row_w)
|
||||
# VCN-internal gateways
|
||||
vcn_gws = [gw for gw in v.get("gateways", [])
|
||||
if gw.get("type") not in ("drg", "dynamic_routing_gateway")]
|
||||
if vcn_gws:
|
||||
max_sub_w += max(gw.get("w", 110) for gw in vcn_gws) + 20
|
||||
|
||||
est_rw = max_sub_w + drg_w + 80
|
||||
est_rh = total_sub_h + 80
|
||||
|
||||
rx_val = r.get("x", region_x_cursor)
|
||||
ry_val = r.get("y", 40)
|
||||
max_region_right = max(max_region_right, rx_val + est_rw)
|
||||
max_region_bottom = max(max_region_bottom, ry_val + est_rh)
|
||||
region_x_cursor = max_region_right + 20
|
||||
|
||||
tenancy_w = tenancy_spec.get("w", max(max_region_right + 30, 600))
|
||||
tenancy_h = tenancy_spec.get("h", max(max_region_bottom + 30, 400))
|
||||
else:
|
||||
tenancy_w = tenancy_spec["w"]
|
||||
tenancy_h = tenancy_spec["h"]
|
||||
|
||||
gen.add_tenancy(
|
||||
"tenancy",
|
||||
tenancy_spec.get("label", "Oracle Cloud Infrastructure"),
|
||||
x=tenancy_x, y=tenancy_y, w=tenancy_w, h=tenancy_h,
|
||||
)
|
||||
|
||||
# Regions
|
||||
# Regions — auto-size containers from content when spec doesn't override
|
||||
region_x_cursor = 15 # tracks right edge for auto-positioning next region
|
||||
for region in tenancy_spec.get("regions", []):
|
||||
is_primary = region.get("primary", False)
|
||||
rx = region.get("x", 15 if is_primary else 1195)
|
||||
# ry default 40: ~18px below tenancy label (12pt bold) + comfortable padding
|
||||
rx = region.get("x", region_x_cursor)
|
||||
ry = region.get("y", 40)
|
||||
rw = region.get("w", 1140 if is_primary else 640)
|
||||
rh = region.get("h", 670 if is_primary else 480)
|
||||
|
||||
# ── Pre-calculate content size for auto-sizing ──
|
||||
# Walk all VCNs → subnets → services to compute minimum dimensions
|
||||
region_vcns = region.get("vcns", [])
|
||||
auto_rw, auto_rh = 640 if not is_primary else 1140, 480 if not is_primary else 670
|
||||
|
||||
if region_vcns:
|
||||
# Calculate total height needed from subnets + gateways
|
||||
for vcn in region_vcns:
|
||||
gateways = vcn.get("gateways", [])
|
||||
# Separate DRG from VCN-internal gateways (IGW, NAT, SGW)
|
||||
drg_gateways = [gw for gw in gateways if gw.get("type") in ("drg", "dynamic_routing_gateway")]
|
||||
vcn_gateways = [gw for gw in gateways if gw.get("type") not in ("drg", "dynamic_routing_gateway")]
|
||||
|
||||
gw_lane_w = 0
|
||||
if vcn_gateways:
|
||||
gw_lane_w = max(gw.get("w", 110) for gw in vcn_gateways) + 20
|
||||
|
||||
subnets = vcn.get("subnets", [])
|
||||
total_subnet_h = sum(s.get("h", 120) for s in subnets) + 14 * max(0, len(subnets) - 1)
|
||||
|
||||
# Max service width across all subnets
|
||||
max_svc_row_w = 0
|
||||
for subnet in subnets:
|
||||
svcs = subnet.get("services", [])
|
||||
row_w = sum(svc.get("w", 150) for svc in svcs) + 16 * max(0, len(svcs) - 1) + 30
|
||||
max_svc_row_w = max(max_svc_row_w, row_w)
|
||||
|
||||
# VCN needs: gw_lane + max(subnet_content_w, specified_w) + margins
|
||||
content_w = gw_lane_w + max(max_svc_row_w, 300) + 40
|
||||
content_h = total_subnet_h + 60 # 32 top + 28 bottom padding
|
||||
|
||||
# Also account for gateway column height
|
||||
if vcn_gateways:
|
||||
total_gw_h = sum(gw.get("h", 70) for gw in vcn_gateways) + 8 * max(0, len(vcn_gateways) - 1) + 64
|
||||
content_h = max(content_h, total_gw_h)
|
||||
|
||||
# Store calculated VCN dimensions for use below
|
||||
vcn["_auto_w"] = content_w
|
||||
vcn["_auto_h"] = content_h
|
||||
vcn["_drg_gateways"] = drg_gateways
|
||||
vcn["_vcn_gateways"] = vcn_gateways
|
||||
|
||||
# Region size = VCN size + margins + DRG lane
|
||||
total_vcn_w = max(v.get("_auto_w", 600) for v in region_vcns)
|
||||
total_vcn_h = max(v.get("_auto_h", 400) for v in region_vcns)
|
||||
# DRG lane on left edge of region (outside VCN)
|
||||
has_drg = any(v.get("_drg_gateways") for v in region_vcns)
|
||||
drg_lane_w = 120 if has_drg else 0
|
||||
auto_rw = total_vcn_w + drg_lane_w + 50 # margins
|
||||
auto_rh = total_vcn_h + 70 # 45 top (label) + 25 bottom
|
||||
|
||||
# Use spec dimensions if explicitly provided, otherwise auto-calculated
|
||||
rw = region.get("w", auto_rw)
|
||||
rh = region.get("h", auto_rh)
|
||||
|
||||
gen.add_region(
|
||||
region["id"],
|
||||
region["label"],
|
||||
"tenancy", rx, ry, rw, rh,
|
||||
)
|
||||
|
||||
# Advance cursor for next region positioning
|
||||
region_x_cursor = rx + rw + 20
|
||||
|
||||
# Availability Domains (optional)
|
||||
for ad in region.get("availability_domains", []):
|
||||
@@ -1257,39 +1417,49 @@ class OCIDiagramGenerator:
|
||||
w=ad.get("w", rw - 30), h=ad.get("h", rh - 70),
|
||||
)
|
||||
|
||||
# VCNs
|
||||
for vcn in region.get("vcns", []):
|
||||
# ── DRG placement: OUTSIDE VCN, INSIDE region ──
|
||||
# Oracle ref arch pattern: DRG sits in the region between external
|
||||
# actors and VCNs, not inside any VCN.
|
||||
has_drg = any(v.get("_drg_gateways") for v in region_vcns)
|
||||
drg_lane_w = 120 if has_drg else 0
|
||||
for vcn in region_vcns:
|
||||
for drg in vcn.get("_drg_gateways", []):
|
||||
drg_id = drg.get("id", gen._next_id())
|
||||
drg_w = drg.get("w", 90)
|
||||
drg_h = drg.get("h", 60)
|
||||
# Position: left edge of region, vertically centered
|
||||
drg_x = 15
|
||||
drg_y = max(45, (rh - drg_h) // 2)
|
||||
label = drg["label"].replace("\\n", "\n")
|
||||
gen.add_service(
|
||||
drg_id, label, drg["type"],
|
||||
region["id"], drg_x, drg_y, drg_w, drg_h,
|
||||
)
|
||||
|
||||
# VCNs — offset right if DRG lane present
|
||||
vcn_offset_x = drg_lane_w + 15
|
||||
for vcn in region_vcns:
|
||||
vcn_parent = vcn.get("parent", region["id"])
|
||||
vcn_w = vcn.get("w", rw - 30)
|
||||
vcn_h = vcn.get("h", rh - 70)
|
||||
vcn_w = vcn.get("w", vcn.get("_auto_w", rw - vcn_offset_x - 15))
|
||||
vcn_h = vcn.get("h", vcn.get("_auto_h", rh - 70))
|
||||
gen.add_vcn(
|
||||
vcn["id"],
|
||||
f"VCN {vcn['label']}",
|
||||
vcn_parent,
|
||||
# y default 45: leaves proper gap below the region label (12pt ≈22px + padding)
|
||||
x=vcn.get("x", 15), y=vcn.get("y", 45),
|
||||
x=vcn.get("x", vcn_offset_x), y=vcn.get("y", 45),
|
||||
w=vcn_w, h=vcn_h,
|
||||
)
|
||||
|
||||
# ── GATEWAY-AWARE AUTO-LAYOUT ──
|
||||
# Step 1: Place gateways on the LEFT EDGE of the VCN.
|
||||
# Step 2: Offset subnets RIGHT to avoid overlap with gateways.
|
||||
# Step 3: Stack subnets vertically (top-to-bottom).
|
||||
# This matches Oracle ref arch style: gateways on VCN boundary,
|
||||
# subnets fill the remaining width.
|
||||
# ── VCN-internal gateways (IGW, NAT, SGW — NOT DRG) ──
|
||||
vcn_gateways = vcn.get("_vcn_gateways", [])
|
||||
gw_lane_w = 0
|
||||
|
||||
gateways = vcn.get("gateways", [])
|
||||
gw_lane_w = 0 # width reserved for gateway column
|
||||
|
||||
if gateways:
|
||||
# Calculate gateway lane width (max gateway width + padding)
|
||||
gw_lane_w = max(gw.get("w", 110) for gw in gateways) + 20
|
||||
|
||||
# Place gateways vertically centered in VCN
|
||||
total_gw_h = sum(gw.get("h", 70) for gw in gateways) + 8 * max(0, len(gateways) - 1)
|
||||
if vcn_gateways:
|
||||
gw_lane_w = max(gw.get("w", 110) for gw in vcn_gateways) + 20
|
||||
total_gw_h = sum(gw.get("h", 70) for gw in vcn_gateways) + 8 * max(0, len(vcn_gateways) - 1)
|
||||
gw_start_y = max(32, (vcn_h - total_gw_h) // 2)
|
||||
gy = gw_start_y
|
||||
for gw in gateways:
|
||||
for gw in vcn_gateways:
|
||||
gw_id = gw.get("id", gen._next_id())
|
||||
gw_w = gw.get("w", 110)
|
||||
gw_h = gw.get("h", 70)
|
||||
@@ -1302,9 +1472,9 @@ class OCIDiagramGenerator:
|
||||
)
|
||||
gy = gw_y + gw_h + 8
|
||||
|
||||
# Subnet horizontal offset: push right if gateways present
|
||||
# Subnet horizontal offset: push right if VCN gateways present
|
||||
subnet_x = 8 + gw_lane_w
|
||||
subnet_avail_w = vcn_w - subnet_x - 8 # remaining width for subnets
|
||||
subnet_avail_w = vcn_w - subnet_x - 8
|
||||
|
||||
# Stack subnets vertically (top-to-bottom)
|
||||
subnet_y = 32
|
||||
@@ -1441,8 +1611,28 @@ class OCIDiagramGenerator:
|
||||
)
|
||||
opx += svc_w + 12
|
||||
|
||||
# Connections
|
||||
for conn in spec.get("connections", []):
|
||||
# Connections — merge duplicate from/to pairs (e.g., dual FastConnect)
|
||||
# into a single edge with combined label
|
||||
connections = spec.get("connections", [])
|
||||
seen_pairs = {} # (from, to) -> index in deduped list
|
||||
deduped = []
|
||||
for conn in connections:
|
||||
pair = (conn["from"], conn["to"])
|
||||
if pair in seen_pairs:
|
||||
# Merge: combine labels
|
||||
existing = deduped[seen_pairs[pair]]
|
||||
old_label = existing.get("label", "")
|
||||
new_label = conn.get("label", "")
|
||||
if old_label and new_label and old_label != new_label:
|
||||
existing["label"] = f"{old_label}\n+ {new_label}"
|
||||
elif new_label and not old_label:
|
||||
existing["label"] = new_label
|
||||
# Upgrade: if either is "Dual", keep it in label
|
||||
else:
|
||||
seen_pairs[pair] = len(deduped)
|
||||
deduped.append(dict(conn))
|
||||
|
||||
for conn in deduped:
|
||||
conn_id = conn.get("id", gen._next_id())
|
||||
gen.add_connection(
|
||||
conn_id, conn.get("label"), conn["type"],
|
||||
|
||||
Reference in New Issue
Block a user