Add toolbars and mobile headers + layout tweaks (#4803)

* Add toolbars and mobile headers + layout tweaks

* Comments
This commit is contained in:
Bram Kragten 2020-02-13 19:53:48 +01:00 committed by GitHub
parent c93e1b0123
commit 7903541689
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 855 additions and 430 deletions

View File

@ -7,14 +7,18 @@ import {
property,
} from "lit-element";
import { fireEvent } from "../dom/fire_event";
import "@polymer/iron-icon/iron-icon";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-icon-button/paper-icon-button";
import "@material/mwc-button";
import "../../components/ha-icon";
import { classMap } from "lit-html/directives/class-map";
@customElement("search-input")
class SearchInput extends LitElement {
@property() public filter?: string;
@property({ type: Boolean, attribute: "no-label-float" })
public noLabelFloat? = false;
@property({ type: Boolean, attribute: "no-underline" })
public noUnderline = false;
public focus() {
this.shadowRoot!.querySelector("paper-input")!.focus();
@ -22,18 +26,24 @@ class SearchInput extends LitElement {
protected render(): TemplateResult {
return html`
<style>
.no-underline {
--paper-input-container-underline: {
display: none;
height: 0;
}
}
</style>
<div class="search-container">
<paper-input
class=${classMap({ "no-underline": this.noUnderline })}
autofocus
label="Search"
.value=${this.filter}
@value-changed=${this._filterInputChanged}
.noLabelFloat=${this.noLabelFloat}
>
<iron-icon
icon="hass:magnify"
slot="prefix"
class="prefix"
></iron-icon>
<ha-icon icon="hass:magnify" slot="prefix" class="prefix"></ha-icon>
${this.filter &&
html`
<paper-icon-button

View File

@ -101,6 +101,8 @@ export class HaDataTable extends BaseElement {
@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;
private _sortColumns: {
[key: string]: DataTableSortColumnData;
} = {};
@ -170,7 +172,7 @@ export class HaDataTable extends BaseElement {
protected render() {
return html`
<div class="mdc-data-table">
<slot name="header">
<slot name="header" @slotchange=${this._calcScrollHeight}>
${this._filterable
? html`
<div class="table-header">
@ -181,112 +183,114 @@ export class HaDataTable extends BaseElement {
`
: ""}
</slot>
<table class="mdc-data-table__table">
<thead>
<tr class="mdc-data-table__header-row">
${this.selectable
? html`
<div class="scroller">
<table class="mdc-data-table__table">
<thead>
<tr class="mdc-data-table__header-row">
${this.selectable
? html`
<th
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}
>
</ha-checkbox>
</th>
`
: ""}
${Object.entries(this.columns).map((columnEntry) => {
const [key, column] = columnEntry;
const sorted = key === this._sortColumn;
const classes = {
"mdc-data-table__header-cell--numeric": Boolean(
column.type && column.type === "numeric"
),
"mdc-data-table__header-cell--icon": Boolean(
column.type && column.type === "icon"
),
sortable: Boolean(column.sortable),
"not-sorted": Boolean(column.sortable && !sorted),
};
return html`
<th
class="mdc-data-table__header-cell mdc-data-table__header-cell--checkbox"
class="mdc-data-table__header-cell ${classMap(classes)}"
role="columnheader"
scope="col"
@click=${this._handleHeaderClick}
data-column-id="${key}"
>
<ha-checkbox
class="mdc-data-table__row-checkbox"
@change=${this._handleHeaderRowCheckboxChange}
.indeterminate=${this._headerIndeterminate}
.checked=${this._headerChecked}
>
</ha-checkbox>
${column.sortable
? html`
<ha-icon
.icon=${sorted && this._sortDirection === "desc"
? "hass:arrow-down"
: "hass:arrow-up"}
></ha-icon>
`
: ""}
<span>${column.title}</span>
</th>
`
: ""}
${Object.entries(this.columns).map((columnEntry) => {
const [key, column] = columnEntry;
const sorted = key === this._sortColumn;
const classes = {
"mdc-data-table__header-cell--numeric": Boolean(
column.type && column.type === "numeric"
),
"mdc-data-table__header-cell--icon": Boolean(
column.type && column.type === "icon"
),
sortable: Boolean(column.sortable),
"not-sorted": Boolean(column.sortable && !sorted),
};
return html`
<th
class="mdc-data-table__header-cell ${classMap(classes)}"
role="columnheader"
scope="col"
@click=${this._handleHeaderClick}
data-column-id="${key}"
`;
})}
</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]}"
@click=${this._handleRowClick}
class="mdc-data-table__row"
>
${column.sortable
${this.selectable
? html`
<ha-icon
.icon=${sorted && this._sortDirection === "desc"
? "hass:arrow-down"
: "hass:arrow-up"}
></ha-icon>
<td
class="mdc-data-table__cell mdc-data-table__cell--checkbox"
>
<ha-checkbox
class="mdc-data-table__row-checkbox"
@change=${this._handleRowCheckboxChange}
.checked=${this._checkedRows.includes(
String(row[this.id])
)}
>
</ha-checkbox>
</td>
`
: ""}
<span>${column.title}</span>
</th>
`;
})}
</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]}"
@click=${this._handleRowClick}
class="mdc-data-table__row"
>
${this.selectable
? html`
${Object.entries(this.columns).map((columnEntry) => {
const [key, column] = columnEntry;
return html`
<td
class="mdc-data-table__cell mdc-data-table__cell--checkbox"
class="mdc-data-table__cell ${classMap({
"mdc-data-table__cell--numeric": Boolean(
column.type && column.type === "numeric"
),
"mdc-data-table__cell--icon": Boolean(
column.type && column.type === "icon"
),
})}"
>
<ha-checkbox
class="mdc-data-table__row-checkbox"
@change=${this._handleRowCheckboxChange}
.checked=${this._checkedRows.includes(
String(row[this.id])
)}
>
</ha-checkbox>
${column.template
? column.template(row[key], row)
: row[key]}
</td>
`
: ""}
${Object.entries(this.columns).map((columnEntry) => {
const [key, column] = columnEntry;
return html`
<td
class="mdc-data-table__cell ${classMap({
"mdc-data-table__cell--numeric": Boolean(
column.type && column.type === "numeric"
),
"mdc-data-table__cell--icon": Boolean(
column.type && column.type === "icon"
),
})}"
>
${column.template
? column.template(row[key], row)
: row[key]}
</td>
`;
})}
</tr>
`
)}
</tbody>
</table>
`;
})}
</tr>
`
)}
</tbody>
</table>
</div>
</div>
`;
}
@ -434,6 +438,11 @@ export class HaDataTable extends BaseElement {
this._debounceSearch(ev.detail.value);
}
private async _calcScrollHeight() {
await this.updateComplete;
this._scroller.style.maxHeight = `calc(100% - ${this._header.clientHeight}px)`;
}
static get styles(): CSSResult {
return css`
/* default mdc styles, colors changed, without checkbox styles */
@ -584,8 +593,14 @@ export class HaDataTable extends BaseElement {
/* custom from here */
:host {
display: block;
}
.mdc-data-table {
display: block;
border-width: var(--data-table-border-width, 1px);
height: 100%;
}
.mdc-data-table__header-cell {
overflow: hidden;
@ -614,6 +629,16 @@ export class HaDataTable extends BaseElement {
.table-header {
border-bottom: 1px solid rgba(var(--rgb-primary-text-color), 0.12);
}
search-input {
position: relative;
top: 2px;
}
.scroller {
overflow: auto;
}
slot[name="header"] {
display: block;
}
`;
}
}

View File

@ -23,6 +23,7 @@ import { fireEvent } from "../../common/dom/fire_event";
import {
DeviceRegistryEntry,
subscribeDeviceRegistry,
DeviceEntityLookup,
} from "../../data/device_registry";
import { compare } from "../../common/string/compare";
import { PolymerChangedEvent } from "../../polymer-types";
@ -30,7 +31,6 @@ import {
AreaRegistryEntry,
subscribeAreaRegistry,
} from "../../data/area_registry";
import { DeviceEntityLookup } from "../../panels/config/devices/ha-devices-data-table";
import {
EntityRegistryEntry,
subscribeEntityRegistry,

View File

@ -22,6 +22,7 @@ import {
DeviceRegistryEntry,
subscribeDeviceRegistry,
computeDeviceName,
DeviceEntityLookup,
} from "../../data/device_registry";
import { compare } from "../../common/string/compare";
import { PolymerChangedEvent } from "../../polymer-types";
@ -29,7 +30,6 @@ import {
AreaRegistryEntry,
subscribeAreaRegistry,
} from "../../data/area_registry";
import { DeviceEntityLookup } from "../../panels/config/devices/ha-devices-data-table";
import {
EntityRegistryEntry,
subscribeEntityRegistry,

View File

@ -17,6 +17,10 @@ export interface DeviceRegistryEntry {
name_by_user?: string;
}
export interface DeviceEntityLookup {
[deviceId: string]: EntityRegistryEntry[];
}
export interface DeviceRegistryEntryMutableParams {
area_id?: string | null;
name_by_user?: string | null;

View File

@ -0,0 +1,157 @@
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
query,
TemplateResult,
} from "lit-element";
import "../components/data-table/ha-data-table";
// tslint:disable-next-line
import {
HaDataTable,
DataTableColumnContainer,
DataTableRowData,
} from "../components/data-table/ha-data-table";
import "./hass-tabs-subpage";
import { HomeAssistant, Route } from "../types";
// tslint:disable-next-line
import { PageNavigation } from "./hass-tabs-subpage";
@customElement("hass-tabs-subpage-data-table")
export class HaTabsSubpageDataTable extends LitElement {
@property() public hass!: HomeAssistant;
@property() public isWide!: boolean;
@property({ type: Boolean, reflect: true }) public narrow!: boolean;
/**
* Object with the columns.
* @type {Object}
*/
@property({ type: Object }) public columns: DataTableColumnContainer = {};
/**
* Data to show in the table.
* @type {Array}
*/
@property({ type: Array }) public data: DataTableRowData[] = [];
/**
* Should rows be selectable.
* @type {Boolean}
*/
@property({ type: Boolean }) public selectable = false;
/**
* Field with a unique id per entry in data.
* @type {String}
*/
@property({ type: String }) public id = "id";
/**
* String to filter the data in the data table on.
* @type {String}
*/
@property({ type: String }) public filter = "";
/**
* What path to use when the back button is pressed.
* @type {String}
* @attr back-path
*/
@property({ type: String, attribute: "back-path" }) public backPath?: string;
/**
* Function to call when the back button is pressed.
* @type {() => void}
*/
@property() public backCallback?: () => void;
@property() public route!: Route;
/**
* Array of tabs to show on the page.
* @type {Array}
*/
@property() public tabs!: PageNavigation[];
@query("ha-data-table") private _dataTable!: HaDataTable;
public clearSelection() {
this._dataTable.clearSelection();
}
protected render(): TemplateResult {
return html`
<hass-tabs-subpage
.hass=${this.hass}
.narrow=${this.narrow}
.backPath=${this.backPath}
.backCallback=${this.backCallback}
.route=${this.route}
.tabs=${this.tabs}
>
${this.narrow
? html`
<div slot="header">
<slot name="header">
<div class="search-toolbar">
<search-input
no-label-float
no-underline
@value-changed=${this._handleSearchChange}
></search-input>
</div>
</slot>
</div>
`
: ""}
<ha-data-table
.columns=${this.columns}
.data=${this.data}
.filter=${this.filter}
.selectable=${this.selectable}
.id=${this.id}
>
${!this.narrow
? html`
<div slot="header">
<slot name="header">
<slot name="header">
<div class="table-header">
<search-input
no-label-float
no-underline
@value-changed=${this._handleSearchChange}
></search-input></div></slot
></slot>
</div>
`
: html`
<div slot="header"></div>
`}
</ha-data-table>
</hass-tabs-subpage>
`;
}
private _handleSearchChange(ev: CustomEvent) {
this.filter = ev.detail.value;
}
static get styles(): CSSResult {
return css`
ha-data-table {
width: 100%;
--data-table-border-width: 0;
}
:host(:not([narrow])) ha-data-table {
height: calc(100vh - 65px);
display: block;
}
.table-header {
border-bottom: 1px solid rgba(var(--rgb-primary-text-color), 0.12);
}
.search-toolbar {
margin-left: -24px;
color: var(--secondary-text-color);
}
search-input {
position: relative;
top: 2px;
}
`;
}
}

View File

@ -56,6 +56,11 @@ class HassTabsSubpage extends LitElement {
.hassio=${this.hassio}
@click=${this._backTapped}
></ha-paper-icon-button-arrow-prev>
${this.narrow
? html`
<div main-title><slot name="header"></slot></div>
`
: ""}
<div id="tabbar" class=${classMap({ "bottom-bar": this.narrow })}>
${this.tabs.map((page, index) =>
(!page.component ||
@ -138,11 +143,6 @@ class HassTabsSubpage extends LitElement {
box-sizing: border-box;
}
:host([narrow]) .toolbar {
background-color: var(--primary-background-color);
border-bottom: none;
}
#tabbar {
display: flex;
font-size: 14px;

View File

@ -77,154 +77,155 @@ export class HaAutomationEditor extends LitElement {
<div class="errors">${this._errors}</div>
`
: ""}
<div
class="content ${classMap({
rtl: computeRTL(this.hass),
})}"
>
${this._config
? html`
<ha-config-section .isWide=${this.isWide}>
<span slot="header">${this._config.alias}</span>
<span slot="introduction">
${this.hass.localize(
"ui.panel.config.automation.editor.introduction"
)}
</span>
<ha-card>
<div class="card-content">
<paper-input
.label=${this.hass.localize(
"ui.panel.config.automation.editor.alias"
)}
name="alias"
.value=${this._config.alias}
@value-changed=${this._valueChanged}
>
</paper-input>
<ha-textarea
.label=${this.hass.localize(
"ui.panel.config.automation.editor.description.label"
)}
.placeholder=${this.hass.localize(
"ui.panel.config.automation.editor.description.placeholder"
)}
name="description"
.value=${this._config.description}
@value-changed=${this._valueChanged}
></ha-textarea>
</div>
${this.creatingNew
? ""
: html`
<div
class="card-actions layout horizontal justified center"
>
<div class="layout horizontal center">
<ha-entity-toggle
.hass=${this.hass}
.stateObj=${this.automation}
></ha-entity-toggle>
${this.hass.localize(
"ui.panel.config.automation.editor.enable_disable"
)}
</div>
<mwc-button @click=${this._excuteAutomation}>
${this.hass.localize(
"ui.card.automation.trigger"
)}
</mwc-button>
${this._config
? html`
${this.narrow
? html`
<span slot="header">${this._config?.alias}</span>
`
: ""}
<ha-config-section .isWide=${this.isWide}>
${!this.narrow
? html`
<span slot="header">${this._config.alias}</span>
`
: ""}
<span slot="introduction">
${this.hass.localize(
"ui.panel.config.automation.editor.introduction"
)}
</span>
<ha-card>
<div class="card-content">
<paper-input
.label=${this.hass.localize(
"ui.panel.config.automation.editor.alias"
)}
name="alias"
.value=${this._config.alias}
@value-changed=${this._valueChanged}
>
</paper-input>
<ha-textarea
.label=${this.hass.localize(
"ui.panel.config.automation.editor.description.label"
)}
.placeholder=${this.hass.localize(
"ui.panel.config.automation.editor.description.placeholder"
)}
name="description"
.value=${this._config.description}
@value-changed=${this._valueChanged}
></ha-textarea>
</div>
${this.creatingNew
? ""
: html`
<div
class="card-actions layout horizontal justified center"
>
<div class="layout horizontal center">
<ha-entity-toggle
.hass=${this.hass}
.stateObj=${this.automation}
></ha-entity-toggle>
${this.hass.localize(
"ui.panel.config.automation.editor.enable_disable"
)}
</div>
`}
</ha-card>
</ha-config-section>
<mwc-button @click=${this._excuteAutomation}>
${this.hass.localize("ui.card.automation.trigger")}
</mwc-button>
</div>
`}
</ha-card>
</ha-config-section>
<ha-config-section .isWide=${this.isWide}>
<span slot="header">
<ha-config-section .isWide=${this.isWide}>
<span slot="header">
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.header"
)}
</span>
<span slot="introduction">
<p>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.header"
"ui.panel.config.automation.editor.triggers.introduction"
)}
</span>
<span slot="introduction">
<p>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.introduction"
)}
</p>
<a
href="https://home-assistant.io/docs/automation/trigger/"
target="_blank"
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.learn_more"
)}
</a>
</span>
<ha-automation-trigger
.triggers=${this._config.trigger}
@value-changed=${this._triggerChanged}
.hass=${this.hass}
></ha-automation-trigger>
</ha-config-section>
</p>
<a
href="https://home-assistant.io/docs/automation/trigger/"
target="_blank"
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.learn_more"
)}
</a>
</span>
<ha-automation-trigger
.triggers=${this._config.trigger}
@value-changed=${this._triggerChanged}
.hass=${this.hass}
></ha-automation-trigger>
</ha-config-section>
<ha-config-section .isWide=${this.isWide}>
<span slot="header">
<ha-config-section .isWide=${this.isWide}>
<span slot="header">
${this.hass.localize(
"ui.panel.config.automation.editor.conditions.header"
)}
</span>
<span slot="introduction">
<p>
${this.hass.localize(
"ui.panel.config.automation.editor.conditions.header"
"ui.panel.config.automation.editor.conditions.introduction"
)}
</span>
<span slot="introduction">
<p>
${this.hass.localize(
"ui.panel.config.automation.editor.conditions.introduction"
)}
</p>
<a
href="https://home-assistant.io/docs/scripts/conditions/"
target="_blank"
>
${this.hass.localize(
"ui.panel.config.automation.editor.conditions.learn_more"
)}
</a>
</span>
<ha-automation-condition
.conditions=${this._config.condition || []}
@value-changed=${this._conditionChanged}
.hass=${this.hass}
></ha-automation-condition>
</ha-config-section>
</p>
<a
href="https://home-assistant.io/docs/scripts/conditions/"
target="_blank"
>
${this.hass.localize(
"ui.panel.config.automation.editor.conditions.learn_more"
)}
</a>
</span>
<ha-automation-condition
.conditions=${this._config.condition || []}
@value-changed=${this._conditionChanged}
.hass=${this.hass}
></ha-automation-condition>
</ha-config-section>
<ha-config-section .isWide=${this.isWide}>
<span slot="header">
<ha-config-section .isWide=${this.isWide}>
<span slot="header">
${this.hass.localize(
"ui.panel.config.automation.editor.actions.header"
)}
</span>
<span slot="introduction">
<p>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.header"
"ui.panel.config.automation.editor.actions.introduction"
)}
</span>
<span slot="introduction">
<p>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.introduction"
)}
</p>
<a
href="https://home-assistant.io/docs/automation/action/"
target="_blank"
>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.learn_more"
)}
</a>
</span>
<ha-automation-action
.actions=${this._config.action}
@value-changed=${this._actionChanged}
.hass=${this.hass}
></ha-automation-action>
</ha-config-section>
`
: ""}
</div>
</p>
<a
href="https://home-assistant.io/docs/automation/action/"
target="_blank"
>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.learn_more"
)}
</a>
</span>
<ha-automation-action
.actions=${this._config.action}
@value-changed=${this._actionChanged}
.hass=${this.hass}
></ha-automation-action>
</ha-config-section>
`
: ""}
<ha-fab
?is-wide="${this.isWide}"
?narrow="${this.narrow}"

View File

@ -121,6 +121,14 @@ export class HaConfigDevicePage extends LitElement {
.tabs=${configSections.integrations}
.route=${this.route}
>
${
this.narrow
? html`
<span slot="header">${device.name_by_user || device.name}</span>
`
: ""
}
<paper-icon-button
slot="toolbar-icon"
icon="hass:settings"
@ -130,7 +138,13 @@ export class HaConfigDevicePage extends LitElement {
<div class="container">
<div class="left">
<div class="device-info">
<h1>${device.name_by_user || device.name}</h1>
${
this.narrow
? ""
: html`
<h1>${device.name_by_user || device.name}</h1>
`
}
<ha-device-card
.hass=${this.hass}
.areas=${this.areas}
@ -498,6 +512,10 @@ export class HaConfigDevicePage extends LitElement {
width: 100%;
}
:host([narrow]) .container > *:first-child {
padding-top: 0;
}
:host([narrow]) .container {
margin-top: 0;
}

View File

@ -1,5 +1,4 @@
import "../../../layouts/hass-tabs-subpage";
import "./ha-devices-data-table";
import "../../../layouts/hass-tabs-subpage-data-table";
import {
LitElement,
@ -11,11 +10,24 @@ import {
css,
} from "lit-element";
import { HomeAssistant, Route } from "../../../types";
import { DeviceRegistryEntry } from "../../../data/device_registry";
import {
DeviceRegistryEntry,
computeDeviceName,
DeviceEntityLookup,
} from "../../../data/device_registry";
import { EntityRegistryEntry } from "../../../data/entity_registry";
import { ConfigEntry } from "../../../data/config_entries";
import { AreaRegistryEntry } from "../../../data/area_registry";
import { configSections } from "../ha-panel-config";
import memoizeOne from "memoize-one";
import { LocalizeFunc } from "../../../common/translations/localize";
import { DeviceRowData } from "./ha-devices-data-table";
import {
DataTableColumnContainer,
DataTableRowData,
RowClickedEvent,
} from "../../../components/data-table/ha-data-table";
import { navigate } from "../../../common/navigate";
@customElement("ha-config-devices-dashboard")
export class HaConfigDeviceDashboard extends LitElement {
@ -29,30 +41,219 @@ export class HaConfigDeviceDashboard extends LitElement {
@property() public domain!: string;
@property() public route!: Route;
private _devices = memoizeOne(
(
devices: DeviceRegistryEntry[],
entries: ConfigEntry[],
entities: EntityRegistryEntry[],
areas: AreaRegistryEntry[],
domain: string,
localize: LocalizeFunc
) => {
// Some older installations might have devices pointing at invalid entryIDs
// So we guard for that.
let outputDevices: DeviceRowData[] = devices;
const deviceLookup: { [deviceId: string]: DeviceRegistryEntry } = {};
for (const device of devices) {
deviceLookup[device.id] = device;
}
const deviceEntityLookup: DeviceEntityLookup = {};
for (const entity of entities) {
if (!entity.device_id) {
continue;
}
if (!(entity.device_id in deviceEntityLookup)) {
deviceEntityLookup[entity.device_id] = [];
}
deviceEntityLookup[entity.device_id].push(entity);
}
const entryLookup: { [entryId: string]: ConfigEntry } = {};
for (const entry of entries) {
entryLookup[entry.entry_id] = entry;
}
const areaLookup: { [areaId: string]: AreaRegistryEntry } = {};
for (const area of areas) {
areaLookup[area.area_id] = area;
}
if (domain) {
outputDevices = outputDevices.filter((device) =>
device.config_entries.find(
(entryId) =>
entryId in entryLookup && entryLookup[entryId].domain === domain
)
);
}
outputDevices = outputDevices.map((device) => {
return {
...device,
name: computeDeviceName(
device,
this.hass,
deviceEntityLookup[device.id]
),
model: device.model || "<unknown>",
manufacturer: device.manufacturer || "<unknown>",
area: device.area_id ? areaLookup[device.area_id].name : "No area",
integration: device.config_entries.length
? device.config_entries
.filter((entId) => entId in entryLookup)
.map(
(entId) =>
localize(
`component.${entryLookup[entId].domain}.config.title`
) || entryLookup[entId].domain
)
.join(", ")
: "No integration",
battery_entity: this._batteryEntity(device.id, deviceEntityLookup),
};
});
return outputDevices;
}
);
private _columns = memoizeOne(
(narrow: boolean): DataTableColumnContainer =>
narrow
? {
name: {
title: "Device",
sortable: true,
filterable: true,
direction: "asc",
template: (name, device: DataTableRowData) => {
const battery = device.battery_entity
? this.hass.states[device.battery_entity]
: undefined;
// Have to work on a nice layout for mobile
return html`
${name}<br />
${device.area} | ${device.integration}<br />
${battery && !isNaN(battery.state as any)
? html`
${battery.state}%
<ha-state-icon
.hass=${this.hass!}
.stateObj=${battery}
></ha-state-icon>
`
: ""}
`;
},
},
}
: {
name: {
title: this.hass.localize(
"ui.panel.config.devices.data_table.device"
),
sortable: true,
filterable: true,
direction: "asc",
},
manufacturer: {
title: this.hass.localize(
"ui.panel.config.devices.data_table.manufacturer"
),
sortable: true,
filterable: true,
},
model: {
title: this.hass.localize(
"ui.panel.config.devices.data_table.model"
),
sortable: true,
filterable: true,
},
area: {
title: this.hass.localize(
"ui.panel.config.devices.data_table.area"
),
sortable: true,
filterable: true,
},
integration: {
title: this.hass.localize(
"ui.panel.config.devices.data_table.integration"
),
sortable: true,
filterable: true,
},
battery_entity: {
title: this.hass.localize(
"ui.panel.config.devices.data_table.battery"
),
sortable: true,
type: "numeric",
template: (batteryEntity: string) => {
const battery = batteryEntity
? this.hass.states[batteryEntity]
: undefined;
return battery && !isNaN(battery.state as any)
? html`
${battery.state}%
<ha-state-icon
.hass=${this.hass!}
.stateObj=${battery}
></ha-state-icon>
`
: html`
-
`;
},
},
}
);
protected render(): TemplateResult {
return html`
<hass-tabs-subpage
<hass-tabs-subpage-data-table
.hass=${this.hass}
.narrow=${this.narrow}
back-path="/config"
.tabs=${configSections.integrations}
.route=${this.route}
.columns=${this._columns(this.narrow)}
.data=${this._devices(
this.devices,
this.entries,
this.entities,
this.areas,
this.domain,
this.hass.localize
)}
@row-click=${this._handleRowClicked}
>
<div class="content">
<ha-devices-data-table
.hass=${this.hass}
.narrow=${this.narrow}
.devices=${this.devices}
.entries=${this.entries}
.entities=${this.entities}
.areas=${this.areas}
.domain=${this.domain}
></ha-devices-data-table>
</div>
</hass-tabs-subpage>
</hass-tabs-subpage-data-table>
`;
}
private _batteryEntity(
deviceId: string,
deviceEntityLookup: DeviceEntityLookup
): string | undefined {
const batteryEntity = (deviceEntityLookup[deviceId] || []).find(
(entity) =>
this.hass.states[entity.entity_id] &&
this.hass.states[entity.entity_id].attributes.device_class === "battery"
);
return batteryEntity ? batteryEntity.entity_id : undefined;
}
private _handleRowClicked(ev: CustomEvent) {
const deviceId = (ev.detail as RowClickedEvent).id;
navigate(this, `/config/devices/device/${deviceId}`);
}
static get styles(): CSSResult {
return css`
.content {

View File

@ -21,6 +21,7 @@ import {
import {
DeviceRegistryEntry,
computeDeviceName,
DeviceEntityLookup,
} from "../../../data/device_registry";
import { EntityRegistryEntry } from "../../../data/entity_registry";
import { ConfigEntry } from "../../../data/config_entries";
@ -35,10 +36,6 @@ export interface DeviceRowData extends DeviceRegistryEntry {
battery_entity?: string;
}
export interface DeviceEntityLookup {
[deviceId: string]: EntityRegistryEntry[];
}
@customElement("ha-devices-data-table")
export class HaDevicesDataTable extends LitElement {
@property() public hass!: HomeAssistant;

View File

@ -19,8 +19,6 @@ import memoize from "memoize-one";
import { computeDomain } from "../../../common/entity/compute_domain";
import { domainIcon } from "../../../common/entity/domain_icon";
import { stateIcon } from "../../../common/entity/state_icon";
import "../../../components/data-table/ha-data-table";
// tslint:disable-next-line
import {
DataTableColumnContainer,
DataTableColumnData,
@ -29,6 +27,7 @@ import {
SelectionChangedEvent,
} from "../../../components/data-table/ha-data-table";
import "../../../components/ha-icon";
import "../../../common/search/search-input";
import {
computeEntityRegistryName,
EntityRegistryEntry,
@ -38,7 +37,7 @@ import {
} from "../../../data/entity_registry";
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
import "../../../layouts/hass-loading-screen";
import "../../../layouts/hass-tabs-subpage";
import "../../../layouts/hass-tabs-subpage-data-table";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { HomeAssistant, Route } from "../../../types";
import { DialogEntityRegistryDetail } from "./dialog-entity-registry-detail";
@ -47,6 +46,7 @@ import {
showEntityRegistryDetailDialog,
} from "./show-dialog-entity-registry-detail";
import { configSections } from "../ha-panel-config";
import { classMap } from "lit-html/directives/class-map";
@customElement("ha-config-entities")
export class HaConfigEntities extends SubscribeMixin(LitElement) {
@ -223,154 +223,136 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
<hass-loading-screen></hass-loading-screen>
`;
}
const headerToolbar = this._selectedEntities.length
? html`
<p class="selected-txt">
${this.hass.localize(
"ui.panel.config.entities.picker.selected",
"number",
this._selectedEntities.length
)}
</p>
<div class="header-btns">
${!this.narrow
? html`
<mwc-button @click=${this._enableSelected}
>${this.hass.localize(
"ui.panel.config.entities.picker.enable_selected.button"
)}</mwc-button
>
<mwc-button @click=${this._disableSelected}
>${this.hass.localize(
"ui.panel.config.entities.picker.disable_selected.button"
)}</mwc-button
>
<mwc-button @click=${this._removeSelected}
>${this.hass.localize(
"ui.panel.config.entities.picker.remove_selected.button"
)}</mwc-button
>
`
: html`
<paper-icon-button
id="enable-btn"
icon="hass:undo"
@click=${this._enableSelected}
></paper-icon-button>
<paper-tooltip for="enable-btn">
${this.hass.localize(
"ui.panel.config.entities.picker.enable_selected.button"
)}
</paper-tooltip>
<paper-icon-button
id="disable-btn"
icon="hass:cancel"
@click=${this._disableSelected}
></paper-icon-button>
<paper-tooltip for="disable-btn">
${this.hass.localize(
"ui.panel.config.entities.picker.disable_selected.button"
)}
</paper-tooltip>
<paper-icon-button
id="remove-btn"
icon="hass:delete"
@click=${this._removeSelected}
></paper-icon-button>
<paper-tooltip for="remove-btn">
${this.hass.localize(
"ui.panel.config.entities.picker.remove_selected.button"
)}
</paper-tooltip>
`}
</div>
`
: html`
<search-input
no-label-float
no-underline
@value-changed=${this._handleSearchChange}
.filter=${this._filter}
></search-input>
<paper-menu-button no-animations horizontal-align="right">
<paper-icon-button
aria-label=${this.hass!.localize(
"ui.panel.config.entities.picker.filter.filter"
)}
title="${this.hass!.localize(
"ui.panel.config.entities.picker.filter.filter"
)}"
icon="hass:filter-variant"
slot="dropdown-trigger"
></paper-icon-button>
<paper-listbox slot="dropdown-content">
<paper-icon-item @tap="${this._showDisabledChanged}">
<paper-checkbox
.checked=${this._showDisabled}
slot="item-icon"
></paper-checkbox>
${this.hass!.localize(
"ui.panel.config.entities.picker.filter.show_disabled"
)}
</paper-icon-item>
<paper-icon-item @tap="${this._showRestoredChanged}">
<paper-checkbox
.checked=${this._showUnavailable}
slot="item-icon"
></paper-checkbox>
${this.hass!.localize(
"ui.panel.config.entities.picker.filter.show_unavailable"
)}
</paper-icon-item>
</paper-listbox>
</paper-menu-button>
`;
return html`
<hass-tabs-subpage
<hass-tabs-subpage-data-table
.hass=${this.hass}
.narrow=${this.narrow}
back-path="/config"
.route=${this.route}
.tabs=${configSections.integrations}
.columns=${this._columns(this.narrow, this.hass.language)}
.data=${this._filteredEntities(
this._entities,
this._showDisabled,
this._showUnavailable
)}
.filter=${this._filter}
selectable
@selection-changed=${this._handleSelectionChanged}
@row-click=${this._openEditEntry}
id="entity_id"
>
<div class="content">
<div class="intro">
<h2>
${this.hass.localize("ui.panel.config.entities.picker.header")}
</h2>
<p>
${this.hass.localize(
"ui.panel.config.entities.picker.introduction"
)}
</p>
<p>
${this.hass.localize(
"ui.panel.config.entities.picker.introduction2"
)}
</p>
<a href="/config/integrations">
${this.hass.localize(
"ui.panel.config.entities.picker.integrations_page"
)}
</a>
</div>
<ha-data-table
.columns=${this._columns(this.narrow, this.hass.language)}
.data=${this._filteredEntities(
this._entities,
this._showDisabled,
this._showUnavailable
)}
.filter=${this._filter}
selectable
@selection-changed=${this._handleSelectionChanged}
@row-click=${this._openEditEntry}
id="entity_id"
>
<div class="table-header" slot="header">
${this._selectedEntities.length
? html`
<p class="selected-txt">
${this.hass.localize(
"ui.panel.config.entities.picker.selected",
"number",
this._selectedEntities.length
)}
</p>
<div class="header-btns">
${!this.narrow
? html`
<mwc-button @click=${this._enableSelected}
>${this.hass.localize(
"ui.panel.config.entities.picker.enable_selected.button"
)}</mwc-button
>
<mwc-button @click=${this._disableSelected}
>${this.hass.localize(
"ui.panel.config.entities.picker.disable_selected.button"
)}</mwc-button
>
<mwc-button @click=${this._removeSelected}
>${this.hass.localize(
"ui.panel.config.entities.picker.remove_selected.button"
)}</mwc-button
>
`
: html`
<paper-icon-button
id="enable-btn"
icon="hass:undo"
@click=${this._enableSelected}
></paper-icon-button>
<paper-tooltip for="enable-btn">
${this.hass.localize(
"ui.panel.config.entities.picker.enable_selected.button"
)}
</paper-tooltip>
<paper-icon-button
id="disable-btn"
icon="hass:cancel"
@click=${this._disableSelected}
></paper-icon-button>
<paper-tooltip for="disable-btn">
${this.hass.localize(
"ui.panel.config.entities.picker.disable_selected.button"
)}
</paper-tooltip>
<paper-icon-button
id="remove-btn"
icon="hass:delete"
@click=${this._removeSelected}
></paper-icon-button>
<paper-tooltip for="remove-btn">
${this.hass.localize(
"ui.panel.config.entities.picker.remove_selected.button"
)}
</paper-tooltip>
`}
</div>
`
: html`
<search-input
@value-changed=${this._handleSearchChange}
.filter=${this._filter}
></search-input>
<paper-menu-button no-animations horizontal-align="right">
<paper-icon-button
aria-label=${this.hass!.localize(
"ui.panel.config.entities.picker.filter.filter"
)}
title="${this.hass!.localize(
"ui.panel.config.entities.picker.filter.filter"
)}"
icon="hass:filter-variant"
slot="dropdown-trigger"
></paper-icon-button>
<paper-listbox slot="dropdown-content">
<paper-icon-item @tap="${this._showDisabledChanged}">
<paper-checkbox
.checked=${this._showDisabled}
slot="item-icon"
></paper-checkbox>
${this.hass!.localize(
"ui.panel.config.entities.picker.filter.show_disabled"
)}
</paper-icon-item>
<paper-icon-item @tap="${this._showRestoredChanged}">
<paper-checkbox
.checked=${this._showUnavailable}
slot="item-icon"
></paper-checkbox>
${this.hass!.localize(
"ui.panel.config.entities.picker.filter.show_unavailable"
)}
</paper-icon-item>
</paper-listbox>
</paper-menu-button>
`}
</div>
</ha-data-table>
</div>
</hass-tabs-subpage>
<div class=${classMap({
"search-toolbar": this.narrow,
"table-header": !this.narrow,
})} slot="header">
${headerToolbar}
</div>
</ha-data-table>
</hass-tabs-subpage-data-table>
`;
}
@ -520,14 +502,13 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
font-weight: var(--paper-font-subhead_-_font-weight);
line-height: var(--paper-font-subhead_-_line-height);
}
.intro {
padding: 24px 16px;
}
.content {
padding: 4px;
}
ha-data-table {
width: 100%;
--data-table-border-width: 0;
}
:host(:not([narrow])) ha-data-table {
height: calc(100vh - 65px);
display: block;
}
ha-switch {
margin-top: 16px;
@ -540,12 +521,26 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
}
search-input {
flex-grow: 1;
position: relative;
top: 2px;
}
.search-toolbar {
display: flex;
justify-content: space-between;
align-items: flex-end;
margin-left: -24px;
color: var(--secondary-text-color);
}
.selected-txt {
font-weight: bold;
margin-top: 38px;
padding-left: 16px;
}
.table-header .selected-txt {
margin-top: 20px;
}
.search-toolbar .selected-txt {
font-size: 16px;
}
.header-btns > mwc-button,
.header-btns > paper-icon-button {
margin: 8px;

View File

@ -160,6 +160,10 @@ export class HaSceneEditor extends SubscribeMixin(LitElement) {
this._deviceEntityLookup,
this._deviceRegistryEntries
);
const name = this.scene
? computeStateName(this.scene)
: this.hass.localize("ui.panel.config.scene.editor.default_name");
return html`
<hass-tabs-subpage
.hass=${this.hass}
@ -191,6 +195,13 @@ export class HaSceneEditor extends SubscribeMixin(LitElement) {
`
: ""
}
${
this.narrow
? html`
<span slot="header">${name}</span>
`
: ""
}
<div
id="root"
class="${classMap({
@ -198,15 +209,13 @@ export class HaSceneEditor extends SubscribeMixin(LitElement) {
})}"
>
<ha-config-section .isWide=${this.isWide}>
<div slot="header">
${
this.scene
? computeStateName(this.scene)
: this.hass.localize(
"ui.panel.config.scene.editor.default_name"
)
}
</div>
${
!this.narrow
? html`
<span slot="header">${name}</span>
`
: ""
}
<div slot="introduction">
${this.hass.localize(
"ui.panel.config.scene.editor.introduction"

View File

@ -63,7 +63,11 @@ export class HaScriptEditor extends LitElement {
@click=${this._deleteConfirm}
></paper-icon-button>
`}
${this.narrow
? html`
<span slot="header">${this._config?.alias}</span>
`
: ""}
<div class="content">
${this._errors
? html`
@ -78,7 +82,11 @@ export class HaScriptEditor extends LitElement {
${this._config
? html`
<ha-config-section .isWide=${this.isWide}>
<span slot="header">${this._config.alias}</span>
${!this.narrow
? html`
<span slot="header">${this._config.alias}</span>
`
: ""}
<span slot="introduction">
${this.hass.localize(
"ui.panel.config.script.editor.introduction"