Add overview summary for backups (#23343)

This commit is contained in:
Paul Bottein 2024-12-19 19:54:50 +01:00 committed by GitHub
parent b693fd1edc
commit 92b02e39c9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 537 additions and 410 deletions

View File

@ -1,5 +1,5 @@
import { import {
mdiAlertCircleCheckOutline, mdiAlertCircleOutline,
mdiAlertOutline, mdiAlertOutline,
mdiCheck, mdiCheck,
mdiInformationOutline, mdiInformationOutline,
@ -16,7 +16,7 @@ type SummaryStatus = "success" | "error" | "info" | "warning" | "loading";
const ICONS: Record<SummaryStatus, string> = { const ICONS: Record<SummaryStatus, string> = {
success: mdiCheck, success: mdiCheck,
error: mdiAlertCircleCheckOutline, error: mdiAlertCircleOutline,
warning: mdiAlertOutline, warning: mdiAlertOutline,
info: mdiInformationOutline, info: mdiInformationOutline,
loading: mdiSync, loading: mdiSync,
@ -60,6 +60,9 @@ class HaBackupSummaryCard extends LitElement {
` `
: nothing} : nothing}
</div> </div>
<div class="content">
<slot></slot>
</div>
</ha-card> </ha-card>
`; `;
} }
@ -71,7 +74,7 @@ class HaBackupSummaryCard extends LitElement {
column-gap: 16px; column-gap: 16px;
row-gap: 8px; row-gap: 8px;
align-items: center; align-items: center;
padding: 20px; padding: 16px;
width: 100%; width: 100%;
box-sizing: border-box; box-sizing: border-box;
} }

View File

@ -1,3 +1,4 @@
import { mdiCalendarSync, mdiGestureTap } from "@mdi/js";
import type { CSSResultGroup } from "lit"; import type { CSSResultGroup } from "lit";
import { css, html, LitElement } from "lit"; import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
@ -62,6 +63,7 @@ class HaBackupOverviewBackups extends LitElement {
<div class="card-content"> <div class="card-content">
<ha-md-list> <ha-md-list>
<ha-md-list-item type="link" href="/config/backup/backups"> <ha-md-list-item type="link" href="/config/backup/backups">
<ha-svg-icon slot="start" .path=${mdiCalendarSync}></ha-svg-icon>
<div slot="headline"> <div slot="headline">
${automaticStats.count} automatic backups ${automaticStats.count} automatic backups
</div> </div>
@ -71,6 +73,7 @@ class HaBackupOverviewBackups extends LitElement {
<ha-icon-next slot="end"></ha-icon-next> <ha-icon-next slot="end"></ha-icon-next>
</ha-md-list-item> </ha-md-list-item>
<ha-md-list-item type="link" href="/config/backup/backups"> <ha-md-list-item type="link" href="/config/backup/backups">
<ha-svg-icon slot="start" .path=${mdiGestureTap}></ha-svg-icon>
<div slot="headline">${manualStats.count} manual backups</div> <div slot="headline">${manualStats.count} manual backups</div>
<div slot="supporting-text"> <div slot="supporting-text">
${bytesToString(manualStats.size, 1)} in total ${bytesToString(manualStats.size, 1)} in total

View File

@ -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`
<ha-card>
<div class="card-header">
<div class="icon">
<ha-svg-icon .path=${mdiInformationOutline}></ha-svg-icon>
</div>
Set up automatic backups
</div>
<div class="card-content">
<p>
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.
</p>
</div>
<div class="card-actions">
<ha-button @click=${this._setup}>
Set up automatic backups
</ha-button>
</div>
</ha-card>
`;
}
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;
}
}

View File

