From 17907151b34bb6efdbd4370cd449e21dcc8eed54 Mon Sep 17 00:00:00 2001 From: Rachel Powers <508861+Ryex@users.noreply.github.com> Date: Mon, 22 Apr 2024 22:45:21 -0700 Subject: [PATCH] feat(slots UI): much better slot occupant card --- www/src/ts/virtual_machine/base_device.ts | 47 +- .../ts/virtual_machine/device/VMDeviceCard.ts | 458 ------------------ www/src/ts/virtual_machine/device/card.ts | 116 +---- www/src/ts/virtual_machine/device/slot.ts | 181 +++++++ www/src/ts/virtual_machine/device/template.ts | 30 +- 5 files changed, 241 insertions(+), 591 deletions(-) delete mode 100644 www/src/ts/virtual_machine/device/VMDeviceCard.ts create mode 100644 www/src/ts/virtual_machine/device/slot.ts diff --git a/www/src/ts/virtual_machine/base_device.ts b/www/src/ts/virtual_machine/base_device.ts index bd0be50..3676589 100644 --- a/www/src/ts/virtual_machine/base_device.ts +++ b/www/src/ts/virtual_machine/base_device.ts @@ -14,7 +14,8 @@ import type { Pins, } from "ic10emu_wasm"; import { structuralEqual } from "utils"; -import { LitElement } from "lit"; +import { LitElement, PropertyValueMap } from "lit"; +import type { DeviceDB } from "./device_db"; type Constructor = new (...args: any[]) => T; @@ -46,7 +47,7 @@ export const VMDeviceMixin = >( superClass: T, ) => { class VMDeviceMixinClass extends superClass { - _deviceID: number; + private _deviceID: number; get deviceID() { return this._deviceID; } @@ -181,6 +182,11 @@ export const VMDeviceMixin = >( this.pins = pins; } } + + update(changedProperties: PropertyValueMap | Map): void { + super.update(changedProperties); + this.updateDevice(); + } } return VMDeviceMixinClass as Constructor & T; }; @@ -215,5 +221,42 @@ export const VMActiveICMixin = >( this.updateDevice(); } } + return VMActiveICMixinClass as Constructor & T; }; + +export declare class VMDeviceDBMixinInterface { + deviceDB: DeviceDB; + _handleDeviceDBLoad(e: CustomEvent): void +} + +export const VMDeviceDBMixin = >(superClass: T) => { + class VMDeviceDBMixinClass extends superClass { + + connectedCallback(): void { + const root = super.connectedCallback(); + window.VM.vm.addEventListener( + "vm-device-db-loaded", + this._handleDeviceDBLoad.bind(this), + ); + return root; + } + + _handleDeviceDBLoad(e: CustomEvent) { + this.deviceDB = e.detail; + } + + private _deviceDB: DeviceDB; + + get deviceDB(): DeviceDB { + return this._deviceDB; + } + + @state() + set deviceDB(val: DeviceDB) { + this._deviceDB = val; + } + } + + return VMDeviceDBMixinClass as Constructor & T +} diff --git a/www/src/ts/virtual_machine/device/VMDeviceCard.ts b/www/src/ts/virtual_machine/device/VMDeviceCard.ts deleted file mode 100644 index 2c9e247..0000000 --- a/www/src/ts/virtual_machine/device/VMDeviceCard.ts +++ /dev/null @@ -1,458 +0,0 @@ -import type { - LogicType, - Slot, SlotOccupant, SlotLogicType, SlotType -} from "ic10emu_wasm"; -import { html, css, HTMLTemplateResult } from "lit"; -import { customElement, property, query, state } from "lit/decorators.js"; -import { BaseElement, defaultCss } from "components"; -import { VMDeviceMixin } from "virtual_machine/base_device"; -import SlInput from "@shoelace-style/shoelace/dist/components/input/input.js"; -import { - displayNumber, - parseIntWithHexOrBinary, - parseNumber -} from "../../utils"; -import SlSelect from "@shoelace-style/shoelace/dist/components/select/select.js"; -import type { DeviceDB } from "virtual_machine/device_db"; -import { SlDialog } from "@shoelace-style/shoelace"; - - -@customElement("vm-device-card") -export class VMDeviceCard extends VMDeviceMixin(BaseElement) { - image_err: boolean; - - @property({ type: Boolean }) open: boolean; - - constructor() { - super(); - this.open = false; - } - - static styles = [ - ...defaultCss, - css` - :host { - display: block; - box-sizing: border-box; - } - .card { - width: 100%; - box-sizing: border-box; - } - .image { - width: 4rem; - height: 4rem; - } - .header { - display: flex; - flex-direction: row; - flex-grow: 1; - } - .header-name { - display: flex; - flex-direction: row; - width: 100%; - flex-grow: 1; - align-items: center; - flex-wrap: wrap; - } - .device-card { - --padding: var(--sl-spacing-small); - } - .device-name::part(input) { - width: 10rem; - } - .device-id::part(input) { - width: 7rem; - } - .device-name-hash::part(input) { - width: 7rem; - } - .slot-header.image { - width: 1.5rem; - height: 1.5rem; - border: var(--sl-panel-border-width) solid var(--sl-panel-border-color); - border-radius: var(--sl-border-radius-medium); - background-color: var(--sl-color-neutral-0); - } - sl-divider { - --spacing: 0.25rem; - } - sl-button[variant="success"] { - /* Changes the success theme color to purple using primitives */ - --sl-color-success-600: var(--sl-color-purple-700); - } - sl-button[variant="primary"] { - /* Changes the success theme color to purple using primitives */ - --sl-color-primary-600: var(--sl-color-cyan-600); - } - sl-button[variant="warning"] { - /* Changes the success theme color to purple using primitives */ - --sl-color-warning-600: var(--sl-color-amber-600); - } - sl-tab-group { - margin-left: 1rem; - margin-right: 1rem; - --indicator-color: var(--sl-color-purple-600); - --sl-color-primary-600: var(--sl-color-purple-600); - } - sl-tab::part(base) { - padding: var(--sl-spacing-small) var(--sl-spacing-medium); - } - sl-tab-group::part(base) { - max-height: 20rem; - overflow-y: auto; - } - sl-icon-button.remove-button::part(base) { - color: var(--sl-color-danger-600); - } - sl-icon-button.remove-button::part(base):hover, - sl-icon-button.remove-button::part(base):focus { - color: var(--sl-color-danger-500); - } - sl-icon-button.remove-button::part(base):active { - color: var(--sl-color-danger-600); - } - .remove-dialog-body { - display: flex; - flex-direction: row; - } - .dialog-image { - width: 3rem; - height: 3rem; - } - `, - ]; - - private _deviceDB: DeviceDB; - - get deviceDB(): DeviceDB { - return this._deviceDB; - } - - @state() - set deviceDB(val: DeviceDB) { - this._deviceDB = val; - this.updateDevice(); - this.requestUpdate(); - } - - connectedCallback(): void { - super.connectedCallback(); - window.VM.vm.addEventListener( - "vm-device-db-loaded", - this._handleDeviceDBLoad.bind(this) - ); - } - - _handleDeviceDBLoad(e: CustomEvent) { - this.deviceDB = e.detail; - } - - onImageErr(e: Event) { - this.image_err = true; - console.log("Image load error", e); - } - - renderHeader(): HTMLTemplateResult { - const activeIc = window.VM.vm.activeIC; - const thisIsActiveIc = activeIc.id === this.deviceID; - const badges: HTMLTemplateResult[] = []; - if (this.deviceID == activeIc?.id) { - badges.push(html`db`); - } - activeIc?.pins?.forEach((id, index) => { - if (this.deviceID == id) { - badges.push( - html`d${index}` - ); - } - }, this); - return html` - - - -
- - Id - - - - Name - - - - Hash - - - ${badges.map((badge) => badge)} -
-
- - - -
- `; - } - - renderFields(): HTMLTemplateResult { - const fields = Array.from(this.fields.entries()); - const inputIdBase = `vmDeviceCard${this.deviceID}Field`; - return html` - ${fields.map(([name, field], _index, _fields) => { - return html` - ${name} - - ${field.field_type} - `; - })} - `; - } - - lookupSlotOccupantImg( - occupant: SlotOccupant | undefined, - typ: SlotType - ): string { - if (typeof occupant !== "undefined") { - const hashLookup = (this.deviceDB ?? {}).names_by_hash ?? {}; - const prefabName = hashLookup[occupant.prefab_hash] ?? "UnknownHash"; - return `img/stationpedia/${prefabName}.png`; - } else { - return `img/stationpedia/SlotIcon_${typ}.png`; - } - } - - _onSlotImageErr(e: Event) { - console.log("image_err", e); - } - - static transparentImg = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" as const; - - renderSlot(slot: Slot, slotIndex: number): HTMLTemplateResult { - const _fields = this.device.getSlotFields(slotIndex); - const fields = Array.from(_fields.entries()); - const inputIdBase = `vmDeviceCard${this.deviceID}Slot${slotIndex}Field`; - const slotImg = this.lookupSlotOccupantImg(slot.occupant, slot.typ); - return html` - - - ${slotIndex} : ${slot.typ} - ${typeof slot.occupant !== "undefined" - ? html` - - Occupant: ${slot.occupant.id} : ${slot.occupant.prefab_hash} - - - Quantity: ${slot.occupant.quantity}/ - ${slot.occupant.max_quantity} - - ` - : ""} -
- ${fields.map( - ([name, field], _index, _fields) => html` - - ${name} - - ${field.field_type} - - ` - )} -
-
- `; - } - - renderSlots(): HTMLTemplateResult { - return html` -
- ${this.slots.map((slot, index, _slots) => this.renderSlot(slot, index))} -
- `; - } - - renderReagents(): HTMLTemplateResult { - return html``; - } - - renderNetworks(): HTMLTemplateResult { - const vmNetworks = window.VM.vm.networks; - const networks = this.connections.map((connection, index, _conns) => { - const conn = typeof connection === "object" ? connection.CableNetwork : null; - return html` - - Connection:${index} - ${vmNetworks.map( - (net) => html`Network ${net}` - )} - ${conn?.typ} - - `; - }); - return html` -
- ${networks} -
- `; - } - renderPins(): HTMLTemplateResult { - const pins = this.pins; - const visibleDevices = window.VM.vm.visibleDevices(this.deviceID); - const pinsHtml = pins?.map( - (pin, index) => html` - - d${index} - ${visibleDevices.map( - (device, _index) => html` - - Device ${device.id} : ${device.name ?? device.prefabName} - - ` - )} - ` - ); - return html` -
- ${pinsHtml} -
- `; - } - - render(): HTMLTemplateResult { - return html` - -
${this.renderHeader()}
- - Fields - Slots - Reagents - Networks - Pins - - ${this.renderFields()} - ${this.renderSlots()} - ${this.renderReagents()} - ${this.renderNetworks()} - ${this.renderPins()} - -
- -
- -
-

Are you sure you want to remove this device?

- Id ${this.deviceID} : ${this.name ?? this.prefabName} -
-
-
- Close - Remove -
-
- `; - } - - @query(".remove-device-dialog") removeDialog: SlDialog; - - _preventOverlayClose(event: CustomEvent) { - if (event.detail.source === "overlay") { - event.preventDefault(); - } - } - - _closeRemoveDialog() { - this.removeDialog.hide(); - } - - _handleChangeID(e: CustomEvent) { - const input = e.target as SlInput; - const val = parseIntWithHexOrBinary(input.value); - if (!isNaN(val)) { - window.VM.get().then(vm => { - if (!vm.changeDeviceId(this.deviceID, val)) { - input.value = this.deviceID.toString(); - } - }); - } else { - input.value = this.deviceID.toString(); - } - } - - _handleChangeName(e: CustomEvent) { - const input = e.target as SlInput; - const name = input.value.length === 0 ? undefined : input.value; - window.VM.get().then(vm => { - if (!vm.setDeviceName(this.deviceID, name)) { - input.value = this.name; - } - this.updateDevice(); - }); - } - - _handleChangeField(e: CustomEvent) { - const input = e.target as SlInput; - const field = input.getAttribute("key")! as LogicType; - const val = parseNumber(input.value); - window.VM.get().then((vm) => { - if (!vm.setDeviceField(this.deviceID, field, val, true)) { - input.value = this.fields.get(field).value.toString(); - } - this.updateDevice(); - }); - } - - _handleChangeSlotField(e: CustomEvent) { - const input = e.target as SlInput; - const slot = parseInt(input.getAttribute("slotIndex")!); - const field = input.getAttribute("key")! as SlotLogicType; - const val = parseNumber(input.value); - window.VM.get().then((vm) => { - if (!vm.setDeviceSlotField(this.deviceID, slot, field, val, true)) { - input.value = this.device.getSlotField(slot, field).toString(); - } - this.updateDevice(); - }); - } - - _handleDeviceRemoveButton(_e: Event) { - this.removeDialog.show(); - } - - _removeDialogRemove() { - this.removeDialog.hide(); - window.VM.get().then((vm) => vm.removeDevice(this.deviceID)); - } - - _handleChangeConnection(e: CustomEvent) { - const select = e.target as SlSelect; - const conn = parseInt(select.getAttribute("key")!); - const val = select.value ? parseInt(select.value as string) : undefined; - window.VM.get().then((vm) => vm.setDeviceConnection(this.deviceID, conn, val) - ); - this.updateDevice(); - } - - _handleChangePin(e: CustomEvent) { - const select = e.target as SlSelect; - const pin = parseInt(select.getAttribute("key")!); - const val = select.value ? parseInt(select.value as string) : undefined; - window.VM.get().then((vm) => vm.setDevicePin(this.deviceID, pin, val)); - this.updateDevice(); - } -} - diff --git a/www/src/ts/virtual_machine/device/card.ts b/www/src/ts/virtual_machine/device/card.ts index 883c87f..5b48c4b 100644 --- a/www/src/ts/virtual_machine/device/card.ts +++ b/www/src/ts/virtual_machine/device/card.ts @@ -1,17 +1,16 @@ - import { html, css, HTMLTemplateResult } from "lit"; -import { customElement, property, query, state } from "lit/decorators.js"; +import { customElement, property, query} from "lit/decorators.js"; import { BaseElement, defaultCss } from "components"; -import { VMDeviceMixin } from "virtual_machine/base_device"; -import type { DeviceDB } from "virtual_machine/device_db"; +import { VMDeviceDBMixin, VMDeviceMixin } from "virtual_machine/base_device"; import SlSelect from "@shoelace-style/shoelace/dist/components/select/select.component.js"; import { displayNumber, parseIntWithHexOrBinary, parseNumber } from "utils"; import { LogicType, Slot, SlotLogicType, SlotOccupant, SlotType } from "ic10emu_wasm"; import SlInput from "@shoelace-style/shoelace/dist/components/input/input.component.js"; import SlDialog from "@shoelace-style/shoelace/dist/components/dialog/dialog.component.js"; +import "./slot" @customElement("vm-device-card") -export class VMDeviceCard extends VMDeviceMixin(BaseElement) { +export class VMDeviceCard extends VMDeviceDBMixin(VMDeviceMixin(BaseElement)) { image_err: boolean; @property({ type: Boolean }) open: boolean; @@ -117,31 +116,6 @@ export class VMDeviceCard extends VMDeviceMixin(BaseElement) { `, ]; - private _deviceDB: DeviceDB; - - get deviceDB(): DeviceDB { - return this._deviceDB; - } - - @state() - set deviceDB(val: DeviceDB) { - this._deviceDB = val; - this.updateDevice(); - this.requestUpdate(); - } - - connectedCallback(): void { - super.connectedCallback(); - window.VM.vm.addEventListener( - "vm-device-db-loaded", - this._handleDeviceDBLoad.bind(this), - ); - } - - _handleDeviceDBLoad(e: CustomEvent) { - this.deviceDB = e.detail; - } - onImageErr(e: Event) { this.image_err = true; console.log("Image load error", e); @@ -178,7 +152,7 @@ export class VMDeviceCard extends VMDeviceMixin(BaseElement) { + value="${this.nameHash}" readonly> Hash @@ -208,18 +182,6 @@ export class VMDeviceCard extends VMDeviceMixin(BaseElement) { `; } - lookupSlotOccupantImg( - occupant: SlotOccupant | undefined, - typ: SlotType, - ): string { - if (typeof occupant !== "undefined") { - const hashLookup = (this.deviceDB ?? {}).names_by_hash ?? {}; - const prefabName = hashLookup[occupant.prefab_hash] ?? "UnknownHash"; - return `img/stationpedia/${prefabName}.png`; - } else { - return `img/stationpedia/SlotIcon_${typ}.png`; - } - } _onSlotImageErr(e: Event) { console.log("image_err", e); @@ -228,54 +190,16 @@ export class VMDeviceCard extends VMDeviceMixin(BaseElement) { static transparentImg = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" as const; - renderSlot(slot: Slot, slotIndex: number): HTMLTemplateResult { - const _fields = this.device.getSlotFields(slotIndex); - const fields = Array.from(_fields.entries()); - const inputIdBase = `vmDeviceCard${this.deviceID}Slot${slotIndex}Field`; - const slotImg = this.lookupSlotOccupantImg(slot.occupant, slot.typ); - return html` - - - ${slotIndex} : ${slot.typ} - ${ - typeof slot.occupant !== "undefined" - ? html` - - Occupant: ${slot.occupant.id} : ${slot.occupant.prefab_hash} - - - Quantity: ${slot.occupant.quantity}/ - ${slot.occupant.max_quantity} - - ` - : "" - } -
- ${fields.map( - ([name, field], _index, _fields) => html` - - ${name} - - ${field.field_type} - - `, - )} -
-
- `; - } - renderSlots(): HTMLTemplateResult { return html`
- ${this.slots.map((slot, index, _slots) => this.renderSlot(slot, index))} + ${this.slots.map((_slot, index, _slots) => html` + + + ` )}
`; } @@ -295,7 +219,7 @@ export class VMDeviceCard extends VMDeviceMixin(BaseElement) { Connection:${index} ${vmNetworks.map( (net) => - html`Network ${net}`, + html`Network ${net}`, )} ${conn?.typ} @@ -415,20 +339,6 @@ export class VMDeviceCard extends VMDeviceMixin(BaseElement) { this.updateDevice(); }); } - - _handleChangeSlotField(e: CustomEvent) { - const input = e.target as SlInput; - const slot = parseInt(input.getAttribute("slotIndex")!); - const field = input.getAttribute("key")! as SlotLogicType; - const val = parseNumber(input.value); - window.VM.get().then((vm) => { - if (!vm.setDeviceSlotField(this.deviceID, slot, field, val, true)) { - input.value = this.device.getSlotField(slot, field).toString(); - } - this.updateDevice(); - }); - } - _handleDeviceRemoveButton(_e: Event) { this.removeDialog.show(); } diff --git a/www/src/ts/virtual_machine/device/slot.ts b/www/src/ts/virtual_machine/device/slot.ts new file mode 100644 index 0000000..05341fd --- /dev/null +++ b/www/src/ts/virtual_machine/device/slot.ts @@ -0,0 +1,181 @@ +import { html, css, HTMLTemplateResult } from "lit"; +import { customElement, property, query, state } from "lit/decorators.js"; +import { BaseElement, defaultCss } from "components"; +import { VMDeviceDBMixin, VMDeviceMixin } from "virtual_machine/base_device"; +import type { DeviceDB } from "virtual_machine/device_db"; +import SlSelect from "@shoelace-style/shoelace/dist/components/select/select.component.js"; +import { displayNumber, parseIntWithHexOrBinary, parseNumber } from "utils"; +import { + LogicType, + Slot, + SlotLogicType, + SlotOccupant, + SlotType, +} from "ic10emu_wasm"; +import SlInput from "@shoelace-style/shoelace/dist/components/input/input.component.js"; +import SlDialog from "@shoelace-style/shoelace/dist/components/dialog/dialog.component.js"; +import { VMDeviceCard } from "./card"; +import { when } from "lit/directives/when.js"; + +@customElement("vm-device-slot") +export class VMDeviceSlot extends VMDeviceMixin(VMDeviceDBMixin(BaseElement)) { + @property({ type: Number }) slotIndex: number; + + constructor() { + super(); + } + + static styles = [ + ...defaultCss, + css` + .slot-card { + --padding: var(--sl-spacing-small); + } + .slot-card::part(base) { + background-color: var(--sl-color-neutral-50); + } + `, + ]; + + slotOccupantImg(): string { + const slot = this.slots[this.slotIndex]; + if (typeof slot.occupant !== "undefined") { + const hashLookup = (this.deviceDB ?? {}).names_by_hash ?? {}; + const prefabName = hashLookup[slot.occupant.prefab_hash] ?? "UnknownHash"; + return `img/stationpedia/${prefabName}.png`; + } else { + return `img/stationpedia/SlotIcon_${slot.typ}.png`; + } + } + + slotOccupantPrefabName(): string { + const slot = this.slots[this.slotIndex]; + if (typeof slot.occupant !== "undefined") { + const hashLookup = (this.deviceDB ?? {}).names_by_hash ?? {}; + const prefabName = hashLookup[slot.occupant.prefab_hash] ?? "UnknownHash"; + return prefabName; + } else { + return undefined; + } + } + + renderHeader() { + const inputIdBase = `vmDeviceSlot${this.deviceID}Slot${this.slotIndex}Head`; + const slot = this.slots[this.slotIndex]; + const slotImg = this.slotOccupantImg(); + const img = html``; + + return html` +
+
+ + ${img} + + ${when( + typeof slot.occupant !== "undefined", + () => html`
+ ${slot.occupant.quantity} / ${slot.occupant.max_quantity} +
` + )} +
+
+
+ ${when( + typeof slot.occupant !== "undefined", + () => html` + + ${slot.occupant.id} : ${this.slotOccupantPrefabName()} + + `, + () => html` + + ${slot.typ} + + `, + )} +
+
+ ${when( + typeof slot.occupant !== "undefined", + () => html` + +
+ Max Quantity:${slot.occupant.max_quantity} +
+
+ `, + () => html` + `, + )} +
+
+ `; + } + + _handleSlotClick(e: Event) { + console.log(e); + } + + renderFields() { + const inputIdBase = `vmDeviceSlot${this.deviceID}Slot${this.slotIndex}Field`; + const _fields = this.device.getSlotFields(this.slotIndex); + const fields = Array.from(_fields.entries()); + + return html` +
+ ${fields.map( + ([name, field], _index, _fields) => html` + + ${name} + + ${field.field_type} + + `, + )} +
+ `; + } + + _handleChangeSlotField(e: CustomEvent) { + const input = e.target as SlInput; + const field = input.getAttribute("key")! as SlotLogicType; + const val = parseNumber(input.value); + window.VM.get().then((vm) => { + if (!vm.setDeviceSlotField(this.deviceID, this.slotIndex, field, val, true)) { + input.value = this.device.getSlotField(this.slotIndex, field).toString(); + } + this.updateDevice(); + }); + } + + render() { + return html` + +
${this.renderHeader()}
+
+ ${this.renderFields()} +
+
+ `; + } +} diff --git a/www/src/ts/virtual_machine/device/template.ts b/www/src/ts/virtual_machine/device/template.ts index 44f51f9..ae9358e 100644 --- a/www/src/ts/virtual_machine/device/template.ts +++ b/www/src/ts/virtual_machine/device/template.ts @@ -24,11 +24,10 @@ import { displayNumber, parseNumber } from "utils"; import SlInput from "@shoelace-style/shoelace/dist/components/input/input.component.js"; import SlSelect from "@shoelace-style/shoelace/dist/components/select/select.component.js"; import { VMDeviceCard } from "./card"; +import { VMDeviceDBMixin } from "virtual_machine/base_device"; @customElement("vm-device-template") -export class VmDeviceTemplate extends BaseElement { - private _deviceDB: DeviceDB; - private image_err: boolean = false; +export class VmDeviceTemplate extends VMDeviceDBMixin(BaseElement) { static styles = [ ...defaultCss, @@ -70,16 +69,6 @@ export class VmDeviceTemplate extends BaseElement { this.deviceDB = window.VM.vm.db; } - get deviceDB(): DeviceDB { - return this._deviceDB; - } - - @state() - set deviceDB(val: DeviceDB) { - this._deviceDB = val; - this.setupState(); - } - private _prefab_name: string; get prefab_name(): string { @@ -138,21 +127,6 @@ export class VmDeviceTemplate extends BaseElement { this.connections = connections.map((conn) => conn[1]); } - - connectedCallback(): void { - super.connectedCallback(); - window.VM.get().then((vm) => - vm.addEventListener( - "vm-device-db-loaded", - this._handleDeviceDBLoad.bind(this), - ), - ); - } - - _handleDeviceDBLoad(e: CustomEvent) { - this.deviceDB = e.detail; - } - renderFields(): HTMLTemplateResult { const fields = Object.entries(this.fields); return html`