Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
"@lit/reactive-element": "2.1.2",
"@mdi/js": "7.4.47",
"@replit/codemirror-indentation-markers": "^6.5.3",
"@tanstack/lit-table": "^8.21.3",
"@tanstack/lit-table": "^9.1.2",
"codemirror": "^6.0.2",
"esptool-js": "^0.6.1",
"improv-wifi-serial-sdk": "^2.8.1",
Expand Down
42 changes: 31 additions & 11 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

40 changes: 40 additions & 0 deletions src/components/dashboard/device-row.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import type { ConfiguredDevice, DeviceState, Label } from "../../api/types/devices.js";
import type { FirmwareJob } from "../../api/types/firmware-jobs.js";

/** One row of the device table, derived from a ``ConfiguredDevice``
* by ``device-table``'s ``willUpdate``. Lives in its own leaf module
* so ``table-columns`` and ``table-features`` can both depend on it
* without importing each other. */
export interface DeviceRow {
status: DeviceState;
name: string;
friendly_name: string;
address: string;
ip: string;
ip_addresses: string[];
mac_address: string;
platform: string;
version: string;
comment: string;
area: string;
/** Resolved label objects (catalog joined against
* ``device.labels``) so the cell renderer doesn't need access to
* the catalog itself. ``device-table`` performs the resolve when
* building rows. */
labels: Label[];
config: string;
build_size_bytes: number;
// Raw has_pending_changes (device truth) — drives the encryption lock only.
hasPendingChanges: boolean;
// mDNS-gated display flags (see util/device-sync.ts): modified dot + install
// button, update column + update button.
showModified: boolean;
showUpdate: boolean;
hasQueuedUpdate: boolean;
api_enabled: boolean;
api_encrypted: boolean;
api_encryption_active: string | null;
busy: boolean;
recentJob: FirmwareJob | null;
_device: ConfiguredDevice;
}
43 changes: 21 additions & 22 deletions src/components/dashboard/device-table-grid.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,23 @@
import { flexRender } from "@tanstack/lit-table";
import type { Cell, Header, HeaderGroup, Row, Table } from "@tanstack/lit-table";
import { html, nothing, type TemplateResult } from "lit";
import { classMap } from "lit/directives/class-map.js";
import { repeat } from "lit/directives/repeat.js";
import type { ConfiguredDevice } from "../../api/types/devices.js";
import type { LocalizeFunc } from "../../common/localize.js";
import { tourAnchor } from "../guided-tour/tour-anchor.js";
import { getActiveTourConfiguration } from "../guided-tour/tour-session.js";
import type { DeviceRow } from "./table-columns.js";
import type { DeviceTable, DeviceTableRow } from "./table-features.js";

export interface DeviceTableHeadProps {
table: Table<DeviceRow>;
table: DeviceTable;
selectMode: boolean;
allSelected: boolean;
onToggleAll: () => void;
}