@ -1,18 +1,15 @@
import { html, LitElement } from "lit"; import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import type { ManagerStateEvent } from "../../../../data/backup_manager"; import type { ManagerStateEvent } from "../../../../../data/backup_manager";
import type { HomeAssistant } from "../../../../types"; import type { HomeAssistant } from "../../../../../types";
import "./ha-backup-summary-card"; import "../ha-backup-summary-card";
@customElement("ha-backup-summary-progress") @customElement("ha-backup-overview-progress")
export class HaBackupSummaryProgress extends LitElement { export class HaBackupOverviewProgress extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public manager!: ManagerStateEvent; @property({ attribute: false }) public manager!: ManagerStateEvent;
@property({ type: Boolean, attribute: "has-action" })
public hasAction = false;
private get _heading() { private get _heading() {
switch (this.manager.manager_state) { switch (this.manager.manager_state) {
case "create_backup": case "create_backup":
@ -93,9 +90,7 @@ export class HaBackupSummaryProgress extends LitElement {
.heading=${this._heading} .heading=${this._heading}
.description=${this._description} .description=${this._description}
status="loading" status="loading"
.hasAction=${this.hasAction}
> >
<slot name="action" slot="action"></slot>
</ha-backup-summary-card> </ha-backup-summary-card>
`; `;
} }
@ -103,6 +98,6 @@ export class HaBackupSummaryProgress extends LitElement {
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"ha-backup-summary-progress": HaBackupSummaryProgress; "ha-backup-overview-progress": HaBackupOverviewProgress;
} }
} }

View File

@ -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`
<ha-backup-summary-card
heading="No automatic backup available"
description="You have no automatic backups yet."
status="warning"
>
</ha-backup-summary-card>
`;
}
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`
<ha-backup-summary-card
heading=${`Last automatic backup failed`}
status="error"
>
<ul class="list">
<li class="item">
<ha-svg-icon slot="start" .path=${mdiBackupRestore}></ha-svg-icon>
<span>${lastAttemptDescription}</span>
</li>
<li class="item">
<ha-svg-icon slot="start" .path=${mdiCalendar}></ha-svg-icon>
<span>${lastBackupDescription}</span>
</li>
</ul>
</ha-backup-summary-card>
`;
}
if (numberOfDays > 0) {
return html`
<ha-backup-summary-card
heading=${`No backup for ${numberOfDays} days`}
status="warning"
>
<ul class="list">
<li class="item">
<ha-svg-icon slot="start" .path=${mdiBackupRestore}></ha-svg-icon>
<span>${lastBackupDescription}</span>
</li>
<li class="item">
<ha-svg-icon slot="start" .path=${mdiCalendar}></ha-svg-icon>
<span>${nextBackupDescription}</span>
</li>
</ul>
</ha-backup-summary-card>
`;
}
return html`
<ha-backup-summary-card heading=${`Backed up`} status="success">
<ul class="list">
<li class="item">
<ha-svg-icon slot="start" .path=${mdiBackupRestore}></ha-svg-icon>
<span>${lastBackupDescription}</span>
</li>
<li class="item">
<ha-svg-icon slot="start" .path=${mdiCalendar}></ha-svg-icon>
<span>${nextBackupDescription}</span>
</li>
</ul>
</ha-backup-summary-card>
`;
}
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;
}
}

View File

@ -82,6 +82,8 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
@state() private _step?: Step; @state() private _step?: Step;
@state() private _steps: Step[] = [];
@state() private _params?: BackupOnboardingDialogParams; @state() private _params?: BackupOnboardingDialogParams;
@query("ha-md-dialog") private _dialog!: HaMdDialog; @query("ha-md-dialog") private _dialog!: HaMdDialog;
@ -90,7 +92,8 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
public showDialog(params: BackupOnboardingDialogParams): void { public showDialog(params: BackupOnboardingDialogParams): void {
this._params = params; 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; this._config = RECOMMENDED_CONFIG;
// Enable local location by default // Enable local location by default
@ -117,6 +120,7 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
} }
this._opened = false; this._opened = false;
this._step = undefined; this._step = undefined;
this._steps = [];
this._config = undefined; this._config = undefined;
this._params = undefined; this._params = undefined;
} }
@ -158,19 +162,19 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
} }
private _previousStep() { private _previousStep() {
const index = STEPS.indexOf(this._step!); const index = this._steps.indexOf(this._step!);
if (index === 0) { if (index === 0) {
return; return;
} }
this._step = STEPS[index - 1]; this._step = this._steps[index - 1];
} }
private _nextStep() { private _nextStep() {
const index = STEPS.indexOf(this._step!); const index = this._steps.indexOf(this._step!);
if (index === STEPS.length - 1) { if (index === this._steps.length - 1) {
return; return;
} }
this._step = STEPS[index + 1]; this._step = this._steps[index + 1];
} }
protected render() { protected render() {
@ -178,8 +182,8 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
return nothing; return nothing;
} }
const isLastStep = this._step === STEPS[STEPS.length - 1]; const isLastStep = this._step === this._steps[this._steps.length - 1];
const isFirstStep = this._step === STEPS[0]; const isFirstStep = this._step === this._steps[0];
return html` return html`
<ha-md-dialog disable-cancel-action open @closed=${this.closeDialog}> <ha-md-dialog disable-cancel-action open @closed=${this.closeDialog}>

View File

