<bmx-rack-designer>
Plans the racks of a server room or data centre: rows of racks drawn as elevations, front and rear; devices placed from a catalog and moved by dragging or from the keyboard, unit by unit; PDUs with their outlets and phases; cables between ports, traced end to end through patch panels; and the sums that matter - space, power per phase against each breaker (and with a supply lost), weight and heat - with a list of everything wrong: overlaps, devices deeper than the rack, overweight racks, overloaded phases, devices without a second supply, outlets used twice, cables to ports that are not there. Find room for a new device across the site; export the site as JSON, devices and cables as CSV, elevations as SVG or a printable PDF.
14 properties · 3 events · 16 methods · 5 parts
Example
Try it with your own plans in confidence: the site is kept in your browser, on your device. Nothing is uploaded, no copy reaches us or anyone else, and nothing is kept once you close this tab unless you export it.
Drag a device from the list onto a rack, or select a rack and press Enter on a device.
Show markup
<bmx-rack-designer id="ex-rack" label="Comms room, ground floor" style="--bmx-rack-designer-height: 40rem"></bmx-rack-designer>
<p class="note" id="ex-rack-out" role="status" style="margin: 12px 0 0">Drag a device from the list onto a rack, or select a rack and press Enter on a device.</p>
<script type="module">
await customElements.whenDefined('bmx-rack-designer');
const designer = document.getElementById('ex-rack');
const out = document.getElementById('ex-rack-out');
// An invented comms room: two 42U racks with A and B power, a pair of core switches, servers, storage and a
// patch-panel trunk between the racks. One server has only one supply and one rack is a little heavy, so the
// Power and Report tabs have something to say. Names, models and numbers are made up.
const panel = n => Array.from({ length: n }, (_, i) => [
{ id: `${i + 1}f`, name: `${i + 1} front`, kind: 'rj45', face: 'front', passThrough: `${i + 1}r` },
{ id: `${i + 1}r`, name: `${i + 1} rear`, kind: 'rj45', face: 'rear', passThrough: `${i + 1}f` },
]).flat();
const nics = (n, kind = 'rj45') => Array.from({ length: n }, (_, i) => ({ id: `eth${i + 1}`, name: `eth${i + 1}`, kind, face: 'rear' }));
const pdus = rack => [
{ id: `${rack}-pa`, name: 'PDU A', phases: 1, voltage: 230, ampsPerPhase: 16, outlets: 24, mount: 'left', feed: 'A', weightKg: 4 },
{ id: `${rack}-pb`, name: 'PDU B', phases: 1, voltage: 230, ampsPerPhase: 16, outlets: 24, mount: 'right', feed: 'B', weightKg: 4 },
];
const dual = (rack, outlet) => [{ pdu: `${rack}-pa`, outlet }, { pdu: `${rack}-pb`, outlet }];
designer.catalog = [
{ id: 'acme-r250', name: 'Northwind R250', type: 'server', model: 'R250 (example)', heightU: 1, depthMm: 720, powerW: 280, maxPowerW: 600, weightKg: 16, psus: 2, ports: [{ kind: 'rj45', count: 2, prefix: 'eth', face: 'rear' }] },
{ id: 'acme-s48', name: 'Northwind S48 switch', type: 'switch', model: 'S48 (example)', heightU: 1, depthMm: 420, powerW: 130, weightKg: 6, psus: 2, ports: [{ kind: 'rj45', count: 48, prefix: 'ge', face: 'front' }] },
];
designer.site = {
name: 'Comms room',
rows: [{ name: 'Row 1', racks: [
{ id: 'a', name: 'R1-01', heightU: 42, depthMm: 1070, maxWeightKg: 300, powerBudgetW: 4000, coolingW: 5000, pdus: pdus('a'),
reserved: [{ fromU: 20, toU: 22, label: 'Future SAN' }],
devices: [
{ id: 'pp-a', name: 'Patch A', type: 'patch-panel', u: 42, ports: panel(12) },
{ id: 'core1', name: 'Core switch 1', type: 'switch', u: 40, depthMm: 450, powerW: 220, maxPowerW: 400, weightKg: 9, feeds: dual('a', 1), ports: nics(24) },
{ id: 'fw1', name: 'Firewall 1', type: 'firewall', u: 38, depthMm: 480, powerW: 110, weightKg: 7, feeds: dual('a', 2), ports: nics(8) },
{ id: 'web1', name: 'web-01', type: 'server', u: 30, depthMm: 750, powerW: 340, weightKg: 18, owner: 'Web', feeds: dual('a', 3), ports: nics(4) },
{ id: 'web2', name: 'web-02', type: 'server', u: 29, depthMm: 750, powerW: 340, weightKg: 18, owner: 'Web', feeds: dual('a', 4), ports: nics(4) },
{ id: 'db1', name: 'db-01', type: 'server', u: 26, heightU: 2, depthMm: 780, powerW: 620, weightKg: 29, owner: 'Data', feeds: dual('a', 5), ports: nics(4) },
{ id: 'san', name: 'Storage 1', type: 'storage', u: 10, heightU: 4, depthMm: 900, powerW: 880, weightKg: 62, owner: 'Data', feeds: dual('a', 6), ports: nics(4, 'sfp') },
{ id: 'ups1', name: 'UPS 1', type: 'ups', u: 1, heightU: 2, depthMm: 650, powerW: 60, weightKg: 32, feeds: [{ pdu: 'a-pa', outlet: 24 }] },
{ id: 'blank1', name: 'Blank', type: 'blank', u: 31, heightU: 1 },
] },
{ id: 'b', name: 'R1-02', heightU: 42, depthMm: 1070, maxWeightKg: 220, powerBudgetW: 3000, pdus: pdus('b'),
devices: [
{ id: 'pp-b', name: 'Patch B', type: 'patch-panel', u: 42, ports: panel(12) },
{ id: 'core2', name: 'Core switch 2', type: 'switch', u: 40, depthMm: 450, powerW: 220, weightKg: 9, feeds: dual('b', 1), ports: nics(24) },
{ id: 'app1', name: 'app-01', type: 'server', u: 32, heightU: 2, depthMm: 780, powerW: 510, weightKg: 27, owner: 'Apps', feeds: dual('b', 2), ports: nics(4) },
{ id: 'app2', name: 'app-02', type: 'server', u: 30, heightU: 2, depthMm: 780, powerW: 510, weightKg: 27, owner: 'Apps', status: 'planned', feeds: [{ pdu: 'b-pa', outlet: 3 }], ports: nics(4) },
{ id: 'bk1', name: 'Backup 1', type: 'storage', u: 8, heightU: 4, depthMm: 900, powerW: 780, weightKg: 70, owner: 'Data', feeds: dual('b', 4), ports: nics(2, 'sfp') },
{ id: 'old1', name: 'legacy-01', type: 'server', u: 20, heightU: 2, depthMm: 800, powerW: 450, weightKg: 30, status: 'decommission', feeds: dual('b', 5), ports: nics(2) },
{ id: 'kvm', name: 'Console', type: 'kvm', u: 24, depthMm: 600, powerW: 40, weightKg: 12, feeds: [{ pdu: 'b-pb', outlet: 6 }] },
] },
] }],
cables: [
{ from: { device: 'web1', port: 'eth1' }, to: { device: 'core1', port: 'eth1' }, kind: 'copper', lengthM: 2 },
{ from: { device: 'web2', port: 'eth1' }, to: { device: 'core1', port: 'eth2' }, kind: 'copper', lengthM: 2 },
{ from: { device: 'db1', port: 'eth1' }, to: { device: 'core1', port: 'eth3' }, kind: 'copper', lengthM: 2 },
{ from: { device: 'core1', port: 'eth24' }, to: { device: 'pp-a', port: '1f' }, kind: 'copper', lengthM: 1 },
{ from: { device: 'pp-a', port: '1r' }, to: { device: 'pp-b', port: '1r' }, kind: 'copper', label: 'Trunk 1', lengthM: 3 },
{ from: { device: 'pp-b', port: '1f' }, to: { device: 'core2', port: 'eth24' }, kind: 'copper', lengthM: 1 },
{ from: { device: 'app1', port: 'eth1' }, to: { device: 'core2', port: 'eth1' }, kind: 'copper', lengthM: 2 },
],
};
designer.addEventListener('bmxSelect', async e => {
if (!e.detail.device) return;
const site = await designer.getSite();
const d = site.rows.flatMap(r => r.racks).flatMap(r => r.devices).find(x => x.id === e.detail.device);
if (d) out.textContent = `${d.name}: U${d.u}${d.heightU > 1 ? `–${d.u + d.heightU - 1}` : ''}, ${d.powerW} W, ${d.weightKg} kg. Up and Down move it; open Cables to trace its ports.`;
});
designer.addEventListener('bmxChange', async () => {
const issues = await designer.getIssues();
const errors = issues.filter(i => i.severity === 'error').length;
out.textContent = `${issues.length} checks to look at (${errors} errors). The Report tab lists them.`;
});
</script>
<bmx-rack-designer src="/plans/hall-1.json" label="Hall 1"></bmx-rack-designer>
The site is plain JSON (site, two-way); the catalog of devices to place is
yours (catalog), with generic templates beside it. Everything runs in the
page; nothing is uploaded.
Keyboard: each device in an elevation takes focus with Tab. Up and Down move it by a unit, Alt with Left and Right to the next rack, F turns it to the other face, Delete removes it, Enter shows its details. In the catalog, Enter places the device in the selected rack, in the first place it fits.
Properties
| Property | Attribute | Type | Default | Description |
|---|---|---|---|---|
catalog |
catalog |
BmxRackTemplate[] | string |
[] |
Device templates to place, as an array or JSON (your makes and models). |
colorMode |
color-mode |
BmxRackColorMode |
'type' |
How devices are coloured. Two-way. |
face |
face |
'front' | 'rear' | 'both' |
'front' |
Which face of the racks is shown. Two-way. |
genericTemplates |
generic-templates |
boolean |
true |
Offer the generic templates (servers, switches, panels, blanks…) beside your catalog. |
label |
label |
string |
— | The name shown in the toolbar and to assistive technology. Default: the site's name. |
panel |
panel |
boolean |
true |
Show the side panel. Two-way (the toolbar's panel button). |
readonly |
readonly |
boolean |
false |
The site can be looked at but not changed. |
row |
row |
string |
— | The row shown, by id. Default: the first. Two-way. |
site |
site |
BmxRackSite | string | null |
null |
The site: rows of racks, their devices and PDUs, and the cables, as an object or JSON. Two-way: bmxChange reports every edit. |
src |
src |
string |
— | An address to read the site's JSON from. |
strings |
strings |
Partial<BmxRackDesignerStrings> | string |
— | Words to show instead of the English ones, as an object or JSON. |
toolbar |
toolbar |
boolean |
true |
Show the toolbar. |
unitHeight |
unit-height |
number |
18 |
Pixels per rack unit. Two-way. |
units |
units |
'metric' | 'imperial' |
'metric' |
Show lengths and weights in millimetres and kilograms, or inches and pounds. The site's file is always metric. |
Events
| Event | Detail | Description |
|---|---|---|
bmxChange |
{ readonly site: BmxRackSite; } |
The site changed (any edit, an undo or a redo). |
bmxError |
{ readonly message: string; } |
A site could not be read. |
bmxSelect |
{ readonly device: string | null; readonly rack: string | null; } |
The selection changed. |
Methods
| Method | Signature | Description |
|---|---|---|
addDevice |
addDevice(rackId: string, template: string | BmxRackTemplate, u?: number, face?: BmxRackFace) => Promise<string | null> |
Places a device - a template from the catalog by id, or a device - in a rack at a unit. Returns the new device's id, or null when it does not fit. |
addRack |
addRack(rack?: Partial<BmxRack>, rowId?: string) => Promise<string> |
Adds a rack to a row (the shown row when not given). Returns its id. |
connect |
connect(from: BmxRackEnd, to: BmxRackEnd, extra?: { kind?: string; label?: string; lengthM?: number; color?: string; }) => Promise<string> |
Joins two ports with a cable. Returns its id. |
export |
export(kind: "json" | "devices" | "cables" | "svg" | "pdf", options?: { racks?: string[]; face?: "front" | "rear" | "both"; paper?: "a4" | "letter"; }) => Promise<Blob> |
The site or its parts as a file: JSON, the devices or the cables as CSV, the shown racks' elevations as SVG or PDF. |
findSpace |
findSpace(request: BmxSpaceRequest) => Promise<BmxSpaceCandidate[]> |
Where a device would fit. |
getIssues |
getIssues() => Promise<BmxRackIssue[]> |
Everything wrong or worth a look. |
getSite |
getSite() => Promise<BmxRackSite> |
The site as it stands. |
getSummary |
getSummary() => Promise<ReturnType<typeof summariseSite>> |
The site's sums: per rack and in total. |
importCsv |
importCsv(text: string) => Promise<{ added: number; skipped: { line: number; reason: string; }[]; }> |
Adds the devices or cables in a CSV (which, by its header). Returns what was added and skipped. |
load |
load(data: string | BmxRackSite | Blob | Record<string, unknown>) => Promise<void> |
Opens a site: JSON text, an object, or a file (JSON, or a devices or cables CSV added to the current site). |
moveDevice |
moveDevice(id: string, rackId: string, u: number, face?: BmxRackFace) => Promise<boolean> |
Moves a device. Returns false when it does not fit there. |
redo |
redo() => Promise<void> |
|
removeDevice |
removeDevice(id: string) => Promise<void> |
|
select |
select(device: string | null, rack?: string | null) => Promise<void> |
Selects a device (and its rack), or a rack alone, or nothing. |
tracePort |
tracePort(device: string, port: string) => Promise<BmxRackTrace> |
Traces the circuit a port is on. |
undo |
undo() => Promise<void> |
CSS shadow parts
| Part | Description |
|---|---|
catalog |
The list of devices to place. |
panel |
The side panel (details, power, cables, report, find space). |
stage |
The racks. |
status |
The line under the racks. |
toolbar |
The bar of commands. |
CSS custom properties
| Property | Description |
|---|---|
--bmx-rack-designer-bay |
The empty space between the rails. |
--bmx-rack-designer-catalog-width |
The catalog's width. |
--bmx-rack-designer-height |
The height of the stage. |
--bmx-rack-designer-panel-width |
The side panel's width. |
--bmx-rack-designer-radius |
Corner radius of the frame. |
--bmx-rack-designer-rail |
The rack's frame and rails. |
--bmx-rack-designer-selected |
The selected device's outline. |
--bmx-rack-designer-unit-line |
The line between units. |