From ed291b57d09de8d17633d15f794b12c87d70c12c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 23 Nov 2021 17:18:40 +0100 Subject: [PATCH] Render update card on add-on page (#10681) --- .../addon-view/info/hassio-addon-info-tab.ts | 5 +- .../src/addon-view/info/hassio-addon-info.ts | 32 +- .../update-available/update-available-card.ts | 398 ++++++++++++++++++ .../update-available-dashboard.ts | 353 +--------------- 4 files changed, 434 insertions(+), 354 deletions(-) create mode 100644 hassio/src/update-available/update-available-card.ts diff --git a/hassio/src/addon-view/info/hassio-addon-info-tab.ts b/hassio/src/addon-view/info/hassio-addon-info-tab.ts index 6b31e546b7..eee42577a9 100644 --- a/hassio/src/addon-view/info/hassio-addon-info-tab.ts +++ b/hassio/src/addon-view/info/hassio-addon-info-tab.ts @@ -4,7 +4,7 @@ import "../../../../src/components/ha-circular-progress"; import { HassioAddonDetails } from "../../../../src/data/hassio/addon"; import { Supervisor } from "../../../../src/data/supervisor/supervisor"; import { haStyle } from "../../../../src/resources/styles"; -import { HomeAssistant } from "../../../../src/types"; +import { HomeAssistant, Route } from "../../../../src/types"; import { hassioStyle } from "../../resources/hassio-style"; import "./hassio-addon-info"; @@ -12,6 +12,8 @@ import "./hassio-addon-info"; class HassioAddonInfoDashboard extends LitElement { @property({ type: Boolean }) public narrow!: boolean; + @property({ attribute: false }) public route!: Route; + @property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public supervisor!: Supervisor; @@ -27,6 +29,7 @@ class HassioAddonInfoDashboard extends LitElement {
- ${this.supervisor.localize( - "addon.dashboard.new_update_available", - { name: this.addon.name, version: this.addon.version_latest } - )} - - - - - + ` : ""} ${!this.addon.protected @@ -1171,6 +1163,10 @@ class HassioAddonInfo extends LitElement { text-decoration: none; } + update-available-card { + padding-bottom: 16px; + } + @media (max-width: 720px) { ha-chip { line-height: 36px; diff --git a/hassio/src/update-available/update-available-card.ts b/hassio/src/update-available/update-available-card.ts new file mode 100644 index 0000000000..93d096e07f --- /dev/null +++ b/hassio/src/update-available/update-available-card.ts @@ -0,0 +1,398 @@ +import "@material/mwc-list/mwc-list-item"; +import { + css, + CSSResultGroup, + html, + LitElement, + PropertyValues, + TemplateResult, +} from "lit"; +import { customElement, property, state } from "lit/decorators"; +import memoizeOne from "memoize-one"; +import { fireEvent } from "../../../src/common/dom/fire_event"; +import "../../../src/common/search/search-input"; +import "../../../src/components/buttons/ha-progress-button"; +import "../../../src/components/ha-alert"; +import "../../../src/components/ha-button-menu"; +import "../../../src/components/ha-card"; +import "../../../src/components/ha-checkbox"; +import "../../../src/components/ha-expansion-panel"; +import "../../../src/components/ha-formfield"; +import "../../../src/components/ha-icon-button"; +import "../../../src/components/ha-markdown"; +import "../../../src/components/ha-settings-row"; +import "../../../src/components/ha-svg-icon"; +import "../../../src/components/ha-switch"; +import { + fetchHassioAddonChangelog, + fetchHassioAddonInfo, + HassioAddonDetails, + updateHassioAddon, +} from "../../../src/data/hassio/addon"; +import { + createHassioPartialBackup, + HassioPartialBackupCreateParams, +} from "../../../src/data/hassio/backup"; +import { + extractApiErrorMessage, + ignoreSupervisorError, +} from "../../../src/data/hassio/common"; +import { updateOS } from "../../../src/data/hassio/host"; +import { updateSupervisor } from "../../../src/data/hassio/supervisor"; +import { updateCore } from "../../../src/data/supervisor/core"; +import { StoreAddon } from "../../../src/data/supervisor/store"; +import { Supervisor } from "../../../src/data/supervisor/supervisor"; +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 { documentationUrl } from "../../../src/util/documentation-url"; +import { addonArchIsSupported, extractChangelog } from "../util/addon"; + +declare global { + interface HASSDomEvents { + "update-complete": undefined; + } +} + +type updateType = "os" | "supervisor" | "core" | "addon"; + +const changelogUrl = ( + hass: HomeAssistant, + entry: updateType, + version: string +): string | undefined => { + if (entry === "addon") { + return undefined; + } + if (entry === "core") { + return version?.includes("dev") + ? "https://github.com/home-assistant/core/commits/dev" + : documentationUrl(hass, "/latest-release-notes/"); + } + if (entry === "os") { + return version?.includes("dev") + ? "https://github.com/home-assistant/operating-system/commits/dev" + : `https://github.com/home-assistant/operating-system/releases/tag/${version}`; + } + if (entry === "supervisor") { + return version?.includes("dev") + ? "https://github.com/home-assistant/supervisor/commits/main" + : `https://github.com/home-assistant/supervisor/releases/tag/${version}`; + } + return undefined; +}; + +@customElement("update-available-card") +class UpdateAvailableCard extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public supervisor!: Supervisor; + + @property({ attribute: false }) public route!: Route; + + @property({ type: Boolean }) public narrow!: boolean; + + @property({ attribute: false }) public addonSlug?: string; + + @state() private _updateType?: updateType; + + @state() private _changelogContent?: string; + + @state() private _addonInfo?: HassioAddonDetails; + + @state() private _action: "backup" | "update" | null = null; + + @state() private _error?: string; + + private _addonStoreInfo = memoizeOne( + (slug: string, storeAddons: StoreAddon[]) => + storeAddons.find((addon) => addon.slug === slug) + ); + + protected render(): TemplateResult { + if ( + !this._updateType || + (this._updateType === "addon" && !this._addonInfo) + ) { + return html``; + } + + const changelog = changelogUrl(this.hass, this._updateType, this._version); + + return html` + +
+ ${this._error + ? html`${this._error}` + : ""} + ${this._action === null + ? html` + ${this._changelogContent + ? html` + + + + + ` + : ""} +
+

+ ${this.supervisor.localize("update_available.description", { + name: this._name, + version: this._version, + newest_version: this._version_latest, + })} +

+
+ ${["core", "addon"].includes(this._updateType) + ? html` + + + + ` + : ""} + ` + : html` + +

+ ${this._action === "update" + ? this.supervisor.localize("update_available.updating", { + name: this._name, + version: this._version_latest, + }) + : this.supervisor.localize( + "update_available.creating_backup", + { name: this._name } + )} +

`} +
+ ${this._action === null + ? html` +
+ ${changelog + ? html` + + + ` + : ""} + + + ${this.supervisor.localize("common.update")} + +
+ ` + : ""} +
+ `; + } + + protected firstUpdated(changedProps: PropertyValues) { + super.firstUpdated(changedProps); + const pathPart = this.route?.path.substring(1, this.route.path.length); + const updateType = ["core", "os", "supervisor"].includes(pathPart) + ? pathPart + : "addon"; + this._updateType = updateType as updateType; + + if (updateType === "addon") { + if (!this.addonSlug) { + this.addonSlug = pathPart; + } + this._loadAddonData(); + } + } + + get _shouldCreateBackup(): boolean { + return this.shadowRoot?.querySelector("ha-checkbox")?.checked || true; + } + + get _version(): string { + return this._updateType + ? this._updateType === "addon" + ? this._addonInfo!.version + : this.supervisor[this._updateType]?.version || "" + : ""; + } + + get _version_latest(): string { + return this._updateType + ? this._updateType === "addon" + ? this._addonInfo!.version_latest + : this.supervisor[this._updateType]?.version_latest || "" + : ""; + } + + get _name(): string { + return this._updateType + ? this._updateType === "addon" + ? this._addonInfo!.name + : SUPERVISOR_UPDATE_NAMES[this._updateType] + : ""; + } + + private async _loadAddonData() { + try { + this._addonInfo = await fetchHassioAddonInfo(this.hass, this.addonSlug!); + } catch (err) { + showAlertDialog(this, { + title: this._updateType, + text: extractApiErrorMessage(err), + }); + return; + } + const addonStoreInfo = + !this._addonInfo.detached && !this._addonInfo.available + ? this._addonStoreInfo( + this._addonInfo.slug, + this.supervisor.store.addons + ) + : undefined; + + if (this._addonInfo.changelog) { + try { + const content = await fetchHassioAddonChangelog( + this.hass, + this._updateType! + ); + this._changelogContent = extractChangelog(this._addonInfo, content); + } catch (err) { + this._error = extractApiErrorMessage(err); + return; + } + } + + if (!this._addonInfo.available && addonStoreInfo) { + if ( + !addonArchIsSupported( + this.supervisor.info.supported_arch, + this._addonInfo.arch + ) + ) { + this._error = this.supervisor.localize( + "addon.dashboard.not_available_arch" + ); + } else { + this._error = this.supervisor.localize( + "addon.dashboard.not_available_version", + { + core_version_installed: this.supervisor.core.version, + core_version_needed: addonStoreInfo.homeassistant, + } + ); + } + } + } + + private async _update() { + if (this._shouldCreateBackup) { + let backupArgs: HassioPartialBackupCreateParams; + if (this._updateType === "addon") { + backupArgs = { + name: `addon_${this._updateType}_${this._addonInfo?.version}`, + addons: [this._updateType!], + homeassistant: false, + }; + } else { + backupArgs = { + name: `${this._updateType}_${this._addonInfo?.version}`, + folders: ["homeassistant"], + homeassistant: true, + }; + } + this._action = "backup"; + try { + await createHassioPartialBackup(this.hass, backupArgs); + } catch (err: any) { + this._error = extractApiErrorMessage(err); + this._action = null; + return; + } + } + + this._action = "update"; + try { + if (this._updateType === "addon") { + await updateHassioAddon(this.hass, this._updateType!); + } else if (this._updateType === "core") { + await updateCore(this.hass); + } else if (this._updateType === "os") { + await updateOS(this.hass); + } else if (this._updateType === "supervisor") { + await updateSupervisor(this.hass); + } + } catch (err: any) { + if (this.hass.connection.connected && !ignoreSupervisorError(err)) { + this._error = extractApiErrorMessage(err); + this._action = null; + return; + } + } + fireEvent(this, "update-complete"); + } + + static get styles(): CSSResultGroup { + return css` + :host { + display: block; + } + ha-card { + margin: auto; + } + a { + text-decoration: none; + color: var(--primary-text-color); + } + ha-settings-row { + padding: 0; + } + .card-actions { + display: flex; + justify-content: space-between; + border-top: none; + padding: 0 8px 8px; + } + + ha-circular-progress { + display: block; + margin: 32px; + text-align: center; + } + + .progress-text { + text-align: center; + } + + ha-markdown { + padding-bottom: 8px; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "update-available-card": UpdateAvailableCard; + } +} diff --git a/hassio/src/update-available/update-available-dashboard.ts b/hassio/src/update-available/update-available-dashboard.ts index ccebe007cd..481808a126 100644 --- a/hassio/src/update-available/update-available-dashboard.ts +++ b/hassio/src/update-available/update-available-dashboard.ts @@ -1,78 +1,11 @@ -import "@material/mwc-list/mwc-list-item"; -import { - css, - CSSResultGroup, - html, - LitElement, - PropertyValues, - TemplateResult, -} from "lit"; -import { property, state } from "lit/decorators"; -import memoizeOne from "memoize-one"; -import "../../../src/common/search/search-input"; -import "../../../src/components/buttons/ha-progress-button"; -import "../../../src/components/ha-alert"; -import "../../../src/components/ha-button-menu"; -import "../../../src/components/ha-card"; -import "../../../src/components/ha-checkbox"; -import "../../../src/components/ha-expansion-panel"; -import "../../../src/components/ha-formfield"; -import "../../../src/components/ha-icon-button"; -import "../../../src/components/ha-markdown"; -import "../../../src/components/ha-settings-row"; -import "../../../src/components/ha-svg-icon"; -import "../../../src/components/ha-switch"; -import { - fetchHassioAddonChangelog, - fetchHassioAddonInfo, - HassioAddonDetails, - updateHassioAddon, -} from "../../../src/data/hassio/addon"; -import { - createHassioPartialBackup, - HassioPartialBackupCreateParams, -} from "../../../src/data/hassio/backup"; -import { - extractApiErrorMessage, - ignoreSupervisorError, -} from "../../../src/data/hassio/common"; -import { updateOS } from "../../../src/data/hassio/host"; -import { updateSupervisor } from "../../../src/data/hassio/supervisor"; -import { updateCore } from "../../../src/data/supervisor/core"; -import { StoreAddon } from "../../../src/data/supervisor/store"; +import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; +import { customElement, property } from "lit/decorators"; import { Supervisor } from "../../../src/data/supervisor/supervisor"; -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 { documentationUrl } from "../../../src/util/documentation-url"; -import { addonArchIsSupported, extractChangelog } from "../util/addon"; - -const changelogUrl = ( - hass: HomeAssistant, - entry: string, - version: string -): string | undefined => { - if (entry === "core") { - return version?.includes("dev") - ? "https://github.com/home-assistant/core/commits/dev" - : documentationUrl(hass, "/latest-release-notes/"); - } - if (entry === "os") { - return version?.includes("dev") - ? "https://github.com/home-assistant/operating-system/commits/dev" - : `https://github.com/home-assistant/operating-system/releases/tag/${version}`; - } - if (entry === "supervisor") { - return version?.includes("dev") - ? "https://github.com/home-assistant/supervisor/commits/main" - : `https://github.com/home-assistant/supervisor/releases/tag/${version}`; - } - return undefined; -}; +import "./update-available-card"; +@customElement("update-available-dashboard") class UpdateAvailableDashboard extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @@ -82,258 +15,25 @@ class UpdateAvailableDashboard extends LitElement { @property({ attribute: false }) public route!: Route; - @state() private _updateEntry?: string; - - @state() private _changelogContent?: string; - - @state() private _addonInfo?: HassioAddonDetails; - - @state() private _createBackup = true; - - @state() private _action: "backup" | "update" | null = null; - - @state() private _error?: string; - - private _isAddon = false; - - private _addonStoreInfo = memoizeOne( - (slug: string, storeAddons: StoreAddon[]) => - storeAddons.find((addon) => addon.slug === slug) - ); - protected render(): TemplateResult { - if (!this._updateEntry) { - return html``; - } - const name = - // @ts-ignore - this._addonInfo?.name || SUPERVISOR_UPDATE_NAMES[this._updateEntry]; - const changelog = !this._isAddon - ? changelogUrl( - this.hass, - this._updateEntry, - this.supervisor[this._updateEntry]?.version - ) - : undefined; return html` - -
- ${this._error - ? html`${this._error}` - : ""} - ${this._action === null - ? html` - ${this._changelogContent - ? html` - - - - - ` - : ""} -
-

- ${this.supervisor.localize( - "update_available.description", - { - version: - this._addonInfo?.version || - this.supervisor[this._updateEntry]?.version, - newest_version: - this._addonInfo?.version_latest || - this.supervisor[this._updateEntry]?.version_latest, - } - )} -

-
- ${!["os", "supervisor"].includes(this._updateEntry) - ? html` - - - - - ` - : ""} - ` - : html` - -

- ${this._action === "update" - ? this.supervisor.localize("update_available.updating", { - name, - version: - this._addonInfo?.version_latest || - this.supervisor[this._updateEntry]?.version_latest, - }) - : this.supervisor.localize( - "update_available.creating_backup", - { name } - )} -

`} -
- ${this._action === null - ? html` -
- ${changelog - ? html` - - - ` - : ""} - - - ${this.supervisor.localize("common.update")} - -
- ` - : ""} -
+
`; } - protected firstUpdated(changedProps: PropertyValues) { - super.firstUpdated(changedProps); - this._updateEntry = this.route.path.substring(1, this.route.path.length); - this._isAddon = !["core", "os", "supervisor"].includes(this._updateEntry); - if (this._isAddon) { - this._loadAddonData(); - } - } - - private async _loadAddonData() { - try { - this._addonInfo = await fetchHassioAddonInfo( - this.hass, - this._updateEntry! - ); - } catch (err) { - showAlertDialog(this, { - title: this._updateEntry, - text: extractApiErrorMessage(err), - confirm: () => history.back(), - }); - return; - } - const addonStoreInfo = - !this._addonInfo.detached && !this._addonInfo.available - ? this._addonStoreInfo( - this._addonInfo.slug, - this.supervisor.store.addons - ) - : undefined; - - if (this._addonInfo.changelog) { - try { - const content = await fetchHassioAddonChangelog( - this.hass, - this._updateEntry! - ); - this._changelogContent = extractChangelog(this._addonInfo, content); - } catch (err) { - this._error = extractApiErrorMessage(err); - return; - } - } - - if (!this._addonInfo.available && addonStoreInfo) { - if ( - !addonArchIsSupported( - this.supervisor.info.supported_arch, - this._addonInfo.arch - ) - ) { - this._error = this.supervisor.localize( - "addon.dashboard.not_available_arch" - ); - } else { - this._error = this.supervisor.localize( - "addon.dashboard.not_available_version", - { - core_version_installed: this.supervisor.core.version, - core_version_needed: addonStoreInfo.homeassistant, - } - ); - } - } - } - - private _toggleBackup() { - this._createBackup = !this._createBackup; - } - - private async _update() { - if (this._createBackup) { - let backupArgs: HassioPartialBackupCreateParams; - if (this._isAddon) { - backupArgs = { - name: `addon_${this._updateEntry}_${this._addonInfo?.version}`, - addons: [this._updateEntry!], - homeassistant: false, - }; - } else { - backupArgs = { - name: `${this._updateEntry}_${this._addonInfo?.version}`, - folders: ["homeassistant"], - homeassistant: true, - }; - } - this._action = "backup"; - try { - await createHassioPartialBackup(this.hass, backupArgs); - } catch (err: any) { - this._error = extractApiErrorMessage(err); - this._action = null; - return; - } - } - - this._action = "update"; - try { - if (this._isAddon) { - await updateHassioAddon(this.hass, this._updateEntry!); - } else if (this._updateEntry === "core") { - await updateCore(this.hass); - } else if (this._updateEntry === "os") { - await updateOS(this.hass); - } else if (this._updateEntry === "supervisor") { - await updateSupervisor(this.hass); - } - } catch (err: any) { - if (this.hass.connection.connected && !ignoreSupervisorError(err)) { - this._error = extractApiErrorMessage(err); - this._action = null; - return; - } - } + private _updateComplete() { history.back(); } @@ -343,34 +43,17 @@ class UpdateAvailableDashboard extends LitElement { --app-header-background-color: var(--primary-background-color); --app-header-text-color: var(--sidebar-text-color); } - ha-card { + update-available-card { margin: auto; margin-top: 16px; max-width: 600px; } - a { - text-decoration: none; - color: var(--primary-text-color); - } - ha-settings-row { - padding: 0; - } - .card-actions { - display: flex; - justify-content: space-between; - } - - ha-circular-progress { - display: block; - margin: 32px; - text-align: center; - } - - .progress-text { - text-align: center; - } `; } } -customElements.define("update-available-dashboard", UpdateAvailableDashboard); +declare global { + interface HTMLElementTagNameMap { + "update-available-dashboard": UpdateAvailableDashboard; + } +}