@ -1,4 +1,4 @@
import { mdiClose, mdiCog, mdiPencil } from "@mdi/js"; import { mdiCalendarSync, mdiClose, mdiGestureTap } from "@mdi/js";
import type { CSSResultGroup } from "lit"; import type { CSSResultGroup } from "lit";
import { LitElement, css, html, nothing } from "lit"; import { LitElement, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators"; 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-button";
import "../../../../components/ha-icon-next"; import "../../../../components/ha-icon-next";
import "../../../../components/ha-md-dialog"; import "../../../../components/ha-md-dialog";
import type { HaMdDialog } from "../../../../components/ha-md-dialog";
import "../../../../components/ha-md-list"; import "../../../../components/ha-md-list";
import "../../../../components/ha-md-list-item"; import "../../../../components/ha-md-list-item";
import "../../../../components/ha-svg-icon"; import "../../../../components/ha-svg-icon";
@ -14,7 +15,6 @@ import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
import { haStyle, haStyleDialog } from "../../../../resources/styles"; import { haStyle, haStyleDialog } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types"; import type { HomeAssistant } from "../../../../types";
import type { NewBackupDialogParams } from "./show-dialog-new-backup"; import type { NewBackupDialogParams } from "./show-dialog-new-backup";
import type { HaMdDialog } from "../../../../components/ha-md-dialog";
@customElement("ha-dialog-new-backup") @customElement("ha-dialog-new-backup")
class DialogNewBackup extends LitElement implements HassDialog { class DialogNewBackup extends LitElement implements HassDialog {
@ -75,7 +75,7 @@ class DialogNewBackup extends LitElement implements HassDialog {
type="button" type="button"
.disabled=${!this._params.config.create_backup.password} .disabled=${!this._params.config.create_backup.password}
> >
<ha-svg-icon slot="start" .path=${mdiCog}></ha-svg-icon> <ha-svg-icon slot="start" .path=${mdiCalendarSync}></ha-svg-icon>
<span slot="headline">Automatic backup</span> <span slot="headline">Automatic backup</span>
<span slot="supporting-text"> <span slot="supporting-text">
Create a backup with the data and locations you have configured. Create a backup with the data and locations you have configured.
@ -83,7 +83,7 @@ class DialogNewBackup extends LitElement implements HassDialog {
<ha-icon-next slot="end"></ha-icon-next> <ha-icon-next slot="end"></ha-icon-next>
</ha-md-list-item> </ha-md-list-item>
<ha-md-list-item @click=${this._manual} type="button"> <ha-md-list-item @click=${this._manual} type="button">
<ha-svg-icon slot="start" .path=${mdiPencil}></ha-svg-icon> <ha-svg-icon slot="start" .path=${mdiGestureTap}></ha-svg-icon>
<span slot="headline">Manual backup</span> <span slot="headline">Manual backup</span>
<span slot="supporting-text"> <span slot="supporting-text">
Select data and locations for a manual backup. Select data and locations for a manual backup.

View File

@ -4,6 +4,7 @@ import type { CloudStatus } from "../../../../data/cloud";
export interface BackupOnboardingDialogParams { export interface BackupOnboardingDialogParams {
submit?: (value: boolean) => void; submit?: (value: boolean) => void;
cancel?: () => void; cancel?: () => void;
showIntro?: boolean;
cloudStatus: CloudStatus; cloudStatus: CloudStatus;
} }

View File

@ -6,14 +6,14 @@ import {
mdiPlus, mdiPlus,
mdiUpload, mdiUpload,
} from "@mdi/js"; } from "@mdi/js";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit"; import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit"; import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { relativeTime } from "../../../common/datetime/relative_time"; import { relativeTime } from "../../../common/datetime/relative_time";
import { storage } from "../../../common/decorators/storage"; 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 { computeDomain } from "../../../common/entity/compute_domain";
import { shouldHandleRequestSelectedEvent } from "../../../common/mwc/handle-request-selected-event"; import { shouldHandleRequestSelectedEvent } from "../../../common/mwc/handle-request-selected-event";
import { navigate } from "../../../common/navigate"; import { navigate } from "../../../common/navigate";
@ -36,11 +36,8 @@ import "../../../components/ha-svg-icon";
import { getSignedPath } from "../../../data/auth"; import { getSignedPath } from "../../../data/auth";
import type { BackupConfig, BackupContent } from "../../../data/backup"; import type { BackupConfig, BackupContent } from "../../../data/backup";
import { import {
compareAgents,
computeBackupAgentName, computeBackupAgentName,
deleteBackup, deleteBackup,
fetchBackupConfig,
fetchBackupInfo,
generateBackup, generateBackup,
generateBackupWithAutomaticSettings, generateBackupWithAutomaticSettings,
getBackupDownloadUrl, getBackupDownloadUrl,
@ -48,10 +45,6 @@ import {
isLocalAgent, isLocalAgent,
} from "../../../data/backup"; } from "../../../data/backup";
import type { ManagerStateEvent } from "../../../data/backup_manager"; import type { ManagerStateEvent } from "../../../data/backup_manager";
import {
DEFAULT_MANAGER_STATE,
subscribeBackupEvents,
} from "../../../data/backup_manager";
import type { CloudStatus } from "../../../data/cloud"; import type { CloudStatus } from "../../../data/cloud";
import { extractApiErrorMessage } from "../../../data/hassio/common"; import { extractApiErrorMessage } from "../../../data/hassio/common";
import { import {
@ -66,7 +59,6 @@ import type { HomeAssistant, Route } from "../../../types";
import { brandsUrl } from "../../../util/brands-url"; import { brandsUrl } from "../../../util/brands-url";
import { bytesToString } from "../../../util/bytes-to-string"; import { bytesToString } from "../../../util/bytes-to-string";
import { fileDownload } from "../../../util/file_download"; import { fileDownload } from "../../../util/file_download";
import { showToast } from "../../../util/toast";
import { showGenerateBackupDialog } from "./dialogs/show-dialog-generate-backup"; import { showGenerateBackupDialog } from "./dialogs/show-dialog-generate-backup";
import { showNewBackupDialog } from "./dialogs/show-dialog-new-backup"; import { showNewBackupDialog } from "./dialogs/show-dialog-new-backup";
import { showUploadBackupDialog } from "./dialogs/show-dialog-upload-backup"; import { showUploadBackupDialog } from "./dialogs/show-dialog-upload-backup";
@ -89,14 +81,14 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public route!: Route; @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[] = [];
@property({ attribute: false }) public config?: BackupConfig;
@state() private _selected: string[] = []; @state() private _selected: string[] = [];
@state() private _config?: BackupConfig;
@storage({ key: "backups-table-grouping", state: false, subscribe: false }) @storage({ key: "backups-table-grouping", state: false, subscribe: false })
private _activeGrouping?: string = "formatted_type"; private _activeGrouping?: string = "formatted_type";
@ -107,8 +99,6 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
}) })
private _activeCollapsed: string[] = []; private _activeCollapsed: string[] = [];
private _subscribed?: Promise<() => void>;
@query("hass-tabs-subpage-data-table", true) @query("hass-tabs-subpage-data-table", true)
private _dataTable!: HaTabsSubpageDataTable; private _dataTable!: HaTabsSubpageDataTable;
@ -251,7 +241,7 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
protected render(): TemplateResult { protected render(): TemplateResult {
const backupInProgress = const backupInProgress =
"state" in this._manager && this._manager.state === "in_progress"; "state" in this.manager && this.manager.state === "in_progress";
return html` return html`
<hass-tabs-subpage-data-table <hass-tabs-subpage-data-table
@ -278,7 +268,7 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
.route=${this.route} .route=${this.route}
@row-click=${this._showBackupDetails} @row-click=${this._showBackupDetails}
.columns=${this._columns(this.hass.localize)} .columns=${this._columns(this.hass.localize)}
.data=${this._data(this._backups)} .data=${this._data(this.backups)}
.noDataText=${this.hass.localize("ui.panel.config.backup.no_backups")} .noDataText=${this.hass.localize("ui.panel.config.backup.no_backups")}
.searchLabel=${this.hass.localize( .searchLabel=${this.hass.localize(
"ui.panel.config.backup.picker.search" "ui.panel.config.backup.picker.search"
@ -351,82 +341,8 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
`; `;
} }
private _unsubscribeEvents() {
if (this._subscribed) {
this._subscribed.then((unsub) => 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() { private get _needsOnboarding() {
return !this._config?.create_backup.password; return !this.config?.create_backup.password;
} }
private async _uploadBackup(ev) { private async _uploadBackup(ev) {
@ -438,7 +354,7 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
} }
private async _newBackup(): Promise<void> { private async _newBackup(): Promise<void> {
const config = this._config!; const config = this.config!;
const type = await showNewBackupDialog(this, { config }); const type = await showNewBackupDialog(this, { config });
@ -454,12 +370,12 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
} }
await generateBackup(this.hass, params); await generateBackup(this.hass, params);
await this._fetchBackupInfo(); fireEvent(this, "ha-refresh-backup-info");
return; return;
} }
if (type === "automatic") { if (type === "automatic") {
await generateBackupWithAutomaticSettings(this.hass); 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); await deleteBackup(this.hass, backup.backup_id);
this._fetchBackupInfo(); fireEvent(this, "ha-refresh-backup-info");
} }
private async _deleteSelected() { private async _deleteSelected() {
@ -516,7 +432,7 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
}); });
return; return;
} }
await this._fetchBackupInfo(); fireEvent(this, "ha-refresh-backup-info");
this._dataTable.clearSelection(); this._dataTable.clearSelection();
} }

View File

@ -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`
<hass-subpage
back-path="/config/backup"
.hass=${this.hass}
.narrow=${this.narrow}
.header=${this.hass.localize("ui.panel.config.backup.caption")}
>
<div class="content">
<div class="header">
<h2 class="title">Locations</h2>
<p class="description">
To keep your data safe it is recommended your backups is at least
on two different locations and one of them is off-site.
</p>
</div>
<ha-card class="agents">
<div class="card-content">
${this._agents.length > 0
? html`
<ha-md-list>
${this._agents.map((agent) => {
const [domain, name] = agent.agent_id.split(".");
const domainName = domainToName(
this.hass.localize,
domain
);
return html`
<ha-md-list-item
type="link"
href="/config/backup/locations/${agent.agent_id}"
>
<img
.src=${brandsUrl({
domain,
type: "icon",
useFallback: true,
darkOptimized: this.hass.themes?.darkMode,
})}
crossorigin="anonymous"
referrerpolicy="no-referrer"
alt=""
slot="start"
/>
<div slot="headline">${domainName}: ${name}</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-md-list-item>
`;
})}
</ha-md-list>
`
: html`<p>No sync agents configured</p>`}
</div>
</ha-card>
</div>
</hass-subpage>
`;
}
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;
}
}

