perf: improve slot UI + device search speedup
Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com>
This commit is contained in:
@@ -183,10 +183,6 @@ export const VMDeviceMixin = <T extends Constructor<LitElement>>(
|
||||
}
|
||||
}
|
||||
|
||||
update(changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>): void {
|
||||
super.update(changedProperties);
|
||||
this.updateDevice();
|
||||
}
|
||||
}
|
||||
return VMDeviceMixinClass as Constructor<VMDeviceMixinInterface> & T;
|
||||
};
|
||||
@@ -239,6 +235,7 @@ export const VMDeviceDBMixin = <T extends Constructor<LitElement>>(superClass: T
|
||||
"vm-device-db-loaded",
|
||||
this._handleDeviceDBLoad.bind(this),
|
||||
);
|
||||
this.deviceDB = window.VM.vm.db!;
|
||||
return root;
|
||||
}
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@ export class VMDeviceCard extends VMDeviceDBMixin(VMDeviceMixin(BaseElement)) {
|
||||
padding: var(--sl-spacing-small) var(--sl-spacing-medium);
|
||||
}
|
||||
sl-tab-group::part(base) {
|
||||
max-height: 20rem;
|
||||
max-height: 30rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
sl-icon-button.remove-button::part(base) {
|
||||
@@ -116,6 +116,11 @@ export class VMDeviceCard extends VMDeviceDBMixin(VMDeviceMixin(BaseElement)) {
|
||||
`,
|
||||
];
|
||||
|
||||
_handleDeviceDBLoad(e: CustomEvent<any>): void {
|
||||
super._handleDeviceDBLoad(e);
|
||||
this.updateDevice();
|
||||
}
|
||||
|
||||
onImageErr(e: Event) {
|
||||
this.image_err = true;
|
||||
console.log("Image load error", e);
|
||||
@@ -137,21 +142,21 @@ export class VMDeviceCard extends VMDeviceDBMixin(VMDeviceMixin(BaseElement)) {
|
||||
}, this);
|
||||
return html`
|
||||
<sl-tooltip content="${this.prefabName}">
|
||||
<img class="image" src="img/stationpedia/${this.prefabName}.png"
|
||||
<img class="image me-2" src="img/stationpedia/${this.prefabName}.png"
|
||||
onerror="this.src = '${VMDeviceCard.transparentImg}'" />
|
||||
</sl-tooltip>
|
||||
<div class="header-name">
|
||||
<sl-input id="vmDeviceCard${this.deviceID}Id" class="device-id" size="small" pill value=${this.deviceID}
|
||||
<sl-input id="vmDeviceCard${this.deviceID}Id" class="device-id me-1" size="small" pill value=${this.deviceID}
|
||||
@sl-change=${this._handleChangeID}>
|
||||
<span slot="prefix">Id</span>
|
||||
<sl-copy-button slot="suffix" value=${this.deviceID}></sl-copy-button>
|
||||
<sl-copy-button slot="suffix" .value=${this.deviceID}></sl-copy-button>
|
||||
</sl-input>
|
||||
<sl-input id="vmDeviceCard${this.deviceID}Name" class="device-name" size="small" pill placeholder=${this.prefabName}
|
||||
<sl-input id="vmDeviceCard${this.deviceID}Name" class="device-name me-1" size="small" pill placeholder=${this.prefabName}
|
||||
value=${this.name} @sl-change=${this._handleChangeName}>
|
||||
<span slot="prefix">Name</span>
|
||||
<sl-copy-button slot="suffix" from="vmDeviceCard${this.deviceID}Name.value"></sl-copy-button>
|
||||
</sl-input>
|
||||
<sl-input id="vmDeviceCard${this.deviceID}NameHash" size="small" pill class="device-name-hash"
|
||||
<sl-input id="vmDeviceCard${this.deviceID}NameHash" size="small" pill class="device-name-hash me-1"
|
||||
value="${this.nameHash}" readonly>
|
||||
<span slot="prefix">Hash</span>
|
||||
<sl-copy-button slot="suffix" from="vmDeviceCard${this.deviceID}NameHash.value"></sl-copy-button>
|
||||
@@ -192,12 +197,12 @@ export class VMDeviceCard extends VMDeviceDBMixin(VMDeviceMixin(BaseElement)) {
|
||||
|
||||
renderSlots(): HTMLTemplateResult {
|
||||
return html`
|
||||
<div class="slots">
|
||||
<div class="flex flex-row flex-wrap">
|
||||
${this.slots.map((_slot, index, _slots) => html`
|
||||
<vm-device-slot
|
||||
.deviceID=${this.deviceID}
|
||||
.slotIndex=${index}
|
||||
class-"w-96"
|
||||
class-"flex flex-row max-w-lg mr-2 mb-2"
|
||||
>
|
||||
</vm-device-slot>
|
||||
` )}
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
|
||||
import { html, css, HTMLTemplateResult } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators.js";
|
||||
import { BaseElement, defaultCss } from "components";
|
||||
|
||||
import SlInput from "@shoelace-style/shoelace/dist/components/input/input.js";
|
||||
import {
|
||||
structuralEqual,
|
||||
} from "../../utils";
|
||||
import { structuralEqual } from "../../utils";
|
||||
|
||||
import SlDrawer from "@shoelace-style/shoelace/dist/components/drawer/drawer.js";
|
||||
import type { DeviceDB, DeviceDBEntry } from "virtual_machine/device_db";
|
||||
import { repeat } from "lit/directives/repeat.js";
|
||||
import { cache } from "lit/directives/cache.js";
|
||||
import { default as uFuzzy } from "@leeoniya/ufuzzy";
|
||||
import { when } from "lit/directives/when.js";
|
||||
import { unsafeHTML } from "lit/directives/unsafe-html.js";
|
||||
|
||||
@customElement("vm-device-list")
|
||||
export class VMDeviceList extends BaseElement {
|
||||
@@ -169,13 +168,7 @@ export class VMAddDeviceButton extends BaseElement {
|
||||
...defaultCss,
|
||||
css`
|
||||
.add-device-drawer {
|
||||
--size: 32rem;
|
||||
}
|
||||
|
||||
.search-results {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
overflow-x: auto;
|
||||
--size: 36rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
@@ -238,7 +231,11 @@ export class VMAddDeviceButton extends BaseElement {
|
||||
this.performSearch();
|
||||
}
|
||||
|
||||
private _searchResults: DeviceDBEntry[];
|
||||
private _searchResults: {
|
||||
entry: DeviceDBEntry;
|
||||
haystackEntry: string;
|
||||
ranges: number[];
|
||||
}[];
|
||||
|
||||
private filterTimeout: number | undefined;
|
||||
|
||||
@@ -252,18 +249,30 @@ export class VMAddDeviceButton extends BaseElement {
|
||||
1e3,
|
||||
);
|
||||
|
||||
const filtered = order?.map(
|
||||
(infoIdx) => this._datapoints[info.idx[infoIdx]],
|
||||
);
|
||||
const names =
|
||||
filtered
|
||||
?.map((data) => data[1])
|
||||
?.filter((val, index, arr) => arr.indexOf(val) === index) ?? [];
|
||||
const filtered = order?.map((infoIdx) => ({
|
||||
name: this._datapoints[info.idx[infoIdx]][1],
|
||||
haystackEntry: this._haystack[info.idx[infoIdx]],
|
||||
ranges: info.ranges[infoIdx],
|
||||
}));
|
||||
|
||||
this._searchResults = names.map((name) => this._strutures.get(name)!);
|
||||
const unique = [...new Set(filtered.map((obj) => obj.name))].map(
|
||||
(result) => {
|
||||
return filtered.find((obj) => obj.name === result);
|
||||
},
|
||||
);
|
||||
|
||||
this._searchResults = unique.map(({ name, haystackEntry, ranges }) => ({
|
||||
entry: this._strutures.get(name)!,
|
||||
haystackEntry,
|
||||
ranges,
|
||||
}));
|
||||
} else {
|
||||
// clear our results and prefilter if the filter is empty
|
||||
this._searchResults = [];
|
||||
// return everything
|
||||
this._searchResults = [...this._strutures.values()].map((st) => ({
|
||||
entry: st,
|
||||
haystackEntry: st.title,
|
||||
ranges: [],
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -283,15 +292,55 @@ export class VMAddDeviceButton extends BaseElement {
|
||||
}
|
||||
|
||||
renderSearchResults() {
|
||||
return repeat(
|
||||
this._searchResults ?? [],
|
||||
(result) => result.name,
|
||||
(result) => cache(html`
|
||||
<vm-device-template prefab_name=${result.name} class="card" @add-device-template=${this._handleDeviceAdd}>
|
||||
</vm-device-template>
|
||||
`)
|
||||
return when(
|
||||
typeof this._searchResults !== "undefined" && this._searchResults.length < 20,
|
||||
() =>
|
||||
repeat(
|
||||
this._searchResults ?? [],
|
||||
(result) => result.entry.name,
|
||||
(result) =>
|
||||
cache(html`
|
||||
<vm-device-template
|
||||
prefab_name=${result.entry.name}
|
||||
class="card"
|
||||
@add-device-template=${this._handleDeviceAdd}
|
||||
>
|
||||
</vm-device-template>
|
||||
`),
|
||||
),
|
||||
() => html`
|
||||
<div class="p-2">
|
||||
<p class="p-2">
|
||||
<sl-format-number
|
||||
.value=${this._searchResults.length}
|
||||
></sl-format-number>
|
||||
results, filter more to get cards
|
||||
</p>
|
||||
<div class="flex flex-row flex-wrap">
|
||||
${[
|
||||
...this._searchResults.slice(0, 50),
|
||||
{ entry: { title: "", name: "" }, haystackEntry: "...", ranges: [] },
|
||||
].map((result) => {
|
||||
const hay = result.haystackEntry.slice(0, 15);
|
||||
const ranges = result.ranges.filter((pos) => pos < 20);
|
||||
const key = result.entry.name;
|
||||
return html`<div class="p-2 text-neutral-200/80 italic cursor-pointer" key=${key} @click=${this._handleHaystackClick}>
|
||||
${result.entry.title} (<small class="text-sm">
|
||||
${unsafeHTML(uFuzzy.highlight(hay, ranges))}
|
||||
</small>)
|
||||
</div>`;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
);
|
||||
}
|
||||
|
||||
_handleHaystackClick(e: Event) {
|
||||
const div = e.currentTarget as HTMLDivElement;
|
||||
const key = div.getAttribute("key");
|
||||
this.filter = key;
|
||||
this.searchInput.value = key;
|
||||
}
|
||||
|
||||
_handleDeviceAdd() {
|
||||
@@ -300,20 +349,33 @@ export class VMAddDeviceButton extends BaseElement {
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<sl-button variant="neutral" outline pill @click=${this._handleAddButtonClick}>
|
||||
<sl-button
|
||||
variant="neutral"
|
||||
outline
|
||||
pill
|
||||
@click=${this._handleAddButtonClick}
|
||||
>
|
||||
Add Device
|
||||
</sl-button>
|
||||
<sl-drawer class="add-device-drawer" placement="bottom" no-header>
|
||||
<sl-input class="device-search-input" autofocus placeholder="Search For Device" clearable
|
||||
@sl-input=${this._handleSearchInput}>
|
||||
<sl-input
|
||||
class="device-search-input"
|
||||
autofocus
|
||||
placeholder="Search For Device"
|
||||
clearable
|
||||
@sl-input=${this._handleSearchInput}
|
||||
>
|
||||
<span slot="prefix">Search Structures</span>
|
||||
<sl-icon slot="suffix" name="search"></sl-icon>
|
||||
</sl-input>
|
||||
<div class="search-results">${this.renderSearchResults()}</div>
|
||||
<sl-button slot="footer" variant="primary" @click=${()=> {
|
||||
this.drawer.hide();
|
||||
<div class="flex flex-row overflow-auto">${this.renderSearchResults()}</div>
|
||||
<sl-button
|
||||
slot="footer"
|
||||
variant="primary"
|
||||
@click=${() => {
|
||||
this.drawer.hide();
|
||||
}}
|
||||
>
|
||||
>
|
||||
Close
|
||||
</sl-button>
|
||||
</sl-drawer>
|
||||
@@ -336,4 +398,3 @@ export class VMAddDeviceButton extends BaseElement {
|
||||
(this.drawer.querySelector(".device-search-input") as SlInput).select();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ 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 type { DeviceDB, DeviceDBEntry } from "virtual_machine/device_db";
|
||||
import SlSelect from "@shoelace-style/shoelace/dist/components/select/select.component.js";
|
||||
import { displayNumber, parseIntWithHexOrBinary, parseNumber } from "utils";
|
||||
import {
|
||||
@@ -29,11 +29,17 @@ export class VMDeviceSlot extends VMDeviceMixin(VMDeviceDBMixin(BaseElement)) {
|
||||
...defaultCss,
|
||||
css`
|
||||
.slot-card {
|
||||
--padding: var(--sl-spacing-small);
|
||||
--padding: var(--sl-spacing-x-small);
|
||||
}
|
||||
.slot-card::part(header) {
|
||||
padding: var(--sl-spacing-x-small);
|
||||
}
|
||||
.slot-card::part(base) {
|
||||
background-color: var(--sl-color-neutral-50);
|
||||
}
|
||||
.quantity-input sl-input::part(input) {
|
||||
width: 3rem;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -59,16 +65,26 @@ export class VMDeviceSlot extends VMDeviceMixin(VMDeviceDBMixin(BaseElement)) {
|
||||
}
|
||||
}
|
||||
|
||||
slotOcccupantTemplate(): { name: string, typ: SlotType} | undefined {
|
||||
if (this.deviceDB) {
|
||||
const entry = this.deviceDB.db[this.prefabName]
|
||||
return entry?.slots[this.slotIndex];
|
||||
} 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`<img class="w-10 h-10" src="${slotImg}" oanerror="this.src = '${VMDeviceCard.transparentImg}'" />`;
|
||||
const img = html`<img class="w-10 h-10" src="${slotImg}" onerror="this.src = '${VMDeviceCard.transparentImg}'" />`;
|
||||
const template = this.slotOcccupantTemplate();
|
||||
|
||||
return html`
|
||||
<div class="flex flex-row me-2">
|
||||
<div
|
||||
class="relative border border-neutral-200/40 rounded-lg p-1
|
||||
class="relative shrink-0 border border-neutral-200/40 rounded-lg p-1
|
||||
hover:ring-2 hover:ring-purple-500 hover:ring-offset-1
|
||||
hover:ring-offset-purple-500 cursor-pointer me-2"
|
||||
@click=${this._handleSlotClick}
|
||||
@@ -88,30 +104,35 @@ export class VMDeviceSlot extends VMDeviceMixin(VMDeviceDBMixin(BaseElement)) {
|
||||
class="absolute bottom-0 right-0 mr-1 mb-1 text-xs
|
||||
text-neutral-200/90 font-mono bg-neutral-500/40 rounded pl-1 pr-1"
|
||||
>
|
||||
<small>${slot.occupant.quantity} / ${slot.occupant.max_quantity}</small>
|
||||
<small>${slot.occupant.quantity}/${slot.occupant.max_quantity}</small>
|
||||
</div>`
|
||||
)}
|
||||
<div></div>
|
||||
</div>
|
||||
<div class="ms-4 mt-auto mb-auto">
|
||||
<div class="flex flex-col justify-end">
|
||||
<div class="text-sm mt-auto mb-auto">
|
||||
${when(
|
||||
typeof slot.occupant !== "undefined",
|
||||
() => html`
|
||||
<span class="">
|
||||
${slot.occupant.id} : ${this.slotOccupantPrefabName()}
|
||||
<span>
|
||||
${this.slotOccupantPrefabName()}
|
||||
</span>
|
||||
`,
|
||||
() => html`
|
||||
<span class="">
|
||||
${slot.typ}
|
||||
<span>
|
||||
${template?.name}
|
||||
</span>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
<div class="text-neutral-400 text-xs mt-auto flex flex-col mb-1">
|
||||
<div><strong class="mt-auto mb-auto">Type:</strong><span class="p-1">${slot.typ}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ms-auto mt-auto mb-auto me-4">
|
||||
${when(
|
||||
typeof slot.occupant !== "undefined",
|
||||
() => html`
|
||||
${when(
|
||||
typeof slot.occupant !== "undefined",
|
||||
() => html`
|
||||
<div class="quantity-input ms-auto pl-2 mt-auto mb-auto me-2">
|
||||
<sl-input
|
||||
type="number"
|
||||
size="small"
|
||||
@@ -120,14 +141,14 @@ export class VMDeviceSlot extends VMDeviceMixin(VMDeviceDBMixin(BaseElement)) {
|
||||
.max=${slot.occupant.max_quantity}
|
||||
>
|
||||
<div slot="help-text">
|
||||
<span><strong>Max Quantity:</strong>${slot.occupant.max_quantity}</span>
|
||||
<span>Max Quantity: ${slot.occupant.max_quantity}</span>
|
||||
</div>
|
||||
</sl-input>
|
||||
`,
|
||||
() => html`
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
() => html`
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -47,6 +47,10 @@ export class VmDeviceTemplate extends VMDeviceDBMixin(BaseElement) {
|
||||
// height: 18rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
sl-tab-group {
|
||||
--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);
|
||||
}
|
||||
@@ -222,7 +226,7 @@ export class VmDeviceTemplate extends VMDeviceDBMixin(BaseElement) {
|
||||
<div class="header" slot="header">
|
||||
<sl-tooltip content="${device?.name}">
|
||||
<img
|
||||
class="image"
|
||||
class="image me-2"
|
||||
src="img/stationpedia/${device?.name}.png"
|
||||
onerror="this.src = '${VMDeviceCard.transparentImg}'"
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user