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`
+
+

+
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")) &&