From c1df1d41f9c0ef3091754fdd943c1117c15d94bd Mon Sep 17 00:00:00 2001 From: Ludeeus Date: Thu, 3 Jun 2021 14:05:39 +0000 Subject: [PATCH] Add add-on restore dialog --- .../src/addon-view/info/hassio-addon-info.ts | 33 +++ .../addon/dialog-hassio-addon-restore.ts | 190 ++++++++++++++++++ .../addon/show-dialog-hassio-addon-restore.ts | 22 ++ src/data/hassio/snapshot.ts | 25 ++- src/translations/en.json | 11 + 5 files changed, 280 insertions(+), 1 deletion(-) create mode 100644 hassio/src/dialogs/addon/dialog-hassio-addon-restore.ts create mode 100644 hassio/src/dialogs/addon/show-dialog-hassio-addon-restore.ts diff --git a/hassio/src/addon-view/info/hassio-addon-info.ts b/hassio/src/addon-view/info/hassio-addon-info.ts index 0fe37230bb..971975f9b3 100644 --- a/hassio/src/addon-view/info/hassio-addon-info.ts +++ b/hassio/src/addon-view/info/hassio-addon-info.ts @@ -50,6 +50,10 @@ import { fetchHassioStats, HassioStats, } from "../../../../src/data/hassio/common"; +import { + fetchHassioSnapshots, + HassioSnapshot, +} from "../../../../src/data/hassio/snapshot"; import { StoreAddon } from "../../../../src/data/supervisor/store"; import { Supervisor } from "../../../../src/data/supervisor/supervisor"; import { @@ -61,6 +65,7 @@ import { HomeAssistant } from "../../../../src/types"; import { bytesToString } from "../../../../src/util/bytes-to-string"; import "../../components/hassio-card-content"; import "../../components/supervisor-metric"; +import { showHassioAddonRestoreDialog } from "../../dialogs/addon/show-dialog-hassio-addon-restore"; import { showHassioMarkdownDialog } from "../../dialogs/markdown/show-dialog-hassio-markdown"; import { showDialogSupervisorUpdate } from "../../dialogs/update/show-dialog-update"; import { hassioStyle } from "../../resources/hassio-style"; @@ -82,6 +87,8 @@ class HassioAddonInfo extends LitElement { @property({ attribute: false }) public supervisor!: Supervisor; + @property({ attribute: false }) public snapshots?: HassioSnapshot[]; + @state() private _metrics?: HassioStats; @state() private _error?: string; @@ -626,6 +633,11 @@ class HassioAddonInfo extends LitElement { ${this.supervisor.localize("addon.dashboard.install")} `} + ${this.snapshots?.length + ? html` + ${this.supervisor.localize("addon.dashboard.restore")} + ` + : ""}
${this.addon.version @@ -698,6 +710,11 @@ class HassioAddonInfo extends LitElement { } private async _loadData(): Promise { + const snapshots = await fetchHassioSnapshots(this.hass); + this.snapshots = snapshots.filter((snapshot) => + snapshot.content.addons.includes(this.addon.slug) + ); + if (this.addon.state === "started") { this._metrics = await fetchHassioStats( this.hass, @@ -1000,6 +1017,22 @@ class HassioAddonInfo extends LitElement { fireEvent(this, "hass-api-called", eventdata); } + private async _restoreClicked(): Promise { + showHassioAddonRestoreDialog(this, { + supervisor: this.supervisor, + snapshots: this.snapshots || [], + addon: this.addon, + onRestore: () => { + const eventdata = { + success: true, + response: undefined, + path: "update", + }; + fireEvent(this, "hass-api-called", eventdata); + }, + }); + } + private async _startClicked(ev: CustomEvent): Promise { const button = ev.currentTarget as any; button.progress = true; diff --git a/hassio/src/dialogs/addon/dialog-hassio-addon-restore.ts b/hassio/src/dialogs/addon/dialog-hassio-addon-restore.ts new file mode 100644 index 0000000000..396a6c9a5c --- /dev/null +++ b/hassio/src/dialogs/addon/dialog-hassio-addon-restore.ts @@ -0,0 +1,190 @@ +import "@material/mwc-button/mwc-button"; +import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import relativeTime from "../../../../src/common/datetime/relative_time"; +import { fireEvent } from "../../../../src/common/dom/fire_event"; +import "../../../../src/common/search/search-input"; +import { compare } from "../../../../src/common/string/compare"; +import { nextRender } from "../../../../src/common/util/render-status"; +import "../../../../src/components/ha-circular-progress"; +import { createCloseHeading } from "../../../../src/components/ha-dialog"; +import "../../../../src/components/ha-expansion-panel"; +import "../../../../src/components/ha-settings-row"; +import { extractApiErrorMessage } from "../../../../src/data/hassio/common"; +import { + fetchHassioSnapshotInfo, + HassioPartialSnapshotCreateParams, + HassioSnapshotDetail, + supervisorRestorePartialSnapshot, +} from "../../../../src/data/hassio/snapshot"; +import { + showAlertDialog, + showPromptDialog, +} from "../../../../src/dialogs/generic/show-dialog-box"; +import { haStyle, haStyleDialog } from "../../../../src/resources/styles"; +import { HomeAssistant } from "../../../../src/types"; +import { HassioAddonRestoreDialogParams } from "./show-dialog-hassio-addon-restore"; + +@customElement("dialog-hassio-addon-restore") +class HassioAddonRestoreDialog extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @state() private _dialogParams?: HassioAddonRestoreDialogParams; + + @state() private _snapshots?: HassioSnapshotDetail[]; + + @state() private _restoring = false; + + public showDialog(params: HassioAddonRestoreDialogParams) { + this._dialogParams = params; + this._restoring = false; + Promise.all( + params.snapshots.map((snapshot) => + fetchHassioSnapshotInfo(this.hass, snapshot.slug) + ) + ).then((data) => { + this._snapshots = data.sort((a, b) => compare(b.date, a.date)); + }); + } + + public closeDialog() { + this._dialogParams = undefined; + this._snapshots = undefined; + this._restoring = false; + fireEvent(this, "dialog-closed", { dialog: this.localName }); + } + + protected render(): TemplateResult { + if (!this._dialogParams || (!this._snapshots && !this._restoring)) { + return html``; + } + + const snapshotCount = this._snapshots?.length || 0; + + return html` + + ${this._restoring + ? html`
+ + ${this._dialogParams.supervisor.localize( + "dialog.addon_restore.restore_in_progress" + )} + +
` + : html`${this._dialogParams.supervisor.localize( + "dialog.addon_restore.description", + { + name: this._dialogParams.addon.name, + count: snapshotCount, + } + )} + ${this._snapshots?.map( + (snapshot) => + html` + + ${snapshot.name || snapshot.slug} + + +
+ ${this._dialogParams!.supervisor.localize( + "dialog.addon_restore.version", + { + version: + snapshot.addons.find( + (addon) => + addon.slug === this._dialogParams?.addon.slug + )?.version || + this._dialogParams!.supervisor.localize( + "dialog.addon_restore.no_version" + ), + } + )} +
+ ${relativeTime(new Date(snapshot.date), this.hass.localize)} +
+ + ${this._dialogParams!.supervisor.localize( + "dialog.addon_restore.restore" + )} + +
` + )}`} +
+ `; + } + + private async _restoreClicked(ev: CustomEvent) { + let password: string | null = null; + const snapshot: HassioSnapshotDetail = (ev.currentTarget as any).snapshot; + if (snapshot.protected) { + password = await showPromptDialog(this, { + text: this._dialogParams?.supervisor.localize( + "dialog.addon_restore.protected" + ), + inputLabel: this._dialogParams?.supervisor.localize( + "dialog.addon_restore.password" + ), + inputType: "password", + }); + await nextRender(); + if (!password) { + return; + } + } + this._restoring = true; + + const data: HassioPartialSnapshotCreateParams = { + addons: [this._dialogParams!.addon.slug], + }; + if (password) { + data.password = password; + } + + try { + await supervisorRestorePartialSnapshot(this.hass, snapshot.slug, data); + } catch (err) { + await showAlertDialog(this, { + text: extractApiErrorMessage(err), + }); + await nextRender(); + return; + } + + this._dialogParams?.onRestore(); + this.closeDialog(); + } + + static get styles(): CSSResultGroup { + return [ + haStyle, + haStyleDialog, + css` + .restore { + display: flex; + flex-direction: column; + align-items: center; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "dialog-hassio-addon-restore": HassioAddonRestoreDialog; + } +} diff --git a/hassio/src/dialogs/addon/show-dialog-hassio-addon-restore.ts b/hassio/src/dialogs/addon/show-dialog-hassio-addon-restore.ts new file mode 100644 index 0000000000..4fce670c54 --- /dev/null +++ b/hassio/src/dialogs/addon/show-dialog-hassio-addon-restore.ts @@ -0,0 +1,22 @@ +import { fireEvent } from "../../../../src/common/dom/fire_event"; +import { HassioAddonDetails } from "../../../../src/data/hassio/addon"; +import { HassioSnapshot } from "../../../../src/data/hassio/snapshot"; +import { Supervisor } from "../../../../src/data/supervisor/supervisor"; + +export interface HassioAddonRestoreDialogParams { + supervisor: Supervisor; + snapshots: HassioSnapshot[]; + addon: HassioAddonDetails; + onRestore: () => void; +} + +export const showHassioAddonRestoreDialog = ( + element: HTMLElement, + dialogParams: HassioAddonRestoreDialogParams +): void => { + fireEvent(element, "show-dialog", { + dialogTag: "dialog-hassio-addon-restore", + dialogImport: () => import("./dialog-hassio-addon-restore"), + dialogParams, + }); +}; diff --git a/src/data/hassio/snapshot.ts b/src/data/hassio/snapshot.ts index b2f1c36c25..2907ac9579 100644 --- a/src/data/hassio/snapshot.ts +++ b/src/data/hassio/snapshot.ts @@ -39,7 +39,7 @@ export interface HassioSnapshotDetail extends HassioSnapshot { } export interface HassioFullSnapshotCreateParams { - name: string; + name?: string; password?: string; } export interface HassioPartialSnapshotCreateParams @@ -194,3 +194,26 @@ export const uploadSnapshot = async ( } return resp.json(); }; + +export const supervisorRestorePartialSnapshot = async ( + hass: HomeAssistant, + slug: string, + data: HassioPartialSnapshotCreateParams +) => { + if (atLeastVersion(hass.config.version, 2021, 2, 4)) { + await hass.callWS({ + type: "supervisor/api", + endpoint: `/snapshots/${slug}/restore/partial`, + method: "post", + timeout: null, + data, + }); + return; + } + + await hass.callApi>( + "POST", + `hassio/snapshots/${slug}/restore/partial`, + data + ); +}; diff --git a/src/translations/en.json b/src/translations/en.json index 458e140bc3..d58e9d42d2 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -3630,6 +3630,7 @@ "restart": "restart", "start": "start", "stop": "stop", + "restore": "restore", "install": "install", "uninstall": "uninstall", "rebuild": "rebuild", @@ -3978,6 +3979,16 @@ "create_snapshot": "Create a snapshot of {name} before updating", "updating": "Updating {name} to version {version}", "snapshotting": "Creating snapshot of {name}" + }, + "addon_restore": { + "title": "Restore {name}", + "description": "You have {count, plural,\n one {one snapshot}\n other {{count} snapshots}\n} that includes restore points for {name}", + "version": "Version {version}", + "no_version": "No version", + "restore": "Restore", + "password": "Password", + "protected": "The snapshot is password protected, please provide the password for it.", + "restore_in_progress": "Restore in progress" } } }