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"
}
}
}