v3.0.0

<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

PropertyAttributeTypeDefaultDescription
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

EventDetailDescription
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

MethodSignatureDescription
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

PartDescription
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

PropertyDescription
--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.