From 9ea8e13c8756f539b958334b121d5562a63a7a79 Mon Sep 17 00:00:00 2001 From: Ludeeus Date: Fri, 4 Mar 2022 09:45:20 +0000 Subject: [PATCH] Use the update integration to provide updates --- .../update-available/update-available-card.ts | 7 +- src/data/update.ts | 37 +++ src/dialogs/update-dialog/ha-update-dialog.ts | 211 ++++++++++++++++++ .../update-dialog/show-ha-update-dialog.ts | 18 ++ .../config/dashboard/ha-config-dashboard.ts | 120 +++++----- .../config/dashboard/ha-config-updates.ts | 124 ++++++---- src/panels/config/ha-panel-config.ts | 56 +++-- src/translations/en.json | 15 +- 8 files changed, 452 insertions(+), 136 deletions(-) create mode 100644 src/data/update.ts create mode 100644 src/dialogs/update-dialog/ha-update-dialog.ts create mode 100644 src/dialogs/update-dialog/show-ha-update-dialog.ts diff --git a/hassio/src/update-available/update-available-card.ts b/hassio/src/update-available/update-available-card.ts index a5a2bc81a2..e26555df6f 100644 --- a/hassio/src/update-available/update-available-card.ts +++ b/hassio/src/update-available/update-available-card.ts @@ -45,7 +45,6 @@ import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box"; import "../../../src/layouts/hass-loading-screen"; import "../../../src/layouts/hass-subpage"; import "../../../src/layouts/hass-tabs-subpage"; -import { SUPERVISOR_UPDATE_NAMES } from "../../../src/panels/config/dashboard/ha-config-updates"; import { HomeAssistant, Route } from "../../../src/types"; import { addonArchIsSupported, extractChangelog } from "../util/addon"; @@ -57,6 +56,12 @@ declare global { type updateType = "os" | "supervisor" | "core" | "addon"; +const SUPERVISOR_UPDATE_NAMES = { + core: "Home Assistant Core", + os: "Home Assistant Operating System", + supervisor: "Home Assistant Supervisor", +}; + const changelogUrl = ( entry: updateType, version: string diff --git a/src/data/update.ts b/src/data/update.ts new file mode 100644 index 0000000000..4a6759e4b9 --- /dev/null +++ b/src/data/update.ts @@ -0,0 +1,37 @@ +import { HomeAssistant } from "../types"; + +export interface UpdateDescription { + identifier: string; + name: string; + domain: string; + current_version: string; + available_version: string; + changelog_content: string | null; + changelog_url: string | null; + icon_url: string | null; + supports_backup: boolean; +} + +export interface SkipUpdateParams { + domain: string; + version: string; + identifier: string; +} + +export interface PerformUpdateParams extends SkipUpdateParams { + backup?: boolean; +} + +export const fetchUpdateInfo = ( + hass: HomeAssistant +): Promise => hass.callWS({ type: "update/info" }); + +export const skipUpdate = ( + hass: HomeAssistant, + params: SkipUpdateParams +): Promise => hass.callWS({ type: "update/skip", ...params }); + +export const performUpdate = ( + hass: HomeAssistant, + params: PerformUpdateParams +): Promise => hass.callWS({ type: "update/update", ...params }); diff --git a/src/dialogs/update-dialog/ha-update-dialog.ts b/src/dialogs/update-dialog/ha-update-dialog.ts new file mode 100644 index 0000000000..bdd9ae608b --- /dev/null +++ b/src/dialogs/update-dialog/ha-update-dialog.ts @@ -0,0 +1,211 @@ +import "@material/mwc-button/mwc-button"; +import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { fireEvent } from "../../common/dom/fire_event"; +import { computeRTL } from "../../common/util/compute_rtl"; +import "../../components/ha-alert"; +import "../../components/ha-checkbox"; +import "../../components/ha-circular-progress"; +import { createCloseHeading } from "../../components/ha-dialog"; +import "../../components/ha-faded"; +import "../../components/ha-formfield"; +import "../../components/ha-icon-button"; +import "../../components/ha-markdown"; +import { + performUpdate, + skipUpdate, + UpdateDescription, +} from "../../data/update"; +import { haStyleDialog } from "../../resources/styles"; +import type { HomeAssistant } from "../../types"; +import { UpdateDialogParams } from "./show-ha-update-dialog"; + +@customElement("ha-update-dialog") +export class HaUpdateDialog extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @state() private _opened = false; + + @state() private _updating = false; + + @state() private _error?: string; + + @state() private _update!: UpdateDescription; + + _refreshCallback!: () => void; + + public async showDialog(dialogParams: UpdateDialogParams): Promise { + this._opened = true; + this._update = dialogParams.update; + this._refreshCallback = dialogParams.refreshCallback; + } + + public async closeDialog(): Promise { + this._opened = false; + fireEvent(this, "dialog-closed", { dialog: this.localName }); + } + + protected render(): TemplateResult { + if (!this._opened) { + return html``; + } + return html` + +
+ ${this._error + ? html` + ${this._error} + ` + : ""} + ${!this._updating + ? html` + ${this._update.changelog_content + ? html` + + + + + ` + : ""} + ${this._update.changelog_url + ? html` + Full changelog + ` + : ""} +

+ ${this.hass.localize( + "ui.panel.config.updates.dialog.description", + { + name: this._update.name, + version: this._update.current_version, + newest_version: this._update.available_version, + } + )} +

+ ${this._update.supports_backup + ? html` + + + + ` + : ""} + ` + : html` + +

+ ${this.hass.localize( + "ui.panel.config.updates.dialog.updating", + { + name: this._update.name, + version: this._update.available_version, + } + )} +

`} +
+ ${!this._updating + ? html` + + ${this.hass.localize("ui.common.skip")} + + + ${this.hass.localize("ui.panel.config.updates.dialog.update")} + + ` + : ""} +
+ `; + } + + get _shouldCreateBackup(): boolean { + if (!this._update.supports_backup) { + return false; + } + const checkbox = this.shadowRoot?.querySelector("ha-checkbox"); + if (checkbox) { + return checkbox.checked; + } + return true; + } + + private async _performUpdate() { + this._error = undefined; + this._updating = true; + try { + await performUpdate(this.hass, { + domain: this._update.domain, + identifier: this._update.identifier, + version: this._update.available_version, + backup: this._shouldCreateBackup, + }); + } catch (err: any) { + this._error = err.message; + this._updating = false; + return; + } + this._updating = false; + this._refreshCallback(); + this.closeDialog(); + } + + private async _skipUpdate() { + this._error = undefined; + try { + await skipUpdate(this.hass, { + domain: this._update.domain, + identifier: this._update.identifier, + version: this._update.available_version, + }); + } catch (err: any) { + this._error = err.message; + return; + } + + this._refreshCallback(); + this.closeDialog(); + } + + static get styles(): CSSResultGroup { + return [ + haStyleDialog, + css` + ha-circular-progress { + display: block; + margin: 32px; + text-align: center; + } + + .progress-text { + text-align: center; + } + + ha-markdown { + padding-bottom: 8px; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-update-dialog": HaUpdateDialog; + } +} diff --git a/src/dialogs/update-dialog/show-ha-update-dialog.ts b/src/dialogs/update-dialog/show-ha-update-dialog.ts new file mode 100644 index 0000000000..4bc787ff54 --- /dev/null +++ b/src/dialogs/update-dialog/show-ha-update-dialog.ts @@ -0,0 +1,18 @@ +import { fireEvent } from "../../common/dom/fire_event"; +import { UpdateDescription } from "../../data/update"; + +export interface UpdateDialogParams { + update: UpdateDescription; + refreshCallback: () => void; +} + +export const showUpdateDialog = ( + element: HTMLElement, + dialogParams: UpdateDialogParams +): void => { + fireEvent(element, "show-dialog", { + dialogTag: "ha-update-dialog", + dialogImport: () => import("./ha-update-dialog"), + dialogParams, + }); +}; diff --git a/src/panels/config/dashboard/ha-config-dashboard.ts b/src/panels/config/dashboard/ha-config-dashboard.ts index 731a5c7079..d4c2cebb10 100644 --- a/src/panels/config/dashboard/ha-config-dashboard.ts +++ b/src/panels/config/dashboard/ha-config-dashboard.ts @@ -26,10 +26,6 @@ import "../../../components/ha-menu-button"; import "../../../components/ha-button-menu"; import "../../../components/ha-svg-icon"; import { CloudStatus } from "../../../data/cloud"; -import { - refreshSupervisorAvailableUpdates, - SupervisorAvailableUpdates, -} from "../../../data/supervisor/root"; import { showQuickBar } from "../../../dialogs/quick-bar/show-dialog-quick-bar"; import "../../../layouts/ha-app-layout"; import { haStyle } from "../../../resources/styles"; @@ -38,10 +34,11 @@ import "../ha-config-section"; import { configSections } from "../ha-panel-config"; import "./ha-config-navigation"; import "./ha-config-updates"; -import { fireEvent } from "../../../common/dom/fire_event"; import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box"; -import { showToast } from "../../../util/toast"; import { documentationUrl } from "../../../util/documentation-url"; +import { UpdateDescription } from "../../../data/update"; +import { fireEvent } from "../../../common/dom/fire_event"; +import { computeRTL } from "../../../common/util/compute_rtl"; const randomTip = (hass: HomeAssistant) => { const weighted: string[] = []; @@ -114,14 +111,12 @@ class HaConfigDashboard extends LitElement { @property() public cloudStatus?: CloudStatus; // null means not available - @property() public supervisorUpdates?: SupervisorAvailableUpdates[] | null; + @property() public updates?: UpdateDescription[] | null; @property() public showAdvanced!: boolean; @state() private _tip?: string; - private _notifyUpdates = false; - protected render(): TemplateResult { return html` @@ -160,50 +155,53 @@ class HaConfigDashboard extends LitElement { .isWide=${this.isWide} full-width > - ${this.supervisorUpdates === undefined - ? // Hide everything until updates loaded - html`` - : html`${this.supervisorUpdates?.length - ? html` - - ` - : ""} - - ${this.narrow && this.supervisorUpdates?.length - ? html`
- ${this.hass.localize("panel.config")} -
` - : ""} - ${this.cloudStatus && isComponentLoaded(this.hass, "cloud") - ? html` - - ` - : ""} + ${this.updates === undefined + ? html` + ${this.hass.localize( + "ui.panel.config.updates.checking_updates" + )} + ` + : this.updates?.length + ? html` + + ` + : ""} + + ${this.narrow && this.updates?.length + ? html`
+ ${this.hass.localize("panel.config")} +
` + : ""} + ${this.cloudStatus && isComponentLoaded(this.hass, "cloud") + ? html` -
`} + ` + : ""} + +
Tip! @@ -220,22 +218,6 @@ class HaConfigDashboard extends LitElement { if (!this._tip && changedProps.has("hass")) { this._tip = randomTip(this.hass); } - - if (!changedProps.has("supervisorUpdates") || !this._notifyUpdates) { - return; - } - this._notifyUpdates = false; - if (this.supervisorUpdates?.length) { - showToast(this, { - message: this.hass.localize( - "ui.panel.config.updates.updates_refreshed" - ), - }); - } else { - showToast(this, { - message: this.hass.localize("ui.panel.config.updates.no_new_updates"), - }); - } } private _showQuickBar(): void { @@ -248,18 +230,16 @@ class HaConfigDashboard extends LitElement { private async _handleMenuAction(ev: CustomEvent) { switch (ev.detail.index) { case 0: - if (isComponentLoaded(this.hass, "hassio")) { - this._notifyUpdates = true; - await refreshSupervisorAvailableUpdates(this.hass); - fireEvent(this, "ha-refresh-supervisor"); + if (isComponentLoaded(this.hass, "update")) { + fireEvent(this, "ha-refresh-updates"); return; } showAlertDialog(this, { title: this.hass.localize( - "ui.panel.config.updates.check_unavailable.title" + "ui.panel.config.updates.update_not_loaded.title" ), text: this.hass.localize( - "ui.panel.config.updates.check_unavailable.description" + "ui.panel.config.updates.update_not_loaded.description" ), warning: true, }); diff --git a/src/panels/config/dashboard/ha-config-updates.ts b/src/panels/config/dashboard/ha-config-updates.ts index 8c11e4325e..4c94899ed1 100644 --- a/src/panels/config/dashboard/ha-config-updates.ts +++ b/src/panels/config/dashboard/ha-config-updates.ts @@ -1,21 +1,48 @@ import "@material/mwc-button/mwc-button"; -import { mdiPackageVariant } from "@mdi/js"; import "@polymer/paper-item/paper-icon-item"; import "@polymer/paper-item/paper-item-body"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; +import memoizeOne from "memoize-one"; +import { fireEvent } from "../../../common/dom/fire_event"; import "../../../components/ha-alert"; +import "../../../components/ha-icon-next"; import "../../../components/ha-logo-svg"; import "../../../components/ha-svg-icon"; -import { SupervisorAvailableUpdates } from "../../../data/supervisor/root"; +import { UpdateDescription } from "../../../data/update"; +import { showUpdateDialog } from "../../../dialogs/update-dialog/show-ha-update-dialog"; import { HomeAssistant } from "../../../types"; -import "../../../components/ha-icon-next"; +import { brandsUrl } from "../../../util/brands-url"; -export const SUPERVISOR_UPDATE_NAMES = { - core: "Home Assistant Core", - os: "Home Assistant Operating System", - supervisor: "Home Assistant Supervisor", -}; +const sortUpdates = memoizeOne((a: UpdateDescription, b: UpdateDescription) => { + if (a.domain === "hassio" && b.domain === "hassio") { + if (a.identifier === "core") { + return -1; + } + if (b.identifier === "core") { + return 1; + } + if (a.identifier === "supervisor") { + return -1; + } + if (b.identifier === "supervisor") { + return 1; + } + if (a.identifier === "os") { + return -1; + } + if (b.identifier === "os") { + return 1; + } + } + if (a.domain === "hassio") { + return -1; + } + if (b.domain === "hassio") { + return 1; + } + return a.name.toUpperCase() < b.name.toUpperCase() ? -1 : 1; +}); @customElement("ha-config-updates") class HaConfigUpdates extends LitElement { @@ -24,62 +51,62 @@ class HaConfigUpdates extends LitElement { @property({ type: Boolean }) public narrow!: boolean; @property({ attribute: false }) - public supervisorUpdates?: SupervisorAvailableUpdates[] | null; + public updates?: UpdateDescription[] | null; @state() private _showAll = false; protected render(): TemplateResult { - if (!this.supervisorUpdates?.length) { + if (!this.updates?.length) { return html``; } + // Make sure the first updates shown are for the Supervisor + const sortedUpdates = this.updates.sort((a, b) => sortUpdates(a, b)); + const updates = - this._showAll || this.supervisorUpdates.length <= 3 - ? this.supervisorUpdates - : this.supervisorUpdates.slice(0, 2); + this._showAll || sortedUpdates.length <= 3 + ? sortedUpdates + : sortedUpdates.slice(0, 2); return html`
${this.hass.localize("ui.panel.config.updates.title", { - count: this.supervisorUpdates.length, + count: sortedUpdates.length, })}
${updates.map( (update) => html` - - - - ${update.update_type === "addon" - ? update.icon - ? html`` - : html`` - : html``} - - - ${update.update_type === "addon" - ? update.name - : SUPERVISOR_UPDATE_NAMES[update.update_type!]} -
- ${this.hass.localize( - "ui.panel.config.updates.version_available", - { - version_available: update.version_latest, - } - )} -
-
- ${!this.narrow ? html`` : ""} -
-
+ + + + + + ${update.name} +
+ ${this.hass.localize( + "ui.panel.config.updates.version_available", + { + version_available: update.available_version, + } + )} +
+
+
` )} - ${!this._showAll && this.supervisorUpdates.length >= 4 + ${!this._showAll && this.updates.length >= 4 ? html` ` @@ -91,6 +118,14 @@ class HaConfigUpdates extends LitElement { this._showAll = true; } + private _showUpdate(ev) { + const update = ev.currentTarget.update as UpdateDescription; + showUpdateDialog(this, { + update, + refreshCallback: () => fireEvent(this, "ha-refresh-updates"), + }); + } + static get styles(): CSSResultGroup[] { return [ css` @@ -139,6 +174,9 @@ class HaConfigUpdates extends LitElement { outline: none; text-decoration: underline; } + paper-icon-item { + cursor: pointer; + } `, ]; } diff --git a/src/panels/config/ha-panel-config.ts b/src/panels/config/ha-panel-config.ts index c938ca0975..6752996f4a 100644 --- a/src/panels/config/ha-panel-config.ts +++ b/src/panels/config/ha-panel-config.ts @@ -27,20 +27,18 @@ import { customElement, property, state } from "lit/decorators"; import { isComponentLoaded } from "../../common/config/is_component_loaded"; import { listenMediaQuery } from "../../common/dom/media_query"; import { CloudStatus, fetchCloudStatus } from "../../data/cloud"; -import { - fetchSupervisorAvailableUpdates, - SupervisorAvailableUpdates, -} from "../../data/supervisor/root"; +import { fetchUpdateInfo, UpdateDescription } from "../../data/update"; import "../../layouts/hass-loading-screen"; import { HassRouterPage, RouterOptions } from "../../layouts/hass-router-page"; import { PageNavigation } from "../../layouts/hass-tabs-subpage"; import { HomeAssistant, Route } from "../../types"; +import { showToast } from "../../util/toast"; declare global { // for fire event interface HASSDomEvents { "ha-refresh-cloud-status": undefined; - "ha-refresh-supervisor": undefined; + "ha-refresh-updates": undefined; } } @@ -407,7 +405,7 @@ class HaPanelConfig extends HassRouterPage { @state() private _cloudStatus?: CloudStatus; - @state() private _supervisorUpdates?: SupervisorAvailableUpdates[] | null; + @state() private _updates?: UpdateDescription[] | null; private _listeners: Array<() => void> = []; @@ -443,18 +441,18 @@ class HaPanelConfig extends HassRouterPage { } }); } - if (isComponentLoaded(this.hass, "hassio")) { - this._loadSupervisorUpdates(); - this.addEventListener("ha-refresh-supervisor", () => { - this._loadSupervisorUpdates(); + if (isComponentLoaded(this.hass, "update")) { + this._loadUpdates(); + this.addEventListener("ha-refresh-updates", () => { + this._loadUpdates(); }); this.addEventListener("connection-status", (ev) => { if (ev.detail === "connected") { - this._loadSupervisorUpdates(); + this._loadUpdates(); } }); } else { - this._supervisorUpdates = null; + this._updates = null; } this.addEventListener("ha-refresh-cloud-status", () => this._updateCloudStatus() @@ -486,7 +484,7 @@ class HaPanelConfig extends HassRouterPage { isWide, narrow: this.narrow, cloudStatus: this._cloudStatus, - supervisorUpdates: this._supervisorUpdates, + updates: this._updates, }); } else { el.route = this.routeTail; @@ -495,7 +493,7 @@ class HaPanelConfig extends HassRouterPage { el.isWide = isWide; el.narrow = this.narrow; el.cloudStatus = this._cloudStatus; - el.supervisorUpdates = this._supervisorUpdates; + el.updates = this._updates; } } @@ -514,13 +512,33 @@ class HaPanelConfig extends HassRouterPage { } } - private async _loadSupervisorUpdates(): Promise { + private async _loadUpdates(): Promise { + const _showToast = this._updates !== undefined; + + if (_showToast) { + showToast(this, { + message: this.hass.localize("ui.panel.config.updates.checking_updates"), + }); + } + try { - this._supervisorUpdates = await fetchSupervisorAvailableUpdates( - this.hass - ); + this._updates = await fetchUpdateInfo(this.hass); } catch (err) { - this._supervisorUpdates = null; + this._updates = null; + } + + if (_showToast) { + if (this._updates?.length) { + showToast(this, { + message: this.hass.localize( + "ui.panel.config.updates.updates_refreshed" + ), + }); + } else { + showToast(this, { + message: this.hass.localize("ui.panel.config.updates.no_new_updates"), + }); + } } } } diff --git a/src/translations/en.json b/src/translations/en.json index d1ec5a56b3..73fea904f9 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -1047,18 +1047,27 @@ "learn_more": "Learn more" }, "updates": { - "check_unavailable": { + "update_not_loaded": { "title": "Unable to check for updates", - "description": "You need to run the Home Assistant operating system to be able to check and install updates from the Home Assistant user interface." + "description": "You need to enable the update integrtion to be able to check and install updates from the Home Assistant user interface." }, "check_updates": "Check for updates", + "checking_updates": "Checking for available updates", "no_new_updates": "No new updates found", "updates_refreshed": "Updates refreshed", "title": "{count} {count, plural,\n one {update}\n other {updates}\n}", "unable_to_fetch": "Unable to load updates", "version_available": "Version {version_available} is available", "more_updates": "+{count} updates", - "show": "show" + "show": "show", + "dialog": { + "title": "[%key:supervisor::update_available::update_name%]", + "create_backup": "[%key:supervisor::update_available::create_backup%]", + "open_changelog": "Open changelog", + "updating": "[%key:supervisor::update_available::updating%]", + "update": "[%key:supervisor::common::update%]", + "description": "[%key:supervisor::update_available::description%]" + } }, "areas": { "caption": "Areas",