Setup backup overview page (#23331)

* Add overview page

* Remove configure button

* Reorganize files

* Add backups summary

* Add settings overview

* Fixes

* Update wording

* Setup onboarding before creating a backup
This commit is contained in:
Paul Bottein 2024-12-19 14:37:05 +01:00 committed by GitHub
parent 3da13b823a
commit 95559cbc2a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 681 additions and 189 deletions

View File

@ -214,7 +214,7 @@ export const getPreferredAgentForDownload = (agents: string[]) => {
};
export const CORE_LOCAL_AGENT = "backup.local";
export const HASSIO_LOCAL_AGENT = "backup.hassio";
export const HASSIO_LOCAL_AGENT = "hassio.local";
export const CLOUD_AGENT = "cloud.cloud";
export const isLocalAgent = (agentId: string) =>

View File

@ -2,22 +2,22 @@ import { mdiDatabase } from "@mdi/js";
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import { computeDomain } from "../../../../common/entity/compute_domain";
import "../../../../components/ha-md-list";
import "../../../../components/ha-md-list-item";
import "../../../../components/ha-svg-icon";
import "../../../../components/ha-switch";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { computeDomain } from "../../../../../common/entity/compute_domain";
import "../../../../../components/ha-md-list";
import "../../../../../components/ha-md-list-item";
import "../../../../../components/ha-svg-icon";
import "../../../../../components/ha-switch";
import {
CLOUD_AGENT,
compareAgents,
computeBackupAgentName,
fetchBackupAgentsInfo,
isLocalAgent,
} from "../../../../data/backup";
import type { CloudStatus } from "../../../../data/cloud";
import type { HomeAssistant } from "../../../../types";
import { brandsUrl } from "../../../../util/brands-url";
} from "../../../../../data/backup";
import type { CloudStatus } from "../../../../../data/cloud";
import type { HomeAssistant } from "../../../../../types";
import { brandsUrl } from "../../../../../util/brands-url";
const DEFAULT_AGENTS = [];

View File