export interface DeviceTableBodyProps {
table: Table<DeviceRow>;
rows: Row<DeviceRow>[];
table: DeviceTable;
rows: DeviceTableRow[];
selectMode: boolean;
selectedDevices: Set<string>;
highlightConfiguration: string | null;
Expand All @@ -40,7 +39,7 @@ export function renderDeviceTableHead(p: DeviceTableHeadProps): TemplateResult {
return html`
<thead>
${p.table.getHeaderGroups().map(
(hg: HeaderGroup<DeviceRow>) => html`
(hg) => html`
<tr role="row">
${
p.selectMode
Expand All @@ -54,19 +53,25 @@ export function renderDeviceTableHead(p: DeviceTableHeadProps): TemplateResult {
</th>`
: nothing
}
${hg.headers.map((header: Header<DeviceRow, unknown>) => {
${hg.headers.map((header) => {
const sorted = header.column.getIsSorted();
const canSort = header.column.getCanSort();
const ariaSort =
sorted === "asc"
? "ascending"
: sorted === "desc"
? "descending"
: "none";
const sortIcon =
sorted === "asc"
? "chevron-up"
: sorted === "desc"
? "chevron-down"
: "unfold-more-horizontal";
return html`
<th
role="columnheader"
aria-sort=${
sorted === "asc"
? "ascending"
: sorted === "desc"
? "descending"
: "none"
}
aria-sort=${ariaSort}
class="${canSort ? "sortable" : ""} ${
sorted ? "sorted" : ""
} col-${header.column.id}"
Expand All @@ -84,13 +89,7 @@ export function renderDeviceTableHead(p: DeviceTableHeadProps): TemplateResult {
? html`<wa-icon
class="sort-icon"
library="mdi"
name=${
sorted === "asc"
? "chevron-up"
: sorted === "desc"
? "chevron-down"
: "unfold-more-horizontal"
}
name=${sortIcon}
></wa-icon>`
: nothing
}
Expand Down Expand Up @@ -161,7 +160,7 @@ export function renderDeviceTableBody(p: DeviceTableBodyProps): TemplateResult {
</td>`
: nothing
}
${row.getVisibleCells().map((cell: Cell<DeviceRow, unknown>) => {
${row.getVisibleCells().map((cell) => {
// The stacked mobile layout (table-styles.ts) shows each
// cell's column header as a field label. It's a real
// span (not a CSS ::before) so screen readers announce
Expand Down
67 changes: 27 additions & 40 deletions src/components/dashboard/device-table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,13 @@ import {
mdiUpload,
} from "@mdi/js";
import {
type ColumnDef,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
type ColumnVisibilityState,
functionalUpdate,
type PaginationState,
type SortingState,
TableController,
type VisibilityState,
type Updater,
} from "@tanstack/lit-table";
import type { Row, Table } from "@tanstack/lit-table";
import type { PropertyValues } from "lit";
import { html, LitElement } from "lit";
import { customElement, property, query, state } from "lit/decorators.js";
Expand Down Expand Up @@ -59,6 +55,13 @@ import {
import { tableCellStyles } from "./table-cell-styles.js";
import type { ToggleableColumn } from "./table-column-toggle.js";
import { createDeviceColumns, type DeviceRow } from "./table-columns.js";
import {
type DeviceColumnDef,
type DeviceTable,
deviceTableFeatures,
type DeviceTableFeatures,
type DeviceTableRow,
} from "./table-features.js";
import { tableLayoutStyles } from "./table-styles.js";

import "@home-assistant/webawesome/dist/components/icon/icon.js";
Expand Down Expand Up @@ -89,12 +92,7 @@ registerMdiIcons({
upload: mdiUpload,
});

const coreRowModel = getCoreRowModel<DeviceRow>();
const sortedRowModel = getSortedRowModel<DeviceRow>();
const filteredRowModel = getFilteredRowModel<DeviceRow>();
const paginatedRowModel = getPaginationRowModel<DeviceRow>();

const DEFAULT_HIDDEN_COLUMNS: VisibilityState = {
const DEFAULT_HIDDEN_COLUMNS: ColumnVisibilityState = {
comment: false,
area: false,
labels: false,
Expand Down Expand Up @@ -147,7 +145,7 @@ export class ESPHomeDeviceTable extends LitElement {

/** Column visibility from preferences — the host mirrors saves back, so a remount reseeds it. */
@property({ attribute: false })
initialColumnVisibility: VisibilityState | null = null;
initialColumnVisibility: ColumnVisibilityState | null = null;

/** Page size from preferences — the host mirrors saves back, so a remount reseeds it. */
@property({ type: Number, attribute: "initial-page-size" })
Expand All @@ -157,7 +155,7 @@ export class ESPHomeDeviceTable extends LitElement {
private _sorting: SortingState = [];

@state()
private _columnVisibility: VisibilityState = { ...DEFAULT_HIDDEN_COLUMNS };
private _columnVisibility: ColumnVisibilityState = { ...DEFAULT_HIDDEN_COLUMNS };

@state()
private _pageSize = 25;
Expand All @@ -181,34 +179,27 @@ export class ESPHomeDeviceTable extends LitElement {
@query(".table-scroll")
private _scrollContainer!: HTMLDivElement;

private _tableController = new TableController<DeviceRow>(this);
private _tableController = new TableController<DeviceTableFeatures, DeviceRow>(this);
private _rows: DeviceRow[] = [];
private _visibleConfigs: string[] = [];
private _columns: ColumnDef<DeviceRow>[] = [];
private _columns: DeviceColumnDef[] = [];
private _prevLocalize: LocalizeFunc | null = null;

// ─── Stable callbacks ───

private _handleSortingChange = (
updater: SortingState | ((old: SortingState) => SortingState)
) => {
this._sorting = typeof updater === "function" ? updater(this._sorting) : updater;
private _handleSortingChange = (updater: Updater<SortingState>) => {
this._sorting = functionalUpdate(updater, this._sorting);
fireEvent(this, "table-sort-change", this._sorting);
};

private _handleVisibilityChange = (
updater: VisibilityState | ((old: VisibilityState) => VisibilityState)
) => {
this._columnVisibility =
typeof updater === "function" ? updater(this._columnVisibility) : updater;
private _handleVisibilityChange = (updater: Updater<ColumnVisibilityState>) => {
this._columnVisibility = functionalUpdate(updater, this._columnVisibility);
fireEvent(this, "table-visibility-change", this._columnVisibility);
};

private _handlePaginationChange = (
updater: PaginationState | ((old: PaginationState) => PaginationState)
) => {
private _handlePaginationChange = (updater: Updater<PaginationState>) => {
const current = { pageSize: this._pageSize, pageIndex: this._pageIndex };
const next = typeof updater === "function" ? updater(current) : updater;
const next = functionalUpdate(updater, current);
const pageSizeChanged = next.pageSize !== this._pageSize;
this._pageSize = next.pageSize;
this._pageIndex = next.pageIndex;
Expand All @@ -218,7 +209,7 @@ export class ESPHomeDeviceTable extends LitElement {
};

private _globalFilterFn = (
row: Row<DeviceRow>,
row: DeviceTableRow,
_columnId: string,
filterValue: unknown
): boolean => {
Expand All @@ -227,7 +218,7 @@ export class ESPHomeDeviceTable extends LitElement {
// dashboard's select-all scoping helper matches the same rows
// this filter makes visible (single source of truth).
const q = (filterValue as string).trim().toLowerCase();
return matchesDeviceRow(row.original as DeviceRow, q);
return matchesDeviceRow(row.original, q);
};

// ─── Lifecycle ───
Expand Down Expand Up @@ -284,7 +275,7 @@ export class ESPHomeDeviceTable extends LitElement {
comment: d.comment || "",
area: d.area || "",
// Resolve labels here once per render rather than from the
// cell renderer — TanStack's sortingFn / filterFn read the
// cell renderer — TanStack's sortFn / filterFn read the
// accessor value, so they need the resolved objects rather
// than opaque ids.
labels: resolveLabelIds(d.labels, this._labelCatalog),
Expand Down Expand Up @@ -327,10 +318,7 @@ export class ESPHomeDeviceTable extends LitElement {
onSortingChange: this._handleSortingChange,
onColumnVisibilityChange: this._handleVisibilityChange,
onPaginationChange: this._handlePaginationChange,
getCoreRowModel: coreRowModel,
getSortedRowModel: sortedRowModel,
getFilteredRowModel: filteredRowModel,
getPaginationRowModel: paginatedRowModel,
features: deviceTableFeatures,
globalFilterFn: this._globalFilterFn,
});

Expand All @@ -342,7 +330,6 @@ export class ESPHomeDeviceTable extends LitElement {
);
const rows = table.getRowModel().rows;
this._visibleConfigs = table.getFilteredRowModel().rows.map((r) => r.original.config);
const pgState = table.getState().pagination;
const toggleCols: ToggleableColumn[] = table
.getAllColumns()
.filter((c) => c.getCanHide())
Expand Down Expand Up @@ -380,7 +367,7 @@ export class ESPHomeDeviceTable extends LitElement {
</table>
</div>
<esphome-table-pagination
page-index=${pgState.pageIndex}
page-index=${effectivePageIndex}
page-count=${table.getPageCount()}
page-size=${this._pageSize}
total-rows=${table.getFilteredRowModel().rows.length}
Expand Down Expand Up @@ -476,7 +463,7 @@ export class ESPHomeDeviceTable extends LitElement {
`;
}

private _renderControls(table: Table<DeviceRow>, toggleCols: ToggleableColumn[]) {
private _renderControls(table: DeviceTable, toggleCols: ToggleableColumn[]) {
return html`
<div class="controls">
<slot name="toolbar"></slot>
Expand Down
Loading
Loading