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 { setHours, setMinutes } from "date-fns";
import type { HassConfig } from "home-assistant-js-websocket"; import type { HassConfig } from "home-assistant-js-websocket";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import checkValidDate from "../common/datetime/check_valid_date";
import { import {
formatDateTime, formatDateTime,
formatDateTimeNumeric, formatDateTimeNumeric,
@ -11,7 +13,6 @@ import type { HomeAssistant } from "../types";
import { fileDownload } from "../util/file_download"; import { fileDownload } from "../util/file_download";
import { domainToName } from "./integration"; import { domainToName } from "./integration";
import type { FrontendLocaleData } from "./translation"; import type { FrontendLocaleData } from "./translation";
import checkValidDate from "../common/datetime/check_valid_date";
export const enum BackupScheduleRecurrence { export const enum BackupScheduleRecurrence {
NEVER = "never", NEVER = "never",
@ -104,6 +105,9 @@ export interface BackupContent {
name: string; name: string;
agents: Record<string, BackupContentAgent>; agents: Record<string, BackupContentAgent>;
failed_agent_ids?: string[]; failed_agent_ids?: string[];
extra_metadata?: {
"supervisor.addon_update"?: string;
};
with_automatic_settings: boolean; with_automatic_settings: boolean;
} }
@ -319,6 +323,29 @@ export const computeBackupAgentName = (
export const computeBackupSize = (backup: BackupContent) => export const computeBackupSize = (backup: BackupContent) =>
Math.max(...Object.values(backup.agents).map((agent) => agent.size)); 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) => { export const compareAgents = (a: string, b: string) => {
const isLocalA = isLocalAgent(a); const isLocalA = isLocalAgent(a);
const isLocalB = isLocalAgent(b); 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 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";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../../../../../common/config/is_component_loaded";
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-md-list"; import "../../../../../components/ha-md-list";
import "../../../../../components/ha-md-list-item"; import "../../../../../components/ha-md-list-item";
import type { BackupContent, BackupType } from "../../../../../data/backup";
import { import {
computeBackupSize, computeBackupSize,
type BackupContent, computeBackupType,
getBackupTypes,
} from "../../../../../data/backup"; } from "../../../../../data/backup";
import { haStyle } from "../../../../../resources/styles"; import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant } from "../../../../../types"; import type { HomeAssistant } from "../../../../../types";
@ -21,6 +24,12 @@ interface BackupStats {
size: number; size: number;
} }
const TYPE_ICONS: Record<BackupType, string> = {
automatic: mdiCalendarSync,
manual: mdiGestureTap,
addon_update: mdiPuzzle,
};
const computeBackupStats = (backups: BackupContent[]): BackupStats => const computeBackupStats = (backups: BackupContent[]): BackupStats =>
backups.reduce( backups.reduce(
(stats, backup) => { (stats, backup) => {
@ -37,23 +46,22 @@ class HaBackupOverviewBackups extends LitElement {
@property({ attribute: false }) public backups: BackupContent[] = []; @property({ attribute: false }) public backups: BackupContent[] = [];
private _automaticStats = memoizeOne((backups: BackupContent[]) => { private _stats = memoizeOne(
const automaticBackups = backups.filter( (
(backup) => backup.with_automatic_settings backups: BackupContent[],
); isHassio: boolean
return computeBackupStats(automaticBackups); ): [BackupType, BackupStats][] =>
}); getBackupTypes(isHassio).map((type) => {
const backupsOfType = backups.filter(
private _manualStats = memoizeOne((backups: BackupContent[]) => { (backup) => computeBackupType(backup, isHassio) === type
const manualBackups = backups.filter( );
(backup) => !backup.with_automatic_settings return [type, computeBackupStats(backupsOfType)] as const;
); })
return computeBackupStats(manualBackups); );
});
render() { render() {
const automaticStats = this._automaticStats(this.backups); const isHassio = isComponentLoaded(this.hass, "hassio");
const manualStats = this._manualStats(this.backups); const stats = this._stats(this.backups, isHassio);
return html` return html`
<ha-card class="my-backups"> <ha-card class="my-backups">
@ -62,44 +70,32 @@ class HaBackupOverviewBackups extends LitElement {
</div> </div>
<div class="card-content"> <div class="card-content">
<ha-md-list> <ha-md-list>
<ha-md-list-item ${stats.map(
type="link" ([type, { count, size }]) => html`
href="/config/backup/backups?type=automatic" <ha-md-list-item
> type="link"
<ha-svg-icon slot="start" .path=${mdiCalendarSync}></ha-svg-icon> href="/config/backup/backups?type=${type}"
<div slot="headline"> >
${this.hass.localize( <ha-svg-icon
"ui.panel.config.backup.overview.backups.automatic", slot="start"
{ count: automaticStats.count } .path=${TYPE_ICONS[type]}
)} ></ha-svg-icon>
</div> <div slot="headline">
<div slot="supporting-text"> ${this.hass.localize(
${this.hass.localize( `ui.panel.config.backup.overview.backups.${type}`,
"ui.panel.config.backup.overview.backups.total_size", { count }
{ size: bytesToString(automaticStats.size, 1) } )}
)} </div>
</div> <div slot="supporting-text">
<ha-icon-next slot="end"></ha-icon-next> ${this.hass.localize(
</ha-md-list-item> "ui.panel.config.backup.overview.backups.total_size",
<ha-md-list-item { size: bytesToString(size) }
type="link" )}
href="/config/backup/backups?type=manual" </div>
> <ha-icon-next slot="end"></ha-icon-next>
<ha-svg-icon slot="start" .path=${mdiGestureTap}></ha-svg-icon> </ha-md-list-item>
<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>
</ha-md-list> </ha-md-list>
</div> </div>
<div class="card-actions"> <div class="card-actions">

View File

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

View File

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

View File

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