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

View File

@ -134,6 +134,7 @@ export class HaDevicesDataTable extends LitElement {
sortable: true, sortable: true,
filterable: true, filterable: true,
direction: "asc", direction: "asc",
grows: true,
template: (name, device: DataTableRowData) => { template: (name, device: DataTableRowData) => {
const battery = device.battery_entity const battery = device.battery_entity
? this.hass.states[device.battery_entity] ? this.hass.states[device.battery_entity]
@ -163,6 +164,7 @@ export class HaDevicesDataTable extends LitElement {
sortable: true, sortable: true,
filterable: true, filterable: true,
direction: "asc", direction: "asc",
grows: true,
}, },
manufacturer: { manufacturer: {
title: this.hass.localize( title: this.hass.localize(
@ -170,6 +172,7 @@ export class HaDevicesDataTable extends LitElement {
), ),
sortable: true, sortable: true,
filterable: true, filterable: true,
width: "15%",
}, },
model: { model: {
title: this.hass.localize( title: this.hass.localize(
@ -177,6 +180,7 @@ export class HaDevicesDataTable extends LitElement {
), ),
sortable: true, sortable: true,
filterable: true, filterable: true,
width: "15%",
}, },
area: { area: {
title: this.hass.localize( title: this.hass.localize(
@ -184,6 +188,7 @@ export class HaDevicesDataTable extends LitElement {
), ),
sortable: true, sortable: true,
filterable: true, filterable: true,
width: "15%",
}, },
integration: { integration: {
title: this.hass.localize( title: this.hass.localize(
@ -191,6 +196,7 @@ export class HaDevicesDataTable extends LitElement {
), ),
sortable: true, sortable: true,
filterable: true, filterable: true,
width: "15%",
}, },
battery_entity: { battery_entity: {
title: this.hass.localize( title: this.hass.localize(
@ -198,6 +204,7 @@ export class HaDevicesDataTable extends LitElement {
), ),
sortable: true, sortable: true,
type: "numeric", type: "numeric",
width: "60px",
template: (batteryEntity: string) => { template: (batteryEntity: string) => {
const battery = batteryEntity const battery = batteryEntity
? this.hass.states[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"; import { computeStateName } from "../../../common/entity/compute_state_name";
// tslint:disable-next-line: no-duplicate-imports // tslint:disable-next-line: no-duplicate-imports
import { HaTabsSubpageDataTable } from "../../../layouts/hass-tabs-subpage-data-table"; import { HaTabsSubpageDataTable } from "../../../layouts/hass-tabs-subpage-data-table";
import { HASSDomEvent } from "../../../common/dom/fire_event";
export interface StateEntity extends EntityRegistryEntry { export interface StateEntity extends EntityRegistryEntry {
readonly?: boolean; readonly?: boolean;
@ -96,6 +97,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
sortable: true, sortable: true,
filterable: true, filterable: true,
direction: "asc", direction: "asc",
grows: true,
}, },
}; };
@ -106,6 +108,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
type: "icon", type: "icon",
sortable: true, sortable: true,
filterable: true, filterable: true,
width: "55px",
template: (_status, entity: any) => template: (_status, entity: any) =>
entity.unavailable || entity.disabled_by || entity.readonly entity.unavailable || entity.disabled_by || entity.readonly
? html` ? html`
@ -166,6 +169,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
), ),
sortable: true, sortable: true,
filterable: true, filterable: true,
width: "20%",
}; };
columns.platform = { columns.platform = {
title: this.hass.localize( title: this.hass.localize(
@ -173,6 +177,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
), ),
sortable: true, sortable: true,
filterable: true, filterable: true,
width: "20%",
template: (platform) => template: (platform) =>
this.hass.localize(`component.${platform}.config.title`) || platform, this.hass.localize(`component.${platform}.config.title`) || platform,
}; };
@ -467,16 +472,10 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
this._filter = ev.detail.value; this._filter = ev.detail.value;
} }
private _handleSelectionChanged(ev: CustomEvent): void { private _handleSelectionChanged(
const changedSelection = ev.detail as SelectionChangedEvent; ev: HASSDomEvent<SelectionChangedEvent>
const entity = changedSelection.id; ): void {
if (changedSelection.selected) { this._selectedEntities = ev.detail.value;
this._selectedEntities = [...this._selectedEntities, entity];
} else {
this._selectedEntities = this._selectedEntities.filter(
(entityId) => entityId !== entity
);
}
} }
private _enableSelected() { private _enableSelected() {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -32,6 +32,7 @@ import { HomeAssistant } from "../../../types";
import { ItemSelectedEvent } from "./types"; import { ItemSelectedEvent } from "./types";
import "@polymer/paper-item/paper-item"; import "@polymer/paper-item/paper-item";
import { SelectionChangedEvent } from "../../../components/data-table/ha-data-table"; import { SelectionChangedEvent } from "../../../components/data-table/ha-data-table";
import { HASSDomEvent } from "../../../common/dom/fire_event";
@customElement("zha-group-binding-control") @customElement("zha-group-binding-control")
export class ZHAGroupBindingControl extends LitElement { export class ZHAGroupBindingControl extends LitElement {
@ -200,21 +201,11 @@ export class ZHAGroupBindingControl extends LitElement {
} }
} }
private _handleClusterSelectionChanged(event: CustomEvent): void { private _handleClusterSelectionChanged(
const changedSelection = event.detail as SelectionChangedEvent; ev: HASSDomEvent<SelectionChangedEvent>
const clusterId = changedSelection.id; ): void {
if ( this._selectedClusters = ev.detail.value;
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];
this._clustersToBind = []; this._clustersToBind = [];
for (const clusterIndex of this._selectedClusters) { for (const clusterIndex of this._selectedClusters) {
const selectedCluster = this._clusters.find((cluster) => { 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 "@polymer/paper-spinner/paper-spinner";
import "@material/mwc-button"; import "@material/mwc-button";
import { SelectionChangedEvent } from "../../../components/data-table/ha-data-table"; import { SelectionChangedEvent } from "../../../components/data-table/ha-data-table";
import { HASSDomEvent } from "../../../common/dom/fire_event";
@customElement("zha-group-page") @customElement("zha-group-page")
export class ZHAGroupPage extends LitElement { export class ZHAGroupPage extends LitElement {
@ -145,7 +146,6 @@ export class ZHAGroupPage extends LitElement {
.narrow=${this.narrow} .narrow=${this.narrow}
selectable selectable
@selection-changed=${this._handleRemoveSelectionChanged} @selection-changed=${this._handleRemoveSelectionChanged}
class="table"
> >
</zha-devices-data-table> </zha-devices-data-table>
@ -180,7 +180,6 @@ export class ZHAGroupPage extends LitElement {
.narrow=${this.narrow} .narrow=${this.narrow}
selectable selectable
@selection-changed=${this._handleAddSelectionChanged} @selection-changed=${this._handleAddSelectionChanged}
class="table"
> >
</zha-devices-data-table> </zha-devices-data-table>
@ -223,38 +222,16 @@ export class ZHAGroupPage extends LitElement {
}); });
} }
private _handleAddSelectionChanged(ev: CustomEvent): void { private _handleAddSelectionChanged(
const changedSelection = ev.detail as SelectionChangedEvent; ev: HASSDomEvent<SelectionChangedEvent>
const entity = changedSelection.id; ): void {
if ( this._selectedDevicesToAdd = ev.detail.value;
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 _handleRemoveSelectionChanged(ev: CustomEvent): void { private _handleRemoveSelectionChanged(
const changedSelection = ev.detail as SelectionChangedEvent; ev: HASSDomEvent<SelectionChangedEvent>
const entity = changedSelection.id; ): void {
if ( this._selectedDevicesToRemove = ev.detail.value;
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 async _addMembersToGroup(): Promise<void> { private async _addMembersToGroup(): Promise<void> {
@ -309,11 +286,6 @@ export class ZHAGroupPage extends LitElement {
float: right; float: right;
} }
.table {
height: 200px;
overflow: auto;
}
mwc-button paper-spinner { mwc-button paper-spinner {
width: 14px; width: 14px;
height: 14px; height: 14px;

View File

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

View File

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

View File

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

View File

@ -6,6 +6,7 @@ import {
CSSResult, CSSResult,
css, css,
property, property,
query,
} from "lit-element"; } from "lit-element";
import { classMap } from "lit-html/directives/class-map"; import { classMap } from "lit-html/directives/class-map";
import "@polymer/app-layout/app-header-layout/app-header-layout"; 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() public route?: { path: string; prefix: string };
@property() private _routeData?: { view: string }; @property() private _routeData?: { view: string };
@property() private _curView?: number | "hass-unused-entities"; @property() private _curView?: number | "hass-unused-entities";
@query("ha-app-layout") private _appLayout!: HTMLElement;
private _viewCache?: { [viewId: string]: HUIView }; private _viewCache?: { [viewId: string]: HUIView };
private _debouncedConfigChanged: () => void; private _debouncedConfigChanged: () => void;
@ -344,7 +346,8 @@ class HUIRoot extends LitElement {
} }
</app-header> </app-header>
<div id='view' class="${classMap({ <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> })}" @ll-rebuild='${this._debouncedConfigChanged}'></div>
</app-header-layout> </app-header-layout>
`; `;
@ -468,7 +471,7 @@ class HUIRoot extends LitElement {
if (changedProperties.has("route")) { if (changedProperties.has("route")) {
const views = this.config.views; 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); navigate(this, `${this.route!.prefix}/${views[0].path || 0}`, true);
newSelectView = 0; newSelectView = 0;
} else if (this._routeData!.view === "hass-unused-entities") { } else if (this._routeData!.view === "hass-unused-entities") {
@ -495,8 +498,6 @@ class HUIRoot extends LitElement {
if (!oldLovelace || oldLovelace.config !== this.lovelace!.config) { if (!oldLovelace || oldLovelace.config !== this.lovelace!.config) {
// On config change, recreate the current view from scratch. // On config change, recreate the current view from scratch.
force = true; 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) { if (!oldLovelace || oldLovelace.editMode !== this.lovelace!.editMode) {
@ -506,13 +507,11 @@ class HUIRoot extends LitElement {
this._routeData!.view === "hass-unused-entities" this._routeData!.view === "hass-unused-entities"
) { ) {
const views = this.config && this.config.views; 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; newSelectView = 0;
} }
// On edit mode change, recreate the current view from scratch // On edit mode change, recreate the current view from scratch
force = true; 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); this.lovelace!.setEditMode(true);
if (this.config.views.length < 2) { if (this.config.views.length < 2) {
fireEvent(this, "iron-resize"); this.updateComplete.then(() => fireEvent(this._appLayout, "iron-resize"));
} }
} }
private _editModeDisable(): void { private _editModeDisable(): void {
this.lovelace!.setEditMode(false); this.lovelace!.setEditMode(false);
if (this.config.views.length < 2) { 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": { "picker": {
"headers": { "headers": {
"name": "Name", "name": "Name",
"entity_id": "Entity ID",
"type": "Type", "type": "Type",
"editable": "Editable" "editable": "Editable"
}, },

View File

@ -1515,24 +1515,6 @@
"@material/typography" "^5.0.0" "@material/typography" "^5.0.0"
tslib "^1.9.3" 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": "@material/density@^5.0.0":
version "5.0.0" version "5.0.0"
resolved "https://registry.yarnpkg.com/@material/density/-/density-5.0.0.tgz#643d9bd1a5d89b3985d48fd1d6572f73d05fb2e9" resolved "https://registry.yarnpkg.com/@material/density/-/density-5.0.0.tgz#643d9bd1a5d89b3985d48fd1d6572f73d05fb2e9"