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
This commit is contained in:
Paul Bottein 2024-12-16 14:08:51 +01:00 committed by GitHub
parent 875ab0cb97
commit d31f4a5f1d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 291 additions and 172 deletions

View File

@ -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";

View File

@ -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() {

View File

@ -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`
<ha-md-list>
${agentIds.map((agentId) => {
${this._agentIds.map((agentId) => {
const domain = computeDomain(agentId);
const name = computeBackupAgentName(
this.hass.localize,
agentId,
agentIds
this._agentIds
);
return html`
<ha-md-list-item>
@ -77,6 +82,14 @@ class HaBackupConfigAgents extends LitElement {
/>
`}
<div slot="headline">${name}</div>
${agentId === CLOUD_AGENT
? html`
<div slot="supporting-text">
It stores one backup. The oldest backups are
deleted.
</div>
`
: nothing}
<ha-switch
slot="end"
id=${agentId}
@ -104,9 +117,9 @@ class HaBackupConfigAgents extends LitElement {
}
// Ensure agents exist in the list
this.value = this.value.filter((agent) =>
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 });
}

View File

@ -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<Step>(["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 {
<span slot="title">${this._stepTitle}</span>
</ha-dialog-header>
<div slot="content">${this._renderStepContent()}</div>
<div slot="actions">
${isLastStep
? html`
<ha-button
@click=${this._done}
.disabled=${!this._isStepValid()}
>
Save
</ha-button>
`
: html`
<ha-button
@click=${this._nextStep}
.disabled=${!this._isStepValid()}
>
Next
</ha-button>
`}
</div>
${!FULL_DIALOG_STEPS.has(this._step)
? html`
<div slot="actions">
${isLastStep
? html`
<ha-button
@click=${this._done}
.disabled=${!this._isStepValid()}
>
Save
</ha-button>
`
: html`
<ha-button
@click=${this._nextStep}
.disabled=${!this._isStepValid()}
>
Next
</ha-button>
`}
</div>
`
: nothing}
</ha-md-dialog>
`;
}
@ -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 {
</p>
</div>
`;
case "new_key":
case "key":
return html`
<p>
All your backups are encrypted to keep your data private and secure.
You need this encryption key to restore any backup.
</p>
<ha-password-field
placeholder="New encryption key"
@input=${this._encryptionKeyChanged}
.value=${this._config.create_backup.password ?? ""}
></ha-password-field>
<ha-md-list>
<ha-md-list-item>
<ha-svg-icon slot="start" .path=${mdiKey}></ha-svg-icon>
<span slot="headline">Use suggested encryption key</span>
<span slot="supporting-text">
${this._suggestedEncryptionKey}
</span>
<ha-button slot="end" @click=${this._useSuggestedEncryptionKey}>
Enter
</ha-button>
</ha-md-list-item>
</ha-md-list>
`;
case "save_key":
return html`
<p>
Its important that you dont 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.
</p>
<div class="encryption-key">
<p>${this._config.create_backup.password}</p>
<ha-icon-button
.path=${mdiContentCopy}
@click=${this._copyKeyToClipboard}
></ha-icon-button>
</div>
<ha-md-list>
<ha-md-list-item>
<span slot="headline">Download emergency kit</span>
@ -313,6 +316,29 @@ class DialogSetBackupEncryptionKey extends LitElement implements HassDialog {
</ha-md-list-item>
</ha-md-list>
`;
case "setup":
return html`
<p>
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.
</p>
<ha-md-list class="full">
<ha-md-list-item type="button" @click=${this._done}>
<span slot="headline">Recommended settings</span>
<span slot="supporting-text">
Set the proven backup strategy of daily backup.
</span>
<ha-icon-next slot="end"> </ha-icon-next>
</ha-md-list-item>
<ha-md-list-item type="button" @click=${this._nextStep}>
<span slot="headline">Custom settings</span>
<span slot="supporting-text">
Select your own automation, data and locations
</span>
<ha-icon-next slot="end"> </ha-icon-next>
</ha-md-list-item>
</ha-md-list>
`;
case "schedule":
return html`
<p>
@ -347,6 +373,7 @@ class DialogSetBackupEncryptionKey extends LitElement implements HassDialog {
<ha-backup-config-agents
.hass=${this.hass}
.value=${this._config.create_backup.agent_ids}
.cloudStatus=${this._params!.cloudStatus}
@value-changed=${this._agentsConfigChanged}
></ha-backup-config-agents>
`;
@ -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;
}
}

View File

@ -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`
<ha-md-dialog disable-cancel-action open @closed=${this.closeDialog}>
@ -120,13 +121,12 @@ class DialogChangeBackupEncryptionKey extends LitElement implements HassDialog {
<ha-button
@click=${this._submit}
.disabled=${!this._newEncryptionKey}
class="danger"
>
Change encryption key
</ha-button>
`
: this._step === "save"
? html`<ha-button @click=${this._done}>Done</ha-button>`
: nothing}
: html`<ha-button @click=${this._done}>Done</ha-button>`}
</div>
</ha-md-dialog>
`;
@ -143,7 +143,7 @@ class DialogChangeBackupEncryptionKey extends LitElement implements HassDialog {
</p>
<ha-md-list>
<ha-md-list-item>
<span slot="headline">Download emergency kit</span>
<span slot="headline">Download old emergency kit</span>
<span slot="supporting-text">
We recommend to save this encryption key somewhere secure.
</span>
@ -155,36 +155,22 @@ class DialogChangeBackupEncryptionKey extends LitElement implements HassDialog {
</ha-md-list>
`;
case "new":
return html`
<p>All next backups will use the new encryption key.</p>
<ha-password-field
placeholder="New encryption key"
@input=${this._encryptionKeyChanged}
.value=${this._newEncryptionKey || ""}
></ha-password-field>
<ha-md-list>
<ha-md-list-item>
<ha-svg-icon slot="start" .path=${mdiKey}></ha-svg-icon>
<span slot="headline">Use suggested encryption key</span>
<span slot="supporting-text">
${this._suggestedEncryptionKey}
</span>
<ha-button slot="end" @click=${this._useSuggestedEncryptionKey}>
Enter
</ha-button>
</ha-md-list-item>
</ha-md-list>
`;
case "save":
return html`
<p>
Its important that you dont 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.
</p>
<div class="encryption-key">
<p>${this._newEncryptionKey}</p>
<ha-icon-button
.path=${mdiContentCopy}
@click=${this._copyKeyToClipboard}
></ha-icon-button>
</div>
<ha-md-list>
<ha-md-list-item>
<span slot="headline">Download emergency kit</span>
<span slot="headline">Download new emergency kit</span>
<span slot="supporting-text">
We recommend to save this encryption key somewhere secure.
</span>
@ -195,10 +181,27 @@ class DialogChangeBackupEncryptionKey extends LitElement implements HassDialog {
</ha-md-list-item>
</ha-md-list>
`;
case "done":
return html`
<div class="done">
<img
src="/static/images/voice-assistant/hi.png"
alt="Casita Home Assistant logo"
/>
<p>Encryption key changed</p>
</div>
`;
}
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;
}
`,
];
}

View File

@ -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");

View File

@ -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<BackupType> = ["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
</ha-button>
</ha-backup-summary-card>
`
@ -401,16 +405,21 @@ class HaConfigBackupDashboard extends SubscribeMixin(LitElement) {
</div>
</div> `
: nothing}
<ha-fab
slot="fab"
?disabled=${backupInProgress}
.label=${this.hass.localize("ui.panel.config.backup.create_backup")}
extended
@click=${this._newBackup}
>
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</ha-fab>
${!this._needsOnboarding
? html`
<ha-fab
slot="fab"
?disabled=${backupInProgress}
.label=${this.hass.localize(
"ui.panel.config.backup.create_backup"
)}
extended
@click=${this._newBackup}
>
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</ha-fab>
`
: nothing}
</hass-tabs-subpage-data-table>
`;
}
@ -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<void> {
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 {

View File

@ -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";
}

View File

@ -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 {
<ha-backup-config-agents
.hass=${this.hass}
.value=${this._backupConfig.create_backup.agent_ids}
.cloudStatus=${this.cloudStatus}
@value-changed=${this._agentsConfigChanged}
></ha-backup-config-agents>
</div>

View File

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