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": {