From e61aa266a6665943ab105aae6e6a899332131401 Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Wed, 25 May 2022 07:55:08 -0500 Subject: [PATCH] Move Logbook and make device page better (#12763) Co-authored-by: Bram Kragten --- .../config/areas/ha-config-area-page.ts | 8 +- .../mqtt/device-actions.ts | 13 + .../mqtt/ha-device-actions-mqtt.ts | 36 - .../zha/device-actions.ts | 108 +++ .../zha/ha-device-actions-zha.ts | 155 ----- .../zha/ha-device-info-zha.ts | 71 +- .../zwave_js/device-actions.ts | 69 ++ .../zwave_js/ha-device-actions-zwave_js.ts | 138 ---- .../zwave_js/ha-device-info-zwave_js.ts | 147 +++-- .../config/devices/ha-config-device-page.ts | 613 ++++++++++-------- src/panels/logbook/ha-logbook-renderer.ts | 11 +- src/resources/styles.ts | 1 + 12 files changed, 664 insertions(+), 706 deletions(-) create mode 100644 src/panels/config/devices/device-detail/integration-elements/mqtt/device-actions.ts delete mode 100644 src/panels/config/devices/device-detail/integration-elements/mqtt/ha-device-actions-mqtt.ts create mode 100644 src/panels/config/devices/device-detail/integration-elements/zha/device-actions.ts delete mode 100644 src/panels/config/devices/device-detail/integration-elements/zha/ha-device-actions-zha.ts create mode 100644 src/panels/config/devices/device-detail/integration-elements/zwave_js/device-actions.ts delete mode 100644 src/panels/config/devices/device-detail/integration-elements/zwave_js/ha-device-actions-zwave_js.ts diff --git a/src/panels/config/areas/ha-config-area-page.ts b/src/panels/config/areas/ha-config-area-page.ts index 801b1902f7..ed4573578c 100644 --- a/src/panels/config/areas/ha-config-area-page.ts +++ b/src/panels/config/areas/ha-config-area-page.ts @@ -19,7 +19,6 @@ import { afterNextRender } from "../../../common/util/render-status"; import "../../../components/ha-card"; import "../../../components/ha-icon-button"; import "../../../components/ha-icon-next"; -import "../../logbook/ha-logbook"; import { AreaRegistryEntry, deleteAreaRegistryEntry, @@ -43,15 +42,16 @@ import { SceneEntity } from "../../../data/scene"; import { ScriptEntity } from "../../../data/script"; import { findRelated, RelatedResult } from "../../../data/search"; import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box"; +import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; import { haStyle } from "../../../resources/styles"; import { HomeAssistant, Route } from "../../../types"; +import "../../logbook/ha-logbook"; import { showEntityEditorDialog } from "../entities/show-dialog-entity-editor"; import { configSections } from "../ha-panel-config"; import { loadAreaRegistryDetailDialog, showAreaRegistryDetailDialog, } from "./show-dialog-area-registry-detail"; -import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; declare type NameAndEntity = { name: string; @@ -761,10 +761,10 @@ class HaConfigAreaPage extends SubscribeMixin(LitElement) { border-radius: 50%; } ha-logbook { - height: 800px; + height: 400px; } :host([narrow]) ha-logbook { - height: 400px; + height: 235px; overflow: auto; } `, diff --git a/src/panels/config/devices/device-detail/integration-elements/mqtt/device-actions.ts b/src/panels/config/devices/device-detail/integration-elements/mqtt/device-actions.ts new file mode 100644 index 0000000000..ff3be5d1a9 --- /dev/null +++ b/src/panels/config/devices/device-detail/integration-elements/mqtt/device-actions.ts @@ -0,0 +1,13 @@ +import type { DeviceRegistryEntry } from "../../../../../../data/device_registry"; +import type { DeviceAction } from "../../../ha-config-device-page"; +import { showMQTTDeviceDebugInfoDialog } from "./show-dialog-mqtt-device-debug-info"; + +export const getMQTTDeviceActions = ( + el: HTMLElement, + device: DeviceRegistryEntry +): DeviceAction[] => [ + { + label: "MQTT Info", + action: async () => showMQTTDeviceDebugInfoDialog(el, { device }), + }, +]; diff --git a/src/panels/config/devices/device-detail/integration-elements/mqtt/ha-device-actions-mqtt.ts b/src/panels/config/devices/device-detail/integration-elements/mqtt/ha-device-actions-mqtt.ts deleted file mode 100644 index f7e7847722..0000000000 --- a/src/panels/config/devices/device-detail/integration-elements/mqtt/ha-device-actions-mqtt.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; -import { customElement, property } from "lit/decorators"; -import { DeviceRegistryEntry } from "../../../../../../data/device_registry"; -import { haStyle } from "../../../../../../resources/styles"; -import { HomeAssistant } from "../../../../../../types"; -import { showMQTTDeviceDebugInfoDialog } from "./show-dialog-mqtt-device-debug-info"; - -@customElement("ha-device-actions-mqtt") -export class HaDeviceActionsMqtt extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; - - @property() public device!: DeviceRegistryEntry; - - protected render(): TemplateResult { - return html` - MQTT Info - `; - } - - private async _showDebugInfo(): Promise { - const device = this.device; - await showMQTTDeviceDebugInfoDialog(this, { device }); - } - - static get styles(): CSSResultGroup { - return [ - haStyle, - css` - :host { - display: flex; - justify-content: space-between; - } - `, - ]; - } -} diff --git a/src/panels/config/devices/device-detail/integration-elements/zha/device-actions.ts b/src/panels/config/devices/device-detail/integration-elements/zha/device-actions.ts new file mode 100644 index 0000000000..476ff74993 --- /dev/null +++ b/src/panels/config/devices/device-detail/integration-elements/zha/device-actions.ts @@ -0,0 +1,108 @@ +import { navigate } from "../../../../../../common/navigate"; +import type { DeviceRegistryEntry } from "../../../../../../data/device_registry"; +import { fetchZHADevice } from "../../../../../../data/zha"; +import { showConfirmationDialog } from "../../../../../../dialogs/generic/show-dialog-box"; +import type { HomeAssistant } from "../../../../../../types"; +import { showZHAClusterDialog } from "../../../../integrations/integration-panels/zha/show-dialog-zha-cluster"; +import { showZHADeviceChildrenDialog } from "../../../../integrations/integration-panels/zha/show-dialog-zha-device-children"; +import { showZHADeviceZigbeeInfoDialog } from "../../../../integrations/integration-panels/zha/show-dialog-zha-device-zigbee-info"; +import { showZHAReconfigureDeviceDialog } from "../../../../integrations/integration-panels/zha/show-dialog-zha-reconfigure-device"; +import type { DeviceAction } from "../../../ha-config-device-page"; + +export const getZHADeviceActions = async ( + el: HTMLElement, + hass: HomeAssistant, + device: DeviceRegistryEntry +): Promise => { + const zigbeeConnection = device.connections.find( + (conn) => conn[0] === "zigbee" + ); + + if (!zigbeeConnection) { + return []; + } + + const zhaDevice = await fetchZHADevice(hass, zigbeeConnection[1]); + + if (!zhaDevice) { + return []; + } + + const actions: DeviceAction[] = []; + + if (zhaDevice.device_type !== "Coordinator") { + actions.push({ + label: hass.localize("ui.dialogs.zha_device_info.buttons.reconfigure"), + action: () => showZHAReconfigureDeviceDialog(el, { device: zhaDevice }), + }); + } + + if ( + zhaDevice.power_source === "Mains" && + (zhaDevice.device_type === "Router" || + zhaDevice.device_type === "Coordinator") + ) { + actions.push( + ...[ + { + label: hass.localize("ui.dialogs.zha_device_info.buttons.add"), + action: () => navigate(`/config/zha/add/${zhaDevice!.ieee}`), + }, + { + label: hass.localize( + "ui.dialogs.zha_device_info.buttons.device_children" + ), + action: () => showZHADeviceChildrenDialog(el, { device: zhaDevice! }), + }, + ] + ); + } + + if (zhaDevice.device_type !== "Coordinator") { + actions.push( + ...[ + { + label: hass.localize( + "ui.dialogs.zha_device_info.buttons.zigbee_information" + ), + action: () => + showZHADeviceZigbeeInfoDialog(el, { device: zhaDevice }), + }, + { + label: hass.localize("ui.dialogs.zha_device_info.buttons.clusters"), + action: () => showZHAClusterDialog(el, { device: zhaDevice }), + }, + { + label: hass.localize( + "ui.dialogs.zha_device_info.buttons.view_in_visualization" + ), + action: () => + navigate(`/config/zha/visualization/${zhaDevice!.device_reg_id}`), + }, + { + label: hass.localize("ui.dialogs.zha_device_info.buttons.remove"), + classes: "warning", + action: async () => { + const confirmed = await showConfirmationDialog(el, { + text: hass.localize( + "ui.dialogs.zha_device_info.confirmations.remove" + ), + }); + + if (!confirmed) { + return; + } + + await hass.callService("zha", "remove", { + ieee: zhaDevice.ieee, + }); + + history.back(); + }, + }, + ] + ); + } + + return actions; +}; diff --git a/src/panels/config/devices/device-detail/integration-elements/zha/ha-device-actions-zha.ts b/src/panels/config/devices/device-detail/integration-elements/zha/ha-device-actions-zha.ts deleted file mode 100644 index 2a0337c043..0000000000 --- a/src/panels/config/devices/device-detail/integration-elements/zha/ha-device-actions-zha.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { - css, - CSSResultGroup, - html, - LitElement, - PropertyValues, - TemplateResult, -} from "lit"; -import { customElement, property, state } from "lit/decorators"; -import { navigate } from "../../../../../../common/navigate"; -import { DeviceRegistryEntry } from "../../../../../../data/device_registry"; -import { fetchZHADevice, ZHADevice } from "../../../../../../data/zha"; -import { showConfirmationDialog } from "../../../../../../dialogs/generic/show-dialog-box"; -import { haStyle } from "../../../../../../resources/styles"; -import { HomeAssistant } from "../../../../../../types"; -import { showZHAClusterDialog } from "../../../../integrations/integration-panels/zha/show-dialog-zha-cluster"; -import { showZHADeviceChildrenDialog } from "../../../../integrations/integration-panels/zha/show-dialog-zha-device-children"; -import { showZHADeviceZigbeeInfoDialog } from "../../../../integrations/integration-panels/zha/show-dialog-zha-device-zigbee-info"; -import { showZHAReconfigureDeviceDialog } from "../../../../integrations/integration-panels/zha/show-dialog-zha-reconfigure-device"; - -@customElement("ha-device-actions-zha") -export class HaDeviceActionsZha extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; - - @property() public device!: DeviceRegistryEntry; - - @state() private _zhaDevice?: ZHADevice; - - protected updated(changedProperties: PropertyValues) { - if (changedProperties.has("device")) { - const zigbeeConnection = this.device.connections.find( - (conn) => conn[0] === "zigbee" - ); - if (!zigbeeConnection) { - return; - } - fetchZHADevice(this.hass, zigbeeConnection[1]).then((device) => { - this._zhaDevice = device; - }); - } - } - - protected render(): TemplateResult { - if (!this._zhaDevice) { - return html``; - } - return html` - ${this._zhaDevice.device_type !== "Coordinator" - ? html` - - ${this.hass!.localize( - "ui.dialogs.zha_device_info.buttons.reconfigure" - )} - - ` - : ""} - ${this._zhaDevice.power_source === "Mains" && - (this._zhaDevice.device_type === "Router" || - this._zhaDevice.device_type === "Coordinator") - ? html` - - ${this.hass!.localize("ui.dialogs.zha_device_info.buttons.add")} - - - ${this.hass!.localize( - "ui.dialogs.zha_device_info.buttons.device_children" - )} - - ` - : ""} - ${this._zhaDevice.device_type !== "Coordinator" - ? html` - - ${this.hass!.localize( - "ui.dialogs.zha_device_info.buttons.zigbee_information" - )} - - - ${this.hass!.localize( - "ui.dialogs.zha_device_info.buttons.clusters" - )} - - - ${this.hass!.localize( - "ui.dialogs.zha_device_info.buttons.view_in_visualization" - )} - - - ${this.hass!.localize( - "ui.dialogs.zha_device_info.buttons.remove" - )} - - ` - : ""} - `; - } - - private async _showClustersDialog(): Promise { - await showZHAClusterDialog(this, { device: this._zhaDevice! }); - } - - private async _onReconfigureNodeClick(): Promise { - if (!this.hass) { - return; - } - showZHAReconfigureDeviceDialog(this, { device: this._zhaDevice! }); - } - - private _onAddDevicesClick() { - navigate(`/config/zha/add/${this._zhaDevice!.ieee}`); - } - - private _onViewInVisualizationClick() { - navigate(`/config/zha/visualization/${this._zhaDevice!.device_reg_id}`); - } - - private async _handleZigbeeInfoClicked() { - showZHADeviceZigbeeInfoDialog(this, { device: this._zhaDevice! }); - } - - private async _handleDeviceChildrenClicked() { - showZHADeviceChildrenDialog(this, { device: this._zhaDevice! }); - } - - private async _removeDevice() { - const confirmed = await showConfirmationDialog(this, { - text: this.hass.localize( - "ui.dialogs.zha_device_info.confirmations.remove" - ), - }); - - if (!confirmed) { - return; - } - - await this.hass.callService("zha", "remove", { - ieee: this._zhaDevice!.ieee, - }); - - history.back(); - } - - static get styles(): CSSResultGroup { - return [ - haStyle, - css` - :host { - display: flex; - flex-direction: column; - align-items: flex-start; - } - `, - ]; - } -} diff --git a/src/panels/config/devices/device-detail/integration-elements/zha/ha-device-info-zha.ts b/src/panels/config/devices/device-detail/integration-elements/zha/ha-device-info-zha.ts index 28a3e3e0e6..5ccaf7c5e5 100644 --- a/src/panels/config/devices/device-detail/integration-elements/zha/ha-device-info-zha.ts +++ b/src/panels/config/devices/device-detail/integration-elements/zha/ha-device-info-zha.ts @@ -7,6 +7,7 @@ import { TemplateResult, } from "lit"; import { customElement, property, state } from "lit/decorators"; +import "../../../../../../components/ha-expansion-panel"; import { DeviceRegistryEntry } from "../../../../../../data/device_registry"; import { fetchZHADevice, ZHADevice } from "../../../../../../data/zha"; import { haStyle } from "../../../../../../resources/styles"; @@ -40,38 +41,39 @@ export class HaDeviceActionsZha extends LitElement { return html``; } return html` -

Zigbee info

-
IEEE: ${this._zhaDevice.ieee}
-
Nwk: ${formatAsPaddedHex(this._zhaDevice.nwk)}
-
Device Type: ${this._zhaDevice.device_type}
-
- LQI: - ${this._zhaDevice.lqi || - this.hass!.localize("ui.dialogs.zha_device_info.unknown")} -
-
- RSSI: - ${this._zhaDevice.rssi || - this.hass!.localize("ui.dialogs.zha_device_info.unknown")} -
-
- ${this.hass!.localize("ui.dialogs.zha_device_info.last_seen")}: - ${this._zhaDevice.last_seen || - this.hass!.localize("ui.dialogs.zha_device_info.unknown")} -
-
- ${this.hass!.localize("ui.dialogs.zha_device_info.power_source")}: - ${this._zhaDevice.power_source || - this.hass!.localize("ui.dialogs.zha_device_info.unknown")} -
- ${this._zhaDevice.quirk_applied - ? html` -
- ${this.hass!.localize("ui.dialogs.zha_device_info.quirk")}: - ${this._zhaDevice.quirk_class} -
- ` - : ""} + +
IEEE: ${this._zhaDevice.ieee}
+
Nwk: ${formatAsPaddedHex(this._zhaDevice.nwk)}
+
Device Type: ${this._zhaDevice.device_type}
+
+ LQI: + ${this._zhaDevice.lqi || + this.hass!.localize("ui.dialogs.zha_device_info.unknown")} +
+
+ RSSI: + ${this._zhaDevice.rssi || + this.hass!.localize("ui.dialogs.zha_device_info.unknown")} +
+
+ ${this.hass!.localize("ui.dialogs.zha_device_info.last_seen")}: + ${this._zhaDevice.last_seen || + this.hass!.localize("ui.dialogs.zha_device_info.unknown")} +
+
+ ${this.hass!.localize("ui.dialogs.zha_device_info.power_source")}: + ${this._zhaDevice.power_source || + this.hass!.localize("ui.dialogs.zha_device_info.unknown")} +
+ ${this._zhaDevice.quirk_applied + ? html` +
+ ${this.hass!.localize("ui.dialogs.zha_device_info.quirk")}: + ${this._zhaDevice.quirk_class} +
+ ` + : ""} +
`; } @@ -86,6 +88,11 @@ export class HaDeviceActionsZha extends LitElement { word-break: break-all; margin-top: 2px; } + ha-expansion-panel { + --expansion-panel-summary-padding: 0; + --expansion-panel-content-padding: 0; + padding-top: 4px; + } `, ]; } diff --git a/src/panels/config/devices/device-detail/integration-elements/zwave_js/device-actions.ts b/src/panels/config/devices/device-detail/integration-elements/zwave_js/device-actions.ts new file mode 100644 index 0000000000..7992a3a767 --- /dev/null +++ b/src/panels/config/devices/device-detail/integration-elements/zwave_js/device-actions.ts @@ -0,0 +1,69 @@ +import { getConfigEntries } from "../../../../../../data/config_entries"; +import { DeviceRegistryEntry } from "../../../../../../data/device_registry"; +import { fetchZwaveNodeStatus } from "../../../../../../data/zwave_js"; +import type { HomeAssistant } from "../../../../../../types"; +import { showZWaveJSHealNodeDialog } from "../../../../integrations/integration-panels/zwave_js/show-dialog-zwave_js-heal-node"; +import { showZWaveJSReinterviewNodeDialog } from "../../../../integrations/integration-panels/zwave_js/show-dialog-zwave_js-reinterview-node"; +import { showZWaveJSRemoveFailedNodeDialog } from "../../../../integrations/integration-panels/zwave_js/show-dialog-zwave_js-remove-failed-node"; +import type { DeviceAction } from "../../../ha-config-device-page"; + +export const getZwaveDeviceActions = async ( + el: HTMLElement, + hass: HomeAssistant, + device: DeviceRegistryEntry +): Promise => { + const configEntries = await getConfigEntries(hass, { + domain: "zwave_js", + }); + + const configEntry = configEntries.find((entry) => + device.config_entries.includes(entry.entry_id) + ); + + if (!configEntry) { + return []; + } + + const entryId = configEntry.entry_id; + + const node = await fetchZwaveNodeStatus(hass, device.id); + + if (!node || node.is_controller_node) { + return []; + } + + return [ + { + label: hass.localize( + "ui.panel.config.zwave_js.device_info.device_config" + ), + href: `/config/zwave_js/node_config/${device.id}?config_entry=${entryId}`, + }, + { + label: hass.localize( + "ui.panel.config.zwave_js.device_info.reinterview_device" + ), + action: () => + showZWaveJSReinterviewNodeDialog(el, { + device_id: device.id, + }), + }, + { + label: hass.localize("ui.panel.config.zwave_js.device_info.heal_node"), + action: () => + showZWaveJSHealNodeDialog(el, { + entry_id: entryId, + device: device, + }), + }, + { + label: hass.localize( + "ui.panel.config.zwave_js.device_info.remove_failed" + ), + action: () => + showZWaveJSRemoveFailedNodeDialog(el, { + device_id: device.id, + }), + }, + ]; +}; diff --git a/src/panels/config/devices/device-detail/integration-elements/zwave_js/ha-device-actions-zwave_js.ts b/src/panels/config/devices/device-detail/integration-elements/zwave_js/ha-device-actions-zwave_js.ts deleted file mode 100644 index b0a707f5b7..0000000000 --- a/src/panels/config/devices/device-detail/integration-elements/zwave_js/ha-device-actions-zwave_js.ts +++ /dev/null @@ -1,138 +0,0 @@ -import "@material/mwc-button/mwc-button"; -import { - css, - CSSResultGroup, - html, - LitElement, - PropertyValues, - TemplateResult, -} from "lit"; -import { customElement, property, state } from "lit/decorators"; -import { DeviceRegistryEntry } from "../../../../../../data/device_registry"; -import { - fetchZwaveNodeStatus, - ZWaveJSNodeStatus, -} from "../../../../../../data/zwave_js"; -import { haStyle } from "../../../../../../resources/styles"; -import { HomeAssistant } from "../../../../../../types"; -import { showZWaveJSReinterviewNodeDialog } from "../../../../integrations/integration-panels/zwave_js/show-dialog-zwave_js-reinterview-node"; -import { showZWaveJSHealNodeDialog } from "../../../../integrations/integration-panels/zwave_js/show-dialog-zwave_js-heal-node"; -import { showZWaveJSRemoveFailedNodeDialog } from "../../../../integrations/integration-panels/zwave_js/show-dialog-zwave_js-remove-failed-node"; -import { getConfigEntries } from "../../../../../../data/config_entries"; - -@customElement("ha-device-actions-zwave_js") -export class HaDeviceActionsZWaveJS extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; - - @property() public device!: DeviceRegistryEntry; - - @state() private _entryId?: string; - - @state() private _node?: ZWaveJSNodeStatus; - - public willUpdate(changedProperties: PropertyValues) { - super.willUpdate(changedProperties); - if (changedProperties.has("device")) { - this._fetchNodeDetails(); - } - } - - protected async _fetchNodeDetails() { - if (!this.device) { - return; - } - - this._node = undefined; - - const configEntries = await getConfigEntries(this.hass, { - domain: "zwave_js", - }); - - const configEntry = configEntries.find((entry) => - this.device.config_entries.includes(entry.entry_id) - ); - - if (!configEntry) { - return; - } - - this._entryId = configEntry.entry_id; - - this._node = await fetchZwaveNodeStatus(this.hass, this.device.id); - } - - protected render(): TemplateResult { - if (!this._node) { - return html``; - } - return html` - ${!this._node.is_controller_node - ? html` - - - ${this.hass.localize( - "ui.panel.config.zwave_js.device_info.device_config" - )} - - - - ${this.hass.localize( - "ui.panel.config.zwave_js.device_info.reinterview_device" - )} - - - ${this.hass.localize( - "ui.panel.config.zwave_js.device_info.heal_node" - )} - - - ${this.hass.localize( - "ui.panel.config.zwave_js.device_info.remove_failed" - )} - - ` - : ""} - `; - } - - private async _reinterviewClicked() { - if (!this.device) { - return; - } - showZWaveJSReinterviewNodeDialog(this, { - device_id: this.device.id, - }); - } - - private async _healNodeClicked() { - if (!this.device) { - return; - } - showZWaveJSHealNodeDialog(this, { - entry_id: this._entryId!, - device: this.device, - }); - } - - private async _removeFailedNode() { - if (!this.device) { - return; - } - showZWaveJSRemoveFailedNodeDialog(this, { - device_id: this.device.id, - }); - } - - static get styles(): CSSResultGroup { - return [ - haStyle, - css` - a { - text-decoration: none; - } - `, - ]; - } -} diff --git a/src/panels/config/devices/device-detail/integration-elements/zwave_js/ha-device-info-zwave_js.ts b/src/panels/config/devices/device-detail/integration-elements/zwave_js/ha-device-info-zwave_js.ts index 02147196f1..eae6b47a1b 100644 --- a/src/panels/config/devices/device-detail/integration-elements/zwave_js/ha-device-info-zwave_js.ts +++ b/src/panels/config/devices/device-detail/integration-elements/zwave_js/ha-device-info-zwave_js.ts @@ -7,16 +7,17 @@ import { TemplateResult, } from "lit"; import { customElement, property, state } from "lit/decorators"; -import { DeviceRegistryEntry } from "../../../../../../data/device_registry"; +import "../../../../../../components/ha-expansion-panel"; import { ConfigEntry, getConfigEntries, } from "../../../../../../data/config_entries"; +import { DeviceRegistryEntry } from "../../../../../../data/device_registry"; import { fetchZwaveNodeStatus, nodeStatus, - ZWaveJSNodeStatus, SecurityClass, + ZWaveJSNodeStatus, } from "../../../../../../data/zwave_js"; import { haStyle } from "../../../../../../resources/styles"; import { HomeAssistant } from "../../../../../../types"; @@ -69,73 +70,76 @@ export class HaDeviceInfoZWaveJS extends LitElement { return html``; } return html` -

- ${this.hass.localize("ui.panel.config.zwave_js.device_info.zwave_info")} -

- ${this._multipleConfigEntries - ? html` -
- ${this.hass.localize("ui.panel.config.zwave_js.common.source")}: - ${this._configEntry!.title} -
- ` - : ""} -
- ${this.hass.localize("ui.panel.config.zwave_js.common.node_id")}: - ${this._node.node_id} -
- ${!this._node.is_controller_node - ? html` -
- ${this.hass.localize( - "ui.panel.config.zwave_js.device_info.node_status" - )}: - ${this.hass.localize( - `ui.panel.config.zwave_js.node_status.${ - nodeStatus[this._node.status] - }` - )} -
-
- ${this.hass.localize( - "ui.panel.config.zwave_js.device_info.node_ready" - )}: - ${this._node.ready - ? this.hass.localize("ui.common.yes") - : this.hass.localize("ui.common.no")} -
-
- ${this.hass.localize( - "ui.panel.config.zwave_js.device_info.highest_security" - )}: - ${this._node.highest_security_class !== null - ? this.hass.localize( - `ui.panel.config.zwave_js.security_classes.${ - SecurityClass[this._node.highest_security_class] - }.title` - ) - : this._node.is_secure === false - ? this.hass.localize( - "ui.panel.config.zwave_js.security_classes.none.title" - ) - : this.hass.localize( - "ui.panel.config.zwave_js.device_info.unknown" - )} -
-
- ${this.hass.localize( - "ui.panel.config.zwave_js.device_info.zwave_plus" - )}: - ${this._node.zwave_plus_version - ? this.hass.localize( - "ui.panel.config.zwave_js.device_info.zwave_plus_version", - "version", - this._node.zwave_plus_version - ) - : this.hass.localize("ui.common.no")} -
- ` - : ""} + + ${this._multipleConfigEntries + ? html` +
+ ${this.hass.localize("ui.panel.config.zwave_js.common.source")}: + ${this._configEntry!.title} +
+ ` + : ""} +
+ ${this.hass.localize("ui.panel.config.zwave_js.common.node_id")}: + ${this._node.node_id} +
+ ${!this._node.is_controller_node + ? html` +
+ ${this.hass.localize( + "ui.panel.config.zwave_js.device_info.node_status" + )}: + ${this.hass.localize( + `ui.panel.config.zwave_js.node_status.${ + nodeStatus[this._node.status] + }` + )} +
+
+ ${this.hass.localize( + "ui.panel.config.zwave_js.device_info.node_ready" + )}: + ${this._node.ready + ? this.hass.localize("ui.common.yes") + : this.hass.localize("ui.common.no")} +
+
+ ${this.hass.localize( + "ui.panel.config.zwave_js.device_info.highest_security" + )}: + ${this._node.highest_security_class !== null + ? this.hass.localize( + `ui.panel.config.zwave_js.security_classes.${ + SecurityClass[this._node.highest_security_class] + }.title` + ) + : this._node.is_secure === false + ? this.hass.localize( + "ui.panel.config.zwave_js.security_classes.none.title" + ) + : this.hass.localize( + "ui.panel.config.zwave_js.device_info.unknown" + )} +
+
+ ${this.hass.localize( + "ui.panel.config.zwave_js.device_info.zwave_plus" + )}: + ${this._node.zwave_plus_version + ? this.hass.localize( + "ui.panel.config.zwave_js.device_info.zwave_plus_version", + "version", + this._node.zwave_plus_version + ) + : this.hass.localize("ui.common.no")} +
+ ` + : ""} +
`; } @@ -150,6 +154,11 @@ export class HaDeviceInfoZWaveJS extends LitElement { word-break: break-all; margin-top: 2px; } + ha-expansion-panel { + --expansion-panel-summary-padding: 0; + --expansion-panel-content-padding: 0; + padding-top: 4px; + } `, ]; } diff --git a/src/panels/config/devices/ha-config-device-page.ts b/src/panels/config/devices/ha-config-device-page.ts index 7cfd800583..305575ebe4 100644 --- a/src/panels/config/devices/ha-config-device-page.ts +++ b/src/panels/config/devices/ha-config-device-page.ts @@ -1,4 +1,9 @@ -import { mdiOpenInNew, mdiPencil, mdiPlusCircle } from "@mdi/js"; +import { + mdiDotsVertical, + mdiOpenInNew, + mdiPencil, + mdiPlusCircle, +} from "@mdi/js"; import "@polymer/paper-tooltip/paper-tooltip"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; @@ -13,6 +18,7 @@ import { slugify } from "../../../common/string/slugify"; import { groupBy } from "../../../common/util/group-by"; import "../../../components/entity/ha-battery-icon"; import "../../../components/ha-alert"; +import "../../../components/ha-button-menu"; import "../../../components/ha-icon-button"; import "../../../components/ha-icon-next"; import "../../../components/ha-svg-icon"; @@ -26,14 +32,14 @@ import { import { computeDeviceName, DeviceRegistryEntry, - updateDeviceRegistryEntry, removeConfigEntryFromDevice, + updateDeviceRegistryEntry, } from "../../../data/device_registry"; import { - fetchDiagnosticHandler, - getDeviceDiagnosticsDownloadUrl, - getConfigEntryDiagnosticsDownloadUrl, DiagnosticInfo, + fetchDiagnosticHandler, + getConfigEntryDiagnosticsDownloadUrl, + getDeviceDiagnosticsDownloadUrl, } from "../../../data/diagnostics"; import { EntityRegistryEntry, @@ -51,9 +57,10 @@ import { import "../../../layouts/hass-error-screen"; import "../../../layouts/hass-tabs-subpage"; import { haStyle } from "../../../resources/styles"; -import { HomeAssistant, Route } from "../../../types"; +import type { HomeAssistant, Route } from "../../../types"; import { brandsUrl } from "../../../util/brands-url"; import { fileDownload } from "../../../util/file_download"; +import "../../logbook/ha-logbook"; import "../ha-config-section"; import { configSections } from "../ha-panel-config"; import "./device-detail/ha-device-entities-card"; @@ -63,12 +70,19 @@ import { loadDeviceRegistryDetailDialog, showDeviceRegistryDetailDialog, } from "./device-registry-detail/show-dialog-device-registry-detail"; -import "../../logbook/ha-logbook"; export interface EntityRegistryStateEntry extends EntityRegistryEntry { stateName?: string | null; } +export interface DeviceAction { + href?: string; + action?: (ev: any) => void; + label: string; + trailingIcon?: string; + classes?: string; +} + @customElement("ha-config-device-page") export class HaConfigDevicePage extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @@ -94,11 +108,11 @@ export class HaConfigDevicePage extends LitElement { @state() private _related?: RelatedResult; // If a number, it's the request ID so we make sure we don't show older info - @state() private _diagnosticDownloadLinks?: - | number - | (TemplateResult | string)[]; + @state() private _diagnosticDownloadLinks?: number | DeviceAction[]; - @state() private _deleteButtons?: (TemplateResult | string)[]; + @state() private _deleteButtons?: DeviceAction[]; + + @state() private _deviceActions?: DeviceAction[]; private _logbookTime = { recent: 86400 }; @@ -196,15 +210,17 @@ export class HaConfigDevicePage extends LitElement { if ( changedProps.has("deviceId") || changedProps.has("devices") || - changedProps.has("deviceId") || changedProps.has("entries") ) { this._diagnosticDownloadLinks = undefined; this._deleteButtons = undefined; + this._deviceActions = undefined; } if ( - (this._diagnosticDownloadLinks && this._deleteButtons) || + (this._diagnosticDownloadLinks && + this._deleteButtons && + this._deviceActions) || !this.devices || !this.deviceId || !this.entries @@ -214,129 +230,10 @@ export class HaConfigDevicePage extends LitElement { this._diagnosticDownloadLinks = Math.random(); this._deleteButtons = []; // To prevent re-rendering if no delete buttons - this._renderDiagnosticButtons(this._diagnosticDownloadLinks); - this._renderDeleteButtons(); - } - - private async _renderDiagnosticButtons(requestId: number): Promise { - if (!isComponentLoaded(this.hass, "diagnostics")) { - return; - } - - const device = this._device(this.deviceId, this.devices); - - if (!device) { - return; - } - - let links = await Promise.all( - this._integrations(device, this.entries).map( - async (entry): Promise => { - if (entry.state !== "loaded") { - return false; - } - let info: DiagnosticInfo; - try { - info = await fetchDiagnosticHandler(this.hass, entry.domain); - } catch (err: any) { - if (err.code === "not_found") { - return false; - } - throw err; - } - - if (!info.handlers.device && !info.handlers.config_entry) { - return false; - } - return { - link: info.handlers.device - ? getDeviceDiagnosticsDownloadUrl(entry.entry_id, this.deviceId) - : getConfigEntryDiagnosticsDownloadUrl(entry.entry_id), - domain: entry.domain, - }; - } - ) - ); - - links = links.filter(Boolean); - - if (this._diagnosticDownloadLinks !== requestId) { - return; - } - if (links.length > 0) { - this._diagnosticDownloadLinks = ( - links as { link: string; domain: string }[] - ).map( - (link) => html` - - - ${links.length > 1 - ? this.hass.localize( - `ui.panel.config.devices.download_diagnostics_integration`, - { - integration: domainToName( - this.hass.localize, - link.domain - ), - } - ) - : this.hass.localize( - `ui.panel.config.devices.download_diagnostics` - )} - - - ` - ); - } - } - - private _renderDeleteButtons() { - const device = this._device(this.deviceId, this.devices); - - if (!device) { - return; - } - - const buttons: TemplateResult[] = []; - this._integrations(device, this.entries).forEach((entry) => { - if (entry.state !== "loaded" || !entry.supports_remove_device) { - return; - } - buttons.push(html` - - ${buttons.length > 1 - ? this.hass.localize( - `ui.panel.config.devices.delete_device_integration`, - { - integration: domainToName(this.hass.localize, entry.domain), - } - ) - : this.hass.localize(`ui.panel.config.devices.delete_device`)} - - `); - }); - - if (buttons.length > 0) { - this._deleteButtons = buttons; - } - } - - private async _confirmDeleteEntry(e: MouseEvent): Promise { - const entryId = (e.currentTarget as any).entryId; - - const confirmed = await showConfirmationDialog(this, { - text: this.hass.localize("ui.panel.config.devices.confirm_delete"), - }); - - if (!confirmed) { - return; - } - - await removeConfigEntryFromDevice(this.hass!, this.deviceId, entryId); + this._deviceActions = []; + this._getDiagnosticButtons(this._diagnosticDownloadLinks); + this._getDeleteActions(); + this._getDeviceActions(); } protected firstUpdated(changedProps) { @@ -381,16 +278,19 @@ export class HaConfigDevicePage extends LitElement { : undefined; const area = this._computeArea(this.areas, device); - const configurationUrlIsHomeAssistant = - device.configuration_url?.startsWith("homeassistant://") || false; - - const configurationUrl = configurationUrlIsHomeAssistant - ? device.configuration_url!.replace("homeassistant://", "/") - : device.configuration_url; - const deviceInfo: TemplateResult[] = []; const deviceAlerts: TemplateResult[] = []; + const actions = [...(this._deviceActions || [])]; + if (Array.isArray(this._diagnosticDownloadLinks)) { + actions.push(...this._diagnosticDownloadLinks); + } + if (this._deleteButtons) { + actions.push(...this._deleteButtons); + } + + const firstDeviceAction = actions.shift(); + if (device.disabled_by) { deviceInfo.push( html` @@ -408,54 +308,19 @@ export class HaConfigDevicePage extends LitElement { )} ${device.disabled_by === "user" - ? html`
- - ${this.hass.localize("ui.common.enable")} - -
` + ? html` +
+ + ${this.hass.localize("ui.common.enable")} + +
+ ` : ""} ` ); } - const deviceActions: (TemplateResult | string)[] = []; - - if (configurationUrl) { - deviceActions.push(html` - - - ${this.hass.localize( - `ui.panel.config.devices.open_configuration_url_${ - device.entry_type || "device" - }` - )} - - - - `); - } - - this._renderIntegrationInfo( - device, - integrations, - deviceInfo, - deviceActions, - deviceAlerts - ); - - if (Array.isArray(this._diagnosticDownloadLinks)) { - deviceActions.push(...this._diagnosticDownloadLinks); - } - if (this._deleteButtons) { - deviceActions.push(...this._deleteButtons); - } + this._renderIntegrationInfo(device, integrations, deviceInfo, deviceAlerts); return html`
${ @@ -562,57 +423,70 @@ export class HaConfigDevicePage extends LitElement { > ${deviceInfo} ${ - deviceActions.length + firstDeviceAction || actions.length ? html`
- ${deviceActions} + + + ${actions.length + ? html` + + + ${actions.map( + (deviceAction) => html` + + + ${deviceAction.label} + ${deviceAction.trailingIcon + ? html` + + ` + : ""} + + + ` + )} + + ` + : ""}
` : "" } -
-
- ${["control", "sensor", "config", "diagnostic"].map((category) => - // Make sure we render controls if no other cards will be rendered - entitiesByCategory[category].length > 0 || - (entities.length === 0 && category === "control") - ? html` - - - ` - : "" - )} - ${ - isComponentLoaded(this.hass, "logbook") - ? html` - -

- ${this.hass.localize("panel.logbook")} -

- -
- ` - : "" - } -
-
+ ${ isComponentLoaded(this.hass, "automation") ? html` @@ -881,12 +755,227 @@ export class HaConfigDevicePage extends LitElement { ` : "" } +
+
+ ${["control", "sensor", "config", "diagnostic"].map((category) => + // Make sure we render controls if no other cards will be rendered + entitiesByCategory[category].length > 0 || + (entities.length === 0 && category === "control") + ? html` + + + ` + : "" + )} +
+
+ ${ + isComponentLoaded(this.hass, "logbook") + ? html` + +

+ ${this.hass.localize("panel.logbook")} +

+ +
+ ` + : "" + }
`; } + private async _getDiagnosticButtons(requestId: number): Promise { + if (!isComponentLoaded(this.hass, "diagnostics")) { + return; + } + + const device = this._device(this.deviceId, this.devices); + + if (!device) { + return; + } + + let links = await Promise.all( + this._integrations(device, this.entries).map( + async (entry): Promise => { + if (entry.state !== "loaded") { + return false; + } + let info: DiagnosticInfo; + try { + info = await fetchDiagnosticHandler(this.hass, entry.domain); + } catch (err: any) { + if (err.code === "not_found") { + return false; + } + throw err; + } + + if (!info.handlers.device && !info.handlers.config_entry) { + return false; + } + return { + link: info.handlers.device + ? getDeviceDiagnosticsDownloadUrl(entry.entry_id, this.deviceId) + : getConfigEntryDiagnosticsDownloadUrl(entry.entry_id), + domain: entry.domain, + }; + } + ) + ); + + links = links.filter(Boolean); + + if (this._diagnosticDownloadLinks !== requestId) { + return; + } + if (links.length > 0) { + this._diagnosticDownloadLinks = ( + links as { link: string; domain: string }[] + ).map((link) => ({ + href: link.link, + action: (ev) => this._signUrl(ev), + label: + links.length > 1 + ? this.hass.localize( + `ui.panel.config.devices.download_diagnostics_integration`, + { + integration: domainToName(this.hass.localize, link.domain), + } + ) + : this.hass.localize( + `ui.panel.config.devices.download_diagnostics` + ), + })); + } + } + + private _getDeleteActions() { + const device = this._device(this.deviceId, this.devices); + + if (!device) { + return; + } + + const buttons: DeviceAction[] = []; + this._integrations(device, this.entries).forEach((entry) => { + if (entry.state !== "loaded" || !entry.supports_remove_device) { + return; + } + buttons.push({ + action: async () => { + const confirmed = await showConfirmationDialog(this, { + text: this.hass.localize("ui.panel.config.devices.confirm_delete"), + }); + + if (!confirmed) { + return; + } + + await removeConfigEntryFromDevice( + this.hass!, + this.deviceId, + entry.entry_id + ); + }, + classes: "warning", + label: + buttons.length > 1 + ? this.hass.localize( + `ui.panel.config.devices.delete_device_integration`, + { + integration: domainToName(this.hass.localize, entry.domain), + } + ) + : this.hass.localize(`ui.panel.config.devices.delete_device`), + }); + }); + + if (buttons.length > 0) { + this._deleteButtons = buttons; + } + } + + private async _getDeviceActions() { + const device = this._device(this.deviceId, this.devices); + + if (!device) { + return; + } + + const deviceActions: DeviceAction[] = []; + + const configurationUrlIsHomeAssistant = + device.configuration_url?.startsWith("homeassistant://") || false; + + const configurationUrl = configurationUrlIsHomeAssistant + ? device.configuration_url!.replace("homeassistant://", "/") + : device.configuration_url; + + if (configurationUrl) { + deviceActions.push({ + href: configurationUrl, + label: this.hass.localize( + `ui.panel.config.devices.open_configuration_url_${ + device.entry_type || "device" + }` + ), + trailingIcon: mdiOpenInNew, + }); + } + + const domains = this._integrations(device, this.entries).map( + (int) => int.domain + ); + + if (domains.includes("mqtt")) { + const mqtt = await import( + "./device-detail/integration-elements/mqtt/device-actions" + ); + const actions = mqtt.getMQTTDeviceActions(this, device); + deviceActions.push(...actions); + } + if (domains.includes("zha")) { + const zha = await import( + "./device-detail/integration-elements/zha/device-actions" + ); + const actions = await zha.getZHADeviceActions(this, this.hass, device); + deviceActions.push(...actions); + } + if (domains.includes("zwave_js")) { + const zwave = await import( + "./device-detail/integration-elements/zwave_js/device-actions" + ); + const actions = await zwave.getZwaveDeviceActions( + this, + this.hass, + device + ); + deviceActions.push(...actions); + } + + this._deviceActions = deviceActions; + } + private _computeEntityName(entity: EntityRegistryEntry) { if (entity.name) { return entity.name; @@ -935,23 +1024,10 @@ export class HaConfigDevicePage extends LitElement { device: DeviceRegistryEntry, integrations: ConfigEntry[], deviceInfo: TemplateResult[], - deviceActions: (string | TemplateResult)[], deviceAlerts: TemplateResult[] ) { const domains = integrations.map((int) => int.domain); - if (domains.includes("mqtt")) { - import( - "./device-detail/integration-elements/mqtt/ha-device-actions-mqtt" - ); - deviceActions.push(html` - - `); - } if (domains.includes("zha")) { - import("./device-detail/integration-elements/zha/ha-device-actions-zha"); import("./device-detail/integration-elements/zha/ha-device-info-zha"); deviceInfo.push(html` `); - deviceActions.push(html` - - `); } if (domains.includes("zwave_js")) { import( @@ -973,9 +1043,6 @@ export class HaConfigDevicePage extends LitElement { import( "./device-detail/integration-elements/zwave_js/ha-device-info-zwave_js" ); - import( - "./device-detail/integration-elements/zwave_js/ha-device-actions-zwave_js" - ); deviceAlerts.push(html` `); - deviceActions.push(html` - - `); } } @@ -1132,8 +1193,7 @@ export class HaConfigDevicePage extends LitElement { } private async _signUrl(ev) { - const anchor = ev.target.closest("a"); - ev.preventDefault(); + const anchor = ev.currentTarget.closest("a"); const signedUrl = await getSignedPath( this.hass, anchor.getAttribute("href") @@ -1141,6 +1201,16 @@ export class HaConfigDevicePage extends LitElement { fileDownload(signedUrl.path); } + private _deviceActionClicked(ev) { + if (!ev.currentTarget.action) { + return; + } + + ev.preventDefault(); + + (ev.currentTarget as any).action(ev); + } + static get styles(): CSSResultGroup { return [ haStyle, @@ -1171,9 +1241,6 @@ export class HaConfigDevicePage extends LitElement { padding: 16px; } - .show-more { - } - h1 { margin: 0; font-family: var(--paper-font-headline_-_font-family); @@ -1282,7 +1349,19 @@ export class HaConfigDevicePage extends LitElement { :host([narrow]) ha-logbook { height: 235px; } + + .card-actions { + display: flex; + justify-content: space-between; + align-items: center; + } `, ]; } } + +declare global { + interface HTMLElementTagNameMap { + "ha-config-device-page": HaConfigDevicePage; + } +} diff --git a/src/panels/logbook/ha-logbook-renderer.ts b/src/panels/logbook/ha-logbook-renderer.ts index 69b7bed3cf..6e382b3cce 100644 --- a/src/panels/logbook/ha-logbook-renderer.ts +++ b/src/panels/logbook/ha-logbook-renderer.ts @@ -556,10 +556,6 @@ class HaLogbookRenderer extends LitElement { padding: 0 16px; } - .narrow .date { - padding: 0 8px; - } - .rtl .date { direction: rtl; } @@ -590,6 +586,12 @@ class HaLogbookRenderer extends LitElement { a { color: var(--primary-color); + text-decoration: none; + } + + button.link { + color: var(--paper-item-icon-color); + text-decoration: none; } .container { @@ -607,7 +609,6 @@ class HaLogbookRenderer extends LitElement { .narrow .entry { line-height: 1.5; - padding: 8px; } .narrow .icon-message state-badge { diff --git a/src/resources/styles.ts b/src/resources/styles.ts index 92f141c829..62af21f471 100644 --- a/src/resources/styles.ts +++ b/src/resources/styles.ts @@ -158,6 +158,7 @@ export const buttonLinkStyle = css` text-align: left; text-decoration: underline; cursor: pointer; + outline: none; } `;