diff --git a/README.md b/README.md index f16e893..78b218d 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,10 @@ Originally Forked from https://github.com/tbotnz/netbox_floorplan ## Demo ![demo](/media/demo.gif) +## New Demo Showing New Plugin Behavior with Advanced Rack Behavior + +![new-enhancements](/media/new-floorplan-demo.gif) + ## Summary A netbox plugin providing floorplan mapping capability for locations and sites diff --git a/media/new-floorplan-demo.gif b/media/new-floorplan-demo.gif new file mode 100644 index 0000000..e372c43 Binary files /dev/null and b/media/new-floorplan-demo.gif differ diff --git a/netbox_floorplan/models.py b/netbox_floorplan/models.py index f7dc25c..87f5962 100644 --- a/netbox_floorplan/models.py +++ b/netbox_floorplan/models.py @@ -187,6 +187,10 @@ def mapped_devices(self): return drawn_devices def resync_canvas(self): + """ + Synchronize canvas objects with current NetBox data. + Handles both advanced mode (role/tenant/status) and original mode (status only) rack displays. + """ changed = False if self.canvas: if self.canvas.get("objects"): @@ -202,19 +206,93 @@ def resync_canvas(self): else: rack = rack_qs.first() self.canvas["objects"][index]["custom_meta"]["object_name"] = rack.name + + # Update rack fill color based on role (only if not manually set + # and for advanced racks only) + # Check if color was manually set by looking for manual_color flag + color_manually_set = obj["custom_meta"].get("manual_color", False) + + # Detect if this is an advanced rack by checking for info text type + is_advanced_mode_rack = False + if obj.get("objects"): + for subobj in obj["objects"]: + if (subobj.get("type") in ["i-text", "textbox"] and + subobj.get("custom_meta", {}).get("text_type") == "info"): + is_advanced_mode_rack = True + break + + # Only apply automatic color updates to advanced racks + if not color_manually_set and is_advanced_mode_rack: + expected_color = None + if rack.role and hasattr(rack.role, 'color'): + expected_color = f"#{rack.role.color}" + else: + # Default color if no role or color is set + expected_color = "#000000" + + # Check if the rack rectangle color needs updating + if obj.get("objects") and len(obj["objects"]) > 0: + rack_rect = obj["objects"][0] # First object is typically the rack rectangle + if rack_rect.get("fill") != expected_color: + self.canvas["objects"][index]["objects"][0]["fill"] = expected_color + changed = True + # End of rack fill color update + if obj.get("objects"): for subcounter, subobj in enumerate(obj["objects"]): - if subobj.get("type") == "i-text": + # Check if the subobject is a rectangle and has custom_meta for rack + # Update the custom_meta and text fields to match the current rack data + # in Netbox + if subobj.get("type") == "rect": + if subobj.get("custom_meta", {}).get("object_type") == "rack": + # Make sure the object_name matches the actual rack name + if subobj["custom_meta"]["object_name"] != f"{rack.name}": + self.canvas["objects"][index]["objects"][ + subcounter]["custom_meta"]["object_name"] = f"{rack.name}" + changed = True + + # Check if the subobject is a textbox or i-text object. This will have both + # the rack name and the info text (status, role, tenant for advanced racks + # or just status for simple racks). + if subobj.get("type") == "i-text" or subobj.get("type") == "textbox": + # Update the name text box with the current rack name if it exists if subobj.get("custom_meta", {}).get("text_type") == "name": if subobj["text"] != f"{rack.name}": self.canvas["objects"][index]["objects"][ subcounter]["text"] = f"{rack.name}" changed = True - if subobj.get("custom_meta", {}).get("text_type") == "status": + # Handle advanced racks combined info text box + elif subobj.get("custom_meta", {}).get("text_type") == "info": + # Handle combined info text box (advanced mode) + rack_role_text = rack.role.name if rack.role else "" + rack_tenant_text = f"{rack.tenant}" if rack.tenant else "" + + # Update stored values in custom_meta + subobj["custom_meta"]["status"] = f"{rack.status}" + subobj["custom_meta"]["role"] = rack_role_text + subobj["custom_meta"]["tenant"] = rack_tenant_text + + # Rebuild the combined text based on visibility settings + info_lines = [] + if subobj["custom_meta"].get("show_status", True): + info_lines.append(f"{rack.status}") + if subobj["custom_meta"].get("show_role", True) and rack_role_text: + info_lines.append(rack_role_text) + if subobj["custom_meta"].get("show_tenant", True) and rack_tenant_text: + info_lines.append(rack_tenant_text) + + new_text = '\n'.join(info_lines) + if subobj["text"] != new_text: + self.canvas["objects"][index]["objects"][subcounter]["text"] = new_text + changed = True + # Handle simple racks status text box, which only shows status + elif subobj.get("custom_meta", {}).get("text_type") == "status": if subobj["text"] != f"{rack.status}": self.canvas["objects"][index]["objects"][ subcounter]["text"] = f"{rack.status}" changed = True + + # Handle device objects on the canvas if obj["custom_meta"].get("object_type") == "device": device_id = int(obj["custom_meta"]["object_id"]) # if device is not in the database, remove it from the canvas @@ -227,13 +305,45 @@ def resync_canvas(self): self.canvas["objects"][index]["custom_meta"]["object_name"] = device.name if obj.get("objects"): for subcounter, subobj in enumerate(obj["objects"]): - if subobj.get("type") == "i-text": + # Update device rectangle metadata + if subobj.get("type") == "rect": + if subobj.get("custom_meta", {}).get("object_type") == "device": + # Make sure the object_name matches the actual device name + if subobj["custom_meta"]["object_name"] != f"{device.name}": + self.canvas["objects"][index]["objects"][ + subcounter]["custom_meta"]["object_name"] = f"{device.name}" + changed = True + # Update device text elements (supports both advanced and simple devices) + if subobj.get("type") == "i-text" or subobj.get("type") == "textbox": + + # Update device name text if subobj.get("custom_meta", {}).get("text_type") == "name": if subobj["text"] != f"{device.name}": self.canvas["objects"][index]["objects"][ subcounter]["text"] = f"{device.name}" changed = True - if subobj.get("custom_meta", {}).get("text_type") == "status": + # Handle advanced devices combined info text box for devices + elif subobj.get("custom_meta", {}).get("text_type") == "info": + # Handle combined info text box for devices (advanced mode) + device_tenant_text = f"{device.tenant}" if device.tenant else "" + + # Update stored values in custom_meta + subobj["custom_meta"]["status"] = f"{device.status}" + subobj["custom_meta"]["tenant"] = device_tenant_text + + # Rebuild the combined text based on visibility settings + info_lines = [] + if subobj["custom_meta"].get("show_status", True): + info_lines.append(f"{device.status}") + if subobj["custom_meta"].get("show_tenant", True) and device_tenant_text: + info_lines.append(device_tenant_text) + + new_text = '\n'.join(info_lines) + if subobj["text"] != new_text: + self.canvas["objects"][index]["objects"][subcounter]["text"] = new_text + changed = True + # Handle 'simple' devices status text box + elif subobj.get("custom_meta", {}).get("text_type") == "status": if subobj["text"] != f"{device.status}": self.canvas["objects"][index]["objects"][ subcounter]["text"] = f"{device.status}" diff --git a/netbox_floorplan/static/netbox_floorplan/floorplan/edit.js b/netbox_floorplan/static/netbox_floorplan/floorplan/edit.js index c21c6e5..9810a0b 100644 --- a/netbox_floorplan/static/netbox_floorplan/floorplan/edit.js +++ b/netbox_floorplan/static/netbox_floorplan/floorplan/edit.js @@ -23,6 +23,7 @@ var record_type = document.getElementById('record_type').value; var site_id = document.getElementById('site_id').value; var location_id = document.getElementById('location_id').value; + htmx.ajax('GET', `/plugins/floorplan/floorplans/racks/?floorplan_id=${obj_pk}`, { source: '#rack-card', target: '#rack-card', swap: 'innerHTML', trigger: 'load' }) htmx.ajax('GET', `/plugins/floorplan/floorplans/devices/?floorplan_id=${obj_pk}`, { source: '#unrack-card', target: '#unrack-card', swap: 'innerHTML', trigger: 'load' }) @@ -38,6 +39,8 @@ var canvas = new fabric.Canvas('canvas'), canvasWidth = document.getElementById('canvas').width, canvasHeight = document.getElementById('canvas').height; +window.canvas = canvas; + // end initial ----------------------------------------------------------------------------- ! @@ -69,7 +72,9 @@ canvas.on('object:moving', function (options) { // start zoom, pan control & resizing ----------------------------------------------------------------------------- ! -$(window).resize(resize_canvas(canvas, window)); +$(window).resize(function() { + resize_canvas(canvas, window); +}); canvas.on('mouse:wheel', function (opt) { wheel_zoom(opt, canvas); @@ -268,14 +273,16 @@ function add_text() { top: 100, fontSize: 12, textAlign: "left", - fill: "#fff" + fill: "#000000" }); canvas.add(object); canvas.centerObject(object); } window.add_text = add_text; -function add_floorplan_object(top, left, width, height, unit, fill, rotation, object_id, object_name, object_type, status, image) { +// Original plugin code to add a rack or device with only name and status +function add_floorplan_object_simple(top, left, width, height, unit, fill, + rotation, object_id, object_name, object_type, status, image) { var object_width; var object_height; if ( !width || !height || !unit ){ @@ -370,7 +377,7 @@ function add_floorplan_object(top, left, width, height, unit, fill, rotation, ob fontFamily: "Courier New", fontSize: 16, splitByGrapheme: text_offset? null : true, - fill: "#FFFF", + fill: "#FFFFFF", width: object_width, textAlign: "center", originX: "center", @@ -402,7 +409,7 @@ function add_floorplan_object(top, left, width, height, unit, fill, rotation, ob includeDefaultValues: true, centeredRotation: true, shadow: text_offset? new fabric.Shadow({ - color: '#FFF', + color: '#FFFFFF', blur: 1 }) : null, custom_meta: { @@ -436,36 +443,505 @@ function add_floorplan_object(top, left, width, height, unit, fill, rotation, ob canvas.centerObject(group); //canvas.bringToFront(group); } -window.add_floorplan_object = add_floorplan_object; +window.add_floorplan_object_simple = add_floorplan_object_simple; function delete_floorplan_object() { - var object = canvas.getActiveObject(); - if (object) { - canvas.remove(object); - canvas.renderAll(); - } - save_floorplan(); - setTimeout(() => { - htmx.ajax('GET', `/plugins/floorplan/floorplans/racks/?floorplan_id=${obj_pk}`, { target: '#rack-card', swap: 'innerHTML' }); - htmx.ajax('GET', `/plugins/floorplan/floorplans/devices/?floorplan_id=${obj_pk}`, { target: '#unrack-card', swap: 'innerHTML' }); - }, 1500); + // Get all active objects (in case of multiple selections) + var objects = canvas.getActiveObjects(); + objects.forEach(object => { + if (object) { + canvas.remove(object); + canvas.renderAll(); + } + save_floorplan(); + setTimeout(() => { + htmx.ajax('GET', `/plugins/floorplan/floorplans/racks/?floorplan_id=${obj_pk}`, { target: '#rack-card', swap: 'innerHTML' }); + htmx.ajax('GET', `/plugins/floorplan/floorplans/devices/?floorplan_id=${obj_pk}`, { target: '#unrack-card', swap: 'innerHTML' }); + }, 1500); + }); + // Clear the selection after deletion + canvas.discardActiveObject(); + canvas.requestRenderAll(); }; window.delete_floorplan_object = delete_floorplan_object; function set_color(color) { - var object = canvas.getActiveObject(); - if (object) { - if (object.type == "i-text") { - object.set('fill', color); + // Get all active objects (in case of multiple selections) + var objects = canvas.getActiveObjects(); + objects.forEach(object => { + if (object) { + if (object.type == "i-text") { + object.set('fill', color); + canvas.renderAll(); + // Update the color picker to match the selected color + document.getElementById("selected_color").value = color; + return; + } + object._objects[0].set('fill', color); + + // Mark that color was manually set if this is a rack or device object + if (object.custom_meta && (object.custom_meta.object_type === "rack" || + object.custom_meta.object_type === "device")) { + object.custom_meta.manual_color = true; + } + + //canvas.renderAll(); + // Update the color picker to match the selected color + document.getElementById("selected_color").value = color; + + } + }); + canvas.renderAll(); +} +window.set_color = set_color; + +// Start of helper functions for advanced racks/devices + +// Calculate the correct text height so textboxes don't overlap for advanced racks +function calculateDynamicTextHeight(textContent, fontSize, textWidth) { + // Return minimum height for empty, null, or whitespace-only text + if (!textContent || textContent.trim() === "") { + return fontSize + 6; + } + + var tempText = new fabric.Textbox(textContent, { + fontSize: fontSize, + width: textWidth + 3, + fontFamily: "Courier New", + splitByGrapheme: true, + breakWords: true, + wordWrap: true, + }); + + // Create a temporary canvas to properly measure the text + var tempCanvas = new fabric.StaticCanvas(); + tempCanvas.add(tempText); + + // Get the actual rendered height + var measuredHeight = tempText.height; + + // Clean up + tempCanvas.dispose(); + + // Add generous padding for multi-line text + return Math.max(measuredHeight + 6, fontSize * 1.5); // Ensure minimum height +} + +// Calculate optimal font size to fit text within available space +// Assistance from Github Copilot used to create this function +function calculateOptimalFontSize(textContent, maxWidth, maxHeight, minFontSize = 8, maxFontSize = 13) { + if (!textContent || textContent.trim() === "") { + return maxFontSize; + } + + var optimalSize = maxFontSize; + + // Binary search for optimal font size + var low = minFontSize; + var high = maxFontSize; + + var tempCanvas = null; + var tempText = null; + + try { + // Create a single canvas instance outside the loop + tempCanvas = new fabric.StaticCanvas(); + + while (low <= high) { + var mid = Math.floor((low + high) / 2); + + // Remove previous temporary text object if it exists + if (tempText) { + tempCanvas.remove(tempText); + } + + // Create new text object with current font size + tempText = new fabric.Textbox(textContent, { + fontSize: mid, + width: maxWidth, + fontFamily: "Courier New", + splitByGrapheme: false, + breakWords: true, + wordWrap: true, + }); + + // Clear the canvas before adding the text + tempCanvas.clear(); + tempCanvas.add(tempText); + + var textHeight = tempText.height; + var textWidth = tempText.width; + + // Check both height and width constraints + if (textHeight <= maxHeight && textWidth <= maxWidth) { + optimalSize = mid; + low = mid + 1; + } else { + high = mid - 1; + } + } + + return Math.max(optimalSize, minFontSize); + } finally { + // Clean up all resources + if (tempCanvas) { + if (tempText) { + tempCanvas.remove(tempText); + } + tempCanvas.clear(); + tempCanvas.dispose(); + } + } +} + +// Create combined info text that shows status, role, and tenant +function buildInfoText(status, role, tenant) { + var infoLines = []; + + if (status) { + infoLines.push(status); + } + if (role) { + infoLines.push(role); + } + if (tenant) { + infoLines.push(tenant); + } + + return infoLines.join('\n'); +} + +// End of helper functions for advanced racks/devices + +// Add a rack or device with additional information (text color, role, tenant) compared to "Simple" rack/device which +// only shows name and status +function add_floorplan_object_advanced(top, left, width, height, unit, fill, rotation, object_id, object_name, + object_type, status, tenant, role, image, text_color) { + // Set default text color (blue) if not provided + if (!text_color) { + text_color = "#000000"; + } + var object_width; + var object_height; + if ( !width || !height || !unit ){ + object_width = 60; + object_height = 91; + } else { + var conversion_scale = 100; + console.log("width: " + width) + console.log("unit: " + unit) + console.log("height: " + height) + if (unit == "in") { + var new_width = (width * 0.0254) * conversion_scale; + var new_height = (height * 0.0254) * conversion_scale; + } else { + var new_width = (width / 1000) * conversion_scale; + var new_height = (height / 1000) * conversion_scale; + } + + object_width = parseFloat(new_width.toFixed(2)); + console.log(object_width) + object_height = parseFloat(new_height.toFixed(2)); + console.log(object_height) + } + document.getElementById(`object_${object_type}_${object_id}`).remove(); + /* if we have an image, we display the text below, otherwise we display the text within */ + var rect, text_offset = 0; + // Variable used to move all text height up or down + var heightAdjustment = -35; + if (!image) { + rect = new fabric.Rect({ + top: top, + name: "rectangle", + left: left, + width: object_width, + height: object_height, + fill: fill, + opacity: 0.8, + lockRotation: false, + originX: "center", + originY: "center", + cornerSize: 15, + hasRotatingPoint: true, + perPixelTargetFind: true, + minScaleLimit: 1, + maxWidth: canvasWidth, + maxHeight: canvasHeight, + centeredRotation: true, + custom_meta: { + "object_type": object_type, + "object_id": object_id, + "object_name": object_name, + "object_url": "/dcim/" + object_type + "s/" + object_id + "/", + }, + }); + } else { + object_height = object_width; + text_offset = object_height/2 + 4; + rect = new fabric.Image(null, { + top: top, + name: "rectangle", + left: left, + width: object_width, + height: object_height, + opacity: 1, + lockRotation: false, + originX: "center", + originY: "center", + cornerSize: 15, + hasRotatingPoint: true, + perPixelTargetFind: true, + minScaleLimit: 1, + maxWidth: canvasWidth, + maxHeight: canvasHeight, + centeredRotation: true, + shadow: new fabric.Shadow({ + color: "red", + blur: 15, + }), + custom_meta: { + "object_type": object_type, + "object_id": object_id, + "object_name": object_name, + "object_url": "/dcim/" + object_type + "s/" + object_id + "/", + }, + }); + rect.setSrc("/media/" + image, function(img){ + img.scaleX = object_width / img.width; + img.scaleY = object_height / img.height; canvas.renderAll(); - return; + }); + } + + var text = new fabric.Textbox(object_name, { + fontFamily: "Courier New", + fontSize: 16, + fontWeight: "bold", + splitByGrapheme: text_offset? null : true, + fill: text_color, + width: object_width, + textAlign: "center", + originX: "center", + originY: "center", + left: left, + top: top + text_offset + heightAdjustment, + excludeFromExport: false, + includeDefaultValues: true, + centeredRotation: true, + custom_meta: { + "text_type": "name", } - object._objects[0].set('fill', color); - canvas.renderAll(); - return; + }); + + // Calculate dynamic spacing for all textboxes + var currentOffset = text_offset + heightAdjustment; + + // Add name text height to offset + var nameHeight = calculateDynamicTextHeight(object_name, 16, object_width); + currentOffset += nameHeight; // Add 6px padding between name and info + + // Ensure role and tenant have default values + if(!role) { + role = "" + } + if(!tenant) { + tenant = "" + } + + var infoText = buildInfoText(status, role, tenant); + + // Calculate available height for info box (you can adjust this based on your layout needs) + var availableHeight = object_height * 0.5; // Use 50% of object height for info text + + // Use slightly less than full width to ensure padding and prevent overflow + var availableWidth = object_width * 0.95; // Use 95% of object width for safety + + // Calculate optimal font size for the info text + var optimalFontSize = calculateOptimalFontSize(infoText, availableWidth, availableHeight, 8, 13); + + var info_box = new fabric.Textbox(infoText, { + fontFamily: "Courier New", + fontSize: optimalFontSize, + fill: text_color, + width: object_width, + splitByGrapheme: false, + breakWords: true, + wordWrap: true, + borderColor: "6ea8fe", + textAlign: "center", + originX: "center", + left: left, + top: top + currentOffset, + excludeFromExport: false, + includeDefaultValues: true, + centeredRotation: true, + shadow: text_offset? new fabric.Shadow({ + color: '#000000', + blur: 1 + }) : null, + custom_meta: { + "text_type": "info", + "status": status, + "role": role, + "tenant": tenant, + "show_status": true, + "show_role": true, + "show_tenant": true + } + }); + + var group = new fabric.Group([rect, text, info_box]); + + group.custom_meta = { + "object_type": object_type, + "object_id": object_id, + "object_name": object_name, + "object_url": "/dcim/" + object_type + "s/" + object_id + "/", + } + group.setControlsVisibility({ + mt: false, + mb: false, + ml: false, + mr: false, + bl: false, + br: false, + tl: false, + tr: false, + }) + + if (object_id) { + group.set('id', object_id); } + + canvas.add(group); + canvas.centerObject(group); } -window.set_color = set_color; +window.add_floorplan_object_advanced = add_floorplan_object_advanced; + +function set_text_color(color) { + // Get all active objects (in case of multiple selections) + var objects = canvas.getActiveObjects(); + objects.forEach(object => { + if (object) { + // If it's a text object, change its color (IText or Textbox) + if (object.type == "i-text" || object.type == "textbox") { + object.set('fill', color); + canvas.renderAll(); + // Update the color picker to match the selected color + document.getElementById("selected_text_color").value = color; + return; + } + + // If it's a group (like a rack), find and update all text objects within it + if (object._objects) { + object._objects.forEach(function(obj) { + if (obj.type == "i-text" || obj.type == "textbox") { + obj.set('fill', color); + } + }); + + // Mark that text color was manually set if this is a rack or device object + if (object.custom_meta && (object.custom_meta.object_type === "rack" || + object.custom_meta.object_type === "device")) { + object.custom_meta.manual_text_color = true; + } + + // Update the color picker to match the selected color + document.getElementById("selected_text_color").value = color; + + } + } + }); + canvas.renderAll(); +} + +window.set_text_color = set_text_color; + +function toggle_text_visibility(text_type, visible) { + // Get all active objects (in case of multiple selections) + var objects = canvas.getActiveObjects(); + objects.forEach(object => { + // If there is an object (rack or device) selected + if (object && object._objects) { + // Find the combined info box + object._objects.forEach(function(obj) { + if (obj.custom_meta && obj.custom_meta.text_type === "info") { + // Update the visibility flag for the specific text type + obj.custom_meta['show_' + text_type] = visible; + + // Rebuild the text content based on current visibility settings + var infoLines = []; + + if (obj.custom_meta.show_status && obj.custom_meta.status) { + infoLines.push(obj.custom_meta.status); + } + if (obj.custom_meta.show_role && obj.custom_meta.role) { + infoLines.push(obj.custom_meta.role); + } + if (obj.custom_meta.show_tenant && obj.custom_meta.tenant) { + infoLines.push(obj.custom_meta.tenant); + } + + var newText = infoLines.join('\n'); + + // Recalculate optimal font size for the new text content + var availableHeight = object.height * 0.5; // Use 50% of object height + var availableWidth = obj.width * 0.95; // Use 95% of object width for safety + var optimalFontSize = calculateOptimalFontSize(newText, availableWidth, availableHeight, 8, 13); + + // Update the text content and font size + obj.set('text', newText); + obj.set('fontSize', optimalFontSize); + + // Hide the entire box if no info is visible + var hasVisibleContent = obj.custom_meta.show_status || obj.custom_meta.show_role || obj.custom_meta.show_tenant; + obj.set('visible', hasVisibleContent && infoLines.length > 0); + } + }); + //canvas.renderAll(); + //save_floorplan(); + } + }); + canvas.renderAll(); + save_floorplan(); +} +window.toggle_text_visibility = toggle_text_visibility; + +function update_text_visibility_controls() { + // Get all active objects (in case of multiple selections) + var objects = canvas.getActiveObjects(); + objects.forEach(object => { + if (object && object._objects) { + // Find the combined info box and get visibility settings + var statusVisible = true, tenantVisible = true, roleVisible = true; + + object._objects.forEach(function(obj) { + if (obj.custom_meta && obj.custom_meta.text_type === 'info') { + statusVisible = obj.custom_meta.show_status !== false; + tenantVisible = obj.custom_meta.show_tenant !== false; + roleVisible = obj.custom_meta.show_role !== false; + } + }); + + // Update checkbox state based on current visibility settings + document.getElementById('show_status').checked = statusVisible; + document.getElementById('show_tenant').checked = tenantVisible; + document.getElementById('show_role').checked = roleVisible; + + } else { + // When no object is selected, reset to default state but keep controls enabled + document.getElementById('show_status').checked = true; + document.getElementById('show_tenant').checked = true; + document.getElementById('show_role').checked = true; + + // Keep all checkboxes enabled when no object is selected + document.getElementById('show_status').disabled = false; + document.getElementById('show_tenant').disabled = false; + document.getElementById('show_role').disabled = false; + } + }); +} +window.update_text_visibility_controls = update_text_visibility_controls; function set_zoom(new_current_zoom) { current_zoom = new_current_zoom; @@ -661,7 +1137,7 @@ function update_dimensions() { var text = new fabric.IText(`${obj_name}`, { fontFamily: "Courier New", fontSize: 16, - fill: "#FFFF", + fill: "#000000", textAlign: "center", originX: "center", originY: "center", @@ -675,7 +1151,7 @@ function update_dimensions() { var dimensions = new fabric.IText(`${width} ${measurement_unit} (width) x ${height} ${measurement_unit} (height)`, { fontFamily: "Courier New", fontSize: 8, - fill: "#FFFF", + fill: "#000000", textAlign: "center", originX: "center", originY: "center", @@ -828,5 +1304,7 @@ window.save_and_redirect = save_and_redirect; // end save floorplan ----------------------------------------------------------------------------- ! // start initialize load ----------------------------------------------------------------------------- ! -document.addEventListener("DOMContentLoaded", init_floor_plan(obj_pk, canvas, "edit")); +document.addEventListener("DOMContentLoaded", function() { + init_floor_plan(obj_pk, canvas, "edit"); +}); // end initialize load ----------------------------------------------------------------------------- ! diff --git a/netbox_floorplan/static/netbox_floorplan/floorplan/utils.js b/netbox_floorplan/static/netbox_floorplan/floorplan/utils.js index f96e5bd..9dd7015 100644 --- a/netbox_floorplan/static/netbox_floorplan/floorplan/utils.js +++ b/netbox_floorplan/static/netbox_floorplan/floorplan/utils.js @@ -3,6 +3,7 @@ export { export_svg, enable_button_selection, disable_button_selection, + updateColorPickers, prevent_leaving_canvas, wheel_zoom, reset_zoom, @@ -65,17 +66,85 @@ function export_svg(canvas) { link.href = locfilesrc; link.download = "floorplan.svg"; link.click(); + // Clean up the URL object to prevent memory leaks + setTimeout(function() { + URL.revokeObjectURL(locfilesrc); + }, 100); } function enable_button_selection() { - document.getElementById("selected_color").value = "#000000"; + // Get current colors from selected object and update color pickers + updateColorPickers(); $(".tools").removeClass("disabled"); + + // Update text visibility controls. Needed when rack or device is selected + // to make function exists before calling it + if (typeof window.update_text_visibility_controls === 'function') { + window.update_text_visibility_controls(); + } +} + +function updateColorPickers() { + var canvas = window.canvas; + if (!canvas) { + return; + } + + var object = canvas.getActiveObject(); + var objectColor = "#000000"; // Default + var textColor = "#6EA8FE"; // Default + + if (object) { + // For single text objects + if (object.type === "i-text" || object.type === "textbox") { + objectColor = textColor = object.fill || "#000000"; + } + // For groups (like racks/devices) + else if (object._objects) { + // Get object color from first object (usually the rectangle) + if (object._objects[0]) { + objectColor = object._objects[0].fill || "#000000"; + } + + // Get text color from first text object found + for (var i = 0; i < object._objects.length; i++) { + if (object._objects[i].type === "i-text" || object._objects[i].type === "textbox") { + textColor = object._objects[i].fill || "#6EA8FE"; + break; + } + } + } + } + + // Convert colors to hex format using Fabric.js Color class + try { + objectColor = "#" + new fabric.Color(objectColor).toHex(); + } catch (e) { + objectColor = "#000000"; // Fallback to default + } + + try { + textColor = "#" + new fabric.Color(textColor).toHex(); + } catch (e) { + textColor = "#6EA8FE"; // Fallback to default + } + + // Update color picker values + document.getElementById("selected_color").value = objectColor; + document.getElementById("selected_text_color").value = textColor; } function disable_button_selection() { // set color to default - document.getElementById("selected_color").value = "#000000"; + document.getElementById("selected_color").value = "#000000"; // Default color black + document.getElementById("selected_text_color").value = "#6EA8FE"; // Default color blue $(".tools").addClass("disabled"); + + // Update text visibility controls. Needed when rack or device is selected + // to make function exists before calling it + if (typeof window.update_text_visibility_controls === 'function') { + window.update_text_visibility_controls(); + } } function prevent_leaving_canvas(e, canvas) { diff --git a/netbox_floorplan/static/netbox_floorplan/floorplan/view.js b/netbox_floorplan/static/netbox_floorplan/floorplan/view.js index b8357cf..516a90e 100644 --- a/netbox_floorplan/static/netbox_floorplan/floorplan/view.js +++ b/netbox_floorplan/static/netbox_floorplan/floorplan/view.js @@ -30,7 +30,9 @@ canvas.on('mouse:down', function (options) { // start zoom, pan control & resizing ----------------------------------------------------------------------------- ! -$(window).resize(resize_canvas(canvas, window)); +$(window).resize(function() { + resize_canvas(canvas, window); +}); canvas.on('mouse:wheel', function (opt) { wheel_zoom(opt, canvas); @@ -56,4 +58,6 @@ document.getElementById('export_svg').addEventListener('click', () => { let floorplan_id = document.getElementById('floorplan_id').value; -document.addEventListener("DOMContentLoaded", init_floor_plan(floorplan_id, canvas, "readonly")); +document.addEventListener("DOMContentLoaded", function() { + init_floor_plan(floorplan_id, canvas, "readonly"); +}); diff --git a/netbox_floorplan/tables.py b/netbox_floorplan/tables.py index 14790e4..69312f5 100644 --- a/netbox_floorplan/tables.py +++ b/netbox_floorplan/tables.py @@ -3,7 +3,7 @@ from netbox.tables import NetBoxTable from .models import Floorplan, FloorplanImage -from dcim.models import Rack +from dcim.models import Rack, Device class FloorplanImageTable(NetBoxTable): @@ -35,15 +35,35 @@ class FloorplanRackTable(NetBoxTable): name = tables.LinkColumn() + role = tables.TemplateColumn( + # Show the role name if it exists, otherwise show "None" on the edit_floorplan view + template_code=""" + {% if record.role %} + {{ record.role.name }} + {% else %} + None + {% endif %} + """, + verbose_name="Role" + ) + actions = tables.TemplateColumn(template_code=""" - Add Rack - - """) +
+ {% if record.role and record.role.color %} + Simple
Rack
+ Advanced
Rack
+ {% else %} + Simple
Rack
+ Advanced
Rack
+ {% endif %} +
+ """, orderable=False) class Meta(NetBoxTable.Meta): model = Rack - fields = ('pk', 'name', 'u_height') - default_columns = ('pk', 'name', 'u_height') + # Show the Rack name, role, and U-height in the table + fields = ('pk', 'name', 'role', 'u_height') + default_columns = ('pk', 'name', 'role', 'u_height') row_attrs = { 'id': lambda record: 'object_rack_{}'.format(record.pk), } @@ -54,12 +74,14 @@ class FloorplanDeviceTable(NetBoxTable): name = tables.LinkColumn() actions = tables.TemplateColumn(template_code=""" - Add Device - - """) +
+ Simple
Device
+ Advanced
Device
+
+ """, orderable=False) class Meta(NetBoxTable.Meta): - model = Rack + model = Device fields = ('pk', 'name', 'device_type') default_columns = ('pk', 'name', 'device_type') row_attrs = { diff --git a/netbox_floorplan/templates/netbox_floorplan/floorplan_edit.html b/netbox_floorplan/templates/netbox_floorplan/floorplan_edit.html index d70ad77..331600d 100644 --- a/netbox_floorplan/templates/netbox_floorplan/floorplan_edit.html +++ b/netbox_floorplan/templates/netbox_floorplan/floorplan_edit.html @@ -45,7 +45,7 @@
-
+
Controls
@@ -70,6 +70,10 @@
Controls
+ + Simple Rack/Device: Add a rack/device that shows name + status only
+ Advanced Rack/Device: Add a rack/device that shows name + status + role + tenant +