From d31f4a5f1db704881047ce1f0828cb1fcdc64530 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Mon, 16 Dec 2024 14:08:51 +0100 Subject: [PATCH] Improve backup onboarding (#23282) * Sort agents, enable cloud and local by default * Enable cloud by default * Improve wording * Hide fab if onboarding is not complete * Add recommended settings * Use one step encryption key during onboarding * Add description for cloud * Update change encryption key dialog --- src/data/backup.ts | 12 +- .../components/ha-backup-agents-picker.ts | 3 +- .../components/ha-backup-config-agents.ts | 39 ++-- .../dialogs/dialog-backup-onboarding.ts | 217 +++++++++++------- .../dialog-change-backup-encryption-key.ts | 119 ++++++---- .../dialogs/show-dialog-backup_onboarding.ts | 2 + .../backup/ha-config-backup-dashboard.ts | 57 +++-- .../config/backup/ha-config-backup-details.ts | 6 +- .../backup/ha-config-backup-strategy.ts | 4 + src/panels/config/backup/ha-config-backup.ts | 4 + 10 files changed, 291 insertions(+), 172 deletions(-) diff --git a/src/data/backup.ts b/src/data/backup.ts index 0d26743d97..39d7a4fc12 100644 --- a/src/data/backup.ts +++ b/src/data/backup.ts @@ -212,8 +212,12 @@ export const getPreferredAgentForDownload = (agents: string[]) => { return localAgents[0] || agents[0]; }; +export const CORE_LOCAL_AGENT = "backup.local"; +export const HASSIO_LOCAL_AGENT = "backup.hassio"; +export const CLOUD_AGENT = "cloud.cloud"; + export const isLocalAgent = (agentId: string) => - ["backup.local", "hassio.local"].includes(agentId); + [CORE_LOCAL_AGENT, HASSIO_LOCAL_AGENT].includes(agentId); export const computeBackupAgentName = ( localize: LocalizeFunc, @@ -234,6 +238,12 @@ export const computeBackupAgentName = ( return showName ? `${domainName}: ${name}` : domainName; }; +export const compareAgents = (a: string, b: string) => { + const isLocalA = isLocalAgent(a); + const isLocalB = isLocalAgent(b); + return isLocalA === isLocalB ? a.localeCompare(b) : isLocalA ? -1 : 1; +}; + export const generateEncryptionKey = () => { const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; const pattern = "xxxx-xxxx-xxxx-xxxx-xxxx-xxxx-xxxx"; diff --git a/src/panels/config/backup/components/ha-backup-agents-picker.ts b/src/panels/config/backup/components/ha-backup-agents-picker.ts index 27b1a033ef..a586eec3f7 100644 --- a/src/panels/config/backup/components/ha-backup-agents-picker.ts +++ b/src/panels/config/backup/components/ha-backup-agents-picker.ts @@ -8,6 +8,7 @@ import "../../../../components/ha-checkbox"; import "../../../../components/ha-formfield"; import "../../../../components/ha-svg-icon"; import { + compareAgents, computeBackupAgentName, isLocalAgent, type BackupAgent, @@ -33,7 +34,7 @@ class HaBackupAgentsPicker extends LitElement { public value!: string[]; private _agentIds = memoizeOne((agents: BackupAgent[]) => - agents.map((agent) => agent.agent_id) + agents.map((agent) => agent.agent_id).sort(compareAgents) ); render() { diff --git a/src/panels/config/backup/components/ha-backup-config-agents.ts b/src/panels/config/backup/components/ha-backup-config-agents.ts index 581b3cd252..4343689f32 100644 --- a/src/panels/config/backup/components/ha-backup-config-agents.ts +++ b/src/panels/config/backup/components/ha-backup-config-agents.ts @@ -1,19 +1,21 @@ import { mdiDatabase } from "@mdi/js"; import type { PropertyValues } from "lit"; -import { css, html, LitElement } from "lit"; +import { css, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import { fireEvent } from "../../../../common/dom/fire_event"; import { computeDomain } from "../../../../common/entity/compute_domain"; import "../../../../components/ha-md-list"; import "../../../../components/ha-md-list-item"; -import "../../../../components/ha-switch"; import "../../../../components/ha-svg-icon"; -import type { BackupAgent } from "../../../../data/backup"; +import "../../../../components/ha-switch"; import { + CLOUD_AGENT, + compareAgents, computeBackupAgentName, fetchBackupAgentsInfo, isLocalAgent, } from "../../../../data/backup"; +import type { CloudStatus } from "../../../../data/cloud"; import type { HomeAssistant } from "../../../../types"; import { brandsUrl } from "../../../../util/brands-url"; @@ -23,7 +25,9 @@ const DEFAULT_AGENTS = []; class HaBackupConfigAgents extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; - @state() private _agents: BackupAgent[] = []; + @property({ attribute: false }) public cloudStatus!: CloudStatus; + + @state() private _agentIds: string[] = []; @state() private value?: string[]; @@ -34,7 +38,10 @@ class HaBackupConfigAgents extends LitElement { private async _fetchAgents() { const { agents } = await fetchBackupAgentsInfo(this.hass); - this._agents = agents; + this._agentIds = agents + .map((agent) => agent.agent_id) + .filter((id) => id !== CLOUD_AGENT || this.cloudStatus.logged_in) + .sort(compareAgents); } private get _value() { @@ -42,18 +49,16 @@ class HaBackupConfigAgents extends LitElement { } protected render() { - const agentIds = this._agents.map((agent) => agent.agent_id); - return html` - ${agentIds.length > 0 + ${this._agentIds.length > 0 ? html` - ${agentIds.map((agentId) => { + ${this._agentIds.map((agentId) => { const domain = computeDomain(agentId); const name = computeBackupAgentName( this.hass.localize, agentId, - agentIds + this._agentIds ); return html` @@ -77,6 +82,14 @@ class HaBackupConfigAgents extends LitElement { /> `}
${name}
+ ${agentId === CLOUD_AGENT + ? html` +
+ It stores one backup. The oldest backups are + deleted. +
+ ` + : nothing} - this._agents.some((a) => a.agent_id === agent) - ); + this.value = this.value + .filter((agent) => this._agentIds.some((id) => id === agent)) + .filter((id) => id !== CLOUD_AGENT || this.cloudStatus); fireEvent(this, "value-changed", { value: this.value }); } diff --git a/src/panels/config/backup/dialogs/dialog-backup-onboarding.ts b/src/panels/config/backup/dialogs/dialog-backup-onboarding.ts index 5ae379e176..790e691002 100644 --- a/src/panels/config/backup/dialogs/dialog-backup-onboarding.ts +++ b/src/panels/config/backup/dialogs/dialog-backup-onboarding.ts @@ -1,13 +1,15 @@ -import { mdiClose, mdiDownload, mdiKey } from "@mdi/js"; +import { mdiClose, mdiContentCopy, mdiDownload } from "@mdi/js"; import type { CSSResultGroup } from "lit"; -import { LitElement, css, html, nothing } from "lit"; +import { css, html, LitElement, nothing } from "lit"; import { customElement, property, query, state } from "lit/decorators"; import { isComponentLoaded } from "../../../../common/config/is_component_loaded"; import { fireEvent } from "../../../../common/dom/fire_event"; +import { copyToClipboard } from "../../../../common/util/copy-clipboard"; import "../../../../components/ha-button"; import "../../../../components/ha-dialog-header"; import "../../../../components/ha-icon-button"; import "../../../../components/ha-icon-button-prev"; +import "../../../../components/ha-icon-next"; import "../../../../components/ha-md-dialog"; import type { HaMdDialog } from "../../../../components/ha-md-dialog"; import "../../../../components/ha-md-list"; @@ -20,7 +22,10 @@ import type { } from "../../../../data/backup"; import { BackupScheduleState, + CLOUD_AGENT, + CORE_LOCAL_AGENT, generateEncryptionKey, + HASSIO_LOCAL_AGENT, updateBackupConfig, } from "../../../../data/backup"; import type { HassDialog } from "../../../../dialogs/make-dialog-manager"; @@ -33,12 +38,12 @@ import "../components/ha-backup-config-data"; import type { BackupConfigData } from "../components/ha-backup-config-data"; import "../components/ha-backup-config-schedule"; import type { BackupConfigSchedule } from "../components/ha-backup-config-schedule"; -import type { SetBackupEncryptionKeyDialogParams } from "./show-dialog-set-backup-encryption-key"; +import type { BackupOnboardingDialogParams } from "./show-dialog-backup_onboarding"; const STEPS = [ "welcome", - "new_key", - "save_key", + "key", + "setup", "schedule", "data", "locations", @@ -46,7 +51,9 @@ const STEPS = [ type Step = (typeof STEPS)[number]; -const INITIAL_CONFIG: BackupConfig = { +const FULL_DIALOG_STEPS = new Set(["setup"]); + +const RECOMMENDED_CONFIG: BackupConfig = { create_backup: { agent_ids: [], include_folders: [], @@ -68,27 +75,37 @@ const INITIAL_CONFIG: BackupConfig = { }; @customElement("ha-dialog-backup-onboarding") -class DialogSetBackupEncryptionKey extends LitElement implements HassDialog { +class DialogBackupOnboarding extends LitElement implements HassDialog { @property({ attribute: false }) public hass!: HomeAssistant; @state() private _opened = false; @state() private _step?: Step; - @state() private _params?: SetBackupEncryptionKeyDialogParams; + @state() private _params?: BackupOnboardingDialogParams; @query("ha-md-dialog") private _dialog!: HaMdDialog; @state() private _config?: BackupConfig; - private _suggestedEncryptionKey?: string; - - public showDialog(params: SetBackupEncryptionKeyDialogParams): void { + public showDialog(params: BackupOnboardingDialogParams): void { this._params = params; this._step = STEPS[0]; - this._config = INITIAL_CONFIG; + this._config = RECOMMENDED_CONFIG; + + // Enable local location by default + if (isComponentLoaded(this.hass, "hassio")) { + this._config.create_backup.agent_ids.push(HASSIO_LOCAL_AGENT); + } else { + this._config.create_backup.agent_ids.push(CORE_LOCAL_AGENT); + } + // Enable cloud location if logged in + if (this._params.cloudStatus.logged_in) { + this._config.create_backup.agent_ids.push(CLOUD_AGENT); + } + + this._config.create_backup.password = generateEncryptionKey(); this._opened = true; - this._suggestedEncryptionKey = generateEncryptionKey(); } public closeDialog(): void { @@ -102,7 +119,6 @@ class DialogSetBackupEncryptionKey extends LitElement implements HassDialog { this._step = undefined; this._config = undefined; this._params = undefined; - this._suggestedEncryptionKey = undefined; } private async _done() { @@ -158,7 +174,7 @@ class DialogSetBackupEncryptionKey extends LitElement implements HassDialog { } protected render() { - if (!this._opened || !this._params) { + if (!this._opened || !this._params || !this._step) { return nothing; } @@ -187,25 +203,29 @@ class DialogSetBackupEncryptionKey extends LitElement implements HassDialog { ${this._stepTitle}
${this._renderStepContent()}
-
- ${isLastStep - ? html` - - Save - - ` - : html` - - Next - - `} -
+ ${!FULL_DIALOG_STEPS.has(this._step) + ? html` +
+ ${isLastStep + ? html` + + Save + + ` + : html` + + Next + + `} +
+ ` + : nothing} `; } @@ -214,10 +234,10 @@ class DialogSetBackupEncryptionKey extends LitElement implements HassDialog { switch (this._step) { case "welcome": return ""; - case "new_key": + case "key": return "Encryption key"; - case "save_key": - return "Save encryption key"; + case "setup": + return "Set up your backup strategy"; case "schedule": return "Automatic backups"; case "data": @@ -231,9 +251,9 @@ class DialogSetBackupEncryptionKey extends LitElement implements HassDialog { private _isStepValid(): boolean { switch (this._step) { - case "new_key": - return !!this._config?.create_backup.password; - case "save_key": + case "key": + return true; + case "setup": return true; case "schedule": return !!this._config?.schedule; @@ -269,37 +289,20 @@ class DialogSetBackupEncryptionKey extends LitElement implements HassDialog {

`; - case "new_key": + case "key": return html`

All your backups are encrypted to keep your data private and secure. - You need this encryption key to restore any backup. -

- - - - - Use suggested encryption key - - ${this._suggestedEncryptionKey} - - - Enter - - - - `; - case "save_key": - return html` -

- It’s important that you don’t lose this encryption key. We recommend - to save this key somewhere secure. As you can only restore your data - with the backup encryption key. + We recommend to save this key somewhere secure. As you can only + restore your data with the backup encryption key.

+
+

${this._config.create_backup.password}

+ +
Download emergency kit @@ -313,6 +316,29 @@ class DialogSetBackupEncryptionKey extends LitElement implements HassDialog { `; + case "setup": + return html` +

+ It is recommended to create a daily backup and keep copies of the + last 3 days on two different locations. And one of them is off-site. +

+ + + Recommended settings + + Set the proven backup strategy of daily backup. + + + + + Custom settings + + Select your own automation, data and locations + + + + + `; case "schedule": return html`

@@ -347,6 +373,7 @@ class DialogSetBackupEncryptionKey extends LitElement implements HassDialog { `; @@ -365,23 +392,11 @@ class DialogSetBackupEncryptionKey extends LitElement implements HassDialog { ); } - private _encryptionKeyChanged(ev) { - const value = ev.target.value; - this._setEncryptionKey(value); - } - - private _useSuggestedEncryptionKey() { - this._setEncryptionKey(this._suggestedEncryptionKey!); - } - - private _setEncryptionKey(value: string) { - this._config = { - ...this._config!, - create_backup: { - ...this._config!.create_backup, - password: value, - }, - }; + private _copyKeyToClipboard() { + copyToClipboard(this._config!.create_backup.password!); + showToast(this, { + message: this.hass.localize("ui.common.copied_clipboard"), + }); } private _dataConfig(config: BackupConfig): BackupConfigData { @@ -442,7 +457,7 @@ class DialogSetBackupEncryptionKey extends LitElement implements HassDialog { css` ha-md-dialog { width: 90vw; - max-width: 500px; + max-width: 560px; } div[slot="content"] { margin-top: -16px; @@ -452,6 +467,12 @@ class DialogSetBackupEncryptionKey extends LitElement implements HassDialog { --md-list-item-leading-space: 0; --md-list-item-trailing-space: 0; } + ha-md-list.full { + --md-list-item-leading-space: 24px; + --md-list-item-trailing-space: 24px; + margin-left: -24px; + margin-right: -24px; + } @media all and (max-width: 450px), all and (max-height: 500px) { ha-md-dialog { max-width: none; @@ -466,6 +487,30 @@ class DialogSetBackupEncryptionKey extends LitElement implements HassDialog { .welcome { text-align: center; } + .encryption-key { + border: 1px solid var(--divider-color); + background-color: var(--primary-background-color); + border-radius: 8px; + padding: 16px; + display: flex; + flex-direction: row; + align-items: center; + gap: 24px; + } + .encryption-key p { + margin: 0; + flex: 1; + font-family: "Roboto Mono", "Consolas", "Menlo", monospace; + font-size: 20px; + font-style: normal; + font-weight: 400; + line-height: 28px; + text-align: center; + } + .encryption-key ha-icon-button { + flex: none; + margin: -16px; + } `, ]; } @@ -473,6 +518,6 @@ class DialogSetBackupEncryptionKey extends LitElement implements HassDialog { declare global { interface HTMLElementTagNameMap { - "ha-dialog-backup-onboarding": DialogSetBackupEncryptionKey; + "ha-dialog-backup-onboarding": DialogBackupOnboarding; } } diff --git a/src/panels/config/backup/dialogs/dialog-change-backup-encryption-key.ts b/src/panels/config/backup/dialogs/dialog-change-backup-encryption-key.ts index a2e183e764..7dbdaed43a 100644 --- a/src/panels/config/backup/dialogs/dialog-change-backup-encryption-key.ts +++ b/src/panels/config/backup/dialogs/dialog-change-backup-encryption-key.ts @@ -1,8 +1,9 @@ -import { mdiClose, mdiDownload, mdiKey } from "@mdi/js"; +import { mdiClose, mdiContentCopy, mdiDownload } from "@mdi/js"; import type { CSSResultGroup } from "lit"; import { LitElement, css, html, nothing } from "lit"; import { customElement, property, query, state } from "lit/decorators"; import { fireEvent } from "../../../../common/dom/fire_event"; +import { copyToClipboard } from "../../../../common/util/copy-clipboard"; import "../../../../components/ha-button"; import "../../../../components/ha-dialog-header"; import "../../../../components/ha-icon-button"; @@ -17,9 +18,12 @@ import type { HassDialog } from "../../../../dialogs/make-dialog-manager"; import { haStyle, haStyleDialog } from "../../../../resources/styles"; import type { HomeAssistant } from "../../../../types"; import { fileDownload } from "../../../../util/file_download"; +import { showToast } from "../../../../util/toast"; import type { ChangeBackupEncryptionKeyDialogParams } from "./show-dialog-change-backup-encryption-key"; -const STEPS = ["current", "new", "save"] as const; +const STEPS = ["current", "new", "done"] as const; + +type Step = (typeof STEPS)[number]; @customElement("ha-dialog-change-backup-encryption-key") class DialogChangeBackupEncryptionKey extends LitElement implements HassDialog { @@ -27,7 +31,7 @@ class DialogChangeBackupEncryptionKey extends LitElement implements HassDialog { @state() private _opened = false; - @state() private _step?: "current" | "new" | "save"; + @state() private _step?: Step; @state() private _params?: ChangeBackupEncryptionKeyDialogParams; @@ -35,13 +39,11 @@ class DialogChangeBackupEncryptionKey extends LitElement implements HassDialog { @state() private _newEncryptionKey?: string; - private _suggestedEncryptionKey?: string; - public showDialog(params: ChangeBackupEncryptionKeyDialogParams): void { this._params = params; this._step = STEPS[0]; this._opened = true; - this._suggestedEncryptionKey = generateEncryptionKey(); + this._newEncryptionKey = generateEncryptionKey(); } public closeDialog(): void { @@ -55,7 +57,6 @@ class DialogChangeBackupEncryptionKey extends LitElement implements HassDialog { this._step = undefined; this._params = undefined; this._newEncryptionKey = undefined; - this._suggestedEncryptionKey = undefined; } private _done() { @@ -89,7 +90,7 @@ class DialogChangeBackupEncryptionKey extends LitElement implements HassDialog { ? "Save current encryption key" : this._step === "new" ? "New encryption key" - : "Save new encryption key"; + : ""; return html` @@ -120,13 +121,12 @@ class DialogChangeBackupEncryptionKey extends LitElement implements HassDialog { Change encryption key ` - : this._step === "save" - ? html`Done` - : nothing} + : html`Done`} `; @@ -143,7 +143,7 @@ class DialogChangeBackupEncryptionKey extends LitElement implements HassDialog {

- Download emergency kit + Download old emergency kit We recommend to save this encryption key somewhere secure. @@ -155,36 +155,22 @@ class DialogChangeBackupEncryptionKey extends LitElement implements HassDialog { `; case "new": - return html` -

All next backups will use the new encryption key.

- - - - - Use suggested encryption key - - ${this._suggestedEncryptionKey} - - - Enter - - - - `; - case "save": return html`

- It’s important that you don’t lose this encryption key. We recommend - to save this key somewhere secure. As you can only restore your data + All next backups will use the new encryption key. We recommend to + save this key somewhere secure. As you can only restore your data with the backup encryption key.

+
+

${this._newEncryptionKey}

+ +
- Download emergency kit + Download new emergency kit We recommend to save this encryption key somewhere secure. @@ -195,10 +181,27 @@ class DialogChangeBackupEncryptionKey extends LitElement implements HassDialog { `; + case "done": + return html` +
+ Casita Home Assistant logo +

Encryption key changed

+
+ `; } return nothing; } + private _copyKeyToClipboard() { + copyToClipboard(this._newEncryptionKey); + showToast(this, { + message: this.hass.localize("ui.common.copied_clipboard"), + }); + } + private _downloadOld() { if (!this._params?.currentKey) { return; @@ -221,14 +224,6 @@ class DialogChangeBackupEncryptionKey extends LitElement implements HassDialog { ); } - private _encryptionKeyChanged(ev) { - this._newEncryptionKey = ev.target.value; - } - - private _useSuggestedEncryptionKey() { - this._newEncryptionKey = this._suggestedEncryptionKey; - } - private async _submit() { if (!this._newEncryptionKey) { return; @@ -244,7 +239,7 @@ class DialogChangeBackupEncryptionKey extends LitElement implements HassDialog { css` ha-md-dialog { width: 90vw; - max-width: 500px; + max-width: 560px; } div[slot="content"] { margin-top: -16px; @@ -254,6 +249,33 @@ class DialogChangeBackupEncryptionKey extends LitElement implements HassDialog { --md-list-item-leading-space: 0; --md-list-item-trailing-space: 0; } + ha-button.danger { + --mdc-theme-primary: var(--error-color); + } + .encryption-key { + border: 1px solid var(--divider-color); + background-color: var(--primary-background-color); + border-radius: 8px; + padding: 16px; + display: flex; + flex-direction: row; + align-items: center; + gap: 24px; + } + .encryption-key p { + margin: 0; + flex: 1; + font-family: "Roboto Mono", "Consolas", "Menlo", monospace; + font-size: 20px; + font-style: normal; + font-weight: 400; + line-height: 28px; + text-align: center; + } + .encryption-key ha-icon-button { + flex: none; + margin: -16px; + } @media all and (max-width: 450px), all and (max-height: 500px) { ha-md-dialog { max-width: none; @@ -265,6 +287,13 @@ class DialogChangeBackupEncryptionKey extends LitElement implements HassDialog { p { margin-top: 0; } + .done { + text-align: center; + font-size: 22px; + font-style: normal; + font-weight: 400; + line-height: 28px; + } `, ]; } diff --git a/src/panels/config/backup/dialogs/show-dialog-backup_onboarding.ts b/src/panels/config/backup/dialogs/show-dialog-backup_onboarding.ts index e8d88a4f4a..578f80bf13 100644 --- a/src/panels/config/backup/dialogs/show-dialog-backup_onboarding.ts +++ b/src/panels/config/backup/dialogs/show-dialog-backup_onboarding.ts @@ -1,8 +1,10 @@ import { fireEvent } from "../../../../common/dom/fire_event"; +import type { CloudStatus } from "../../../../data/cloud"; export interface BackupOnboardingDialogParams { submit?: (value: boolean) => void; cancel?: () => void; + cloudStatus: CloudStatus; } const loadDialog = () => import("./dialog-backup-onboarding"); diff --git a/src/panels/config/backup/ha-config-backup-dashboard.ts b/src/panels/config/backup/ha-config-backup-dashboard.ts index 0a6da83f2c..4e72b9c55f 100644 --- a/src/panels/config/backup/ha-config-backup-dashboard.ts +++ b/src/panels/config/backup/ha-config-backup-dashboard.ts @@ -18,6 +18,7 @@ import type { HASSDomEvent } from "../../../common/dom/fire_event"; import { computeDomain } from "../../../common/entity/compute_domain"; import { shouldHandleRequestSelectedEvent } from "../../../common/mwc/handle-request-selected-event"; import { navigate } from "../../../common/navigate"; +import { capitalizeFirstLetter } from "../../../common/string/capitalize-first-letter"; import type { LocalizeFunc } from "../../../common/translations/localize"; import type { DataTableColumnContainer, @@ -36,6 +37,7 @@ import "../../../components/ha-svg-icon"; import { getSignedPath } from "../../../data/auth"; import type { BackupConfig, BackupContent } from "../../../data/backup"; import { + compareAgents, computeBackupAgentName, deleteBackup, fetchBackupConfig, @@ -51,6 +53,7 @@ import { DEFAULT_MANAGER_STATE, subscribeBackupEvents, } from "../../../data/backup_manager"; +import type { CloudStatus } from "../../../data/cloud"; import { extractApiErrorMessage } from "../../../data/hassio/common"; import { showAlertDialog, @@ -72,7 +75,6 @@ import { showBackupOnboardingDialog } from "./dialogs/show-dialog-backup_onboard import { showGenerateBackupDialog } from "./dialogs/show-dialog-generate-backup"; import { showNewBackupDialog } from "./dialogs/show-dialog-new-backup"; import { showUploadBackupDialog } from "./dialogs/show-dialog-upload-backup"; -import { capitalizeFirstLetter } from "../../../common/string/capitalize-first-letter"; interface BackupRow extends BackupContent { formatted_type: string; @@ -86,6 +88,8 @@ const TYPE_ORDER: Array = ["strategy", "custom"]; class HaConfigBackupDashboard extends SubscribeMixin(LitElement) { @property({ attribute: false }) public hass!: HomeAssistant; + @property({ attribute: false }) public cloudStatus!: CloudStatus; + @property({ type: Boolean }) public narrow = false; @property({ attribute: false }) public route!: Route; @@ -331,7 +335,7 @@ class HaConfigBackupDashboard extends SubscribeMixin(LitElement) { slot="action" @click=${this._setupBackupStrategy} > - Setup backup strategy + Set up backup strategy ` @@ -401,16 +405,21 @@ class HaConfigBackupDashboard extends SubscribeMixin(LitElement) { ` : nothing} - - - - + ${!this._needsOnboarding + ? html` + + + + ` + : nothing} `; } @@ -480,7 +489,10 @@ class HaConfigBackupDashboard extends SubscribeMixin(LitElement) { private async _fetchBackupInfo() { const info = await fetchBackupInfo(this.hass); - this._backups = info.backups; + this._backups = info.backups.map((backup) => ({ + ...backup, + agent_ids: backup.agent_ids?.sort(compareAgents), + })); } private async _fetchBackupConfig() { @@ -489,7 +501,7 @@ class HaConfigBackupDashboard extends SubscribeMixin(LitElement) { } private get _needsOnboarding() { - return this._config && !this._config.create_backup.password; + return !this._config?.create_backup.password; } private async _uploadBackup(ev) { @@ -501,15 +513,6 @@ class HaConfigBackupDashboard extends SubscribeMixin(LitElement) { } private async _newBackup(): Promise { - if (this._needsOnboarding) { - const success = await showBackupOnboardingDialog(this, {}); - if (!success) { - return; - } - } - - await this._fetchBackupConfig(); - const config = this._config!; const type = await showNewBackupDialog(this, { config }); @@ -603,12 +606,16 @@ class HaConfigBackupDashboard extends SubscribeMixin(LitElement) { } private async _setupBackupStrategy() { - const success = await showBackupOnboardingDialog(this, {}); + const success = await showBackupOnboardingDialog(this, { + cloudStatus: this.cloudStatus, + }); if (!success) { return; } - await this._fetchBackupConfig(); + this._fetchBackupConfig(); + await generateBackupWithStrategySettings(this.hass); + await this._fetchBackupInfo(); } static get styles(): CSSResultGroup { diff --git a/src/panels/config/backup/ha-config-backup-details.ts b/src/panels/config/backup/ha-config-backup-details.ts index 6c65925bb3..1d36c170c0 100644 --- a/src/panels/config/backup/ha-config-backup-details.ts +++ b/src/panels/config/backup/ha-config-backup-details.ts @@ -17,6 +17,7 @@ import "../../../components/ha-md-list-item"; import { getSignedPath } from "../../../data/auth"; import type { BackupContentExtended } from "../../../data/backup"; import { + compareAgents, computeBackupAgentName, deleteBackup, fetchBackupDetails, @@ -265,7 +266,10 @@ class HaConfigBackupDetails extends LitElement { private async _fetchBackup() { try { const response = await fetchBackupDetails(this.hass, this.backupId); - this._backup = response.backup; + this._backup = { + ...response.backup, + agent_ids: response.backup.agent_ids?.sort(compareAgents), + }; } catch (err: any) { this._error = err?.message || "Could not fetch backup details"; } diff --git a/src/panels/config/backup/ha-config-backup-strategy.ts b/src/panels/config/backup/ha-config-backup-strategy.ts index 459d7798a1..ca2dbf6c2d 100644 --- a/src/panels/config/backup/ha-config-backup-strategy.ts +++ b/src/panels/config/backup/ha-config-backup-strategy.ts @@ -12,6 +12,7 @@ import { fetchBackupConfig, updateBackupConfig, } from "../../../data/backup"; +import type { CloudStatus } from "../../../data/cloud"; import "../../../layouts/hass-subpage"; import type { HomeAssistant } from "../../../types"; import "./components/ha-backup-config-agents"; @@ -46,6 +47,8 @@ const INITIAL_BACKUP_CONFIG: BackupConfig = { class HaConfigBackupStrategy extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; + @property({ attribute: false }) public cloudStatus!: CloudStatus; + @property({ type: Boolean }) public narrow = false; @state() private _backupConfig: BackupConfig = INITIAL_BACKUP_CONFIG; @@ -111,6 +114,7 @@ class HaConfigBackupStrategy extends LitElement { diff --git a/src/panels/config/backup/ha-config-backup.ts b/src/panels/config/backup/ha-config-backup.ts index 62186b3791..fc7af23aeb 100644 --- a/src/panels/config/backup/ha-config-backup.ts +++ b/src/panels/config/backup/ha-config-backup.ts @@ -1,5 +1,6 @@ import type { PropertyValues } from "lit"; import { customElement, property } from "lit/decorators"; +import type { CloudStatus } from "../../../data/cloud"; import type { RouterOptions } from "../../../layouts/hass-router-page"; import { HassRouterPage } from "../../../layouts/hass-router-page"; import "../../../layouts/hass-tabs-subpage-data-table"; @@ -10,6 +11,8 @@ import "./ha-config-backup-dashboard"; class HaConfigBackup extends HassRouterPage { @property({ attribute: false }) public hass!: HomeAssistant; + @property({ attribute: false }) public cloudStatus!: CloudStatus; + @property({ type: Boolean }) public narrow = false; protected routerOptions: RouterOptions = { @@ -38,6 +41,7 @@ class HaConfigBackup extends HassRouterPage { pageEl.hass = this.hass; pageEl.route = this.routeTail; pageEl.narrow = this.narrow; + pageEl.cloudStatus = this.cloudStatus; if ( (!changedProps || changedProps.has("route")) &&