From 92b02e39c902b3686d5b1ba1585ef2ef25fd5db7 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Thu, 19 Dec 2024 19:54:50 +0100 Subject: [PATCH] Add overview summary for backups (#23343) --- .../components/ha-backup-summary-card.ts | 9 +- .../overview/ha-backup-overview-backups.ts | 3 + .../overview/ha-backup-overview-onboarding.ts | 104 ++++++++++ .../ha-backup-overview-progress.ts} | 17 +- .../overview/ha-backup-overview-summary.ts | 195 ++++++++++++++++++ .../dialogs/dialog-backup-onboarding.ts | 20 +- .../backup/dialogs/dialog-new-backup.ts | 8 +- .../dialogs/show-dialog-backup_onboarding.ts | 1 + .../config/backup/ha-config-backup-backups.ts | 112 ++-------- .../backup/ha-config-backup-locations.ts | 138 ------------- .../backup/ha-config-backup-overview.ts | 123 +++++------ .../backup/ha-config-backup-settings.ts | 104 ++++------ src/panels/config/backup/ha-config-backup.ts | 113 +++++++++- 13 files changed, 537 insertions(+), 410 deletions(-) create mode 100644 src/panels/config/backup/components/overview/ha-backup-overview-onboarding.ts rename src/panels/config/backup/components/{ha-backup-summary-progress.ts => overview/ha-backup-overview-progress.ts} (85%) create mode 100644 src/panels/config/backup/components/overview/ha-backup-overview-summary.ts delete mode 100644 src/panels/config/backup/ha-config-backup-locations.ts diff --git a/src/panels/config/backup/components/ha-backup-summary-card.ts b/src/panels/config/backup/components/ha-backup-summary-card.ts index 1054f0b06146..fb894266a777 100644 --- a/src/panels/config/backup/components/ha-backup-summary-card.ts +++ b/src/panels/config/backup/components/ha-backup-summary-card.ts @@ -1,5 +1,5 @@ import { - mdiAlertCircleCheckOutline, + mdiAlertCircleOutline, mdiAlertOutline, mdiCheck, mdiInformationOutline, @@ -16,7 +16,7 @@ type SummaryStatus = "success" | "error" | "info" | "warning" | "loading"; const ICONS: Record = { success: mdiCheck, - error: mdiAlertCircleCheckOutline, + error: mdiAlertCircleOutline, warning: mdiAlertOutline, info: mdiInformationOutline, loading: mdiSync, @@ -60,6 +60,9 @@ class HaBackupSummaryCard extends LitElement { ` : nothing} +
+ +
`; } @@ -71,7 +74,7 @@ class HaBackupSummaryCard extends LitElement { column-gap: 16px; row-gap: 8px; align-items: center; - padding: 20px; + padding: 16px; width: 100%; box-sizing: border-box; } diff --git a/src/panels/config/backup/components/overview/ha-backup-overview-backups.ts b/src/panels/config/backup/components/overview/ha-backup-overview-backups.ts index c79a0aeeaeae..82f72082c02f 100644 --- a/src/panels/config/backup/components/overview/ha-backup-overview-backups.ts +++ b/src/panels/config/backup/components/overview/ha-backup-overview-backups.ts @@ -1,3 +1,4 @@ +import { mdiCalendarSync, mdiGestureTap } from "@mdi/js"; import type { CSSResultGroup } from "lit"; import { css, html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; @@ -62,6 +63,7 @@ class HaBackupOverviewBackups extends LitElement {
+
${automaticStats.count} automatic backups
@@ -71,6 +73,7 @@ class HaBackupOverviewBackups extends LitElement {
+
${manualStats.count} manual backups
${bytesToString(manualStats.size, 1)} in total diff --git a/src/panels/config/backup/components/overview/ha-backup-overview-onboarding.ts b/src/panels/config/backup/components/overview/ha-backup-overview-onboarding.ts new file mode 100644 index 000000000000..b030c8b7d899 --- /dev/null +++ b/src/panels/config/backup/components/overview/ha-backup-overview-onboarding.ts @@ -0,0 +1,104 @@ +import { mdiInformationOutline } from "@mdi/js"; +import type { CSSResultGroup } from "lit"; +import { css, html, LitElement } from "lit"; +import { customElement, property } from "lit/decorators"; +import { fireEvent } from "../../../../../common/dom/fire_event"; +import "../../../../../components/ha-button"; +import "../../../../../components/ha-card"; +import "../../../../../components/ha-svg-icon"; +import { haStyle } from "../../../../../resources/styles"; +import type { HomeAssistant } from "../../../../../types"; + +declare global { + // for fire event + interface HASSDomEvents { + "button-click": undefined; + } +} + +@customElement("ha-backup-overview-onboarding") +class HaBackupOverviewBackups extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + private async _setup() { + fireEvent(this, "button-click"); + } + + render() { + return html` + +
+
+ +
+ Set up automatic backups +
+
+

+ Backups are essential to a reliable smart home. They protect your + setup against failures and allows you to quickly have a working + system again. 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. +

+
+
+ + Set up automatic backups + +
+
+ `; + } + + static get styles(): CSSResultGroup { + return [ + haStyle, + css` + .card-header { + display: flex; + flex-direction: row; + align-items: center; + gap: 16px; + } + .icon { + position: relative; + border-radius: 20px; + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + } + .icon::before { + display: block; + content: ""; + position: absolute; + inset: 0; + background-color: var(--primary-color); + opacity: 0.2; + } + .icon ha-svg-icon { + color: var(--primary-color); + width: 24px; + height: 24px; + } + p { + margin: 0; + } + .card-actions { + display: flex; + justify-content: flex-end; + border-top: none; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-backup-overview-onboarding": HaBackupOverviewBackups; + } +} diff --git a/src/panels/config/backup/components/ha-backup-summary-progress.ts b/src/panels/config/backup/components/overview/ha-backup-overview-progress.ts similarity index 85% rename from src/panels/config/backup/components/ha-backup-summary-progress.ts rename to src/panels/config/backup/components/overview/ha-backup-overview-progress.ts index 63a0a9aa6b45..b9bfeffff6ac 100644 --- a/src/panels/config/backup/components/ha-backup-summary-progress.ts +++ b/src/panels/config/backup/components/overview/ha-backup-overview-progress.ts @@ -1,18 +1,15 @@ import { html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; -import type { ManagerStateEvent } from "../../../../data/backup_manager"; -import type { HomeAssistant } from "../../../../types"; -import "./ha-backup-summary-card"; +import type { ManagerStateEvent } from "../../../../../data/backup_manager"; +import type { HomeAssistant } from "../../../../../types"; +import "../ha-backup-summary-card"; -@customElement("ha-backup-summary-progress") -export class HaBackupSummaryProgress extends LitElement { +@customElement("ha-backup-overview-progress") +export class HaBackupOverviewProgress extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public manager!: ManagerStateEvent; - @property({ type: Boolean, attribute: "has-action" }) - public hasAction = false; - private get _heading() { switch (this.manager.manager_state) { case "create_backup": @@ -93,9 +90,7 @@ export class HaBackupSummaryProgress extends LitElement { .heading=${this._heading} .description=${this._description} status="loading" - .hasAction=${this.hasAction} > - `; } @@ -103,6 +98,6 @@ export class HaBackupSummaryProgress extends LitElement { declare global { interface HTMLElementTagNameMap { - "ha-backup-summary-progress": HaBackupSummaryProgress; + "ha-backup-overview-progress": HaBackupOverviewProgress; } } diff --git a/src/panels/config/backup/components/overview/ha-backup-overview-summary.ts b/src/panels/config/backup/components/overview/ha-backup-overview-summary.ts new file mode 100644 index 000000000000..5882675bf7c7 --- /dev/null +++ b/src/panels/config/backup/components/overview/ha-backup-overview-summary.ts @@ -0,0 +1,195 @@ +import { mdiBackupRestore, mdiCalendar } from "@mdi/js"; +import { differenceInDays, setHours, setMinutes } from "date-fns"; +import type { CSSResultGroup } from "lit"; +import { css, html, LitElement } from "lit"; +import { customElement, property } from "lit/decorators"; +import memoizeOne from "memoize-one"; +import { formatTime } from "../../../../../common/datetime/format_time"; +import { relativeTime } from "../../../../../common/datetime/relative_time"; +import "../../../../../components/ha-button"; +import "../../../../../components/ha-card"; +import "../../../../../components/ha-md-list"; +import "../../../../../components/ha-md-list-item"; +import "../../../../../components/ha-svg-icon"; +import type { BackupConfig, BackupContent } from "../../../../../data/backup"; +import { BackupScheduleState } from "../../../../../data/backup"; +import { haStyle } from "../../../../../resources/styles"; +import type { HomeAssistant } from "../../../../../types"; +import "../ha-backup-summary-card"; + +@customElement("ha-backup-overview-summary") +class HaBackupOverviewBackups extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public backups: BackupContent[] = []; + + @property({ attribute: false }) public config!: BackupConfig; + + private _lastBackup = memoizeOne((backups: BackupContent[]) => { + const sortedBackups = backups + .filter((backup) => backup.with_automatic_settings) + .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); + + return sortedBackups[0] as BackupContent | undefined; + }); + + private _nextBackupDescription(schedule: BackupScheduleState) { + const newDate = setMinutes(setHours(new Date(), 4), 45); + const time = formatTime(newDate, this.hass.locale, this.hass.config); + + switch (schedule) { + case BackupScheduleState.DAILY: + return `Next automatic backup tomorrow at ${time}`; + case BackupScheduleState.MONDAY: + return `Next automatic backup next Monday at ${time}`; + case BackupScheduleState.TUESDAY: + return `Next automatic backup next Thuesday at ${time}`; + case BackupScheduleState.WEDNESDAY: + return `Next automatic backup next Wednesday at ${time}`; + case BackupScheduleState.THURSDAY: + return `Next automatic backup next Thursday at ${time}`; + case BackupScheduleState.FRIDAY: + return `Next automatic backup next Friday at ${time}`; + case BackupScheduleState.SATURDAY: + return `Next automatic backup next Saturday at ${time}`; + case BackupScheduleState.SUNDAY: + return `Next automatic backup next Sunday at ${time}`; + default: + return "No automatic backup scheduled"; + } + } + + protected render() { + const lastBackup = this._lastBackup(this.backups); + + if (!lastBackup) { + return html` + + + `; + } + + const lastBackupDate = new Date(lastBackup.date); + + const numberOfDays = differenceInDays(new Date(), lastBackupDate); + const now = new Date(); + + const lastBackupDescription = `Last successful backup ${relativeTime(lastBackupDate, this.hass.locale, now, true)} and synced to ${lastBackup.agent_ids?.length} locations.`; + const nextBackupDescription = this._nextBackupDescription( + this.config.schedule.state + ); + + const lastAttempt = this.config.last_attempted_automatic_backup + ? new Date(this.config.last_attempted_automatic_backup) + : undefined; + + if (lastAttempt && lastAttempt > lastBackupDate) { + const lastAttemptDescription = `The last automatic backup trigged ${relativeTime(lastAttempt, this.hass.locale, now, true)} wasn't successful.`; + return html` + +
    +
  • + + ${lastAttemptDescription} +
  • +
  • + + ${lastBackupDescription} +
  • +
+
+ `; + } + + if (numberOfDays > 0) { + return html` + +
    +
  • + + ${lastBackupDescription} +
  • +
  • + + ${nextBackupDescription} +
  • +
+
+ `; + } + return html` + +
    +
  • + + ${lastBackupDescription} +
  • +
  • + + ${nextBackupDescription} +
  • +
+
+ `; + } + + static get styles(): CSSResultGroup { + return [ + haStyle, + css` + .card-header { + display: flex; + flex-direction: row; + align-items: center; + gap: 16px; + } + p { + margin: 0; + } + .list { + display: flex; + flex-direction: column; + gap: 16px; + padding: 8px 24px 24px 24px; + margin: 0; + } + .item { + display: flex; + flex-direction: row; + gap: 16px; + align-items: center; + color: var(--secondary-text-color); + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; + letter-spacing: 0.25px; + } + ha-svg-icon { + flex: none; + } + .card-actions { + display: flex; + justify-content: flex-end; + border-top: none; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-backup-overview-summary": HaBackupOverviewBackups; + } +} diff --git a/src/panels/config/backup/dialogs/dialog-backup-onboarding.ts b/src/panels/config/backup/dialogs/dialog-backup-onboarding.ts index 512932468bf9..e373a3ad361a 100644 --- a/src/panels/config/backup/dialogs/dialog-backup-onboarding.ts +++ b/src/panels/config/backup/dialogs/dialog-backup-onboarding.ts @@ -82,6 +82,8 @@ class DialogBackupOnboarding extends LitElement implements HassDialog { @state() private _step?: Step; + @state() private _steps: Step[] = []; + @state() private _params?: BackupOnboardingDialogParams; @query("ha-md-dialog") private _dialog!: HaMdDialog; @@ -90,7 +92,8 @@ class DialogBackupOnboarding extends LitElement implements HassDialog { public showDialog(params: BackupOnboardingDialogParams): void { this._params = params; - this._step = STEPS[0]; + this._steps = params.showIntro ? STEPS.concat() : STEPS.slice(1); + this._step = this._steps[0]; this._config = RECOMMENDED_CONFIG; // Enable local location by default @@ -117,6 +120,7 @@ class DialogBackupOnboarding extends LitElement implements HassDialog { } this._opened = false; this._step = undefined; + this._steps = []; this._config = undefined; this._params = undefined; } @@ -158,19 +162,19 @@ class DialogBackupOnboarding extends LitElement implements HassDialog { } private _previousStep() { - const index = STEPS.indexOf(this._step!); + const index = this._steps.indexOf(this._step!); if (index === 0) { return; } - this._step = STEPS[index - 1]; + this._step = this._steps[index - 1]; } private _nextStep() { - const index = STEPS.indexOf(this._step!); - if (index === STEPS.length - 1) { + const index = this._steps.indexOf(this._step!); + if (index === this._steps.length - 1) { return; } - this._step = STEPS[index + 1]; + this._step = this._steps[index + 1]; } protected render() { @@ -178,8 +182,8 @@ class DialogBackupOnboarding extends LitElement implements HassDialog { return nothing; } - const isLastStep = this._step === STEPS[STEPS.length - 1]; - const isFirstStep = this._step === STEPS[0]; + const isLastStep = this._step === this._steps[this._steps.length - 1]; + const isFirstStep = this._step === this._steps[0]; return html` diff --git a/src/panels/config/backup/dialogs/dialog-new-backup.ts b/src/panels/config/backup/dialogs/dialog-new-backup.ts index 363a365ea9d6..0ac81c454ade 100644 --- a/src/panels/config/backup/dialogs/dialog-new-backup.ts +++ b/src/panels/config/backup/dialogs/dialog-new-backup.ts @@ -1,4 +1,4 @@ -import { mdiClose, mdiCog, mdiPencil } from "@mdi/js"; +import { mdiCalendarSync, mdiClose, mdiGestureTap } from "@mdi/js"; import type { CSSResultGroup } from "lit"; import { LitElement, css, html, nothing } from "lit"; import { customElement, property, query, state } from "lit/decorators"; @@ -7,6 +7,7 @@ import "../../../../components/ha-dialog-header"; import "../../../../components/ha-icon-button"; import "../../../../components/ha-icon-next"; 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-svg-icon"; @@ -14,7 +15,6 @@ import type { HassDialog } from "../../../../dialogs/make-dialog-manager"; import { haStyle, haStyleDialog } from "../../../../resources/styles"; import type { HomeAssistant } from "../../../../types"; import type { NewBackupDialogParams } from "./show-dialog-new-backup"; -import type { HaMdDialog } from "../../../../components/ha-md-dialog"; @customElement("ha-dialog-new-backup") class DialogNewBackup extends LitElement implements HassDialog { @@ -75,7 +75,7 @@ class DialogNewBackup extends LitElement implements HassDialog { type="button" .disabled=${!this._params.config.create_backup.password} > - + Automatic backup Create a backup with the data and locations you have configured. @@ -83,7 +83,7 @@ class DialogNewBackup extends LitElement implements HassDialog { - + Manual backup Select data and locations for a manual backup. diff --git a/src/panels/config/backup/dialogs/show-dialog-backup_onboarding.ts b/src/panels/config/backup/dialogs/show-dialog-backup_onboarding.ts index 578f80bf1344..7955d29ddba7 100644 --- a/src/panels/config/backup/dialogs/show-dialog-backup_onboarding.ts +++ b/src/panels/config/backup/dialogs/show-dialog-backup_onboarding.ts @@ -4,6 +4,7 @@ import type { CloudStatus } from "../../../../data/cloud"; export interface BackupOnboardingDialogParams { submit?: (value: boolean) => void; cancel?: () => void; + showIntro?: boolean; cloudStatus: CloudStatus; } diff --git a/src/panels/config/backup/ha-config-backup-backups.ts b/src/panels/config/backup/ha-config-backup-backups.ts index 2d00aed0fa79..c89d39f46a35 100644 --- a/src/panels/config/backup/ha-config-backup-backups.ts +++ b/src/panels/config/backup/ha-config-backup-backups.ts @@ -6,14 +6,14 @@ import { mdiPlus, mdiUpload, } from "@mdi/js"; -import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit"; +import type { CSSResultGroup, TemplateResult } from "lit"; import { css, html, LitElement, nothing } from "lit"; import { customElement, property, query, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; import memoizeOne from "memoize-one"; import { relativeTime } from "../../../common/datetime/relative_time"; import { storage } from "../../../common/decorators/storage"; -import type { HASSDomEvent } from "../../../common/dom/fire_event"; +import { fireEvent, 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"; @@ -36,11 +36,8 @@ import "../../../components/ha-svg-icon"; import { getSignedPath } from "../../../data/auth"; import type { BackupConfig, BackupContent } from "../../../data/backup"; import { - compareAgents, computeBackupAgentName, deleteBackup, - fetchBackupConfig, - fetchBackupInfo, generateBackup, generateBackupWithAutomaticSettings, getBackupDownloadUrl, @@ -48,10 +45,6 @@ import { isLocalAgent, } from "../../../data/backup"; import type { ManagerStateEvent } from "../../../data/backup_manager"; -import { - DEFAULT_MANAGER_STATE, - subscribeBackupEvents, -} from "../../../data/backup_manager"; import type { CloudStatus } from "../../../data/cloud"; import { extractApiErrorMessage } from "../../../data/hassio/common"; import { @@ -66,7 +59,6 @@ import type { HomeAssistant, Route } from "../../../types"; import { brandsUrl } from "../../../util/brands-url"; import { bytesToString } from "../../../util/bytes-to-string"; import { fileDownload } from "../../../util/file_download"; -import { showToast } from "../../../util/toast"; import { showGenerateBackupDialog } from "./dialogs/show-dialog-generate-backup"; import { showNewBackupDialog } from "./dialogs/show-dialog-new-backup"; import { showUploadBackupDialog } from "./dialogs/show-dialog-upload-backup"; @@ -89,13 +81,13 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) { @property({ attribute: false }) public route!: Route; - @state() private _manager: ManagerStateEvent = DEFAULT_MANAGER_STATE; + @property({ attribute: false }) public manager!: ManagerStateEvent; - @state() private _backups: BackupContent[] = []; + @property({ attribute: false }) public backups: BackupContent[] = []; - @state() private _selected: string[] = []; + @property({ attribute: false }) public config?: BackupConfig; - @state() private _config?: BackupConfig; + @state() private _selected: string[] = []; @storage({ key: "backups-table-grouping", state: false, subscribe: false }) private _activeGrouping?: string = "formatted_type"; @@ -107,8 +99,6 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) { }) private _activeCollapsed: string[] = []; - private _subscribed?: Promise<() => void>; - @query("hass-tabs-subpage-data-table", true) private _dataTable!: HaTabsSubpageDataTable; @@ -251,7 +241,7 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) { protected render(): TemplateResult { const backupInProgress = - "state" in this._manager && this._manager.state === "in_progress"; + "state" in this.manager && this.manager.state === "in_progress"; return html` unsub()); - this._subscribed = undefined; - } - } - - private async _subscribeEvents() { - this._unsubscribeEvents(); - if (!this.isConnected) { - return; - } - - this._subscribed = subscribeBackupEvents(this.hass!, (event) => { - this._manager = event; - if ("state" in event) { - if (event.state === "completed" || event.state === "failed") { - this._fetchBackupInfo(); - } - if (event.state === "failed") { - let message = ""; - switch (this._manager.manager_state) { - case "create_backup": - message = "Failed to create backup"; - break; - case "restore_backup": - message = "Failed to restore backup"; - break; - case "receive_backup": - message = "Failed to upload backup"; - break; - } - if (message) { - showToast(this, { message }); - } - } - } - }); - } - - protected firstUpdated(changedProps: PropertyValues) { - super.firstUpdated(changedProps); - this._fetchBackupInfo(); - this._subscribeEvents(); - this._fetchBackupConfig(); - } - - public connectedCallback() { - super.connectedCallback(); - if (this.hasUpdated) { - this._fetchBackupInfo(); - this._subscribeEvents(); - } - } - - public disconnectedCallback(): void { - super.disconnectedCallback(); - this._unsubscribeEvents(); - } - - 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), - })); - } - - private async _fetchBackupConfig() { - const { config } = await fetchBackupConfig(this.hass); - this._config = config; - } - private get _needsOnboarding() { - return !this._config?.create_backup.password; + return !this.config?.create_backup.password; } private async _uploadBackup(ev) { @@ -438,7 +354,7 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) { } private async _newBackup(): Promise { - const config = this._config!; + const config = this.config!; const type = await showNewBackupDialog(this, { config }); @@ -454,12 +370,12 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) { } await generateBackup(this.hass, params); - await this._fetchBackupInfo(); + fireEvent(this, "ha-refresh-backup-info"); return; } if (type === "automatic") { await generateBackupWithAutomaticSettings(this.hass); - await this._fetchBackupInfo(); + fireEvent(this, "ha-refresh-backup-info"); } } @@ -490,7 +406,7 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) { } await deleteBackup(this.hass, backup.backup_id); - this._fetchBackupInfo(); + fireEvent(this, "ha-refresh-backup-info"); } private async _deleteSelected() { @@ -516,7 +432,7 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) { }); return; } - await this._fetchBackupInfo(); + fireEvent(this, "ha-refresh-backup-info"); this._dataTable.clearSelection(); } diff --git a/src/panels/config/backup/ha-config-backup-locations.ts b/src/panels/config/backup/ha-config-backup-locations.ts deleted file mode 100644 index 80312f30c219..000000000000 --- a/src/panels/config/backup/ha-config-backup-locations.ts +++ /dev/null @@ -1,138 +0,0 @@ -import type { TemplateResult } from "lit"; -import { css, html, LitElement } from "lit"; -import { customElement, property, state } from "lit/decorators"; -import "../../../components/ha-card"; -import "../../../components/ha-icon-next"; -import "../../../components/ha-md-list"; -import "../../../components/ha-md-list-item"; -import type { BackupAgent } from "../../../data/backup"; -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 { - @property({ attribute: false }) public hass!: HomeAssistant; - - @property({ type: Boolean }) public narrow = false; - - @state() private _agents: BackupAgent[] = []; - - protected firstUpdated(changedProps) { - super.firstUpdated(changedProps); - this._fetchAgents(); - } - - protected render(): TemplateResult { - return html` - -
-
-

Locations

-

- To keep your data safe it is recommended your backups is at least - on two different locations and one of them is off-site. -

-
- -
- ${this._agents.length > 0 - ? html` - - ${this._agents.map((agent) => { - const [domain, name] = agent.agent_id.split("."); - const domainName = domainToName( - this.hass.localize, - domain - ); - return html` - - -
${domainName}: ${name}
- -
- `; - })} -
- ` - : html`

No sync agents configured

`} -
-
-
-
- `; - } - - private async _fetchAgents() { - const data = await fetchBackupAgentsInfo(this.hass); - this._agents = data.agents; - } - - static styles = css` - .content { - padding: 28px 20px 0; - max-width: 690px; - margin: 0 auto; - gap: 24px; - display: flex; - flex-direction: column; - } - - .header .title { - font-size: 22px; - font-style: normal; - font-weight: 400; - line-height: 28px; - color: var(--primary-text-color); - margin: 0; - margin-bottom: 8px; - } - - .header .description { - font-size: 14px; - font-style: normal; - font-weight: 400; - line-height: 20px; - letter-spacing: 0.25px; - color: var(--secondary-text-color); - margin: 0; - } - - ha-md-list { - background: none; - } - ha-md-list-item img { - width: 48px; - } - .card-content { - padding: 0; - } - `; -} - -declare global { - interface HTMLElementTagNameMap { - "ha-config-backup-locations": HaConfigBackupLocations; - } -} diff --git a/src/panels/config/backup/ha-config-backup-overview.ts b/src/panels/config/backup/ha-config-backup-overview.ts index fb49f5612057..afa203244fd9 100644 --- a/src/panels/config/backup/ha-config-backup-overview.ts +++ b/src/panels/config/backup/ha-config-backup-overview.ts @@ -1,7 +1,8 @@ import { mdiDotsVertical, mdiPlus, mdiUpload } from "@mdi/js"; -import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit"; +import type { CSSResultGroup, TemplateResult } from "lit"; import { css, html, LitElement, nothing } from "lit"; -import { customElement, property, state } from "lit/decorators"; +import { customElement, property } from "lit/decorators"; +import { fireEvent } from "../../../common/dom/fire_event"; import { shouldHandleRequestSelectedEvent } from "../../../common/mwc/handle-request-selected-event"; import "../../../components/ha-button"; import "../../../components/ha-button-menu"; @@ -13,25 +14,24 @@ import "../../../components/ha-icon-overflow-menu"; import "../../../components/ha-list-item"; import "../../../components/ha-svg-icon"; import { - fetchBackupConfig, - fetchBackupInfo, generateBackup, generateBackupWithAutomaticSettings, type BackupConfig, type BackupContent, } from "../../../data/backup"; import type { ManagerStateEvent } from "../../../data/backup_manager"; -import { DEFAULT_MANAGER_STATE } from "../../../data/backup_manager"; import type { CloudStatus } from "../../../data/cloud"; import "../../../layouts/hass-subpage"; import "../../../layouts/hass-tabs-subpage-data-table"; import { haStyle } from "../../../resources/styles"; import type { HomeAssistant, Route } from "../../../types"; import "./components/ha-backup-summary-card"; -import "./components/ha-backup-summary-progress"; import "./components/ha-backup-summary-status"; import "./components/overview/ha-backup-overview-backups"; +import "./components/overview/ha-backup-overview-onboarding"; +import "./components/overview/ha-backup-overview-progress"; import "./components/overview/ha-backup-overview-settings"; +import "./components/overview/ha-backup-overview-summary"; import { showBackupOnboardingDialog } from "./dialogs/show-dialog-backup_onboarding"; import { showGenerateBackupDialog } from "./dialogs/show-dialog-generate-backup"; import { showNewBackupDialog } from "./dialogs/show-dialog-new-backup"; @@ -47,27 +47,13 @@ class HaConfigBackupOverview extends LitElement { @property({ attribute: false }) public route!: Route; - @state() private _manager: ManagerStateEvent = DEFAULT_MANAGER_STATE; + @property({ attribute: false }) public manager!: ManagerStateEvent; - @state() private _backups: BackupContent[] = []; + @property({ attribute: false }) public backups: BackupContent[] = []; - @state() private _fetching = false; + @property({ attribute: false }) public fetching = false; - @state() private _config?: BackupConfig; - - protected firstUpdated(changedProps: PropertyValues) { - super.firstUpdated(changedProps); - this._fetchBackupInfo(); - this._fetchBackupConfig(); - } - - public connectedCallback() { - super.connectedCallback(); - if (this.hasUpdated) { - this._fetchBackupInfo(); - this._fetchBackupConfig(); - } - } + @property({ attribute: false }) public config?: BackupConfig; private async _uploadBackup(ev) { if (!shouldHandleRequestSelectedEvent(ev)) { @@ -77,40 +63,36 @@ class HaConfigBackupOverview extends LitElement { await showUploadBackupDialog(this, {}); } - private async _setupAutomaticBackup() { + private _handleOnboardingButtonClick(ev) { + ev.stopPropagation(); + this._setupAutomaticBackup(false); + } + + private async _setupAutomaticBackup(showIntro: boolean) { const success = await showBackupOnboardingDialog(this, { cloudStatus: this.cloudStatus, + showIntro: showIntro, }); if (!success) { return; } - this._fetchBackupConfig(); + fireEvent(this, "ha-refresh-backup-config"); await generateBackupWithAutomaticSettings(this.hass); - await this._fetchBackupInfo(); - } - - private async _fetchBackupInfo() { - const info = await fetchBackupInfo(this.hass); - this._backups = info.backups; - } - - private async _fetchBackupConfig() { - const { config } = await fetchBackupConfig(this.hass); - this._config = config; + fireEvent(this, "ha-refresh-backup-info"); } private async _newBackup(): Promise { if (this._needsOnboarding) { - this._setupAutomaticBackup(); + this._setupAutomaticBackup(true); return; } - if (!this._config) { + if (!this.config) { return; } - const config = this._config; + const config = this.config; const type = await showNewBackupDialog(this, { config }); @@ -126,22 +108,22 @@ class HaConfigBackupOverview extends LitElement { } await generateBackup(this.hass, params); - await this._fetchBackupInfo(); + fireEvent(this, "ha-refresh-backup-info"); return; } if (type === "automatic") { await generateBackupWithAutomaticSettings(this.hass); - await this._fetchBackupInfo(); + fireEvent(this, "ha-refresh-backup-info"); } } private get _needsOnboarding() { - return !this._config?.create_backup.password; + return !this.config?.create_backup.password; } protected render(): TemplateResult { const backupInProgress = - "state" in this._manager && this._manager.state === "in_progress"; + "state" in this.manager && this.manager.state === "in_progress"; return html`
- ${this._fetching + ${backupInProgress ? html` - - + ` - : backupInProgress + : this.fetching ? html` - - + ` : this._needsOnboarding ? html` - - - Set up automatic backups - - + ` : html` - - + `} ${!this._needsOnboarding ? html` ` : nothing} @@ -258,6 +233,10 @@ class HaConfigBackupOverview extends LitElement { padding-left: 0; padding-right: 0; } + ha-fab[disabled] { + --mdc-theme-secondary: var(--disabled-text-color) !important; + pointer-events: none; + } `, ]; } diff --git a/src/panels/config/backup/ha-config-backup-settings.ts b/src/panels/config/backup/ha-config-backup-settings.ts index ffb4c2719041..f249af35eb16 100644 --- a/src/panels/config/backup/ha-config-backup-settings.ts +++ b/src/panels/config/backup/ha-config-backup-settings.ts @@ -1,17 +1,14 @@ +import type { PropertyValues } from "lit"; import { css, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; +import { fireEvent } from "../../../common/dom/fire_event"; import { debounce } from "../../../common/util/debounce"; import "../../../components/ha-button"; import "../../../components/ha-card"; import "../../../components/ha-icon-next"; import "../../../components/ha-password-field"; -import "../../../components/ha-settings-row"; import type { BackupConfig } from "../../../data/backup"; -import { - BackupScheduleState, - fetchBackupConfig, - updateBackupConfig, -} from "../../../data/backup"; +import { updateBackupConfig } from "../../../data/backup"; import type { CloudStatus } from "../../../data/cloud"; import "../../../layouts/hass-subpage"; import type { HomeAssistant } from "../../../types"; @@ -22,27 +19,6 @@ import "./components/config/ha-backup-config-encryption-key"; import "./components/config/ha-backup-config-schedule"; import type { BackupConfigSchedule } from "./components/config/ha-backup-config-schedule"; -const INITIAL_BACKUP_CONFIG: BackupConfig = { - create_backup: { - agent_ids: [], - include_folders: [], - include_database: true, - include_addons: [], - include_all_addons: true, - password: null, - name: null, - }, - retention: { - copies: 3, - days: null, - }, - schedule: { - state: BackupScheduleState.DAILY, - }, - last_attempted_automatic_backup: null, - last_completed_automatic_backup: null, -}; - @customElement("ha-config-backup-settings") class HaConfigBackupSettings extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @@ -51,22 +27,19 @@ class HaConfigBackupSettings extends LitElement { @property({ type: Boolean }) public narrow = false; - @state() private _backupConfig: BackupConfig = INITIAL_BACKUP_CONFIG; + @property({ attribute: false }) public config?: BackupConfig; - protected willUpdate(changedProps) { - super.willUpdate(changedProps); - if (!this.hasUpdated) { - this._fetchData(); - } - } + @state() private _config?: BackupConfig; - private async _fetchData() { - const { config } = await fetchBackupConfig(this.hass); - this._backupConfig = config; + protected willUpdate(changedProperties: PropertyValues): void { + super.willUpdate(changedProperties); + if (changedProperties.has("config") && !this._config) { + this._config = this.config; + } } protected render() { - if (!this._backupConfig) { + if (!this._config) { return nothing; } @@ -87,7 +60,7 @@ class HaConfigBackupSettings extends LitElement {

@@ -113,7 +86,7 @@ class HaConfigBackupSettings extends LitElement {

@@ -130,7 +103,7 @@ class HaConfigBackupSettings extends LitElement {

@@ -142,8 +115,8 @@ class HaConfigBackupSettings extends LitElement { private _scheduleConfigChanged(ev) { const value = ev.detail.value as BackupConfigSchedule; - this._backupConfig = { - ...this._backupConfig, + this._config = { + ...this._config!, schedule: value.schedule, retention: value.retention, }; @@ -156,7 +129,7 @@ class HaConfigBackupSettings extends LitElement { include_all_addons, include_database, include_folders, - } = this._backupConfig.create_backup; + } = this._config!.create_backup; return { include_homeassistant: true, @@ -169,10 +142,10 @@ class HaConfigBackupSettings extends LitElement { private _dataConfigChanged(ev) { const data = ev.detail.value as BackupConfigData; - this._backupConfig = { - ...this._backupConfig, + this._config = { + ...this._config!, create_backup: { - ...this._backupConfig.create_backup, + ...this.config!.create_backup, include_database: data.include_database, include_folders: data.include_folders || null, include_all_addons: data.include_all_addons, @@ -184,10 +157,10 @@ class HaConfigBackupSettings extends LitElement { private _agentsConfigChanged(ev) { const agents = ev.detail.value as string[]; - this._backupConfig = { - ...this._backupConfig, + this._config = { + ...this._config!, create_backup: { - ...this._backupConfig.create_backup, + ...this._config!.create_backup, agent_ids: agents, }, }; @@ -196,10 +169,10 @@ class HaConfigBackupSettings extends LitElement { private _encryptionKeyChanged(ev) { const password = ev.detail.value as string; - this._backupConfig = { - ...this._backupConfig, + this._config = { + ...this._config!, create_backup: { - ...this._backupConfig.create_backup, + ...this._config!.create_backup, password: password, }, }; @@ -211,16 +184,17 @@ class HaConfigBackupSettings extends LitElement { private async _save() { await updateBackupConfig(this.hass, { create_backup: { - agent_ids: this._backupConfig.create_backup.agent_ids, - include_folders: this._backupConfig.create_backup.include_folders ?? [], - include_database: this._backupConfig.create_backup.include_database, - include_addons: this._backupConfig.create_backup.include_addons ?? [], - include_all_addons: this._backupConfig.create_backup.include_all_addons, - password: this._backupConfig.create_backup.password, + agent_ids: this._config!.create_backup.agent_ids, + include_folders: this._config!.create_backup.include_folders ?? [], + include_database: this._config!.create_backup.include_database, + include_addons: this._config!.create_backup.include_addons ?? [], + include_all_addons: this._config!.create_backup.include_all_addons, + password: this._config!.create_backup.password, }, - retention: this._backupConfig.retention, - schedule: this._backupConfig.schedule.state, + retention: this._config!.retention, + schedule: this._config!.schedule.state, }); + fireEvent(this, "ha-refresh-backup-config"); } static styles = css` @@ -233,14 +207,6 @@ class HaConfigBackupSettings extends LitElement { flex-direction: column; margin-bottom: 24px; } - ha-settings-row { - --settings-row-prefix-display: flex; - padding: 0; - } - ha-settings-row > ha-svg-icon { - align-self: center; - margin-inline-end: 16px; - } .alert { --mdc-theme-primary: var(--error-color); } diff --git a/src/panels/config/backup/ha-config-backup.ts b/src/panels/config/backup/ha-config-backup.ts index 9bd27362774f..bf8be4d454d1 100644 --- a/src/panels/config/backup/ha-config-backup.ts +++ b/src/panels/config/backup/ha-config-backup.ts @@ -1,21 +1,89 @@ +import type { UnsubscribeFunc } from "home-assistant-js-websocket"; import type { PropertyValues } from "lit"; -import { customElement, property } from "lit/decorators"; +import { customElement, property, state } from "lit/decorators"; +import type { ManagerStateEvent } from "../../../data/backup_manager"; +import { + DEFAULT_MANAGER_STATE, + subscribeBackupEvents, +} from "../../../data/backup_manager"; 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"; +import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; import type { HomeAssistant } from "../../../types"; -import "./ha-config-backup-overview"; +import { showToast } from "../../../util/toast"; import "./ha-config-backup-backups"; +import "./ha-config-backup-overview"; +import type { BackupConfig, BackupContent } from "../../../data/backup"; +import { + compareAgents, + fetchBackupConfig, + fetchBackupInfo, +} from "../../../data/backup"; + +declare global { + interface HASSDomEvents { + "ha-refresh-backup-info": undefined; + "ha-refresh-backup-config": undefined; + } +} @customElement("ha-config-backup") -class HaConfigBackup extends HassRouterPage { +class HaConfigBackup extends SubscribeMixin(HassRouterPage) { @property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public cloudStatus!: CloudStatus; @property({ type: Boolean }) public narrow = false; + @state() private _manager: ManagerStateEvent = DEFAULT_MANAGER_STATE; + + @state() private _backups: BackupContent[] = []; + + @state() private _fetching = false; + + @state() private _config?: BackupConfig; + + protected firstUpdated(changedProps: PropertyValues) { + super.firstUpdated(changedProps); + this._fetching = true; + Promise.all([this._fetchBackupInfo(), this._fetchBackupConfig()]).finally( + () => { + this._fetching = false; + } + ); + + this.addEventListener("ha-refresh-backup-info", () => { + this._fetchBackupInfo(); + }); + this.addEventListener("ha-refresh-backup-config", () => { + this._fetchBackupConfig(); + }); + } + + public connectedCallback() { + super.connectedCallback(); + if (this.hasUpdated) { + this._fetchBackupInfo(); + this._fetchBackupConfig(); + } + } + + 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), + })); + } + + private async _fetchBackupConfig() { + const { config } = await fetchBackupConfig(this.hass); + this._config = config; + } + protected routerOptions: RouterOptions = { defaultPage: "overview", routes: { @@ -31,10 +99,6 @@ class HaConfigBackup extends HassRouterPage { tag: "ha-config-backup-details", load: () => import("./ha-config-backup-details"), }, - locations: { - tag: "ha-config-backup-locations", - load: () => import("./ha-config-backup-locations"), - }, settings: { tag: "ha-config-backup-settings", load: () => import("./ha-config-backup-settings"), @@ -47,7 +111,12 @@ class HaConfigBackup extends HassRouterPage { pageEl.route = this.routeTail; pageEl.narrow = this.narrow; pageEl.cloudStatus = this.cloudStatus; + pageEl.manager = this._manager; + pageEl.backups = this._backups; + pageEl.config = this._config; + pageEl.fetching = this._fetching; + pageEl.addEventListener("reload", () => {}); if ( (!changedProps || changedProps.has("route")) && this._currentPage === "details" @@ -55,6 +124,36 @@ class HaConfigBackup extends HassRouterPage { pageEl.backupId = this.routeTail.path.substr(1); } } + + public hassSubscribe(): Promise[] { + return [ + subscribeBackupEvents(this.hass!, (event) => { + this._manager = event; + if ("state" in event) { + if (event.state === "completed" || event.state === "failed") { + this._fetchBackupInfo(); + } + if (event.state === "failed") { + let message = ""; + switch (this._manager.manager_state) { + case "create_backup": + message = "Failed to create backup"; + break; + case "restore_backup": + message = "Failed to restore backup"; + break; + case "receive_backup": + message = "Failed to upload backup"; + break; + } + if (message) { + showToast(this, { message }); + } + } + } + }), + ]; + } } declare global {