View File

@ -1,7 +1,8 @@
import { mdiDotsVertical, mdiPlus, mdiUpload } from "@mdi/js"; 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 { 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 { shouldHandleRequestSelectedEvent } from "../../../common/mwc/handle-request-selected-event";
import "../../../components/ha-button"; import "../../../components/ha-button";
import "../../../components/ha-button-menu"; import "../../../components/ha-button-menu";
@ -13,25 +14,24 @@ import "../../../components/ha-icon-overflow-menu";
import "../../../components/ha-list-item"; import "../../../components/ha-list-item";
import "../../../components/ha-svg-icon"; import "../../../components/ha-svg-icon";
import { import {
fetchBackupConfig,
fetchBackupInfo,
generateBackup, generateBackup,
generateBackupWithAutomaticSettings, generateBackupWithAutomaticSettings,
type BackupConfig, type BackupConfig,
type BackupContent, type BackupContent,
} from "../../../data/backup"; } from "../../../data/backup";
import type { ManagerStateEvent } from "../../../data/backup_manager"; import type { ManagerStateEvent } from "../../../data/backup_manager";
import { DEFAULT_MANAGER_STATE } from "../../../data/backup_manager";
import type { CloudStatus } from "../../../data/cloud"; import type { CloudStatus } from "../../../data/cloud";
import "../../../layouts/hass-subpage"; import "../../../layouts/hass-subpage";
import "../../../layouts/hass-tabs-subpage-data-table"; import "../../../layouts/hass-tabs-subpage-data-table";
import { haStyle } from "../../../resources/styles"; import { haStyle } from "../../../resources/styles";
import type { HomeAssistant, Route } from "../../../types"; import type { HomeAssistant, Route } from "../../../types";
import "./components/ha-backup-summary-card"; import "./components/ha-backup-summary-card";
import "./components/ha-backup-summary-progress";
import "./components/ha-backup-summary-status"; import "./components/ha-backup-summary-status";
import "./components/overview/ha-backup-overview-backups"; 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-settings";
import "./components/overview/ha-backup-overview-summary";
import { showBackupOnboardingDialog } from "./dialogs/show-dialog-backup_onboarding"; import { showBackupOnboardingDialog } from "./dialogs/show-dialog-backup_onboarding";
import { showGenerateBackupDialog } from "./dialogs/show-dialog-generate-backup"; import { showGenerateBackupDialog } from "./dialogs/show-dialog-generate-backup";
import { showNewBackupDialog } from "./dialogs/show-dialog-new-backup"; import { showNewBackupDialog } from "./dialogs/show-dialog-new-backup";
@ -47,27 +47,13 @@ class HaConfigBackupOverview extends LitElement {
@property({ attribute: false }) public route!: Route; @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; @property({ attribute: false }) public config?: BackupConfig;
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
this._fetchBackupInfo();
this._fetchBackupConfig();
}
public connectedCallback() {
super.connectedCallback();
if (this.hasUpdated) {
this._fetchBackupInfo();
this._fetchBackupConfig();
}
}
private async _uploadBackup(ev) { private async _uploadBackup(ev) {
if (!shouldHandleRequestSelectedEvent(ev)) { if (!shouldHandleRequestSelectedEvent(ev)) {
@ -77,40 +63,36 @@ class HaConfigBackupOverview extends LitElement {
await showUploadBackupDialog(this, {}); await showUploadBackupDialog(this, {});
} }
private async _setupAutomaticBackup() { private _handleOnboardingButtonClick(ev) {
ev.stopPropagation();
this._setupAutomaticBackup(false);
}
private async _setupAutomaticBackup(showIntro: boolean) {
const success = await showBackupOnboardingDialog(this, { const success = await showBackupOnboardingDialog(this, {
cloudStatus: this.cloudStatus, cloudStatus: this.cloudStatus,
showIntro: showIntro,
}); });
if (!success) { if (!success) {
return; return;
} }
this._fetchBackupConfig(); fireEvent(this, "ha-refresh-backup-config");
await generateBackupWithAutomaticSettings(this.hass); await generateBackupWithAutomaticSettings(this.hass);
await this._fetchBackupInfo(); fireEvent(this, "ha-refresh-backup-info");
}
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;
} }
private async _newBackup(): Promise<void> { private async _newBackup(): Promise<void> {
if (this._needsOnboarding) { if (this._needsOnboarding) {
this._setupAutomaticBackup(); this._setupAutomaticBackup(true);
return; return;
} }
if (!this._config) { if (!this.config) {
return; return;
} }
const config = this._config; const config = this.config;
const type = await showNewBackupDialog(this, { config }); const type = await showNewBackupDialog(this, { config });
@ -126,22 +108,22 @@ class HaConfigBackupOverview extends LitElement {
} }
await generateBackup(this.hass, params); await generateBackup(this.hass, params);
await this._fetchBackupInfo(); fireEvent(this, "ha-refresh-backup-info");
return; return;
} }
if (type === "automatic") { if (type === "automatic") {
await generateBackupWithAutomaticSettings(this.hass); await generateBackupWithAutomaticSettings(this.hass);
await this._fetchBackupInfo(); fireEvent(this, "ha-refresh-backup-info");
} }
} }
private get _needsOnboarding() { private get _needsOnboarding() {
return !this._config?.create_backup.password; return !this.config?.create_backup.password;
} }
protected render(): TemplateResult { protected render(): TemplateResult {
const backupInProgress = const backupInProgress =
"state" in this._manager && this._manager.state === "in_progress"; "state" in this.manager && this.manager.state === "in_progress";
return html` return html`
<hass-subpage <hass-subpage
@ -167,7 +149,15 @@ class HaConfigBackupOverview extends LitElement {
</ha-button-menu> </ha-button-menu>
</div> </div>
<div class="content"> <div class="content">
${this._fetching ${backupInProgress
? html`
<ha-backup-overview-progress
.hass=${this.hass}
.manager=${this.manager}
>
</ha-backup-overview-progress>
`
: this.fetching
? html` ? html`
<ha-backup-summary-card <ha-backup-summary-card
heading="Loading backups" heading="Loading backups"
@ -176,48 +166,33 @@ class HaConfigBackupOverview extends LitElement {
> >
</ha-backup-summary-card> </ha-backup-summary-card>
` `
: backupInProgress
? html`
<ha-backup-summary-progress
.hass=${this.hass}
.manager=${this._manager}
>
</ha-backup-summary-progress>
`
: this._needsOnboarding : this._needsOnboarding
? html` ? html`
<ha-backup-summary-card <ha-backup-overview-onboarding
heading="Configure automatic backups" .hass=${this.hass}
description="Have a one-click backup automation with selected data and locations." @button-click=${this._handleOnboardingButtonClick}
has-action
status="info"
> >
<ha-button </ha-backup-overview-onboarding>
slot="action"
@click=${this._setupAutomaticBackup}
>
Set up automatic backups
</ha-button>
</ha-backup-summary-card>
` `
: html` : html`
<ha-backup-summary-status <ha-backup-overview-summary
.hass=${this.hass} .hass=${this.hass}
.backups=${this._backups} .backups=${this.backups}
.config=${this.config}
> >
</ha-backup-summary-status> </ha-backup-overview-summary>
`} `}
<ha-backup-overview-backups <ha-backup-overview-backups
.hass=${this.hass} .hass=${this.hass}
.backups=${this._backups} .backups=${this.backups}
></ha-backup-overview-backups> ></ha-backup-overview-backups>
${!this._needsOnboarding ${!this._needsOnboarding
? html` ? html`
<ha-backup-overview-settings <ha-backup-overview-settings
.hass=${this.hass} .hass=${this.hass}
.config=${this._config!} .config=${this.config!}
></ha-backup-overview-settings> ></ha-backup-overview-settings>
` `
: nothing} : nothing}
@ -258,6 +233,10 @@ class HaConfigBackupOverview extends LitElement {
padding-left: 0; padding-left: 0;
padding-right: 0; padding-right: 0;
} }
ha-fab[disabled] {
--mdc-theme-secondary: var(--disabled-text-color) !important;
pointer-events: none;
}
`, `,
]; ];
} }

