mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-14 04:46:34 +00:00
Allow to change encryption for each backup location (#23861)
* Add backup location settings page * Add encryption settings * Display unencrypted locations and backups * Improve cloud detail page * Fix encryption flag * Fix restore backup * Fix lint * Fix translations * Add warning * Use agents in backup locations * Feedback * Use updated * Improve encrypted/unencrypted status * Improve code quality * Remove hardcoded failed id * Extend agent interface * Use willupdate
This commit is contained in:
parent
fcdcbbda05
commit
a2f2d64f5c
@ -57,6 +57,7 @@ export interface BackupConfig {
|
||||
time?: string | null;
|
||||
days: BackupDay[];
|
||||
};
|
||||
agents: BackupAgentsConfig;
|
||||
}
|
||||
|
||||
export interface BackupMutableConfig {
|
||||
@ -78,6 +79,13 @@ export interface BackupMutableConfig {
|
||||
time?: string | null;
|
||||
days?: BackupDay[] | null;
|
||||
};
|
||||
agents?: BackupAgentsConfig;
|
||||
}
|
||||
|
||||
export type BackupAgentsConfig = Record<string, BackupAgentConfig>;
|
||||
|
||||
export interface BackupAgentConfig {
|
||||
protected: boolean;
|
||||
}
|
||||
|
||||
export interface BackupAgent {
|
||||
@ -85,13 +93,16 @@ export interface BackupAgent {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface BackupContentAgent {
|
||||
size: number;
|
||||
protected: boolean;
|
||||
}
|
||||
|
||||
export interface BackupContent {
|
||||
backup_id: string;
|
||||
date: string;
|
||||
name: string;
|
||||
protected: boolean;
|
||||
size: number;
|
||||
agent_ids?: string[];
|
||||
agents: Record<string, BackupContentAgent>;
|
||||
failed_agent_ids?: string[];
|
||||
with_automatic_settings: boolean;
|
||||
}
|
||||
@ -305,6 +316,9 @@ export const computeBackupAgentName = (
|
||||
return showName ? `${domainName}: ${name}` : domainName;
|
||||
};
|
||||
|
||||
export const computeBackupSize = (backup: BackupContent) =>
|
||||
Math.max(...Object.values(backup.agents).map((agent) => agent.size));
|
||||
|
||||
export const compareAgents = (a: string, b: string) => {
|
||||
const isLocalA = isLocalAgent(a);
|
||||
const isLocalB = isLocalAgent(b);
|
||||
|
@ -1,14 +1,18 @@
|
||||
import { mdiHarddisk, mdiNas } from "@mdi/js";
|
||||
import { mdiCog, mdiHarddisk, mdiNas } from "@mdi/js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../../../../common/entity/compute_domain";
|
||||
import "../../../../../components/ha-icon-button";
|
||||
import "../../../../../components/ha-md-list";
|
||||
import "../../../../../components/ha-md-list-item";
|
||||
import "../../../../../components/ha-svg-icon";
|
||||
import "../../../../../components/ha-switch";
|
||||
import type { BackupAgent } from "../../../../../data/backup";
|
||||
import type {
|
||||
BackupAgent,
|
||||
BackupAgentsConfig,
|
||||
} from "../../../../../data/backup";
|
||||
import {
|
||||
CLOUD_AGENT,
|
||||
computeBackupAgentName,
|
||||
@ -18,6 +22,7 @@ import {
|
||||
import type { CloudStatus } from "../../../../../data/cloud";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import { brandsUrl } from "../../../../../util/brands-url";
|
||||
import { navigate } from "../../../../../common/navigate";
|
||||
|
||||
const DEFAULT_AGENTS = [];
|
||||
|
||||
@ -29,6 +34,11 @@ class HaBackupConfigAgents extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public agents: BackupAgent[] = [];
|
||||
|
||||
@property({ attribute: false }) public agentsConfig?: BackupAgentsConfig;
|
||||
|
||||
@property({ type: Boolean, attribute: "show-settings" }) public showSettings =
|
||||
false;
|
||||
|
||||
@state() private value?: string[];
|
||||
|
||||
private _availableAgents = memoizeOne(
|
||||
@ -53,6 +63,21 @@ class HaBackupConfigAgents extends LitElement {
|
||||
"ui.panel.config.backup.agents.cloud_agent_description"
|
||||
);
|
||||
}
|
||||
|
||||
const encryptionTurnedOff =
|
||||
this.agentsConfig?.[agentId]?.protected === false;
|
||||
|
||||
if (encryptionTurnedOff) {
|
||||
return html`
|
||||
<span class="dot warning"></span>
|
||||
<span>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.agents.encryption_turned_off"
|
||||
)}
|
||||
</span>
|
||||
`;
|
||||
}
|
||||
|
||||
if (isNetworkMountAgent(agentId)) {
|
||||
return this.hass.localize(
|
||||
"ui.panel.config.backup.agents.network_mount_agent_description"
|
||||
@ -80,6 +105,7 @@ class HaBackupConfigAgents extends LitElement {
|
||||
agentId === CLOUD_AGENT &&
|
||||
this.cloudStatus.logged_in &&
|
||||
!this.cloudStatus.active_subscription;
|
||||
|
||||
return html`
|
||||
<ha-md-list-item>
|
||||
${isLocalAgent(agentId)
|
||||
@ -112,6 +138,16 @@ class HaBackupConfigAgents extends LitElement {
|
||||
${description
|
||||
? html`<div slot="supporting-text">${description}</div>`
|
||||
: nothing}
|
||||
${this.showSettings
|
||||
? html`
|
||||
<ha-icon-button
|
||||
id=${agentId}
|
||||
slot="end"
|
||||
path=${mdiCog}
|
||||
@click=${this._showAgentSettings}
|
||||
></ha-icon-button>
|
||||
`
|
||||
: nothing}
|
||||
<ha-switch
|
||||
slot="end"
|
||||
id=${agentId}
|
||||
@ -133,6 +169,11 @@ class HaBackupConfigAgents extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _showAgentSettings(ev): void {
|
||||
const agentId = ev.currentTarget.id;
|
||||
navigate(`/config/backup/location/${agentId}`);
|
||||
}
|
||||
|
||||
private _agentToggled(ev) {
|
||||
ev.stopPropagation();
|
||||
const value = ev.currentTarget.checked;
|
||||
@ -180,6 +221,25 @@ class HaBackupConfigAgents extends LitElement {
|
||||
--mdc-icon-size: 48px;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
ha-md-list-item [slot="supporting-text"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
line-height: normal;
|
||||
}
|
||||
.dot {
|
||||
display: block;
|
||||
position: relative;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background-color: var(--disabled-color);
|
||||
border-radius: 50%;
|
||||
flex: none;
|
||||
}
|
||||
.dot.warning {
|
||||
background-color: var(--warning-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
@ -8,7 +8,10 @@ import "../../../../../components/ha-card";
|
||||
import "../../../../../components/ha-icon-next";
|
||||
import "../../../../../components/ha-md-list";
|
||||
import "../../../../../components/ha-md-list-item";
|
||||
import type { BackupContent } from "../../../../../data/backup";
|
||||
import {
|
||||
computeBackupSize,
|
||||
type BackupContent,
|
||||
} from "../../../../../data/backup";
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import { bytesToString } from "../../../../../util/bytes-to-string";
|
||||
@ -22,7 +25,7 @@ const computeBackupStats = (backups: BackupContent[]): BackupStats =>
|
||||
backups.reduce(
|
||||
(stats, backup) => {
|
||||
stats.count++;
|
||||
stats.size += backup.size;
|
||||
stats.size += computeBackupSize(backup);
|
||||
return stats;
|
||||
},
|
||||
{ count: 0, size: 0 }
|
||||
|
@ -179,7 +179,8 @@ class HaBackupOverviewBackups extends LitElement {
|
||||
now,
|
||||
true
|
||||
),
|
||||
count: lastUploadedBackup.agent_ids?.length ?? 0,
|
||||
count: Object.keys(lastUploadedBackup.agents)
|
||||
.length,
|
||||
}
|
||||
)
|
||||
: nextBackupDescription}
|
||||
@ -265,7 +266,8 @@ class HaBackupOverviewBackups extends LitElement {
|
||||
now,
|
||||
true
|
||||
),
|
||||
count: lastUploadedBackup.agent_ids?.length ?? 0,
|
||||
count: Object.keys(lastUploadedBackup.agents)
|
||||
.length,
|
||||
}
|
||||
)
|
||||
: nextBackupDescription}
|
||||
@ -286,7 +288,7 @@ class HaBackupOverviewBackups extends LitElement {
|
||||
now,
|
||||
true
|
||||
),
|
||||
count: lastBackup.agent_ids?.length ?? 0,
|
||||
count: Object.keys(lastBackup.agents).length,
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -72,6 +72,7 @@ const RECOMMENDED_CONFIG: BackupConfig = {
|
||||
time: null,
|
||||
days: [],
|
||||
},
|
||||
agents: {},
|
||||
last_attempted_automatic_backup: null,
|
||||
last_completed_automatic_backup: null,
|
||||
next_automatic_backup: null,
|
||||
|
@ -78,7 +78,12 @@ class DialogRestoreBackup extends LitElement implements HassDialog {
|
||||
this._error = undefined;
|
||||
this._state = undefined;
|
||||
this._stage = undefined;
|
||||
if (this._params.backup.protected) {
|
||||
|
||||
const agentIds = Object.keys(this._params.backup.agents);
|
||||
const preferedAgent = getPreferredAgentForDownload(agentIds);
|
||||
const isProtected = this._params.backup.agents[preferedAgent]?.protected;
|
||||
|
||||
if (isProtected) {
|
||||
this._backupEncryptionKey = await this._fetchEncryptionKey();
|
||||
if (!this._backupEncryptionKey) {
|
||||
this._step = STEPS[1];
|
||||
@ -322,9 +327,8 @@ class DialogRestoreBackup extends LitElement implements HassDialog {
|
||||
return;
|
||||
}
|
||||
|
||||
const preferedAgent = getPreferredAgentForDownload(
|
||||
this._params.backup.agent_ids!
|
||||
);
|
||||
const agentIds = Object.keys(this._params.backup.agents);
|
||||
const preferedAgent = getPreferredAgentForDownload(agentIds);
|
||||
|
||||
const { addons, database_included, homeassistant_included, folders } =
|
||||
this._params.selectedData;
|
||||
|
@ -39,7 +39,9 @@ import type {
|
||||
BackupContent,
|
||||
} from "../../../data/backup";
|
||||
import {
|
||||
compareAgents,
|
||||
computeBackupAgentName,
|
||||
computeBackupSize,
|
||||
deleteBackup,
|
||||
generateBackup,
|
||||
generateBackupWithAutomaticSettings,
|
||||
@ -68,6 +70,8 @@ import { downloadBackup } from "./helper/download_backup";
|
||||
|
||||
interface BackupRow extends DataTableRowData, BackupContent {
|
||||
formatted_type: string;
|
||||
size: number;
|
||||
agent_ids: string[];
|
||||
}
|
||||
|
||||
type BackupType = "automatic" | "manual";
|
||||
@ -291,6 +295,8 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
|
||||
const type = backup.with_automatic_settings ? "automatic" : "manual";
|
||||
return {
|
||||
...backup,
|
||||
size: computeBackupSize(backup),
|
||||
agent_ids: Object.keys(backup.agents).sort(compareAgents),
|
||||
formatted_type: localize(`ui.panel.config.backup.type.${type}`),
|
||||
};
|
||||
});
|
||||
|
@ -23,12 +23,14 @@ import "../../../components/ha-md-list-item";
|
||||
import type {
|
||||
BackupAgent,
|
||||
BackupConfig,
|
||||
BackupContentAgent,
|
||||
BackupContentExtended,
|
||||
BackupData,
|
||||
} from "../../../data/backup";
|
||||
import {
|
||||
compareAgents,
|
||||
computeBackupAgentName,
|
||||
computeBackupSize,
|
||||
deleteBackup,
|
||||
fetchBackupDetails,
|
||||
isLocalAgent,
|
||||
@ -45,21 +47,31 @@ import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import { downloadBackup } from "./helper/download_backup";
|
||||
|
||||
interface Agent {
|
||||
interface Agent extends BackupContentAgent {
|
||||
id: string;
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
const computeAgents = (agent_ids: string[], failed_agent_ids: string[]) =>
|
||||
[
|
||||
...agent_ids.filter((id) => !failed_agent_ids.includes(id)),
|
||||
...failed_agent_ids,
|
||||
const computeAgents = (backup: BackupContentExtended) => {
|
||||
const agentIds = Object.keys(backup.agents);
|
||||
const failedAgentIds = backup.failed_agent_ids ?? [];
|
||||
return [
|
||||
...agentIds.filter((id) => !failedAgentIds.includes(id)),
|
||||
...failedAgentIds,
|
||||
]
|
||||
.map<Agent>((id) => ({
|
||||
id,
|
||||
success: !failed_agent_ids.includes(id),
|
||||
}))
|
||||
.map<Agent>((id) => {
|
||||
const agent: BackupContentAgent = backup.agents[id] ?? {
|
||||
protected: false,
|
||||
size: 0,
|
||||
};
|
||||
return {
|
||||
...agent,
|
||||
id: id,
|
||||
success: !failedAgentIds.includes(id),
|
||||
};
|
||||
})
|
||||
.sort((a, b) => compareAgents(a.id, b.id));
|
||||
};
|
||||
|
||||
@customElement("ha-config-backup-details")
|
||||
class HaConfigBackupDetails extends LitElement {
|
||||
@ -156,7 +168,7 @@ class HaConfigBackupDetails extends LitElement {
|
||||
)}
|
||||
</span>
|
||||
<span slot="supporting-text">
|
||||
${bytesToString(this._backup.size)}
|
||||
${bytesToString(computeBackupSize(this._backup))}
|
||||
</span>
|
||||
</ha-md-list-item>
|
||||
<ha-md-list-item>
|
||||
@ -173,22 +185,6 @@ class HaConfigBackupDetails extends LitElement {
|
||||
)}
|
||||
</span>
|
||||
</ha-md-list-item>
|
||||
<ha-md-list-item>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.details.summary.protection"
|
||||
)}
|
||||
</span>
|
||||
<span slot="supporting-text">
|
||||
${this._backup.protected
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.backup.details.summary.protected_encrypted_aes_128"
|
||||
)
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.backup.details.summary.protected_not_encrypted"
|
||||
)}
|
||||
</span>
|
||||
</ha-md-list-item>
|
||||
</ha-md-list>
|
||||
</div>
|
||||
</ha-card>
|
||||
@ -230,87 +226,108 @@ class HaConfigBackupDetails extends LitElement {
|
||||
<ha-md-list>
|
||||
${this._agents.map((agent) => {
|
||||
const agentId = agent.id;
|
||||
const success = agent.success;
|
||||
|
||||
const domain = computeDomain(agentId);
|
||||
const name = computeBackupAgentName(
|
||||
this.hass.localize,
|
||||
agentId,
|
||||
this.agents
|
||||
);
|
||||
const success = agent.success;
|
||||
const failed = !agent.success;
|
||||
const unencrypted = !agent.protected;
|
||||
|
||||
return html`
|
||||
<ha-md-list-item>
|
||||
${isLocalAgent(agentId)
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
.path=${mdiHarddisk}
|
||||
slot="start"
|
||||
>
|
||||
</ha-svg-icon>
|
||||
`
|
||||
: isNetworkMountAgent(agentId)
|
||||
${
|
||||
isLocalAgent(agentId)
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
.path=${mdiNas}
|
||||
.path=${mdiHarddisk}
|
||||
slot="start"
|
||||
></ha-svg-icon>
|
||||
>
|
||||
</ha-svg-icon>
|
||||
`
|
||||
: html`
|
||||
<img
|
||||
.src=${brandsUrl({
|
||||
domain,
|
||||
type: "icon",
|
||||
useFallback: true,
|
||||
darkOptimized:
|
||||
this.hass.themes?.darkMode,
|
||||
})}
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
alt=""
|
||||
slot="start"
|
||||
/>
|
||||
`}
|
||||
: isNetworkMountAgent(agentId)
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
.path=${mdiNas}
|
||||
slot="start"
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: html`
|
||||
<img
|
||||
.src=${brandsUrl({
|
||||
domain,
|
||||
type: "icon",
|
||||
useFallback: true,
|
||||
darkOptimized:
|
||||
this.hass.themes?.darkMode,
|
||||
})}
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
alt=""
|
||||
slot="start"
|
||||
/>
|
||||
`
|
||||
}
|
||||
<div slot="headline">${name}</div>
|
||||
<div slot="supporting-text">
|
||||
<span
|
||||
class="dot ${success ? "success" : "error"}"
|
||||
>
|
||||
</span>
|
||||
<span>
|
||||
${success
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.backup.details.locations.backup_stored"
|
||||
)
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.backup.details.locations.backup_failed"
|
||||
)}
|
||||
</span>
|
||||
${
|
||||
failed
|
||||
? html`
|
||||
<div slot="supporting-text">
|
||||
<span class="dot error"></span>
|
||||
<span>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.details.locations.backup_failed"
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
`
|
||||
: unencrypted
|
||||
? html`
|
||||
<div slot="supporting-text">
|
||||
<span class="dot warning"></span>
|
||||
<span> Unencrypted </span>
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<div slot="supporting-text">
|
||||
<span class="dot success"></span>
|
||||
<span> Encrypted </span>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
</div>
|
||||
${success
|
||||
? html`<ha-button-menu
|
||||
slot="end"
|
||||
@action=${this._handleAgentAction}
|
||||
.agent=${agentId}
|
||||
fixed
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize(
|
||||
"ui.common.menu"
|
||||
)}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
<ha-list-item graphic="icon">
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${mdiDownload}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.details.locations.download"
|
||||
)}
|
||||
</ha-list-item>
|
||||
</ha-button-menu>`
|
||||
: nothing}
|
||||
${
|
||||
success
|
||||
? html`
|
||||
<ha-button-menu
|
||||
slot="end"
|
||||
@action=${this._handleAgentAction}
|
||||
.agent=${agentId}
|
||||
fixed
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize(
|
||||
"ui.common.menu"
|
||||
)}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
<ha-list-item graphic="icon">
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${mdiDownload}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.details.locations.download"
|
||||
)}
|
||||
</ha-list-item>
|
||||
</ha-button-menu>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</ha-md-list-item>
|
||||
`;
|
||||
})}
|
||||
@ -354,10 +371,7 @@ class HaConfigBackupDetails extends LitElement {
|
||||
try {
|
||||
const response = await fetchBackupDetails(this.hass, this.backupId);
|
||||
this._backup = response.backup;
|
||||
this._agents = computeAgents(
|
||||
response.backup.agent_ids || [],
|
||||
response.backup.failed_agent_ids || []
|
||||
);
|
||||
this._agents = computeAgents(response.backup);
|
||||
} catch (err: any) {
|
||||
this._error =
|
||||
err?.message ||
|
||||
@ -479,6 +493,9 @@ class HaConfigBackupDetails extends LitElement {
|
||||
.dot.success {
|
||||
background-color: var(--success-color);
|
||||
}
|
||||
.dot.warning {
|
||||
background-color: var(--warning-color);
|
||||
}
|
||||
.dot.error {
|
||||
background-color: var(--error-color);
|
||||
}
|
||||
|
369
src/panels/config/backup/ha-config-backup-location.ts
Normal file
369
src/panels/config/backup/ha-config-backup-location.ts
Normal file
@ -0,0 +1,369 @@
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-switch";
|
||||
import "../../../components/ha-button-menu";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-circular-progress";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-list-item";
|
||||
import "../../../components/ha-md-list";
|
||||
import "../../../components/ha-md-list-item";
|
||||
import type {
|
||||
BackupAgent,
|
||||
BackupAgentConfig,
|
||||
BackupConfig,
|
||||
} from "../../../data/backup";
|
||||
import {
|
||||
CLOUD_AGENT,
|
||||
computeBackupAgentName,
|
||||
fetchBackupAgentsInfo,
|
||||
updateBackupConfig,
|
||||
} from "../../../data/backup";
|
||||
import "../../../layouts/hass-subpage";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import "./components/ha-backup-data-picker";
|
||||
import { showConfirmationDialog } from "../../lovelace/custom-card-helpers";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
|
||||
@customElement("ha-config-backup-location")
|
||||
class HaConfigBackupDetails extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ attribute: "agent-id" }) public agentId!: string;
|
||||
|
||||
@property({ attribute: false }) public config?: BackupConfig;
|
||||
|
||||
@property({ attribute: false }) public agents: BackupAgent[] = [];
|
||||
|
||||
@state() private _agent?: BackupAgent | null;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues): void {
|
||||
if (changedProps.has("agentId")) {
|
||||
if (this.agentId) {
|
||||
this._fetchAgent();
|
||||
} else {
|
||||
this._error = "Agent id not defined";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.hass) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const encrypted = this._isEncryptionTurnedOn();
|
||||
|
||||
return html`
|
||||
<hass-subpage
|
||||
back-path="/config/backup/settings"
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.header=${(this._agent &&
|
||||
computeBackupAgentName(
|
||||
this.hass.localize,
|
||||
this.agentId,
|
||||
this.agents
|
||||
)) ||
|
||||
this.hass.localize("ui.panel.config.backup.location.header")}
|
||||
>
|
||||
<div class="content">
|
||||
${this._error &&
|
||||
html`<ha-alert alert-type="error">${this._error}</ha-alert>`}
|
||||
${this._agent === null
|
||||
? html`
|
||||
<ha-alert
|
||||
alert-type="warning"
|
||||
.title=${this.hass.localize(
|
||||
"ui.panel.config.backup.location.not_found"
|
||||
)}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.location.not_found_description",
|
||||
{ agentId: this.agentId }
|
||||
)}
|
||||
</ha-alert>
|
||||
`
|
||||
: !this.agentId
|
||||
? html`<ha-circular-progress active></ha-circular-progress>`
|
||||
: html`
|
||||
${CLOUD_AGENT === this.agentId
|
||||
? html`
|
||||
<ha-card>
|
||||
<div class="card-header">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.location.configuration.title"
|
||||
)}
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.location.configuration.cloud_description"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</ha-card>
|
||||
`
|
||||
: nothing}
|
||||
<ha-card>
|
||||
<div class="card-header">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.location.encryption.title"
|
||||
)}
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.location.encryption.description"
|
||||
)}
|
||||
</p>
|
||||
<ha-md-list>
|
||||
${CLOUD_AGENT === this.agentId
|
||||
? html`
|
||||
<ha-md-list-item>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.location.encryption.location_encrypted"
|
||||
)}
|
||||
</span>
|
||||
<span slot="supporting-text">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.location.encryption.location_encrypted_cloud_description"
|
||||
)}
|
||||
</span>
|
||||
<a
|
||||
href="https://www.nabucasa.com/config/backups/"
|
||||
target="_blank"
|
||||
slot="end"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
<ha-button>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.location.encryption.location_encrypted_cloud_learn_more"
|
||||
)}
|
||||
</ha-button>
|
||||
</a>
|
||||
</ha-md-list-item>
|
||||
`
|
||||
: encrypted
|
||||
? html`
|
||||
<ha-md-list-item>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.location.encryption.location_encrypted"
|
||||
)}
|
||||
</span>
|
||||
<span slot="supporting-text">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.backup.location.encryption.location_encrypted_description`
|
||||
)}
|
||||
</span>
|
||||
|
||||
<ha-button
|
||||
slot="end"
|
||||
@click=${this._turnOffEncryption}
|
||||
destructive
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.location.encryption.encryption_turn_off"
|
||||
)}
|
||||
</ha-button>
|
||||
</ha-md-list-item>
|
||||
`
|
||||
: html`
|
||||
<ha-alert
|
||||
alert-type="warning"
|
||||
.title=${this.hass.localize(
|
||||
"ui.panel.config.backup.location.encryption.warning_encryption_turn_off"
|
||||
)}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.location.encryption.warning_encryption_turn_off_description"
|
||||
)}
|
||||
</ha-alert>
|
||||
<ha-md-list-item>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.location.encryption.location_unencrypted"
|
||||
)}
|
||||
</span>
|
||||
<span slot="supporting-text">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.backup.location.encryption.location_unencrypted_description`
|
||||
)}
|
||||
</span>
|
||||
|
||||
<ha-button
|
||||
slot="end"
|
||||
@click=${this._turnOnEncryption}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.location.encryption.encryption_turn_on"
|
||||
)}
|
||||
</ha-button>
|
||||
</ha-md-list-item>
|
||||
`}
|
||||
</ha-md-list>
|
||||
</div>
|
||||
</ha-card>
|
||||
`}
|
||||
</div>
|
||||
</hass-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
private _isEncryptionTurnedOn() {
|
||||
const agentConfig = this.config?.agents[this.agentId] as
|
||||
| BackupAgentConfig
|
||||
| undefined;
|
||||
|
||||
if (!agentConfig) {
|
||||
return true;
|
||||
}
|
||||
return agentConfig.protected;
|
||||
}
|
||||
|
||||
private async _fetchAgent() {
|
||||
try {
|
||||
// Todo fetch agent details
|
||||
const { agents } = await fetchBackupAgentsInfo(this.hass);
|
||||
const agent = agents.find((a) => a.agent_id === this.agentId);
|
||||
if (!agent) {
|
||||
throw new Error("Agent not found");
|
||||
}
|
||||
this._agent = agent;
|
||||
} catch (err: any) {
|
||||
this._error =
|
||||
err?.message ||
|
||||
this.hass.localize("ui.panel.config.backup.details.error");
|
||||
}
|
||||
}
|
||||
|
||||
private async _updateAgentEncryption(value: boolean) {
|
||||
const agentsConfig = {
|
||||
...this.config?.agents,
|
||||
[this.agentId]: {
|
||||
...this.config?.agents[this.agentId],
|
||||
protected: value,
|
||||
},
|
||||
};
|
||||
await updateBackupConfig(this.hass, {
|
||||
agents: agentsConfig,
|
||||
});
|
||||
fireEvent(this, "ha-refresh-backup-config");
|
||||
}
|
||||
|
||||
private _turnOnEncryption() {
|
||||
this._updateAgentEncryption(true);
|
||||
}
|
||||
|
||||
private async _turnOffEncryption() {
|
||||
const response = await showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.backup.location.encryption.encryption_turn_off_confirm_title"
|
||||
),
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.backup.location.encryption.encryption_turn_off_confirm_text"
|
||||
),
|
||||
confirmText: this.hass.localize(
|
||||
"ui.panel.config.backup.location.encryption.encryption_turn_off_confirm_action"
|
||||
),
|
||||
dismissText: this.hass.localize("ui.common.cancel"),
|
||||
destructive: true,
|
||||
});
|
||||
if (response) {
|
||||
this._updateAgentEncryption(false);
|
||||
}
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.content {
|
||||
padding: 28px 20px 0;
|
||||
max-width: 690px;
|
||||
margin: 0 auto;
|
||||
gap: 24px;
|
||||
display: grid;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.card-content {
|
||||
padding: 0 20px;
|
||||
}
|
||||
.card-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
ha-md-list {
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
ha-md-list-item {
|
||||
--md-list-item-leading-space: 0;
|
||||
--md-list-item-trailing-space: 0;
|
||||
--md-list-item-two-line-container-height: 64px;
|
||||
}
|
||||
ha-md-list-item img {
|
||||
width: 48px;
|
||||
}
|
||||
ha-md-list-item ha-svg-icon[slot="start"] {
|
||||
--mdc-icon-size: 48px;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
ha-md-list.summary ha-md-list-item {
|
||||
--md-list-item-supporting-text-size: 1rem;
|
||||
--md-list-item-label-text-size: 0.875rem;
|
||||
|
||||
--md-list-item-label-text-color: var(--secondary-text-color);
|
||||
--md-list-item-supporting-text-color: var(--primary-text-color);
|
||||
}
|
||||
.warning {
|
||||
color: var(--error-color);
|
||||
}
|
||||
.warning ha-svg-icon {
|
||||
color: var(--error-color);
|
||||
}
|
||||
ha-button.danger {
|
||||
--mdc-theme-primary: var(--error-color);
|
||||
}
|
||||
ha-backup-data-picker {
|
||||
display: block;
|
||||
}
|
||||
ha-md-list-item [slot="supporting-text"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
line-height: normal;
|
||||
}
|
||||
.dot {
|
||||
display: block;
|
||||
position: relative;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background-color: var(--disabled-color);
|
||||
border-radius: 50%;
|
||||
flex: none;
|
||||
}
|
||||
.dot.success {
|
||||
background-color: var(--success-color);
|
||||
}
|
||||
.dot.error {
|
||||
background-color: var(--error-color);
|
||||
}
|
||||
.card-header {
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-config-backup-location": HaConfigBackupDetails;
|
||||
}
|
||||
}
|
@ -179,9 +179,11 @@ class HaConfigBackupSettings extends LitElement {
|
||||
<ha-backup-config-agents
|
||||
.hass=${this.hass}
|
||||
.value=${this._config.create_backup.agent_ids}
|
||||
.agentsConfig=${this._config.agents}
|
||||
.cloudStatus=${this.cloudStatus}
|
||||
.agents=${this.agents}
|
||||
@value-changed=${this._agentsConfigChanged}
|
||||
show-settings
|
||||
></ha-backup-config-agents>
|
||||
${!this._config.create_backup.agent_ids.length
|
||||
? html`
|
||||
|
@ -88,11 +88,7 @@ class HaConfigBackup extends SubscribeMixin(HassRouterPage) {
|
||||
|
||||
private async _fetchBackupInfo() {
|
||||
const info = await fetchBackupInfo(this.hass);
|
||||
this._backups = info.backups.map((backup) => ({
|
||||
...backup,
|
||||
agent_ids: backup.agent_ids?.sort(compareAgents),
|
||||
failed_agent_ids: backup.failed_agent_ids?.sort(compareAgents),
|
||||
}));
|
||||
this._backups = info.backups;
|
||||
}
|
||||
|
||||
private async _fetchBackupConfig() {
|
||||
@ -124,6 +120,10 @@ class HaConfigBackup extends SubscribeMixin(HassRouterPage) {
|
||||
tag: "ha-config-backup-settings",
|
||||
load: () => import("./ha-config-backup-settings"),
|
||||
},
|
||||
location: {
|
||||
tag: "ha-config-backup-location",
|
||||
load: () => import("./ha-config-backup-location"),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@ -138,11 +138,15 @@ class HaConfigBackup extends SubscribeMixin(HassRouterPage) {
|
||||
pageEl.agents = this._agents;
|
||||
pageEl.fetching = this._fetching;
|
||||
|
||||
if (
|
||||
(!changedProps || changedProps.has("route")) &&
|
||||
this._currentPage === "details"
|
||||
) {
|
||||
pageEl.backupId = this.routeTail.path.substr(1);
|
||||
if (!changedProps || changedProps.has("route")) {
|
||||
switch (this._currentPage) {
|
||||
case "details":
|
||||
pageEl.backupId = this.routeTail.path.substr(1);
|
||||
break;
|
||||
case "location":
|
||||
pageEl.agentId = this.routeTail.path.substr(1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -42,11 +42,10 @@ const downloadEncryptedBackup = async (
|
||||
confirmText: "Download encrypted",
|
||||
})
|
||||
) {
|
||||
triggerDownload(
|
||||
hass,
|
||||
backup.backup_id,
|
||||
agentId ?? getPreferredAgentForDownload(backup.agent_ids!)
|
||||
);
|
||||
const agentIds = Object.keys(backup.agents);
|
||||
const preferedAgent = agentId ?? getPreferredAgentForDownload(agentIds);
|
||||
|
||||
triggerDownload(hass, backup.backup_id, preferedAgent);
|
||||
}
|
||||
};
|
||||
|
||||
@ -83,10 +82,11 @@ export const downloadBackup = async (
|
||||
agentId?: string,
|
||||
userProvided = false
|
||||
): Promise<void> => {
|
||||
const preferedAgent =
|
||||
agentId ?? getPreferredAgentForDownload(backup.agent_ids!);
|
||||
const agentIds = Object.keys(backup.agents);
|
||||
const preferedAgent = agentId ?? getPreferredAgentForDownload(agentIds);
|
||||
const isProtected = backup.agents[preferedAgent]?.protected;
|
||||
|
||||
if (backup.protected) {
|
||||
if (isProtected) {
|
||||
if (encryptionKey) {
|
||||
try {
|
||||
await canDecryptBackupOnDownload(
|
||||
|
@ -2404,6 +2404,7 @@
|
||||
"cloud_agent_no_subcription": "You currently do not have an active Home Assistant Cloud subscription.",
|
||||
"network_mount_agent_description": "Network storage",
|
||||
"no_agents": "No locations configured",
|
||||
"encryption_turned_off": "Encryption turned off",
|
||||
"local_agent": "This system"
|
||||
},
|
||||
"data": {
|
||||
@ -2650,10 +2651,7 @@
|
||||
"summary": {
|
||||
"title": "Backup",
|
||||
"size": "Size",
|
||||
"created": "Created",
|
||||
"protection": "Protection",
|
||||
"protected_encrypted_aes_128": "Encrypted AES-128",
|
||||
"protected_not_encrypted": "Not encrypted"
|
||||
"created": "Created"
|
||||
},
|
||||
"restore": {
|
||||
"title": "Select what to restore",
|
||||
@ -2661,10 +2659,37 @@
|
||||
},
|
||||
"locations": {
|
||||
"title": "Locations",
|
||||
"backup_stored": "Backup stored",
|
||||
"backup_failed": "Backup failed",
|
||||
"encryption_turned_off": "Encryption turned off",
|
||||
"download": "Download from this location"
|
||||
}
|
||||
},
|
||||
"location": {
|
||||
"header": "Location",
|
||||
"not_found": "Not found",
|
||||
"not_found_description": "Location matching ''{backupId}'' not found",
|
||||
"error": "Could not fetch location details",
|
||||
"configuration": {
|
||||
"title": "Configuration",
|
||||
"cloud_description": "Home Assistant Cloud backup stores only one backup. The oldest backups are deleted."
|
||||
},
|
||||
"encryption": {
|
||||
"title": "Encryption",
|
||||
"description": "All your backups are encrypted by default to keep your data private and secure.",
|
||||
"location_encrypted": "This location is encrypted",
|
||||
"location_unencrypted": "This location is unencrypted",
|
||||
"location_encrypted_description": "Your data private and secure by securing it with your encryption key.",
|
||||
"location_encrypted_cloud_description": "Home Assistant Cloud is the privacy-focused cloud. This is why it will only accept encrypted backups and why we don’t store your encryption key.",
|
||||
"location_encrypted_cloud_learn_more": "Learn more",
|
||||
"location_unencrypted_description": "Please keep your backups private and secure.",
|
||||
"encryption_turn_on": "Turn on",
|
||||
"encryption_turn_off": "Turn off",
|
||||
"encryption_turn_off_confirm_title": "Turn encryption off?",
|
||||
"encryption_turn_off_confirm_text": "All your next backups will not be encrypted for this location. Please keep your backups private and secure.",
|
||||
"encryption_turn_off_confirm_action": "Turn encryption off",
|
||||
"warning_encryption_turn_off": "Encryption turned off",
|
||||
"warning_encryption_turn_off_description": "All your next backups will not be encrypted."
|
||||
}
|
||||
}
|
||||
},
|
||||
"tag": {
|
||||
|
Loading…
x
Reference in New Issue
Block a user