mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-19 15:26:36 +00:00
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:
parent
1599dc9e16
commit
5a2649a65b
@ -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",
|
||||
|
@ -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;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@ -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}`);
|
||||
}
|
||||
}
|
||||
|
@ -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]
|
||||
|
@ -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() {
|
||||
|
@ -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}
|
||||
>
|
||||
|
@ -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%;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@ -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`
|
||||
|
@ -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(
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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>
|
||||
`;
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
`;
|
||||
}
|
||||
|
@ -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) => {
|
||||
|
@ -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;
|
||||
|
@ -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> {
|
||||
|
@ -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>
|
||||
`;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -811,6 +811,7 @@
|
||||
"picker": {
|
||||
"headers": {
|
||||
"name": "Name",
|
||||
"entity_id": "Entity ID",
|
||||
"type": "Type",
|
||||
"editable": "Editable"
|
||||
},
|
||||
|
18
yarn.lock
18
yarn.lock
@ -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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user