diff --git a/src/panels/config/backup/components/ha-backup-agents-picker.ts b/src/panels/config/backup/components/ha-backup-agents-picker.ts index 6a5fb7ca63f4..acf486dd6da5 100644 --- a/src/panels/config/backup/components/ha-backup-agents-picker.ts +++ b/src/panels/config/backup/components/ha-backup-agents-picker.ts @@ -1,18 +1,13 @@ import { mdiHarddisk } from "@mdi/js"; import { css, html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; -import memoizeOne from "memoize-one"; +import { classMap } from "lit/directives/class-map"; import { fireEvent } from "../../../../common/dom/fire_event"; import { computeDomain } from "../../../../common/entity/compute_domain"; import "../../../../components/ha-checkbox"; import "../../../../components/ha-formfield"; import "../../../../components/ha-svg-icon"; -import { - compareAgents, - computeBackupAgentName, - isLocalAgent, - type BackupAgent, -} from "../../../../data/backup"; +import { computeBackupAgentName, isLocalAgent } from "../../../../data/backup"; import type { HomeAssistant } from "../../../../types"; import { brandsUrl } from "../../../../util/brands-url"; @@ -25,22 +20,18 @@ class HaBackupAgentsPicker extends LitElement { public disabled = false; @property({ attribute: false }) - public agents!: BackupAgent[]; + public agentIds!: string[]; @property({ attribute: false }) - public disabledAgents?: string[]; + public disabledAgentIds?: string[]; @property({ attribute: false }) public value!: string[]; - private _agentIds = memoizeOne((agents: BackupAgent[]) => - agents.map((agent) => agent.agent_id).sort(compareAgents) - ); - render() { return html`
- ${this._agentIds(this.agents).map((agent) => this._renderAgent(agent))} + ${this.agentIds.map((agent) => this._renderAgent(agent))}
`; } @@ -50,15 +41,15 @@ class HaBackupAgentsPicker extends LitElement { const name = computeBackupAgentName( this.hass.localize, agentId, - this._agentIds(this.agents) + this.agentIds ); const disabled = - this.disabled || this.disabledAgents?.includes(agentId) || false; + this.disabled || this.disabledAgentIds?.includes(agentId) || false; return html` - + ${isLocalAgent(agentId) ? html` @@ -127,6 +118,12 @@ class HaBackupAgentsPicker extends LitElement { line-height: 24px; letter-spacing: 0.5px; } + span.disabled { + color: var(--disabled-text-color); + } + span.disabled ha-svg-icon { + color: var(--disabled-text-color); + } `; } diff --git a/src/panels/config/backup/dialogs/dialog-generate-backup.ts b/src/panels/config/backup/dialogs/dialog-generate-backup.ts index 34b8d2b4d8a6..eb5d53407e73 100644 --- a/src/panels/config/backup/dialogs/dialog-generate-backup.ts +++ b/src/panels/config/backup/dialogs/dialog-generate-backup.ts @@ -1,9 +1,10 @@ import { mdiClose } from "@mdi/js"; -import type { CSSResultGroup } from "lit"; +import type { CSSResultGroup, PropertyValues } 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 "../../../../components/ha-alert"; import "../../../../components/ha-button"; import "../../../../components/ha-dialog-header"; import "../../../../components/ha-expansion-panel"; @@ -17,20 +18,21 @@ import "../../../../components/ha-md-select"; import "../../../../components/ha-md-select-option"; import "../../../../components/ha-textfield"; import type { - BackupAgent, BackupConfig, GenerateBackupParams, } from "../../../../data/backup"; import { + CLOUD_AGENT, + compareAgents, fetchBackupAgentsInfo, fetchBackupConfig, } from "../../../../data/backup"; import type { HassDialog } from "../../../../dialogs/make-dialog-manager"; import { haStyle, haStyleDialog } from "../../../../resources/styles"; import type { HomeAssistant } from "../../../../types"; -import "../components/ha-backup-agents-picker"; import "../components/config/ha-backup-config-data"; import type { BackupConfigData } from "../components/config/ha-backup-config-data"; +import "../components/ha-backup-agents-picker"; import type { GenerateBackupDialogParams } from "./show-dialog-generate-backup"; type FormData = { @@ -54,13 +56,15 @@ const INITIAL_DATA: FormData = { const STEPS = ["data", "sync"] as const; +const DISALLOWED_AGENTS_NO_HA = [CLOUD_AGENT]; + @customElement("ha-dialog-generate-backup") class DialogGenerateBackup extends LitElement implements HassDialog { @property({ attribute: false }) public hass!: HomeAssistant; @state() private _step?: "data" | "sync"; - @state() private _agents: BackupAgent[] = []; + @state() private _agentIds: string[] = []; @state() private _backupConfig?: BackupConfig; @@ -74,6 +78,7 @@ class DialogGenerateBackup extends LitElement implements HassDialog { this._step = STEPS[0]; this._formData = INITIAL_DATA; this._params = _params; + this._fetchAgents(); this._fetchBackupConfig(); } @@ -84,7 +89,7 @@ class DialogGenerateBackup extends LitElement implements HassDialog { } this._step = undefined; this._formData = undefined; - this._agents = []; + this._agentIds = []; this._backupConfig = undefined; this._params = undefined; fireEvent(this, "dialog-closed", { dialog: this.localName }); @@ -92,7 +97,10 @@ class DialogGenerateBackup extends LitElement implements HassDialog { 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._params?.cloudStatus.logged_in) + .sort(compareAgents); } private async _fetchBackupConfig() { @@ -120,6 +128,32 @@ class DialogGenerateBackup extends LitElement implements HassDialog { this._step = STEPS[index + 1]; } + protected willUpdate(changedProperties: PropertyValues): void { + super.willUpdate(changedProperties); + + if (changedProperties.has("_step")) { + if (this._step === "sync" && this._formData) { + const disallowedAgents = this._disabledAgentIds(); + if (disallowedAgents.length) { + // Remove disallowed agents from the list + const agentsIds = + this._formData.agents_mode === "all" + ? this._agentIds + : this._formData.agent_ids; + + const filteredAgents = agentsIds.filter( + (agentId) => !disallowedAgents.includes(agentId) + ); + this._formData = { + ...this._formData, + agents_mode: "custom", + agent_ids: filteredAgents, + }; + } + } + } + } + protected render() { if (!this._step || !this._formData) { return nothing; @@ -131,6 +165,8 @@ class DialogGenerateBackup extends LitElement implements HassDialog { const isFirstStep = this._step === STEPS[0]; const isLastStep = this._step === STEPS[STEPS.length - 1]; + const selectedAgents = this._formData.agent_ids; + return html` @@ -159,7 +195,14 @@ class DialogGenerateBackup extends LitElement implements HassDialog { ? html`Cancel` : nothing} ${isLastStep - ? html`Create backup` + ? html` + + Create backup + + ` : html`Next`} @@ -194,6 +237,8 @@ class DialogGenerateBackup extends LitElement implements HassDialog { return nothing; } + const disabledAgentIds = this._disabledAgentIds(); + return html` - -
All (${this._agents.length})
+ +
All (${this._agentIds.length})
Custom
@@ -223,6 +271,17 @@ class DialogGenerateBackup extends LitElement implements HassDialog { + ${disabledAgentIds.length + ? html` + + Add Home Assistant settings data to synchronize this backup to + Home Assistant Cloud. + + ` + : nothing} ${this._formData.agents_mode === "custom" ? html` @@ -230,7 +289,8 @@ class DialogGenerateBackup extends LitElement implements HassDialog { .hass=${this.hass} .value=${this._formData.agent_ids} @value-changed=${this._agentsChanged} - .agents=${this._agents} + .agentIds=${this._agentIds} + .disabledAgentIds=${disabledAgentIds} > ` @@ -260,6 +320,16 @@ class DialogGenerateBackup extends LitElement implements HassDialog { }; } + private _disabledAgentIds() { + if (!this._formData) { + return []; + } + const allAgents = this._agentIds; + return !this._formData.data.include_homeassistant + ? DISALLOWED_AGENTS_NO_HA.filter((agentId) => allAgents.includes(agentId)) + : []; + } + private async _submit() { if (!this._formData) { return; @@ -269,12 +339,10 @@ class DialogGenerateBackup extends LitElement implements HassDialog { const password = this._backupConfig?.create_backup.password || undefined; - const ALL_AGENT_IDS = this._agents.map((agent) => agent.agent_id); - const params: GenerateBackupParams = { name, password, - agent_ids: agents_mode === "all" ? ALL_AGENT_IDS : agent_ids, + agent_ids: agents_mode === "all" ? this._agentIds : agent_ids, // We always include homeassistant if we include database include_homeassistant: data.include_homeassistant || data.include_database, @@ -287,6 +355,13 @@ class DialogGenerateBackup extends LitElement implements HassDialog { params.include_addons = data.include_addons; } + // Ensure we don't upload to disallowed agents if we are not including homeassistant + if (!params.include_homeassistant) { + params.agent_ids = params.agent_ids.filter( + (agentId) => !DISALLOWED_AGENTS_NO_HA.includes(agentId) + ); + } + this._params!.submit?.(params); this.closeDialog(); } @@ -333,6 +408,10 @@ class DialogGenerateBackup extends LitElement implements HassDialog { .content { padding-top: 0; } + ha-alert { + margin-bottom: 16px; + display: block; + } `, ]; } diff --git a/src/panels/config/backup/dialogs/dialog-upload-backup.ts b/src/panels/config/backup/dialogs/dialog-upload-backup.ts index 53809752e5bd..a9b3e9262ce9 100644 --- a/src/panels/config/backup/dialogs/dialog-upload-backup.ts +++ b/src/panels/config/backup/dialogs/dialog-upload-backup.ts @@ -2,7 +2,7 @@ import { mdiClose, mdiFolderUpload } from "@mdi/js"; import type { CSSResultGroup } from "lit"; import { css, html, LitElement, nothing } from "lit"; import { customElement, property, query, state } from "lit/decorators"; -import { keyed } from "lit/directives/keyed"; +import { isComponentLoaded } from "../../../../common/config/is_component_loaded"; import { fireEvent } from "../../../../common/dom/fire_event"; import "../../../../components/ha-alert"; import "../../../../components/ha-dialog-header"; @@ -11,12 +11,11 @@ import "../../../../components/ha-file-upload"; import "../../../../components/ha-icon-button"; import "../../../../components/ha-md-dialog"; import type { HaMdDialog } from "../../../../components/ha-md-dialog"; -import "../../../../components/ha-md-list"; -import "../../../../components/ha-md-list-item"; -import "../../../../components/ha-md-select"; -import "../../../../components/ha-md-select-option"; -import type { BackupAgent } from "../../../../data/backup"; -import { fetchBackupAgentsInfo, uploadBackup } from "../../../../data/backup"; +import { + CORE_LOCAL_AGENT, + HASSIO_LOCAL_AGENT, + uploadBackup, +} from "../../../../data/backup"; import type { HassDialog } from "../../../../dialogs/make-dialog-manager"; import { haStyle, haStyleDialog } from "../../../../resources/styles"; import type { HomeAssistant } from "../../../../types"; @@ -27,14 +26,10 @@ import type { UploadBackupDialogParams } from "./show-dialog-upload-backup"; const SUPPORTED_FORMAT = "application/x-tar"; type FormData = { - agents_mode: "all" | "custom"; - agent_ids: string[]; file?: File; }; const INITIAL_DATA: FormData = { - agents_mode: "all", - agent_ids: [], file: undefined, }; @@ -51,8 +46,6 @@ export class DialogUploadBackup @state() private _error?: string; - @state() private _agents: BackupAgent[] = []; - @state() private _formData?: FormData; @query("ha-md-dialog") private _dialog?: HaMdDialog; @@ -60,7 +53,6 @@ export class DialogUploadBackup public async showDialog(params: UploadBackupDialogParams): Promise { this._params = params; this._formData = INITIAL_DATA; - this._fetchAgents(); } private _dialogClosed() { @@ -68,7 +60,6 @@ export class DialogUploadBackup this._params!.cancel(); } this._formData = undefined; - this._agents = []; this._params = undefined; fireEvent(this, "dialog-closed", { dialog: this.localName }); } @@ -77,17 +68,8 @@ export class DialogUploadBackup this._dialog?.close(); } - private async _fetchAgents() { - const { agents } = await fetchBackupAgentsInfo(this.hass); - this._agents = agents; - } - private _formValid() { - return ( - this._formData?.file !== undefined && - (this._formData.agents_mode === "all" || - this._formData.agent_ids.length > 0) - ); + return this._formData?.file !== undefined; } protected render() { @@ -117,44 +99,6 @@ export class DialogUploadBackup supports="Supports .tar files" @file-picked=${this._filePicked} > - - - Locations - - What locations you want to upload this backup. - - ${keyed( - this._agents.length, - html` - - -
All (${this._agents.length})
-
- -
Custom
-
-
- ` - )} -
-
- ${this._formData.agents_mode === "custom" - ? html` - - - - ` - : nothing} ${this._error ? html`${this._error}` : nothing} @@ -169,21 +113,6 @@ export class DialogUploadBackup `; } - private _selectChanged(ev) { - const select = ev.currentTarget; - this._formData = { - ...this._formData!, - [select.id]: select.value, - }; - } - - private _agentsChanged(ev) { - this._formData = { - ...this._formData!, - agent_ids: ev.detail.value, - }; - } - private async _filePicked(ev: CustomEvent<{ files: File[] }>): Promise { this._error = undefined; const file = ev.detail.files[0]; @@ -195,7 +124,7 @@ export class DialogUploadBackup } private async _upload() { - const { file, agent_ids, agents_mode } = this._formData!; + const { file } = this._formData!; if (!file || file.type !== SUPPORTED_FORMAT) { showAlertDialog(this, { title: "Unsupported file format", @@ -205,14 +134,13 @@ export class DialogUploadBackup return; } - const agents = - agents_mode === "all" - ? this._agents.map((agent) => agent.agent_id) - : agent_ids; + const agentIds = isComponentLoaded(this.hass!, "hassio") + ? [HASSIO_LOCAL_AGENT] + : [CORE_LOCAL_AGENT]; this._uploading = true; try { - await uploadBackup(this.hass!, file, agents); + await uploadBackup(this.hass!, file, agentIds); this._params!.submit?.(); this.closeDialog(); } catch (err: any) { @@ -233,20 +161,6 @@ export class DialogUploadBackup max-width: 500px; max-height: 100%; } - ha-md-list { - background: none; - --md-list-item-leading-space: 0; - --md-list-item-trailing-space: 0; - } - ha-md-select { - min-width: 210px; - } - @media all and (max-width: 450px) { - ha-md-select { - min-width: 160px; - width: 160px; - } - } `, ]; } diff --git a/src/panels/config/backup/dialogs/show-dialog-generate-backup.ts b/src/panels/config/backup/dialogs/show-dialog-generate-backup.ts index c764290fa434..ccd57deb44c6 100644 --- a/src/panels/config/backup/dialogs/show-dialog-generate-backup.ts +++ b/src/panels/config/backup/dialogs/show-dialog-generate-backup.ts @@ -1,9 +1,11 @@ import { fireEvent } from "../../../../common/dom/fire_event"; import type { GenerateBackupParams } from "../../../../data/backup"; +import type { CloudStatus } from "../../../../data/cloud"; export interface GenerateBackupDialogParams { submit?: (response: GenerateBackupParams) => void; cancel?: () => void; + cloudStatus: CloudStatus; } export const loadGenerateBackupDialog = () => diff --git a/src/panels/config/backup/ha-config-backup-backups.ts b/src/panels/config/backup/ha-config-backup-backups.ts index 1c7cdefcce7b..ed478a577e32 100644 --- a/src/panels/config/backup/ha-config-backup-backups.ts +++ b/src/panels/config/backup/ha-config-backup-backups.ts @@ -363,7 +363,9 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) { } if (type === "manual") { - const params = await showGenerateBackupDialog(this, {}); + const params = await showGenerateBackupDialog(this, { + cloudStatus: this.cloudStatus, + }); if (!params) { return; diff --git a/src/panels/config/backup/ha-config-backup-overview.ts b/src/panels/config/backup/ha-config-backup-overview.ts index 16247dbcac3a..45cdf544853f 100644 --- a/src/panels/config/backup/ha-config-backup-overview.ts +++ b/src/panels/config/backup/ha-config-backup-overview.ts @@ -110,7 +110,9 @@ class HaConfigBackupOverview extends LitElement { } if (type === "manual") { - const params = await showGenerateBackupDialog(this, {}); + const params = await showGenerateBackupDialog(this, { + cloudStatus: this.cloudStatus, + }); if (!params) { return;