Backup status banner improvement (#23400)

* Improve overdue backups and don't consider backup with failed agents as success

* Improve margin and loader

* Improve margin

* Show agent name if only one off site location is configured
This commit is contained in:
Paul Bottein 2024-12-23 15:06:36 +01:00 committed by GitHub
parent 76cb9ce807
commit 5fb384ad31
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 141 additions and 160 deletions

View File

@ -75,6 +75,7 @@ class HaBackupSummaryCard extends LitElement {
row-gap: 8px; row-gap: 8px;
align-items: center; align-items: center;
padding: 16px; padding: 16px;
padding-bottom: 8px;
width: 100%; width: 100%;
box-sizing: border-box; box-sizing: border-box;
} }
@ -145,10 +146,6 @@ class HaBackupSummaryCard extends LitElement {
} }
@media all and (max-width: 550px) { @media all and (max-width: 550px) {
.summary {
flex-wrap: wrap;
padding: 8px;
}
.action { .action {
width: 100%; width: 100%;
display: flex; display: flex;

View File

@ -1,84 +0,0 @@
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.with_automatic_settings;
})
.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`
<ha-backup-summary-card
heading="No backup available"
description="You have not created any backups yet."
.hasAction=${this.hasAction}
status="warning"
>
<slot name="action" slot="action"></slot>
</ha-backup-summary-card>
`;
}
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`
<ha-backup-summary-card
heading=${`No backup for ${numberOfDays} days`}
description=${description}
.hasAction=${this.hasAction}
status="warning"
>
<slot name="action" slot="action"></slot>
</ha-backup-summary-card>
`;
}
return html`
<ha-backup-summary-card
heading=${`Backed up`}
description=${description}
.hasAction=${this.hasAction}
status="success"
>
<slot name="action" slot="action"></slot>
</ha-backup-summary-card>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-backup-summary-status": HaBackupSummaryProgress;
}
}

View File

@ -109,9 +109,13 @@ class HaBackupOverviewBackups extends LitElement {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
} }
.card-header {
padding-bottom: 8px;
}
.card-content { .card-content {
padding-left: 0; padding-left: 0;
padding-right: 0; padding-right: 0;
padding-bottom: 0;
} }
`, `,
]; ];

View File

@ -10,7 +10,11 @@ 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";
import type { BackupConfig } from "../../../../../data/backup"; import type { BackupConfig } from "../../../../../data/backup";
import { BackupScheduleState, isLocalAgent } from "../../../../../data/backup"; import {
BackupScheduleState,
computeBackupAgentName,
isLocalAgent,
} from "../../../../../data/backup";
import { haStyle } from "../../../../../resources/styles"; import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant } from "../../../../../types"; import type { HomeAssistant } from "../../../../../types";
@ -88,6 +92,14 @@ class HaBackupBackupsSummary extends LitElement {
); );
if (offsiteLocations.length) { if (offsiteLocations.length) {
if (offsiteLocations.length === 1) {
const name = computeBackupAgentName(
this.hass.localize,
offsiteLocations[0],
offsiteLocations
);
return `Upload to ${name}`;
}
return `Upload to ${offsiteLocations.length} off-site locations`; return `Upload to ${offsiteLocations.length} off-site locations`;
} }
if (hasLocal) { if (hasLocal) {
@ -184,9 +196,13 @@ class HaBackupBackupsSummary extends LitElement {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
} }
.card-header {
padding-bottom: 8px;
}
.card-content { .card-content {
padding-left: 0; padding-left: 0;
padding-right: 0; padding-right: 0;
padding-bottom: 0;
} }
`, `,
]; ];

View File

@ -1,5 +1,5 @@
import { mdiBackupRestore, mdiCalendar } from "@mdi/js"; import { mdiBackupRestore, mdiCalendar } from "@mdi/js";
import { differenceInDays, setHours, setMinutes } from "date-fns"; import { addHours, differenceInDays, setHours, setMinutes } from "date-fns";
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";
@ -17,6 +17,8 @@ import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant } from "../../../../../types"; import type { HomeAssistant } from "../../../../../types";
import "../ha-backup-summary-card"; import "../ha-backup-summary-card";
const OVERDUE_MARGIN_HOURS = 3;
@customElement("ha-backup-overview-summary") @customElement("ha-backup-overview-summary")
class HaBackupOverviewBackups extends LitElement { class HaBackupOverviewBackups extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@ -25,9 +27,14 @@ class HaBackupOverviewBackups extends LitElement {
@property({ attribute: false }) public config!: BackupConfig; @property({ attribute: false }) public config!: BackupConfig;
@property({ type: Boolean }) public fetching = false;
private _lastBackup = memoizeOne((backups: BackupContent[]) => { private _lastBackup = memoizeOne((backups: BackupContent[]) => {
const sortedBackups = backups const sortedBackups = backups
.filter((backup) => backup.with_automatic_settings) .filter(
(backup) =>
backup.with_automatic_settings && !backup.failed_agent_ids?.length
)
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
return sortedBackups[0] as BackupContent | undefined; return sortedBackups[0] as BackupContent | undefined;
@ -60,6 +67,23 @@ class HaBackupOverviewBackups extends LitElement {
} }
protected render() { protected render() {
if (this.fetching) {
return html`
<ha-backup-summary-card heading="Loading backups" status="loading">
<ha-md-list>
<ha-md-list-item>
<ha-svg-icon slot="start" .path=${mdiBackupRestore}></ha-svg-icon>
<span slot="headline" class="skeleton"></span>
</ha-md-list-item>
<ha-md-list-item>
<ha-svg-icon slot="start" .path=${mdiCalendar}></ha-svg-icon>
<span slot="headline" class="skeleton"></span>
</ha-md-list-item>
</ha-md-list>
</ha-backup-summary-card>
`;
}
const lastBackup = this._lastBackup(this.backups); const lastBackup = this._lastBackup(this.backups);
if (!lastBackup) { if (!lastBackup) {
@ -75,10 +99,9 @@ class HaBackupOverviewBackups extends LitElement {
const lastBackupDate = new Date(lastBackup.date); const lastBackupDate = new Date(lastBackup.date);
const numberOfDays = differenceInDays(new Date(), lastBackupDate);
const now = new Date(); 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 lastBackupDescription = `Last successful backup ${relativeTime(lastBackupDate, this.hass.locale, now, true)} and stored to ${lastBackup.agent_ids?.length} locations.`;
const nextBackupDescription = this._nextBackupDescription( const nextBackupDescription = this._nextBackupDescription(
this.config.schedule.state this.config.schedule.state
); );
@ -94,51 +117,62 @@ class HaBackupOverviewBackups extends LitElement {
heading=${`Last automatic backup failed`} heading=${`Last automatic backup failed`}
status="error" status="error"
> >
<ul class="list"> <ha-md-list>
<li class="item"> <ha-md-list-item>
<ha-svg-icon slot="start" .path=${mdiBackupRestore}></ha-svg-icon> <ha-svg-icon slot="start" .path=${mdiBackupRestore}></ha-svg-icon>
<span>${lastAttemptDescription}</span> <span slot="headline">${lastAttemptDescription}</span>
</li> </ha-md-list-item>
<li class="item"> <ha-md-list-item>
<ha-svg-icon slot="start" .path=${mdiCalendar}></ha-svg-icon> <ha-svg-icon slot="start" .path=${mdiCalendar}></ha-svg-icon>
<span>${lastBackupDescription}</span> <span slot="headline">${lastBackupDescription}</span>
</li> </ha-md-list-item>
</ul> </ha-md-list>
</ha-backup-summary-card> </ha-backup-summary-card>
`; `;
} }
if (numberOfDays > 0) { const numberOfDays = differenceInDays(
// Subtract a few hours to avoid showing as overdue if it's just a few hours (e.g. daylight saving)
addHours(now, -OVERDUE_MARGIN_HOURS),
lastBackupDate
);
const isOverdue =
(numberOfDays >= 1 &&
this.config.schedule.state === BackupScheduleState.DAILY) ||
numberOfDays >= 7;
if (isOverdue) {
return html` return html`
<ha-backup-summary-card <ha-backup-summary-card
heading=${`No backup for ${numberOfDays} days`} heading=${`No backup for ${numberOfDays} days`}
status="warning" status="warning"
> >
<ul class="list"> <ha-md-list>
<li class="item"> <ha-md-list-item>
<ha-svg-icon slot="start" .path=${mdiBackupRestore}></ha-svg-icon> <ha-svg-icon slot="start" .path=${mdiBackupRestore}></ha-svg-icon>
<span>${lastBackupDescription}</span> <span slot="headline">${lastBackupDescription}</span>
</li> </ha-md-list-item>
<li class="item"> <ha-md-list-item>
<ha-svg-icon slot="start" .path=${mdiCalendar}></ha-svg-icon> <ha-svg-icon slot="start" .path=${mdiCalendar}></ha-svg-icon>
<span>${nextBackupDescription}</span> <span slot="headline">${nextBackupDescription}</span>
</li> </ha-md-list-item>
</ul> </ha-md-list>
</ha-backup-summary-card> </ha-backup-summary-card>
`; `;
} }
return html` return html`
<ha-backup-summary-card heading=${`Backed up`} status="success"> <ha-backup-summary-card heading=${`Backed up`} status="success">
<ul class="list"> <ha-md-list>
<li class="item"> <ha-md-list-item>
<ha-svg-icon slot="start" .path=${mdiBackupRestore}></ha-svg-icon> <ha-svg-icon slot="start" .path=${mdiBackupRestore}></ha-svg-icon>
<span>${lastBackupDescription}</span> <span slot="headline">${lastBackupDescription}</span>
</li> </ha-md-list-item>
<li class="item"> <ha-md-list-item>
<ha-svg-icon slot="start" .path=${mdiCalendar}></ha-svg-icon> <ha-svg-icon slot="start" .path=${mdiCalendar}></ha-svg-icon>
<span>${nextBackupDescription}</span> <span slot="headline">${nextBackupDescription}</span>
</li> </ha-md-list-item>
</ul> </ha-md-list>
</ha-backup-summary-card> </ha-backup-summary-card>
`; `;
} }
@ -156,25 +190,6 @@ class HaBackupOverviewBackups extends LitElement {
p { p {
margin: 0; 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 { ha-svg-icon {
flex: none; flex: none;
} }
@ -183,6 +198,42 @@ class HaBackupOverviewBackups extends LitElement {
justify-content: flex-end; justify-content: flex-end;
border-top: none; border-top: none;
} }
ha-md-list {
background: none;
}
ha-md-list-item {
--md-list-item-top-space: 8px;
--md-list-item-bottom-space: 8px;
--md-list-item-one-line-container-height: 40x;
}
span.skeleton {
position: relative;
display: block;
width: 160px;
animation-fill-mode: forwards;
animation-iteration-count: infinite;
animation-name: loading;
animation-timing-function: linear;
animation-duration: 1.2s;
border-radius: 4px;
height: 20px;
background: linear-gradient(
to right,
rgb(247, 249, 250) 8%,
rgb(235, 238, 240) 18%,
rgb(247, 249, 250) 33%
)
0% 0% / 936px 104px;
}
@keyframes loading {
0% {
background-position: -468px 0;
}
100% {
background-position: 468px 0;
}
}
`, `,
]; ];
} }

View File

@ -27,7 +27,6 @@ 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-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-onboarding";
import "./components/overview/ha-backup-overview-progress"; import "./components/overview/ha-backup-overview-progress";
@ -182,15 +181,6 @@ class HaConfigBackupOverview extends LitElement {
> >
</ha-backup-overview-progress> </ha-backup-overview-progress>
` `
: this.fetching
? html`
<ha-backup-summary-card
heading="Loading backups"
description="Your backup information is being retrieved."
status="loading"
>
</ha-backup-summary-card>
`
: this._needsOnboarding : this._needsOnboarding
? html` ? html`
<ha-backup-overview-onboarding <ha-backup-overview-onboarding
@ -204,6 +194,7 @@ class HaConfigBackupOverview extends LitElement {
.hass=${this.hass} .hass=${this.hass}
.backups=${this.backups} .backups=${this.backups}
.config=${this.config} .config=${this.config}
.fetching=${this.fetching}
> >
</ha-backup-overview-summary> </ha-backup-overview-summary>
`} `}

View File

@ -257,6 +257,12 @@ class HaConfigBackupSettings extends LitElement {
.alert { .alert {
--mdc-theme-primary: var(--error-color); --mdc-theme-primary: var(--error-color);
} }
.card-header {
padding-bottom: 8px;
}
.card-content {
padding-bottom: 0;
}
`; `;
} }