Skip to content

Commit

Permalink
Add support for add-on update type for backups in the UI (#24044)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
piitaya authored Feb 4, 2025
1 parent 3a12019 commit 11ae3a7
Show file tree
Hide file tree
Showing 5 changed files with 126 additions and 77 deletions.
29 changes: 28 additions & 1 deletion src/data/backup.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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",
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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) => {
Expand All @@ -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">
Expand All @@ -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">
Expand Down
48 changes: 28 additions & 20 deletions src/panels/config/backup/ha-config-backup-backups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -42,9 +43,11 @@ import {
compareAgents,
computeBackupAgentName,
computeBackupSize,
computeBackupType,
deleteBackup,
generateBackup,
generateBackupWithAutomaticSettings,
getBackupTypes,
isLocalAgent,
isNetworkMountAgent,
} from "../../../data/backup";
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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}
Expand Down Expand Up @@ -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
Expand All @@ -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}`),
}))
Expand Down
16 changes: 16 additions & 0 deletions src/panels/config/backup/ha-config-backup-details.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
compareAgents,
computeBackupAgentName,
computeBackupSize,
computeBackupType,
deleteBackup,
fetchBackupDetails,
isLocalAgent,
Expand All @@ -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;
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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(
Expand Down
4 changes: 3 additions & 1 deletion src/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -2223,7 +2223,8 @@
"backup_type": "Type",
"type": {
"manual": "Manual",
"automatic": "Automatic"
"automatic": "Automatic",
"addon_update": "Add-on update"
},
"locations": "Locations",
"create": {
Expand Down Expand Up @@ -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"
},
Expand Down

0 comments on commit 11ae3a7

Please sign in to comment.