@ -9,21 +9,21 @@ import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../../../../common/config/is_component_loaded";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-button";
import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-md-list";
import "../../../../components/ha-md-list-item";
import "../../../../components/ha-md-select";
import type { HaMdSelect } from "../../../../components/ha-md-select";
import "../../../../components/ha-md-select-option";
import "../../../../components/ha-switch";
import type { HaSwitch } from "../../../../components/ha-switch";
import { fetchHassioAddonsInfo } from "../../../../data/hassio/addon";
import type { HomeAssistant } from "../../../../types";
import "./ha-backup-addons-picker";
import type { BackupAddonItem } from "./ha-backup-addons-picker";
import { isComponentLoaded } from "../../../../../common/config/is_component_loaded";
import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/ha-button";
import "../../../../../components/ha-expansion-panel";
import "../../../../../components/ha-md-list";
import "../../../../../components/ha-md-list-item";
import "../../../../../components/ha-md-select";
import type { HaMdSelect } from "../../../../../components/ha-md-select";
import "../../../../../components/ha-md-select-option";
import "../../../../../components/ha-switch";
import type { HaSwitch } from "../../../../../components/ha-switch";
import { fetchHassioAddonsInfo } from "../../../../../data/hassio/addon";
import type { HomeAssistant } from "../../../../../types";
import "../ha-backup-addons-picker";
import type { BackupAddonItem } from "../ha-backup-addons-picker";
export type FormData = {
homeassistant: boolean;

View File

@ -1,13 +1,13 @@
import { mdiDownload } from "@mdi/js";
import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-md-list";
import "../../../../components/ha-md-list-item";
import type { HomeAssistant } from "../../../../types";
import { showChangeBackupEncryptionKeyDialog } from "../dialogs/show-dialog-change-backup-encryption-key";
import { fileDownload } from "../../../../util/file_download";
import { showSetBackupEncryptionKeyDialog } from "../dialogs/show-dialog-set-backup-encryption-key";
import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/ha-md-list";
import "../../../../../components/ha-md-list-item";
import type { HomeAssistant } from "../../../../../types";
import { showChangeBackupEncryptionKeyDialog } from "../../dialogs/show-dialog-change-backup-encryption-key";
import { fileDownload } from "../../../../../util/file_download";
import { showSetBackupEncryptionKeyDialog } from "../../dialogs/show-dialog-set-backup-encryption-key";
@customElement("ha-backup-config-encryption-key")
class HaBackupConfigEncryptionKey extends LitElement {

View File

@ -2,19 +2,19 @@ import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../common/dom/fire_event";
import type { HaCheckbox } from "../../../../components/ha-checkbox";
import "../../../../components/ha-md-list";
import "../../../../components/ha-md-list-item";
import "../../../../components/ha-md-select";
import "../../../../components/ha-md-textfield";
import type { HaMdSelect } from "../../../../components/ha-md-select";
import "../../../../components/ha-md-select-option";
import "../../../../components/ha-switch";
import type { BackupConfig } from "../../../../data/backup";
import { BackupScheduleState } from "../../../../data/backup";
import type { HomeAssistant } from "../../../../types";
import { clamp } from "../../../../common/number/clamp";
import { fireEvent } from "../../../../../common/dom/fire_event";
import type { HaCheckbox } from "../../../../../components/ha-checkbox";
import "../../../../../components/ha-md-list";
import "../../../../../components/ha-md-list-item";
import "../../../../../components/ha-md-select";
import "../../../../../components/ha-md-textfield";
import type { HaMdSelect } from "../../../../../components/ha-md-select";
import "../../../../../components/ha-md-select-option";
import "../../../../../components/ha-switch";
import type { BackupConfig } from "../../../../../data/backup";
import { BackupScheduleState } from "../../../../../data/backup";
import type { HomeAssistant } from "../../../../../types";
import { clamp } from "../../../../../common/number/clamp";
export type BackupConfigSchedule = Pick<BackupConfig, "schedule" | "retention">;
@ -24,7 +24,7 @@ const MAX_VALUE = 50;
enum RetentionPreset {
COPIES_3 = "copies_3",
DAYS_7 = "days_7",
FOREOVER = "forever",
FOREVER = "forever",
CUSTOM = "custom",
}
@ -191,7 +191,7 @@ class HaBackupConfigSchedule extends LitElement {
<ha-md-select-option .value=${RetentionPreset.DAYS_7}>
<div slot="headline">Keep 7 days</div>
</ha-md-select-option>
<ha-md-select-option .value=${RetentionPreset.FOREOVER}>
<ha-md-select-option .value=${RetentionPreset.FOREVER}>
<div slot="headline">Keep forever</div>
</ha-md-select-option>
<ha-md-select-option .value=${RetentionPreset.CUSTOM}>
@ -270,7 +270,9 @@ class HaBackupConfigSchedule extends LitElement {
const data = this._getData(this.value);
const retention = RETENTION_PRESETS[value];
// Ensure we have at least 1 in defaut value because user can't select 0
retention.value = Math.max(retention.value, 1);
if (value !== RetentionPreset.FOREVER) {
retention.value = Math.max(retention.value, 1);
}
this._setData({
...data,
retention: RETENTION_PRESETS[value],

View File

@ -0,0 +1,122 @@
import type { CSSResultGroup } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { navigate } from "../../../../../common/navigate";
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 } from "../../../../../data/backup";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant } from "../../../../../types";
import { bytesToString } from "../../../../../util/bytes-to-string";
type BackupStats = {
count: number;
size: number;
};
const computeBackupStats = (backups: BackupContent[]): BackupStats =>
backups.reduce(
(stats, backup) => {
stats.count++;
stats.size += backup.size;
return stats;
},
{ count: 0, size: 0 }
);
@customElement("ha-backup-overview-backups")
class HaBackupOverviewBackups extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public backups: BackupContent[] = [];
private _showAll() {
navigate("/config/backup/backups");
}
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);
});
render() {
const automaticStats = this._automaticStats(this.backups);
const manualStats = this._manualStats(this.backups);
return html`
<ha-card class="my-backups">
<div class="card-header">My backups</div>
<div class="card-content">
<ha-md-list>
<ha-md-list-item type="link" href="/config/backup/backups">
<div slot="headline">
${automaticStats.count} automatic backups
</div>
<div slot="supporting-text">
${bytesToString(automaticStats.size, 1)} in total
</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-md-list-item>
<ha-md-list-item type="link" href="/config/backup/backups">
<div slot="headline">${manualStats.count} manual backups</div>
<div slot="supporting-text">
${bytesToString(manualStats.size, 1)} in total
</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-md-list-item>
</ha-md-list>
</div>
<div class="card-actions">
<ha-button href="/config/backup/backups" @click=${this._showAll}>
Show all backups
</ha-button>
</div>
</ha-card>
`;
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
.content {
padding: 28px 20px 0;
max-width: 690px;
margin: 0 auto;
gap: 24px;
display: flex;
flex-direction: column;
margin-bottom: 24px;
margin-bottom: 72px;
}
.card-actions {
display: flex;
justify-content: flex-end;
}
.card-content {
padding-left: 0;
padding-right: 0;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-backup-overview-backups": HaBackupOverviewBackups;
}
}

View File

@ -0,0 +1,188 @@
import { mdiCalendar, mdiCog, mdiPuzzle, mdiUpload } from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { navigate } from "../../../../../common/navigate";
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 } from "../../../../../data/backup";
import { BackupScheduleState, isLocalAgent } from "../../../../../data/backup";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant } from "../../../../../types";
@customElement("ha-backup-overview-settings")
class HaBackupBackupsSummary extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public config!: BackupConfig;
private _configure() {
navigate("/config/backup/settings");
}
private _scheduleDescription(config: BackupConfig): string {
const { copies, days } = config.retention;
const { state: schedule } = config.schedule;
if (schedule === BackupScheduleState.NEVER) {
return "Automatic backups are disabled";
}
let copiesText = "and keep all backups";
if (copies) {
copiesText = `and keep the latest ${copies} copie(s)`;
} else if (days) {
copiesText = `and keep backups for ${days} day(s)`;
}
let scheduleText = "";
if (schedule === BackupScheduleState.DAILY) {
scheduleText = `Daily at 04:45`;
}
if (schedule === BackupScheduleState.MONDAY) {
scheduleText = `Weekly on Mondays at 04:45`;
}
if (schedule === BackupScheduleState.TUESDAY) {
scheduleText = `Weekly on Thuesdays at 04:45`;
}
if (schedule === BackupScheduleState.WEDNESDAY) {
scheduleText = `Weekly on Wednesdays at 04:45`;
}
if (schedule === BackupScheduleState.THURSDAY) {
scheduleText = `Weekly on Thursdays at 04:45`;
}
if (schedule === BackupScheduleState.FRIDAY) {
scheduleText = `Weekly on Fridays at 04:45`;
}
if (schedule === BackupScheduleState.SATURDAY) {
scheduleText = `Weekly on Saturdays at 04:45`;
}
if (schedule === BackupScheduleState.SUNDAY) {
scheduleText = `Weekly on Sundays at 04:45`;
}
return scheduleText + " " + copiesText;
}
private _addonsDescription(config: BackupConfig): string {
if (config.create_backup.include_all_addons) {
return "All add-ons, including new";
}
if (config.create_backup.include_addons?.length) {
return `${config.create_backup.include_addons.length} add-ons`;
}
return "No add-ons";
}
private _agentsDescription(config: BackupConfig): string {
const hasLocal = config.create_backup.agent_ids.some((a) =>
isLocalAgent(a)
);
const offsiteLocations = config.create_backup.agent_ids.filter(
(a) => !isLocalAgent(a)
);
if (offsiteLocations.length) {
return `Upload to ${offsiteLocations.length} off-site locations`;
}
if (hasLocal) {
return "Local backup only";
}
return "No location configured";
}
render() {
const isHassio = this.hass.config.components.includes("hassio");
return html`
<ha-card class="my-backups">
<div class="card-header">Automatic backups</div>
<div class="card-content">
<ha-md-list>
<ha-md-list-item>
<ha-svg-icon slot="start" .path=${mdiCalendar}></ha-svg-icon>
<div slot="headline">
${this._scheduleDescription(this.config)}
</div>
<div slot="supporting-text">
Schedule and number of backups to keep
</div>
</ha-md-list-item>
<ha-md-list-item>
<ha-svg-icon slot="start" .path=${mdiCog}></ha-svg-icon>
<div slot="headline">
${this.config.create_backup.include_database
? "Settings and history"
: "Settings only"}
</div>
<div slot="supporting-text">
Home Assistant data that is included
</div>
</ha-md-list-item>
${isHassio
? html`
<ha-md-list-item>
<ha-svg-icon slot="start" .path=${mdiPuzzle}></ha-svg-icon>
<div slot="headline">
${this._addonsDescription(this.config)}
</div>
<div slot="supporting-text">Add-ons that are included</div>
</ha-md-list-item>
<ha-md-list-item>
<ha-svg-icon slot="start" .path=${mdiUpload}></ha-svg-icon>
<div slot="headline">
${this._agentsDescription(this.config)}
</div>
<div slot="supporting-text">
Locations where backup is uploaded to
</div>
</ha-md-list-item>
`
: nothing}
</ha-md-list>
</div>
<div class="card-actions">
<ha-button @click=${this._configure}>
Configure automatic backups
</ha-button>
</div>
</ha-card>
`;
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
.content {
padding: 28px 20px 0;
max-width: 690px;
margin: 0 auto;
gap: 24px;
display: flex;
flex-direction: column;
margin-bottom: 24px;
margin-bottom: 72px;
}
.card-actions {
display: flex;
justify-content: flex-end;
}
.card-content {
padding-left: 0;
padding-right: 0;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-backup-overview-settings": HaBackupBackupsSummary;
}
}

