diff --git a/hassio/src/components/hassio-upload-backup.ts b/hassio/src/components/hassio-upload-backup.ts
index c09b8af823..4344204c92 100644
--- a/hassio/src/components/hassio-upload-backup.ts
+++ b/hassio/src/components/hassio-upload-backup.ts
@@ -14,7 +14,7 @@ import type { LocalizeFunc } from "../../../src/common/translations/localize";
declare global {
interface HASSDomEvents {
- "backup-uploaded": { backup: HassioBackup };
+ "hassio-backup-uploaded": { backup: HassioBackup };
"backup-cleared": undefined;
}
}
@@ -70,7 +70,7 @@ export class HassioUploadBackup extends LitElement {
this._uploading = true;
try {
const backup = await uploadBackup(this.hass, file);
- fireEvent(this, "backup-uploaded", { backup: backup.data });
+ fireEvent(this, "hassio-backup-uploaded", { backup: backup.data });
} catch (err: any) {
showAlertDialog(this, {
title: "Upload failed",
diff --git a/hassio/src/components/supervisor-backup-content.ts b/hassio/src/components/supervisor-backup-content.ts
index 15fde4f3b0..422d8452eb 100644
--- a/hassio/src/components/supervisor-backup-content.ts
+++ b/hassio/src/components/supervisor-backup-content.ts
@@ -5,7 +5,6 @@ import { customElement, property, query } from "lit/decorators";
import { atLeastVersion } from "../../../src/common/config/version";
import { formatDate } from "../../../src/common/datetime/format_date";
import { formatDateTime } from "../../../src/common/datetime/format_date_time";
-import type { LocalizeFunc } from "../../../src/common/translations/localize";
import "../../../src/components/ha-checkbox";
import "../../../src/components/ha-formfield";
import "../../../src/components/ha-textfield";
@@ -19,13 +18,10 @@ import type {
} from "../../../src/data/hassio/backup";
import type { Supervisor } from "../../../src/data/supervisor/supervisor";
import { mdiHomeAssistant } from "../../../src/resources/home-assistant-logo-svg";
-import type { HomeAssistant, TranslationDict } from "../../../src/types";
+import type { HomeAssistant } from "../../../src/types";
import "./supervisor-formfield-label";
import type { HaTextField } from "../../../src/components/ha-textfield";
-type BackupOrRestoreKey = keyof TranslationDict["supervisor"]["backup"] &
- keyof TranslationDict["ui"]["panel"]["page-onboarding"]["restore"];
-
interface CheckboxItem {
slug: string;
checked: boolean;
@@ -67,8 +63,6 @@ const _computeAddons = (addons): AddonCheckboxItem[] =>
export class SupervisorBackupContent extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;
- @property({ attribute: false }) public localize?: LocalizeFunc;
-
@property({ attribute: false }) public supervisor?: Supervisor;
@property({ attribute: false }) public backup?: HassioBackupDetail;
@@ -115,10 +109,6 @@ export class SupervisorBackupContent extends LitElement {
this._focusTarget?.focus();
}
- private _localize = (key: BackupOrRestoreKey) =>
- this.supervisor?.localize(`backup.${key}`) ||
- this.localize!(`ui.panel.page-onboarding.restore.${key}`);
-
protected render() {
if (!this.onboarding && !this.supervisor) {
return nothing;
@@ -132,8 +122,8 @@ export class SupervisorBackupContent extends LitElement {
${this.backup
? html`
${this.backup.type === "full"
- ? this._localize("full_backup")
- : this._localize("partial_backup")}
+ ? this.supervisor?.localize("backup.full_backup")
+ : this.supervisor?.localize("backup.partial_backup")}
(${Math.ceil(this.backup.size * 10) / 10 + " MB"})
${this.hass
? formatDateTime(
@@ -145,7 +135,7 @@ export class SupervisorBackupContent extends LitElement {
`
: html`
@@ -153,11 +143,13 @@ export class SupervisorBackupContent extends LitElement {
${!this.backup || this.backup.type === "full"
? html`
-
+
-
+
`}
@@ -222,7 +216,7 @@ export class SupervisorBackupContent extends LitElement {
? html`
`}
@@ -247,7 +241,7 @@ export class SupervisorBackupContent extends LitElement {
${!this.backup
? html`
${!this.backup
? html`
diff --git a/hassio/src/dialogs/backup/dialog-hassio-backup.ts b/hassio/src/dialogs/backup/dialog-hassio-backup.ts
index b83ee02ad1..55ccb0bec6 100644
--- a/hassio/src/dialogs/backup/dialog-hassio-backup.ts
+++ b/hassio/src/dialogs/backup/dialog-hassio-backup.ts
@@ -35,7 +35,6 @@ import { fileDownload } from "../../../../src/util/file_download";
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")
@@ -43,7 +42,7 @@ class HassioBackupDialog
extends LitElement
implements HassDialog
{
- @property({ attribute: false }) public hass?: HomeAssistant;
+ @property({ attribute: false }) public hass!: HomeAssistant;
@state() private _error?: string;
@@ -62,9 +61,13 @@ class HassioBackupDialog
this._dialogParams = dialogParams;
this._backup = await fetchHassioBackupInfo(this.hass, dialogParams.slug);
if (!this._backup) {
- this._error = this._localize("no_backup_found");
+ this._error = this._dialogParams.supervisor?.localize(
+ "backup.no_backup_found"
+ );
} else if (this._dialogParams.onboarding && !this._backup.homeassistant) {
- this._error = this._localize("restore_no_home_assistant");
+ this._error = this._dialogParams.supervisor?.localize(
+ "backup.restore_no_home_assistant"
+ );
}
this._restoringBackup = false;
}
@@ -82,13 +85,6 @@ class HassioBackupDialog
return true;
}
- private _localize(key: BackupOrRestoreKey) {
- return (
- this._dialogParams!.supervisor?.localize(`backup.${key}`) ||
- this._dialogParams!.localize!(`ui.panel.page-onboarding.restore.${key}`)
- );
- }
-
protected render() {
if (!this._dialogParams || !this._backup) {
return nothing;
@@ -102,7 +98,7 @@ class HassioBackupDialog
@@ -161,7 +156,7 @@ class HassioBackupDialog
.disabled=${this._restoringBackup || !!this._error}
@click=${this._restoreClicked}
>
- ${this._localize("restore")}
+ ${this._dialogParams.supervisor?.localize("backup.restore")}
@@ -196,18 +191,22 @@ class HassioBackupDialog
}
if (
!(await showConfirmationDialog(this, {
- title: this._localize(
- this._backup!.type === "full"
- ? "confirm_restore_full_backup_title"
- : "confirm_restore_partial_backup_title"
+ title: supervisor?.localize(
+ `backup.${
+ this._backup!.type === "full"
+ ? "confirm_restore_full_backup_title"
+ : "confirm_restore_partial_backup_title"
+ }`
),
- text: this._localize(
- this._backup!.type === "full"
- ? "confirm_restore_full_backup_text"
- : "confirm_restore_partial_backup_text"
+ text: supervisor?.localize(
+ `backup.${
+ this._backup!.type === "full"
+ ? "confirm_restore_full_backup_text"
+ : "confirm_restore_partial_backup_text"
+ }`
),
- confirmText: this._localize("restore"),
- dismissText: this._localize("cancel"),
+ confirmText: supervisor?.localize("backup.restore"),
+ dismissText: supervisor?.localize("backup.cancel"),
}))
) {
this._restoringBackup = false;
@@ -227,7 +226,8 @@ class HassioBackupDialog
this.closeDialog();
} catch (error: any) {
this._error =
- error?.body?.message || this._localize("restore_start_failed");
+ error?.body?.message ||
+ supervisor?.localize("backup.restore_start_failed");
} finally {
this._restoringBackup = false;
}
@@ -286,7 +286,7 @@ class HassioBackupDialog
title: supervisor.localize("backup.remote_download_title"),
text: supervisor.localize("backup.remote_download_text"),
confirmText: supervisor.localize("backup.download"),
- dismissText: this._localize("cancel"),
+ dismissText: supervisor?.localize("backup.cancel"),
});
if (!confirm) {
return;
@@ -302,7 +302,7 @@ class HassioBackupDialog
private get _computeName() {
return this._backup
? this._backup.name || this._backup.slug
- : this._localize("unnamed_backup");
+ : this._dialogParams!.supervisor?.localize("backup.unnamed_backup") || "";
}
static get styles(): CSSResultGroup {
diff --git a/hassio/src/dialogs/backup/show-dialog-hassio-backup.ts b/hassio/src/dialogs/backup/show-dialog-hassio-backup.ts
index eef9a22f5e..57b354f087 100644
--- a/hassio/src/dialogs/backup/show-dialog-hassio-backup.ts
+++ b/hassio/src/dialogs/backup/show-dialog-hassio-backup.ts
@@ -1,5 +1,4 @@
import { fireEvent } from "../../../../src/common/dom/fire_event";
-import type { LocalizeFunc } from "../../../../src/common/translations/localize";
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
export interface HassioBackupDialogParams {
@@ -8,7 +7,6 @@ export interface HassioBackupDialogParams {
onRestoring?: () => void;
onboarding?: boolean;
supervisor?: Supervisor;
- localize?: LocalizeFunc;
}
export const showHassioBackupDialog = (
diff --git a/hassio/src/util/translations.ts b/hassio/src/util/translations.ts
deleted file mode 100644
index 1a7af29ef3..0000000000
--- a/hassio/src/util/translations.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-import type { TranslationDict } from "../../../src/types";
-
-export type BackupOrRestoreKey = keyof TranslationDict["supervisor"]["backup"] &
- keyof TranslationDict["ui"]["panel"]["page-onboarding"]["restore"];
diff --git a/src/common/datetime/format_date_time.ts b/src/common/datetime/format_date_time.ts
index 92c6e185ac..b0d6a5c68e 100644
--- a/src/common/datetime/format_date_time.ts
+++ b/src/common/datetime/format_date_time.ts
@@ -26,6 +26,20 @@ const formatDateTimeMem = memoizeOne(
})
);
+export const formatDateTimeWithBrowserDefaults = (dateObj: Date) =>
+ formatDateTimeWithBrowserDefaultsMem().format(dateObj);
+
+const formatDateTimeWithBrowserDefaultsMem = memoizeOne(
+ () =>
+ new Intl.DateTimeFormat(undefined, {
+ year: "numeric",
+ month: "long",
+ day: "numeric",
+ hour: "2-digit",
+ minute: "2-digit",
+ })
+);
+
// Aug 9, 2021, 8:23 AM
export const formatShortDateTimeWithYear = (
dateObj: Date,
diff --git a/src/components/ha-file-upload.ts b/src/components/ha-file-upload.ts
index 245331a3b9..d665910525 100644
--- a/src/components/ha-file-upload.ts
+++ b/src/components/ha-file-upload.ts
@@ -11,6 +11,7 @@ import "./ha-icon-button";
import { blankBeforePercent } from "../common/translations/blank_before_percent";
import { ensureArray } from "../common/array/ensure-array";
import { bytesToString } from "../util/bytes-to-string";
+import type { LocalizeFunc } from "../common/translations/localize";
declare global {
interface HASSDomEvents {
@@ -23,6 +24,8 @@ declare global {
export class HaFileUpload extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;
+ @property({ attribute: false }) public localize?: LocalizeFunc;
+
@property() public accept!: string;
@property() public icon?: string;
@@ -31,6 +34,10 @@ export class HaFileUpload extends LitElement {
@property() public secondary?: string;
+ @property({ attribute: "uploading-label" }) public uploadingLabel?: string;
+
+ @property({ attribute: "delete-label" }) public deleteLabel?: string;
+
@property() public supports?: string;
@property({ type: Object }) public value?: File | File[] | FileList | string;
@@ -73,23 +80,22 @@ export class HaFileUpload extends LitElement {
}
public render(): TemplateResult {
+ const localize = this.localize || this.hass!.localize;
return html`
${this.uploading
? html`
${this.uploadingLabel || this.value
+ ? localize("ui.components.file-upload.uploading_name", {
+ name: this._name,
+ })
+ : localize("ui.components.file-upload.uploading")}
${this.progress
? html`
- ${this.progress}${blankBeforePercent(this.hass!.locale)}%
+ ${this.progress}${this.hass &&
+ blankBeforePercent(this.hass!.locale)}%
`
: nothing}
@@ -116,14 +122,11 @@ export class HaFileUpload extends LitElement {
.path=${this.icon || mdiFileUpload}
>
- ${this.label ||
- this.hass?.localize("ui.components.file-upload.label")}
+ ${this.label || localize("ui.components.file-upload.label")}
${this.secondary ||
- this.hass?.localize(
- "ui.components.file-upload.secondary"
- )}
${this.supports}`
: typeof this.value === "string"
@@ -136,8 +139,7 @@ export class HaFileUpload extends LitElement {
`
@@ -155,8 +157,8 @@ export class HaFileUpload extends LitElement {
`
@@ -238,6 +240,10 @@ export class HaFileUpload extends LitElement {
border-radius: var(--mdc-shape-small, 4px);
height: 100%;
}
+ .row {
+ display: flex;
+ align-items: center;
+ }
label.container {
border: dashed 1px
var(--mdc-text-field-idle-line-color, rgba(0, 0, 0, 0.42));
diff --git a/src/data/backup.ts b/src/data/backup.ts
index 756ca4b47a..54424f0b21 100644
--- a/src/data/backup.ts
+++ b/src/data/backup.ts
@@ -2,7 +2,6 @@ import { memoize } from "@fullcalendar/core/internal";
import { setHours, setMinutes } from "date-fns";
import type { HassConfig } from "home-assistant-js-websocket";
import memoizeOne from "memoize-one";
-import checkValidDate from "../common/datetime/check_valid_date";
import {
formatDateTime,
formatDateTimeNumeric,
@@ -13,6 +12,8 @@ import type { HomeAssistant } from "../types";
import { fileDownload } from "../util/file_download";
import { domainToName } from "./integration";
import type { FrontendLocaleData } from "./translation";
+import checkValidDate from "../common/datetime/check_valid_date";
+import { handleFetchPromise } from "../util/hass-call-api";
export const enum BackupScheduleRecurrence {
NEVER = "never",
@@ -231,27 +232,23 @@ export const restoreBackup = (
export const uploadBackup = async (
hass: HomeAssistant,
file: File,
- agent_ids: string[]
-): Promise => {
+ agentIds: string[]
+): Promise<{ backup_id: string }> => {
const fd = new FormData();
fd.append("file", file);
- const params = agent_ids.reduce((acc, agent_id) => {
- acc.append("agent_id", agent_id);
- return acc;
- }, new URLSearchParams());
+ const params = new URLSearchParams();
- const resp = await hass.fetchWithAuth(
- `/api/backup/upload?${params.toString()}`,
- {
+ agentIds.forEach((agentId) => {
+ params.append("agent_id", agentId);
+ });
+
+ return handleFetchPromise(
+ hass.fetchWithAuth(`/api/backup/upload?${params.toString()}`, {
method: "POST",
body: fd,
- }
+ })
);
-
- if (!resp.ok) {
- throw new Error(`${resp.status} ${resp.statusText}`);
- }
};
export const getPreferredAgentForDownload = (agents: string[]) => {
@@ -449,3 +446,13 @@ export const getFormattedBackupTime = memoizeOne(
return `${formatTime(DEFAULT_OPTIMIZED_BACKUP_START_TIME, locale, config)} - ${formatTime(DEFAULT_OPTIMIZED_BACKUP_END_TIME, locale, config)}`;
}
);
+
+export const SUPPORTED_UPLOAD_FORMAT = "application/x-tar";
+
+export interface BackupUploadFileFormData {
+ file?: File;
+}
+
+export const INITIAL_UPLOAD_FORM_DATA: BackupUploadFileFormData = {
+ file: undefined,
+};
diff --git a/src/data/backup_onboarding.ts b/src/data/backup_onboarding.ts
new file mode 100644
index 0000000000..bca3253679
--- /dev/null
+++ b/src/data/backup_onboarding.ts
@@ -0,0 +1,66 @@
+import { handleFetchPromise } from "../util/hass-call-api";
+import type { BackupContentExtended } from "./backup";
+import type {
+ BackupManagerState,
+ RestoreBackupStage,
+ RestoreBackupState,
+} from "./backup_manager";
+
+export interface BackupOnboardingInfo {
+ state: BackupManagerState;
+ last_non_idle_event?: {
+ manager_state: BackupManagerState;
+ stage: RestoreBackupStage | null;
+ state: RestoreBackupState;
+ reason: string | null;
+ } | null;
+}
+
+export interface BackupOnboardingConfig extends BackupOnboardingInfo {
+ backups: BackupContentExtended[];
+}
+
+export const fetchBackupOnboardingInfo = async () =>
+ handleFetchPromise(
+ fetch("/api/onboarding/backup/info")
+ );
+
+export interface RestoreOnboardingBackupParams {
+ backup_id: string;
+ agent_id: string;
+ password?: string;
+ restore_addons?: string[];
+ restore_database?: boolean;
+ restore_folders?: string[];
+}
+
+export const restoreOnboardingBackup = async (
+ params: RestoreOnboardingBackupParams
+) =>
+ handleFetchPromise(
+ fetch("/api/onboarding/backup/restore", {
+ method: "POST",
+ body: JSON.stringify(params),
+ })
+ );
+
+export const uploadOnboardingBackup = async (
+ file: File,
+ agentIds: string[]
+): Promise<{ backup_id: string }> => {
+ const fd = new FormData();
+ fd.append("file", file);
+
+ const params = new URLSearchParams();
+
+ agentIds.forEach((agentId) => {
+ params.append("agent_id", agentId);
+ });
+
+ return handleFetchPromise(
+ fetch(`/api/onboarding/backup/upload?${params.toString()}`, {
+ method: "POST",
+ body: fd,
+ })
+ );
+};
diff --git a/src/data/hassio/backup.ts b/src/data/hassio/backup.ts
index d9937e1dce..0879ec28bd 100644
--- a/src/data/hassio/backup.ts
+++ b/src/data/hassio/backup.ts
@@ -1,6 +1,5 @@
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";
@@ -82,34 +81,24 @@ export const fetchHassioBackups = async (
};
export const fetchHassioBackupInfo = async (
- hass: HomeAssistant | undefined,
+ hass: HomeAssistant,
backup: string
): Promise => {
- if (hass) {
- if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
- return hass.callWS({
- type: "supervisor/api",
- endpoint: `/${
- atLeastVersion(hass.config.version, 2021, 9) ? "backups" : "snapshots"
- }/${backup}/info`,
- method: "get",
- });
- }
- return hassioApiResultExtractor(
- await hass.callApi>(
- "GET",
- `hassio/${
- atLeastVersion(hass.config.version, 2021, 9) ? "backups" : "snapshots"
- }/${backup}/info`
- )
- );
+ if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
+ return hass.callWS({
+ type: "supervisor/api",
+ endpoint: `/${
+ atLeastVersion(hass.config.version, 2021, 9) ? "backups" : "snapshots"
+ }/${backup}/info`,
+ method: "get",
+ });
}
- // When called from onboarding we don't have hass
return hassioApiResultExtractor(
- await handleFetchPromise(
- fetch(`/api/hassio/backups/${backup}/info`, {
- method: "GET",
- })
+ await hass.callApi>(
+ "GET",
+ `hassio/${
+ atLeastVersion(hass.config.version, 2021, 9) ? "backups" : "snapshots"
+ }/${backup}/info`
)
);
};
@@ -240,24 +229,15 @@ export const uploadBackup = async (
};
export const restoreBackup = async (
- hass: HomeAssistant | undefined,
+ hass: HomeAssistant,
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),
- })
- );
- }
+ await hass.callApi>(
+ "POST",
+ `hassio/${useSnapshotUrl ? "snapshots" : "backups"}/${backupSlug}/restore/${type}`,
+ backupDetails
+ );
};
diff --git a/src/mixins/lit-localize-lite-mixin.ts b/src/mixins/lit-localize-lite-mixin.ts
index 8ba189bba7..2f2b4b9094 100644
--- a/src/mixins/lit-localize-lite-mixin.ts
+++ b/src/mixins/lit-localize-lite-mixin.ts
@@ -17,7 +17,7 @@ export const litLocalizeLiteMixin = >(
@property({ attribute: false }) public localize: LocalizeFunc = empty;
// Use browser language setup before login.
- @property() public language?: string = getLocalLanguage();
+ @property() public language: string = getLocalLanguage();
@property() public translationFragment?: string;
diff --git a/src/onboarding/ha-onboarding.ts b/src/onboarding/ha-onboarding.ts
index 25012234ba..6adc49e287 100644
--- a/src/onboarding/ha-onboarding.ts
+++ b/src/onboarding/ha-onboarding.ts
@@ -41,6 +41,7 @@ import "./onboarding-analytics";
import "./onboarding-create-user";
import "./onboarding-loading";
import "./onboarding-welcome";
+import "./onboarding-restore-backup";
import "./onboarding-welcome-links";
import { makeDialogManager } from "../dialogs/make-dialog-manager";
import { navigate } from "../common/navigate";
@@ -157,8 +158,9 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
private _renderStep() {
if (this._restoring) {
return html`
`;
}
@@ -166,8 +168,6 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
if (this._init) {
return html``;
}
@@ -236,7 +236,7 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
}
}
if (changedProps.has("language")) {
- document.querySelector("html")!.setAttribute("lang", this.language!);
+ document.querySelector("html")!.setAttribute("lang", this.language);
}
if (changedProps.has("hass")) {
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
@@ -272,10 +272,6 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
"Home Assistant OS",
"Home Assistant Supervised",
].includes(response.installation_type);
- if (this._supervisor) {
- // Only load if we have supervisor
- import("./onboarding-restore-backup");
- }
} catch (err: any) {
// eslint-disable-next-line no-console
console.error(
@@ -454,7 +450,7 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
subscribeOne(conn, subscribeUser),
]);
this.initializeHass(auth, conn);
- if (this.language && this.language !== this.hass!.language) {
+ if (this.language !== this.hass!.language) {
this._updateHass({
locale: { ...this.hass!.locale, language: this.language },
language: this.language,
diff --git a/src/onboarding/onboarding-restore-backup.ts b/src/onboarding/onboarding-restore-backup.ts
index a02f51ff7c..7881cd7b33 100644
--- a/src/onboarding/onboarding-restore-backup.ts
+++ b/src/onboarding/onboarding-restore-backup.ts
@@ -1,136 +1,336 @@
-import type { CSSResultGroup, TemplateResult } from "lit";
+import type { TemplateResult } 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 "./restore-backup/onboarding-restore-backup-upload";
+import "./restore-backup/onboarding-restore-backup-details";
+import "./restore-backup/onboarding-restore-backup-restore";
+import "./restore-backup/onboarding-restore-backup-status";
import type { LocalizeFunc } from "../common/translations/localize";
-import "../components/ha-ansi-to-html";
import "../components/ha-card";
+import "../components/ha-icon-button-arrow-prev";
+import "../components/ha-circular-progress";
import "../components/ha-alert";
-import "../components/ha-button";
-import { fetchInstallationType } from "../data/onboarding";
-import type { HomeAssistant } from "../types";
import "./onboarding-loading";
-import { onBoardingStyles } from "./styles";
import { removeSearchParam } from "../common/url/search-params";
import { navigate } from "../common/navigate";
+import { onBoardingStyles } from "./styles";
+import {
+ fetchBackupOnboardingInfo,
+ type BackupOnboardingConfig,
+ type BackupOnboardingInfo,
+} from "../data/backup_onboarding";
+import type { BackupContentExtended, BackupData } from "../data/backup";
+import { showConfirmationDialog } from "../dialogs/generic/show-dialog-box";
+import { storage } from "../common/decorators/storage";
+
+const STATUS_INTERVAL_IN_MS = 5000;
@customElement("onboarding-restore-backup")
class OnboardingRestoreBackup extends LitElement {
- @property({ attribute: false }) public hass?: HomeAssistant;
-
@property({ attribute: false }) public localize!: LocalizeFunc;
@property() public language!: string;
- @state() private _restoring = false;
+ @property({ type: Boolean }) public supervisor = false;
- @state() private _backupSlug?: string;
+ @state() private _view:
+ | "loading"
+ | "upload"
+ | "select_data"
+ | "confirm_restore"
+ | "status" = "loading";
+
+ @state() private _backup?: BackupContentExtended;
+
+ @state() private _backupInfo?: BackupOnboardingInfo;
+
+ @state() private _selectedData?: BackupData;
+
+ @state() private _error?: string;
+
+ @state() private _failed?: boolean;
+
+ @storage({
+ key: "onboarding-restore-backup-backup-id",
+ })
+ private _backupId?: string;
+
+ @storage({
+ key: "onboarding-restore-running",
+ })
+ private _restoreRunning?: boolean;
protected render(): TemplateResult {
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._failed && this._view !== "status"
+ ? this.localize(
+ `ui.panel.page-onboarding.restore.${this._backupInfo?.last_non_idle_event?.reason === "password_incorrect" ? "failed_wrong_password_description" : "failed_description"}`
+ )
+ : this._error}
+ `
+ : nothing
+ }
+ ${
+ this._view === "loading"
+ ? html`
+
+
`
+ : this._view === "upload"
+ ? html`
+
+ `
+ : this._view === "select_data"
+ ? html``
+ : this._view === "confirm_restore"
+ ? html``
+ : nothing
+ }
+ ${
+ this._view === "status" && this._backupInfo
+ ? html``
+ : nothing
+ }
+ ${
+ ["select_data", "confirm_restore"].includes(this._view) && this._backup
+ ? html`
+
+
`
+ : nothing
+ }
`;
}
- private _back(): void {
- navigate(`${location.pathname}?${removeSearchParam("page")}`);
- }
-
- private _backupUploaded(ev) {
- const backup = ev.detail.backup;
- this._backupSlug = backup.slug;
- this._showBackupDialog();
- }
-
- private _backupCleared() {
- this._backupSlug = undefined;
- }
-
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
+
+ this._loadBackupInfo();
}
- private async _checkRestoreStatus(): Promise {
- if (this._restoring) {
- try {
- await fetchInstallationType();
- } catch (err: any) {
+ private async _loadBackupInfo() {
+ let onboardingInfo: BackupOnboardingConfig;
+ try {
+ onboardingInfo = await fetchBackupOnboardingInfo();
+ } catch (err: any) {
+ if (this._restoreRunning) {
if (
- (err as Error).message === "unauthorized" ||
- (err as Error).message === "not_found"
+ err.error === "Request error" ||
+ // core can restart but haven't loaded the backup integration yet
+ (err.status_code === 500 && err.body?.error === "backup_disabled")
) {
+ // core is down because of restore, keep trying
+ this._scheduleLoadBackupInfo();
+ return;
+ }
+
+ // core seems to be back up restored
+ if (err.status_code === 404) {
+ this._restoreRunning = undefined;
+ this._backupId = undefined;
window.location.replace("/");
+ return;
}
}
+
+ this._error = err?.message || "Cannot get backup info";
+
+ // if we are in an unknown state, show upload
+ if (this._view === "loading") {
+ this._view = "upload";
+ }
+ return;
+ }
+
+ const {
+ last_non_idle_event: lastNonIdleEvent,
+ state: currentState,
+ backups,
+ } = onboardingInfo;
+
+ this._backupInfo = {
+ state: currentState,
+ last_non_idle_event: lastNonIdleEvent,
+ };
+
+ if (this._backupId) {
+ this._backup = backups.find(
+ ({ backup_id }) => backup_id === this._backupId
+ );
+ }
+
+ const failedRestore =
+ lastNonIdleEvent?.manager_state === "restore_backup" &&
+ lastNonIdleEvent?.state === "failed";
+
+ if (failedRestore) {
+ this._failed = true;
+ }
+
+ if (this._restoreRunning) {
+ this._view = "status";
+ if (failedRestore || currentState !== "restore_backup") {
+ this._failed = true;
+ this._restoreRunning = undefined;
+ } else {
+ this._scheduleLoadBackupInfo();
+ }
+ return;
+ }
+
+ if (
+ this._backup &&
+ // after backup was uploaded
+ (lastNonIdleEvent?.manager_state === "receive_backup" ||
+ // when restore was confirmed but failed to start (for example, encryption key was wrong)
+ failedRestore)
+ ) {
+ if (!this.supervisor && this._backup.homeassistant_included) {
+ this._selectedData = {
+ homeassistant_included: true,
+ folders: [],
+ addons: [],
+ homeassistant_version: this._backup.homeassistant_version,
+ database_included: this._backup.database_included,
+ };
+ // skip select data when supervisor is not available and backup includes HA
+ this._view = "confirm_restore";
+ } else {
+ this._view = "select_data";
+ }
+ return;
+ }
+
+ // show upload as default
+ this._view = "upload";
+ }
+
+ private _scheduleLoadBackupInfo() {
+ setTimeout(() => this._loadBackupInfo(), STATUS_INTERVAL_IN_MS);
+ }
+
+ private async _backupUploaded(ev: CustomEvent) {
+ this._backupId = ev.detail.backupId;
+ await this._loadBackupInfo();
+ }
+
+ private async _restoreStarted() {
+ if (this._backupInfo) {
+ this._backupInfo.state = "restore_backup";
+ }
+ this._view = "status";
+ this._restoreRunning = true;
+ await this._loadBackupInfo();
+ }
+
+ private async _back() {
+ if (this._view === "upload" || (this._view === "status" && this._failed)) {
+ navigate(`${location.pathname}?${removeSearchParam("page")}`);
+ } else {
+ const confirmed = await showConfirmationDialog(this, {
+ title: this.localize(
+ "ui.panel.page-onboarding.restore.cancel_restore.title"
+ ),
+ text: this.localize(
+ "ui.panel.page-onboarding.restore.cancel_restore.text"
+ ),
+ confirmText: this.localize(
+ "ui.panel.page-onboarding.restore.cancel_restore.yes"
+ ),
+ dismissText: this.localize(
+ "ui.panel.page-onboarding.restore.cancel_restore.no"
+ ),
+ });
+
+ if (!confirmed) {
+ return;
+ }
+ navigate(`${location.pathname}?${removeSearchParam("page")}`);
}
}
- private _scheduleCheckRestoreStatus(): void {
- setTimeout(() => this._checkRestoreStatus(), 1000);
+ private _restore(ev: CustomEvent) {
+ if (!this._backup || !ev.detail.selectedData) {
+ return;
+ }
+ this._selectedData = ev.detail.selectedData;
+
+ this._view = "confirm_restore";
}
- private _showBackupDialog(): void {
- showHassioBackupDialog(this, {
- slug: this._backupSlug!,
- onboarding: true,
- localize: this.localize,
- onRestoring: () => {
- this._restoring = true;
- this._scheduleCheckRestoreStatus();
- },
- });
+ private _reupload() {
+ this._backup = undefined;
+ this._backupId = undefined;
+ this._view = "upload";
}
- static get styles(): CSSResultGroup {
- return [
- onBoardingStyles,
- css`
- :host {
- display: flex;
- flex-direction: column;
- align-items: center;
- }
- hassio-upload-backup {
- width: 100%;
- }
- .footer {
- display: flex;
- justify-content: space-between;
- width: 100%;
- }
- `,
- ];
- }
+ static styles = [
+ onBoardingStyles,
+ css`
+ :host {
+ display: flex;
+ flex-direction: column;
+ position: relative;
+ }
+ ha-icon-button-arrow-prev {
+ position: absolute;
+ top: 12px;
+ }
+ ha-card {
+ width: 100%;
+ }
+ .loading {
+ display: flex;
+ justify-content: center;
+ padding: 32px;
+ }
+ .backup-summary-wrapper {
+ margin-top: 24px;
+ padding: 0 20px;
+ }
+ `,
+ ];
}
declare global {
diff --git a/src/onboarding/onboarding-welcome.ts b/src/onboarding/onboarding-welcome.ts
index a3f0dba051..56f30e28ab 100644
--- a/src/onboarding/onboarding-welcome.ts
+++ b/src/onboarding/onboarding-welcome.ts
@@ -1,5 +1,5 @@
import type { CSSResultGroup, TemplateResult } from "lit";
-import { LitElement, css, html, nothing } from "lit";
+import { LitElement, css, html } from "lit";
import { customElement, property } from "lit/decorators";
import type { LocalizeFunc } from "../common/translations/localize";
import type { HomeAssistant } from "../types";
@@ -13,8 +13,6 @@ class OnboardingWelcome extends LitElement {
@property({ attribute: false }) public localize!: LocalizeFunc;
- @property({ type: Boolean }) public supervisor = false;
-
protected render(): TemplateResult {
return html`
${this.localize("ui.panel.page-onboarding.welcome.header")}
@@ -24,11 +22,9 @@ class OnboardingWelcome extends LitElement {
${this.localize("ui.panel.page-onboarding.welcome.start")}
- ${this.supervisor
- ? html`
- ${this.localize("ui.panel.page-onboarding.welcome.restore_backup")}
- `
- : nothing}
+
+ ${this.localize("ui.panel.page-onboarding.welcome.restore_backup")}
+
`;
}
diff --git a/src/onboarding/restore-backup/onboarding-restore-backup-details.ts b/src/onboarding/restore-backup/onboarding-restore-backup-details.ts
new file mode 100644
index 0000000000..5c5d976d4d
--- /dev/null
+++ b/src/onboarding/restore-backup/onboarding-restore-backup-details.ts
@@ -0,0 +1,57 @@
+import { css, html, LitElement, type CSSResultGroup } from "lit";
+import { customElement, property } from "lit/decorators";
+import "../../components/ha-card";
+import "../../components/ha-circular-progress";
+import "../../components/ha-alert";
+import "../../components/ha-button";
+import "../../panels/config/backup/components/ha-backup-details-restore";
+import "../../panels/config/backup/components/ha-backup-details-summary";
+import { haStyle } from "../../resources/styles";
+import type { LocalizeFunc } from "../../common/translations/localize";
+import type { BackupContentExtended } from "../../data/backup";
+
+@customElement("onboarding-restore-backup-details")
+class OnboardingRestoreBackupDetails extends LitElement {
+ @property({ attribute: false }) public localize!: LocalizeFunc;
+
+ @property({ attribute: false }) public backup!: BackupContentExtended;
+
+ render() {
+ return html`
+ ${this.backup.homeassistant_included
+ ? html``
+ : html`
+
+ ${this.localize(
+ "ui.panel.page-onboarding.restore.details.home_assistant_missing"
+ )}
+
+ `}
+ `;
+ }
+
+ static get styles(): CSSResultGroup {
+ return [
+ haStyle,
+ css`
+ :host {
+ padding: 28px 20px 0;
+ }
+ ha-backup-details-restore {
+ display: block;
+ }
+ `,
+ ];
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "onboarding-restore-backup-details": OnboardingRestoreBackupDetails;
+ }
+}
diff --git a/src/onboarding/restore-backup/onboarding-restore-backup-restore.ts b/src/onboarding/restore-backup/onboarding-restore-backup-restore.ts
new file mode 100644
index 0000000000..bde1502f5b
--- /dev/null
+++ b/src/onboarding/restore-backup/onboarding-restore-backup-restore.ts
@@ -0,0 +1,174 @@
+import { css, html, LitElement, nothing, type CSSResultGroup } from "lit";
+import { customElement, property, state } from "lit/decorators";
+import "../../components/ha-card";
+import "../../components/ha-alert";
+import "../../components/buttons/ha-progress-button";
+import "../../components/ha-password-field";
+import { haStyle } from "../../resources/styles";
+import type { LocalizeFunc } from "../../common/translations/localize";
+import {
+ CORE_LOCAL_AGENT,
+ HASSIO_LOCAL_AGENT,
+ type BackupContentExtended,
+ type BackupData,
+} from "../../data/backup";
+import { restoreOnboardingBackup } from "../../data/backup_onboarding";
+import type { HaProgressButton } from "../../components/buttons/ha-progress-button";
+import { fireEvent } from "../../common/dom/fire_event";
+
+@customElement("onboarding-restore-backup-restore")
+class OnboardingRestoreBackupRestore extends LitElement {
+ @property({ attribute: false }) public localize!: LocalizeFunc;
+
+ @property({ attribute: false }) public backup!: BackupContentExtended;
+
+ @property({ attribute: false })
+ public selectedData!: BackupData;
+
+ @property({ type: Boolean }) public supervisor = false;
+
+ @state() private _encryptionKey = "";
+
+ @state() private _encryptionKeyWrong = false;
+
+ @state() private _error?: string;
+
+ @state() private _loading = false;
+
+ render() {
+ const agentId = this.supervisor ? HASSIO_LOCAL_AGENT : CORE_LOCAL_AGENT;
+ const backupProtected = this.backup.agents[agentId].protected;
+
+ return html`
+ ${this.backup.homeassistant_included &&
+ !this.supervisor &&
+ (this.backup.addons.length > 0 || this.backup.folders.length > 0)
+ ? html`
+ ${this.localize(
+ "ui.panel.page-onboarding.restore.details.addons_unsupported"
+ )}
+ `
+ : nothing}
+
+
+ ${this._error
+ ? html`
${this._error} `
+ : nothing}
+
+ ${this.localize(
+ "ui.panel.page-onboarding.restore.confirm_restore_full_backup_text"
+ )}
+
+ ${backupProtected
+ ? html`
+ ${this.localize(
+ "ui.panel.page-onboarding.restore.details.restore.encryption.title"
+ )}
+
+ ${this._encryptionKeyWrong
+ ? html`
+
+ ${this.localize(
+ "ui.panel.page-onboarding.restore.details.restore.encryption.incorrect_key"
+ )}
+
+ `
+ : nothing}
+
`
+ : nothing}
+
+
+
+ ${this.localize(
+ "ui.panel.page-onboarding.restore.details.restore.action"
+ )}
+
+
+
+ `;
+ }
+
+ private _encryptionKeyChanged(ev): void {
+ this._encryptionKey = ev.target.value;
+ }
+
+ private async _startRestore(ev: CustomEvent): Promise {
+ const button = ev.currentTarget as HaProgressButton;
+ this._loading = true;
+ this._error = undefined;
+ this._encryptionKeyWrong = false;
+
+ const backupAgent = this.supervisor ? HASSIO_LOCAL_AGENT : CORE_LOCAL_AGENT;
+
+ try {
+ await restoreOnboardingBackup({
+ agent_id: backupAgent,
+ backup_id: this.backup.backup_id,
+ password: this._encryptionKey || undefined,
+ restore_addons: this.selectedData.addons.map((addon) => addon.slug),
+ restore_database: this.selectedData.database_included,
+ restore_folders: this.selectedData.folders,
+ });
+ button.actionSuccess();
+ fireEvent(this, "restore-started");
+ } catch (err: any) {
+ if (err.error === "Request error") {
+ // core can shutdown before we get a response
+ button.actionSuccess();
+ fireEvent(this, "restore-started");
+ return;
+ }
+
+ button.actionError();
+ if (err.body?.message === "incorrect_password") {
+ this._encryptionKeyWrong = true;
+ } else {
+ this._error =
+ err.body?.message || err.message || "Unknown error occurred";
+ }
+ this._loading = false;
+ }
+ }
+
+ static get styles(): CSSResultGroup {
+ return [
+ haStyle,
+ css`
+ :host {
+ padding: 28px 20px 0;
+ }
+ .card-actions {
+ display: flex;
+ justify-content: flex-end;
+ }
+ .supervisor-warning {
+ display: block;
+ margin-bottom: 16px;
+ }
+ `,
+ ];
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "onboarding-restore-backup-restore": OnboardingRestoreBackupRestore;
+ }
+ interface HASSDomEvents {
+ "restore-started";
+ }
+}
diff --git a/src/onboarding/restore-backup/onboarding-restore-backup-status.ts b/src/onboarding/restore-backup/onboarding-restore-backup-status.ts
new file mode 100644
index 0000000000..6ce7ce01b8
--- /dev/null
+++ b/src/onboarding/restore-backup/onboarding-restore-backup-status.ts
@@ -0,0 +1,119 @@
+import { css, html, LitElement, nothing, type CSSResultGroup } from "lit";
+import { customElement, property } from "lit/decorators";
+import "../../components/ha-card";
+import "../../components/ha-circular-progress";
+import "../../components/ha-alert";
+import "../../components/ha-button";
+import { haStyle } from "../../resources/styles";
+import type { LocalizeFunc } from "../../common/translations/localize";
+import type { BackupOnboardingInfo } from "../../data/backup_onboarding";
+import { fireEvent } from "../../common/dom/fire_event";
+import { navigate } from "../../common/navigate";
+import { removeSearchParam } from "../../common/url/search-params";
+
+@customElement("onboarding-restore-backup-status")
+class OnboardingRestoreBackupStatus extends LitElement {
+ @property({ attribute: false }) public localize!: LocalizeFunc;
+
+ @property({ attribute: false })
+ public backupInfo!: BackupOnboardingInfo;
+
+ render() {
+ return html`
+
+
+ ${this.backupInfo.state === "restore_backup"
+ ? html`
+
+
+
+
+ ${this.localize(
+ "ui.panel.page-onboarding.restore.in_progress_description"
+ )}
+
+ `
+ : html`
+
+ ${this.localize(
+ "ui.panel.page-onboarding.restore.failed_status_description"
+ )}
+
+ ${this.backupInfo.last_non_idle_event?.reason
+ ? html`
+
+
Error:
+ ${this.backupInfo.last_non_idle_event?.reason}
+
+ `
+ : nothing}
+ `}
+
+ ${this.backupInfo.state !== "restore_backup"
+ ? html`
+
+ ${this.localize(
+ `ui.panel.page-onboarding.restore.details.summary.upload_another`
+ )}
+
+
+ ${this.localize(
+ `ui.panel.page-onboarding.restore.details.summary.home`
+ )}
+
+
`
+ : nothing}
+
+ `;
+ }
+
+ private _uploadAnother() {
+ fireEvent(this, "show-backup-upload");
+ }
+
+ private _home() {
+ navigate(`${location.pathname}?${removeSearchParam("page")}`);
+ }
+
+ static get styles(): CSSResultGroup {
+ return [
+ haStyle,
+ css`
+ :host {
+ padding: 28px 20px 0;
+ }
+ .card-actions {
+ display: flex;
+ justify-content: flex-end;
+ }
+ .loading {
+ display: flex;
+ justify-content: center;
+ padding: 32px;
+ }
+ p {
+ text-align: center;
+ padding: 0 16px;
+ font-size: 16px;
+ }
+ .failed {
+ padding: 16px 0;
+ font-size: 16px;
+ }
+ `,
+ ];
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "onboarding-restore-backup-status": OnboardingRestoreBackupStatus;
+ }
+ interface HASSDomEvents {
+ "restore-started";
+ }
+}
diff --git a/src/onboarding/restore-backup/onboarding-restore-backup-upload.ts b/src/onboarding/restore-backup/onboarding-restore-backup-upload.ts
new file mode 100644
index 0000000000..2927fe86a2
--- /dev/null
+++ b/src/onboarding/restore-backup/onboarding-restore-backup-upload.ts
@@ -0,0 +1,126 @@
+import { mdiFolderUpload } from "@mdi/js";
+import { css, html, LitElement, nothing, type CSSResultGroup } from "lit";
+import { customElement, property, state } from "lit/decorators";
+import "../../components/ha-card";
+import "../../components/ha-file-upload";
+import "../../components/ha-alert";
+import { haStyle } from "../../resources/styles";
+import { fireEvent, type HASSDomEvent } from "../../common/dom/fire_event";
+import { showAlertDialog } from "../../dialogs/generic/show-dialog-box";
+import {
+ CORE_LOCAL_AGENT,
+ HASSIO_LOCAL_AGENT,
+ SUPPORTED_UPLOAD_FORMAT,
+} from "../../data/backup";
+import type { LocalizeFunc } from "../../common/translations/localize";
+import { uploadOnboardingBackup } from "../../data/backup_onboarding";
+
+declare global {
+ interface HASSDomEvents {
+ "backup-uploaded": { backupId: string };
+ }
+}
+@customElement("onboarding-restore-backup-upload")
+class OnboardingRestoreBackupUpload extends LitElement {
+ @property({ type: Boolean }) public supervisor = false;
+
+ @property({ attribute: false }) public localize!: LocalizeFunc;
+
+ @state() private _uploading = false;
+
+ @state() private _error?: string;
+
+ render() {
+ return html`
+
+
+ ${this._error
+ ? html`${this._error}`
+ : nothing}
+
+
+
+ `;
+ }
+
+ private async _filePicked(ev: HASSDomEvent<{ files: File[] }>) {
+ this._error = undefined;
+ const file = ev.detail.files[0];
+
+ if (!file || file.type !== SUPPORTED_UPLOAD_FORMAT) {
+ showAlertDialog(this, {
+ title: this.localize(
+ "ui.panel.page-onboarding.restore.unsupported.title"
+ ),
+ text: this.localize(
+ "ui.panel.page-onboarding.restore.unsupported.text"
+ ),
+ confirmText: this.localize("ui.panel.page-onboarding.restore.ok"),
+ });
+ return;
+ }
+
+ const agentIds = this.supervisor
+ ? [HASSIO_LOCAL_AGENT]
+ : [CORE_LOCAL_AGENT];
+
+ this._uploading = true;
+ try {
+ const { backup_id } = await uploadOnboardingBackup(file, agentIds);
+ fireEvent(this, "backup-uploaded", { backupId: backup_id });
+ } catch (err: any) {
+ this._error =
+ typeof err.body === "string"
+ ? err.body
+ : err.body?.message || err.message || "Unknown error occurred";
+ } finally {
+ this._uploading = false;
+ }
+ }
+
+ static get styles(): CSSResultGroup {
+ return [
+ haStyle,
+ css`
+ :host {
+ width: 100%;
+ }
+ .card-actions {
+ display: flex;
+ justify-content: flex-end;
+ }
+ `,
+ ];
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "onboarding-restore-backup-upload": OnboardingRestoreBackupUpload;
+ }
+}
diff --git a/src/panels/config/backup/components/ha-backup-addons-picker.ts b/src/panels/config/backup/components/ha-backup-addons-picker.ts
index e298a870f9..eb3a20e794 100644
--- a/src/panels/config/backup/components/ha-backup-addons-picker.ts
+++ b/src/panels/config/backup/components/ha-backup-addons-picker.ts
@@ -21,7 +21,7 @@ export interface BackupAddonItem {
@customElement("ha-backup-addons-picker")
export class HaBackupAddonsPicker extends LitElement {
- @property({ attribute: false }) public hass!: HomeAssistant;
+ @property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public addons!: BackupAddonItem[];
@@ -32,7 +32,7 @@ export class HaBackupAddonsPicker extends LitElement {
private _addons = memoizeOne((addons: BackupAddonItem[]) =>
addons.sort((a, b) =>
- stringCompare(a.name, b.name, this.hass.locale.language)
+ stringCompare(a.name, b.name, this.hass?.locale?.language)
)
);
diff --git a/src/panels/config/backup/components/ha-backup-data-picker.ts b/src/panels/config/backup/components/ha-backup-data-picker.ts
index 96a72a348a..b1276b89f5 100644
--- a/src/panels/config/backup/components/ha-backup-data-picker.ts
+++ b/src/panels/config/backup/components/ha-backup-data-picker.ts
@@ -47,23 +47,32 @@ interface SelectedItems {
@customElement("ha-backup-data-picker")
export class HaBackupDataPicker extends LitElement {
- @property({ attribute: false }) public hass!: HomeAssistant;
+ @property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public data!: BackupData;
@property({ attribute: false }) public value?: BackupData;
+ @property({ attribute: false }) public localize?: LocalizeFunc;
+
+ @property({ type: Array, attribute: "required-items" })
+ public requiredItems: string[] = [];
+
+ @property({ attribute: "translation-key-panel" }) public translationKeyPanel:
+ | "page-onboarding.restore"
+ | "config.backup" = "config.backup";
+
@state() public _addonIcons: Record = {};
protected firstUpdated(changedProps: PropertyValues): void {
super.firstUpdated(changedProps);
- if (isComponentLoaded(this.hass, "hassio")) {
+ if (this.hass && isComponentLoaded(this.hass, "hassio")) {
this._fetchAddonInfo();
}
}
private async _fetchAddonInfo() {
- const { addons } = await fetchHassioAddonsInfo(this.hass);
+ const { addons } = await fetchHassioAddonsInfo(this.hass!);
this._addonIcons = addons.reduce>(
(acc, addon) => ({
...acc,
@@ -74,16 +83,14 @@ export class HaBackupDataPicker extends LitElement {
}
private _homeAssistantItems = memoizeOne(
- (data: BackupData, _localize: LocalizeFunc) => {
+ (data: BackupData, localize: LocalizeFunc) => {
const items: CheckBoxItem[] = [];
if (data.homeassistant_included) {
items.push({
- label: data.database_included
- ? this.hass.localize(
- "ui.panel.config.backup.data_picker.settings_and_history"
- )
- : this.hass.localize("ui.panel.config.backup.data_picker.settings"),
+ label: localize(
+ `ui.panel.${this.translationKeyPanel}.data_picker.${data.database_included ? "settings_and_history" : "settings"}`
+ ),
id: "config",
version: data.homeassistant_version,
});
@@ -99,18 +106,22 @@ export class HaBackupDataPicker extends LitElement {
);
private _localizeFolder(folder: string): string {
+ const localize = this.localize || this.hass!.localize;
+
switch (folder) {
case "media":
- return this.hass.localize("ui.panel.config.backup.data_picker.media");
+ return localize(
+ `ui.panel.${this.translationKeyPanel}.data_picker.media`
+ );
case "share":
- return this.hass.localize(
- "ui.panel.config.backup.data_picker.share_folder"
+ return localize(
+ `ui.panel.${this.translationKeyPanel}.data_picker.share_folder`
);
case "ssl":
- return this.hass.localize("ui.panel.config.backup.data_picker.ssl");
+ return localize(`ui.panel.${this.translationKeyPanel}.data_picker.ssl`);
case "addons/local":
- return this.hass.localize(
- "ui.panel.config.backup.data_picker.local_addons"
+ return localize(
+ `ui.panel.${this.translationKeyPanel}.data_picker.local_addons`
);
}
return capitalizeFirstLetter(folder);
@@ -215,14 +226,13 @@ export class HaBackupDataPicker extends LitElement {
}
protected render() {
- const homeAssistantItems = this._homeAssistantItems(
- this.data,
- this.hass.localize
- );
+ const localize = this.localize || this.hass!.localize;
+
+ const homeAssistantItems = this._homeAssistantItems(this.data, localize);
const addonsItems = this._addonsItems(
this.data,
- this.hass.localize,
+ localize,
this._addonIcons
);
@@ -247,6 +257,7 @@ export class HaBackupDataPicker extends LitElement {
selectedItems.homeassistant.length <
homeAssistantItems.length}
@change=${this._sectionChanged}
+ ?disabled=${this.requiredItems.length > 0}
>
@@ -266,6 +277,7 @@ export class HaBackupDataPicker extends LitElement {
item.id
)}
@change=${this._homeassistantChanged}
+ .disabled=${this.requiredItems.includes(item.id)}
>
`
@@ -280,8 +292,8 @@ export class HaBackupDataPicker extends LitElement {
diff --git a/src/panels/config/backup/components/ha-backup-details-restore.ts b/src/panels/config/backup/components/ha-backup-details-restore.ts
new file mode 100644
index 0000000000..170a39bf5b
--- /dev/null
+++ b/src/panels/config/backup/components/ha-backup-details-restore.ts
@@ -0,0 +1,148 @@
+import memoizeOne from "memoize-one";
+import { css, html, LitElement } from "lit";
+import { customElement, property, state } from "lit/decorators";
+import "../../../../components/ha-card";
+import "../../../../components/ha-md-list";
+import "../../../../components/ha-md-list-item";
+import "../../../../components/ha-button";
+import "./ha-backup-data-picker";
+import type { HomeAssistant } from "../../../../types";
+import type { LocalizeFunc } from "../../../../common/translations/localize";
+import type {
+ BackupContentExtended,
+ BackupData,
+} from "../../../../data/backup";
+import { fireEvent } from "../../../../common/dom/fire_event";
+
+@customElement("ha-backup-details-restore")
+class HaBackupDetailsRestore extends LitElement {
+ @property({ attribute: false }) public hass?: HomeAssistant;
+
+ @property({ attribute: false }) public localize!: LocalizeFunc;
+
+ @property({ type: Object }) public backup!: BackupContentExtended;
+
+ @property({ type: Boolean, attribute: "ha-required" })
+ public haRequired = false;
+
+ @property({ attribute: "translation-key-panel" }) public translationKeyPanel:
+ | "page-onboarding.restore"
+ | "config.backup" = "config.backup";
+
+ @state() private _selectedData?: BackupData;
+
+ protected willUpdate() {
+ if (!this.hasUpdated && this.haRequired) {
+ this._selectedData = {
+ homeassistant_included: true,
+ folders: [],
+ addons: [],
+ homeassistant_version: this.backup.homeassistant_version,
+ database_included: this.backup.database_included,
+ };
+ }
+ }
+
+ render() {
+ return html`
+
+
+
+
+
+
+
+
+ ${this.localize(
+ `ui.panel.${this.translationKeyPanel}.details.restore.action`
+ )}
+
+
+
+ `;
+ }
+
+ private _restore() {
+ fireEvent(this, "backup-restore", { selectedData: this._selectedData });
+ }
+
+ private _selectedBackupChanged(ev: CustomEvent) {
+ ev.stopPropagation();
+ this._selectedData = ev.detail.value;
+ }
+
+ private _isHomeAssistantRequired = memoizeOne((required: boolean) =>
+ required ? ["config"] : []
+ );
+
+ private get _isRestoreDisabled(): boolean {
+ return (
+ !this._selectedData ||
+ (this.haRequired && !this._selectedData.homeassistant_included) ||
+ !(
+ this._selectedData?.database_included ||
+ this._selectedData?.homeassistant_included ||
+ this._selectedData.addons.length ||
+ this._selectedData.folders.length
+ )
+ );
+ }
+
+ static styles = css`
+ :host {
+ max-width: 690px;
+ width: 100%;
+ margin: 0 auto;
+ gap: 24px;
+ display: grid;
+ }
+ .card-content {
+ padding: 0 20px;
+ }
+ .card-actions {
+ display: flex;
+ justify-content: flex-end;
+ }
+ ha-md-list {
+ background: none;
+ padding: 0;
+ }
+ ha-md-list-item {
+ --md-list-item-leading-space: 0;
+ --md-list-item-trailing-space: 0;
+ --md-list-item-two-line-container-height: 64px;
+ }
+ ha-md-list-item [slot="supporting-text"] {
+ display: flex;
+ align-items: center;
+ flex-direction: row;
+ gap: 8px;
+ line-height: normal;
+ }
+ `;
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "ha-backup-details-restore": HaBackupDetailsRestore;
+ }
+ interface HASSDomEvents {
+ "backup-restore": { selectedData?: BackupData };
+ }
+}
diff --git a/src/panels/config/backup/components/ha-backup-details-summary.ts b/src/panels/config/backup/components/ha-backup-details-summary.ts
new file mode 100644
index 0000000000..e39ffbc552
--- /dev/null
+++ b/src/panels/config/backup/components/ha-backup-details-summary.ts
@@ -0,0 +1,153 @@
+import { css, html, LitElement, nothing } from "lit";
+import { customElement, property } from "lit/decorators";
+import "../../../../components/ha-card";
+import "../../../../components/ha-md-list";
+import "../../../../components/ha-md-list-item";
+import "../../../../components/ha-button";
+import type { HomeAssistant } from "../../../../types";
+import type { LocalizeFunc } from "../../../../common/translations/localize";
+import {
+ formatDateTime,
+ formatDateTimeWithBrowserDefaults,
+} from "../../../../common/datetime/format_date_time";
+import {
+ computeBackupSize,
+ computeBackupType,
+ type BackupContentExtended,
+} from "../../../../data/backup";
+import { fireEvent } from "../../../../common/dom/fire_event";
+import { bytesToString } from "../../../../util/bytes-to-string";
+
+declare global {
+ interface HASSDomEvents {
+ "show-backup-upload": undefined;
+ }
+}
+
+@customElement("ha-backup-details-summary")
+class HaBackupDetailsSummary extends LitElement {
+ @property({ attribute: false }) public hass?: HomeAssistant;
+
+ @property({ attribute: false }) public localize!: LocalizeFunc;
+
+ @property({ type: Object }) public backup!: BackupContentExtended;
+
+ @property({ type: Boolean, attribute: "hassio" }) public isHassio = false;
+
+ @property({ attribute: "translation-key-panel" }) public translationKeyPanel:
+ | "page-onboarding.restore"
+ | "config.backup" = "config.backup";
+
+ @property({ type: Boolean, attribute: "show-upload-another" })
+ public showUploadAnother = false;
+
+ render() {
+ const backupDate = new Date(this.backup.date);
+ const formattedDate = this.hass
+ ? formatDateTime(backupDate, this.hass.locale, this.hass.config)
+ : formatDateTimeWithBrowserDefaults(backupDate);
+
+ return html`
+
+
+
+
+ ${this.translationKeyPanel === "config.backup"
+ ? html`
+
+ ${this.localize("ui.panel.config.backup.backup_type")}
+
+
+ ${this.localize(
+ `ui.panel.config.backup.type.${computeBackupType(this.backup, this.isHassio)}`
+ )}
+
+ `
+ : nothing}
+
+
+ ${this.localize(
+ `ui.panel.${this.translationKeyPanel}.details.summary.size`
+ )}
+
+
+ ${bytesToString(computeBackupSize(this.backup))}
+
+
+
+
+ ${this.localize(
+ `ui.panel.${this.translationKeyPanel}.details.summary.created`
+ )}
+
+ ${formattedDate}
+
+
+
+ ${this.showUploadAnother
+ ? html`
+
+ ${this.localize(
+ `ui.panel.page-onboarding.restore.details.summary.upload_another`
+ )}
+
+
`
+ : nothing}
+
+ `;
+ }
+
+ private _uploadAnother() {
+ fireEvent(this, "show-backup-upload");
+ }
+
+ static styles = css`
+ :host {
+ max-width: 690px;
+ width: 100%;
+ margin: 0 auto;
+ gap: 24px;
+ display: grid;
+ }
+ .card-content {
+ padding: 0 20px;
+ }
+ .card-actions {
+ display: flex;
+ justify-content: flex-end;
+ }
+ ha-md-list {
+ background: none;
+ padding: 0;
+ }
+ ha-md-list-item {
+ --md-list-item-leading-space: 0;
+ --md-list-item-trailing-space: 0;
+ --md-list-item-two-line-container-height: 64px;
+ }
+ ha-md-list.summary ha-md-list-item {
+ --md-list-item-supporting-text-size: 1rem;
+ --md-list-item-label-text-size: 0.875rem;
+
+ --md-list-item-label-text-color: var(--secondary-text-color);
+ --md-list-item-supporting-text-color: var(--primary-text-color);
+ }
+ ha-md-list-item [slot="supporting-text"] {
+ display: flex;
+ align-items: center;
+ flex-direction: row;
+ gap: 8px;
+ line-height: normal;
+ }
+ `;
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "ha-backup-details-summary": HaBackupDetailsSummary;
+ }
+}
diff --git a/src/panels/config/backup/dialogs/dialog-upload-backup.ts b/src/panels/config/backup/dialogs/dialog-upload-backup.ts
index 66f9dcfaaa..ab396fd4d1 100644
--- a/src/panels/config/backup/dialogs/dialog-upload-backup.ts
+++ b/src/panels/config/backup/dialogs/dialog-upload-backup.ts
@@ -3,7 +3,10 @@ import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { isComponentLoaded } from "../../../../common/config/is_component_loaded";
-import { fireEvent } from "../../../../common/dom/fire_event";
+import {
+ fireEvent,
+ type HASSDomEvent,
+} from "../../../../common/dom/fire_event";
import "../../../../components/ha-alert";
import "../../../../components/ha-dialog-header";
import "../../../../components/ha-expansion-panel";
@@ -14,7 +17,10 @@ import type { HaMdDialog } from "../../../../components/ha-md-dialog";
import {
CORE_LOCAL_AGENT,
HASSIO_LOCAL_AGENT,
+ SUPPORTED_UPLOAD_FORMAT,
uploadBackup,
+ INITIAL_UPLOAD_FORM_DATA,
+ type BackupUploadFileFormData,
} from "../../../../data/backup";
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
import { haStyle, haStyleDialog } from "../../../../resources/styles";
@@ -22,16 +28,6 @@ import type { HomeAssistant } from "../../../../types";
import { showAlertDialog } from "../../../lovelace/custom-card-helpers";
import type { UploadBackupDialogParams } from "./show-dialog-upload-backup";
-const SUPPORTED_FORMAT = "application/x-tar";
-
-interface FormData {
- file?: File;
-}
-
-const INITIAL_DATA: FormData = {
- file: undefined,
-};
-
@customElement("ha-dialog-upload-backup")
export class DialogUploadBackup
extends LitElement
@@ -45,13 +41,13 @@ export class DialogUploadBackup
@state() private _error?: string;
- @state() private _formData?: FormData;
+ @state() private _formData?: BackupUploadFileFormData;
@query("ha-md-dialog") private _dialog?: HaMdDialog;
public async showDialog(params: UploadBackupDialogParams): Promise {
this._params = params;
- this._formData = INITIAL_DATA;
+ this._formData = INITIAL_UPLOAD_FORM_DATA;
}
private _dialogClosed() {
@@ -78,13 +74,18 @@ export class DialogUploadBackup
}
return html`
-
+
@@ -99,7 +100,8 @@ export class DialogUploadBackup
.hass=${this.hass}
.uploading=${this._uploading}
.icon=${mdiFolderUpload}
- accept=${SUPPORTED_FORMAT}
+ .accept=${SUPPORTED_UPLOAD_FORMAT}
+ .localize=${this.hass.localize}
.label=${this.hass.localize(
"ui.panel.config.backup.dialogs.upload.input_label"
)}
@@ -107,13 +109,17 @@ export class DialogUploadBackup
"ui.panel.config.backup.dialogs.upload.supports_tar"
)}
@file-picked=${this._filePicked}
+ @files-cleared=${this._filesCleared}
>
-
${this.hass.localize("ui.common.cancel")}
-
+
${this.hass.localize(
"ui.panel.config.backup.dialogs.upload.action"
)}
@@ -123,7 +129,7 @@ export class DialogUploadBackup
`;
}
- private async _filePicked(ev: CustomEvent<{ files: File[] }>): Promise {
+ private _filePicked(ev: HASSDomEvent<{ files: File[] }>) {
this._error = undefined;
const file = ev.detail.files[0];
@@ -133,9 +139,14 @@ export class DialogUploadBackup
};
}
+ private _filesCleared() {
+ this._error = undefined;
+ this._formData = INITIAL_UPLOAD_FORM_DATA;
+ }
+
private async _upload() {
const { file } = this._formData!;
- if (!file || file.type !== SUPPORTED_FORMAT) {
+ if (!file || file.type !== SUPPORTED_UPLOAD_FORMAT) {
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.backup.dialogs.upload.unsupported.title"
@@ -154,7 +165,7 @@ export class DialogUploadBackup
this._uploading = true;
try {
- await uploadBackup(this.hass!, file, agentIds);
+ await uploadBackup(this.hass, file, agentIds);
this._params!.submit?.();
this.closeDialog();
} catch (err: any) {
diff --git a/src/panels/config/backup/ha-config-backup-details.ts b/src/panels/config/backup/ha-config-backup-details.ts
index c3bbd93552..42a553c098 100644
--- a/src/panels/config/backup/ha-config-backup-details.ts
+++ b/src/panels/config/backup/ha-config-backup-details.ts
@@ -8,7 +8,6 @@ import {
} from "@mdi/js";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
-import { formatDateTime } from "../../../common/datetime/format_date_time";
import { computeDomain } from "../../../common/entity/compute_domain";
import { navigate } from "../../../common/navigate";
import "../../../components/ha-alert";
@@ -25,24 +24,20 @@ import type {
BackupConfig,
BackupContentAgent,
BackupContentExtended,
- BackupData,
} from "../../../data/backup";
+import "./components/ha-backup-details-summary";
+import "./components/ha-backup-details-restore";
import {
compareAgents,
computeBackupAgentName,
- computeBackupSize,
- computeBackupType,
deleteBackup,
fetchBackupDetails,
isLocalAgent,
isNetworkMountAgent,
} from "../../../data/backup";
-import type { HassioAddonInfo } from "../../../data/hassio/addon";
import "../../../layouts/hass-subpage";
import type { HomeAssistant } from "../../../types";
import { brandsUrl } from "../../../util/brands-url";
-import { bytesToString } from "../../../util/bytes-to-string";
-import "./components/ha-backup-data-picker";
import { showRestoreBackupDialog } from "./dialogs/show-dialog-restore-backup";
import { fireEvent } from "../../../common/dom/fire_event";
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
@@ -93,10 +88,6 @@ class HaConfigBackupDetails extends LitElement {
@state() private _error?: string;
- @state() private _selectedData?: BackupData;
-
- @state() private _addonsInfo?: HassioAddonInfo[];
-
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
@@ -157,81 +148,18 @@ class HaConfigBackupDetails extends LitElement {
: !this._backup
? html``
: html`
-
-
-
-
-
-
- ${this.hass.localize(
- "ui.panel.config.backup.backup_type"
- )}
-
-
- ${this.hass.localize(
- `ui.panel.config.backup.type.${computeBackupType(this._backup, isHassio)}`
- )}
-
-
-
-
- ${this.hass.localize(
- "ui.panel.config.backup.details.summary.size"
- )}
-
-
- ${bytesToString(computeBackupSize(this._backup))}
-
-
-
-
- ${this.hass.localize(
- "ui.panel.config.backup.details.summary.created"
- )}
-
-
- ${formatDateTime(
- new Date(this._backup.date),
- this.hass.locale,
- this.hass.config
- )}
-
-
-
-
-
-
-
-
-
-
-
-
-
- ${this.hass.localize(
- "ui.panel.config.backup.details.restore.action"
- )}
-
-
-
+
+