From e81f596d76fbb02989e8fdfcecc763d3e82b0680 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Tue, 14 Feb 2023 13:04:18 +0100 Subject: [PATCH] Add restart dialog (#15337) --- src/dialogs/restart/dialog-restart.ts | 382 ++++++++++++++++++ src/dialogs/restart/show-dialog-restart.ts | 14 + .../core/ha-config-system-navigation.ts | 53 +-- .../config/hardware/ha-config-hardware.ts | 141 ++----- src/translations/en.json | 58 ++- 5 files changed, 483 insertions(+), 165 deletions(-) create mode 100644 src/dialogs/restart/dialog-restart.ts create mode 100644 src/dialogs/restart/show-dialog-restart.ts diff --git a/src/dialogs/restart/dialog-restart.ts b/src/dialogs/restart/dialog-restart.ts new file mode 100644 index 0000000000..156fb3b826 --- /dev/null +++ b/src/dialogs/restart/dialog-restart.ts @@ -0,0 +1,382 @@ +import "@material/mwc-list/mwc-list"; +import { mdiAutoFix, mdiPower, mdiPowerCycle, mdiRefresh } from "@mdi/js"; +import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { isComponentLoaded } from "../../common/config/is_component_loaded"; +import { fireEvent } from "../../common/dom/fire_event"; +import { shouldHandleRequestSelectedEvent } from "../../common/mwc/handle-request-selected-event"; +import "../../components/ha-circular-progress"; +import { createCloseHeading } from "../../components/ha-dialog"; +import "../../components/ha-list-item"; +import { + extractApiErrorMessage, + ignoreSupervisorError, +} from "../../data/hassio/common"; +import { + fetchHassioHostInfo, + HassioHostInfo, + rebootHost, + shutdownHost, +} from "../../data/hassio/host"; +import { haStyle, haStyleDialog } from "../../resources/styles"; +import { HomeAssistant } from "../../types"; +import { showToast } from "../../util/toast"; +import { + showAlertDialog, + showConfirmationDialog, +} from "../generic/show-dialog-box"; + +@customElement("dialog-restart") +class DialogRestart extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @state() private _open = false; + + @state() + private _loadingHostInfo = false; + + @state() + private _hostInfo?: HassioHostInfo; + + public async showDialog(): Promise { + const isHassioLoaded = isComponentLoaded(this.hass, "hassio"); + + this._open = true; + + if (isHassioLoaded && !this._hostInfo) { + this._loadingHostInfo = true; + try { + this._hostInfo = await fetchHassioHostInfo(this.hass); + } catch (_err) { + // Do nothing + } finally { + this._loadingHostInfo = false; + } + } + + const showReload = this.hass.userData?.showAdvanced; + const showRebootShutdown = !!this._hostInfo; + + // Present restart core dialog if no host actions and not advanced mode as it's the only option + if (!showReload && !showRebootShutdown) { + this._open = false; + this._showRestartDialog().then(() => this.closeDialog()); + return; + } + + await this.updateComplete; + } + + public closeDialog(): void { + this._open = false; + this._loadingHostInfo = false; + fireEvent(this, "dialog-closed", { dialog: this.localName }); + } + + protected render(): TemplateResult { + if (!this._open) { + return html``; + } + + const showReload = this.hass.userData?.showAdvanced; + const showRebootShutdown = !!this._hostInfo; + + return html` + + ${this._loadingHostInfo + ? html` +
+ +
+ ` + : html` + + ${showReload + ? html` + +
+ +
+ + ${this.hass.localize( + "ui.dialogs.restart.reload.title" + )} + + + ${this.hass.localize( + "ui.dialogs.restart.reload.description" + )} + +
+ ` + : null} + +
+ +
+ + ${this.hass.localize("ui.dialogs.restart.restart.title")} + + + ${this.hass.localize( + "ui.dialogs.restart.restart.description" + )} + +
+ ${showRebootShutdown + ? html` +
+

+ ${this.hass.localize( + "ui.dialogs.restart.advanced_options" + )} +

+ +
+ +
+ + ${this.hass.localize( + "ui.dialogs.restart.reboot.title" + )} + + + ${this.hass.localize( + "ui.dialogs.restart.reboot.description" + )} + +
+ +
+ +
+ + ${this.hass.localize( + "ui.dialogs.restart.shutdown.title" + )} + + + ${this.hass.localize( + "ui.dialogs.restart.shutdown.description" + )} + +
+ ` + : null} +
+ `} +
+ `; + } + + private async _reload(ev) { + if (!shouldHandleRequestSelectedEvent(ev)) { + return; + } + + this.closeDialog(); + + showToast(this, { + message: this.hass.localize("ui.dialogs.restart.reload.reloading"), + duration: 1000, + }); + + await this.hass.callService("homeassistant", "reload_all"); + } + + private async _restart(ev) { + if (!shouldHandleRequestSelectedEvent(ev)) { + return; + } + this._showRestartDialog(); + } + + private async _showRestartDialog() { + const confirmed = await showConfirmationDialog(this, { + title: this.hass.localize("ui.dialogs.restart.restart.confirm_title"), + text: this.hass.localize( + "ui.dialogs.restart.restart.confirm_description" + ), + confirmText: this.hass.localize( + "ui.dialogs.restart.restart.confirm_action" + ), + destructive: true, + }); + + if (!confirmed) { + return; + } + + this.closeDialog(); + + try { + await this.hass.callService("homeassistant", "restart"); + } catch (err: any) { + showAlertDialog(this, { + title: this.hass.localize("ui.dialogs.restart.restart.failed"), + text: err.message, + }); + } + } + + private async _hostReboot(ev): Promise { + if (!shouldHandleRequestSelectedEvent(ev)) { + return; + } + const confirmed = await showConfirmationDialog(this, { + title: this.hass.localize("ui.dialogs.restart.reboot.confirm_title"), + text: this.hass.localize("ui.dialogs.restart.reboot.confirm_description"), + confirmText: this.hass.localize( + "ui.dialogs.restart.reboot.confirm_action" + ), + destructive: true, + }); + + if (!confirmed) { + return; + } + + this.closeDialog(); + + showToast(this, { + message: this.hass.localize("ui.dialogs.restart.reboot.rebooting"), + duration: 0, + }); + + try { + await rebootHost(this.hass); + } catch (err: any) { + // Ignore connection errors, these are all expected + if (this.hass.connection.connected && !ignoreSupervisorError(err)) { + showAlertDialog(this, { + title: this.hass.localize("ui.dialogs.restart.reboot.failed"), + text: extractApiErrorMessage(err), + }); + } + } + } + + private async _hostShutdown(ev): Promise { + if (!shouldHandleRequestSelectedEvent(ev)) { + return; + } + const confirmed = await showConfirmationDialog(this, { + title: this.hass.localize("ui.dialogs.restart.shutdown.confirm_title"), + text: this.hass.localize( + "ui.dialogs.restart.shutdown.confirm_description" + ), + confirmText: this.hass.localize( + "ui.dialogs.restart.shutdown.confirm_action" + ), + destructive: true, + }); + + if (!confirmed) { + return; + } + + this.closeDialog(); + + showToast(this, { + message: this.hass.localize("ui.dialogs.restart.shutdown.shutting_down"), + duration: 0, + }); + + try { + await shutdownHost(this.hass); + } catch (err: any) { + // Ignore connection errors, these are all expected + if (this.hass.connection.connected && !ignoreSupervisorError(err)) { + showAlertDialog(this, { + title: this.hass.localize("ui.dialogs.restart.shutdown.failed"), + text: extractApiErrorMessage(err), + }); + } + } + } + + static get styles(): CSSResultGroup { + return [ + haStyle, + haStyleDialog, + css` + ha-dialog { + --dialog-content-padding: 0; + } + .icon-background { + border-radius: 50%; + color: #fff; + } + .reload { + background-color: #5f8a49; + } + .restart { + background-color: #ffd500; + color: #665500; + } + .reboot { + background-color: #ba1b1b; + color: #fff; + } + .shutdown { + background-color: #0b1d29; + color: #fff; + } + .divider { + height: 1px; + background-color: var(--divider-color); + } + .section { + font-weight: 500; + font-size: 14px; + line-height: 20px; + margin: 8px 0 4px 0; + padding-left: var(--mdc-list-side-padding, 20px); + padding-right: var(--mdc-list-side-padding, 20px); + } + .loader { + display: flex; + align-items: center; + justify-content: center; + padding: 24px; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "dialog-restart": DialogRestart; + } +} diff --git a/src/dialogs/restart/show-dialog-restart.ts b/src/dialogs/restart/show-dialog-restart.ts new file mode 100644 index 0000000000..794209818e --- /dev/null +++ b/src/dialogs/restart/show-dialog-restart.ts @@ -0,0 +1,14 @@ +import { fireEvent } from "../../common/dom/fire_event"; + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface RestartDialogParams {} + +export const loadRestartDialog = () => import("./dialog-restart"); + +export const showRestartDialog = (element: HTMLElement): void => { + fireEvent(element, "show-dialog", { + dialogTag: "dialog-restart", + dialogImport: loadRestartDialog, + dialogParams: {}, + }); +}; diff --git a/src/panels/config/core/ha-config-system-navigation.ts b/src/panels/config/core/ha-config-system-navigation.ts index 6bab7dfb82..54a22a9988 100644 --- a/src/panels/config/core/ha-config-system-navigation.ts +++ b/src/panels/config/core/ha-config-system-navigation.ts @@ -1,3 +1,4 @@ +import { mdiPower } from "@mdi/js"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; import { canShowPage } from "../../../common/config/can_show_page"; @@ -5,6 +6,7 @@ import { isComponentLoaded } from "../../../common/config/is_component_loaded"; import { relativeTime } from "../../../common/datetime/relative_time"; import { blankBeforePercent } from "../../../common/translations/blank_before_percent"; import "../../../components/ha-card"; +import "../../../components/ha-icon-button"; import "../../../components/ha-navigation-list"; import "../../../components/ha-tip"; import { BackupContent, fetchBackupInfo } from "../../../data/backup"; @@ -17,10 +19,7 @@ import { HassioHassOSInfo, HassioHostInfo, } from "../../../data/hassio/host"; -import { - showAlertDialog, - showConfirmationDialog, -} from "../../../dialogs/generic/show-dialog-box"; +import { showRestartDialog } from "../../../dialogs/restart/show-dialog-restart"; import "../../../layouts/hass-subpage"; import { haStyle } from "../../../resources/styles"; import type { HomeAssistant } from "../../../types"; @@ -121,13 +120,14 @@ class HaConfigSystemNavigation extends LitElement { back-path="/config" .header=${this.hass.localize("ui.panel.config.dashboard.system.main")} > - + @click=${this._showRestartDialog} + > { - this.hass.callService("homeassistant", "restart").catch((reason) => { - showAlertDialog(this, { - title: this.hass.localize( - "ui.panel.config.system_dashboard.restart_error" - ), - text: reason.message, - }); - }); - }, - destructive: true, - }); - } - private async _fetchBackupInfo(isHassioLoaded: boolean) { const backups: BackupContent[] | HassioBackup[] = isHassioLoaded ? await fetchHassioBackups(this.hass) @@ -238,6 +213,10 @@ class HaConfigSystemNavigation extends LitElement { this._externalAccess = this.hass.config.external_url !== null; } + private async _showRestartDialog() { + showRestartDialog(this); + } + static get styles(): CSSResultGroup { return [ haStyle, @@ -269,6 +248,14 @@ class HaConfigSystemNavigation extends LitElement { padding-bottom: 0; } + .restart-section { + display: flex; + align-items: center; + flex-direction: column; + justify-content: center; + margin-bottom: 24px; + } + @media all and (max-width: 600px) { ha-card { border-width: 1px 0; diff --git a/src/panels/config/hardware/ha-config-hardware.ts b/src/panels/config/hardware/ha-config-hardware.ts index b4578716ba..bea75f5176 100644 --- a/src/panels/config/hardware/ha-config-hardware.ts +++ b/src/panels/config/hardware/ha-config-hardware.ts @@ -1,6 +1,6 @@ import "@material/mwc-list/mwc-list"; import "@material/mwc-list/mwc-list-item"; -import { mdiDotsVertical } from "@mdi/js"; +import { mdiPower } from "@mdi/js"; import type { ChartOptions } from "chart.js"; import { UnsubscribeFunc } from "home-assistant-js-websocket"; import { css, html, LitElement, PropertyValues, TemplateResult } from "lit"; @@ -13,9 +13,9 @@ import { blankBeforePercent } from "../../../common/translations/blank_before_pe import "../../../components/buttons/ha-progress-button"; import "../../../components/chart/ha-chart-base"; import "../../../components/ha-alert"; -import "../../../components/ha-button-menu"; import "../../../components/ha-card"; import "../../../components/ha-clickable-list-item"; +import "../../../components/ha-icon-button"; import "../../../components/ha-icon-next"; import "../../../components/ha-settings-row"; import { @@ -27,31 +27,19 @@ import { HardwareInfo, SystemStatusStreamMessage, } from "../../../data/hardware"; -import { - extractApiErrorMessage, - ignoreSupervisorError, -} from "../../../data/hassio/common"; import { fetchHassioHassOsInfo, - fetchHassioHostInfo, HassioHassOSInfo, - HassioHostInfo, - rebootHost, - shutdownHost, } from "../../../data/hassio/host"; import { scanUSBDevices } from "../../../data/usb"; import { showOptionsFlowDialog } from "../../../dialogs/config-flow/show-dialog-options-flow"; -import { - showAlertDialog, - showConfirmationDialog, -} from "../../../dialogs/generic/show-dialog-box"; +import { showRestartDialog } from "../../../dialogs/restart/show-dialog-restart"; import "../../../layouts/hass-subpage"; import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; import { DEFAULT_PRIMARY_COLOR } from "../../../resources/ha-style"; import { haStyle } from "../../../resources/styles"; import type { HomeAssistant } from "../../../types"; import { hardwareBrandsUrl } from "../../../util/brands-url"; -import { showToast } from "../../../util/toast"; import { showhardwareAvailableDialog } from "./show-dialog-hardware-available"; const DATASAMPLES = 60; @@ -75,8 +63,6 @@ class HaConfigHardware extends SubscribeMixin(LitElement) { @state() private _OSData?: HassioHassOSInfo; - @state() private _hostData?: HassioHostInfo; - @state() private _hardwareInfo?: HardwareInfo; @state() private _chartOptions?: ChartOptions; @@ -273,32 +259,16 @@ class HaConfigHardware extends SubscribeMixin(LitElement) { .header=${this.hass.localize("ui.panel.config.hardware.caption")} > ${isComponentLoaded(this.hass, "hassio") - ? html` + ? html` - ${this.hass.localize( - "ui.panel.config.hardware.available_hardware.title" - )} - ${this._hostData - ? html` - ${this.hass.localize( - "ui.panel.config.hardware.reboot_host" - )} - ${this.hass.localize( - "ui.panel.config.hardware.shutdown_host" - )} - ` - : ""} - ` + ` : ""} ${this._error ? html` @@ -367,6 +337,15 @@ class HaConfigHardware extends SubscribeMixin(LitElement) { "ui.panel.config.hardware.configure" )} + ${isComponentLoaded(this.hass, "hassio") + ? html` + + ${this.hass.localize( + "ui.panel.config.hardware.available_hardware.title" + )} + + ` + : null} ` : ""} @@ -475,10 +454,6 @@ class HaConfigHardware extends SubscribeMixin(LitElement) { if (isHassioLoaded && !this._hardwareInfo?.hardware.length) { this._OSData = await fetchHassioHassOsInfo(this.hass); } - - if (isHassioLoaded) { - this._hostData = await fetchHassioHostInfo(this.hass); - } } catch (err: any) { this._error = err.message || err; } @@ -496,72 +471,8 @@ class HaConfigHardware extends SubscribeMixin(LitElement) { showhardwareAvailableDialog(this); } - private async _hostReboot(): Promise { - const confirmed = await showConfirmationDialog(this, { - title: this.hass.localize("ui.panel.config.hardware.reboot_host_title"), - text: this.hass.localize("ui.panel.config.hardware.reboot_host_text"), - confirmText: this.hass.localize("ui.panel.config.hardware.reboot"), - dismissText: this.hass.localize("ui.common.cancel"), - destructive: true, - }); - - if (!confirmed) { - return; - } - - showToast(this, { - message: this.hass.localize("ui.panel.config.hardware.rebooting_host"), - duration: 0, - }); - - try { - await rebootHost(this.hass); - } catch (err: any) { - // Ignore connection errors, these are all expected - if (this.hass.connection.connected && !ignoreSupervisorError(err)) { - showAlertDialog(this, { - title: this.hass.localize( - "ui.panel.config.hardware.failed_to_reboot_host" - ), - text: extractApiErrorMessage(err), - }); - } - } - } - - private async _hostShutdown(): Promise { - const confirmed = await showConfirmationDialog(this, { - title: this.hass.localize("ui.panel.config.hardware.shutdown_host_title"), - text: this.hass.localize("ui.panel.config.hardware.shutdown_host_text"), - confirmText: this.hass.localize("ui.panel.config.hardware.shutdown"), - dismissText: this.hass.localize("ui.common.cancel"), - destructive: true, - }); - - if (!confirmed) { - return; - } - - showToast(this, { - message: this.hass.localize( - "ui.panel.config.hardware.host_shutting_down" - ), - duration: 0, - }); - - try { - await shutdownHost(this.hass); - } catch (err: any) { - // Ignore connection errors, these are all expected - if (this.hass.connection.connected && !ignoreSupervisorError(err)) { - showAlertDialog(this, { - title: this.hass.localize( - "ui.panel.config.hardware.failed_to_shutdown_host" - ), - text: extractApiErrorMessage(err), - }); - } - } + private async _showRestartDialog() { + showRestartDialog(this); } static styles = [ @@ -587,10 +498,6 @@ class HaConfigHardware extends SubscribeMixin(LitElement) { flex-direction: column; padding: 16px; } - ha-button-menu { - color: var(--secondary-text-color); - --mdc-menu-min-width: 200px; - } .primary-text { font-size: 16px; @@ -620,6 +527,10 @@ class HaConfigHardware extends SubscribeMixin(LitElement) { height: 48px; padding: 8px 16px; } + .card-actions { + display: flex; + justify-content: space-between; + } `, ]; } diff --git a/src/translations/en.json b/src/translations/en.json index b5f4750a93..ec48e92cde 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -1008,6 +1008,41 @@ "aliases_description": "Aliases are alternative names used in voice assistants to refer to this entity." } }, + "restart": { + "heading": "Restart Home Assistant", + "advanced_options": "Advanced options", + "reload": { + "title": "Quick reload", + "description": "Load new YAML configurations without a restart.", + "reloading": "Reloading configuration" + }, + "restart": { + "title": "Restart Home Assistant", + "description": "Interrupts all running automations and scripts.", + "confirm_title": "Restart?", + "confirm_description": "This will interrupt all running automations and scripts.", + "confirm_action": "Restart Home Assistant", + "failed": "Failed to restart Home Assistant" + }, + "reboot": { + "title": "Reboot system", + "description": "Restart the system running Home Assistant and all Add-ons.", + "confirm_title": "Reboot system?", + "confirm_description": "This will reboot the complete system which includes Home Assistant and all the Add-ons.", + "confirm_action": "Reboot", + "rebooting": "Rebooting system", + "failed": "Failed to reboot system" + }, + "shutdown": { + "title": "Shutdown system", + "description": "Shutdown the system running Home Assistant and all Add-ons.", + "confirm_title": "Shutdown system?", + "confirm_description": "This will shutdown the complete system whitch includes Home Assistant and all Add-ons.", + "confirm_action": "Shutdown", + "shutting_down": "Shutting down system", + "failed": "Failed to shutdown system" + } + }, "aliases": { "heading": "{name} aliases", "remove_alias": "Remove alias {number}", @@ -1742,24 +1777,16 @@ "id": "ID", "attributes": "Attributes" }, + "reboot_moved_title": "Reboot and shutdown moved", + "reboot_moved_description": "Reboot and shutdown actions has been moved to system page.", + "reboot_moved_link": "Go to system page.", "processor": "Processor", "memory": "Memory", - "rebooting_host": "Rebooting system", - "reboot": "Reboot", - "reboot_host": "Reboot system", - "reboot_host_title": "Reboot system?", - "reboot_host_text": "This will reboot the complete system which includes the Core and all Add-ons.", - "failed_to_reboot_host": "Failed to reboot system", - "host_shutting_down": "system shutting down", - "shutdown": "Shutdown", - "shutdown_host": "Shutdown system", - "shutdown_host_title": "Shutdown system?", - "shutdown_host_text": "This will shutdown the complete system which includes the Core and all Add-ons.", - "failed_to_shutdown_host": "Failed to shutdown system", "board": "Board", "documentation": "Documentation", "configure": "Configure", - "documentation_description": "Find extra information about your device" + "documentation_description": "Find extra information about your device", + "restart_homeassistant": "[%key:ui::panel::config::system_dashboard::restart_homeassistant%]" }, "info": { "caption": "About", @@ -3734,10 +3761,7 @@ "integration_start_time": "Integration Startup Time" }, "system_dashboard": { - "confirm_restart_text": "This will stop all your active dashboards, automations and scripts.", - "confirm_restart_title": "Restart Home Assistant?", - "restart_homeassistant_short": "Restart", - "restart_error": "Failed to restart Home Assistant" + "restart_homeassistant": "Restart Home Assistant" } }, "lovelace": {