From d9cd428bf481d78de8ee31629baaf13073fa8c48 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Tue, 19 Nov 2024 12:33:45 +0100 Subject: [PATCH] Create generate backup dialog (#22866) Co-authored-by: Bram Kragten --- .../components/ha-backup-agents-select.ts | 104 +++++ .../backup/dialogs/dialog-generate-backup.ts | 372 ++++++++++++++++++ .../dialogs/show-dialog-generate-backup.ts | 37 ++ .../backup/ha-config-backup-dashboard.ts | 27 +- .../backup/ha-config-backup-locations.ts | 8 +- 5 files changed, 526 insertions(+), 22 deletions(-) create mode 100644 src/panels/config/backup/components/ha-backup-agents-select.ts create mode 100644 src/panels/config/backup/dialogs/dialog-generate-backup.ts create mode 100644 src/panels/config/backup/dialogs/show-dialog-generate-backup.ts diff --git a/src/panels/config/backup/components/ha-backup-agents-select.ts b/src/panels/config/backup/components/ha-backup-agents-select.ts new file mode 100644 index 0000000000..120c4fb34b --- /dev/null +++ b/src/panels/config/backup/components/ha-backup-agents-select.ts @@ -0,0 +1,104 @@ +import { css, html, LitElement } from "lit"; +import { customElement, property } from "lit/decorators"; +import { fireEvent } from "../../../../common/dom/fire_event"; +import "../../../../components/ha-checkbox"; +import "../../../../components/ha-formfield"; +import type { BackupAgent } from "../../../../data/backup"; +import type { HomeAssistant } from "../../../../types"; +import { brandsUrl } from "../../../../util/brands-url"; +import { domainToName } from "../../../../data/integration"; + +@customElement("ha-backup-agents-select") +class HaBackupAgentsSelect extends LitElement { + @property({ attribute: false }) + public hass!: HomeAssistant; + + @property({ type: Boolean }) + public disabled = false; + + @property({ attribute: false }) + public agents!: BackupAgent[]; + + @property({ attribute: false }) + public disabledAgents?: string[]; + + @property({ attribute: false }) + public value!: string[]; + + render() { + return html` +
+ ${this.agents.map((agent) => this._renderAgent(agent))} +
+ `; + } + + private _renderAgent(agent: BackupAgent) { + const [domain, name] = agent.agent_id.split("."); + const domainName = domainToName(this.hass.localize, domain); + return html` + + + + ${domainName}: ${name} + + + `; + } + + private _checkboxChanged(ev: Event) { + const checkbox = ev.target as HTMLInputElement; + const value = checkbox.value; + const index = this.value.indexOf(value); + if (checkbox.checked && index === -1) { + this.value = [...this.value, value]; + } else if (!checkbox.checked && index !== -1) { + this.value = [ + ...this.value.slice(0, index), + ...this.value.slice(index + 1), + ]; + } + fireEvent(this, "value-changed", { value: this.value }); + } + + static styles = css` + img { + height: 24px; + width: 24px; + } + .agents { + display: flex; + flex-direction: column; + } + .label { + display: flex; + flex-direction: row; + align-items: center; + gap: 16px; + } + `; +} + +declare global { + interface HTMLElementTagNameMap { + "ha-backup-agents-select": HaBackupAgentsSelect; + } +} diff --git a/src/panels/config/backup/dialogs/dialog-generate-backup.ts b/src/panels/config/backup/dialogs/dialog-generate-backup.ts new file mode 100644 index 0000000000..97e011e104 --- /dev/null +++ b/src/panels/config/backup/dialogs/dialog-generate-backup.ts @@ -0,0 +1,372 @@ +import { + mdiChartBox, + mdiClose, + mdiCog, + mdiFolder, + mdiPlayBoxMultiple, +} 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 "../../../../components/ha-button"; +import "../../../../components/ha-dialog-header"; +import "../../../../components/ha-expansion-panel"; +import "../../../../components/ha-icon-button"; +import "../../../../components/ha-icon-button-prev"; +import "../../../../components/ha-md-dialog"; +import type { HaMdDialog } from "../../../../components/ha-md-dialog"; +import "../../../../components/ha-md-select"; +import "../../../../components/ha-md-select-option"; +import "../../../../components/ha-settings-row"; +import "../../../../components/ha-svg-icon"; +import "../../../../components/ha-switch"; +import "../../../../components/ha-textfield"; +import type { BackupAgent } from "../../../../data/backup"; +import { fetchBackupAgentsInfo, generateBackup } 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-select"; +import type { GenerateBackupDialogParams } from "./show-dialog-generate-backup"; + +type FormData = { + name: string; + history: boolean; + media: boolean; + share: boolean; + addons_mode: "all" | "custom"; + addons: string[]; + agents_mode: "all" | "custom"; + agents: string[]; +}; + +const INITIAL_FORM_DATA: FormData = { + name: "", + history: true, + media: false, + share: false, + addons_mode: "all", + addons: [], + agents_mode: "all", + agents: [], +}; + +const STEPS = ["data", "sync"] as const; + +@customElement("ha-dialog-generate-backup") +class DialogGenerateBackup extends LitElement implements HassDialog { + @property({ attribute: false }) public hass!: HomeAssistant; + + @state() private _formData?: FormData; + + @state() private _step?: "data" | "sync"; + + @state() private _agents: BackupAgent[] = []; + + @state() private _params?: GenerateBackupDialogParams; + + @query("ha-md-dialog") private _dialog?: HaMdDialog; + + public showDialog(_params: GenerateBackupDialogParams): void { + this._step = STEPS[0]; + this._formData = INITIAL_FORM_DATA; + this._params = _params; + this._fetchAgents(); + } + + private _dialogClosed() { + this._step = undefined; + this._formData = undefined; + this._agents = []; + this._params = undefined; + fireEvent(this, "dialog-closed", { dialog: this.localName }); + } + + private async _fetchAgents() { + const { agents } = await fetchBackupAgentsInfo(this.hass); + this._agents = agents; + } + + public closeDialog() { + this._dialog?.close(); + } + + private _previousStep() { + const index = STEPS.indexOf(this._step!); + if (index === 0) { + return; + } + this._step = STEPS[index - 1]; + } + + private _nextStep() { + const index = STEPS.indexOf(this._step!); + if (index === STEPS.length - 1) { + return; + } + this._step = STEPS[index + 1]; + } + + protected render() { + if (!this._step || !this._formData) { + return nothing; + } + + const dialogTitle = + this._step === "sync" ? "Synchronization" : "Backup data"; + + const isFirstStep = this._step === STEPS[0]; + const isLastStep = this._step === STEPS[STEPS.length - 1]; + + return html` + + + ${isFirstStep + ? html` + + ` + : html` + + `} + ${dialogTitle} + +
+ ${this._step === "data" ? this._renderData() : this._renderSync()} +
+
+ ${isFirstStep + ? html`Cancel` + : nothing} + ${isLastStep + ? html`Create backup` + : html`Next`} +
+
+ `; + } + + private _renderData() { + if (!this._formData) { + return nothing; + } + return html` + + + Home Assistant settings + + With these settings you are able to restore your system. + + + + + + History + For example of your energy dashboard. + + + + + Media + + Folder that is often used for advanced or older configurations. + + + + + + Share folder + + Folder that is often used for advanced or older configurations. + + + + `; + } + + private _renderSync() { + if (!this._formData) { + return nothing; + } + return html` + + + + Locations + + What locations you want to automatically backup to. + + + +
All (${this._agents.length})
+
+ +
Custom
+
+
+
+ ${this._formData.agents_mode === "custom" + ? html` + + + + ` + : nothing} + `; + } + + private _agentModeChanged(ev) { + const select = ev.currentTarget; + this._formData = { + ...this._formData!, + agents_mode: select.value, + }; + } + + private _agentsChanged(ev) { + this._formData = { + ...this._formData!, + agents: ev.detail.value, + }; + } + + private _switchChanged(ev) { + const _switch = ev.currentTarget; + this._formData = { + ...this._formData!, + [_switch.id]: _switch.checked, + }; + } + + private _nameChanged(ev) { + this._formData = { + ...this._formData!, + name: ev.target.value, + }; + } + + private async _submit() { + if (!this._formData) { + return; + } + + const { + addons, + addons_mode, + agents, + agents_mode, + history, + media, + name, + share, + } = this._formData; + + const folders: string[] = []; + if (media) { + folders.push("media"); + } + if (share) { + folders.push("share"); + } + + // TODO: Fetch all addons + const ALL_ADDONS = []; + const { slug } = await generateBackup(this.hass, { + name, + agent_ids: + agents_mode === "all" + ? this._agents.map((agent) => agent.agent_id) + : agents, + database_included: history, + folders_included: folders, + addons_included: addons_mode === "all" ? ALL_ADDONS : addons, + }); + + this._params!.submit?.({ slug }); + this.closeDialog(); + } + + static get styles(): CSSResultGroup { + return [ + haStyle, + haStyleDialog, + css` + :host { + --dialog-content-overflow: visible; + } + ha-md-dialog { + --dialog-content-padding: 24px; + } + ha-settings-row { + --settings-row-prefix-display: flex; + padding: 0; + } + ha-settings-row > ha-svg-icon { + align-self: center; + margin-inline-end: 16px; + } + ha-settings-row > ha-md-select { + min-width: 150px; + } + ha-settings-row > ha-md-select > span { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } + ha-settings-row > ha-md-select-option { + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + } + ha-textfield { + width: 100%; + } + .content { + padding-top: 0; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-dialog-generate-backup": DialogGenerateBackup; + } +} diff --git a/src/panels/config/backup/dialogs/show-dialog-generate-backup.ts b/src/panels/config/backup/dialogs/show-dialog-generate-backup.ts new file mode 100644 index 0000000000..79bd55a3b3 --- /dev/null +++ b/src/panels/config/backup/dialogs/show-dialog-generate-backup.ts @@ -0,0 +1,37 @@ +import { fireEvent } from "../../../../common/dom/fire_event"; + +export interface GenerateBackupDialogParams { + submit?: (response: { slug: string }) => void; + cancel?: () => void; +} + +export const loadGenerateBackupDialog = () => + import("./dialog-generate-backup"); + +export const showGenerateBackupDialog = ( + element: HTMLElement, + params: GenerateBackupDialogParams +) => + new Promise<{ slug: string } | null>((resolve) => { + const origCancel = params.cancel; + const origSubmit = params.submit; + fireEvent(element, "show-dialog", { + dialogTag: "ha-dialog-generate-backup", + dialogImport: loadGenerateBackupDialog, + dialogParams: { + ...params, + cancel: () => { + resolve(null); + if (origCancel) { + origCancel(); + } + }, + submit: (response) => { + resolve(response); + if (origSubmit) { + origSubmit(response); + } + }, + }, + }); + }); diff --git a/src/panels/config/backup/ha-config-backup-dashboard.ts b/src/panels/config/backup/ha-config-backup-dashboard.ts index 0bf26fa3a9..5b6ae60a17 100644 --- a/src/panels/config/backup/ha-config-backup-dashboard.ts +++ b/src/panels/config/backup/ha-config-backup-dashboard.ts @@ -22,22 +22,22 @@ import "../../../components/ha-icon-overflow-menu"; import "../../../components/ha-svg-icon"; import { fetchBackupInfo, - generateBackup, removeBackup, type BackupContent, } from "../../../data/backup"; import { extractApiErrorMessage } from "../../../data/hassio/common"; +import { + showAlertDialog, + showConfirmationDialog, +} from "../../../dialogs/generic/show-dialog-box"; import "../../../layouts/hass-tabs-subpage-data-table"; import type { HaTabsSubpageDataTable } from "../../../layouts/hass-tabs-subpage-data-table"; import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; import { haStyle } from "../../../resources/styles"; import type { HomeAssistant, Route } from "../../../types"; import { brandsUrl } from "../../../util/brands-url"; -import { - showAlertDialog, - showConfirmationDialog, -} from "../../lovelace/custom-card-helpers"; import "./components/ha-backup-summary-card"; +import { showGenerateBackupDialog } from "./dialogs/show-dialog-generate-backup"; @customElement("ha-config-backup-dashboard") class HaConfigBackupDashboard extends SubscribeMixin(LitElement) { @@ -244,21 +244,10 @@ class HaConfigBackupDashboard extends SubscribeMixin(LitElement) { } private async _generateBackup(): Promise { - const confirm = await showConfirmationDialog(this, { - title: this.hass.localize("ui.panel.config.backup.create.title"), - text: this.hass.localize("ui.panel.config.backup.create.description"), - confirmText: this.hass.localize("ui.panel.config.backup.create.confirm"), - }); - if (!confirm) { - return; - } + const response = await showGenerateBackupDialog(this, {}); - try { - await generateBackup(this.hass, { - agent_ids: ["backup.local"], - }); - } catch (err) { - showAlertDialog(this, { text: (err as Error).message }); + if (!response) { + return; } await this._fetchBackupInfo(); diff --git a/src/panels/config/backup/ha-config-backup-locations.ts b/src/panels/config/backup/ha-config-backup-locations.ts index 505f04b95b..a6623b1324 100644 --- a/src/panels/config/backup/ha-config-backup-locations.ts +++ b/src/panels/config/backup/ha-config-backup-locations.ts @@ -10,6 +10,7 @@ import { fetchBackupAgentsInfo } from "../../../data/backup"; import "../../../layouts/hass-subpage"; import type { HomeAssistant } from "../../../types"; import { brandsUrl } from "../../../util/brands-url"; +import { domainToName } from "../../../data/integration"; @customElement("ha-config-backup-locations") class HaConfigBackupLocations extends LitElement { @@ -47,9 +48,10 @@ class HaConfigBackupLocations extends LitElement { ${this._agents.map((agent) => { const [domain, name] = agent.agent_id.split("."); - const domainName = - this.hass.localize(`component.${domain}.title`) || - domain; + const domainName = domainToName( + this.hass.localize, + domain + ); return html`