Add images/labels toggle, clean up label styling, merge real racks
- Replace the device-image label background bar and text-shadow with NetBox's own technique: a black-stroke pass behind a white-fill pass of the same text, giving crisp readable labels over any image without a dark box behind them. - Add an "Images and labels / Images only / Labels only" selector above the elevation, mirroring the equivalent control on real NetBox rack elevations. The plain label is always in the markup; the selector only toggles the image and its over-image label, exactly like core NetBox does it. Choice persists per browser via localStorage and works without edit permissions. - Add a "Merge into concept" button to the real dcim Rack list: select two racks via the existing bulk-select checkboxes and it forwards them to the concept-merge page, pre-filled. New deploy.merge_racks_to_concept() combines two real racks' devices directly into one new, stacked blueprint without an intermediate per-rack copy step. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -32,33 +32,38 @@
|
||||
data-u-height="{{ block.device.effective_u_height }}"
|
||||
data-width-fraction="{{ block.device.width_fraction }}">
|
||||
<a href="{{ block.device.get_absolute_url }}">
|
||||
{% if block.image %}
|
||||
<clipPath id="concept-block-{{ elevation.face }}-{{ block.device.pk }}">
|
||||
<rect x="{{ block.x }}" y="{{ block.y }}" width="{{ block.width }}" height="{{ block.height }}" rx="2"></rect>
|
||||
</clipPath>
|
||||
{% endif %}
|
||||
<rect x="{{ block.x }}" y="{{ block.y }}" width="{{ block.width }}" height="{{ block.height }}"
|
||||
rx="2" fill="#{{ block.color }}"
|
||||
fill-opacity="{% if block.opposite %}0.35{% else %}1{% endif %}"
|
||||
class="device-rect"></rect>
|
||||
{% comment %}
|
||||
The plain label is always in the markup, image or not - toggling "images
|
||||
only" just layers the (opaque) image on top of it, exactly like NetBox's
|
||||
own rack elevation does. That way there is one code path, not two.
|
||||
{% endcomment %}
|
||||
{% if block.show_text %}
|
||||
<text x="{{ block.x }}" y="{{ block.y }}" dy="15" dx="6"
|
||||
fill="#{{ block.text_color }}" class="device-label">
|
||||
{{ block.label }}{% if block.opposite %} <tspan class="opposite-hint">({% trans "rear" %})</tspan>{% endif %}
|
||||
</text>
|
||||
{% endif %}
|
||||
{% if block.image %}
|
||||
<clipPath id="concept-block-{{ elevation.face }}-{{ block.device.pk }}">
|
||||
<rect x="{{ block.x }}" y="{{ block.y }}" width="{{ block.width }}" height="{{ block.height }}" rx="2"></rect>
|
||||
</clipPath>
|
||||
<image href="{{ block.image.url }}" x="{{ block.x }}" y="{{ block.y }}"
|
||||
width="{{ block.width }}" height="{{ block.height }}"
|
||||
preserveAspectRatio="xMidYMid slice"
|
||||
clip-path="url(#concept-block-{{ elevation.face }}-{{ block.device.pk }})"
|
||||
opacity="{% if block.opposite %}0.35{% else %}1{% endif %}"
|
||||
class="device-image"></image>
|
||||
{% endif %}
|
||||
{% if block.show_text %}
|
||||
{% if block.image %}
|
||||
<rect x="{{ block.x }}" y="{{ block.y }}" width="{{ block.width }}" height="16"
|
||||
class="device-label-bg"></rect>
|
||||
{% if block.show_text %}
|
||||
{# Outlined white-on-black text, drawn as a stroke pass then a fill pass, so it reads over any image without a background box. #}
|
||||
<text x="{{ block.x }}" y="{{ block.y }}" dy="15" dx="6"
|
||||
class="device-image-label device-image-label-stroke">{{ block.label }}</text>
|
||||
<text x="{{ block.x }}" y="{{ block.y }}" dy="15" dx="6"
|
||||
class="device-image-label device-image-label-fill">{{ block.label }}</text>
|
||||
{% endif %}
|
||||
<text x="{{ block.x }}" y="{{ block.y }}" dy="15" dx="6"
|
||||
fill="{% if block.image %}#ffffff{% else %}#{{ block.text_color }}{% endif %}"
|
||||
class="device-label {% if block.image %}on-image{% endif %}">
|
||||
{{ block.label }}{% if block.opposite %} <tspan class="opposite-hint">({% trans "rear" %})</tspan>{% endif %}
|
||||
</text>
|
||||
{% endif %}
|
||||
<title>{{ block.label }}{% if block.sublabel %} · {{ block.sublabel }}{% endif %} · U{{ block.device.position }}</title>
|
||||
</a>
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
<a href="{% url 'plugins:netbox_rack_concept:racks_merge' %}" class="btn btn-outline-secondary"
|
||||
id="concept-merge-racks-button" data-merge-url="{% url 'plugins:netbox_rack_concept:racks_merge' %}"
|
||||
title="{% trans 'Select two racks below, then click this to combine them into one concept' %}">
|
||||
<i class="mdi mdi-call-merge"></i> {% trans "Merge into concept" %}
|
||||
</a>
|
||||
<script src="{% static 'netbox_rack_concept/rack_list_merge.js' %}"></script>
|
||||
@@ -128,12 +128,20 @@
|
||||
|
||||
<div class="col col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">
|
||||
{% trans "Elevation" %}
|
||||
{% if perms.netbox_rack_concept.change_conceptdevice %}
|
||||
<span class="badge text-bg-secondary float-end">{% trans "Drag devices to move them" %}</span>
|
||||
{% endif %}
|
||||
</h2>
|
||||
<div class="card-header d-flex justify-content-between align-items-center flex-wrap gap-2">
|
||||
<h2 class="mb-0">{% trans "Elevation" %}</h2>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<select id="concept-elevation-view" class="form-select form-select-sm" style="width: auto;"
|
||||
aria-label="{% trans 'Images and labels' %}">
|
||||
<option value="images-and-labels">{% trans "Images and labels" %}</option>
|
||||
<option value="images-only">{% trans "Images only" %}</option>
|
||||
<option value="labels-only">{% trans "Labels only" %}</option>
|
||||
</select>
|
||||
{% if perms.netbox_rack_concept.change_conceptdevice %}
|
||||
<span class="badge text-bg-secondary">{% trans "Drag devices to move them" %}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-around flex-wrap"
|
||||
id="concept-elevations"
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
{% extends 'base/layout.html' %}
|
||||
{% load i18n %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}{% trans "Merge Racks into a Concept" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form action="" method="post">
|
||||
{% csrf_token %}
|
||||
<div class="row">
|
||||
<div class="col col-md-8 offset-md-2">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Merge two racks into one concept" %}</h2>
|
||||
<div class="card-body">
|
||||
<p class="text-muted">
|
||||
{% trans "Snapshots both racks' devices into one new blueprint - the top rack's devices shifted up by the bottom rack's height. Neither rack is modified." %}
|
||||
</p>
|
||||
{% render_form form %}
|
||||
</div>
|
||||
<div class="card-footer text-end">
|
||||
<a href="{{ return_url }}" class="btn btn-outline-danger">{% trans "Cancel" %}</a>
|
||||
<button type="submit" class="btn btn-primary">{% trans "Create concept" %}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user