View File

@ -1,17 +1,14 @@
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit"; import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import { debounce } from "../../../common/util/debounce"; import { debounce } from "../../../common/util/debounce";
import "../../../components/ha-button"; import "../../../components/ha-button";
import "../../../components/ha-card"; import "../../../components/ha-card";
import "../../../components/ha-icon-next"; import "../../../components/ha-icon-next";
import "../../../components/ha-password-field"; import "../../../components/ha-password-field";
import "../../../components/ha-settings-row";
import type { BackupConfig } from "../../../data/backup"; import type { BackupConfig } from "../../../data/backup";
import { import { updateBackupConfig } from "../../../data/backup";
BackupScheduleState,
fetchBackupConfig,
updateBackupConfig,
} from "../../../data/backup";
import type { CloudStatus } from "../../../data/cloud"; import type { CloudStatus } from "../../../data/cloud";
import "../../../layouts/hass-subpage"; import "../../../layouts/hass-subpage";
import type { HomeAssistant } from "../../../types"; 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 "./components/config/ha-backup-config-schedule";
import type { BackupConfigSchedule } from "./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") @customElement("ha-config-backup-settings")
class HaConfigBackupSettings extends LitElement { class HaConfigBackupSettings extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@ -51,22 +27,19 @@ class HaConfigBackupSettings extends LitElement {
@property({ type: Boolean }) public narrow = false; @property({ type: Boolean }) public narrow = false;
@state() private _backupConfig: BackupConfig = INITIAL_BACKUP_CONFIG; @property({ attribute: false }) public config?: BackupConfig;
protected willUpdate(changedProps) { @state() private _config?: BackupConfig;
super.willUpdate(changedProps);
if (!this.hasUpdated) {
this._fetchData();
}
}
private async _fetchData() { protected willUpdate(changedProperties: PropertyValues): void {
const { config } = await fetchBackupConfig(this.hass); super.willUpdate(changedProperties);
this._backupConfig = config; if (changedProperties.has("config") && !this._config) {
this._config = this.config;
}
} }
protected render() { protected render() {
if (!this._backupConfig) { if (!this._config) {
return nothing; return nothing;
} }
@ -87,7 +60,7 @@ class HaConfigBackupSettings extends LitElement {
</p> </p>
<ha-backup-config-schedule <ha-backup-config-schedule
.hass=${this.hass} .hass=${this.hass}
.value=${this._backupConfig} .value=${this._config}
@value-changed=${this._scheduleConfigChanged} @value-changed=${this._scheduleConfigChanged}
></ha-backup-config-schedule> ></ha-backup-config-schedule>
</div> </div>
@ -113,7 +86,7 @@ class HaConfigBackupSettings extends LitElement {
</p> </p>
<ha-backup-config-agents <ha-backup-config-agents
.hass=${this.hass} .hass=${this.hass}
.value=${this._backupConfig.create_backup.agent_ids} .value=${this._config.create_backup.agent_ids}
.cloudStatus=${this.cloudStatus} .cloudStatus=${this.cloudStatus}
@value-changed=${this._agentsConfigChanged} @value-changed=${this._agentsConfigChanged}
></ha-backup-config-agents> ></ha-backup-config-agents>
@ -130,7 +103,7 @@ class HaConfigBackupSettings extends LitElement {
</p> </p>
<ha-backup-config-encryption-key <ha-backup-config-encryption-key
.hass=${this.hass} .hass=${this.hass}
.value=${this._backupConfig.create_backup.password} .value=${this._config.create_backup.password}
@value-changed=${this._encryptionKeyChanged} @value-changed=${this._encryptionKeyChanged}
></ha-backup-config-encryption-key> ></ha-backup-config-encryption-key>
</div> </div>
@ -142,8 +115,8 @@ class HaConfigBackupSettings extends LitElement {
private _scheduleConfigChanged(ev) { private _scheduleConfigChanged(ev) {
const value = ev.detail.value as BackupConfigSchedule; const value = ev.detail.value as BackupConfigSchedule;
this._backupConfig = { this._config = {
...this._backupConfig, ...this._config!,
schedule: value.schedule, schedule: value.schedule,
retention: value.retention, retention: value.retention,
}; };
@ -156,7 +129,7 @@ class HaConfigBackupSettings extends LitElement {
include_all_addons, include_all_addons,
include_database, include_database,
include_folders, include_folders,
} = this._backupConfig.create_backup; } = this._config!.create_backup;
return { return {
include_homeassistant: true, include_homeassistant: true,
@ -169,10 +142,10 @@ class HaConfigBackupSettings extends LitElement {
private _dataConfigChanged(ev) { private _dataConfigChanged(ev) {
const data = ev.detail.value as BackupConfigData; const data = ev.detail.value as BackupConfigData;
this._backupConfig = { this._config = {
...this._backupConfig, ...this._config!,
create_backup: { create_backup: {
...this._backupConfig.create_backup, ...this.config!.create_backup,
include_database: data.include_database, include_database: data.include_database,
include_folders: data.include_folders || null, include_folders: data.include_folders || null,
include_all_addons: data.include_all_addons, include_all_addons: data.include_all_addons,
@ -184,10 +157,10 @@ class HaConfigBackupSettings extends LitElement {
private _agentsConfigChanged(ev) { private _agentsConfigChanged(ev) {
const agents = ev.detail.value as string[]; const agents = ev.detail.value as string[];
this._backupConfig = { this._config = {
...this._backupConfig, ...this._config!,
create_backup: { create_backup: {
...this._backupConfig.create_backup, ...this._config!.create_backup,
agent_ids: agents, agent_ids: agents,
}, },
}; };
@ -196,10 +169,10 @@ class HaConfigBackupSettings extends LitElement {
private _encryptionKeyChanged(ev) { private _encryptionKeyChanged(ev) {
const password = ev.detail.value as string; const password = ev.detail.value as string;
this._backupConfig = { this._config = {
...this._backupConfig, ...this._config!,
create_backup: { create_backup: {
...this._backupConfig.create_backup, ...this._config!.create_backup,
password: password, password: password,
}, },
}; };
@ -211,16 +184,17 @@ class HaConfigBackupSettings extends LitElement {
private async _save() { private async _save() {
await updateBackupConfig(this.hass, { await updateBackupConfig(this.hass, {
create_backup: { create_backup: {
agent_ids: this._backupConfig.create_backup.agent_ids, agent_ids: this._config!.create_backup.agent_ids,
include_folders: this._backupConfig.create_backup.include_folders ?? [], include_folders: this._config!.create_backup.include_folders ?? [],
include_database: this._backupConfig.create_backup.include_database, include_database: this._config!.create_backup.include_database,
include_addons: this._backupConfig.create_backup.include_addons ?? [], include_addons: this._config!.create_backup.include_addons ?? [],
include_all_addons: this._backupConfig.create_backup.include_all_addons, include_all_addons: this._config!.create_backup.include_all_addons,
password: this._backupConfig.create_backup.password, password: this._config!.create_backup.password,
}, },
retention: this._backupConfig.retention, retention: this._config!.retention,
schedule: this._backupConfig.schedule.state, schedule: this._config!.schedule.state,
}); });
fireEvent(this, "ha-refresh-backup-config");
} }
static styles = css` static styles = css`
@ -233,14 +207,6 @@ class HaConfigBackupSettings extends LitElement {
flex-direction: column; flex-direction: column;
margin-bottom: 24px; 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 { .alert {
--mdc-theme-primary: var(--error-color); --mdc-theme-primary: var(--error-color);
} }

View File

@ -1,21 +1,89 @@
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { PropertyValues } from "lit"; 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 { CloudStatus } from "../../../data/cloud";
import type { RouterOptions } from "../../../layouts/hass-router-page"; import type { RouterOptions } from "../../../layouts/hass-router-page";
import { HassRouterPage } from "../../../layouts/hass-router-page"; import { HassRouterPage } from "../../../layouts/hass-router-page";
import "../../../layouts/hass-tabs-subpage-data-table"; import "../../../layouts/hass-tabs-subpage-data-table";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import "./ha-config-backup-overview"; import { showToast } from "../../../util/toast";
import "./ha-config-backup-backups"; 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") @customElement("ha-config-backup")
class HaConfigBackup extends HassRouterPage { class HaConfigBackup extends SubscribeMixin(HassRouterPage) {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public cloudStatus!: CloudStatus; @property({ attribute: false }) public cloudStatus!: CloudStatus;
@property({ type: Boolean }) public narrow = false; @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 = { protected routerOptions: RouterOptions = {
defaultPage: "overview", defaultPage: "overview",
routes: { routes: {
@ -31,10 +99,6 @@ class HaConfigBackup extends HassRouterPage {
tag: "ha-config-backup-details", tag: "ha-config-backup-details",
load: () => import("./ha-config-backup-details"), load: () => import("./ha-config-backup-details"),
}, },
locations: {
tag: "ha-config-backup-locations",
load: () => import("./ha-config-backup-locations"),
},
settings: { settings: {
tag: "ha-config-backup-settings", tag: "ha-config-backup-settings",
load: () => import("./ha-config-backup-settings"), load: () => import("./ha-config-backup-settings"),
@ -47,7 +111,12 @@ class HaConfigBackup extends HassRouterPage {
pageEl.route = this.routeTail; pageEl.route = this.routeTail;
pageEl.narrow = this.narrow; pageEl.narrow = this.narrow;
pageEl.cloudStatus = this.cloudStatus; pageEl.cloudStatus = this.cloudStatus;
pageEl.manager = this._manager;
pageEl.backups = this._backups;
pageEl.config = this._config;
pageEl.fetching = this._fetching;
pageEl.addEventListener("reload", () => {});
if ( if (
(!changedProps || changedProps.has("route")) && (!changedProps || changedProps.has("route")) &&
this._currentPage === "details" this._currentPage === "details"
@ -55,6 +124,36 @@ class HaConfigBackup extends HassRouterPage {
pageEl.backupId = this.routeTail.path.substr(1); pageEl.backupId = this.routeTail.path.substr(1);
} }
} }
public hassSubscribe(): Promise<UnsubscribeFunc>[] {
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 { declare global {