diff --git a/src/components/data-table/ha-data-table.ts b/src/components/data-table/ha-data-table.ts index 6fef89eb4a..f68793bb04 100644 --- a/src/components/data-table/ha-data-table.ts +++ b/src/components/data-table/ha-data-table.ts @@ -72,6 +72,7 @@ export interface DataTableColumnData extends DataTableSortColumnData { type?: "numeric" | "icon"; template?: (data: any, row: T) => TemplateResult | string; width?: string; + maxWidth?: string; grows?: boolean; } @@ -241,9 +242,8 @@ export class HaDataTable extends LitElement { class="mdc-data-table__header-cell ${classMap(classes)}" style=${column.width ? styleMap({ - [column.grows ? "minWidth" : "width"]: String( - column.width - ), + [column.grows ? "minWidth" : "width"]: column.width, + maxWidth: column.maxWidth || "", }) : ""} role="columnheader" @@ -329,7 +329,10 @@ export class HaDataTable extends LitElement { ? styleMap({ [column.grows ? "minWidth" - : "width"]: String(column.width), + : "width"]: column.width, + maxWidth: column.maxWidth + ? column.maxWidth + : "", }) : ""} > @@ -532,6 +535,7 @@ export class HaDataTable extends LitElement { overflow: hidden; text-overflow: ellipsis; flex-shrink: 0; + box-sizing: border-box; } .mdc-data-table__cell.mdc-data-table__cell--icon { @@ -544,7 +548,7 @@ export class HaDataTable extends LitElement { padding-left: 16px; /* @noflip */ padding-right: 0; - width: 40px; + width: 56px; } [dir="rtl"] .mdc-data-table__header-cell--checkbox, .mdc-data-table__header-cell--checkbox[dir="rtl"], @@ -591,7 +595,7 @@ export class HaDataTable extends LitElement { .mdc-data-table__header-cell--icon, .mdc-data-table__cell--icon { - width: 24px; + width: 54px; } .mdc-data-table__header-cell.mdc-data-table__header-cell--icon { @@ -695,6 +699,9 @@ export class HaDataTable extends LitElement { .center { text-align: center; } + .secondary { + color: var(--secondary-text-color); + } .scroller { display: flex; position: relative; diff --git a/src/components/ha-sidebar.ts b/src/components/ha-sidebar.ts index 321a703bda..dd27336fa8 100644 --- a/src/components/ha-sidebar.ts +++ b/src/components/ha-sidebar.ts @@ -530,6 +530,7 @@ class HaSidebar extends LitElement { overflow-x: hidden; scrollbar-color: var(--scrollbar-thumb-color) transparent; scrollbar-width: thin; + background: none; } a { diff --git a/src/data/entity_registry.ts b/src/data/entity_registry.ts index c4b94aab21..18a1512b41 100644 --- a/src/data/entity_registry.ts +++ b/src/data/entity_registry.ts @@ -27,6 +27,16 @@ export interface EntityRegistryEntryUpdateParams { new_entity_id?: string; } +export const findBatteryEntity = ( + hass: HomeAssistant, + entities: EntityRegistryEntry[] +): EntityRegistryEntry | undefined => + entities.find( + (entity) => + hass.states[entity.entity_id] && + hass.states[entity.entity_id].attributes.device_class === "battery" + ); + export const computeEntityRegistryName = ( hass: HomeAssistant, entry: EntityRegistryEntry diff --git a/src/panels/config/devices/device-detail/ha-device-card.ts b/src/panels/config/devices/device-detail/ha-device-card.ts deleted file mode 100644 index 141e3389d2..0000000000 --- a/src/panels/config/devices/device-detail/ha-device-card.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { - DeviceRegistryEntry, - computeDeviceName, -} from "../../../../data/device_registry"; -import { loadDeviceRegistryDetailDialog } from "../../../../dialogs/device-registry-detail/show-dialog-device-registry-detail"; -import { - LitElement, - html, - customElement, - property, - TemplateResult, - CSSResult, - css, -} from "lit-element"; -import { HomeAssistant } from "../../../../types"; -import { AreaRegistryEntry } from "../../../../data/area_registry"; - -@customElement("ha-device-card") -export class HaDeviceCard extends LitElement { - @property() public hass!: HomeAssistant; - @property() public device!: DeviceRegistryEntry; - @property() public devices!: DeviceRegistryEntry[]; - @property() public areas!: AreaRegistryEntry[]; - @property() public narrow!: boolean; - - protected render(): TemplateResult { - return html` -
- ${this.device.model - ? html` -
${this.device.model}
- ` - : ""} - ${this.device.manufacturer - ? html` -
- ${this.hass.localize( - "ui.panel.config.integrations.config_entry.manuf", - "manufacturer", - this.device.manufacturer - )} -
- ` - : ""} - ${this.device.area_id - ? html` -
-
- ${this.hass.localize( - "ui.panel.config.integrations.config_entry.area", - "area", - this._computeArea(this.areas, this.device) - )} -
-
- ` - : ""} - ${this.device.via_device_id - ? html` -
- ${this.hass.localize( - "ui.panel.config.integrations.config_entry.via" - )} - ${this._computeDeviceName( - this.devices, - this.device.via_device_id - )} -
- ` - : ""} - ${this.device.sw_version - ? html` -
- ${this.hass.localize( - "ui.panel.config.integrations.config_entry.firmware", - "version", - this.device.sw_version - )} -
- ` - : ""} -
- `; - } - - protected firstUpdated(changedProps) { - super.firstUpdated(changedProps); - loadDeviceRegistryDetailDialog(); - } - - private _computeArea(areas, device) { - if (!areas || !device || !device.area_id) { - return "No Area"; - } - // +1 because of "No Area" entry - return areas.find((area) => area.area_id === device.area_id).name; - } - - private _computeDeviceName(devices, deviceId) { - const device = devices.find((dev) => dev.id === deviceId); - return device - ? computeDeviceName(device, this.hass) - : `(${this.hass.localize( - "ui.panel.config.integrations.config_entry.device_unavailable" - )})`; - } - - static get styles(): CSSResult { - return css` - ha-card { - flex: 1 0 100%; - padding-bottom: 10px; - min-width: 0; - } - .device { - width: 30%; - } - .area { - color: var(--primary-text-color); - } - .extra-info { - margin-top: 8px; - } - .manuf, - .entity-id, - .model { - color: var(--secondary-text-color); - } - `; - } -} diff --git a/src/panels/config/devices/device-detail/ha-device-entities-card.ts b/src/panels/config/devices/device-detail/ha-device-entities-card.ts index 327e3b5ffe..dcf006bf08 100644 --- a/src/panels/config/devices/device-detail/ha-device-entities-card.ts +++ b/src/panels/config/devices/device-detail/ha-device-entities-card.ts @@ -166,6 +166,9 @@ export class HaDeviceEntitiesCard extends LitElement { static get styles(): CSSResult { return css` + :host { + display: block; + } ha-icon { width: 40px; } @@ -182,6 +185,9 @@ export class HaDeviceEntitiesCard extends LitElement { #entities > * { margin: 8px 16px 8px 8px; } + #entities > paper-icon-item { + margin: 0; + } paper-icon-item { min-height: 40px; padding: 0 8px; diff --git a/src/panels/config/devices/device-detail/ha-device-info-card.ts b/src/panels/config/devices/device-detail/ha-device-info-card.ts new file mode 100644 index 0000000000..7ad3e79ea4 --- /dev/null +++ b/src/panels/config/devices/device-detail/ha-device-info-card.ts @@ -0,0 +1,118 @@ +import { + DeviceRegistryEntry, + computeDeviceName, +} from "../../../../data/device_registry"; +import { loadDeviceRegistryDetailDialog } from "../../../../dialogs/device-registry-detail/show-dialog-device-registry-detail"; +import { + LitElement, + html, + customElement, + property, + TemplateResult, + CSSResult, + css, +} from "lit-element"; +import { HomeAssistant } from "../../../../types"; +import { AreaRegistryEntry } from "../../../../data/area_registry"; + +@customElement("ha-device-info-card") +export class HaDeviceCard extends LitElement { + @property() public hass!: HomeAssistant; + @property() public device!: DeviceRegistryEntry; + @property() public devices!: DeviceRegistryEntry[]; + @property() public areas!: AreaRegistryEntry[]; + @property() public narrow!: boolean; + + protected render(): TemplateResult { + return html` + +
+ ${this.device.model + ? html` +
${this.device.model}
+ ` + : ""} + ${this.device.manufacturer + ? html` +
+ ${this.hass.localize( + "ui.panel.config.integrations.config_entry.manuf", + "manufacturer", + this.device.manufacturer + )} +
+ ` + : ""} + ${this.device.via_device_id + ? html` +
+ ${this.hass.localize( + "ui.panel.config.integrations.config_entry.via" + )} + ${this._computeDeviceName( + this.devices, + this.device.via_device_id + )} +
+ ` + : ""} + ${this.device.sw_version + ? html` +
+ ${this.hass.localize( + "ui.panel.config.integrations.config_entry.firmware", + "version", + this.device.sw_version + )} +
+ ` + : ""} + +
+
+ `; + } + + protected firstUpdated(changedProps) { + super.firstUpdated(changedProps); + loadDeviceRegistryDetailDialog(); + } + + private _computeDeviceName(devices, deviceId) { + const device = devices.find((dev) => dev.id === deviceId); + return device + ? computeDeviceName(device, this.hass) + : `(${this.hass.localize( + "ui.panel.config.integrations.config_entry.device_unavailable" + )})`; + } + + static get styles(): CSSResult { + return css` + :host { + display: block; + } + ha-card { + flex: 1 0 100%; + padding-bottom: 10px; + min-width: 0; + } + .device { + width: 30%; + } + .area { + color: var(--primary-text-color); + } + .extra-info { + margin-top: 8px; + } + .manuf, + .entity-id, + .model { + color: var(--secondary-text-color); + } + `; + } +} diff --git a/src/panels/config/devices/ha-config-device-page.ts b/src/panels/config/devices/ha-config-device-page.ts index af89408ad5..02ca4cd905 100644 --- a/src/panels/config/devices/ha-config-device-page.ts +++ b/src/panels/config/devices/ha-config-device-page.ts @@ -15,7 +15,7 @@ import "../../../layouts/hass-tabs-subpage"; import "../../../layouts/hass-error-screen"; import "../ha-config-section"; -import "./device-detail/ha-device-card"; +import "./device-detail/ha-device-info-card"; import "./device-detail/ha-device-card-mqtt"; import "./device-detail/ha-device-entities-card"; import { HomeAssistant, Route } from "../../../types"; @@ -23,6 +23,7 @@ import { ConfigEntry } from "../../../data/config_entries"; import { EntityRegistryEntry, updateEntityRegistryEntry, + findBatteryEntity, } from "../../../data/entity_registry"; import { DeviceRegistryEntry, @@ -41,9 +42,9 @@ import { createValidEntityId } from "../../../common/entity/valid_entity_id"; import { configSections } from "../ha-panel-config"; import { RelatedResult, findRelated } from "../../../data/search"; import { SceneEntities, showSceneEditor } from "../../../data/scene"; -import { navigate } from "../../../common/navigate"; import { showDeviceAutomationDialog } from "./device-detail/show-dialog-device-automation"; import { isComponentLoaded } from "../../../common/config/is_component_loaded"; +import { ifDefined } from "lit-html/directives/if-defined"; export interface EntityRegistryStateEntry extends EntityRegistryEntry { stateName?: string; @@ -96,6 +97,10 @@ export class HaConfigDevicePage extends LitElement { ) ); + private _batteryEntity = memoizeOne((entities: EntityRegistryEntry[]): + | EntityRegistryEntry + | undefined => findBatteryEntity(this.hass, entities)); + protected firstUpdated(changedProps) { super.firstUpdated(changedProps); loadDeviceRegistryDetailDialog(); @@ -123,6 +128,11 @@ export class HaConfigDevicePage extends LitElement { const integrations = this._integrations(device, this.entries); const entities = this._entities(this.deviceId, this.entities); + const batteryEntity = this._batteryEntity(entities); + const batteryState = batteryEntity + ? this.hass.states[batteryEntity.entity_id] + : undefined; + const areaName = this._computeAreaName(this.areas, device); return html`
-
-
- ${ - this.narrow - ? "" - : html` +
+ ${ + this.narrow + ? "" + : html` +

${computeDeviceName(device, this.hass)}

- ` - } - + ` + } +
+ ${ + batteryState + ? html` +
+ ${batteryState.state}% + +
+ ` + : "" + } + +
+
+
+ + > ${ integrations.includes("mqtt") ? html` @@ -173,7 +220,7 @@ export class HaConfigDevicePage extends LitElement { ` : html`` } -
+ ${ entities.length @@ -187,32 +234,46 @@ export class HaConfigDevicePage extends LitElement { : html`` }
-
${ isComponentLoaded(this.hass, "automation") ? html` - ${this._related?.automation?.length + +
+ ${this.hass.localize( + "ui.panel.config.devices.automation.automations" + )} + +
+ ${this._related?.automation?.length ? this._related.automation.map((automation) => { const state = this.hass.states[automation]; return state ? html`
- - - ${state.attributes.friendly_name || - automation} - - - + + + ${computeStateName(state)} + + + + ${!state.attributes.id ? html` `} -
- - ${this.hass.localize( - "ui.panel.config.devices.automation.create" - )} - -
` : "" @@ -249,58 +303,72 @@ export class HaConfigDevicePage extends LitElement { ${ isComponentLoaded(this.hass, "scene") ? html` - ${this._related?.scene?.length - ? this._related.scene.map((scene) => { - const state = this.hass.states[scene]; - return state + +
+ ${this.hass.localize( + "ui.panel.config.devices.scene.scenes" + )} + ${ + entities.length ? html` -
- - - ${state.attributes.friendly_name || - scene} - - - - ${!state.attributes.id - ? html` - ${this.hass.localize( - "ui.panel.config.devices.cant_edit" - )} - - ` - : ""} -
+ ` - : ""; - }) - : html` - ${this.hass.localize( - "ui.panel.config.devices.scene.no_scenes" - )} - `} - ${entities.length - ? html` -
- - ${this.hass.localize( - "ui.panel.config.devices.scene.create" - )} - -
- ` - : ""} + : "" + } +
+ + ${ + this._related?.scene?.length + ? this._related.scene.map((scene) => { + const state = this.hass.states[scene]; + return state + ? html` +
+ + + + ${computeStateName(state)} + + + + + ${!state.attributes.id + ? html` + ${this.hass.localize( + "ui.panel.config.devices.cant_edit" + )} + + ` + : ""} +
+ ` + : ""; + }) + : html` + ${this.hass.localize( + "ui.panel.config.devices.scene.no_scenes" + )} + ` + } +
` : "" @@ -308,25 +376,38 @@ export class HaConfigDevicePage extends LitElement { ${ isComponentLoaded(this.hass, "script") ? html` - ${this._related?.script?.length + +
+ ${this.hass.localize( + "ui.panel.config.devices.script.scripts" + )} + +
+ ${this._related?.script?.length ? this._related.script.map((script) => { const state = this.hass.states[script]; return state ? html` - - - ${state.attributes.friendly_name || - script} - - - + + + ${computeStateName(state)} + + + + ` : ""; }) @@ -337,19 +418,11 @@ export class HaConfigDevicePage extends LitElement { )} `} -
- - ${this.hass.localize( - "ui.panel.config.devices.script.create" - )} - -
` : "" }
-
`; @@ -363,6 +436,21 @@ export class HaConfigDevicePage extends LitElement { return state ? computeStateName(state) : null; } + private _computeAreaName(areas, device): string | undefined { + if (!areas || !device || !device.area_id) { + return undefined; + } + return areas.find((area) => area.area_id === device.area_id).name; + } + + private _onImageLoad(ev) { + ev.target.style.display = "inline-block"; + } + + private _onImageError(ev) { + ev.target.style.display = "none"; + } + private async _findRelated() { this._related = await findRelated(this.hass, "device", this.deviceId); } @@ -377,25 +465,6 @@ export class HaConfigDevicePage extends LitElement { }); } - private _openScene(ev: Event) { - const state = (ev.currentTarget as any).scene; - if (state.attributes.id) { - navigate(this, `/config/scene/edit/${state.attributes.id}`); - } - } - - private _openScript(ev: Event) { - const script = (ev.currentTarget as any).script; - navigate(this, `/config/script/edit/${script}`); - } - - private _openAutomation(ev: Event) { - const state = (ev.currentTarget as any).automation; - if (state.attributes.id) { - navigate(this, `/config/automation/edit/${state.attributes.id}`); - } - } - private _showScriptDialog() { showDeviceAutomationDialog(this, { deviceId: this.deviceId, script: true }); } @@ -478,6 +547,18 @@ export class HaConfigDevicePage extends LitElement { margin-bottom: 32px; } + .card-header { + display: flex; + align-items: center; + justify-content: space-between; + } + + .card-header paper-icon-button { + margin-right: -8px; + color: var(--primary-color); + height: auto; + } + .device-info { padding: 16px; } @@ -486,7 +567,7 @@ export class HaConfigDevicePage extends LitElement { } h1 { - margin-top: 0; + margin: 0; font-family: var(--paper-font-headline_-_font-family); -webkit-font-smoothing: var( --paper-font-headline_-_-webkit-font-smoothing @@ -498,46 +579,60 @@ export class HaConfigDevicePage extends LitElement { opacity: var(--dark-primary-opacity); } - .left, + .header { + display: flex; + justify-content: space-between; + } + .column, .fullwidth { padding: 8px; box-sizing: border-box; } - - .left { - width: 33.33%; - padding-bottom: 0; + .column { + width: 33%; + flex-grow: 1; } - - .right { - width: 66.66%; - display: flex; - flex-wrap: wrap; - } - .fullwidth { width: 100%; + flex-grow: 1; } - .column { - width: 50%; + .header-right { + align-self: center; + } + + .header-right img { + height: 30px; + } + + .header-right { + display: flex; + } + + .header-right:first-child { + width: 100%; + justify-content: flex-end; + } + + .header-right > *:not(:first-child) { + margin-left: 16px; + } + + .battery { + align-self: center; + align-items: center; + display: flex; } .column > *:not(:first-child) { margin-top: 16px; } - :host([narrow]) .left, - :host([narrow]) .right, :host([narrow]) .column { width: 100%; } - :host([narrow]) .container > *:first-child { - padding-top: 0; - } - :host([narrow]) .container { margin-top: 0; } @@ -549,6 +644,11 @@ export class HaConfigDevicePage extends LitElement { paper-item.no-link { cursor: default; } + + a { + text-decoration: none; + color: var(--primary-text-color); + } `; } } diff --git a/src/panels/config/devices/ha-config-devices-dashboard.ts b/src/panels/config/devices/ha-config-devices-dashboard.ts index a0c05669d4..28bc4922e0 100644 --- a/src/panels/config/devices/ha-config-devices-dashboard.ts +++ b/src/panels/config/devices/ha-config-devices-dashboard.ts @@ -13,7 +13,10 @@ import { computeDeviceName, DeviceEntityLookup, } from "../../../data/device_registry"; -import { EntityRegistryEntry } from "../../../data/entity_registry"; +import { + EntityRegistryEntry, + findBatteryEntity, +} from "../../../data/entity_registry"; import { ConfigEntry } from "../../../data/config_entries"; import { AreaRegistryEntry } from "../../../data/area_registry"; import { configSections } from "../ha-panel-config"; @@ -130,25 +133,38 @@ export class HaConfigDeviceDashboard extends LitElement { direction: "asc", grows: true, 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}
- ${device.area} | ${device.integration}
- ${battery && !isNaN(battery.state as any) - ? html` - ${battery.state}% - - ` - : ""} + ${name} +
+ ${device.area} | ${device.integration} +
`; }, }, + battery_entity: { + title: this.hass.localize( + "ui.panel.config.devices.data_table.battery" + ), + sortable: true, + type: "numeric", + width: "90px", + template: (batteryEntity: string) => { + const battery = batteryEntity + ? this.hass.states[batteryEntity] + : undefined; + return battery + ? html` + ${isNaN(battery.state as any) ? "-" : battery.state}% + + ` + : html` + - + `; + }, + }, } : { name: { @@ -198,7 +214,8 @@ export class HaConfigDeviceDashboard extends LitElement { ), sortable: true, type: "numeric", - width: "60px", + width: "15%", + maxWidth: "90px", template: (batteryEntity: string) => { const battery = batteryEntity ? this.hass.states[batteryEntity] @@ -246,12 +263,10 @@ export class HaConfigDeviceDashboard extends LitElement { 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" + const batteryEntity = findBatteryEntity( + this.hass, + deviceEntityLookup[deviceId] || [] ); - return batteryEntity ? batteryEntity.entity_id : undefined; } diff --git a/src/panels/config/devices/ha-devices-data-table.ts b/src/panels/config/devices/ha-devices-data-table.ts index b9ee20a35c..9a0bbdfc5c 100644 --- a/src/panels/config/devices/ha-devices-data-table.ts +++ b/src/panels/config/devices/ha-devices-data-table.ts @@ -23,7 +23,10 @@ import { computeDeviceName, DeviceEntityLookup, } from "../../../data/device_registry"; -import { EntityRegistryEntry } from "../../../data/entity_registry"; +import { + EntityRegistryEntry, + findBatteryEntity, +} from "../../../data/entity_registry"; import { ConfigEntry } from "../../../data/config_entries"; import { AreaRegistryEntry } from "../../../data/area_registry"; import { navigate } from "../../../common/navigate"; @@ -204,7 +207,8 @@ export class HaDevicesDataTable extends LitElement { ), sortable: true, type: "numeric", - width: "60px", + width: "15%", + maxWidth: "90px", template: (batteryEntity: string) => { const battery = batteryEntity ? this.hass.states[batteryEntity] @@ -250,12 +254,10 @@ export class HaDevicesDataTable extends LitElement { 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" + const batteryEntity = findBatteryEntity( + this.hass, + deviceEntityLookup[deviceId] || [] ); - return batteryEntity ? batteryEntity.entity_id : undefined; }