Add support for add-on update type for backups in the UI (#24044)

* Add support for add-on update type for backups in the UI

* Add type to backup detail page

* Use new model

* Fix detail page

* Fix type
This commit is contained in:
Paul Bottein 2025-02-04 16:04:11 +01:00 committed by GitHub
parent 3a12019b64
commit 11ae3a77e8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 126 additions and 77 deletions

View File

@ -1,6 +1,8 @@
import { memoize } from "@fullcalendar/core/internal";
import { setHours, setMinutes } from "date-fns";
import type { HassConfig } from "home-assistant-js-websocket";
import memoizeOne from "memoize-one";
import checkValidDate from "../common/datetime/check_valid_date";
import {
formatDateTime,
formatDateTimeNumeric,
@ -11,7 +13,6 @@ import type { HomeAssistant } from "../types";
import { fileDownload } from "../util/file_download";
import { domainToName } from "./integration";
import type { FrontendLocaleData } from "./translation";
import checkValidDate from "../common/datetime/check_valid_date";
export const enum BackupScheduleRecurrence {
NEVER = "never",
@ -104,6 +105,9 @@ export interface BackupContent {
name: string;
agents: Record<string, BackupContentAgent>;
failed_agent_ids?: string[];
extra_metadata?: {
"supervisor.addon_update"?: string;
};
with_automatic_settings: boolean;
}
@ -319,6 +323,29 @@ export const computeBackupAgentName = (
export const computeBackupSize = (backup: BackupContent) =>
Math.max(...Object.values(backup.agents).map((agent) => agent.size));
export type BackupType = "automatic" | "manual" | "addon_update";
const BACKUP_TYPE_ORDER: BackupType[] = ["automatic", "manual", "addon_update"];
export const getBackupTypes = memoize((isHassio: boolean) =>
isHassio
? BACKUP_TYPE_ORDER
: BACKUP_TYPE_ORDER.filter((type) => type !== "addon_update")
);
export const computeBackupType = (
backup: BackupContent,
isHassio: boolean
): BackupType => {
if (backup.with_automatic_settings) {
return "automatic";
}
if (isHassio && backup.extra_metadata?.["supervisor.addon_update"] != null) {
return "addon_update";
}
return "manual";
};
export const compareAgents = (a: string, b: string) => {
const isLocalA = isLocalAgent(a);
const isLocalB = isLocalAgent(b);

View File

@ -1,16 +1,19 @@
import { mdiCalendarSync, mdiGestureTap } from "@mdi/js";
import { mdiCalendarSync, mdiGestureTap, mdiPuzzle } from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../../../../../common/config/is_component_loaded";
import "../../../../../components/ha-button";
import "../../../../../components/ha-card";
import "../../../../../components/ha-icon-next";
import "../../../../../components/ha-md-list";
import "../../../../../components/ha-md-list-item";
import type { BackupContent, BackupType } from "../../../../../data/backup";
import {
computeBackupSize,
type BackupContent,
computeBackupType,
getBackupTypes,
} from "../../../../../data/backup";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant } from "../../../../../types";
@ -21,6 +24,12 @@ interface BackupStats {
size: number;
}
const TYPE_ICONS: Record<BackupType, string> = {
automatic: mdiCalendarSync,
manual: mdiGestureTap,
addon_update: mdiPuzzle,
};
const computeBackupStats = (backups: BackupContent[]): BackupStats =>
backups.reduce(
(stats, backup) => {
@ -37,23 +46,22 @@ class HaBackupOverviewBackups extends LitElement {
@property({ attribute: false }) public backups: BackupContent[] = [];
private _automaticStats = memoizeOne((backups: BackupContent[]) => {
const automaticBackups = backups.filter(
(backup) => backup.with_automatic_settings
);
return computeBackupStats(automaticBackups);
});
private _manualStats = memoizeOne((backups: BackupContent[]) => {
const manualBackups = backups.filter(
(backup) => !backup.with_automatic_settings
);
return computeBackupStats(manualBackups);
});
private _stats = memoizeOne(
(
backups: BackupContent[],
isHassio: boolean
): [BackupType, BackupStats][] =>
getBackupTypes(isHassio).map((type) => {
const backupsOfType = backups.filter(
(backup) => computeBackupType(backup, isHassio) === type
);
return [type, computeBackupStats(backupsOfType)] as const;
})
);
render() {
const automaticStats = this._automaticStats(this.backups);
const manualStats = this._manualStats(this.backups);
const isHassio = isComponentLoaded(this.hass, "hassio");
const stats = this._stats(this.backups, isHassio);
return html`
<ha-card class="my-backups">
@ -62,44 +70,32 @@ class HaBackupOverviewBackups extends LitElement {
</div>
<div class="card-content">
<ha-md-list>
<ha-md-list-item
type="link"
href="/config/backup/backups?type=automatic"
>
<ha-svg-icon slot="start" .path=${mdiCalendarSync}></ha-svg-icon>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.backup.overview.backups.automatic",
{ count: automaticStats.count }
)}
</div>
<div slot="supporting-text">
${this.hass.localize(
"ui.panel.config.backup.overview.backups.total_size",
{ size: bytesToString(automaticStats.size, 1) }
)}
</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-md-list-item>
<ha-md-list-item
type="link"
href="/config/backup/backups?type=manual"
>
<ha-svg-icon slot="start" .path=${mdiGestureTap}></ha-svg-icon>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.backup.overview.backups.manual",
{ count: manualStats.count }
)}
</div>
<div slot="supporting-text">
${this.hass.localize(
"ui.panel.config.backup.overview.backups.total_size",
{ size: bytesToString(manualStats.size, 1) }
)}
</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-md-list-item>
${stats.map(
([type, { count, size }]) => html`
<ha-md-list-item
type="link"
href="/config/backup/backups?type=${type}"
>
<ha-svg-icon
slot="start"
.path=${TYPE_ICONS[type]}
></ha-svg-icon>
<div slot="headline">
${this.hass.localize(
`ui.panel.config.backup.overview.backups.${type}`,
{ count }
)}
</div>
<div slot="supporting-text">
${this.hass.localize(
"ui.panel.config.backup.overview.backups.total_size",
{ size: bytesToString(size) }
)}
</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-md-list-item>
`
)}
</ha-md-list>
</div>
<div class="card-actions">

View File

@ -11,6 +11,7 @@ import type { CSSResultGroup, TemplateResult } from "lit";
import { html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { relativeTime } from "../../../common/datetime/relative_time";
import { storage } from "../../../common/decorators/storage";
import { fireEvent, type HASSDomEvent } from "../../../common/dom/fire_event";
@ -42,9 +43,11 @@ import {
compareAgents,
computeBackupAgentName,
computeBackupSize,
computeBackupType,
deleteBackup,
generateBackup,
generateBackupWithAutomaticSettings,
getBackupTypes,
isLocalAgent,
isNetworkMountAgent,
} from "../../../data/backup";
@ -74,10 +77,6 @@ interface BackupRow extends DataTableRowData, BackupContent {
agent_ids: string[];
}
type BackupType = "automatic" | "manual";
const TYPE_ORDER: BackupType[] = ["automatic", "manual"];
@customElement("ha-config-backup-backups")
class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@ -277,9 +276,13 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
);
private _groupOrder = memoizeOne(
(activeGrouping: string | undefined, localize: LocalizeFunc) =>
(
activeGrouping: string | undefined,
localize: LocalizeFunc,
isHassio: boolean
) =>
activeGrouping === "formatted_type"
? TYPE_ORDER.map((type) =>
? getBackupTypes(isHassio).map((type) =>
localize(`ui.panel.config.backup.type.${type}`)
)
: undefined
@ -303,20 +306,19 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
(
backups: BackupContent[],
filters: DataTableFiltersValues,
localize: LocalizeFunc
localize: LocalizeFunc,
isHassio: boolean
): BackupRow[] => {
const typeFilter = filters["ha-filter-states"] as string[] | undefined;
let filteredBackups = backups;
if (typeFilter?.length) {
filteredBackups = filteredBackups.filter(
(backup) =>
(backup.with_automatic_settings &&
typeFilter.includes("automatic")) ||
(!backup.with_automatic_settings && typeFilter.includes("manual"))
);
filteredBackups = filteredBackups.filter((backup) => {
const type = computeBackupType(backup, isHassio);
return typeFilter.includes(type);
});
}
return filteredBackups.map((backup) => {
const type = backup.with_automatic_settings ? "automatic" : "manual";
const type = computeBackupType(backup, isHassio);
const agentIds = Object.keys(backup.agents);
return {
...backup,
@ -335,8 +337,13 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
protected render(): TemplateResult {
const backupInProgress =
"state" in this.manager && this.manager.state === "in_progress";
const data = this._data(this.backups, this._filters, this.hass.localize);
const isHassio = isComponentLoaded(this.hass, "hassio");
const data = this._data(
this.backups,
this._filters,
this.hass.localize,
isHassio
);
const maxDisplayedAgents = Math.min(
this._maxAgents(data),
this.narrow ? 3 : 5
@ -371,7 +378,8 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
.initialCollapsedGroups=${this._activeCollapsed}
.groupOrder=${this._groupOrder(
this._activeGrouping,
this.hass.localize
this.hass.localize,
isHassio
)}
@grouping-changed=${this._handleGroupingChanged}
@collapsed-changed=${this._handleCollapseChanged}
@ -435,7 +443,7 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
.hass=${this.hass}
.label=${this.hass.localize("ui.panel.config.backup.backup_type")}
.value=${this._filters["ha-filter-states"]}
.states=${this._states(this.hass.localize)}
.states=${this._states(this.hass.localize, isHassio)}
@data-table-filter-changed=${this._filterChanged}
slot="filter-pane"
expanded
@ -460,8 +468,8 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
`;
}
private _states = memoizeOne((localize: LocalizeFunc) =>
TYPE_ORDER.map((type) => ({
private _states = memoizeOne((localize: LocalizeFunc, isHassio: boolean) =>
getBackupTypes(isHassio).map((type) => ({
value: type,
label: localize(`ui.panel.config.backup.type.${type}`),
}))

View File

@ -31,6 +31,7 @@ import {
compareAgents,
computeBackupAgentName,
computeBackupSize,
computeBackupType,
deleteBackup,
fetchBackupDetails,
isLocalAgent,
@ -46,6 +47,7 @@ import { showRestoreBackupDialog } from "./dialogs/show-dialog-restore-backup";
import { fireEvent } from "../../../common/dom/fire_event";
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
import { downloadBackup } from "./helper/download_backup";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
interface Agent extends BackupContentAgent {
id: string;
@ -110,6 +112,8 @@ class HaConfigBackupDetails extends LitElement {
return nothing;
}
const isHassio = isComponentLoaded(this.hass, "hassio");
return html`
<hass-subpage
back-path="/config/backup/backups"
@ -161,6 +165,18 @@ class HaConfigBackupDetails extends LitElement {
</div>
<div class="card-content">
<ha-md-list class="summary">
<ha-md-list-item>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.backup.backup_type"
)}
</span>
<span slot="supporting-text">
${this.hass.localize(
`ui.panel.config.backup.type.${computeBackupType(this._backup, isHassio)}`
)}
</span>
</ha-md-list-item>
<ha-md-list-item>
<span slot="headline">
${this.hass.localize(

View File

@ -2223,7 +2223,8 @@
"backup_type": "Type",
"type": {
"manual": "Manual",
"automatic": "Automatic"
"automatic": "Automatic",
"addon_update": "Add-on update"
},
"locations": "Locations",
"create": {
@ -2566,6 +2567,7 @@
"title": "My backups",
"automatic": "{count} automatic {count, plural,\n one {backup}\n other {backups}\n}",
"manual": "{count} manual {count, plural,\n one {backup}\n other {backups}\n}",
"addon_update": "{count} add-on update {count, plural,\n one {backup}\n other {backups}\n}",
"total_size": "{size} in total",
"show_all": "Show all backups"
},