{% extends 'generic/object.html' %} {% load static %} {% load i18n %} {% load rack_design %} {# Interactive single-rack layout editor (Stage 2, slice 2a: MOVE + REMOVE). #} {# Forks design_elevation.html but initialises GridStack WITHOUT staticGrid so #} {# slots can be dragged vertically (and between faces / into the non-racked #} {# tray). editor.js hydrates from #rd-editor-data, tracks each widget's original #} {# (u_position, face), and POSTs a diff to data-save-url. NO model changes. #} {# #} {# Widget indices: views.py builds `widgets` (and widgets_json) in the order #} {# (*front, *rear, *non_racked). Each grid-stack-item below is stamped with the #} {# matching GLOBAL index via `gs-id`/data-widget-index so editor.js can pair the #} {# JSON payload to its DOM tile: front uses forloop.counter0; rear adds the #} {# front length; the tray adds front+rear lengths. #} {% block head %} {{ block.super }} {% endblock head %} {% block content %}

{% blocktrans with design=object.title %}Edit layout: {{ design }}{% endblocktrans %}
{# Three INDEPENDENT section toggles for the ONE push/collapse tool #} {# drawer. Each button shows or hides ONLY its own section; any #} {# combination can be open at once (open sections sit side by side as #} {# columns in the drawer). The #} {# drawer is open whenever ANY section is active. editor_panels.js marks #} {# each active button and persists the SET of open sections per browser. #}
{# ONE design-level Save: editor.js collects edits from EVERY rendered #} {# rack block and POSTs a single multi-rack payload. #}

{# The editor root: carries the design-level save URL + shared context the #} {# JS needs. Per-rack meta lives on each .nbx-rd-rack-block below. #}
{# Custom-field bridge schema (docs/pdu-distribution-spec.md §5): read #} {# once by editor.js to render the rack-power dialog's fields #} {# dynamically -- no cf name is ever hardcoded in JS/HTML. #} {{ planning_fields|json_script:"rd-planning-fields" }} {# ---- ONE always-visible toolbar line: the state-LEGEND filter and the #} {# Role/Tenant selects share a single horizontal row above the drawer + #} {# racks shell, visible regardless of which drawer sections are open. #} {# #} {# Left group — the legend, which also acts as a per-state filter #} {# (legend_filter.js binds [data-rd-legend] and the data-rd-state #} {# checkboxes; unchecking hides that state's slots). Shared across every #} {# rack block in this card-body. #} {# Right group — Role + Tenant, the SAME API-backed #} {# DynamicModelChoiceFields with the SAME widget ids (#id_device_role / #} {# #id_tenant): NetBox's select-init enhances them into TomSelect and #} {# editor.js onPaletteDrop reads them by id to stamp the next drag-in. #} {# A `vr` divider separates the two groups; the line wraps only when the #} {# viewport is too narrow. #}
{# Power heatmap toggle (docs/power-projection-spec.md §3): recolor #} {# tiles green->red by each device's share of the rack's total draw. #}
{{ palette_form.device_role }}
{{ palette_form.tenant }}
{# Single minimal affordance: the duplicated inline hint text is gone; #} {# only this ⓘ remains, explained via its tooltip. #}
{# ---- Editor shell: a PUSH/COLLAPSE sidebar layout. The tool drawer is a #} {# left flex column; the rack workspace is the remaining width. When #} {# the drawer is OPEN both are visible side by side (so a device-type #} {# can still be dragged from the catalog onto a rack face); when #} {# CLOSED (the default) the drawer is removed and the racks take the #} {# full width and sit at the top, immediately visible. editor_panels #} {# .js toggles .drawer-open here from the set of active sections. ---- #} {# data-drawer-section-initial signals the section(s) editor_panels.js #} {# opens when the user has NO stored preference: "" (closed) normally, but #} {# "racks" for a design with NO racks yet so the Add-rack panel is #} {# immediately reachable for the first rack. #}
{# ---- Collapsible tool drawer: ONE area, three INDEPENDENT ---- #} {# sections (Device / Favorites / Racks). Each card-header toggle shows #} {# or hides its own section; any combination can be open and the active #} {# sections lay out side by side as columns (the drawer widens right). #}
{# ===== Section: Device (device-type catalog only) ===== #} {# Role + Tenant moved OUT to the always-visible toolbar above. #}
{# ---- Catalog palette (drag a device type onto a face to plan an add) ---- #}

{% trans "Device-type catalog" %}

{# NetBox API-backed searchable select; the editor JS reads its #} {# value (#id_manufacturer) to filter the CATALOG search only. #}
{{ palette_form.manufacturer }}
{# Discoverability: a dragged-in device is auto-named by the naming #} {# engine and the name is editable on its tile. In template mode the #} {# tokens are dotted NetBox-model paths. #}
{% blocktrans %}Dropped devices are auto-named; edit the name on the tile. Template tokens are dotted model paths, e.g. {design.name}, {device.site.name}.{% endblocktrans %}
{# Only this list scrolls; the controls above stay pinned. #}
{# /section: device #} {# ===== Section: Racks (add-rack + design-racks) ===== #}
{# ---- Add a rack to this design's scope (filtered by location) ---- #} {# Location + rack are API-backed DynamicModelChoiceFields scoped to #} {# the design's site; the rack chooser chains on the chosen location. #} {# editor_panels.js reads #id_add_rack and POSTs it to add-rack, then #} {# reloads so the server re-renders the new rack block. #}

{% trans "Add rack" %}

{{ add_rack_form.add_location }}
{{ add_rack_form.add_rack }}
{# ---- Design racks: per-rack show/hide + remove-from-design ---- #} {# Visibility toggles are personal view-state (they POST to #} {# hidden-design-racks and toggle the .hidden class on the matching #} {# rack block — no reload, never marks the layout dirty). "All" reveals #} {# every block. The trash control removes the rack from design.racks #} {# (destructive; confirms via the remove-rack 409 flow). #}

{% trans "Design racks" %}

{% for row in scoped_rack_rows %}
{{ row.rack.name }} {% if row.rack.location %}· {{ row.rack.location }}{% endif %}
{% empty %}
{% trans "No racks in this design yet." %}
{% endfor %}
{# /section: racks #} {# ===== Section: Favorites (quick access) ===== #}
{# ---- Quick access: per-user favorites, drag a row onto a face. ---- #} {# Lives in the drawer alongside the catalog; favorites list scrolls #} {# internally. Independent of the catalog search + manufacturer filter; #} {# drag a row onto a face to plan an add, click its filled star to #} {# unpin. ---- #}

{% trans "Quick access" %}

{# This list scrolls if many favorites; populated by editor.js. #}
{# /section: favorites #}
{# /.nbx-rd-drawer #} {# ---- Multi-rack workspace: one block per VISIBLE scoped rack, laid #} {# out horizontally in a bounded, scrollable container. It is the #} {# PRIMARY region: when the drawer is closed it takes the full #} {# width and sits at the top; the racks scroll sideways (more #} {# racks) and vertically (full 42U) WITHIN this container so the #} {# page itself never grows tall. editor.js inits each block #} {# independently from its own rd-editor-data- payload. ---- #}
{% for b in all_rack_blocks %} {% include 'netbox_rack_design/inc/rack_block.html' with b=b editable=True %} {% empty %} {# Empty state: a design with ZERO scoped racks still opens the editor #} {# (no bounce to detail). The button opens the Racks section so the #} {# Add-rack panel is shown and the first rack can be added here. #}

{% trans "No racks in this design yet — open Racks → Add rack to start." %}

{% endfor %}
{# /.nbx-rd-racks-scroll #}
{# /.nbx-rd-editor-shell #}
{# Toast container (toasts are appended here by editor.js). #}
{% endblock content %} {% block javascript %} {{ block.super }} {% if rd_debug %} {# Developer builds only (DEBUG on, or Django Debug Toolbar installed): let #} {# editor.js expose its opt-in drag-lifecycle tracer. This inline (non-defer) #} {# script runs at parse time, so the flag is set before the deferred editor.js #} {# below reads it. Never rendered on a production deployment. #} {% endif %} {% endblock javascript %}