mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-15 21:36:36 +00:00
Onboarding restore use core api (#23920)
* Fix type issues * Extract backup-upload * Add onboarding upload section * Extract and use ha-backup-details * Implement backup details and restore * remove unused hassio onboarding calls * Require hass in dialog-hassio-backup * Add restore view * Add formatDateTime without locale and config * Add restore status * Fix prettier * Fix styles of backup details * Remove unused localize * Fix onboarding restore translations * Hide data-picker on core only instance * Split uploadBackup into 2 separate funcs * Add formatDateTimeWithBrowserDefaults * Fix selected data for core only * Show error reasons on status page * Use new backup info agents * Add mem function for formatDateTimeWithBrowserDefaults * Fix overflow on mobile * Handle errors when in hassio mode * Fix cancel restore texts * Fix hassio localize type issue * Remove unused onboarding localize in hassio backup restore * improve format_date_time * Fix tests * Fix and simplify backup upload issues * Use foreach instead of reduce * Fix attributes, unused styles and properties * Simplify supervisor warning * Fix language type issues * Fix ha-backup-data-picker * Improve backup-details-restore * Fix onboarding-restore issues * Improve loadBackupInfo * Revert uploadBackup changes * Improve cancel restore * Use destructive * Update src/panels/config/backup/dialogs/dialog-upload-backup.ts Co-authored-by: Bram Kragten <mail@bramkragten.nl> * Show backup type not at onboarding * Only show backup type in correct translationPanel * Fix quotes --------- Co-authored-by: Bram Kragten <mail@bramkragten.nl>
This commit is contained in:
parent
44cc75afbc
commit
03a415beff
@ -14,7 +14,7 @@ import type { LocalizeFunc } from "../../../src/common/translations/localize";
|
|||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HASSDomEvents {
|
interface HASSDomEvents {
|
||||||
"backup-uploaded": { backup: HassioBackup };
|
"hassio-backup-uploaded": { backup: HassioBackup };
|
||||||
"backup-cleared": undefined;
|
"backup-cleared": undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -70,7 +70,7 @@ export class HassioUploadBackup extends LitElement {
|
|||||||
this._uploading = true;
|
this._uploading = true;
|
||||||
try {
|
try {
|
||||||
const backup = await uploadBackup(this.hass, file);
|
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) {
|
} catch (err: any) {
|
||||||
showAlertDialog(this, {
|
showAlertDialog(this, {
|
||||||
title: "Upload failed",
|
title: "Upload failed",
|
||||||
|
@ -5,7 +5,6 @@ import { customElement, property, query } from "lit/decorators";
|
|||||||
import { atLeastVersion } from "../../../src/common/config/version";
|
import { atLeastVersion } from "../../../src/common/config/version";
|
||||||
import { formatDate } from "../../../src/common/datetime/format_date";
|
import { formatDate } from "../../../src/common/datetime/format_date";
|
||||||
import { formatDateTime } from "../../../src/common/datetime/format_date_time";
|
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-checkbox";
|
||||||
import "../../../src/components/ha-formfield";
|
import "../../../src/components/ha-formfield";
|
||||||
import "../../../src/components/ha-textfield";
|
import "../../../src/components/ha-textfield";
|
||||||
@ -19,13 +18,10 @@ import type {
|
|||||||
} from "../../../src/data/hassio/backup";
|
} from "../../../src/data/hassio/backup";
|
||||||
import type { Supervisor } from "../../../src/data/supervisor/supervisor";
|
import type { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||||
import { mdiHomeAssistant } from "../../../src/resources/home-assistant-logo-svg";
|
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 "./supervisor-formfield-label";
|
||||||
import type { HaTextField } from "../../../src/components/ha-textfield";
|
import type { HaTextField } from "../../../src/components/ha-textfield";
|
||||||
|
|
||||||
type BackupOrRestoreKey = keyof TranslationDict["supervisor"]["backup"] &
|
|
||||||
keyof TranslationDict["ui"]["panel"]["page-onboarding"]["restore"];
|
|
||||||
|
|
||||||
interface CheckboxItem {
|
interface CheckboxItem {
|
||||||
slug: string;
|
slug: string;
|
||||||
checked: boolean;
|
checked: boolean;
|
||||||
@ -67,8 +63,6 @@ const _computeAddons = (addons): AddonCheckboxItem[] =>
|
|||||||
export class SupervisorBackupContent extends LitElement {
|
export class SupervisorBackupContent extends LitElement {
|
||||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||||
|
|
||||||
@property({ attribute: false }) public localize?: LocalizeFunc;
|
|
||||||
|
|
||||||
@property({ attribute: false }) public supervisor?: Supervisor;
|
@property({ attribute: false }) public supervisor?: Supervisor;
|
||||||
|
|
||||||
@property({ attribute: false }) public backup?: HassioBackupDetail;
|
@property({ attribute: false }) public backup?: HassioBackupDetail;
|
||||||
@ -115,10 +109,6 @@ export class SupervisorBackupContent extends LitElement {
|
|||||||
this._focusTarget?.focus();
|
this._focusTarget?.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
private _localize = (key: BackupOrRestoreKey) =>
|
|
||||||
this.supervisor?.localize(`backup.${key}`) ||
|
|
||||||
this.localize!(`ui.panel.page-onboarding.restore.${key}`);
|
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
if (!this.onboarding && !this.supervisor) {
|
if (!this.onboarding && !this.supervisor) {
|
||||||
return nothing;
|
return nothing;
|
||||||
@ -132,8 +122,8 @@ export class SupervisorBackupContent extends LitElement {
|
|||||||
${this.backup
|
${this.backup
|
||||||
? html`<div class="details">
|
? html`<div class="details">
|
||||||
${this.backup.type === "full"
|
${this.backup.type === "full"
|
||||||
? this._localize("full_backup")
|
? this.supervisor?.localize("backup.full_backup")
|
||||||
: this._localize("partial_backup")}
|
: this.supervisor?.localize("backup.partial_backup")}
|
||||||
(${Math.ceil(this.backup.size * 10) / 10 + " MB"})<br />
|
(${Math.ceil(this.backup.size * 10) / 10 + " MB"})<br />
|
||||||
${this.hass
|
${this.hass
|
||||||
? formatDateTime(
|
? formatDateTime(
|
||||||
@ -145,7 +135,7 @@ export class SupervisorBackupContent extends LitElement {
|
|||||||
</div>`
|
</div>`
|
||||||
: html`<ha-textfield
|
: html`<ha-textfield
|
||||||
name="backupName"
|
name="backupName"
|
||||||
.label=${this._localize("name")}
|
.label=${this.supervisor?.localize("backup.name")}
|
||||||
.value=${this.backupName}
|
.value=${this.backupName}
|
||||||
@change=${this._handleTextValueChanged}
|
@change=${this._handleTextValueChanged}
|
||||||
>
|
>
|
||||||
@ -153,11 +143,13 @@ export class SupervisorBackupContent extends LitElement {
|
|||||||
${!this.backup || this.backup.type === "full"
|
${!this.backup || this.backup.type === "full"
|
||||||
? html`<div class="sub-header">
|
? html`<div class="sub-header">
|
||||||
${!this.backup
|
${!this.backup
|
||||||
? this._localize("type")
|
? this.supervisor?.localize("backup.type")
|
||||||
: this._localize("select_type")}
|
: this.supervisor?.localize("backup.select_type")}
|
||||||
</div>
|
</div>
|
||||||
<div class="backup-types">
|
<div class="backup-types">
|
||||||
<ha-formfield .label=${this._localize("full_backup")}>
|
<ha-formfield
|
||||||
|
.label=${this.supervisor?.localize("backup.full_backup")}
|
||||||
|
>
|
||||||
<ha-radio
|
<ha-radio
|
||||||
@change=${this._handleRadioValueChanged}
|
@change=${this._handleRadioValueChanged}
|
||||||
value="full"
|
value="full"
|
||||||
@ -166,7 +158,9 @@ export class SupervisorBackupContent extends LitElement {
|
|||||||
>
|
>
|
||||||
</ha-radio>
|
</ha-radio>
|
||||||
</ha-formfield>
|
</ha-formfield>
|
||||||
<ha-formfield .label=${this._localize("partial_backup")}>
|
<ha-formfield
|
||||||
|
.label=${this.supervisor?.localize("backup.partial_backup")}
|
||||||
|
>
|
||||||
<ha-radio
|
<ha-radio
|
||||||
@change=${this._handleRadioValueChanged}
|
@change=${this._handleRadioValueChanged}
|
||||||
value="partial"
|
value="partial"
|
||||||
@ -202,7 +196,7 @@ export class SupervisorBackupContent extends LitElement {
|
|||||||
? html`
|
? html`
|
||||||
<ha-formfield
|
<ha-formfield
|
||||||
.label=${html`<supervisor-formfield-label
|
.label=${html`<supervisor-formfield-label
|
||||||
.label=${this._localize("folders")}
|
.label=${this.supervisor?.localize("backup.folders")}
|
||||||
.iconPath=${mdiFolder}
|
.iconPath=${mdiFolder}
|
||||||
>
|
>
|
||||||
</supervisor-formfield-label>`}
|
</supervisor-formfield-label>`}
|
||||||
@ -222,7 +216,7 @@ export class SupervisorBackupContent extends LitElement {
|
|||||||
? html`
|
? html`
|
||||||
<ha-formfield
|
<ha-formfield
|
||||||
.label=${html`<supervisor-formfield-label
|
.label=${html`<supervisor-formfield-label
|
||||||
.label=${this._localize("addons")}
|
.label=${this.supervisor?.localize("backup.addons")}
|
||||||
.iconPath=${mdiPuzzle}
|
.iconPath=${mdiPuzzle}
|
||||||
>
|
>
|
||||||
</supervisor-formfield-label>`}
|
</supervisor-formfield-label>`}
|
||||||
@ -247,7 +241,7 @@ export class SupervisorBackupContent extends LitElement {
|
|||||||
${!this.backup
|
${!this.backup
|
||||||
? html`<ha-formfield
|
? html`<ha-formfield
|
||||||
class="password"
|
class="password"
|
||||||
.label=${this._localize("password_protection")}
|
.label=${this.supervisor?.localize("backup.password_protection")}
|
||||||
>
|
>
|
||||||
<ha-checkbox
|
<ha-checkbox
|
||||||
.checked=${this.backupHasPassword}
|
.checked=${this.backupHasPassword}
|
||||||
@ -259,7 +253,7 @@ export class SupervisorBackupContent extends LitElement {
|
|||||||
${this.backupHasPassword
|
${this.backupHasPassword
|
||||||
? html`
|
? html`
|
||||||
<ha-password-field
|
<ha-password-field
|
||||||
.label=${this._localize("password")}
|
.label=${this.supervisor?.localize("backup.password")}
|
||||||
name="backupPassword"
|
name="backupPassword"
|
||||||
.value=${this.backupPassword}
|
.value=${this.backupPassword}
|
||||||
@change=${this._handleTextValueChanged}
|
@change=${this._handleTextValueChanged}
|
||||||
@ -267,7 +261,7 @@ export class SupervisorBackupContent extends LitElement {
|
|||||||
</ha-password-field>
|
</ha-password-field>
|
||||||
${!this.backup
|
${!this.backup
|
||||||
? html`<ha-password-field
|
? html`<ha-password-field
|
||||||
.label=${this._localize("confirm_password")}
|
.label=${this.supervisor?.localize("backup.confirm_password")}
|
||||||
name="confirmBackupPassword"
|
name="confirmBackupPassword"
|
||||||
.value=${this.confirmBackupPassword}
|
.value=${this.confirmBackupPassword}
|
||||||
@change=${this._handleTextValueChanged}
|
@change=${this._handleTextValueChanged}
|
||||||
|
@ -72,7 +72,7 @@ export class DialogHassioBackupUpload
|
|||||||
</ha-header-bar>
|
</ha-header-bar>
|
||||||
</div>
|
</div>
|
||||||
<hassio-upload-backup
|
<hassio-upload-backup
|
||||||
@backup-uploaded=${this._backupUploaded}
|
@hassio-backup-uploaded=${this._backupUploaded}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
></hassio-upload-backup>
|
></hassio-upload-backup>
|
||||||
</ha-dialog>
|
</ha-dialog>
|
||||||
|
@ -35,7 +35,6 @@ import { fileDownload } from "../../../../src/util/file_download";
|
|||||||
import "../../components/supervisor-backup-content";
|
import "../../components/supervisor-backup-content";
|
||||||
import type { SupervisorBackupContent } from "../../components/supervisor-backup-content";
|
import type { SupervisorBackupContent } from "../../components/supervisor-backup-content";
|
||||||
import type { HassioBackupDialogParams } from "./show-dialog-hassio-backup";
|
import type { HassioBackupDialogParams } from "./show-dialog-hassio-backup";
|
||||||
import type { BackupOrRestoreKey } from "../../util/translations";
|
|
||||||
import type { HaMdDialog } from "../../../../src/components/ha-md-dialog";
|
import type { HaMdDialog } from "../../../../src/components/ha-md-dialog";
|
||||||
|
|
||||||
@customElement("dialog-hassio-backup")
|
@customElement("dialog-hassio-backup")
|
||||||
@ -43,7 +42,7 @@ class HassioBackupDialog
|
|||||||
extends LitElement
|
extends LitElement
|
||||||
implements HassDialog<HassioBackupDialogParams>
|
implements HassDialog<HassioBackupDialogParams>
|
||||||
{
|
{
|
||||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
@state() private _error?: string;
|
@state() private _error?: string;
|
||||||
|
|
||||||
@ -62,9 +61,13 @@ class HassioBackupDialog
|
|||||||
this._dialogParams = dialogParams;
|
this._dialogParams = dialogParams;
|
||||||
this._backup = await fetchHassioBackupInfo(this.hass, dialogParams.slug);
|
this._backup = await fetchHassioBackupInfo(this.hass, dialogParams.slug);
|
||||||
if (!this._backup) {
|
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) {
|
} 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;
|
this._restoringBackup = false;
|
||||||
}
|
}
|
||||||
@ -82,13 +85,6 @@ class HassioBackupDialog
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _localize(key: BackupOrRestoreKey) {
|
|
||||||
return (
|
|
||||||
this._dialogParams!.supervisor?.localize(`backup.${key}`) ||
|
|
||||||
this._dialogParams!.localize!(`ui.panel.page-onboarding.restore.${key}`)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
if (!this._dialogParams || !this._backup) {
|
if (!this._dialogParams || !this._backup) {
|
||||||
return nothing;
|
return nothing;
|
||||||
@ -102,7 +98,7 @@ class HassioBackupDialog
|
|||||||
<ha-dialog-header slot="headline">
|
<ha-dialog-header slot="headline">
|
||||||
<ha-icon-button
|
<ha-icon-button
|
||||||
slot="navigationIcon"
|
slot="navigationIcon"
|
||||||
.label=${this._localize("close")}
|
.label=${this._dialogParams.supervisor?.localize("backup.close")}
|
||||||
.path=${mdiClose}
|
.path=${mdiClose}
|
||||||
@click=${this.closeDialog}
|
@click=${this.closeDialog}
|
||||||
.disabled=${this._restoringBackup}
|
.disabled=${this._restoringBackup}
|
||||||
@ -150,7 +146,6 @@ class HassioBackupDialog
|
|||||||
.supervisor=${this._dialogParams.supervisor}
|
.supervisor=${this._dialogParams.supervisor}
|
||||||
.backup=${this._backup}
|
.backup=${this._backup}
|
||||||
.onboarding=${this._dialogParams.onboarding || false}
|
.onboarding=${this._dialogParams.onboarding || false}
|
||||||
.localize=${this._dialogParams.localize}
|
|
||||||
dialogInitialFocus
|
dialogInitialFocus
|
||||||
>
|
>
|
||||||
</supervisor-backup-content>
|
</supervisor-backup-content>
|
||||||
@ -161,7 +156,7 @@ class HassioBackupDialog
|
|||||||
.disabled=${this._restoringBackup || !!this._error}
|
.disabled=${this._restoringBackup || !!this._error}
|
||||||
@click=${this._restoreClicked}
|
@click=${this._restoreClicked}
|
||||||
>
|
>
|
||||||
${this._localize("restore")}
|
${this._dialogParams.supervisor?.localize("backup.restore")}
|
||||||
</ha-button>
|
</ha-button>
|
||||||
</div>
|
</div>
|
||||||
</ha-md-dialog>
|
</ha-md-dialog>
|
||||||
@ -196,18 +191,22 @@ class HassioBackupDialog
|
|||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
!(await showConfirmationDialog(this, {
|
!(await showConfirmationDialog(this, {
|
||||||
title: this._localize(
|
title: supervisor?.localize(
|
||||||
this._backup!.type === "full"
|
`backup.${
|
||||||
? "confirm_restore_full_backup_title"
|
this._backup!.type === "full"
|
||||||
: "confirm_restore_partial_backup_title"
|
? "confirm_restore_full_backup_title"
|
||||||
|
: "confirm_restore_partial_backup_title"
|
||||||
|
}`
|
||||||
),
|
),
|
||||||
text: this._localize(
|
text: supervisor?.localize(
|
||||||
this._backup!.type === "full"
|
`backup.${
|
||||||
? "confirm_restore_full_backup_text"
|
this._backup!.type === "full"
|
||||||
: "confirm_restore_partial_backup_text"
|
? "confirm_restore_full_backup_text"
|
||||||
|
: "confirm_restore_partial_backup_text"
|
||||||
|
}`
|
||||||
),
|
),
|
||||||
confirmText: this._localize("restore"),
|
confirmText: supervisor?.localize("backup.restore"),
|
||||||
dismissText: this._localize("cancel"),
|
dismissText: supervisor?.localize("backup.cancel"),
|
||||||
}))
|
}))
|
||||||
) {
|
) {
|
||||||
this._restoringBackup = false;
|
this._restoringBackup = false;
|
||||||
@ -227,7 +226,8 @@ class HassioBackupDialog
|
|||||||
this.closeDialog();
|
this.closeDialog();
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
this._error =
|
this._error =
|
||||||
error?.body?.message || this._localize("restore_start_failed");
|
error?.body?.message ||
|
||||||
|
supervisor?.localize("backup.restore_start_failed");
|
||||||
} finally {
|
} finally {
|
||||||
this._restoringBackup = false;
|
this._restoringBackup = false;
|
||||||
}
|
}
|
||||||
@ -286,7 +286,7 @@ class HassioBackupDialog
|
|||||||
title: supervisor.localize("backup.remote_download_title"),
|
title: supervisor.localize("backup.remote_download_title"),
|
||||||
text: supervisor.localize("backup.remote_download_text"),
|
text: supervisor.localize("backup.remote_download_text"),
|
||||||
confirmText: supervisor.localize("backup.download"),
|
confirmText: supervisor.localize("backup.download"),
|
||||||
dismissText: this._localize("cancel"),
|
dismissText: supervisor?.localize("backup.cancel"),
|
||||||
});
|
});
|
||||||
if (!confirm) {
|
if (!confirm) {
|
||||||
return;
|
return;
|
||||||
@ -302,7 +302,7 @@ class HassioBackupDialog
|
|||||||
private get _computeName() {
|
private get _computeName() {
|
||||||
return this._backup
|
return this._backup
|
||||||
? this._backup.name || this._backup.slug
|
? this._backup.name || this._backup.slug
|
||||||
: this._localize("unnamed_backup");
|
: this._dialogParams!.supervisor?.localize("backup.unnamed_backup") || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||||
import type { LocalizeFunc } from "../../../../src/common/translations/localize";
|
|
||||||
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
|
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
|
||||||
|
|
||||||
export interface HassioBackupDialogParams {
|
export interface HassioBackupDialogParams {
|
||||||
@ -8,7 +7,6 @@ export interface HassioBackupDialogParams {
|
|||||||
onRestoring?: () => void;
|
onRestoring?: () => void;
|
||||||
onboarding?: boolean;
|
onboarding?: boolean;
|
||||||
supervisor?: Supervisor;
|
supervisor?: Supervisor;
|
||||||
localize?: LocalizeFunc;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const showHassioBackupDialog = (
|
export const showHassioBackupDialog = (
|
||||||
|
@ -1,4 +0,0 @@
|
|||||||
import type { TranslationDict } from "../../../src/types";
|
|
||||||
|
|
||||||
export type BackupOrRestoreKey = keyof TranslationDict["supervisor"]["backup"] &
|
|
||||||
keyof TranslationDict["ui"]["panel"]["page-onboarding"]["restore"];
|
|
@ -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
|
// Aug 9, 2021, 8:23 AM
|
||||||
export const formatShortDateTimeWithYear = (
|
export const formatShortDateTimeWithYear = (
|
||||||
dateObj: Date,
|
dateObj: Date,
|
||||||
|
@ -11,6 +11,7 @@ import "./ha-icon-button";
|
|||||||
import { blankBeforePercent } from "../common/translations/blank_before_percent";
|
import { blankBeforePercent } from "../common/translations/blank_before_percent";
|
||||||
import { ensureArray } from "../common/array/ensure-array";
|
import { ensureArray } from "../common/array/ensure-array";
|
||||||
import { bytesToString } from "../util/bytes-to-string";
|
import { bytesToString } from "../util/bytes-to-string";
|
||||||
|
import type { LocalizeFunc } from "../common/translations/localize";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HASSDomEvents {
|
interface HASSDomEvents {
|
||||||
@ -23,6 +24,8 @@ declare global {
|
|||||||
export class HaFileUpload extends LitElement {
|
export class HaFileUpload extends LitElement {
|
||||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public localize?: LocalizeFunc;
|
||||||
|
|
||||||
@property() public accept!: string;
|
@property() public accept!: string;
|
||||||
|
|
||||||
@property() public icon?: string;
|
@property() public icon?: string;
|
||||||
@ -31,6 +34,10 @@ export class HaFileUpload extends LitElement {
|
|||||||
|
|
||||||
@property() public secondary?: string;
|
@property() public secondary?: string;
|
||||||
|
|
||||||
|
@property({ attribute: "uploading-label" }) public uploadingLabel?: string;
|
||||||
|
|
||||||
|
@property({ attribute: "delete-label" }) public deleteLabel?: string;
|
||||||
|
|
||||||
@property() public supports?: string;
|
@property() public supports?: string;
|
||||||
|
|
||||||
@property({ type: Object }) public value?: File | File[] | FileList | string;
|
@property({ type: Object }) public value?: File | File[] | FileList | string;
|
||||||
@ -73,23 +80,22 @@ export class HaFileUpload extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public render(): TemplateResult {
|
public render(): TemplateResult {
|
||||||
|
const localize = this.localize || this.hass!.localize;
|
||||||
return html`
|
return html`
|
||||||
${this.uploading
|
${this.uploading
|
||||||
? html`<div class="container">
|
? html`<div class="container">
|
||||||
<div class="uploading">
|
<div class="uploading">
|
||||||
<span class="header"
|
<span class="header"
|
||||||
>${this.value
|
>${this.uploadingLabel || this.value
|
||||||
? this.hass?.localize(
|
? localize("ui.components.file-upload.uploading_name", {
|
||||||
"ui.components.file-upload.uploading_name",
|
name: this._name,
|
||||||
{ name: this._name }
|
})
|
||||||
)
|
: localize("ui.components.file-upload.uploading")}</span
|
||||||
: this.hass?.localize(
|
|
||||||
"ui.components.file-upload.uploading"
|
|
||||||
)}</span
|
|
||||||
>
|
>
|
||||||
${this.progress
|
${this.progress
|
||||||
? html`<div class="progress">
|
? html`<div class="progress">
|
||||||
${this.progress}${blankBeforePercent(this.hass!.locale)}%
|
${this.progress}${this.hass &&
|
||||||
|
blankBeforePercent(this.hass!.locale)}%
|
||||||
</div>`
|
</div>`
|
||||||
: nothing}
|
: nothing}
|
||||||
</div>
|
</div>
|
||||||
@ -116,14 +122,11 @@ export class HaFileUpload extends LitElement {
|
|||||||
.path=${this.icon || mdiFileUpload}
|
.path=${this.icon || mdiFileUpload}
|
||||||
></ha-svg-icon>
|
></ha-svg-icon>
|
||||||
<ha-button unelevated @click=${this._openFilePicker}>
|
<ha-button unelevated @click=${this._openFilePicker}>
|
||||||
${this.label ||
|
${this.label || localize("ui.components.file-upload.label")}
|
||||||
this.hass?.localize("ui.components.file-upload.label")}
|
|
||||||
</ha-button>
|
</ha-button>
|
||||||
<span class="secondary"
|
<span class="secondary"
|
||||||
>${this.secondary ||
|
>${this.secondary ||
|
||||||
this.hass?.localize(
|
localize("ui.components.file-upload.secondary")}</span
|
||||||
"ui.components.file-upload.secondary"
|
|
||||||
)}</span
|
|
||||||
>
|
>
|
||||||
<span class="supports">${this.supports}</span>`
|
<span class="supports">${this.supports}</span>`
|
||||||
: typeof this.value === "string"
|
: typeof this.value === "string"
|
||||||
@ -136,8 +139,7 @@ export class HaFileUpload extends LitElement {
|
|||||||
</div>
|
</div>
|
||||||
<ha-icon-button
|
<ha-icon-button
|
||||||
@click=${this._clearValue}
|
@click=${this._clearValue}
|
||||||
.label=${this.hass?.localize("ui.common.delete") ||
|
.label=${this.deleteLabel || localize("ui.common.delete")}
|
||||||
"Delete"}
|
|
||||||
.path=${mdiDelete}
|
.path=${mdiDelete}
|
||||||
></ha-icon-button>
|
></ha-icon-button>
|
||||||
</div>`
|
</div>`
|
||||||
@ -155,8 +157,8 @@ export class HaFileUpload extends LitElement {
|
|||||||
</div>
|
</div>
|
||||||
<ha-icon-button
|
<ha-icon-button
|
||||||
@click=${this._clearValue}
|
@click=${this._clearValue}
|
||||||
.label=${this.hass?.localize("ui.common.delete") ||
|
.label=${this.deleteLabel ||
|
||||||
"Delete"}
|
localize("ui.common.delete")}
|
||||||
.path=${mdiDelete}
|
.path=${mdiDelete}
|
||||||
></ha-icon-button>
|
></ha-icon-button>
|
||||||
</div>`
|
</div>`
|
||||||
@ -238,6 +240,10 @@ export class HaFileUpload extends LitElement {
|
|||||||
border-radius: var(--mdc-shape-small, 4px);
|
border-radius: var(--mdc-shape-small, 4px);
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
label.container {
|
label.container {
|
||||||
border: dashed 1px
|
border: dashed 1px
|
||||||
var(--mdc-text-field-idle-line-color, rgba(0, 0, 0, 0.42));
|
var(--mdc-text-field-idle-line-color, rgba(0, 0, 0, 0.42));
|
||||||
|
@ -2,7 +2,6 @@ import { memoize } from "@fullcalendar/core/internal";
|
|||||||
import { setHours, setMinutes } from "date-fns";
|
import { setHours, setMinutes } from "date-fns";
|
||||||
import type { HassConfig } from "home-assistant-js-websocket";
|
import type { HassConfig } from "home-assistant-js-websocket";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import checkValidDate from "../common/datetime/check_valid_date";
|
|
||||||
import {
|
import {
|
||||||
formatDateTime,
|
formatDateTime,
|
||||||
formatDateTimeNumeric,
|
formatDateTimeNumeric,
|
||||||
@ -13,6 +12,8 @@ import type { HomeAssistant } from "../types";
|
|||||||
import { fileDownload } from "../util/file_download";
|
import { fileDownload } from "../util/file_download";
|
||||||
import { domainToName } from "./integration";
|
import { domainToName } from "./integration";
|
||||||
import type { FrontendLocaleData } from "./translation";
|
import type { FrontendLocaleData } from "./translation";
|
||||||
|
import checkValidDate from "../common/datetime/check_valid_date";
|
||||||
|
import { handleFetchPromise } from "../util/hass-call-api";
|
||||||
|
|
||||||
export const enum BackupScheduleRecurrence {
|
export const enum BackupScheduleRecurrence {
|
||||||
NEVER = "never",
|
NEVER = "never",
|
||||||
@ -231,27 +232,23 @@ export const restoreBackup = (
|
|||||||
export const uploadBackup = async (
|
export const uploadBackup = async (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
file: File,
|
file: File,
|
||||||
agent_ids: string[]
|
agentIds: string[]
|
||||||
): Promise<void> => {
|
): Promise<{ backup_id: string }> => {
|
||||||
const fd = new FormData();
|
const fd = new FormData();
|
||||||
fd.append("file", file);
|
fd.append("file", file);
|
||||||
|
|
||||||
const params = agent_ids.reduce((acc, agent_id) => {
|
const params = new URLSearchParams();
|
||||||
acc.append("agent_id", agent_id);
|
|
||||||
return acc;
|
|
||||||
}, new URLSearchParams());
|
|
||||||
|
|
||||||
const resp = await hass.fetchWithAuth(
|
agentIds.forEach((agentId) => {
|
||||||
`/api/backup/upload?${params.toString()}`,
|
params.append("agent_id", agentId);
|
||||||
{
|
});
|
||||||
|
|
||||||
|
return handleFetchPromise(
|
||||||
|
hass.fetchWithAuth(`/api/backup/upload?${params.toString()}`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: fd,
|
body: fd,
|
||||||
}
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!resp.ok) {
|
|
||||||
throw new Error(`${resp.status} ${resp.statusText}`);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getPreferredAgentForDownload = (agents: string[]) => {
|
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)}`;
|
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,
|
||||||
|
};
|
||||||
|
66
src/data/backup_onboarding.ts
Normal file
66
src/data/backup_onboarding.ts
Normal file
@ -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<BackupOnboardingConfig>(
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
@ -1,6 +1,5 @@
|
|||||||
import { atLeastVersion } from "../../common/config/version";
|
import { atLeastVersion } from "../../common/config/version";
|
||||||
import type { HomeAssistant } from "../../types";
|
import type { HomeAssistant } from "../../types";
|
||||||
import { handleFetchPromise } from "../../util/hass-call-api";
|
|
||||||
import type { HassioResponse } from "./common";
|
import type { HassioResponse } from "./common";
|
||||||
import { hassioApiResultExtractor } from "./common";
|
import { hassioApiResultExtractor } from "./common";
|
||||||
|
|
||||||
@ -82,34 +81,24 @@ export const fetchHassioBackups = async (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const fetchHassioBackupInfo = async (
|
export const fetchHassioBackupInfo = async (
|
||||||
hass: HomeAssistant | undefined,
|
hass: HomeAssistant,
|
||||||
backup: string
|
backup: string
|
||||||
): Promise<HassioBackupDetail> => {
|
): Promise<HassioBackupDetail> => {
|
||||||
if (hass) {
|
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
||||||
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
return hass.callWS({
|
||||||
return hass.callWS({
|
type: "supervisor/api",
|
||||||
type: "supervisor/api",
|
endpoint: `/${
|
||||||
endpoint: `/${
|
atLeastVersion(hass.config.version, 2021, 9) ? "backups" : "snapshots"
|
||||||
atLeastVersion(hass.config.version, 2021, 9) ? "backups" : "snapshots"
|
}/${backup}/info`,
|
||||||
}/${backup}/info`,
|
method: "get",
|
||||||
method: "get",
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
return hassioApiResultExtractor(
|
|
||||||
await hass.callApi<HassioResponse<HassioBackupDetail>>(
|
|
||||||
"GET",
|
|
||||||
`hassio/${
|
|
||||||
atLeastVersion(hass.config.version, 2021, 9) ? "backups" : "snapshots"
|
|
||||||
}/${backup}/info`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
// When called from onboarding we don't have hass
|
|
||||||
return hassioApiResultExtractor(
|
return hassioApiResultExtractor(
|
||||||
await handleFetchPromise(
|
await hass.callApi<HassioResponse<HassioBackupDetail>>(
|
||||||
fetch(`/api/hassio/backups/${backup}/info`, {
|
"GET",
|
||||||
method: "GET",
|
`hassio/${
|
||||||
})
|
atLeastVersion(hass.config.version, 2021, 9) ? "backups" : "snapshots"
|
||||||
|
}/${backup}/info`
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -240,24 +229,15 @@ export const uploadBackup = async (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const restoreBackup = async (
|
export const restoreBackup = async (
|
||||||
hass: HomeAssistant | undefined,
|
hass: HomeAssistant,
|
||||||
type: HassioBackupDetail["type"],
|
type: HassioBackupDetail["type"],
|
||||||
backupSlug: string,
|
backupSlug: string,
|
||||||
backupDetails: HassioPartialBackupCreateParams | HassioFullBackupCreateParams,
|
backupDetails: HassioPartialBackupCreateParams | HassioFullBackupCreateParams,
|
||||||
useSnapshotUrl: boolean
|
useSnapshotUrl: boolean
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
if (hass) {
|
await hass.callApi<HassioResponse<{ job_id: string }>>(
|
||||||
await hass.callApi<HassioResponse<{ job_id: string }>>(
|
"POST",
|
||||||
"POST",
|
`hassio/${useSnapshotUrl ? "snapshots" : "backups"}/${backupSlug}/restore/${type}`,
|
||||||
`hassio/${useSnapshotUrl ? "snapshots" : "backups"}/${backupSlug}/restore/${type}`,
|
backupDetails
|
||||||
backupDetails
|
);
|
||||||
);
|
|
||||||
} else {
|
|
||||||
await handleFetchPromise(
|
|
||||||
fetch(`/api/hassio/backups/${backupSlug}/restore/${type}`, {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify(backupDetails),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
@ -17,7 +17,7 @@ export const litLocalizeLiteMixin = <T extends Constructor<LitElement>>(
|
|||||||
@property({ attribute: false }) public localize: LocalizeFunc = empty;
|
@property({ attribute: false }) public localize: LocalizeFunc = empty;
|
||||||
|
|
||||||
// Use browser language setup before login.
|
// Use browser language setup before login.
|
||||||
@property() public language?: string = getLocalLanguage();
|
@property() public language: string = getLocalLanguage();
|
||||||
|
|
||||||
@property() public translationFragment?: string;
|
@property() public translationFragment?: string;
|
||||||
|
|
||||||
|
@ -41,6 +41,7 @@ import "./onboarding-analytics";
|
|||||||
import "./onboarding-create-user";
|
import "./onboarding-create-user";
|
||||||
import "./onboarding-loading";
|
import "./onboarding-loading";
|
||||||
import "./onboarding-welcome";
|
import "./onboarding-welcome";
|
||||||
|
import "./onboarding-restore-backup";
|
||||||
import "./onboarding-welcome-links";
|
import "./onboarding-welcome-links";
|
||||||
import { makeDialogManager } from "../dialogs/make-dialog-manager";
|
import { makeDialogManager } from "../dialogs/make-dialog-manager";
|
||||||
import { navigate } from "../common/navigate";
|
import { navigate } from "../common/navigate";
|
||||||
@ -157,8 +158,9 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
|
|||||||
private _renderStep() {
|
private _renderStep() {
|
||||||
if (this._restoring) {
|
if (this._restoring) {
|
||||||
return html`<onboarding-restore-backup
|
return html`<onboarding-restore-backup
|
||||||
.hass=${this.hass}
|
|
||||||
.localize=${this.localize}
|
.localize=${this.localize}
|
||||||
|
.supervisor=${this._supervisor ?? false}
|
||||||
|
.language=${this.language}
|
||||||
>
|
>
|
||||||
</onboarding-restore-backup>`;
|
</onboarding-restore-backup>`;
|
||||||
}
|
}
|
||||||
@ -166,8 +168,6 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
|
|||||||
if (this._init) {
|
if (this._init) {
|
||||||
return html`<onboarding-welcome
|
return html`<onboarding-welcome
|
||||||
.localize=${this.localize}
|
.localize=${this.localize}
|
||||||
.language=${this.language}
|
|
||||||
.supervisor=${this._supervisor}
|
|
||||||
></onboarding-welcome>`;
|
></onboarding-welcome>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -236,7 +236,7 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (changedProps.has("language")) {
|
if (changedProps.has("language")) {
|
||||||
document.querySelector("html")!.setAttribute("lang", this.language!);
|
document.querySelector("html")!.setAttribute("lang", this.language);
|
||||||
}
|
}
|
||||||
if (changedProps.has("hass")) {
|
if (changedProps.has("hass")) {
|
||||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||||
@ -272,10 +272,6 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
|
|||||||
"Home Assistant OS",
|
"Home Assistant OS",
|
||||||
"Home Assistant Supervised",
|
"Home Assistant Supervised",
|
||||||
].includes(response.installation_type);
|
].includes(response.installation_type);
|
||||||
if (this._supervisor) {
|
|
||||||
// Only load if we have supervisor
|
|
||||||
import("./onboarding-restore-backup");
|
|
||||||
}
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.error(
|
console.error(
|
||||||
@ -454,7 +450,7 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
|
|||||||
subscribeOne(conn, subscribeUser),
|
subscribeOne(conn, subscribeUser),
|
||||||
]);
|
]);
|
||||||
this.initializeHass(auth, conn);
|
this.initializeHass(auth, conn);
|
||||||
if (this.language && this.language !== this.hass!.language) {
|
if (this.language !== this.hass!.language) {
|
||||||
this._updateHass({
|
this._updateHass({
|
||||||
locale: { ...this.hass!.locale, language: this.language },
|
locale: { ...this.hass!.locale, language: this.language },
|
||||||
language: this.language,
|
language: this.language,
|
||||||
|
@ -1,136 +1,336 @@
|
|||||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
import type { TemplateResult } from "lit";
|
||||||
import { css, html, LitElement, nothing } from "lit";
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { showHassioBackupDialog } from "../../hassio/src/dialogs/backup/show-dialog-hassio-backup";
|
import "./restore-backup/onboarding-restore-backup-upload";
|
||||||
import "../../hassio/src/components/hassio-upload-backup";
|
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 type { LocalizeFunc } from "../common/translations/localize";
|
||||||
import "../components/ha-ansi-to-html";
|
|
||||||
import "../components/ha-card";
|
import "../components/ha-card";
|
||||||
|
import "../components/ha-icon-button-arrow-prev";
|
||||||
|
import "../components/ha-circular-progress";
|
||||||
import "../components/ha-alert";
|
import "../components/ha-alert";
|
||||||
import "../components/ha-button";
|
|
||||||
import { fetchInstallationType } from "../data/onboarding";
|
|
||||||
import type { HomeAssistant } from "../types";
|
|
||||||
import "./onboarding-loading";
|
import "./onboarding-loading";
|
||||||
import { onBoardingStyles } from "./styles";
|
|
||||||
import { removeSearchParam } from "../common/url/search-params";
|
import { removeSearchParam } from "../common/url/search-params";
|
||||||
import { navigate } from "../common/navigate";
|
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")
|
@customElement("onboarding-restore-backup")
|
||||||
class OnboardingRestoreBackup extends LitElement {
|
class OnboardingRestoreBackup extends LitElement {
|
||||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
|
||||||
|
|
||||||
@property({ attribute: false }) public localize!: LocalizeFunc;
|
@property({ attribute: false }) public localize!: LocalizeFunc;
|
||||||
|
|
||||||
@property() public language!: string;
|
@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 {
|
protected render(): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
${this._restoring
|
${
|
||||||
? html`<h1>
|
this._view !== "status" || this._failed
|
||||||
${this.localize("ui.panel.page-onboarding.restore.in_progress")}
|
? html`<ha-icon-button-arrow-prev
|
||||||
</h1>
|
.label=${this.localize("ui.panel.page-onboarding.restore.back")}
|
||||||
<ha-alert alert-type="info">
|
@click=${this._back}
|
||||||
${this.localize("ui.panel.page-onboarding.restore.in_progress")}
|
></ha-icon-button-arrow-prev>`
|
||||||
</ha-alert>
|
: nothing
|
||||||
<onboarding-loading></onboarding-loading>`
|
}
|
||||||
: html` <h1>
|
</ha-icon-button>
|
||||||
${this.localize("ui.panel.page-onboarding.restore.header")}
|
<h1>${this.localize("ui.panel.page-onboarding.restore.header")}</h1>
|
||||||
</h1>
|
${
|
||||||
<hassio-upload-backup
|
this._error || (this._failed && this._view !== "status")
|
||||||
@backup-uploaded=${this._backupUploaded}
|
? html`<ha-alert
|
||||||
@backup-cleared=${this._backupCleared}
|
alert-type="error"
|
||||||
.hass=${this.hass}
|
.title=${this._failed && this._view !== "status"
|
||||||
.localize=${this.localize}
|
? this.localize("ui.panel.page-onboarding.restore.failed")
|
||||||
></hassio-upload-backup>`}
|
: ""}
|
||||||
<div class="footer">
|
|
||||||
<ha-button @click=${this._back} .disabled=${this._restoring}>
|
|
||||||
${this.localize("ui.panel.page-onboarding.back")}
|
|
||||||
</ha-button>
|
|
||||||
${this._backupSlug
|
|
||||||
? html`<ha-button
|
|
||||||
@click=${this._showBackupDialog}
|
|
||||||
.disabled=${this._restoring}
|
|
||||||
>
|
>
|
||||||
${this.localize("ui.panel.page-onboarding.restore.restore")}
|
${this._failed && this._view !== "status"
|
||||||
</ha-button>`
|
? this.localize(
|
||||||
: nothing}
|
`ui.panel.page-onboarding.restore.${this._backupInfo?.last_non_idle_event?.reason === "password_incorrect" ? "failed_wrong_password_description" : "failed_description"}`
|
||||||
</div>
|
)
|
||||||
|
: this._error}
|
||||||
|
</ha-alert>`
|
||||||
|
: nothing
|
||||||
|
}
|
||||||
|
${
|
||||||
|
this._view === "loading"
|
||||||
|
? html`<div class="loading">
|
||||||
|
<ha-circular-progress indeterminate></ha-circular-progress>
|
||||||
|
</div>`
|
||||||
|
: this._view === "upload"
|
||||||
|
? html`
|
||||||
|
<onboarding-restore-backup-upload
|
||||||
|
.supervisor=${this.supervisor}
|
||||||
|
.localize=${this.localize}
|
||||||
|
@backup-uploaded=${this._backupUploaded}
|
||||||
|
></onboarding-restore-backup-upload>
|
||||||
|
`
|
||||||
|
: this._view === "select_data"
|
||||||
|
? html`<onboarding-restore-backup-details
|
||||||
|
.localize=${this.localize}
|
||||||
|
.backup=${this._backup!}
|
||||||
|
@backup-restore=${this._restore}
|
||||||
|
></onboarding-restore-backup-details>`
|
||||||
|
: this._view === "confirm_restore"
|
||||||
|
? html`<onboarding-restore-backup-restore
|
||||||
|
.localize=${this.localize}
|
||||||
|
.backup=${this._backup!}
|
||||||
|
.supervisor=${this.supervisor}
|
||||||
|
.selectedData=${this._selectedData!}
|
||||||
|
@restore-started=${this._restoreStarted}
|
||||||
|
></onboarding-restore-backup-restore>`
|
||||||
|
: nothing
|
||||||
|
}
|
||||||
|
${
|
||||||
|
this._view === "status" && this._backupInfo
|
||||||
|
? html`<onboarding-restore-backup-status
|
||||||
|
.localize=${this.localize}
|
||||||
|
.backupInfo=${this._backupInfo}
|
||||||
|
@show-backup-upload=${this._reupload}
|
||||||
|
></onboarding-restore-backup-status>`
|
||||||
|
: nothing
|
||||||
|
}
|
||||||
|
${
|
||||||
|
["select_data", "confirm_restore"].includes(this._view) && this._backup
|
||||||
|
? html`<div class="backup-summary-wrapper">
|
||||||
|
<ha-backup-details-summary
|
||||||
|
translation-key-panel="page-onboarding.restore"
|
||||||
|
show-upload-another
|
||||||
|
.backup=${this._backup}
|
||||||
|
.localize=${this.localize}
|
||||||
|
@show-backup-upload=${this._reupload}
|
||||||
|
.isHassio=${this.supervisor}
|
||||||
|
></ha-backup-details-summary>
|
||||||
|
</div>`
|
||||||
|
: 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) {
|
protected firstUpdated(changedProps) {
|
||||||
super.firstUpdated(changedProps);
|
super.firstUpdated(changedProps);
|
||||||
|
|
||||||
|
this._loadBackupInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _checkRestoreStatus(): Promise<void> {
|
private async _loadBackupInfo() {
|
||||||
if (this._restoring) {
|
let onboardingInfo: BackupOnboardingConfig;
|
||||||
try {
|
try {
|
||||||
await fetchInstallationType();
|
onboardingInfo = await fetchBackupOnboardingInfo();
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
if (this._restoreRunning) {
|
||||||
if (
|
if (
|
||||||
(err as Error).message === "unauthorized" ||
|
err.error === "Request error" ||
|
||||||
(err as Error).message === "not_found"
|
// 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("/");
|
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 {
|
private _restore(ev: CustomEvent) {
|
||||||
setTimeout(() => this._checkRestoreStatus(), 1000);
|
if (!this._backup || !ev.detail.selectedData) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._selectedData = ev.detail.selectedData;
|
||||||
|
|
||||||
|
this._view = "confirm_restore";
|
||||||
}
|
}
|
||||||
|
|
||||||
private _showBackupDialog(): void {
|
private _reupload() {
|
||||||
showHassioBackupDialog(this, {
|
this._backup = undefined;
|
||||||
slug: this._backupSlug!,
|
this._backupId = undefined;
|
||||||
onboarding: true,
|
this._view = "upload";
|
||||||
localize: this.localize,
|
|
||||||
onRestoring: () => {
|
|
||||||
this._restoring = true;
|
|
||||||
this._scheduleCheckRestoreStatus();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static styles = [
|
||||||
return [
|
onBoardingStyles,
|
||||||
onBoardingStyles,
|
css`
|
||||||
css`
|
:host {
|
||||||
:host {
|
display: flex;
|
||||||
display: flex;
|
flex-direction: column;
|
||||||
flex-direction: column;
|
position: relative;
|
||||||
align-items: center;
|
}
|
||||||
}
|
ha-icon-button-arrow-prev {
|
||||||
hassio-upload-backup {
|
position: absolute;
|
||||||
width: 100%;
|
top: 12px;
|
||||||
}
|
}
|
||||||
.footer {
|
ha-card {
|
||||||
display: flex;
|
width: 100%;
|
||||||
justify-content: space-between;
|
}
|
||||||
width: 100%;
|
.loading {
|
||||||
}
|
display: flex;
|
||||||
`,
|
justify-content: center;
|
||||||
];
|
padding: 32px;
|
||||||
}
|
}
|
||||||
|
.backup-summary-wrapper {
|
||||||
|
margin-top: 24px;
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
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 { customElement, property } from "lit/decorators";
|
||||||
import type { LocalizeFunc } from "../common/translations/localize";
|
import type { LocalizeFunc } from "../common/translations/localize";
|
||||||
import type { HomeAssistant } from "../types";
|
import type { HomeAssistant } from "../types";
|
||||||
@ -13,8 +13,6 @@ class OnboardingWelcome extends LitElement {
|
|||||||
|
|
||||||
@property({ attribute: false }) public localize!: LocalizeFunc;
|
@property({ attribute: false }) public localize!: LocalizeFunc;
|
||||||
|
|
||||||
@property({ type: Boolean }) public supervisor = false;
|
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
<h1>${this.localize("ui.panel.page-onboarding.welcome.header")}</h1>
|
<h1>${this.localize("ui.panel.page-onboarding.welcome.header")}</h1>
|
||||||
@ -24,11 +22,9 @@ class OnboardingWelcome extends LitElement {
|
|||||||
${this.localize("ui.panel.page-onboarding.welcome.start")}
|
${this.localize("ui.panel.page-onboarding.welcome.start")}
|
||||||
</ha-button>
|
</ha-button>
|
||||||
|
|
||||||
${this.supervisor
|
<ha-button @click=${this._restoreBackup}>
|
||||||
? html`<ha-button @click=${this._restoreBackup}>
|
${this.localize("ui.panel.page-onboarding.welcome.restore_backup")}
|
||||||
${this.localize("ui.panel.page-onboarding.welcome.restore_backup")}
|
</ha-button>
|
||||||
</ha-button>`
|
|
||||||
: nothing}
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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`<ha-backup-details-restore
|
||||||
|
.backup=${this.backup}
|
||||||
|
.localize=${this.localize}
|
||||||
|
translation-key-panel="page-onboarding.restore"
|
||||||
|
ha-required
|
||||||
|
></ha-backup-details-restore>`
|
||||||
|
: html`
|
||||||
|
<ha-alert alert-type="error">
|
||||||
|
${this.localize(
|
||||||
|
"ui.panel.page-onboarding.restore.details.home_assistant_missing"
|
||||||
|
)}
|
||||||
|
</ha-alert>
|
||||||
|
`}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
@ -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`<ha-alert alert-type="warning" class="supervisor-warning">
|
||||||
|
${this.localize(
|
||||||
|
"ui.panel.page-onboarding.restore.details.addons_unsupported"
|
||||||
|
)}
|
||||||
|
</ha-alert>`
|
||||||
|
: nothing}
|
||||||
|
<ha-card
|
||||||
|
.header=${this.localize("ui.panel.page-onboarding.restore.restore")}
|
||||||
|
>
|
||||||
|
<div class="card-content">
|
||||||
|
${this._error
|
||||||
|
? html`<ha-alert alert-type="error">${this._error}</ha-alert> `
|
||||||
|
: nothing}
|
||||||
|
<p>
|
||||||
|
${this.localize(
|
||||||
|
"ui.panel.page-onboarding.restore.confirm_restore_full_backup_text"
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
${backupProtected
|
||||||
|
? html`<p>
|
||||||
|
${this.localize(
|
||||||
|
"ui.panel.page-onboarding.restore.details.restore.encryption.title"
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
${this._encryptionKeyWrong
|
||||||
|
? html`
|
||||||
|
<ha-alert alert-type="error">
|
||||||
|
${this.localize(
|
||||||
|
"ui.panel.page-onboarding.restore.details.restore.encryption.incorrect_key"
|
||||||
|
)}
|
||||||
|
</ha-alert>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
<ha-password-field
|
||||||
|
.disabled=${this._loading}
|
||||||
|
@input=${this._encryptionKeyChanged}
|
||||||
|
.label=${this.localize(
|
||||||
|
"ui.panel.page-onboarding.restore.details.restore.encryption.input_label"
|
||||||
|
)}
|
||||||
|
.value=${this._encryptionKey}
|
||||||
|
></ha-password-field>`
|
||||||
|
: nothing}
|
||||||
|
</div>
|
||||||
|
<div class="card-actions">
|
||||||
|
<ha-progress-button
|
||||||
|
.progress=${this._loading}
|
||||||
|
.disabled=${this._loading ||
|
||||||
|
(backupProtected && this._encryptionKey === "")}
|
||||||
|
@click=${this._startRestore}
|
||||||
|
>
|
||||||
|
${this.localize(
|
||||||
|
"ui.panel.page-onboarding.restore.details.restore.action"
|
||||||
|
)}
|
||||||
|
</ha-progress-button>
|
||||||
|
</div>
|
||||||
|
</ha-card>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _encryptionKeyChanged(ev): void {
|
||||||
|
this._encryptionKey = ev.target.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _startRestore(ev: CustomEvent): Promise<void> {
|
||||||
|
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";
|
||||||
|
}
|
||||||
|
}
|
@ -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`
|
||||||
|
<ha-card
|
||||||
|
.header=${this.localize(
|
||||||
|
`ui.panel.page-onboarding.restore.${this.backupInfo.state === "restore_backup" ? "in_progress" : "failed"}`
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div class="card-content">
|
||||||
|
${this.backupInfo.state === "restore_backup"
|
||||||
|
? html`
|
||||||
|
<div class="loading">
|
||||||
|
<ha-circular-progress indeterminate></ha-circular-progress>
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
${this.localize(
|
||||||
|
"ui.panel.page-onboarding.restore.in_progress_description"
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
`
|
||||||
|
: html`
|
||||||
|
<ha-alert alert-type="error">
|
||||||
|
${this.localize(
|
||||||
|
"ui.panel.page-onboarding.restore.failed_status_description"
|
||||||
|
)}
|
||||||
|
</ha-alert>
|
||||||
|
${this.backupInfo.last_non_idle_event?.reason
|
||||||
|
? html`
|
||||||
|
<div class="failed">
|
||||||
|
<h4>Error:</h4>
|
||||||
|
${this.backupInfo.last_non_idle_event?.reason}
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
`}
|
||||||
|
</div>
|
||||||
|
${this.backupInfo.state !== "restore_backup"
|
||||||
|
? html`<div class="card-actions">
|
||||||
|
<ha-button @click=${this._uploadAnother} destructive>
|
||||||
|
${this.localize(
|
||||||
|
`ui.panel.page-onboarding.restore.details.summary.upload_another`
|
||||||
|
)}
|
||||||
|
</ha-button>
|
||||||
|
<ha-button @click=${this._home} destructive>
|
||||||
|
${this.localize(
|
||||||
|
`ui.panel.page-onboarding.restore.details.summary.home`
|
||||||
|
)}
|
||||||
|
</ha-button>
|
||||||
|
</div>`
|
||||||
|
: nothing}
|
||||||
|
</ha-card>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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";
|
||||||
|
}
|
||||||
|
}
|
@ -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`
|
||||||
|
<ha-card
|
||||||
|
.header=${this.localize(
|
||||||
|
"ui.panel.page-onboarding.restore.upload_backup"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div class="card-content">
|
||||||
|
${this._error
|
||||||
|
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||||
|
: nothing}
|
||||||
|
<ha-file-upload
|
||||||
|
.uploading=${this._uploading}
|
||||||
|
.icon=${mdiFolderUpload}
|
||||||
|
accept=${SUPPORTED_UPLOAD_FORMAT}
|
||||||
|
.localize=${this.localize}
|
||||||
|
.label=${this.localize(
|
||||||
|
"ui.panel.page-onboarding.restore.upload_input_label"
|
||||||
|
)}
|
||||||
|
.secondary=${this.localize(
|
||||||
|
"ui.panel.page-onboarding.restore.upload_secondary"
|
||||||
|
)}
|
||||||
|
.supports=${this.localize(
|
||||||
|
"ui.panel.page-onboarding.restore.upload_supports_tar"
|
||||||
|
)}
|
||||||
|
.deleteLabel=${this.localize(
|
||||||
|
"ui.panel.page-onboarding.restore.delete"
|
||||||
|
)}
|
||||||
|
.uploadingLabel=${this.localize(
|
||||||
|
"ui.panel.page-onboarding.restore.uploading"
|
||||||
|
)}
|
||||||
|
@file-picked=${this._filePicked}
|
||||||
|
></ha-file-upload>
|
||||||
|
</div>
|
||||||
|
</ha-card>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
@ -21,7 +21,7 @@ export interface BackupAddonItem {
|
|||||||
|
|
||||||
@customElement("ha-backup-addons-picker")
|
@customElement("ha-backup-addons-picker")
|
||||||
export class HaBackupAddonsPicker extends LitElement {
|
export class HaBackupAddonsPicker extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||||
|
|
||||||
@property({ attribute: false }) public addons!: BackupAddonItem[];
|
@property({ attribute: false }) public addons!: BackupAddonItem[];
|
||||||
|
|
||||||
@ -32,7 +32,7 @@ export class HaBackupAddonsPicker extends LitElement {
|
|||||||
|
|
||||||
private _addons = memoizeOne((addons: BackupAddonItem[]) =>
|
private _addons = memoizeOne((addons: BackupAddonItem[]) =>
|
||||||
addons.sort((a, b) =>
|
addons.sort((a, b) =>
|
||||||
stringCompare(a.name, b.name, this.hass.locale.language)
|
stringCompare(a.name, b.name, this.hass?.locale?.language)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -47,23 +47,32 @@ interface SelectedItems {
|
|||||||
|
|
||||||
@customElement("ha-backup-data-picker")
|
@customElement("ha-backup-data-picker")
|
||||||
export class HaBackupDataPicker extends LitElement {
|
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 data!: BackupData;
|
||||||
|
|
||||||
@property({ attribute: false }) public value?: 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<string, boolean> = {};
|
@state() public _addonIcons: Record<string, boolean> = {};
|
||||||
|
|
||||||
protected firstUpdated(changedProps: PropertyValues): void {
|
protected firstUpdated(changedProps: PropertyValues): void {
|
||||||
super.firstUpdated(changedProps);
|
super.firstUpdated(changedProps);
|
||||||
if (isComponentLoaded(this.hass, "hassio")) {
|
if (this.hass && isComponentLoaded(this.hass, "hassio")) {
|
||||||
this._fetchAddonInfo();
|
this._fetchAddonInfo();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _fetchAddonInfo() {
|
private async _fetchAddonInfo() {
|
||||||
const { addons } = await fetchHassioAddonsInfo(this.hass);
|
const { addons } = await fetchHassioAddonsInfo(this.hass!);
|
||||||
this._addonIcons = addons.reduce<Record<string, boolean>>(
|
this._addonIcons = addons.reduce<Record<string, boolean>>(
|
||||||
(acc, addon) => ({
|
(acc, addon) => ({
|
||||||
...acc,
|
...acc,
|
||||||
@ -74,16 +83,14 @@ export class HaBackupDataPicker extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _homeAssistantItems = memoizeOne(
|
private _homeAssistantItems = memoizeOne(
|
||||||
(data: BackupData, _localize: LocalizeFunc) => {
|
(data: BackupData, localize: LocalizeFunc) => {
|
||||||
const items: CheckBoxItem[] = [];
|
const items: CheckBoxItem[] = [];
|
||||||
|
|
||||||
if (data.homeassistant_included) {
|
if (data.homeassistant_included) {
|
||||||
items.push({
|
items.push({
|
||||||
label: data.database_included
|
label: localize(
|
||||||
? this.hass.localize(
|
`ui.panel.${this.translationKeyPanel}.data_picker.${data.database_included ? "settings_and_history" : "settings"}`
|
||||||
"ui.panel.config.backup.data_picker.settings_and_history"
|
),
|
||||||
)
|
|
||||||
: this.hass.localize("ui.panel.config.backup.data_picker.settings"),
|
|
||||||
id: "config",
|
id: "config",
|
||||||
version: data.homeassistant_version,
|
version: data.homeassistant_version,
|
||||||
});
|
});
|
||||||
@ -99,18 +106,22 @@ export class HaBackupDataPicker extends LitElement {
|
|||||||
);
|
);
|
||||||
|
|
||||||
private _localizeFolder(folder: string): string {
|
private _localizeFolder(folder: string): string {
|
||||||
|
const localize = this.localize || this.hass!.localize;
|
||||||
|
|
||||||
switch (folder) {
|
switch (folder) {
|
||||||
case "media":
|
case "media":
|
||||||
return this.hass.localize("ui.panel.config.backup.data_picker.media");
|
return localize(
|
||||||
|
`ui.panel.${this.translationKeyPanel}.data_picker.media`
|
||||||
|
);
|
||||||
case "share":
|
case "share":
|
||||||
return this.hass.localize(
|
return localize(
|
||||||
"ui.panel.config.backup.data_picker.share_folder"
|
`ui.panel.${this.translationKeyPanel}.data_picker.share_folder`
|
||||||
);
|
);
|
||||||
case "ssl":
|
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":
|
case "addons/local":
|
||||||
return this.hass.localize(
|
return localize(
|
||||||
"ui.panel.config.backup.data_picker.local_addons"
|
`ui.panel.${this.translationKeyPanel}.data_picker.local_addons`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return capitalizeFirstLetter(folder);
|
return capitalizeFirstLetter(folder);
|
||||||
@ -215,14 +226,13 @@ export class HaBackupDataPicker extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
const homeAssistantItems = this._homeAssistantItems(
|
const localize = this.localize || this.hass!.localize;
|
||||||
this.data,
|
|
||||||
this.hass.localize
|
const homeAssistantItems = this._homeAssistantItems(this.data, localize);
|
||||||
);
|
|
||||||
|
|
||||||
const addonsItems = this._addonsItems(
|
const addonsItems = this._addonsItems(
|
||||||
this.data,
|
this.data,
|
||||||
this.hass.localize,
|
localize,
|
||||||
this._addonIcons
|
this._addonIcons
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -247,6 +257,7 @@ export class HaBackupDataPicker extends LitElement {
|
|||||||
selectedItems.homeassistant.length <
|
selectedItems.homeassistant.length <
|
||||||
homeAssistantItems.length}
|
homeAssistantItems.length}
|
||||||
@change=${this._sectionChanged}
|
@change=${this._sectionChanged}
|
||||||
|
?disabled=${this.requiredItems.length > 0}
|
||||||
></ha-checkbox>
|
></ha-checkbox>
|
||||||
</ha-formfield>
|
</ha-formfield>
|
||||||
<div class="items">
|
<div class="items">
|
||||||
@ -266,6 +277,7 @@ export class HaBackupDataPicker extends LitElement {
|
|||||||
item.id
|
item.id
|
||||||
)}
|
)}
|
||||||
@change=${this._homeassistantChanged}
|
@change=${this._homeassistantChanged}
|
||||||
|
.disabled=${this.requiredItems.includes(item.id)}
|
||||||
></ha-checkbox>
|
></ha-checkbox>
|
||||||
</ha-formfield>
|
</ha-formfield>
|
||||||
`
|
`
|
||||||
@ -280,8 +292,8 @@ export class HaBackupDataPicker extends LitElement {
|
|||||||
<ha-formfield>
|
<ha-formfield>
|
||||||
<ha-backup-formfield-label
|
<ha-backup-formfield-label
|
||||||
slot="label"
|
slot="label"
|
||||||
.label=${this.hass.localize(
|
.label=${localize(
|
||||||
"ui.panel.config.backup.data_picker.addons"
|
`ui.panel.${this.translationKeyPanel}.data_picker.addons`
|
||||||
)}
|
)}
|
||||||
.iconPath=${mdiPuzzle}
|
.iconPath=${mdiPuzzle}
|
||||||
>
|
>
|
||||||
|
148
src/panels/config/backup/components/ha-backup-details-restore.ts
Normal file
148
src/panels/config/backup/components/ha-backup-details-restore.ts
Normal file
@ -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`
|
||||||
|
<ha-card>
|
||||||
|
<div class="card-header">
|
||||||
|
${this.localize(
|
||||||
|
`ui.panel.${this.translationKeyPanel}.details.restore.title`
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div class="card-content">
|
||||||
|
<ha-backup-data-picker
|
||||||
|
.translationKeyPanel=${this.translationKeyPanel}
|
||||||
|
.localize=${this.localize}
|
||||||
|
.hass=${this.hass}
|
||||||
|
.data=${this.backup}
|
||||||
|
.value=${this._selectedData}
|
||||||
|
@value-changed=${this._selectedBackupChanged}
|
||||||
|
.requiredItems=${this._isHomeAssistantRequired(this.haRequired)}
|
||||||
|
>
|
||||||
|
</ha-backup-data-picker>
|
||||||
|
</div>
|
||||||
|
<div class="card-actions">
|
||||||
|
<ha-button
|
||||||
|
@click=${this._restore}
|
||||||
|
.disabled=${this._isRestoreDisabled}
|
||||||
|
destructive
|
||||||
|
>
|
||||||
|
${this.localize(
|
||||||
|
`ui.panel.${this.translationKeyPanel}.details.restore.action`
|
||||||
|
)}
|
||||||
|
</ha-button>
|
||||||
|
</div>
|
||||||
|
</ha-card>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 };
|
||||||
|
}
|
||||||
|
}
|
153
src/panels/config/backup/components/ha-backup-details-summary.ts
Normal file
153
src/panels/config/backup/components/ha-backup-details-summary.ts
Normal file
@ -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`
|
||||||
|
<ha-card>
|
||||||
|
<div class="card-header">
|
||||||
|
${this.localize(
|
||||||
|
`ui.panel.${this.translationKeyPanel}.details.summary.title`
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div class="card-content">
|
||||||
|
<ha-md-list class="summary">
|
||||||
|
${this.translationKeyPanel === "config.backup"
|
||||||
|
? html`<ha-md-list-item>
|
||||||
|
<span slot="headline">
|
||||||
|
${this.localize("ui.panel.config.backup.backup_type")}
|
||||||
|
</span>
|
||||||
|
<span slot="supporting-text">
|
||||||
|
${this.localize(
|
||||||
|
`ui.panel.config.backup.type.${computeBackupType(this.backup, this.isHassio)}`
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</ha-md-list-item>`
|
||||||
|
: nothing}
|
||||||
|
<ha-md-list-item>
|
||||||
|
<span slot="headline">
|
||||||
|
${this.localize(
|
||||||
|
`ui.panel.${this.translationKeyPanel}.details.summary.size`
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<span slot="supporting-text">
|
||||||
|
${bytesToString(computeBackupSize(this.backup))}
|
||||||
|
</span>
|
||||||
|
</ha-md-list-item>
|
||||||
|
<ha-md-list-item>
|
||||||
|
<span slot="headline">
|
||||||
|
${this.localize(
|
||||||
|
`ui.panel.${this.translationKeyPanel}.details.summary.created`
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<span slot="supporting-text"> ${formattedDate} </span>
|
||||||
|
</ha-md-list-item>
|
||||||
|
</ha-md-list>
|
||||||
|
</div>
|
||||||
|
${this.showUploadAnother
|
||||||
|
? html`<div class="card-actions">
|
||||||
|
<ha-button @click=${this._uploadAnother} destructive>
|
||||||
|
${this.localize(
|
||||||
|
`ui.panel.page-onboarding.restore.details.summary.upload_another`
|
||||||
|
)}
|
||||||
|
</ha-button>
|
||||||
|
</div>`
|
||||||
|
: nothing}
|
||||||
|
</ha-card>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
@ -3,7 +3,10 @@ import type { CSSResultGroup } from "lit";
|
|||||||
import { css, html, LitElement, nothing } from "lit";
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
import { customElement, property, query, state } from "lit/decorators";
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
import { isComponentLoaded } from "../../../../common/config/is_component_loaded";
|
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-alert";
|
||||||
import "../../../../components/ha-dialog-header";
|
import "../../../../components/ha-dialog-header";
|
||||||
import "../../../../components/ha-expansion-panel";
|
import "../../../../components/ha-expansion-panel";
|
||||||
@ -14,7 +17,10 @@ import type { HaMdDialog } from "../../../../components/ha-md-dialog";
|
|||||||
import {
|
import {
|
||||||
CORE_LOCAL_AGENT,
|
CORE_LOCAL_AGENT,
|
||||||
HASSIO_LOCAL_AGENT,
|
HASSIO_LOCAL_AGENT,
|
||||||
|
SUPPORTED_UPLOAD_FORMAT,
|
||||||
uploadBackup,
|
uploadBackup,
|
||||||
|
INITIAL_UPLOAD_FORM_DATA,
|
||||||
|
type BackupUploadFileFormData,
|
||||||
} from "../../../../data/backup";
|
} from "../../../../data/backup";
|
||||||
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
||||||
import { haStyle, haStyleDialog } from "../../../../resources/styles";
|
import { haStyle, haStyleDialog } from "../../../../resources/styles";
|
||||||
@ -22,16 +28,6 @@ import type { HomeAssistant } from "../../../../types";
|
|||||||
import { showAlertDialog } from "../../../lovelace/custom-card-helpers";
|
import { showAlertDialog } from "../../../lovelace/custom-card-helpers";
|
||||||
import type { UploadBackupDialogParams } from "./show-dialog-upload-backup";
|
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")
|
@customElement("ha-dialog-upload-backup")
|
||||||
export class DialogUploadBackup
|
export class DialogUploadBackup
|
||||||
extends LitElement
|
extends LitElement
|
||||||
@ -45,13 +41,13 @@ export class DialogUploadBackup
|
|||||||
|
|
||||||
@state() private _error?: string;
|
@state() private _error?: string;
|
||||||
|
|
||||||
@state() private _formData?: FormData;
|
@state() private _formData?: BackupUploadFileFormData;
|
||||||
|
|
||||||
@query("ha-md-dialog") private _dialog?: HaMdDialog;
|
@query("ha-md-dialog") private _dialog?: HaMdDialog;
|
||||||
|
|
||||||
public async showDialog(params: UploadBackupDialogParams): Promise<void> {
|
public async showDialog(params: UploadBackupDialogParams): Promise<void> {
|
||||||
this._params = params;
|
this._params = params;
|
||||||
this._formData = INITIAL_DATA;
|
this._formData = INITIAL_UPLOAD_FORM_DATA;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _dialogClosed() {
|
private _dialogClosed() {
|
||||||
@ -78,13 +74,18 @@ export class DialogUploadBackup
|
|||||||
}
|
}
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-md-dialog open @closed=${this._dialogClosed}>
|
<ha-md-dialog
|
||||||
|
open
|
||||||
|
@closed=${this._dialogClosed}
|
||||||
|
.disableCancelAction=${this._uploading}
|
||||||
|
>
|
||||||
<ha-dialog-header slot="headline">
|
<ha-dialog-header slot="headline">
|
||||||
<ha-icon-button
|
<ha-icon-button
|
||||||
slot="navigationIcon"
|
slot="navigationIcon"
|
||||||
.label=${this.hass.localize("ui.dialogs.generic.close")}
|
.label=${this.hass.localize("ui.dialogs.generic.close")}
|
||||||
.path=${mdiClose}
|
.path=${mdiClose}
|
||||||
@click=${this.closeDialog}
|
@click=${this.closeDialog}
|
||||||
|
.disabled=${this._uploading}
|
||||||
></ha-icon-button>
|
></ha-icon-button>
|
||||||
|
|
||||||
<span slot="title">
|
<span slot="title">
|
||||||
@ -99,7 +100,8 @@ export class DialogUploadBackup
|
|||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.uploading=${this._uploading}
|
.uploading=${this._uploading}
|
||||||
.icon=${mdiFolderUpload}
|
.icon=${mdiFolderUpload}
|
||||||
accept=${SUPPORTED_FORMAT}
|
.accept=${SUPPORTED_UPLOAD_FORMAT}
|
||||||
|
.localize=${this.hass.localize}
|
||||||
.label=${this.hass.localize(
|
.label=${this.hass.localize(
|
||||||
"ui.panel.config.backup.dialogs.upload.input_label"
|
"ui.panel.config.backup.dialogs.upload.input_label"
|
||||||
)}
|
)}
|
||||||
@ -107,13 +109,17 @@ export class DialogUploadBackup
|
|||||||
"ui.panel.config.backup.dialogs.upload.supports_tar"
|
"ui.panel.config.backup.dialogs.upload.supports_tar"
|
||||||
)}
|
)}
|
||||||
@file-picked=${this._filePicked}
|
@file-picked=${this._filePicked}
|
||||||
|
@files-cleared=${this._filesCleared}
|
||||||
></ha-file-upload>
|
></ha-file-upload>
|
||||||
</div>
|
</div>
|
||||||
<div slot="actions">
|
<div slot="actions">
|
||||||
<ha-button @click=${this.closeDialog}
|
<ha-button @click=${this.closeDialog} .disabled=${this._uploading}
|
||||||
>${this.hass.localize("ui.common.cancel")}</ha-button
|
>${this.hass.localize("ui.common.cancel")}</ha-button
|
||||||
>
|
>
|
||||||
<ha-button @click=${this._upload} .disabled=${!this._formValid()}>
|
<ha-button
|
||||||
|
@click=${this._upload}
|
||||||
|
.disabled=${!this._formValid() || this._uploading}
|
||||||
|
>
|
||||||
${this.hass.localize(
|
${this.hass.localize(
|
||||||
"ui.panel.config.backup.dialogs.upload.action"
|
"ui.panel.config.backup.dialogs.upload.action"
|
||||||
)}
|
)}
|
||||||
@ -123,7 +129,7 @@ export class DialogUploadBackup
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _filePicked(ev: CustomEvent<{ files: File[] }>): Promise<void> {
|
private _filePicked(ev: HASSDomEvent<{ files: File[] }>) {
|
||||||
this._error = undefined;
|
this._error = undefined;
|
||||||
const file = ev.detail.files[0];
|
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() {
|
private async _upload() {
|
||||||
const { file } = this._formData!;
|
const { file } = this._formData!;
|
||||||
if (!file || file.type !== SUPPORTED_FORMAT) {
|
if (!file || file.type !== SUPPORTED_UPLOAD_FORMAT) {
|
||||||
showAlertDialog(this, {
|
showAlertDialog(this, {
|
||||||
title: this.hass.localize(
|
title: this.hass.localize(
|
||||||
"ui.panel.config.backup.dialogs.upload.unsupported.title"
|
"ui.panel.config.backup.dialogs.upload.unsupported.title"
|
||||||
@ -154,7 +165,7 @@ export class DialogUploadBackup
|
|||||||
|
|
||||||
this._uploading = true;
|
this._uploading = true;
|
||||||
try {
|
try {
|
||||||
await uploadBackup(this.hass!, file, agentIds);
|
await uploadBackup(this.hass, file, agentIds);
|
||||||
this._params!.submit?.();
|
this._params!.submit?.();
|
||||||
this.closeDialog();
|
this.closeDialog();
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
@ -8,7 +8,6 @@ import {
|
|||||||
} from "@mdi/js";
|
} from "@mdi/js";
|
||||||
import { css, html, LitElement, nothing } from "lit";
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { formatDateTime } from "../../../common/datetime/format_date_time";
|
|
||||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||||
import { navigate } from "../../../common/navigate";
|
import { navigate } from "../../../common/navigate";
|
||||||
import "../../../components/ha-alert";
|
import "../../../components/ha-alert";
|
||||||
@ -25,24 +24,20 @@ import type {
|
|||||||
BackupConfig,
|
BackupConfig,
|
||||||
BackupContentAgent,
|
BackupContentAgent,
|
||||||
BackupContentExtended,
|
BackupContentExtended,
|
||||||
BackupData,
|
|
||||||
} from "../../../data/backup";
|
} from "../../../data/backup";
|
||||||
|
import "./components/ha-backup-details-summary";
|
||||||
|
import "./components/ha-backup-details-restore";
|
||||||
import {
|
import {
|
||||||
compareAgents,
|
compareAgents,
|
||||||
computeBackupAgentName,
|
computeBackupAgentName,
|
||||||
computeBackupSize,
|
|
||||||
computeBackupType,
|
|
||||||
deleteBackup,
|
deleteBackup,
|
||||||
fetchBackupDetails,
|
fetchBackupDetails,
|
||||||
isLocalAgent,
|
isLocalAgent,
|
||||||
isNetworkMountAgent,
|
isNetworkMountAgent,
|
||||||
} from "../../../data/backup";
|
} from "../../../data/backup";
|
||||||
import type { HassioAddonInfo } from "../../../data/hassio/addon";
|
|
||||||
import "../../../layouts/hass-subpage";
|
import "../../../layouts/hass-subpage";
|
||||||
import type { HomeAssistant } from "../../../types";
|
import type { HomeAssistant } from "../../../types";
|
||||||
import { brandsUrl } from "../../../util/brands-url";
|
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 { showRestoreBackupDialog } from "./dialogs/show-dialog-restore-backup";
|
||||||
import { fireEvent } from "../../../common/dom/fire_event";
|
import { fireEvent } from "../../../common/dom/fire_event";
|
||||||
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
|
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||||
@ -93,10 +88,6 @@ class HaConfigBackupDetails extends LitElement {
|
|||||||
|
|
||||||
@state() private _error?: string;
|
@state() private _error?: string;
|
||||||
|
|
||||||
@state() private _selectedData?: BackupData;
|
|
||||||
|
|
||||||
@state() private _addonsInfo?: HassioAddonInfo[];
|
|
||||||
|
|
||||||
protected firstUpdated(changedProps) {
|
protected firstUpdated(changedProps) {
|
||||||
super.firstUpdated(changedProps);
|
super.firstUpdated(changedProps);
|
||||||
|
|
||||||
@ -157,81 +148,18 @@ class HaConfigBackupDetails extends LitElement {
|
|||||||
: !this._backup
|
: !this._backup
|
||||||
? html`<ha-circular-progress active></ha-circular-progress>`
|
? html`<ha-circular-progress active></ha-circular-progress>`
|
||||||
: html`
|
: html`
|
||||||
<ha-card>
|
<ha-backup-details-summary
|
||||||
<div class="card-header">
|
.backup=${this._backup}
|
||||||
${this.hass.localize(
|
.hass=${this.hass}
|
||||||
"ui.panel.config.backup.details.summary.title"
|
.localize=${this.hass.localize}
|
||||||
)}
|
.isHassio=${isHassio}
|
||||||
</div>
|
></ha-backup-details-summary>
|
||||||
<div class="card-content">
|
<ha-backup-details-restore
|
||||||
<ha-md-list class="summary">
|
.backup=${this._backup}
|
||||||
<ha-md-list-item>
|
@backup-restore=${this._restore}
|
||||||
<span slot="headline">
|
.hass=${this.hass}
|
||||||
${this.hass.localize(
|
.localize=${this.hass.localize}
|
||||||
"ui.panel.config.backup.backup_type"
|
></ha-backup-details-restore>
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
<span slot="supporting-text">
|
|
||||||
${this.hass.localize(
|
|
||||||
`ui.panel.config.backup.type.${computeBackupType(this._backup, isHassio)}`
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</ha-md-list-item>
|
|
||||||
<ha-md-list-item>
|
|
||||||
<span slot="headline">
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.backup.details.summary.size"
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
<span slot="supporting-text">
|
|
||||||
${bytesToString(computeBackupSize(this._backup))}
|
|
||||||
</span>
|
|
||||||
</ha-md-list-item>
|
|
||||||
<ha-md-list-item>
|
|
||||||
<span slot="headline">
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.backup.details.summary.created"
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
<span slot="supporting-text">
|
|
||||||
${formatDateTime(
|
|
||||||
new Date(this._backup.date),
|
|
||||||
this.hass.locale,
|
|
||||||
this.hass.config
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</ha-md-list-item>
|
|
||||||
</ha-md-list>
|
|
||||||
</div>
|
|
||||||
</ha-card>
|
|
||||||
<ha-card>
|
|
||||||
<div class="card-header">
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.backup.details.restore.title"
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div class="card-content">
|
|
||||||
<ha-backup-data-picker
|
|
||||||
.hass=${this.hass}
|
|
||||||
.data=${this._backup}
|
|
||||||
.value=${this._selectedData}
|
|
||||||
@value-changed=${this._selectedBackupChanged}
|
|
||||||
.addonsInfo=${this._addonsInfo}
|
|
||||||
>
|
|
||||||
</ha-backup-data-picker>
|
|
||||||
</div>
|
|
||||||
<div class="card-actions">
|
|
||||||
<ha-button
|
|
||||||
@click=${this._restore}
|
|
||||||
.disabled=${this._isRestoreDisabled()}
|
|
||||||
class="danger"
|
|
||||||
>
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.backup.details.restore.action"
|
|
||||||
)}
|
|
||||||
</ha-button>
|
|
||||||
</div>
|
|
||||||
</ha-card>
|
|
||||||
<ha-card>
|
<ha-card>
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
${this.hass.localize(
|
${this.hass.localize(
|
||||||
@ -360,30 +288,13 @@ class HaConfigBackupDetails extends LitElement {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _selectedBackupChanged(ev: CustomEvent) {
|
private _restore(ev: CustomEvent) {
|
||||||
ev.stopPropagation();
|
if (!this._backup || !ev.detail.selectedData) {
|
||||||
this._selectedData = ev.detail.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _isRestoreDisabled() {
|
|
||||||
return (
|
|
||||||
!this._selectedData ||
|
|
||||||
!(
|
|
||||||
this._selectedData?.database_included ||
|
|
||||||
this._selectedData?.homeassistant_included ||
|
|
||||||
this._selectedData.addons.length ||
|
|
||||||
this._selectedData.folders.length
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _restore() {
|
|
||||||
if (!this._backup || !this._selectedData) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
showRestoreBackupDialog(this, {
|
showRestoreBackupDialog(this, {
|
||||||
backup: this._backup,
|
backup: this._backup,
|
||||||
selectedData: this._selectedData,
|
selectedData: ev.detail.selectedData,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -469,13 +380,6 @@ class HaConfigBackupDetails extends LitElement {
|
|||||||
--mdc-icon-size: 48px;
|
--mdc-icon-size: 48px;
|
||||||
color: var(--primary-text-color);
|
color: var(--primary-text-color);
|
||||||
}
|
}
|
||||||
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);
|
|
||||||
}
|
|
||||||
.warning {
|
.warning {
|
||||||
color: var(--error-color);
|
color: var(--error-color);
|
||||||
}
|
}
|
||||||
@ -485,9 +389,6 @@ class HaConfigBackupDetails extends LitElement {
|
|||||||
ha-button.danger {
|
ha-button.danger {
|
||||||
--mdc-theme-primary: var(--error-color);
|
--mdc-theme-primary: var(--error-color);
|
||||||
}
|
}
|
||||||
ha-backup-data-picker {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
ha-md-list-item [slot="supporting-text"] {
|
ha-md-list-item [slot="supporting-text"] {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -8193,9 +8193,53 @@
|
|||||||
},
|
},
|
||||||
"restore": {
|
"restore": {
|
||||||
"header": "Restore a backup",
|
"header": "Restore a backup",
|
||||||
|
"upload_backup": "[%key:ui::panel::config::backup::dialogs::upload::title%]",
|
||||||
|
"unsupported": {
|
||||||
|
"title": "[%key:ui::panel::config::backup::dialogs::upload::unsupported::title%]",
|
||||||
|
"text": "[%key:ui::panel::config::backup::dialogs::upload::unsupported::text%]"
|
||||||
|
},
|
||||||
|
"ok": "[%key:ui::common::ok%]",
|
||||||
|
"upload_input_label": "[%key:ui::panel::config::backup::dialogs::upload::input_label%]",
|
||||||
|
"upload_supports_tar": "[%key:ui::panel::config::backup::dialogs::upload::supports_tar%]",
|
||||||
|
"upload_secondary": "[%key:ui::components::file-upload::secondary%]",
|
||||||
|
"delete": "[%key:ui::common::delete%]",
|
||||||
|
"uploading": "[%key:ui::components::file-upload::uploading%]",
|
||||||
|
"details": {
|
||||||
|
"home_assistant_missing": "This backup does not include your Home Assistant configuration, you cannot use it to restore your instance.",
|
||||||
|
"addons_unsupported": "This backup includes add-ons and folders, which are not supported in this installation method of Home Assistant. You can still restore Home Assistant, but the unsupported files will not be restored.",
|
||||||
|
"summary": {
|
||||||
|
"title": "[%key:ui::panel::config::backup::details::summary::title%]",
|
||||||
|
"size": "[%key:ui::panel::config::backup::details::summary::size%]",
|
||||||
|
"created": "[%key:ui::panel::config::backup::details::summary::created%]",
|
||||||
|
"upload_another": "Upload another",
|
||||||
|
"home": "Home"
|
||||||
|
},
|
||||||
|
"restore": {
|
||||||
|
"title": "[%key:ui::panel::config::backup::details::restore::title%]",
|
||||||
|
"action": "[%key:ui::panel::config::backup::details::restore::action%]",
|
||||||
|
"encryption": {
|
||||||
|
"title": "[%key:ui::panel::config::backup::dialogs::restore::encryption::different_key%]",
|
||||||
|
"incorrect_key": "[%key:ui::panel::config::backup::dialogs::restore::encryption::incorrect_key%]",
|
||||||
|
"input_label": "[%key:ui::panel::config::backup::dialogs::restore::encryption::input_label%]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"data_picker": {
|
||||||
|
"settings": "[%key:ui::panel::config::backup::data_picker::settings%]",
|
||||||
|
"settings_and_history": "[%key:ui::panel::config::backup::data_picker::settings_and_history%]",
|
||||||
|
"media": "[%key:ui::panel::config::backup::data_picker::media%]",
|
||||||
|
"share_folder": "[%key:ui::panel::config::backup::data_picker::share_folder%]",
|
||||||
|
"local_addons": "[%key:ui::panel::config::backup::data_picker::local_addons%]",
|
||||||
|
"addons": "[%key:ui::panel::config::backup::data_picker::addons%]",
|
||||||
|
"ssl": "[%key:ui::panel::config::backup::data_picker::ssl%]"
|
||||||
|
},
|
||||||
|
"restore_no_home_assistant": "[%key:supervisor::backup::restore_no_home_assistant%]",
|
||||||
"in_progress": "Restore in progress",
|
"in_progress": "Restore in progress",
|
||||||
|
"in_progress_description": "The restore process is running in the background. Home Assistant will automatically start again once the restore is complete. Please be patient, this can take a while. Do not close or refresh this page.",
|
||||||
"failed": "Restore failed",
|
"failed": "Restore failed",
|
||||||
"upload_backup": "[%key:supervisor::backup::upload_backup%]",
|
"failed_status_description": "The backup could not be restored. Please try again.",
|
||||||
|
"failed_description": "The last restore attempt failed. Please try it again.",
|
||||||
|
"failed_wrong_password_description": "The last restore attempt failed, because the encryption key was incorrect. Please try it again.",
|
||||||
"upload_supports": "Supports .TAR files",
|
"upload_supports": "Supports .TAR files",
|
||||||
"upload_drop": "[%key:ui::components::file-upload::secondary%]",
|
"upload_drop": "[%key:ui::components::file-upload::secondary%]",
|
||||||
"show_log": "Show full log",
|
"show_log": "Show full log",
|
||||||
@ -8203,7 +8247,6 @@
|
|||||||
"full_backup": "[%key:supervisor::backup::full_backup%]",
|
"full_backup": "[%key:supervisor::backup::full_backup%]",
|
||||||
"partial_backup": "[%key:supervisor::backup::partial_backup%]",
|
"partial_backup": "[%key:supervisor::backup::partial_backup%]",
|
||||||
"name": "[%key:supervisor::backup::name%]",
|
"name": "[%key:supervisor::backup::name%]",
|
||||||
"type": "[%key:supervisor::backup::type%]",
|
|
||||||
"select_type": "[%key:supervisor::backup::select_type%]",
|
"select_type": "[%key:supervisor::backup::select_type%]",
|
||||||
"folders": "[%key:supervisor::backup::folders%]",
|
"folders": "[%key:supervisor::backup::folders%]",
|
||||||
"addons": "[%key:supervisor::backup::addons%]",
|
"addons": "[%key:supervisor::backup::addons%]",
|
||||||
@ -8218,10 +8261,16 @@
|
|||||||
"close": "[%key:ui::common::close%]",
|
"close": "[%key:ui::common::close%]",
|
||||||
"cancel": "[%key:ui::common::cancel%]",
|
"cancel": "[%key:ui::common::cancel%]",
|
||||||
"retry": "Retry",
|
"retry": "Retry",
|
||||||
|
"back": "[%key:ui::common::back%]",
|
||||||
"restore_start_failed": "[%key:supervisor::backup::restore_start_failed%]",
|
"restore_start_failed": "[%key:supervisor::backup::restore_start_failed%]",
|
||||||
"no_backup_found": "[%key:supervisor::backup::no_backup_found%]",
|
"no_backup_found": "[%key:supervisor::backup::no_backup_found%]",
|
||||||
"restore_no_home_assistant": "[%key:supervisor::backup::restore_no_home_assistant%]",
|
"unnamed_backup": "[%key:supervisor::backup::unnamed_backup%]",
|
||||||
"unnamed_backup": "[%key:supervisor::backup::unnamed_backup%]"
|
"cancel_restore": {
|
||||||
|
"title": "Cancel restore process?",
|
||||||
|
"text": "Are you sure you want to cancel the restore process and return to the onboarding?",
|
||||||
|
"yes": "[%key:ui::common::yes%]",
|
||||||
|
"no": "[%key:ui::common::no%]"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"custom": {
|
"custom": {
|
||||||
|
@ -4,6 +4,7 @@ import {
|
|||||||
formatDateTime,
|
formatDateTime,
|
||||||
formatDateTimeWithSeconds,
|
formatDateTimeWithSeconds,
|
||||||
formatDateTimeNumeric,
|
formatDateTimeNumeric,
|
||||||
|
formatDateTimeWithBrowserDefaults,
|
||||||
} from "../../../src/common/datetime/format_date_time";
|
} from "../../../src/common/datetime/format_date_time";
|
||||||
import {
|
import {
|
||||||
NumberFormat,
|
NumberFormat,
|
||||||
@ -49,6 +50,19 @@ describe("formatDateTime", () => {
|
|||||||
"November 18, 2017 at 23:12"
|
"November 18, 2017 at 23:12"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("Formats date times without optional params", () => {
|
||||||
|
assert.strictEqual(
|
||||||
|
formatDateTimeWithBrowserDefaults(dateObj),
|
||||||
|
new Intl.DateTimeFormat(undefined, {
|
||||||
|
year: "numeric",
|
||||||
|
month: "long",
|
||||||
|
day: "numeric",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
}).format(dateObj)
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("formatDateTimeWithSeconds", () => {
|
describe("formatDateTimeWithSeconds", () => {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user