diff --git a/src/components/ha-file-upload.ts b/src/components/ha-file-upload.ts index bd55538ef9bb..f68e52f673a3 100644 --- a/src/components/ha-file-upload.ts +++ b/src/components/ha-file-upload.ts @@ -56,6 +56,21 @@ export class HaFileUpload extends LitElement { } } + private get _name() { + if (this.value === undefined) { + return ""; + } + if (typeof this.value === "string") { + return this.value; + } + const files = + this.value instanceof FileList + ? Array.from(this.value) + : ensureArray(this.value); + + return files.map((file) => file.name).join(", "); + } + public render(): TemplateResult { return html` ${this.uploading @@ -65,7 +80,7 @@ export class HaFileUpload extends LitElement { >${this.value ? this.hass?.localize( "ui.components.file-upload.uploading_name", - { name: this.value.toString() } + { name: this._name } ) : this.hass?.localize( "ui.components.file-upload.uploading" diff --git a/src/data/backup.ts b/src/data/backup.ts index d6d7298a0f6c..658675783942 100644 --- a/src/data/backup.ts +++ b/src/data/backup.ts @@ -203,23 +203,6 @@ export const uploadBackup = async ( } }; -type BackupEvent = BackupProgressEvent; - -type BackupProgressEvent = { - event_type: "backup_progress"; - done: boolean; - stage: string; - success?: boolean; -}; - -export const subscribeBackupEvents = ( - hass: HomeAssistant, - callback: (event: BackupEvent) => void -) => - hass.connection.subscribeMessage(callback, { - type: "backup/subscribe_events", - }); - export const getPreferredAgentForDownload = (agents: string[]) => { const localAgents = agents.filter( (agent) => agent.split(".")[0] === "backup" diff --git a/src/data/backup_manager.ts b/src/data/backup_manager.ts new file mode 100644 index 000000000000..be6e26217a8e --- /dev/null +++ b/src/data/backup_manager.ts @@ -0,0 +1,77 @@ +import type { HomeAssistant } from "../types"; + +export type BackupManagerState = + | "idle" + | "create_backup" + | "receive_backup" + | "restore_backup"; + +export type CreateBackupStage = + | "addon_repositories" + | "addons" + | "await_addon_restarts" + | "docker_config" + | "finishing_file" + | "folders" + | "home_assistant" + | "upload_to_agents"; + +export type CreateBackupState = "completed" | "failed" | "in_progress"; + +export type ReceiveBackupStage = "receive_file" | "upload_to_agents"; + +export type ReceiveBackupState = "completed" | "failed" | "in_progress"; + +export type RestoreBackupStage = + | "addon_repositories" + | "addons" + | "await_addon_restarts" + | "await_home_assistant_restart" + | "check_home_assistant" + | "docker_config" + | "download_from_agent" + | "folders" + | "home_assistant" + | "remove_delta_addons"; + +export type RestoreBackupState = "completed" | "failed" | "in_progress"; + +type IdleEvent = { + manager_state: "idle"; +}; + +type CreateBackupEvent = { + manager_state: "create_backup"; + stage: CreateBackupStage | null; + state: CreateBackupState; +}; + +type ReceiveBackupEvent = { + manager_state: "receive_backup"; + stage: ReceiveBackupStage | null; + state: ReceiveBackupState; +}; + +type RestoreBackupEvent = { + manager_state: "restore_backup"; + stage: RestoreBackupStage | null; + state: RestoreBackupState; +}; + +export type ManagerStateEvent = + | IdleEvent + | CreateBackupEvent + | ReceiveBackupEvent + | RestoreBackupEvent; + +export const subscribeBackupEvents = ( + hass: HomeAssistant, + callback: (event: ManagerStateEvent) => void +) => + hass.connection.subscribeMessage(callback, { + type: "backup/subscribe_events", + }); + +export const DEFAULT_MANAGER_STATE: ManagerStateEvent = { + manager_state: "idle", +}; 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 8ac74ed9302a..0efef8c3b5be 100644 --- a/src/panels/config/backup/components/ha-backup-summary-card.ts +++ b/src/panels/config/backup/components/ha-backup-summary-card.ts @@ -25,7 +25,7 @@ const ICONS: Record = { @customElement("ha-backup-summary-card") class HaBackupSummaryCard extends LitElement { @property() - public title!: string; + public heading!: string; @property() public description!: string; @@ -49,7 +49,7 @@ class HaBackupSummaryCard extends LitElement { `}
-

