{% 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. #} {% block rd_layer_switch %} {# Chassis layer (spec §10.3): only offered when the design's scope #} {# actually contains a chassis WITH BAYS, so a deployment with no such #} {# never sees the feature. #} {% if has_chassis_in_scope %} {% trans "Chassis" %} {% endif %} {% endblock rd_layer_switch %}

{# 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. #}
{# T1.5 "Create rack" dialog data (PLAN-multi-site.md M8): the #} {# design's own sites, each with its own locations, read once by #} {# editor_panels.js to populate the dialog's plain {% trans "Existing" %} {# ---- Phase 4 (PLAN-design-chains.md §8.4/§8.6) ---- #} {# `inherited`/`conflict` are FLAGS on top of a state, not new #} {# states -- they get their OWN checkboxes (data-rd-flag, wired #} {# by legend_filter.js) that filter independently of, and #} {# combine by OR with, the five state checkboxes above. #} {# Peer conflict (PLAN-peer-conflicts.md P7), the same flag-not-state #} {# pattern: a design OUTSIDE this design's chain claims this unit, #} {# name, or device. Never blocks save either. #} {# Apply markers (apply-projection phase): same flag-not-state pattern. #} {# Legend debt fix (§8.6): these two existing markers -- the #} {# displacement stripe and the save-rejection ring -- had NO #} {# legend entry at all. INFO-only keys (no checkbox): unchecking #} {# a state already hides that state's own stripe, and the error #} {# ring is a transient highlight, not something to filter. #} {% trans "Displaced (was here)" %} {% trans "Rejected" %}
{# Power heatmap toggle (docs/power-projection-spec.md §3): recolor #} {# tiles green->red by each device's share of the rack's total draw. #} {# Bank zone strip toggle: the narrow per-PDU bars beside each face #} {# grid showing which U range each power bank serves. Independent #} {# of the heatmap -- these are structural, not a coloring view. #}
{{ palette_form.device_role }}
{{ palette_form.tenant }}
{# Sticky defaults for the deployment's own config-declared planning #} {# fields, rendered by editor.js from the placement_fields schema -- #} {# an empty mount here so no field name lives in this template. #}
{# 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) ===== #} {# The third drawer section is the SCOPE panel. The chassis layer #} {# overrides #} {# it with a Chassis panel -- same show/hide behaviour, chassis instead #} {# of racks (spec §10.3). #} {% block rd_scope_panel %}
{# ---- 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" %}

{# A live picker over every site, never a fixed chip (user #} {# ruling 2026-09-22): it is the filter for Location/Rack, #} {# and picking a site the design does not cover yet is how #} {# a design becomes multi-site -- add-rack widens it. A #} {# design with exactly one site starts pre-filled with it. #} {{ add_rack_form.add_site }}
{{ add_rack_form.add_location }}
{{ add_rack_form.add_rack }}
{# ---- Create a rack that does not exist in NetBox yet (T1.5, ---- #} {# PLAN-templates.md §1: a "planned rack"). Opens a dialog (name, #} {# U height, location); editor_panels.js builds it at click time, #} {# the same pattern power.js's own dialogs use, and POSTs to #} {# create-planned-rack, then reloads on success so the server #} {# re-renders the new (badged) rack block -- identical to Add rack #} {# above. #}

{% trans "Create rack" %}

{% trans "Plan a rack before it exists in NetBox -- it is created (or an existing match adopted) when the design is applied." %}

{# ---- 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 %}
{# One eye for every rack, planned ones too; the id is the #} {# block's data-rack-id (bare pk, or p- when planned). #} {{ row.rack.name }} {% if row.planned %}{% trans "Planned" %}{% endif %} {% if row.rack.location %}· {{ row.rack.location }}{% endif %}
{% empty %}
{% trans "No racks in this design yet." %}
{% endfor %}
{# /section: racks #} {% endblock rd_scope_panel %} {# ===== 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. ---- #}
{# Two deliberate rows: the title, then the set controls. The #} {# drawer column is 300px, so squeezing both onto one line left #} {# the title and the set name as ellipses -- and it was the #} {# title WRAPPING that wasted the space in the first place. #}

{% trans "Quick access" %}

{# Named favorite SETS: one starred list per way of working #} {# ("Default", "for server", "for network"). The select drives #} {# which set the stars read and write; the buttons create, #} {# rename and delete the user's own sets. Populated by #} {# editor.js from the favorite-sets endpoint. #}
{# `no-ts` opts out of NetBox's TomSelect enhancement: the #} {# options are (re)built by editor.js after every set change, #} {# which a TomSelect wrapper would not pick up. #}
{# This list scrolls if many favorites; populated by editor.js. #}
{# /section: favorites #} {# ===== Section: Templates (PLAN-templates.md Sec 3, T3.3) ===== #} {# One card per Template/TemplateGroup, fetched from the plugin's #} {# own REST API by editor/templates.js (same pattern palette.js #} {# uses for core's device-type catalog). Drag a template card #} {# onto a rack to stamp it there directly; click a template OR a #} {# group card to open the multi-rack / group-mapping dialog. #} {# Nothing is written until the ordinary design-level Save (D19). #}

{% trans "Templates" %}

{% blocktrans %}Drag a template onto a rack, or click a template (or a group) to pick target racks.{% endblocktrans %}
{# /section: templates #}
{# /.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. ---- #} {% block rd_canvas %}
{% 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 #} {% endblock rd_canvas %}
{# /.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 %} {# ---------------------------------------------------------------------- #} {# editor.js is an ES module and imports its submodules by BARE specifier #} {# ("rd/model.js"), which this map resolves. The indirection buys the one #} {# thing a relative import cannot: the ?v= cache-bust #} {# token. A relative "./editor/model.js" would resolve to an unversioned #} {# URL, so an upgrade could pair a fresh editor.js with a browser-cached #} {# stale model.js -- a broken editor, and a hard refresh the only cure. #} {# The map must precede every module script on the page. #} {# Module scripts are deferred implicitly and still execute in document #} {# order alongside the defer'd classic scripts below, so editor.js keeps #} {# publishing window.NbxRdEditor before its consumers run. #} {% endblock javascript %}