View File

@ -33,11 +33,11 @@ import { haStyle, haStyleDialog } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import { fileDownload } from "../../../../util/file_download";
import { showToast } from "../../../../util/toast";
import "../components/ha-backup-config-agents";
import "../components/ha-backup-config-data";
import type { BackupConfigData } from "../components/ha-backup-config-data";
import "../components/ha-backup-config-schedule";
import type { BackupConfigSchedule } from "../components/ha-backup-config-schedule";
import "../components/config/ha-backup-config-agents";
import "../components/config/ha-backup-config-data";
import type { BackupConfigData } from "../components/config/ha-backup-config-data";
import "../components/config/ha-backup-config-schedule";
import type { BackupConfigSchedule } from "../components/config/ha-backup-config-schedule";
import type { BackupOnboardingDialogParams } from "./show-dialog-backup_onboarding";
const STEPS = [
@ -367,8 +367,8 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
case "locations":
return html`
<p>
Home Assistant will upload to these locations when this backup
settings are used. You can use all locations for custom backups.
Home Assistant will upload to these locations when an automatic
backup is made. You can use all locations for manual backups.
</p>
<ha-backup-config-agents
.hass=${this.hass}

View File

@ -2,6 +2,7 @@ import { mdiClose } from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { isComponentLoaded } from "../../../../common/config/is_component_loaded";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-button";
import "../../../../components/ha-dialog-header";
@ -28,8 +29,8 @@ import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
import { haStyle, haStyleDialog } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import "../components/ha-backup-agents-picker";
import "../components/ha-backup-config-data";
import type { BackupConfigData } from "../components/ha-backup-config-data";
import "../components/config/ha-backup-config-data";
import type { BackupConfigData } from "../components/config/ha-backup-config-data";
import type { GenerateBackupDialogParams } from "./show-dialog-generate-backup";
type FormData = {
@ -278,11 +279,14 @@ class DialogGenerateBackup extends LitElement implements HassDialog {
include_homeassistant:
data.include_homeassistant || data.include_database,
include_database: data.include_database,
include_addons: data.include_addons,
include_folders: data.include_folders,
include_all_addons: data.include_all_addons,
};
if (isComponentLoaded(this.hass, "hassio")) {
params.include_folders = data.include_folders;
params.include_all_addons = data.include_all_addons;
params.include_addons = data.include_addons;
}
this._params!.submit?.(params);
this.closeDialog();
}

View File

@ -71,22 +71,22 @@ class DialogNewBackup extends LitElement implements HassDialog {
dialogInitialFocus
>
<ha-md-list-item
@click=${this._default}
@click=${this._automatic}
type="button"
.disabled=${!this._params.config.create_backup.password}
>
<ha-svg-icon slot="start" .path=${mdiCog}></ha-svg-icon>
<span slot="headline">Use automatic backups</span>
<span slot="headline">Automatic backup</span>
<span slot="supporting-text">
Create a backup with the data and locations you have configured.
</span>
<ha-icon-next slot="end"></ha-icon-next>
</ha-md-list-item>
<ha-md-list-item @click=${this._custom} type="button">
<ha-md-list-item @click=${this._manual} type="button">
<ha-svg-icon slot="start" .path=${mdiPencil}></ha-svg-icon>
<span slot="headline">Make custom backup</span>
<span slot="headline">Manual backup</span>
<span slot="supporting-text">
Select specific data and locations for a custom backup.
Select data and locations for a manual backup.
</span>
<ha-icon-next slot="end"></ha-icon-next>
</ha-md-list-item>
@ -96,12 +96,12 @@ class DialogNewBackup extends LitElement implements HassDialog {
`;
}
private async _custom() {
private async _manual() {
this._params!.submit?.("manual");
this.closeDialog();
}
private async _default() {
private async _automatic() {
this._params!.submit?.("automatic");
this.closeDialog();
}

View File

@ -11,7 +11,6 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
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 type { HASSDomEvent } from "../../../common/dom/fire_event";
@ -22,12 +21,12 @@ import { capitalizeFirstLetter } from "../../../common/string/capitalize-first-l
import type { LocalizeFunc } from "../../../common/translations/localize";
import type {
DataTableColumnContainer,
DataTableRowData,
RowClickedEvent,
SelectionChangedEvent,
} from "../../../components/data-table/ha-data-table";
import "../../../components/ha-button";
import "../../../components/ha-button-menu";
import "../../../components/ha-card";
import "../../../components/ha-fab";
import "../../../components/ha-icon";
import "../../../components/ha-icon-next";
@ -68,24 +67,20 @@ import { brandsUrl } from "../../../util/brands-url";
import { bytesToString } from "../../../util/bytes-to-string";
import { fileDownload } from "../../../util/file_download";
import { showToast } from "../../../util/toast";
import "./components/ha-backup-summary-card";
import "./components/ha-backup-summary-progress";
import "./components/ha-backup-summary-status";
import { showBackupOnboardingDialog } from "./dialogs/show-dialog-backup_onboarding";
import { showGenerateBackupDialog } from "./dialogs/show-dialog-generate-backup";
import { showNewBackupDialog } from "./dialogs/show-dialog-new-backup";
import { showUploadBackupDialog } from "./dialogs/show-dialog-upload-backup";
interface BackupRow extends BackupContent {
interface BackupRow extends DataTableRowData, BackupContent {
formatted_type: string;
}
type BackupType = "automatic" | "manual";
type BackupType = "automatic" | "manual" | "imported";
const TYPE_ORDER: Array<BackupType> = ["automatic", "manual"];
const TYPE_ORDER: Array<BackupType> = ["automatic", "manual", "imported"];
@customElement("ha-config-backup-dashboard")
class HaConfigBackupDashboard extends SubscribeMixin(LitElement) {
@customElement("ha-config-backup-backups")
class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public cloudStatus!: CloudStatus;
@ -98,8 +93,6 @@ class HaConfigBackupDashboard extends SubscribeMixin(LitElement) {
@state() private _backups: BackupContent[] = [];
@state() private _fetching = false;
@state() private _selected: string[] = [];
@state() private _config?: BackupConfig;
@ -112,7 +105,7 @@ class HaConfigBackupDashboard extends SubscribeMixin(LitElement) {
state: false,
subscribe: false,
})
private _activeCollapsed?: string;
private _activeCollapsed: string[] = [];
private _subscribed?: Promise<() => void>;
@ -271,7 +264,7 @@ class HaConfigBackupDashboard extends SubscribeMixin(LitElement) {
]}
.hass=${this.hass}
.narrow=${this.narrow}
back-path="/config/system"
back-path="/config/backup/overview"
clickable
id="backup_id"
selectable
@ -291,70 +284,6 @@ class HaConfigBackupDashboard extends SubscribeMixin(LitElement) {
"ui.panel.config.backup.picker.search"
)}
>
<div slot="top-header" class="header">
${this._fetching
? html`
<ha-backup-summary-card
heading="Loading backups"
description="Your backup information is being retrieved."
has-action
status="loading"
>
<ha-button
slot="action"
@click=${this._configureAutomaticBackups}
>
Configure
</ha-button>
</ha-backup-summary-card>
`
: backupInProgress
? html`
<ha-backup-summary-progress
.hass=${this.hass}
.manager=${this._manager}
has-action
>
<ha-button
slot="action"
@click=${this._configureAutomaticBackups}
>
Configure
</ha-button>
</ha-backup-summary-progress>
`
: this._needsOnboarding
? html`
<ha-backup-summary-card
heading="Configure automatic backups"
description="Have a one-click backup automation with selected data and locations."
has-action
status="info"
>
<ha-button
slot="action"
@click=${this._setupAutomaticBackups}
>
Set up automatic backups
</ha-button>
</ha-backup-summary-card>
`
: html`
<ha-backup-summary-status
.hass=${this.hass}
.backups=${this._backups}
has-action
>
<ha-button
slot="action"
@click=${this._configureAutomaticBackups}
>
Configure
</ha-button>
</ha-backup-summary-status>
`}
</div>
<div slot="toolbar-icon">
<ha-button-menu>
<ha-icon-button
@ -410,9 +339,7 @@ class HaConfigBackupDashboard extends SubscribeMixin(LitElement) {
<ha-fab
slot="fab"
?disabled=${backupInProgress}
.label=${this.hass.localize(
"ui.panel.config.backup.create_backup"
)}
.label=${"Backup now"}
extended
@click=${this._newBackup}
>
@ -466,10 +393,7 @@ class HaConfigBackupDashboard extends SubscribeMixin(LitElement) {
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
this._fetching = true;
this._fetchBackupInfo().then(() => {
this._fetching = false;
});
this._fetchBackupInfo();
this._subscribeEvents();
this._fetchBackupConfig();
}
@ -529,12 +453,6 @@ class HaConfigBackupDashboard extends SubscribeMixin(LitElement) {
return;
}
if (!isComponentLoaded(this.hass, "hassio")) {
delete params.include_folders;
delete params.include_all_addons;
delete params.include_addons;
}
await generateBackup(this.hass, params);
await this._fetchBackupInfo();
return;
@ -602,23 +520,6 @@ class HaConfigBackupDashboard extends SubscribeMixin(LitElement) {
this._dataTable.clearSelection();
}
private _configureAutomaticBackups() {
navigate("/config/backup/settings");
}
private async _setupAutomaticBackups() {
const success = await showBackupOnboardingDialog(this, {
cloudStatus: this.cloudStatus,
});
if (!success) {
return;
}
this._fetchBackupConfig();
await generateBackupWithAutomaticSettings(this.hass);
await this._fetchBackupInfo();
}
static get styles(): CSSResultGroup {
return [
haStyle,
@ -688,6 +589,6 @@ class HaConfigBackupDashboard extends SubscribeMixin(LitElement) {
declare global {
interface HTMLElementTagNameMap {
"ha-config-backup-dashboard": HaConfigBackupDashboard;
"ha-config-backup-backups": HaConfigBackupBackups;
}
}

View File

@ -87,7 +87,7 @@ class HaConfigBackupDetails extends LitElement {
return html`
<hass-subpage
back-path="/config/backup"
back-path="/config/backup/backups"
.hass=${this.hass}
.narrow=${this.narrow}
.header=${this._backup?.name || "Backup"}

View File

@ -0,0 +1,270 @@
import { mdiDotsVertical, mdiPlus, mdiUpload } from "@mdi/js";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { shouldHandleRequestSelectedEvent } from "../../../common/mwc/handle-request-selected-event";
import "../../../components/ha-button";
import "../../../components/ha-button-menu";
import "../../../components/ha-card";
import "../../../components/ha-fab";
import "../../../components/ha-icon";
import "../../../components/ha-icon-next";
import "../../../components/ha-icon-overflow-menu";
import "../../../components/ha-list-item";
import "../../../components/ha-svg-icon";
import {
fetchBackupConfig,
fetchBackupInfo,
generateBackup,
generateBackupWithAutomaticSettings,
type BackupConfig,
type BackupContent,
} from "../../../data/backup";
import type { ManagerStateEvent } from "../../../data/backup_manager";
import { DEFAULT_MANAGER_STATE } from "../../../data/backup_manager";
import type { CloudStatus } from "../../../data/cloud";
import "../../../layouts/hass-subpage";
import "../../../layouts/hass-tabs-subpage-data-table";
import { haStyle } from "../../../resources/styles";
import type { HomeAssistant, Route } from "../../../types";
import "./components/ha-backup-summary-card";
import "./components/ha-backup-summary-progress";
import "./components/ha-backup-summary-status";
import "./components/overview/ha-backup-overview-backups";
import "./components/overview/ha-backup-overview-settings";
import { showBackupOnboardingDialog } from "./dialogs/show-dialog-backup_onboarding";
import { showGenerateBackupDialog } from "./dialogs/show-dialog-generate-backup";
import { showNewBackupDialog } from "./dialogs/show-dialog-new-backup";
import { showUploadBackupDialog } from "./dialogs/show-dialog-upload-backup";
@customElement("ha-config-backup-overview")
class HaConfigBackupOverview extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public cloudStatus!: CloudStatus;
@property({ type: Boolean }) public narrow = false;
@property({ attribute: false }) public route!: Route;
@state() private _manager: ManagerStateEvent = DEFAULT_MANAGER_STATE;
@state() private _backups: BackupContent[] = [];
@state() private _fetching = false;
@state() private _config?: BackupConfig;
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
this._fetchBackupInfo();
this._fetchBackupConfig();
}
public connectedCallback() {
super.connectedCallback();
if (this.hasUpdated) {
this._fetchBackupInfo();
this._fetchBackupConfig();
}
}
private async _uploadBackup(ev) {
if (!shouldHandleRequestSelectedEvent(ev)) {
return;
}
await showUploadBackupDialog(this, {});
}
private async _setupAutomaticBackup() {
const success = await showBackupOnboardingDialog(this, {
cloudStatus: this.cloudStatus,
});
if (!success) {
return;
}
this._fetchBackupConfig();
await generateBackupWithAutomaticSettings(this.hass);
await this._fetchBackupInfo();
}
private async _fetchBackupInfo() {
const info = await fetchBackupInfo(this.hass);
this._backups = info.backups;
}
private async _fetchBackupConfig() {
const { config } = await fetchBackupConfig(this.hass);
this._config = config;
}
private async _newBackup(): Promise<void> {
if (this._needsOnboarding) {
this._setupAutomaticBackup();
return;
}
if (!this._config) {
return;
}
const config = this._config;
const type = await showNewBackupDialog(this, { config });
if (!type) {
return;
}
if (type === "manual") {
const params = await showGenerateBackupDialog(this, {});
if (!params) {
return;
}
await generateBackup(this.hass, params);
await this._fetchBackupInfo();
return;
}
if (type === "automatic") {
await generateBackupWithAutomaticSettings(this.hass);
await this._fetchBackupInfo();
}
}
private get _needsOnboarding() {
return !this._config?.create_backup.password;
}
protected render(): TemplateResult {
const backupInProgress =
"state" in this._manager && this._manager.state === "in_progress";
return html`
<hass-subpage
back-path="/config/system"
.hass=${this.hass}
.narrow=${this.narrow}
.header=${"Backup"}
>
<div slot="toolbar-icon">
<ha-button-menu>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
></ha-icon-button>
<ha-list-item
graphic="icon"
@request-selected=${this._uploadBackup}
>
<ha-svg-icon slot="graphic" .path=${mdiUpload}></ha-svg-icon>
Upload backup
</ha-list-item>
</ha-button-menu>
</div>
<div class="content">
${this._fetching
? html`
<ha-backup-summary-card
heading="Loading backups"
description="Your backup information is being retrieved."
status="loading"
>
</ha-backup-summary-card>
`
: backupInProgress
? html`
<ha-backup-summary-progress
.hass=${this.hass}
.manager=${this._manager}
>
</ha-backup-summary-progress>
`
: this._needsOnboarding
? html`
<ha-backup-summary-card
heading="Configure automatic backups"
description="Have a one-click backup automation with selected data and locations."
has-action
status="info"
>
<ha-button
slot="action"
@click=${this._setupAutomaticBackup}
>
Set up automatic backups
</ha-button>
</ha-backup-summary-card>
`
: html`
<ha-backup-summary-status
.hass=${this.hass}
.backups=${this._backups}
>
</ha-backup-summary-status>
`}
<ha-backup-overview-backups
.hass=${this.hass}
.backups=${this._backups}
></ha-backup-overview-backups>
${!this._needsOnboarding
? html`
<ha-backup-overview-settings
.hass=${this.hass}
.config=${this._config!}
></ha-backup-overview-settings>
`
: nothing}
</div>
<ha-fab
slot="fab"
?disabled=${backupInProgress}
.label=${"Backup now"}
extended
@click=${this._newBackup}
>
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</ha-fab>
</hass-subpage>
`;
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
.content {
padding: 28px 20px 0;
max-width: 690px;
margin: 0 auto;
gap: 24px;
display: flex;
flex-direction: column;
margin-bottom: 24px;
margin-bottom: 72px;
}
.card-actions {
display: flex;
justify-content: flex-end;
}
.card-content {
padding-left: 0;
padding-right: 0;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-config-backup-overview": HaConfigBackupOverview;
}
}

View File

@ -15,12 +15,12 @@ import {
import type { CloudStatus } from "../../../data/cloud";
import "../../../layouts/hass-subpage";
import type { HomeAssistant } from "../../../types";
import "./components/ha-backup-config-agents";
import "./components/ha-backup-config-data";
import type { BackupConfigData } from "./components/ha-backup-config-data";
import "./components/ha-backup-config-encryption-key";
import "./components/ha-backup-config-schedule";
import type { BackupConfigSchedule } from "./components/ha-backup-config-schedule";
import "./components/config/ha-backup-config-agents";
import "./components/config/ha-backup-config-data";
import type { BackupConfigData } from "./components/config/ha-backup-config-data";
import "./components/config/ha-backup-config-encryption-key";
import "./components/config/ha-backup-config-schedule";
import type { BackupConfigSchedule } from "./components/config/ha-backup-config-schedule";
const INITIAL_BACKUP_CONFIG: BackupConfig = {
create_backup: {

View File

@ -5,7 +5,8 @@ import type { RouterOptions } from "../../../layouts/hass-router-page";
import { HassRouterPage } from "../../../layouts/hass-router-page";
import "../../../layouts/hass-tabs-subpage-data-table";
import type { HomeAssistant } from "../../../types";
import "./ha-config-backup-dashboard";
import "./ha-config-backup-overview";
import "./ha-config-backup-backups";
@customElement("ha-config-backup")
class HaConfigBackup extends HassRouterPage {
@ -16,10 +17,14 @@ class HaConfigBackup extends HassRouterPage {
@property({ type: Boolean }) public narrow = false;
protected routerOptions: RouterOptions = {
defaultPage: "dashboard",
defaultPage: "overview",
routes: {
dashboard: {
tag: "ha-config-backup-dashboard",
overview: {
tag: "ha-config-backup-overview",
cache: true,
},
backups: {
tag: "ha-config-backup-backups",
cache: true,
},
details: {