${this.title}

+

${this.heading}

${this.description}

${this.hasAction @@ -116,7 +116,7 @@ class HaBackupSummaryCard extends LitElement { flex: 1; min-width: 0; } - .title { + .heading { font-size: 22px; font-style: normal; font-weight: 400; diff --git a/src/panels/config/backup/components/ha-backup-summary-progress.ts b/src/panels/config/backup/components/ha-backup-summary-progress.ts new file mode 100644 index 000000000000..63a0a9aa6b45 --- /dev/null +++ b/src/panels/config/backup/components/ha-backup-summary-progress.ts @@ -0,0 +1,108 @@ +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"; + +@customElement("ha-backup-summary-progress") +export class HaBackupSummaryProgress 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": + return "Creating backup"; + case "restore_backup": + return "Restoring backup"; + case "receive_backup": + return "Receiving backup"; + default: + return ""; + } + } + + private get _description() { + switch (this.manager.manager_state) { + case "create_backup": + switch (this.manager.stage) { + case "addon_repositories": + case "addons": + return "Backing up add-ons"; + case "await_addon_restarts": + return "Waiting for add-ons to restart"; + case "docker_config": + return "Backing up Docker configuration"; + case "finishing_file": + return "Finishing backup file"; + case "folders": + return "Backing up folders"; + case "home_assistant": + return "Backing up Home Assistant"; + case "upload_to_agents": + return "Uploading to locations"; + default: + return ""; + } + case "restore_backup": + switch (this.manager.stage) { + case "addon_repositories": + case "addons": + return "Restoring add-ons"; + case "await_addon_restarts": + return "Waiting for add-ons to restart"; + case "await_home_assistant_restart": + return "Waiting for Home Assistant to restart"; + case "check_home_assistant": + return "Checking Home Assistant"; + case "docker_config": + return "Restoring Docker configuration"; + case "download_from_agent": + return "Downloading from location"; + case "folders": + return "Restoring folders"; + case "home_assistant": + return "Restoring Home Assistant"; + case "remove_delta_addons": + return "Removing delta add-ons"; + default: + return ""; + } + case "receive_backup": + switch (this.manager.stage) { + case "receive_file": + return "Receiving file"; + case "upload_to_agents": + return "Uploading to locations"; + default: + return ""; + } + default: + return ""; + } + } + + protected render() { + return html` + + + + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-backup-summary-progress": HaBackupSummaryProgress; + } +} diff --git a/src/panels/config/backup/components/ha-backup-summary-status.ts b/src/panels/config/backup/components/ha-backup-summary-status.ts new file mode 100644 index 000000000000..cfb16bac9dc4 --- /dev/null +++ b/src/panels/config/backup/components/ha-backup-summary-status.ts @@ -0,0 +1,84 @@ +import { differenceInDays } from "date-fns"; +import { html, LitElement } from "lit"; +import { customElement, property } from "lit/decorators"; +import memoizeOne from "memoize-one"; +import { formatShortDateTime } from "../../../../common/datetime/format_date_time"; +import type { BackupContent } from "../../../../data/backup"; +import type { ManagerStateEvent } from "../../../../data/backup_manager"; +import type { HomeAssistant } from "../../../../types"; +import "./ha-backup-summary-card"; + +@customElement("ha-backup-summary-status") +export class HaBackupSummaryProgress extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public manager!: ManagerStateEvent; + + @property({ attribute: false }) public backups!: BackupContent[]; + + @property({ type: Boolean, attribute: "has-action" }) + public hasAction = false; + + private _lastBackup = memoizeOne((backups: BackupContent[]) => { + const sortedBackups = backups + // eslint-disable-next-line arrow-body-style + .filter((backup) => { + // TODO : only show backups with default flag + return backup; + }) + .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); + + return sortedBackups[0] as BackupContent | undefined; + }); + + protected render() { + const lastBackup = this._lastBackup(this.backups); + + if (!lastBackup) { + return html` + + + + `; + } + + const lastBackupDate = new Date(lastBackup.date); + const numberOfDays = differenceInDays(new Date(), lastBackupDate); + + // TODO : Improve time format + const description = `Last successful backup ${formatShortDateTime(lastBackupDate, this.hass.locale, this.hass.config)} and synced to ${lastBackup.agent_ids?.length} locations`; + if (numberOfDays > 8) { + return html` + + + + `; + } + return html` + + + + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-backup-summary-status": HaBackupSummaryProgress; + } +} diff --git a/src/panels/config/backup/ha-config-backup-dashboard.ts b/src/panels/config/backup/ha-config-backup-dashboard.ts index 4a91cfb26029..71d697cfe954 100644 --- a/src/panels/config/backup/ha-config-backup-dashboard.ts +++ b/src/panels/config/backup/ha-config-backup-dashboard.ts @@ -10,6 +10,7 @@ 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 { isComponentLoaded } from "../../../common/config/is_component_loaded"; import { relativeTime } from "../../../common/datetime/relative_time"; import type { HASSDomEvent } from "../../../common/dom/fire_event"; import { shouldHandleRequestSelectedEvent } from "../../../common/mwc/handle-request-selected-event"; @@ -40,8 +41,12 @@ import { generateBackupWithStoredSettings, getBackupDownloadUrl, getPreferredAgentForDownload, - subscribeBackupEvents, } from "../../../data/backup"; +import type { ManagerStateEvent } from "../../../data/backup_manager"; +import { + DEFAULT_MANAGER_STATE, + subscribeBackupEvents, +} from "../../../data/backup_manager"; import { extractApiErrorMessage } from "../../../data/hassio/common"; import { showAlertDialog, @@ -55,7 +60,10 @@ 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 "./components/ha-backup-summary-card"; +import "./components/ha-backup-summary-progress"; +import "./components/ha-backup-summary-status"; import { showBackupOnboardingDialog } from "./dialogs/show-dialog-backup_onboarding"; import { showGenerateBackupDialog } from "./dialogs/show-dialog-generate-backup"; import { showNewBackupDialog } from "./dialogs/show-dialog-new-backup"; @@ -69,10 +77,12 @@ class HaConfigBackupDashboard extends SubscribeMixin(LitElement) { @property({ attribute: false }) public route!: Route; - @state() private _backupInProgress = false; + @state() private _manager: ManagerStateEvent = DEFAULT_MANAGER_STATE; @state() private _backups: BackupContent[] = []; + @state() private _fetching = false; + @state() private _selected: string[] = []; @state() private _config?: BackupConfig; @@ -167,6 +177,11 @@ class HaConfigBackupDashboard extends SubscribeMixin(LitElement) { } protected render(): TemplateResult { + const backupInProgress = + "state" in this._manager && this._manager.state === "in_progress"; + + const data: DataTableRowData[] = this._backups; + return html`
- ${this._needsOnboarding + ${this._fetching ? html` Setup backup strategy ` - : html` - - - Configure - - - `} + + Configure + + + ` + : this._needsOnboarding + ? html` + + + Setup backup strategy + + + ` + : html` + + + Configure + + + `}
@@ -277,7 +322,7 @@ class HaConfigBackupDashboard extends SubscribeMixin(LitElement) { { - if (event.event_type === "backup_progress" && event.done) { - this._fetchBackupInfo(); + 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._fetching = true; + this._fetchBackupInfo().then(() => { + this._fetching = false; + }); this._subscribeEvents(); this._fetchBackupConfig(); } @@ -331,7 +399,6 @@ class HaConfigBackupDashboard extends SubscribeMixin(LitElement) { private async _fetchBackupInfo() { const info = await fetchBackupInfo(this.hass); this._backups = info.backups; - this._backupInProgress = info.backing_up; } private async _fetchBackupConfig() { @@ -376,6 +443,12 @@ class HaConfigBackupDashboard extends SubscribeMixin(LitElement) { return; } + if (!isComponentLoaded(this.hass, "hassio")) { + delete params.include_folders; + delete params.include_all_addons; + delete params.include_addons; + } + await generateBackup(this.hass, params); await this._fetchBackupInfo(); return;