Virtualize data tabel (#5066)

* WIP

* Fixes and implement further

* Give more space to table on mobile

* Remove unused deps

* Update ha-config-lovelace-dashboards.ts

* Add auto-height

* Console.bye

hihi

* lint
This commit is contained in:
Bram Kragten 2020-03-06 12:58:13 +01:00 committed by GitHub
parent 1599dc9e16
commit 5a2649a65b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 453 additions and 441 deletions

View File

@ -19,8 +19,6 @@
"license": "Apache-2.0",
"dependencies": {
"@material/chips": "^5.0.0",
"@material/data-table": "^5.0.0",
"@material/mwc-base": "^0.13.0",
"@material/mwc-button": "^0.13.0",
"@material/mwc-checkbox": "^0.13.0",
"@material/mwc-dialog": "^0.13.0",

View File

@ -1,27 +1,21 @@
import { repeat } from "lit-html/directives/repeat";
import deepClone from "deep-clone-simple";
import {
MDCDataTableAdapter,
MDCDataTableFoundation,
} from "@material/data-table";
import { classMap } from "lit-html/directives/class-map";
import { scroll } from "lit-virtualizer";
import {
html,
query,
queryAll,
CSSResult,
css,
customElement,
property,
TemplateResult,
PropertyValues,
LitElement,
} from "lit-element";
import { BaseElement } from "@material/mwc-base/base-element";
// eslint-disable-next-line import/no-webpack-loader-syntax
// @ts-ignore
// tslint:disable-next-line: no-implicit-dependencies
@ -35,6 +29,8 @@ import { HaCheckbox } from "../ha-checkbox";
import { fireEvent } from "../../common/dom/fire_event";
import { nextRender } from "../../common/util/render-status";
import { debounce } from "../../common/util/debounce";
import { styleMap } from "lit-html/directives/style-map";
import { ifDefined } from "lit-html/directives/if-defined";
declare global {
// for fire event
@ -50,8 +46,7 @@ export interface RowClickedEvent {
}
export interface SelectionChangedEvent {
id: string;
selected: boolean;
value: string[];
}
export interface SortingChangedEvent {
@ -76,6 +71,8 @@ export interface DataTableColumnData extends DataTableSortColumnData {
title: string;
type?: "numeric" | "icon";
template?: <T>(data: any, row: T) => TemplateResult | string;
width?: string;
grows?: boolean;
}
export interface DataTableRowData {
@ -84,26 +81,23 @@ export interface DataTableRowData {
}
@customElement("ha-data-table")
export class HaDataTable extends BaseElement {
export class HaDataTable extends LitElement {
@property({ type: Object }) public columns: DataTableColumnContainer = {};
@property({ type: Array }) public data: DataTableRowData[] = [];
@property({ type: Boolean }) public selectable = false;
@property({ type: Boolean, attribute: "auto-height" })
public autoHeight = false;
@property({ type: String }) public id = "id";
@property({ type: String }) public filter = "";
protected mdcFoundation!: MDCDataTableFoundation;
protected readonly mdcFoundationClass = MDCDataTableFoundation;
@query(".mdc-data-table") protected mdcRoot!: HTMLElement;
@queryAll(".mdc-data-table__row") protected rowElements!: HTMLElement[];
@property({ type: Boolean }) private _filterable = false;
@property({ type: Boolean }) private _headerChecked = false;
@property({ type: Boolean }) private _headerIndeterminate = false;
@property({ type: Array }) private _checkedRows: string[] = [];
@property({ type: String }) private _filter = "";
@property({ type: String }) private _sortColumn?: string;
@property({ type: String }) private _sortDirection: SortingDirection = null;
@property({ type: Array }) private _filteredData: DataTableRowData[] = [];
@query("slot[name='header']") private _header!: HTMLSlotElement;
@query(".scroller") private _scroller!: HTMLDivElement;
@query(".mdc-data-table__table") private _table!: HTMLDivElement;
private _checkableRowsCount?: number;
private _checkedRows: string[] = [];
private _sortColumns: {
[key: string]: DataTableSortColumnData;
} = {};
@ -114,18 +108,17 @@ export class HaDataTable extends BaseElement {
(value: string) => {
this._filter = value;
},
200,
100,
false
);
public clearSelection(): void {
this._headerChecked = false;
this._headerIndeterminate = false;
this.mdcFoundation.handleHeaderRowCheckboxChange();
this._checkedRows = [];
this._checkedRowsChanged();
}
protected firstUpdated() {
super.firstUpdated();
protected firstUpdated(properties: PropertyValues) {
super.firstUpdated(properties);
this._worker = sortFilterWorker();
}
@ -159,6 +152,12 @@ export class HaDataTable extends BaseElement {
this._debounceSearch(this.filter);
}
if (properties.has("data")) {
this._checkableRowsCount = this.data.filter(
(row) => row.selectable !== false
).length;
}
if (
properties.has("data") ||
properties.has("columns") ||
@ -173,7 +172,7 @@ export class HaDataTable extends BaseElement {
protected render() {
return html`
<div class="mdc-data-table">
<slot name="header" @slotchange=${this._calcScrollHeight}>
<slot name="header" @slotchange=${this._calcTableHeight}>
${this._filterable
? html`
<div class="table-header">
@ -184,25 +183,34 @@ export class HaDataTable extends BaseElement {
`
: ""}
</slot>
<div class="scroller">
<table class="mdc-data-table__table">
<thead>
<tr class="mdc-data-table__header-row">
<div
class="mdc-data-table__table ${classMap({
"auto-height": this.autoHeight,
})}"
style=${styleMap({
height: this.autoHeight
? `${this._filteredData.length * 52 + 56}px`
: `calc(100% - ${this._header?.clientHeight}px)`,
})}
>
<div class="mdc-data-table__header-row">
${this.selectable
? html`
<th
<div
class="mdc-data-table__header-cell mdc-data-table__header-cell--checkbox"
role="columnheader"
scope="col"
>
<ha-checkbox
class="mdc-data-table__row-checkbox"
@change=${this._handleHeaderRowCheckboxChange}
.indeterminate=${this._headerIndeterminate}
.checked=${this._headerChecked}
@change=${this._handleHeaderRowCheckboxClick}
.indeterminate=${this._checkedRows.length &&
this._checkedRows.length !== this._checkableRowsCount}
.checked=${this._checkedRows.length ===
this._checkableRowsCount}
>
</ha-checkbox>
</th>
</div>
`
: ""}
${Object.entries(this.columns).map((columnEntry) => {
@ -217,14 +225,22 @@ export class HaDataTable extends BaseElement {
),
sortable: Boolean(column.sortable),
"not-sorted": Boolean(column.sortable && !sorted),
grows: Boolean(column.grows),
};
return html`
<th
<div
class="mdc-data-table__header-cell ${classMap(classes)}"
style=${column.width
? styleMap({
[column.grows ? "minWidth" : "width"]: String(
column.width
),
})
: ""}
role="columnheader"
scope="col"
@click=${this._handleHeaderClick}
data-column-id="${key}"
.columnId=${key}
>
${column.sortable
? html`
@ -236,43 +252,50 @@ export class HaDataTable extends BaseElement {
`
: ""}
<span>${column.title}</span>
</th>
</div>
`;
})}
</tr>
</thead>
<tbody class="mdc-data-table__content">
${repeat(
this._filteredData!,
(row: DataTableRowData) => row[this.id],
(row: DataTableRowData) => html`
<tr
data-row-id="${row[this.id]}"
</div>
<div class="mdc-data-table__content scroller">
${scroll({
items: this._filteredData,
renderItem: (row: DataTableRowData) => html`
<div
.rowId="${row[this.id]}"
@click=${this._handleRowClick}
class="mdc-data-table__row"
class="mdc-data-table__row ${classMap({
"mdc-data-table__row--selected": this._checkedRows.includes(
String(row[this.id])
),
})}"
aria-selected=${ifDefined(
this._checkedRows.includes(String(row[this.id]))
? true
: undefined
)}
.selectable=${row.selectable !== false}
>
${this.selectable
? html`
<td
<div
class="mdc-data-table__cell mdc-data-table__cell--checkbox"
>
<ha-checkbox
class="mdc-data-table__row-checkbox"
@change=${this._handleRowCheckboxChange}
@change=${this._handleRowCheckboxClick}
.disabled=${row.selectable === false}
.checked=${this._checkedRows.includes(
String(row[this.id])
)}
>
</ha-checkbox>
</td>
</div>
`
: ""}
${Object.entries(this.columns).map((columnEntry) => {
const [key, column] = columnEntry;
return html`
<td
<div
class="mdc-data-table__cell ${classMap({
"mdc-data-table__cell--numeric": Boolean(
column.type && column.type === "numeric"
@ -280,72 +303,31 @@ export class HaDataTable extends BaseElement {
"mdc-data-table__cell--icon": Boolean(
column.type && column.type === "icon"
),
grows: Boolean(column.grows),
})}"
style=${column.width
? styleMap({
[column.grows ? "minWidth" : "width"]: String(
column.width
),
})
: ""}
>
${column.template
? column.template(row[key], row)
: row[key]}
</td>
</div>
`;
})}
</tr>
`
)}
</tbody>
</table>
</div>
`,
})}
</div>
</div>
</div>
`;
}
protected createAdapter(): MDCDataTableAdapter {
return {
addClassAtRowIndex: (rowIndex: number, cssClasses: string) => {
if (!(this.rowElements[rowIndex] as any).selectable) {
return;
}
this.rowElements[rowIndex].classList.add(cssClasses);
},
getRowCount: () => this.rowElements.length,
getRowElements: () => this.rowElements,
getRowIdAtIndex: (rowIndex: number) => this._getRowIdAtIndex(rowIndex),
getRowIndexByChildElement: (el: Element) =>
Array.prototype.indexOf.call(this.rowElements, el.closest("tr")),
getSelectedRowCount: () => this._checkedRows.length,
isCheckboxAtRowIndexChecked: (rowIndex: number) =>
this._checkedRows.includes(this._getRowIdAtIndex(rowIndex)),
isHeaderRowCheckboxChecked: () => this._headerChecked,
isRowsSelectable: () => this.selectable,
notifyRowSelectionChanged: () => undefined,
notifySelectedAll: () => undefined,
notifyUnselectedAll: () => undefined,
registerHeaderRowCheckbox: () => undefined,
registerRowCheckboxes: () => undefined,
removeClassAtRowIndex: (rowIndex: number, cssClasses: string) => {
this.rowElements[rowIndex].classList.remove(cssClasses);
},
setAttributeAtRowIndex: (
rowIndex: number,
attr: string,
value: string
) => {
this.rowElements[rowIndex].setAttribute(attr, value);
},
setHeaderRowCheckboxChecked: (checked: boolean) => {
this._headerChecked = checked;
},
setHeaderRowCheckboxIndeterminate: (indeterminate: boolean) => {
this._headerIndeterminate = indeterminate;
},
setRowCheckboxCheckedAtIndex: (rowIndex: number, checked: boolean) => {
if (!(this.rowElements[rowIndex] as any).selectable) {
return;
}
this._setRowChecked(this._getRowIdAtIndex(rowIndex), checked);
},
};
}
private async _filterData() {
const startTime = new Date().getTime();
this.curRequest++;
@ -373,14 +355,10 @@ export class HaDataTable extends BaseElement {
this._filteredData = data;
}
private _getRowIdAtIndex(rowIndex: number): string {
return this.rowElements[rowIndex].getAttribute("data-row-id")!;
}
private _handleHeaderClick(ev: Event) {
const columnId = (ev.target as HTMLElement)
.closest("th")!
.getAttribute("data-column-id")!;
const columnId = ((ev.target as HTMLElement).closest(
".mdc-data-table__header-cell"
) as any).columnId;
if (!this.columns[columnId].sortable) {
return;
}
@ -400,19 +378,32 @@ export class HaDataTable extends BaseElement {
});
}
private _handleHeaderRowCheckboxChange(ev: Event) {
private _handleHeaderRowCheckboxClick(ev: Event) {
const checkbox = ev.target as HaCheckbox;
this._headerChecked = checkbox.checked;
this._headerIndeterminate = checkbox.indeterminate;
this.mdcFoundation.handleHeaderRowCheckboxChange();
if (checkbox.checked) {
this._checkedRows = this._filteredData
.filter((data) => data.selectable !== false)
.map((data) => data[this.id]);
this._checkedRowsChanged();
} else {
this._checkedRows = [];
this._checkedRowsChanged();
}
}
private _handleRowCheckboxChange(ev: Event) {
private _handleRowCheckboxClick(ev: Event) {
const checkbox = ev.target as HaCheckbox;
const rowId = checkbox.closest("tr")!.getAttribute("data-row-id");
const rowId = (checkbox.closest(".mdc-data-table__row") as any).rowId;
this._setRowChecked(rowId!, checkbox.checked);
this.mdcFoundation.handleRowCheckboxChange(ev);
if (checkbox.checked) {
if (this._checkedRows.includes(rowId)) {
return;
}
this._checkedRows = [...this._checkedRows, rowId];
} else {
this._checkedRows = this._checkedRows.filter((row) => row !== rowId);
}
this._checkedRowsChanged();
}
private _handleRowClick(ev: Event) {
@ -420,26 +411,15 @@ export class HaDataTable extends BaseElement {
if (target.tagName === "HA-CHECKBOX") {
return;
}
const rowId = target.closest("tr")!.getAttribute("data-row-id")!;
const rowId = (target.closest(".mdc-data-table__row") as any).rowId;
fireEvent(this, "row-click", { id: rowId }, { bubbles: false });
}
private _setRowChecked(rowId: string, checked: boolean) {
if (checked) {
if (this._checkedRows.includes(rowId)) {
return;
}
this._checkedRows = [...this._checkedRows, rowId];
} else {
const index = this._checkedRows.indexOf(rowId);
if (index === -1) {
return;
}
this._checkedRows.splice(index, 1);
}
private _checkedRowsChanged() {
// force scroller to update, change it's items
this._filteredData = [...this._filteredData];
fireEvent(this, "selection-changed", {
id: rowId,
selected: checked,
value: this._checkedRows,
});
}
@ -447,15 +427,20 @@ export class HaDataTable extends BaseElement {
this._debounceSearch(ev.detail.value);
}
private async _calcScrollHeight() {
private async _calcTableHeight() {
if (this.autoHeight) {
return;
}
await this.updateComplete;
this._scroller.style.height = `calc(100% - ${this._header.clientHeight}px)`;
this._table.style.height = `calc(100% - ${this._header.clientHeight}px)`;
}
static get styles(): CSSResult {
return css`
/* default mdc styles, colors changed, without checkbox styles */
:host {
height: 100%;
}
.mdc-data-table__content {
font-family: Roboto, sans-serif;
-moz-osx-font-smoothing: grayscale;
@ -477,7 +462,7 @@ export class HaDataTable extends BaseElement {
display: inline-flex;
flex-direction: column;
box-sizing: border-box;
overflow-x: auto;
overflow: hidden;
}
.mdc-data-table__row--selected {
@ -485,12 +470,13 @@ export class HaDataTable extends BaseElement {
}
.mdc-data-table__row {
border-top-color: rgba(var(--rgb-primary-text-color), 0.12);
display: flex;
width: 100%;
height: 52px;
}
.mdc-data-table__row {
border-top-width: 1px;
border-top-style: solid;
.mdc-data-table__row ~ .mdc-data-table__row {
border-top: 1px solid rgba(var(--rgb-primary-text-color), 0.12);
}
.mdc-data-table__row:not(.mdc-data-table__row--selected):hover {
@ -507,16 +493,24 @@ export class HaDataTable extends BaseElement {
.mdc-data-table__header-row {
height: 56px;
display: flex;
width: 100%;
border-bottom: 1px solid rgba(var(--rgb-primary-text-color), 0.12);
overflow-x: auto;
}
.mdc-data-table__row {
height: 52px;
.mdc-data-table__header-row::-webkit-scrollbar {
display: none;
}
.mdc-data-table__cell,
.mdc-data-table__header-cell {
padding-right: 16px;
padding-left: 16px;
align-self: center;
overflow: hidden;
text-overflow: ellipsis;
flex-shrink: 0;
}
.mdc-data-table__header-cell--checkbox,
@ -538,10 +532,10 @@ export class HaDataTable extends BaseElement {
}
.mdc-data-table__table {
height: 100%;
width: 100%;
border: 0;
white-space: nowrap;
border-collapse: collapse;
}
.mdc-data-table__cell {
@ -568,12 +562,20 @@ export class HaDataTable extends BaseElement {
.mdc-data-table__cell--icon {
color: var(--secondary-text-color);
text-align: center;
}
.mdc-data-table__header-cell--icon,
.mdc-data-table__cell--icon {
width: 24px;
}
.mdc-data-table__header-cell--icon {
.mdc-data-table__header-cell.mdc-data-table__header-cell--icon {
text-align: center;
}
.mdc-data-table__header-cell.sortable.mdc-data-table__header-cell--icon:hover,
.mdc-data-table__header-cell.sortable.mdc-data-table__header-cell--icon:not(.not-sorted) {
text-align: left;
}
.mdc-data-table__cell--icon:first-child ha-icon {
margin-left: 8px;
@ -604,6 +606,10 @@ export class HaDataTable extends BaseElement {
.mdc-data-table__header-cell--numeric {
text-align: right;
}
.mdc-data-table__header-cell--numeric.sortable:hover,
.mdc-data-table__header-cell--numeric.sortable:not(.not-sorted) {
text-align: left;
}
[dir="rtl"] .mdc-data-table__header-cell--numeric,
.mdc-data-table__header-cell--numeric[dir="rtl"] {
/* @noflip */
@ -634,27 +640,21 @@ export class HaDataTable extends BaseElement {
cursor: pointer;
}
.mdc-data-table__header-cell > * {
transition: left 0.2s ease 0s;
transition: left 0.2s ease;
}
.mdc-data-table__header-cell ha-icon {
top: 15px;
top: -3px;
position: absolute;
}
.mdc-data-table__header-cell.not-sorted ha-icon {
left: -20px;
}
.mdc-data-table__header-cell:not(.not-sorted) span,
.mdc-data-table__header-cell.not-sorted:hover span {
.mdc-data-table__header-cell.sortable:not(.not-sorted) span,
.mdc-data-table__header-cell.sortable.not-sorted:hover span {
left: 24px;
}
.mdc-data-table__header-cell.mdc-data-table__header-cell--numeric:not(.not-sorted)
span,
.mdc-data-table__header-cell.mdc-data-table__header-cell--numeric.not-sorted:hover
span {
left: 12px;
}
.mdc-data-table__header-cell:not(.not-sorted) ha-icon,
.mdc-data-table__header-cell:hover.not-sorted ha-icon {
.mdc-data-table__header-cell.sortable:not(.not-sorted) ha-icon,
.mdc-data-table__header-cell.sortable:hover.not-sorted ha-icon {
left: 12px;
}
.table-header {
@ -664,14 +664,24 @@ export class HaDataTable extends BaseElement {
position: relative;
top: 2px;
}
.scroller {
overflow: auto;
}
slot[name="header"] {
display: block;
}
.secondary {
color: var(--secondary-text-color);
.center {
text-align: center;
}
.scroller {
display: flex;
position: relative;
contain: strict;
height: calc(100% - 57px);
}
.mdc-data-table__table:not(.auto-height) .scroller {
overflow: auto;
}
.grows {
flex-grow: 1;
flex-shrink: 1;
}
`;
}

View File

@ -26,6 +26,7 @@ import {
RowClickedEvent,
} from "../../../components/data-table/ha-data-table";
import { navigate } from "../../../common/navigate";
import { HASSDomEvent } from "../../../common/dom/fire_event";
@customElement("ha-config-devices-dashboard")
export class HaConfigDeviceDashboard extends LitElement {
@ -127,6 +128,7 @@ export class HaConfigDeviceDashboard extends LitElement {
sortable: true,
filterable: true,
direction: "asc",
grows: true,
template: (name, device: DataTableRowData) => {
const battery = device.battery_entity
? this.hass.states[device.battery_entity]
@ -155,6 +157,7 @@ export class HaConfigDeviceDashboard extends LitElement {
),
sortable: true,
filterable: true,
grows: true,
direction: "asc",
},
manufacturer: {
@ -163,6 +166,7 @@ export class HaConfigDeviceDashboard extends LitElement {
),
sortable: true,
filterable: true,
width: "15%",
},
model: {
title: this.hass.localize(
@ -170,6 +174,7 @@ export class HaConfigDeviceDashboard extends LitElement {
),
sortable: true,
filterable: true,
width: "15%",
},
area: {
title: this.hass.localize(
@ -177,6 +182,7 @@ export class HaConfigDeviceDashboard extends LitElement {
),
sortable: true,
filterable: true,
width: "15%",
},
integration: {
title: this.hass.localize(
@ -184,6 +190,7 @@ export class HaConfigDeviceDashboard extends LitElement {
),
sortable: true,
filterable: true,
width: "15%",
},
battery_entity: {
title: this.hass.localize(
@ -191,6 +198,7 @@ export class HaConfigDeviceDashboard extends LitElement {
),
sortable: true,
type: "numeric",
width: "60px",
template: (batteryEntity: string) => {
const battery = batteryEntity
? this.hass.states[batteryEntity]
@ -247,8 +255,8 @@ export class HaConfigDeviceDashboard extends LitElement {
return batteryEntity ? batteryEntity.entity_id : undefined;
}
private _handleRowClicked(ev: CustomEvent) {
const deviceId = (ev.detail as RowClickedEvent).id;
private _handleRowClicked(ev: HASSDomEvent<RowClickedEvent>) {
const deviceId = ev.detail.id;
navigate(this, `/config/devices/device/${deviceId}`);
}
}

View File

@ -134,6 +134,7 @@ export class HaDevicesDataTable extends LitElement {
sortable: true,
filterable: true,
direction: "asc",
grows: true,
template: (name, device: DataTableRowData) => {
const battery = device.battery_entity
? this.hass.states[device.battery_entity]
@ -163,6 +164,7 @@ export class HaDevicesDataTable extends LitElement {
sortable: true,
filterable: true,
direction: "asc",
grows: true,
},
manufacturer: {
title: this.hass.localize(
@ -170,6 +172,7 @@ export class HaDevicesDataTable extends LitElement {
),
sortable: true,
filterable: true,
width: "15%",
},
model: {
title: this.hass.localize(
@ -177,6 +180,7 @@ export class HaDevicesDataTable extends LitElement {
),
sortable: true,
filterable: true,
width: "15%",
},
area: {
title: this.hass.localize(
@ -184,6 +188,7 @@ export class HaDevicesDataTable extends LitElement {
),
sortable: true,
filterable: true,
width: "15%",
},
integration: {
title: this.hass.localize(
@ -191,6 +196,7 @@ export class HaDevicesDataTable extends LitElement {
),
sortable: true,
filterable: true,
width: "15%",
},
battery_entity: {
title: this.hass.localize(
@ -198,6 +204,7 @@ export class HaDevicesDataTable extends LitElement {
),
sortable: true,
type: "numeric",
width: "60px",
template: (batteryEntity: string) => {
const battery = batteryEntity
? this.hass.states[batteryEntity]

View File

@ -49,6 +49,7 @@ import { classMap } from "lit-html/directives/class-map";
import { computeStateName } from "../../../common/entity/compute_state_name";
// tslint:disable-next-line: no-duplicate-imports
import { HaTabsSubpageDataTable } from "../../../layouts/hass-tabs-subpage-data-table";
import { HASSDomEvent } from "../../../common/dom/fire_event";
export interface StateEntity extends EntityRegistryEntry {
readonly?: boolean;
@ -96,6 +97,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
sortable: true,
filterable: true,
direction: "asc",
grows: true,
},
};
@ -106,6 +108,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
type: "icon",
sortable: true,
filterable: true,
width: "55px",
template: (_status, entity: any) =>
entity.unavailable || entity.disabled_by || entity.readonly
? html`
@ -166,6 +169,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
),
sortable: true,
filterable: true,
width: "20%",
};
columns.platform = {
title: this.hass.localize(
@ -173,6 +177,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
),
sortable: true,
filterable: true,
width: "20%",
template: (platform) =>
this.hass.localize(`component.${platform}.config.title`) || platform,
};
@ -467,16 +472,10 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
this._filter = ev.detail.value;
}
private _handleSelectionChanged(ev: CustomEvent): void {
const changedSelection = ev.detail as SelectionChangedEvent;
const entity = changedSelection.id;
if (changedSelection.selected) {
this._selectedEntities = [...this._selectedEntities, entity];
} else {
this._selectedEntities = this._selectedEntities.filter(
(entityId) => entityId !== entity
);
}
private _handleSelectionChanged(
ev: HASSDomEvent<SelectionChangedEvent>
): void {
this._selectedEntities = ev.detail.value;
}
private _enableSelected() {

View File

@ -39,8 +39,8 @@ export class HaConfigHelpers extends LitElement {
@property() private _stateItems: HassEntity[] = [];
private _columns = memoize(
(_language): DataTableColumnContainer => {
return {
(narrow, _language): DataTableColumnContainer => {
const columns: DataTableColumnContainer = {
icon: {
title: "",
type: "icon",
@ -54,28 +54,45 @@ export class HaConfigHelpers extends LitElement {
),
sortable: true,
filterable: true,
grows: true,
direction: "asc",
template: (name, item: any) =>
html`
${name}
<div style="color: var(--secondary-text-color)">
${narrow
? html`
<div class="secondary">
${item.entity_id}
</div>
`
: ""}
`,
},
type: {
};
if (!narrow) {
columns.entity_id = {
title: this.hass.localize(
"ui.panel.config.helpers.picker.headers.entity_id"
),
sortable: true,
filterable: true,
width: "30%",
};
}
columns.type = {
title: this.hass.localize(
"ui.panel.config.helpers.picker.headers.type"
),
sortable: true,
width: "30%",
filterable: true,
template: (type) =>
html`
${this.hass.localize(`ui.panel.config.helpers.types.${type}`) ||
type}
`,
},
};
return columns;
}
);
@ -106,7 +123,7 @@ export class HaConfigHelpers extends LitElement {
back-path="/config"
.route=${this.route}
.tabs=${configSections.automation}
.columns=${this._columns(this.hass.language)}
.columns=${this._columns(this.narrow, this.hass.language)}
.data=${this._getItems(this._stateItems)}
@row-click=${this._openEditDialog}
>

View File

@ -194,12 +194,14 @@ class HaConfigEntryPage extends LitElement {
return css`
.content {
padding: 4px;
height: 100%;
}
p {
text-align: center;
}
ha-devices-data-table {
width: 100%;
height: 100%;
}
`;
}

View File

@ -61,6 +61,7 @@ export class HaConfigLovelaceDashboards extends LitElement {
sortable: true,
filterable: true,
direction: "asc",
grows: true,
template: (title, dashboard: any) => {
const titleTemplate = html`
${title}
@ -101,6 +102,7 @@ export class HaConfigLovelaceDashboards extends LitElement {
),
sortable: true,
filterable: true,
width: "15%",
template: (mode) =>
html`
${this.hass.localize(
@ -113,6 +115,7 @@ export class HaConfigLovelaceDashboards extends LitElement {
title: this.hass.localize(
"ui.panel.config.lovelace.dashboards.picker.headers.filename"
),
width: "15%",
sortable: true,
filterable: true,
};
@ -123,6 +126,7 @@ export class HaConfigLovelaceDashboards extends LitElement {
),
sortable: true,
type: "icon",
width: "100px",
template: (requireAdmin: boolean) =>
requireAdmin
? html`
@ -137,6 +141,7 @@ export class HaConfigLovelaceDashboards extends LitElement {
"ui.panel.config.lovelace.dashboards.picker.headers.sidebar"
),
type: "icon",
width: "100px",
template: (sidebar) =>
sidebar
? html`
@ -151,6 +156,7 @@ export class HaConfigLovelaceDashboards extends LitElement {
columns.url_path = {
title: "",
filterable: true,
width: "75px",
template: (urlPath) =>
narrow
? html`

View File

@ -57,6 +57,7 @@ export class HaConfigLovelaceRescources extends LitElement {
sortable: true,
filterable: true,
direction: "asc",
grows: true,
},
type: {
title: this.hass.localize(
@ -64,6 +65,7 @@ export class HaConfigLovelaceRescources extends LitElement {
),
sortable: true,
filterable: true,
width: "30%",
template: (type) =>
html`
${this.hass.localize(

View File

@ -25,6 +25,7 @@ import { PolymerChangedEvent } from "../../../polymer-types";
import "@polymer/paper-spinner/paper-spinner";
import "@material/mwc-button";
import { PaperInputElement } from "@polymer/paper-input/paper-input";
import { HASSDomEvent } from "../../../common/dom/fire_event";
@customElement("zha-add-group-page")
export class ZHAAddGroupPage extends LitElement {
@ -82,7 +83,6 @@ export class ZHAAddGroupPage extends LitElement {
.narrow=${this.narrow}
selectable
@selection-changed=${this._handleAddSelectionChanged}
class="table"
>
</zha-devices-data-table>
@ -114,21 +114,10 @@ export class ZHAAddGroupPage extends LitElement {
this.devices = await fetchGroupableDevices(this.hass!);
}
private _handleAddSelectionChanged(ev: CustomEvent): void {
const changedSelection = ev.detail as SelectionChangedEvent;
const entity = changedSelection.id;
if (
changedSelection.selected &&
!this._selectedDevicesToAdd.includes(entity)
) {
this._selectedDevicesToAdd.push(entity);
} else {
const index = this._selectedDevicesToAdd.indexOf(entity);
if (index !== -1) {
this._selectedDevicesToAdd.splice(index, 1);
}
}
this._selectedDevicesToAdd = [...this._selectedDevicesToAdd];
private _handleAddSelectionChanged(
ev: HASSDomEvent<SelectionChangedEvent>
): void {
this._selectedDevicesToAdd = ev.detail.value;
}
private async _createGroup(): Promise<void> {
@ -168,11 +157,6 @@ export class ZHAAddGroupPage extends LitElement {
float: right;
}
.table {
height: 400px;
overflow: auto;
}
ha-config-section *:last-child {
padding-bottom: 24px;
}

View File

@ -49,6 +49,7 @@ export class ZHAClustersDataTable extends LitElement {
title: "Name",
sortable: true,
direction: "asc",
grows: true,
},
}
: {
@ -56,6 +57,7 @@ export class ZHAClustersDataTable extends LitElement {
title: "Name",
sortable: true,
direction: "asc",
grows: true,
},
id: {
title: "ID",
@ -65,10 +67,12 @@ export class ZHAClustersDataTable extends LitElement {
`;
},
sortable: true,
width: "15%",
},
endpoint_id: {
title: "Endpoint ID",
sortable: true,
width: "15%",
},
}
);
@ -80,6 +84,7 @@ export class ZHAClustersDataTable extends LitElement {
.data=${this._clusters(this.clusters)}
.id=${"cluster_id"}
selectable
auto-height
></ha-data-table>
`;
}

View File

@ -63,6 +63,7 @@ class ZHAConfigDashboard extends LitElement {
sortable: true,
filterable: true,
direction: "asc",
grows: true,
},
}
: {
@ -71,16 +72,19 @@ class ZHAConfigDashboard extends LitElement {
sortable: true,
filterable: true,
direction: "asc",
grows: true,
},
nwk: {
title: "Nwk",
sortable: true,
filterable: true,
width: "15%",
},
ieee: {
title: "IEEE",
sortable: true,
filterable: true,
width: "25%",
},
}
);
@ -139,6 +143,7 @@ class ZHAConfigDashboard extends LitElement {
.data=${this._memoizeDevices(this._devices)}
@row-click=${this._handleDeviceClicked}
.id=${"ieee"}
auto-height
></ha-data-table>
</ha-card>
</ha-config-section>

View File

@ -53,6 +53,7 @@ export class ZHADevicesDataTable extends LitElement {
sortable: true,
filterable: true,
direction: "asc",
grows: true,
template: (name) => html`
<div @click=${this._handleClicked} style="cursor: pointer;">
${name}
@ -66,6 +67,7 @@ export class ZHADevicesDataTable extends LitElement {
sortable: true,
filterable: true,
direction: "asc",
grows: true,
template: (name) => html`
<div @click=${this._handleClicked} style="cursor: pointer;">
${name}
@ -76,11 +78,13 @@ export class ZHADevicesDataTable extends LitElement {
title: "Manufacturer",
sortable: true,
filterable: true,
width: "20%",
},
model: {
title: "Model",
sortable: true,
filterable: true,
width: "20%",
},
}
);
@ -91,6 +95,7 @@ export class ZHADevicesDataTable extends LitElement {
.columns=${this._columns(this.narrow)}
.data=${this._devices(this.devices)}
.selectable=${this.selectable}
auto-height
></ha-data-table>
`;
}

View File

@ -32,6 +32,7 @@ import { HomeAssistant } from "../../../types";
import { ItemSelectedEvent } from "./types";
import "@polymer/paper-item/paper-item";
import { SelectionChangedEvent } from "../../../components/data-table/ha-data-table";
import { HASSDomEvent } from "../../../common/dom/fire_event";
@customElement("zha-group-binding-control")
export class ZHAGroupBindingControl extends LitElement {
@ -200,21 +201,11 @@ export class ZHAGroupBindingControl extends LitElement {
}
}
private _handleClusterSelectionChanged(event: CustomEvent): void {
const changedSelection = event.detail as SelectionChangedEvent;
const clusterId = changedSelection.id;
if (
changedSelection.selected &&
!this._selectedClusters.includes(clusterId)
) {
this._selectedClusters.push(clusterId);
} else {
const index = this._selectedClusters.indexOf(clusterId);
if (index !== -1) {
this._selectedClusters.splice(index, 1);
}
}
this._selectedClusters = [...this._selectedClusters];
private _handleClusterSelectionChanged(
ev: HASSDomEvent<SelectionChangedEvent>
): void {
this._selectedClusters = ev.detail.value;
this._clustersToBind = [];
for (const clusterIndex of this._selectedClusters) {
const selectedCluster = this._clusters.find((cluster) => {

View File

@ -31,6 +31,7 @@ import "@polymer/paper-icon-button/paper-icon-button";
import "@polymer/paper-spinner/paper-spinner";
import "@material/mwc-button";
import { SelectionChangedEvent } from "../../../components/data-table/ha-data-table";
import { HASSDomEvent } from "../../../common/dom/fire_event";
@customElement("zha-group-page")
export class ZHAGroupPage extends LitElement {
@ -145,7 +146,6 @@ export class ZHAGroupPage extends LitElement {
.narrow=${this.narrow}
selectable
@selection-changed=${this._handleRemoveSelectionChanged}
class="table"
>
</zha-devices-data-table>
@ -180,7 +180,6 @@ export class ZHAGroupPage extends LitElement {
.narrow=${this.narrow}
selectable
@selection-changed=${this._handleAddSelectionChanged}
class="table"
>
</zha-devices-data-table>
@ -223,38 +222,16 @@ export class ZHAGroupPage extends LitElement {
});
}
private _handleAddSelectionChanged(ev: CustomEvent): void {
const changedSelection = ev.detail as SelectionChangedEvent;
const entity = changedSelection.id;
if (
changedSelection.selected &&
!this._selectedDevicesToAdd.includes(entity)
) {
this._selectedDevicesToAdd.push(entity);
} else {
const index = this._selectedDevicesToAdd.indexOf(entity);
if (index !== -1) {
this._selectedDevicesToAdd.splice(index, 1);
}
}
this._selectedDevicesToAdd = [...this._selectedDevicesToAdd];
private _handleAddSelectionChanged(
ev: HASSDomEvent<SelectionChangedEvent>
): void {
this._selectedDevicesToAdd = ev.detail.value;
}
private _handleRemoveSelectionChanged(ev: CustomEvent): void {
const changedSelection = ev.detail as SelectionChangedEvent;
const entity = changedSelection.id;
if (
changedSelection.selected &&
!this._selectedDevicesToRemove.includes(entity)
) {
this._selectedDevicesToRemove.push(entity);
} else {
const index = this._selectedDevicesToRemove.indexOf(entity);
if (index !== -1) {
this._selectedDevicesToRemove.splice(index, 1);
}
}
this._selectedDevicesToRemove = [...this._selectedDevicesToRemove];
private _handleRemoveSelectionChanged(
ev: HASSDomEvent<SelectionChangedEvent>
): void {
this._selectedDevicesToRemove = ev.detail.value;
}
private async _addMembersToGroup(): Promise<void> {
@ -309,11 +286,6 @@ export class ZHAGroupPage extends LitElement {
float: right;
}
.table {
height: 200px;
overflow: auto;
}
mwc-button paper-spinner {
width: 14px;
height: 14px;

View File

@ -19,6 +19,7 @@ import "@polymer/paper-spinner/paper-spinner";
import "@polymer/paper-icon-button/paper-icon-button";
import { navigate } from "../../../common/navigate";
import "../../../layouts/hass-subpage";
import { HASSDomEvent } from "../../../common/dom/fire_event";
@customElement("zha-groups-dashboard")
export class ZHAGroupsDashboard extends LitElement {
@ -102,21 +103,12 @@ export class ZHAGroupsDashboard extends LitElement {
this._groups = (await fetchGroups(this.hass!)).sort(sortZHAGroups);
}
private _handleRemoveSelectionChanged(ev: CustomEvent): void {
const changedSelection = ev.detail as SelectionChangedEvent;
const groupId = Number(changedSelection.id);
if (
changedSelection.selected &&
!this._selectedGroupsToRemove.includes(groupId)
) {
this._selectedGroupsToRemove.push(groupId);
} else {
const index = this._selectedGroupsToRemove.indexOf(groupId);
if (index !== -1) {
this._selectedGroupsToRemove.splice(index, 1);
}
}
this._selectedGroupsToRemove = [...this._selectedGroupsToRemove];
private _handleRemoveSelectionChanged(
ev: HASSDomEvent<SelectionChangedEvent>
): void {
this._selectedGroupsToRemove = ev.detail.value.map((value) =>
Number(value)
);
}
private async _removeGroup(): Promise<void> {

View File

@ -52,6 +52,7 @@ export class ZHAGroupsDataTable extends LitElement {
sortable: true,
filterable: true,
direction: "asc",
grows: true,
template: (name) => html`
<div @click=${this._handleRowClicked} style="cursor: pointer;">
${name}
@ -65,6 +66,7 @@ export class ZHAGroupsDataTable extends LitElement {
sortable: true,
filterable: true,
direction: "asc",
grows: true,
template: (name) => html`
<div @click=${this._handleRowClicked} style="cursor: pointer;">
${name}
@ -73,6 +75,8 @@ export class ZHAGroupsDataTable extends LitElement {
},
group_id: {
title: this.hass.localize("ui.panel.config.zha.groups.group_id"),
type: "numeric",
width: "15%",
template: (groupId: number) => {
return html`
${formatAsPaddedHex(groupId)}
@ -82,6 +86,8 @@ export class ZHAGroupsDataTable extends LitElement {
},
members: {
title: this.hass.localize("ui.panel.config.zha.groups.members"),
type: "numeric",
width: "15%",
template: (members: ZHADevice[]) => {
return html`
${members.length}
@ -98,6 +104,7 @@ export class ZHAGroupsDataTable extends LitElement {
.columns=${this._columns(this.narrow)}
.data=${this._groups(this.groups)}
.selectable=${this.selectable}
auto-height
></ha-data-table>
`;
}

View File

@ -34,7 +34,7 @@ import { computeUnusedEntities } from "../../common/compute-unused-entities";
import { HomeAssistant } from "../../../../types";
import { Lovelace } from "../../types";
import { LovelaceConfig } from "../../../../data/lovelace";
import { fireEvent } from "../../../../common/dom/fire_event";
import { fireEvent, HASSDomEvent } from "../../../../common/dom/fire_event";
import { addEntitiesToLovelaceView } from "../add-entities-to-view";
@customElement("hui-unused-entities")
@ -55,19 +55,33 @@ export class HuiUnusedEntities extends LitElement {
private _columns = memoizeOne((narrow: boolean) => {
const columns: DataTableColumnContainer = {
entity: {
icon: {
title: "",
type: "icon",
template: (_icon, entity: any) => html`
<state-badge
@click=${this._handleEntityClicked}
.hass=${this.hass!}
.stateObj=${entity.stateObj}
></state-badge>
`,
},
name: {
title: this.hass!.localize("ui.panel.lovelace.unused_entities.entity"),
sortable: true,
filterable: true,
filterKey: "friendly_name",
grows: true,
direction: "asc",
template: (stateObj) => html`
template: (name, entity: any) => html`
<div @click=${this._handleEntityClicked} style="cursor: pointer;">
<state-badge
.hass=${this.hass!}
.stateObj=${stateObj}
></state-badge>
${stateObj.friendly_name}
${name}
${narrow
? html`
<div class="secondary">
${entity.stateObj.entity_id}
</div>
`
: ""}
</div>
`,
},
@ -81,11 +95,13 @@ export class HuiUnusedEntities extends LitElement {
title: this.hass!.localize("ui.panel.lovelace.unused_entities.entity_id"),
sortable: true,
filterable: true,
width: "30%",
};
columns.domain = {
title: this.hass!.localize("ui.panel.lovelace.unused_entities.domain"),
sortable: true,
filterable: true,
width: "15%",
};
columns.last_changed = {
title: this.hass!.localize(
@ -93,6 +109,7 @@ export class HuiUnusedEntities extends LitElement {
),
type: "numeric",
sortable: true,
width: "15%",
template: (lastChanged: string) => html`
<ha-relative-time
.hass=${this.hass!}
@ -122,6 +139,8 @@ export class HuiUnusedEntities extends LitElement {
}
return html`
${!this.narrow
? html`
<ha-card
header="${this.hass.localize(
"ui.panel.lovelace.unused_entities.title"
@ -140,16 +159,17 @@ export class HuiUnusedEntities extends LitElement {
: ""}
</div>
</ha-card>
`
: ""}
<ha-data-table
.columns=${this._columns(this.narrow!)}
.data=${this._unusedEntities.map((entity) => {
const stateObj = this.hass!.states[entity];
return {
icon: "",
entity_id: entity,
entity: {
...stateObj,
friendly_name: computeStateName(stateObj),
},
stateObj,
name: computeStateName(stateObj),
domain: computeDomain(entity),
last_changed: stateObj!.last_changed,
};
@ -178,23 +198,16 @@ export class HuiUnusedEntities extends LitElement {
this._unusedEntities = computeUnusedEntities(this.hass, this._config!);
}
private _handleSelectionChanged(ev: CustomEvent): void {
const changedSelection = ev.detail as SelectionChangedEvent;
const entity = changedSelection.id;
if (changedSelection.selected) {
this._selectedEntities.push(entity);
} else {
const index = this._selectedEntities.indexOf(entity);
if (index !== -1) {
this._selectedEntities.splice(index, 1);
}
}
private _handleSelectionChanged(
ev: HASSDomEvent<SelectionChangedEvent>
): void {
this._selectedEntities = ev.detail.value;
}
private _handleEntityClicked(ev: Event) {
const entityId = (ev.target as HTMLElement)
.closest("tr")!
.getAttribute("data-row-id")!;
const entityId = ((ev.target as HTMLElement).closest(
".mdc-data-table__row"
) as any).rowId;
fireEvent(this, "hass-more-info", {
entityId,
});
@ -214,20 +227,27 @@ export class HuiUnusedEntities extends LitElement {
return css`
:host {
background: var(--lovelace-background);
padding: 16px;
box-sizing: border-box;
display: flex;
flex-direction: column;
}
ha-card {
--ha-card-box-shadow: none;
--ha-card-border-radius: 0;
}
ha-data-table {
--data-table-border-width: 0;
flex-grow: 1;
margin-top: -20px;
}
ha-fab {
position: sticky;
float: right;
position: absolute;
right: 16px;
bottom: 16px;
z-index: 1;
}
ha-fab.rtl {
float: left;
}
ha-card {
margin-bottom: 16px;
left: 16px;
right: auto;
}
`;
}

View File

@ -6,6 +6,7 @@ import {
CSSResult,
css,
property,
query,
} from "lit-element";
import { classMap } from "lit-html/directives/class-map";
import "@polymer/app-layout/app-header-layout/app-header-layout";
@ -60,6 +61,7 @@ class HUIRoot extends LitElement {
@property() public route?: { path: string; prefix: string };
@property() private _routeData?: { view: string };
@property() private _curView?: number | "hass-unused-entities";
@query("ha-app-layout") private _appLayout!: HTMLElement;
private _viewCache?: { [viewId: string]: HUIView };
private _debouncedConfigChanged: () => void;
@ -344,7 +346,8 @@ class HUIRoot extends LitElement {
}
</app-header>
<div id='view' class="${classMap({
"tabs-hidden": this.lovelace!.config.views.length < 2,
"tabs-hidden":
!this._editMode && this.lovelace!.config.views.length < 2,
})}" @ll-rebuild='${this._debouncedConfigChanged}'></div>
</app-header-layout>
`;
@ -468,7 +471,7 @@ class HUIRoot extends LitElement {
if (changedProperties.has("route")) {
const views = this.config.views;
if (this.route!.path === "" && views) {
if (this.route!.path === "" && views.length) {
navigate(this, `${this.route!.prefix}/${views[0].path || 0}`, true);
newSelectView = 0;
} else if (this._routeData!.view === "hass-unused-entities") {
@ -495,8 +498,6 @@ class HUIRoot extends LitElement {
if (!oldLovelace || oldLovelace.config !== this.lovelace!.config) {
// On config change, recreate the current view from scratch.
force = true;
// Recalculate to see if we need to adjust content area for tab bar
fireEvent(this, "iron-resize");
}
if (!oldLovelace || oldLovelace.editMode !== this.lovelace!.editMode) {
@ -506,13 +507,11 @@ class HUIRoot extends LitElement {
this._routeData!.view === "hass-unused-entities"
) {
const views = this.config && this.config.views;
navigate(this, `${this.route?.prefix}/${views[0].path || 0}`);
navigate(this, `${this.route?.prefix}/${views[0]?.path || 0}`);
newSelectView = 0;
}
// On edit mode change, recreate the current view from scratch
force = true;
// Recalculate to see if we need to adjust content area for tab bar
fireEvent(this, "iron-resize");
}
}
@ -578,14 +577,14 @@ class HUIRoot extends LitElement {
}
this.lovelace!.setEditMode(true);
if (this.config.views.length < 2) {
fireEvent(this, "iron-resize");
this.updateComplete.then(() => fireEvent(this._appLayout, "iron-resize"));
}
}
private _editModeDisable(): void {
this.lovelace!.setEditMode(false);
if (this.config.views.length < 2) {
fireEvent(this, "iron-resize");
this.updateComplete.then(() => fireEvent(this._appLayout, "iron-resize"));
}
}

View File

@ -811,6 +811,7 @@
"picker": {
"headers": {
"name": "Name",
"entity_id": "Entity ID",
"type": "Type",
"editable": "Editable"
},

View File

@ -1515,24 +1515,6 @@
"@material/typography" "^5.0.0"
tslib "^1.9.3"
"@material/data-table@^5.0.0":
version "5.0.0"
resolved "https://registry.yarnpkg.com/@material/data-table/-/data-table-5.0.0.tgz#441f4bde8f4206273cbc304baf26d003cac4ea8d"
integrity sha512-h7GHVGwStqeBigkWrr+5/VWX6iqCG1eXWoD/my7YfBZzOOcJ3xvSfF+jNPchK0bRPSzX06jhaNRvGOmu3HCAzg==
dependencies:
"@material/animation" "^5.0.0"
"@material/base" "^5.0.0"
"@material/checkbox" "^5.0.0"
"@material/density" "^5.0.0"
"@material/dom" "^5.0.0"
"@material/elevation" "^5.0.0"
"@material/feature-targeting" "^5.0.0"
"@material/rtl" "^5.0.0"
"@material/shape" "^5.0.0"
"@material/theme" "^5.0.0"
"@material/typography" "^5.0.0"
tslib "^1.10.0"
"@material/density@^5.0.0":
version "5.0.0"
resolved "https://registry.yarnpkg.com/@material/density/-/density-5.0.0.tgz#643d9bd1a5d89b3985d48fd1d6572f73d05fb2e9"