diff --git a/hassio/src/components/hassio-upload-snapshot.ts b/hassio/src/components/hassio-upload-snapshot.ts
index dc838988a8..94c7828ad7 100644
--- a/hassio/src/components/hassio-upload-snapshot.ts
+++ b/hassio/src/components/hassio-upload-snapshot.ts
@@ -13,7 +13,6 @@ import {
import { fireEvent } from "../../../src/common/dom/fire_event";
import "../../../src/components/ha-circular-progress";
import "../../../src/components/ha-svg-icon";
-import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import {
HassioSnapshot,
uploadSnapshot,
@@ -38,12 +37,12 @@ export class HassioUploadSnapshot extends LitElement {
public render(): TemplateResult {
return html`
`;
}
@@ -55,6 +54,7 @@ export class HassioUploadSnapshot extends LitElement {
showAlertDialog(this, {
title: "Unsupported file format",
text: "Please choose a Home Assistant snapshot file (.tar)",
+ confirmText: "ok",
});
return;
}
@@ -65,7 +65,8 @@ export class HassioUploadSnapshot extends LitElement {
} catch (err) {
showAlertDialog(this, {
title: "Upload failed",
- text: extractApiErrorMessage(err),
+ text: err.toString(),
+ confirmText: "ok",
});
} finally {
this._uploading = false;
diff --git a/hassio/src/dialogs/snapshot/dialog-hassio-snapshot-upload.ts b/hassio/src/dialogs/snapshot/dialog-hassio-snapshot-upload.ts
index 38862da37d..ed0532d29f 100644
--- a/hassio/src/dialogs/snapshot/dialog-hassio-snapshot-upload.ts
+++ b/hassio/src/dialogs/snapshot/dialog-hassio-snapshot-upload.ts
@@ -1,4 +1,6 @@
+import { mdiClose } from "@mdi/js";
import {
+ css,
CSSResult,
customElement,
html,
@@ -8,7 +10,7 @@ import {
TemplateResult,
} from "lit-element";
import { fireEvent } from "../../../../src/common/dom/fire_event";
-import { createCloseHeading } from "../../../../src/components/ha-dialog";
+import "../../../../src/components/ha-header-bar";
import { HassDialog } from "../../../../src/dialogs/make-dialog-manager";
import { haStyleDialog } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
@@ -30,7 +32,11 @@ export class DialogHassioSnapshotUpload extends LitElement
}
public closeDialog(): void {
- this._params?.reloadSnapshot();
+ if (this._params && !this._params.onboarding) {
+ if (this._params.reloadSnapshot) {
+ this._params.reloadSnapshot();
+ }
+ }
this._params = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
@@ -46,9 +52,19 @@ export class DialogHassioSnapshotUpload extends LitElement
scrimClickAction
escapeKeyAction
hideActions
+ .heading=${true}
@closed=${this.closeDialog}
- .heading=${createCloseHeading(this.hass, "Upload snapshot")}
>
+
+
+
+ Upload snapshot
+
+
+
+
+
+
(a.name > b.name ? 1 : -1));
this._addons = _computeAddons(
- this._snapshot.addons
+ this._snapshot?.addons
).sort((a: AddonItem, b: AddonItem) => (a.name > b.name ? 1 : -1));
this._dialogParams = params;
+ this._onboarding = params.onboarding ?? false;
}
protected render(): TemplateResult {
@@ -104,12 +108,17 @@ class HassioSnapshotDialog extends LitElement {
return html``;
}
return html`
-
+
+
+
+
+ ${this._computeName}
+
+
+
+
+
+
${this._snapshot.type === "full"
? "Full snapshot"
@@ -182,11 +191,15 @@ class HassioSnapshotDialog extends LitElement {
${this._error ? html`
Error: ${this._error}
` : ""}
Actions:
-
-
-
- Download Snapshot
-
+ ${!this._onboarding
+ ? html`
+
+ Download Snapshot
+ `
+ : ""}
`
: ""}
-
-
- Delete Snapshot
-
+ ${!this._onboarding
+ ? html`
+
+ Delete Snapshot
+ `
+ : ""}
`;
}
static get styles(): CSSResult[] {
return [
+ haStyle,
haStyleDialog,
css`
paper-checkbox {
@@ -242,6 +261,18 @@ class HassioSnapshotDialog extends LitElement {
.no-margin-top {
margin-top: 0;
}
+ ha-header-bar {
+ --mdc-theme-on-primary: var(--primary-text-color);
+ --mdc-theme-primary: var(--mdc-theme-surface);
+ flex-shrink: 0;
+ }
+ /* overrule the ha-style-dialog max-height on small screens */
+ @media all and (max-width: 450px), all and (max-height: 500px) {
+ ha-header-bar {
+ --mdc-theme-primary: var(--app-header-background-color);
+ --mdc-theme-on-primary: var(--app-header-text-color, white);
+ }
+ }
`,
];
}
@@ -272,6 +303,8 @@ class HassioSnapshotDialog extends LitElement {
if (
!(await showConfirmationDialog(this, {
title: "Are you sure you want partially to restore this snapshot?",
+ confirmText: "restore",
+ dismissText: "cancel",
}))
) {
return;
@@ -300,22 +333,31 @@ class HassioSnapshotDialog extends LitElement {
data.password = this._snapshotPassword;
}
- this.hass
- .callApi(
- "POST",
+ if (!this._onboarding) {
+ this.hass
+ .callApi(
+ "POST",
- `hassio/snapshots/${this._snapshot!.slug}/restore/partial`,
- data
- )
- .then(
- () => {
- alert("Snapshot restored!");
- this._closeDialog();
- },
- (error) => {
- this._error = error.body.message;
- }
- );
+ `hassio/snapshots/${this._snapshot!.slug}/restore/partial`,
+ data
+ )
+ .then(
+ () => {
+ alert("Snapshot restored!");
+ this._closeDialog();
+ },
+ (error) => {
+ this._error = error.body.message;
+ }
+ );
+ } else {
+ fireEvent(this, "restoring");
+ fetch(`/api/hassio/snapshots/${this._snapshot!.slug}/restore/partial`, {
+ method: "POST",
+ body: JSON.stringify(data),
+ });
+ this._closeDialog();
+ }
}
private async _fullRestoreClicked() {
@@ -323,6 +365,8 @@ class HassioSnapshotDialog extends LitElement {
!(await showConfirmationDialog(this, {
title:
"Are you sure you want to wipe your system and restore this snapshot?",
+ confirmText: "restore",
+ dismissText: "cancel",
}))
) {
return;
@@ -331,28 +375,38 @@ class HassioSnapshotDialog extends LitElement {
const data = this._snapshot!.protected
? { password: this._snapshotPassword }
: undefined;
-
- this.hass
- .callApi(
- "POST",
- `hassio/snapshots/${this._snapshot!.slug}/restore/full`,
- data
- )
- .then(
- () => {
- alert("Snapshot restored!");
- this._closeDialog();
- },
- (error) => {
- this._error = error.body.message;
- }
- );
+ if (!this._onboarding) {
+ this.hass
+ .callApi(
+ "POST",
+ `hassio/snapshots/${this._snapshot!.slug}/restore/full`,
+ data
+ )
+ .then(
+ () => {
+ alert("Snapshot restored!");
+ this._closeDialog();
+ },
+ (error) => {
+ this._error = error.body.message;
+ }
+ );
+ } else {
+ fireEvent(this, "restoring");
+ fetch(`/api/hassio/snapshots/${this._snapshot!.slug}/restore/full`, {
+ method: "POST",
+ body: JSON.stringify(data),
+ });
+ this._closeDialog();
+ }
}
private async _deleteClicked() {
if (
!(await showConfirmationDialog(this, {
title: "Are you sure you want to delete this snapshot?",
+ confirmText: "delete",
+ dismissText: "cancel",
}))
) {
return;
@@ -363,7 +417,9 @@ class HassioSnapshotDialog extends LitElement {
.callApi("POST", `hassio/snapshots/${this._snapshot!.slug}/remove`)
.then(
() => {
- this._dialogParams!.onDelete();
+ if (this._dialogParams!.onDelete) {
+ this._dialogParams!.onDelete();
+ }
this._closeDialog();
},
(error) => {
diff --git a/hassio/src/dialogs/snapshot/show-dialog-hassio-snapshot.ts b/hassio/src/dialogs/snapshot/show-dialog-hassio-snapshot.ts
index 478f295b7d..8631815c29 100644
--- a/hassio/src/dialogs/snapshot/show-dialog-hassio-snapshot.ts
+++ b/hassio/src/dialogs/snapshot/show-dialog-hassio-snapshot.ts
@@ -2,7 +2,8 @@ import { fireEvent } from "../../../../src/common/dom/fire_event";
export interface HassioSnapshotDialogParams {
slug: string;
- onDelete: () => void;
+ onDelete?: () => void;
+ onboarding?: boolean;
}
export const showHassioSnapshotDialog = (
diff --git a/hassio/src/dialogs/snapshot/show-dialog-snapshot-upload.ts b/hassio/src/dialogs/snapshot/show-dialog-snapshot-upload.ts
index 4d8f1986b9..2fe087f8d3 100644
--- a/hassio/src/dialogs/snapshot/show-dialog-snapshot-upload.ts
+++ b/hassio/src/dialogs/snapshot/show-dialog-snapshot-upload.ts
@@ -3,7 +3,8 @@ import "./dialog-hassio-snapshot-upload";
export interface HassioSnapshotUploadDialogParams {
showSnapshot: (slug: string) => void;
- reloadSnapshot: () => Promise;
+ reloadSnapshot?: () => Promise;
+ onboarding?: boolean;
}
export const showSnapshotUploadDialog = (
diff --git a/src/components/ha-file-upload.ts b/src/components/ha-file-upload.ts
index c9199a4425..02d2dd6877 100644
--- a/src/components/ha-file-upload.ts
+++ b/src/components/ha-file-upload.ts
@@ -10,11 +10,11 @@ import {
LitElement,
property,
PropertyValues,
+ query,
TemplateResult,
} from "lit-element";
import { classMap } from "lit-html/directives/class-map";
import { fireEvent } from "../common/dom/fire_event";
-import { HomeAssistant } from "../types";
import "./ha-circular-progress";
import "./ha-svg-icon";
@@ -26,8 +26,6 @@ declare global {
@customElement("ha-file-upload")
export class HaFileUpload extends LitElement {
- public hass!: HomeAssistant;
-
@property() public accept!: string;
@property() public icon!: string;
@@ -38,8 +36,20 @@ export class HaFileUpload extends LitElement {
@property({ type: Boolean }) private uploading = false;
+ @property({ type: Boolean, attribute: "auto-open-file-dialog" })
+ private autoOpenFileDialog = false;
+
@internalProperty() private _drag = false;
+ @query("#input") private _input?: HTMLInputElement;
+
+ protected firstUpdated(changedProperties: PropertyValues) {
+ super.firstUpdated(changedProperties);
+ if (this.autoOpenFileDialog) {
+ this._input?.click();
+ }
+ }
+
protected updated(changedProperties: PropertyValues) {
if (changedProperties.has("_drag") && !this.uploading) {
(this.shadowRoot!.querySelector(
diff --git a/src/components/ha-picture-upload.ts b/src/components/ha-picture-upload.ts
index 38a55aed55..0ac990c667 100644
--- a/src/components/ha-picture-upload.ts
+++ b/src/components/ha-picture-upload.ts
@@ -41,7 +41,6 @@ export class HaPictureUpload extends LitElement {
public render(): TemplateResult {
return html`
=> {
+ const response = await fetch("/api/discovery_info", { method: "GET" });
+ return await response.json();
+};
diff --git a/src/data/hassio/snapshot.ts b/src/data/hassio/snapshot.ts
index f08d5a3428..18157d1255 100644
--- a/src/data/hassio/snapshot.ts
+++ b/src/data/hassio/snapshot.ts
@@ -46,12 +46,20 @@ export const fetchHassioSnapshotInfo = async (
hass: HomeAssistant,
snapshot: string
) => {
- return hassioApiResultExtractor(
- await hass.callApi>(
- "GET",
- `hassio/snapshots/${snapshot}/info`
- )
- );
+ if (hass) {
+ return hassioApiResultExtractor(
+ await hass.callApi>(
+ "GET",
+ `hassio/snapshots/${snapshot}/info`
+ )
+ );
+ }
+ // When called from onboarding we don't have hass
+ const resp = await fetch(`/api/hassio/snapshots/${snapshot}/info`, {
+ method: "GET",
+ });
+ const data = (await resp.json()).data;
+ return data;
};
export const reloadHassioSnapshots = async (hass: HomeAssistant) => {
@@ -85,15 +93,25 @@ export const uploadSnapshot = async (
file: File
): Promise> => {
const fd = new FormData();
+ let resp;
fd.append("file", file);
- const resp = await hass.fetchWithAuth("/api/hassio/snapshots/new/upload", {
- method: "POST",
- body: fd,
- });
+ if (hass) {
+ resp = await hass.fetchWithAuth("/api/hassio/snapshots/new/upload", {
+ method: "POST",
+ body: fd,
+ });
+ } else {
+ // When called from onboarding we don't have hass
+ resp = await fetch("/api/hassio/snapshots/new/upload", {
+ method: "POST",
+ body: fd,
+ });
+ }
+
if (resp.status === 413) {
throw new Error("Uploaded snapshot is too large");
} else if (resp.status !== 200) {
- throw new Error("Unknown error");
+ throw new Error(`${resp.status} ${resp.statusText}`);
}
return await resp.json();
};
diff --git a/src/onboarding/ha-onboarding.ts b/src/onboarding/ha-onboarding.ts
index cdd1a3af02..8a60163bc1 100644
--- a/src/onboarding/ha-onboarding.ts
+++ b/src/onboarding/ha-onboarding.ts
@@ -1,21 +1,23 @@
import {
Auth,
createConnection,
+ genClientId,
getAuth,
subscribeConfig,
- genClientId,
} from "home-assistant-js-websocket";
import {
customElement,
html,
- property,
internalProperty,
+ property,
PropertyValues,
TemplateResult,
} from "lit-element";
import { HASSDomEvent } from "../common/dom/fire_event";
+import { extractSearchParamsObject } from "../common/url/search-params";
import { subscribeOne } from "../common/util/subscribe-one";
-import { hassUrl, AuthUrlSearchParams } from "../data/auth";
+import { AuthUrlSearchParams, hassUrl } from "../data/auth";
+import { fetchDiscoveryInformation } from "../data/discovery";
import {
fetchOnboardingOverview,
OnboardingResponses,
@@ -29,7 +31,6 @@ import { HomeAssistant } from "../types";
import { registerServiceWorker } from "../util/register-service-worker";
import "./onboarding-create-user";
import "./onboarding-loading";
-import { extractSearchParamsObject } from "../common/url/search-params";
type OnboardingEvent =
| {
@@ -62,6 +63,10 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
@internalProperty() private _loading = false;
+ @internalProperty() private _restoring = false;
+
+ @internalProperty() private _supervisor?: boolean;
+
@internalProperty() private _steps?: OnboardingStep[];
protected render(): TemplateResult {
@@ -72,10 +77,21 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
}
if (step.step === "user") {
return html`
-
+ ${!this._restoring
+ ? html`
+ `
+ : ""}
+ ${this._supervisor
+ ? html`
+ `
+ : ""}
`;
}
if (step.step === "core_config") {
@@ -100,6 +116,7 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
this._fetchOnboardingSteps();
+ this._fetchDiscoveryInformation();
import(
/* webpackChunkName: "onboarding-integrations" */ "./onboarding-integrations"
);
@@ -127,6 +144,32 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
return this._steps ? this._steps.find((stp) => !stp.done) : undefined;
}
+ private _restoringSnapshot() {
+ this._restoring = true;
+ }
+
+ private async _fetchDiscoveryInformation(): Promise {
+ try {
+ const response = await fetchDiscoveryInformation();
+ this._supervisor = [
+ "Home Assistant OS",
+ "Home Assistant Supervised",
+ ].includes(response.installation_type);
+ if (this._supervisor) {
+ // Only load if we have supervisor
+ import(
+ /* webpackChunkName: "onboarding-restore-snapshot" */ "./onboarding-restore-snapshot"
+ );
+ }
+ } catch (err) {
+ // eslint-disable-next-line no-console
+ console.error(
+ "Something went wrong loading onboarding-restore-snapshot",
+ err
+ );
+ }
+ }
+
private async _fetchOnboardingSteps() {
try {
const response = await (window.stepsPromise || fetchOnboardingOverview());
diff --git a/src/onboarding/onboarding-restore-snapshot.ts b/src/onboarding/onboarding-restore-snapshot.ts
new file mode 100644
index 0000000000..d3c38cfc1b
--- /dev/null
+++ b/src/onboarding/onboarding-restore-snapshot.ts
@@ -0,0 +1,172 @@
+import "@material/mwc-button/mwc-button";
+import {
+ css,
+ CSSResult,
+ customElement,
+ html,
+ internalProperty,
+ LitElement,
+ property,
+ TemplateResult,
+} from "lit-element";
+import "../../hassio/src/components/hassio-ansi-to-html";
+import { showHassioSnapshotDialog } from "../../hassio/src/dialogs/snapshot/show-dialog-hassio-snapshot";
+import { showSnapshotUploadDialog } from "../../hassio/src/dialogs/snapshot/show-dialog-snapshot-upload";
+import { navigate } from "../common/navigate";
+import type { LocalizeFunc } from "../common/translations/localize";
+import "../components/ha-card";
+import { makeDialogManager } from "../dialogs/make-dialog-manager";
+import { ProvideHassLitMixin } from "../mixins/provide-hass-lit-mixin";
+import { haStyle } from "../resources/styles";
+import "./onboarding-loading";
+
+declare global {
+ interface HASSDomEvents {
+ restoring: undefined;
+ }
+}
+
+@customElement("onboarding-restore-snapshot")
+class OnboardingRestoreSnapshot extends ProvideHassLitMixin(LitElement) {
+ @property() public localize!: LocalizeFunc;
+
+ @property() public language!: string;
+
+ @property({ type: Boolean }) private restoring = false;
+
+ @internalProperty() private _log?: string;
+
+ @internalProperty() private _showFullLog = false;
+
+ protected render(): TemplateResult {
+ return this.restoring
+ ? html`
+ ${this._log
+ ? this._showFullLog
+ ? html`
+ `
+ : html`
+
+ `
+ : ""}
+
+
+ ${this._showFullLog
+ ? this.localize("ui.panel.page-onboarding.restore.hide_log")
+ : this.localize("ui.panel.page-onboarding.restore.show_log")}
+
+
+ `
+ : html`
+
+ `;
+ }
+
+ private _toggeFullLog(): void {
+ this._showFullLog = !this._showFullLog;
+ }
+
+ private _filterLogs(logs: string): string {
+ // Filter out logs that is not relevant to show during the restore
+ return logs
+ .split("\n")
+ .filter(
+ (entry) =>
+ !entry.includes("/supervisor/logs") &&
+ !entry.includes("/supervisor/ping") &&
+ !entry.includes("DEBUG")
+ )
+ .join("\n")
+ .replace(/\s[A-Z]+\s\(\w+\)\s\[[\w.]+\]/gi, "")
+ .replace(/\d{2}-\d{2}-\d{2}\s/gi, "");
+ }
+
+ private _lastLogEntry(logs: string): string {
+ return logs
+ .split("\n")
+ .slice(-2)[0]
+ .replace(/\d{2}:\d{2}:\d{2}\s/gi, "");
+ }
+
+ private _uploadSnapshot(): void {
+ showSnapshotUploadDialog(this, {
+ showSnapshot: (slug: string) => this._showSnapshotDialog(slug),
+ onboarding: true,
+ });
+ }
+
+ protected firstUpdated(changedProps) {
+ super.firstUpdated(changedProps);
+ makeDialogManager(this, this.shadowRoot!);
+ setInterval(() => this._getLogs(), 1000);
+ }
+
+ private async _getLogs(): Promise {
+ if (this.restoring) {
+ try {
+ const response = await fetch("/api/hassio/supervisor/logs", {
+ method: "GET",
+ });
+ const logs = await response.text();
+ this._log = this._filterLogs(logs);
+ if (this._log.match(/\d{2}:\d{2}:\d{2}\s.*Restore\s\w+\sdone/)) {
+ // The log indicates that the restore done, navigate the user back to base
+ navigate(this, "/", true);
+ location.reload();
+ }
+ } catch (err) {
+ this._log = err.toString();
+ }
+ }
+ }
+
+ private _showSnapshotDialog(slug: string): void {
+ showHassioSnapshotDialog(this, {
+ slug,
+ onboarding: true,
+ });
+ }
+
+ static get styles(): CSSResult[] {
+ return [
+ haStyle,
+ css`
+ .logentry {
+ text-align: center;
+ }
+ ha-card {
+ padding: 4px;
+ margin-top: 8px;
+ }
+ hassio-ansi-to-html {
+ display: block;
+ line-height: 22px;
+ padding: 0 8px;
+ white-space: pre-wrap;
+ }
+
+ @media all and (min-width: 600px) {
+ ha-card {
+ width: 600px;
+ margin-left: -100px;
+ }
+ }
+ `,
+ ];
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "onboarding-restore-snapshot": OnboardingRestoreSnapshot;
+ }
+}
diff --git a/src/translations/en.json b/src/translations/en.json
index 5498d6a555..1d1b8a9890 100755
--- a/src/translations/en.json
+++ b/src/translations/en.json
@@ -2827,6 +2827,12 @@
"intro": "Devices and services are represented in Home Assistant as integrations. You can set them up now, or do it later from the configuration screen.",
"more_integrations": "More",
"finish": "Finish"
+ },
+ "restore": {
+ "description": "Alternatively you can restore from a previous snapshot.",
+ "in_progress": "Restore in progress",
+ "show_log": "Show full log",
+ "hide_log": "Hide full log"
}
},
"custom": {