From 37aa2bd86914a7b44b6c430b3cc0c371d26b7bc8 Mon Sep 17 00:00:00 2001 From: Wendelin <12148533+wendevlin@users.noreply.github.com> Date: Fri, 20 Dec 2024 12:13:16 +0100 Subject: [PATCH] Improve onboarding backup restore (#23340) * Improve onboarding backup restore * Fix onboarding backup restore * Fix restoring value in onboarding-restore-backup --- hassio/src/components/hassio-upload-backup.ts | 23 +- .../components/supervisor-backup-content.ts | 14 +- .../dialogs/backup/dialog-hassio-backup.ts | 315 ++++++++---------- src/components/ha-file-upload.ts | 2 + src/data/hassio/backup.ts | 36 +- src/onboarding/onboarding-restore-backup.ts | 54 ++- src/translations/en.json | 28 +- 7 files changed, 265 insertions(+), 207 deletions(-) diff --git a/hassio/src/components/hassio-upload-backup.ts b/hassio/src/components/hassio-upload-backup.ts index dddcc68147..236e4657f7 100644 --- a/hassio/src/components/hassio-upload-backup.ts +++ b/hassio/src/components/hassio-upload-backup.ts @@ -1,7 +1,7 @@ import { mdiFolderUpload } from "@mdi/js"; import type { TemplateResult } from "lit"; import { html, LitElement } from "lit"; -import { customElement, state } from "lit/decorators"; +import { customElement, property, state } from "lit/decorators"; import { fireEvent } from "../../../src/common/dom/fire_event"; import "../../../src/components/ha-circular-progress"; import "../../../src/components/ha-file-upload"; @@ -10,10 +10,12 @@ import { uploadBackup } from "../../../src/data/hassio/backup"; import { extractApiErrorMessage } from "../../../src/data/hassio/common"; import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box"; import type { HomeAssistant } from "../../../src/types"; +import type { LocalizeFunc } from "../../../src/common/translations/localize"; declare global { interface HASSDomEvents { "backup-uploaded": { backup: HassioBackup }; + "backup-cleared": void; } } @@ -21,6 +23,8 @@ declare global { export class HassioUploadBackup extends LitElement { public hass?: HomeAssistant; + @property({ attribute: false }) public localize?: LocalizeFunc; + @state() public value: string | null = null; @state() private _uploading = false; @@ -32,13 +36,26 @@ export class HassioUploadBackup extends LitElement { .uploading=${this._uploading} .icon=${mdiFolderUpload} accept="application/x-tar" - label="Upload backup" - supports="Supports .TAR files" + .label=${this.localize?.( + "ui.panel.page-onboarding.restore.upload_backup" + ) || "Upload backup"} + .supports=${this.localize?.( + "ui.panel.page-onboarding.restore.upload_supports" + ) || "Supports .TAR files"} + .secondary=${this.localize?.( + "ui.panel.page-onboarding.restore.upload_drop" + ) || "Or drop your file here"} @file-picked=${this._uploadFile} + @files-cleared=${this._clear} > `; } + private _clear() { + this.value = null; + fireEvent(this, "backup-cleared"); + } + private async _uploadFile(ev) { const file = ev.detail.files[0]; diff --git a/hassio/src/components/supervisor-backup-content.ts b/hassio/src/components/supervisor-backup-content.ts index a4491c3e80..babc4a7bdd 100644 --- a/hassio/src/components/supervisor-backup-content.ts +++ b/hassio/src/components/supervisor-backup-content.ts @@ -65,7 +65,7 @@ const _computeAddons = (addons): AddonCheckboxItem[] => @customElement("supervisor-backup-content") export class SupervisorBackupContent extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; + @property({ attribute: false }) public hass?: HomeAssistant; @property({ attribute: false }) public localize?: LocalizeFunc; @@ -186,12 +186,13 @@ export class SupervisorBackupContent extends LitElement { .iconPath=${mdiHomeAssistant} .version=${this.backup ? this.backup.homeassistant - : this.hass.config.version} + : this.hass?.config.version} > `} > @@ -334,7 +335,7 @@ export class SupervisorBackupContent extends LitElement { | HassioFullBackupCreateParams { const data: any = {}; - if (!this.backup) { + if (!this.backup && this.hass) { data.name = this.backupName || formatDate(new Date(), this.hass.locale, this.hass.config); @@ -364,7 +365,9 @@ export class SupervisorBackupContent extends LitElement { if (folders?.length) { data.folders = folders; } - data.homeassistant = this.homeAssistant; + + // onboarding needs at least homeassistant to restore + data.homeassistant = this.onboarding || this.homeAssistant; return data; } @@ -386,6 +389,7 @@ export class SupervisorBackupContent extends LitElement { .iconPath=${section === "addons" ? mdiPuzzle : mdiFolder} .imageUrl=${section === "addons" && !this.onboarding && + this.hass && atLeastVersion(this.hass.config.version, 0, 105) && addons?.get(item.slug)?.icon ? `/api/hassio/addons/${item.slug}/icon` diff --git a/hassio/src/dialogs/backup/dialog-hassio-backup.ts b/hassio/src/dialogs/backup/dialog-hassio-backup.ts index dccdd23d08..91b6643094 100644 --- a/hassio/src/dialogs/backup/dialog-hassio-backup.ts +++ b/hassio/src/dialogs/backup/dialog-hassio-backup.ts @@ -8,9 +8,11 @@ import { atLeastVersion } from "../../../../src/common/config/version"; import { fireEvent } from "../../../../src/common/dom/fire_event"; import { stopPropagation } from "../../../../src/common/dom/stop_propagation"; import { slugify } from "../../../../src/common/string/slugify"; -import "../../../../src/components/ha-dialog"; +import "../../../../src/components/ha-md-dialog"; +import "../../../../src/components/ha-dialog-header"; import "../../../../src/components/buttons/ha-progress-button"; import "../../../../src/components/ha-alert"; +import "../../../../src/components/ha-button"; import "../../../../src/components/ha-button-menu"; import "../../../../src/components/ha-header-bar"; import "../../../../src/components/ha-icon-button"; @@ -19,6 +21,7 @@ import type { HassioBackupDetail } from "../../../../src/data/hassio/backup"; import { fetchHassioBackupInfo, removeBackup, + restoreBackup, } from "../../../../src/data/hassio/backup"; import { extractApiErrorMessage } from "../../../../src/data/hassio/common"; import { @@ -33,6 +36,7 @@ import "../../components/supervisor-backup-content"; import type { SupervisorBackupContent } from "../../components/supervisor-backup-content"; import type { HassioBackupDialogParams } from "./show-dialog-hassio-backup"; import type { BackupOrRestoreKey } from "../../util/translations"; +import type { HaMdDialog } from "../../../../src/components/ha-md-dialog"; @customElement("dialog-hassio-backup") class HassioBackupDialog @@ -52,13 +56,20 @@ class HassioBackupDialog @query("supervisor-backup-content") private _backupContent!: SupervisorBackupContent; + @query("ha-md-dialog") private _dialog?: HaMdDialog; + public async showDialog(dialogParams: HassioBackupDialogParams) { - this._backup = await fetchHassioBackupInfo(this.hass, dialogParams.slug); this._dialogParams = dialogParams; + this._backup = await fetchHassioBackupInfo(this.hass, dialogParams.slug); + if (!this._backup) { + this._error = this._localize("no_backup_found"); + } else if (this._dialogParams.onboarding && !this._backup.homeassistant) { + this._error = this._localize("restore_no_home_assistant"); + } this._restoringBackup = false; } - public closeDialog() { + private _dialogClosed(): void { this._backup = undefined; this._dialogParams = undefined; this._restoringBackup = false; @@ -66,6 +77,10 @@ class HassioBackupDialog fireEvent(this, "dialog-closed", { dialog: this.localName }); } + public closeDialog(): void { + this._dialog?.close(); + } + private _localize(key: BackupOrRestoreKey) { return ( this._dialogParams!.supervisor?.localize(`backup.${key}`) || @@ -78,100 +93,80 @@ class HassioBackupDialog return nothing; } return html` - -
- - ${this._backup.name} - - + + + ${this._backup.name} + ${!this._dialogParams.onboarding && this._dialogParams.supervisor + ? html` + + ${this._dialogParams.supervisor.localize( + "backup.download_backup" + )} + ${this._dialogParams.supervisor.localize( + "backup.delete_backup_title" + )} + ` + : nothing} + +
+ ${this._error + ? html`${this._error}` + : this._restoringBackup + ? html`
+ +
` + : html` + + + `}
- ${this._restoringBackup - ? html`` - : html` - - - `} - ${this._error - ? html`${this._error}` - : nothing} - - - ${this._localize("restore")} - - - ${!this._dialogParams.onboarding && this._dialogParams.supervisor - ? html` - - ${this._dialogParams.supervisor.localize( - "backup.download_backup" - )} - ${this._dialogParams.supervisor.localize( - "backup.delete_backup_title" - )} - ` - : nothing} - +
+ + ${this._localize("restore")} + +
+ `; } - static get styles(): CSSResultGroup { - return [ - haStyle, - haStyleDialog, - css` - ha-circular-progress { - display: block; - text-align: center; - } - ha-header-bar { - --mdc-theme-on-primary: var(--primary-text-color); - --mdc-theme-primary: var(--mdc-theme-surface); - flex-shrink: 0; - display: block; - } - ha-icon-button { - color: var(--secondary-text-color); - } - `, - ]; - } - private _handleMenuAction(ev: CustomEvent) { switch (ev.detail.index) { case 0: @@ -184,18 +179,9 @@ class HassioBackupDialog } private async _restoreClicked() { - const backupDetails = this._backupContent.backupDetails(); this._restoringBackup = true; - this._dialogParams?.onRestoring?.(); - if (this._backupContent.backupType === "full") { - await this._fullRestoreClicked(backupDetails); - } else { - await this._partialRestoreClicked(backupDetails); - } - this._restoringBackup = false; - } + const backupDetails = this._backupContent.backupDetails(); - private async _partialRestoreClicked(backupDetails) { const supervisor = this._dialogParams?.supervisor; if (supervisor !== undefined && supervisor.info.state !== "running") { await showAlertDialog(this, { @@ -204,91 +190,45 @@ class HassioBackupDialog state: supervisor.info.state, }), }); + this._restoringBackup = false; return; } if ( !(await showConfirmationDialog(this, { - title: this._localize("confirm_restore_partial_backup_title"), - text: this._localize("confirm_restore_partial_backup_text"), + title: this._localize( + this._backupContent.backupType === "full" + ? "confirm_restore_full_backup_title" + : "confirm_restore_partial_backup_title" + ), + text: this._localize( + this._backupContent.backupType === "full" + ? "confirm_restore_full_backup_text" + : "confirm_restore_partial_backup_text" + ), confirmText: this._localize("restore"), dismissText: this._localize("cancel"), })) ) { + this._restoringBackup = false; return; } - if (!this._dialogParams?.onboarding) { - try { - await this.hass!.callApi( - "POST", - - `hassio/${ - atLeastVersion(this.hass!.config.version, 2021, 9) - ? "backups" - : "snapshots" - }/${this._backup!.slug}/restore/partial`, - backupDetails - ); - this.closeDialog(); - } catch (error: any) { - this._error = error.body.message; - } - } else { - this._dialogParams?.onRestoring?.(); - await fetch(`/api/hassio/backups/${this._backup!.slug}/restore/partial`, { - method: "POST", - body: JSON.stringify(backupDetails), - }); - this.closeDialog(); - } - } - - private async _fullRestoreClicked(backupDetails) { - const supervisor = this._dialogParams?.supervisor; - if (supervisor !== undefined && supervisor.info.state !== "running") { - await showAlertDialog(this, { - title: supervisor.localize("backup.could_not_restore"), - text: supervisor.localize("backup.restore_blocked_not_running", { - state: supervisor.info.state, - }), - }); - return; - } - if ( - !(await showConfirmationDialog(this, { - title: this._localize("confirm_restore_full_backup_title"), - text: this._localize("confirm_restore_full_backup_text"), - confirmText: this._localize("restore"), - dismissText: this._localize("cancel"), - })) - ) { - return; - } - - if (!this._dialogParams?.onboarding) { - this.hass!.callApi( - "POST", - `hassio/${ - atLeastVersion(this.hass!.config.version, 2021, 9) - ? "backups" - : "snapshots" - }/${this._backup!.slug}/restore/full`, - backupDetails - ).then( - () => { - this.closeDialog(); - }, - (error) => { - this._error = error.body.message; - } + try { + await restoreBackup( + this.hass, + this._backupContent.backupType, + this._backup!.slug, + backupDetails, + !!this.hass && atLeastVersion(this.hass.config.version, 2021, 9) ); - } else { + this._dialogParams?.onRestoring?.(); - fetch(`/api/hassio/backups/${this._backup!.slug}/restore/full`, { - method: "POST", - body: JSON.stringify(backupDetails), - }); this.closeDialog(); + } catch (error: any) { + this._error = + error?.body?.message || this._localize("restore_start_failed"); + } finally { + this._restoringBackup = false; } } @@ -361,7 +301,36 @@ class HassioBackupDialog private get _computeName() { return this._backup ? this._backup.name || this._backup.slug - : "Unnamed backup"; + : this._localize("unnamed_backup"); + } + + static get styles(): CSSResultGroup { + return [ + haStyle, + haStyleDialog, + css` + ha-circular-progress { + display: block; + text-align: center; + } + ha-header-bar { + --mdc-theme-on-primary: var(--primary-text-color); + --mdc-theme-primary: var(--mdc-theme-surface); + flex-shrink: 0; + display: block; + } + ha-icon-button { + color: var(--secondary-text-color); + } + .loading { + width: 100%; + display: flex; + height: 100%; + justify-content: center; + align-items: center; + } + `, + ]; } } diff --git a/src/components/ha-file-upload.ts b/src/components/ha-file-upload.ts index 3c8eb6c775..7b588d09b0 100644 --- a/src/components/ha-file-upload.ts +++ b/src/components/ha-file-upload.ts @@ -15,6 +15,7 @@ import { bytesToString } from "../util/bytes-to-string"; declare global { interface HASSDomEvents { "file-picked": { files: File[] }; + "files-cleared": void; } } @@ -216,6 +217,7 @@ export class HaFileUpload extends LitElement { this._input!.value = ""; this.value = undefined; fireEvent(this, "change"); + fireEvent(this, "files-cleared"); } static get styles() { diff --git a/src/data/hassio/backup.ts b/src/data/hassio/backup.ts index f2f3abe1e1..8a88e1a5b9 100644 --- a/src/data/hassio/backup.ts +++ b/src/data/hassio/backup.ts @@ -1,5 +1,6 @@ import { atLeastVersion } from "../../common/config/version"; import type { HomeAssistant } from "../../types"; +import { handleFetchPromise } from "../../util/hass-call-api"; import type { HassioResponse } from "./common"; import { hassioApiResultExtractor } from "./common"; @@ -105,11 +106,13 @@ export const fetchHassioBackupInfo = async ( ); } // When called from onboarding we don't have hass - const resp = await fetch(`/api/hassio/backups/${backup}/info`, { - method: "GET", - }); - const data = (await resp.json()).data; - return data; + return hassioApiResultExtractor( + await handleFetchPromise( + fetch(`/api/hassio/backups/${backup}/info`, { + method: "GET", + }) + ) + ); }; export const reloadHassioBackups = async (hass: HomeAssistant) => { @@ -236,3 +239,26 @@ export const uploadBackup = async ( } return resp.json(); }; + +export const restoreBackup = async ( + hass: HomeAssistant | undefined, + type: HassioBackupDetail["type"], + backupSlug: string, + backupDetails: HassioPartialBackupCreateParams | HassioFullBackupCreateParams, + useSnapshotUrl: boolean +): Promise => { + if (hass) { + await hass.callApi>( + "POST", + `hassio/${useSnapshotUrl ? "snapshots" : "backups"}/${backupSlug}/restore/${type}`, + backupDetails + ); + } else { + await handleFetchPromise( + fetch(`/api/hassio/backups/${backupSlug}/restore/${type}`, { + method: "POST", + body: JSON.stringify(backupDetails), + }) + ); + } +}; diff --git a/src/onboarding/onboarding-restore-backup.ts b/src/onboarding/onboarding-restore-backup.ts index 7832cb44ad..a02f51ff7c 100644 --- a/src/onboarding/onboarding-restore-backup.ts +++ b/src/onboarding/onboarding-restore-backup.ts @@ -1,12 +1,13 @@ -import "@material/mwc-button/mwc-button"; import type { CSSResultGroup, TemplateResult } from "lit"; -import { css, html, LitElement } from "lit"; +import { css, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import { showHassioBackupDialog } from "../../hassio/src/dialogs/backup/show-dialog-hassio-backup"; import "../../hassio/src/components/hassio-upload-backup"; import type { LocalizeFunc } from "../common/translations/localize"; import "../components/ha-ansi-to-html"; import "../components/ha-card"; +import "../components/ha-alert"; +import "../components/ha-button"; import { fetchInstallationType } from "../data/onboarding"; import type { HomeAssistant } from "../types"; import "./onboarding-loading"; @@ -16,32 +17,49 @@ import { navigate } from "../common/navigate"; @customElement("onboarding-restore-backup") class OnboardingRestoreBackup extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; + @property({ attribute: false }) public hass?: HomeAssistant; @property({ attribute: false }) public localize!: LocalizeFunc; @property() public language!: string; - @state() public _restoring = false; + @state() private _restoring = false; + + @state() private _backupSlug?: string; protected render(): TemplateResult { - return html`${this._restoring + return html` + ${this._restoring ? html`

${this.localize("ui.panel.page-onboarding.restore.in_progress")}

+ + ${this.localize("ui.panel.page-onboarding.restore.in_progress")} + ` : html`

${this.localize("ui.panel.page-onboarding.restore.header")}

`} `; + + ${this._backupSlug + ? html` + ${this.localize("ui.panel.page-onboarding.restore.restore")} + ` + : nothing} +
+ `; } private _back(): void { @@ -50,12 +68,16 @@ class OnboardingRestoreBackup extends LitElement { private _backupUploaded(ev) { const backup = ev.detail.backup; - this._showBackupDialog(backup.slug); + this._backupSlug = backup.slug; + this._showBackupDialog(); + } + + private _backupCleared() { + this._backupSlug = undefined; } protected firstUpdated(changedProps) { super.firstUpdated(changedProps); - setInterval(() => this._checkRestoreStatus(), 1000); } private async _checkRestoreStatus(): Promise { @@ -73,13 +95,18 @@ class OnboardingRestoreBackup extends LitElement { } } - private _showBackupDialog(slug: string): void { + private _scheduleCheckRestoreStatus(): void { + setTimeout(() => this._checkRestoreStatus(), 1000); + } + + private _showBackupDialog(): void { showHassioBackupDialog(this, { - slug, + slug: this._backupSlug!, onboarding: true, localize: this.localize, onRestoring: () => { this._restoring = true; + this._scheduleCheckRestoreStatus(); }, }); } @@ -97,8 +124,9 @@ class OnboardingRestoreBackup extends LitElement { width: 100%; } .footer { + display: flex; + justify-content: space-between; width: 100%; - text-align: left; } `, ]; diff --git a/src/translations/en.json b/src/translations/en.json index 987bc3e497..3c837c68ca 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -7587,7 +7587,10 @@ "restore": { "header": "Restore a backup", "in_progress": "Restore in progress", + "failed": "Restore failed", "upload_backup": "[%key:supervisor::backup::upload_backup%]", + "upload_supports": "Supports .TAR files", + "upload_drop": "[%key:ui::components::file-upload::secondary%]", "show_log": "Show full log", "hide_log": "Hide full log", "full_backup": "[%key:supervisor::backup::full_backup%]", @@ -7601,12 +7604,17 @@ "password": "[%key:supervisor::backup::password%]", "confirm_password": "[%key:supervisor::backup::confirm_password%]", "confirm_restore_partial_backup_title": "[%key:supervisor::backup::confirm_restore_partial_backup_title%]", - "confirm_restore_partial_backup_text": "[%key:supervisor::backup::confirm_restore_partial_backup_text%]", + "confirm_restore_partial_backup_text": "The backup will be restored. Depending on the size of the backup, this can take up to 45 min. Home Assistant needs to shutdown and the restore progress is running in the background. If it succeeds, Home Assistant will automatically start again and you see the login screen. If it fails it will bring you back to the onboarding.", "confirm_restore_full_backup_title": "[%key:supervisor::backup::confirm_restore_full_backup_title%]", - "confirm_restore_full_backup_text": "[%key:supervisor::backup::confirm_restore_full_backup_text%]", + "confirm_restore_full_backup_text": "Your entire system will be wiped and the backup will be restored. Depending on the size of the backup, this can take up to 45 min. Home Assistant needs to shutdown and the restore progress is running in the background. If it succeeds, Home Assistant will automatically start again and you see the login screen. If it fails it will bring you back to the onboarding.", "restore": "[%key:supervisor::backup::restore%]", "close": "[%key:ui::common::close%]", - "cancel": "[%key:ui::common::cancel%]" + "cancel": "[%key:ui::common::cancel%]", + "retry": "Retry", + "restore_start_failed": "[%key:supervisor::backup::restore_start_failed%]", + "no_backup_found": "[%key:supervisor::backup::no_backup_found%]", + "restore_no_home_assistant": "[%key:supervisor::backup::restore_no_home_assistant%]", + "unnamed_backup": "[%key:supervisor::backup::unnamed_backup%]" } }, "custom": { @@ -8092,16 +8100,16 @@ "addons": "Add-ons", "folders": "Folders", "size": "Size", - "password": "Backup password", - "confirm_password": "Confirm backup password", + "password": "Backup encryption key", + "confirm_password": "Confirm encryption key", "password_protection": "Password protection", "enter_password": "Please enter a password.", "passwords_not_matching": "The passwords does not match", "backup_already_running": "A backup or restore is already running. Creating a new backup is currently not possible, try again later.", "confirm_restore_partial_backup_title": "Restore partial backup", - "confirm_restore_partial_backup_text": "The backup will be restored. Depending on the size of the backup, this can take up to 45 min.", + "confirm_restore_partial_backup_text": "The backup will be restored. Depending on the size of the backup, this can take up to 45 min. Home Assistant needs to shutdown and the restore progress is running in the background. If it succeeds, Home Assistant will automatically start again.", "confirm_restore_full_backup_title": "Restore full backup", - "confirm_restore_full_backup_text": "Your entire system will be wiped and the backup will be restored. Depending on the size of the backup, this can take up to 45 min.", + "confirm_restore_full_backup_text": "Your entire system will be wiped and the backup will be restored. Depending on the size of the backup, this can take up to 45 min. Home Assistant needs to shutdown and the restore progress is running in the background. If it succeeds, Home Assistant will automatically start again.", "confirm_delete_title": "Delete backup", "confirm_delete_text": "This backup will be permanently deleted and cannot be restored later.", "restore": "Restore", @@ -8111,7 +8119,11 @@ "download": "Download", "more_actions": "More actions", "remote_download_title": "Potentially slow download", - "remote_download_text": "You are accessing Home Assistant via remote access. Downloading backups over the Nabu Casa URL will take some time. If you are at home, cancel this dialog and enter your local URL, such as 'http://homeassistant.local:8123'" + "remote_download_text": "You are accessing Home Assistant via remote access. Downloading backups over the Nabu Casa URL will take some time. If you are at home, cancel this dialog and enter your local URL, such as 'http://homeassistant.local:8123'", + "restore_start_failed": "Failed to start restore. Unknown error.", + "no_backup_found": "No backup found.", + "restore_no_home_assistant": "Backup does not contain Home Assistant data. To restore Home Assistant you need a backup of Home Assistant core.", + "unnamed_backup": "Unnamed backup" }, "dialog": { "network": {