Skip to content

Commit

Permalink
Add overview summary for backups (#23343)
Browse files Browse the repository at this point in the history
  • Loading branch information
piitaya authored Dec 19, 2024
1 parent b693fd1 commit 92b02e3
Show file tree
Hide file tree
Showing 13 changed files with 537 additions and 410 deletions.
9 changes: 6 additions & 3 deletions src/panels/config/backup/components/ha-backup-summary-card.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {
mdiAlertCircleCheckOutline,
mdiAlertCircleOutline,
mdiAlertOutline,
mdiCheck,
mdiInformationOutline,
Expand All @@ -16,7 +16,7 @@ type SummaryStatus = "success" | "error" | "info" | "warning" | "loading";

const ICONS: Record<SummaryStatus, string> = {
success: mdiCheck,
error: mdiAlertCircleCheckOutline,
error: mdiAlertCircleOutline,
warning: mdiAlertOutline,
info: mdiInformationOutline,
loading: mdiSync,
Expand Down Expand Up @@ -60,6 +60,9 @@ class HaBackupSummaryCard extends LitElement {
`
: nothing}
</div>
<div class="content">
<slot></slot>
</div>
</ha-card>
`;
}
Expand All @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -62,6 +63,7 @@ class HaBackupOverviewBackups extends LitElement {
<div class="card-content">
<ha-md-list>
<ha-md-list-item type="link" href="/config/backup/backups">
<ha-svg-icon slot="start" .path=${mdiCalendarSync}></ha-svg-icon>
<div slot="headline">
${automaticStats.count} automatic backups
</div>
Expand All @@ -71,6 +73,7 @@ class HaBackupOverviewBackups extends LitElement {
<ha-icon-next slot="end"></ha-icon-next>
</ha-md-list-item>
<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="supporting-text">
${bytesToString(manualStats.size, 1)} in total
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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":
Expand Down Expand Up @@ -93,16 +90,14 @@ export class HaBackupSummaryProgress extends LitElement {
.heading=${this._heading}
.description=${this._description}
status="loading"
.hasAction=${this.hasAction}
>
<slot name="action" slot="action"></slot>
</ha-backup-summary-card>
`;
}
}

declare global {
interface HTMLElementTagNameMap {
"ha-backup-summary-progress": HaBackupSummaryProgress;
"ha-backup-overview-progress": HaBackupOverviewProgress;
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading

0 comments on commit 92b02e3

Please sign in to comment.