mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-24 09:46:36 +00:00
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:
parent
875ab0cb97
commit
d31f4a5f1d
@ -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";
|
||||
|
@ -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() {
|
||||
|
@ -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 });
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
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.
|
||||
</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;
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
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.
|
||||
</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;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@ -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");
|
||||
|
@ -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 {
|
||||
|
@ -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";
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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")) &&
|
||||
|
Loading…
x
Reference in New Issue
Block a user