Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add overview summary for backups #23343

Merged
merged 6 commits into from
Dec 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading