diff --git a/src/components/ha-mount-picker.ts b/src/components/ha-mount-picker.ts index ab861ac90a..61a1585b99 100644 --- a/src/components/ha-mount-picker.ts +++ b/src/components/ha-mount-picker.ts @@ -1,5 +1,6 @@ import { mdiBackupRestore, mdiFolder, mdiHarddisk, mdiPlayBox } from "@mdi/js"; -import { html, LitElement, nothing } from "lit"; +import type { CSSResultGroup } 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"; @@ -173,6 +174,16 @@ class HaMountPicker extends LitElement { fireEvent(this, "change"); }, 0); } + + static get styles(): CSSResultGroup { + return [ + css` + ha-select { + width: 100%; + } + `, + ]; + } } declare global { diff --git a/src/data/selector.ts b/src/data/selector.ts index e63f396ec9..7228624d70 100644 --- a/src/data/selector.ts +++ b/src/data/selector.ts @@ -68,7 +68,8 @@ export type Selector = | TTSVoiceSelector | UiActionSelector | UiColorSelector - | UiStateContentSelector; + | UiStateContentSelector + | BackupLocationSelector; export interface ActionSelector { // eslint-disable-next-line @typescript-eslint/ban-types diff --git a/src/panels/config/backup/components/config/ha-backup-config-agents.ts b/src/panels/config/backup/components/config/ha-backup-config-agents.ts index f8245a891f..f20aa8878b 100644 --- a/src/panels/config/backup/components/config/ha-backup-config-agents.ts +++ b/src/panels/config/backup/components/config/ha-backup-config-agents.ts @@ -48,6 +48,13 @@ class HaBackupConfigAgents extends LitElement { return this.value ?? DEFAULT_AGENTS; } + private _description(agentId: string) { + if (agentId === CLOUD_AGENT) { + return "It stores one backup. The oldest backups are deleted."; + } + return ""; + } + protected render() { return html` ${this._agentIds.length > 0 @@ -60,6 +67,7 @@ class HaBackupConfigAgents extends LitElement { agentId, this._agentIds ); + const description = this._description(agentId); return html` ${isLocalAgent(agentId) @@ -82,13 +90,8 @@ class HaBackupConfigAgents extends LitElement { /> `}
${name}
- ${agentId === CLOUD_AGENT - ? html` -
- It stores one backup. The oldest backups are - deleted. -
- ` + ${description + ? html`
${description}
` : nothing} ${this.forceHomeAssistant - ? html`Learn more` + ? nothing : html` -
All, including new
+
All
None
@@ -276,6 +279,7 @@ class HaBackupConfigData extends LitElement { .value=${data.addons} @value-changed=${this._addonsChanged} .addons=${this._addons} + .hideVersion=${this.hideAddonVersion} > ` diff --git a/src/panels/config/backup/components/config/ha-backup-config-schedule.ts b/src/panels/config/backup/components/config/ha-backup-config-schedule.ts index 64922e2c73..aef76ddb2a 100644 --- a/src/panels/config/backup/components/config/ha-backup-config-schedule.ts +++ b/src/panels/config/backup/components/config/ha-backup-config-schedule.ts @@ -23,7 +23,6 @@ const MAX_VALUE = 50; enum RetentionPreset { COPIES_3 = "copies_3", - DAYS_7 = "days_7", FOREVER = "forever", CUSTOM = "custom", } @@ -38,7 +37,6 @@ const RETENTION_PRESETS: Record< RetentionData > = { copies_3: { type: "copies", value: 3 }, - days_7: { type: "days", value: 7 }, forever: { type: "days", value: 0 }, }; @@ -176,7 +174,7 @@ class HaBackupConfigSchedule extends LitElement {
- Maximum copies + Backups to keep The number of backups that are saved @@ -186,13 +184,10 @@ class HaBackupConfigSchedule extends LitElement { .value=${this._retentionPreset} > -
Latest 3 copies
-
- -
Keep 7 days
+
3 backups
-
Keep forever
+
All backups
Custom
@@ -223,7 +218,7 @@ class HaBackupConfigSchedule extends LitElement {
days
-
copies
+
backups
diff --git a/src/panels/config/backup/components/ha-backup-addons-picker.ts b/src/panels/config/backup/components/ha-backup-addons-picker.ts index 1401f44172..938593de1d 100644 --- a/src/panels/config/backup/components/ha-backup-addons-picker.ts +++ b/src/panels/config/backup/components/ha-backup-addons-picker.ts @@ -26,6 +26,9 @@ export class HaBackupAddonsPicker extends LitElement { @property({ attribute: false }) public value?: string[]; + @property({ attribute: "hide-version", type: Boolean }) + public hideVersion = false; + protected render() { return html`
@@ -35,7 +38,7 @@ export class HaBackupAddonsPicker extends LitElement { a.slug === item.slug)?.icon ? `/api/hassio/addons/${item.slug}/icon` diff --git a/src/panels/config/backup/components/ha-backup-formfield-label.ts b/src/panels/config/backup/components/ha-backup-formfield-label.ts index d2d0036f5b..def87edf35 100644 --- a/src/panels/config/backup/components/ha-backup-formfield-label.ts +++ b/src/panels/config/backup/components/ha-backup-formfield-label.ts @@ -43,7 +43,7 @@ class SupervisorFormfieldLabel extends LitElement { margin-right: 4px; margin-inline-end: 4px; margin-inline-start: initial; - font-size: 16px; + font-size: 14px; font-weight: 400; line-height: 24px; letter-spacing: 0.5px; diff --git a/src/panels/config/backup/components/overview/ha-backup-overview-backups.ts b/src/panels/config/backup/components/overview/ha-backup-overview-backups.ts index 82f72082c0..d2a6accbed 100644 --- a/src/panels/config/backup/components/overview/ha-backup-overview-backups.ts +++ b/src/panels/config/backup/components/overview/ha-backup-overview-backups.ts @@ -102,8 +102,7 @@ class HaBackupOverviewBackups extends LitElement { gap: 24px; display: flex; flex-direction: column; - margin-bottom: 24px; - margin-bottom: 72px; + margin-bottom: calc(72px + env(safe-area-inset-bottom)); } .card-actions { display: flex; diff --git a/src/panels/config/backup/components/overview/ha-backup-overview-onboarding.ts b/src/panels/config/backup/components/overview/ha-backup-overview-onboarding.ts index b030c8b7d8..b12fd9376f 100644 --- a/src/panels/config/backup/components/overview/ha-backup-overview-onboarding.ts +++ b/src/panels/config/backup/components/overview/ha-backup-overview-onboarding.ts @@ -38,7 +38,7 @@ class HaBackupOverviewBackups extends LitElement { Backups are essential to a reliable smart home. They protect your setup against failures and allows you to quickly have a working system again. It is recommended to create a daily backup and keep - copies of the last 3 days on two different locations. And one of + backups of the last 3 days on two different locations. And one of them is off-site.

diff --git a/src/panels/config/backup/components/overview/ha-backup-overview-settings.ts b/src/panels/config/backup/components/overview/ha-backup-overview-settings.ts index 794e881982..9e3ebd0206 100644 --- a/src/panels/config/backup/components/overview/ha-backup-overview-settings.ts +++ b/src/panels/config/backup/components/overview/ha-backup-overview-settings.ts @@ -33,7 +33,7 @@ class HaBackupBackupsSummary extends LitElement { let copiesText = "and keep all backups"; if (copies) { - copiesText = `and keep the latest ${copies} copie(s)`; + copiesText = `and keep the latest ${copies} backup(s)`; } else if (days) { copiesText = `and keep backups for ${days} day(s)`; } @@ -69,7 +69,7 @@ class HaBackupBackupsSummary extends LitElement { private _addonsDescription(config: BackupConfig): string { if (config.create_backup.include_all_addons) { - return "All add-ons, including new"; + return "All add-ons"; } if (config.create_backup.include_addons?.length) { return `${config.create_backup.include_addons.length} add-ons`; diff --git a/src/panels/config/backup/dialogs/dialog-backup-onboarding.ts b/src/panels/config/backup/dialogs/dialog-backup-onboarding.ts index 3a17c454f9..b70ed88102 100644 --- a/src/panels/config/backup/dialogs/dialog-backup-onboarding.ts +++ b/src/panels/config/backup/dialogs/dialog-backup-onboarding.ts @@ -82,8 +82,6 @@ class DialogBackupOnboarding extends LitElement implements HassDialog { @state() private _step?: Step; - @state() private _steps: Step[] = []; - @state() private _params?: BackupOnboardingDialogParams; @query("ha-md-dialog") private _dialog!: HaMdDialog; @@ -92,8 +90,7 @@ class DialogBackupOnboarding extends LitElement implements HassDialog { public showDialog(params: BackupOnboardingDialogParams): void { this._params = params; - this._steps = params.showIntro ? STEPS.concat() : STEPS.slice(1); - this._step = this._steps[0]; + this._step = STEPS[0]; this._config = RECOMMENDED_CONFIG; const agents: string[] = []; @@ -128,7 +125,6 @@ class DialogBackupOnboarding extends LitElement implements HassDialog { } this._opened = false; this._step = undefined; - this._steps = []; this._config = undefined; this._params = undefined; } @@ -170,19 +166,19 @@ class DialogBackupOnboarding extends LitElement implements HassDialog { } private _previousStep() { - const index = this._steps.indexOf(this._step!); + const index = STEPS.indexOf(this._step!); if (index === 0) { return; } - this._step = this._steps[index - 1]; + this._step = STEPS[index - 1]; } private _nextStep() { - const index = this._steps.indexOf(this._step!); - if (index === this._steps.length - 1) { + const index = STEPS.indexOf(this._step!); + if (index === STEPS.length - 1) { return; } - this._step = this._steps[index + 1]; + this._step = STEPS[index + 1]; } protected render() { @@ -190,8 +186,8 @@ class DialogBackupOnboarding extends LitElement implements HassDialog { return nothing; } - const isLastStep = this._step === this._steps[this._steps.length - 1]; - const isFirstStep = this._step === this._steps[0]; + const isLastStep = this._step === STEPS[STEPS.length - 1]; + const isFirstStep = this._step === STEPS[0]; return html` @@ -224,7 +220,7 @@ class DialogBackupOnboarding extends LitElement implements HassDialog { @click=${this._done} .disabled=${!this._isStepValid()} > - Save + Save and create backup ` : html` @@ -296,7 +292,7 @@ class DialogBackupOnboarding extends LitElement implements HassDialog { Backups are essential to a reliable smart home. They protect your setup against failures and allows you to quickly have a working system again. It is recommended to create a daily backup and keep - copies of the last 3 days on two different locations. And one of + backups of the last 3 days on two different locations. And one of them is off-site.

@@ -331,7 +327,7 @@ class DialogBackupOnboarding extends LitElement implements HassDialog { case "setup": return html`

- It is recommended to create a daily backup and keep copies of the + It is recommended to create a daily backup and keep backups of the last 3 days on two different locations. And one of them is off-site.

@@ -355,7 +351,7 @@ class DialogBackupOnboarding extends LitElement implements HassDialog { return html`

Let Home Assistant take care of your backups by creating a scheduled - backup that also removes older copies. + backup that also removes older backups.

`; case "locations": @@ -470,9 +467,7 @@ class DialogBackupOnboarding extends LitElement implements HassDialog { ha-md-dialog { width: 90vw; max-width: 560px; - } - div[slot="content"] { - margin-top: -16px; + --dialog-content-padding: 8px 24px; } ha-md-list { background: none; diff --git a/src/panels/config/backup/dialogs/dialog-change-backup-encryption-key.ts b/src/panels/config/backup/dialogs/dialog-change-backup-encryption-key.ts index 7dbdaed43a..ac8ae0bae6 100644 --- a/src/panels/config/backup/dialogs/dialog-change-backup-encryption-key.ts +++ b/src/panels/config/backup/dialogs/dialog-change-backup-encryption-key.ts @@ -141,6 +141,13 @@ class DialogChangeBackupEncryptionKey extends LitElement implements HassDialog { have access to all your current backups. All next backups will use the new encryption key.

+
+

${this._params?.currentKey}

+ +
Download old emergency kit @@ -202,6 +209,16 @@ class DialogChangeBackupEncryptionKey extends LitElement implements HassDialog { }); } + private _copyOldKeyToClipboard() { + if (!this._params?.currentKey) { + return; + } + copyToClipboard(this._params.currentKey); + showToast(this, { + message: this.hass.localize("ui.common.copied_clipboard"), + }); + } + private _downloadOld() { if (!this._params?.currentKey) { return; @@ -240,9 +257,7 @@ class DialogChangeBackupEncryptionKey extends LitElement implements HassDialog { ha-md-dialog { width: 90vw; max-width: 560px; - } - div[slot="content"] { - margin-top: -16px; + --dialog-content-padding: 8px 24px; } ha-md-list { background: none; diff --git a/src/panels/config/backup/dialogs/dialog-local-backup-location.ts b/src/panels/config/backup/dialogs/dialog-local-backup-location.ts new file mode 100644 index 0000000000..9ef762c8a3 --- /dev/null +++ b/src/panels/config/backup/dialogs/dialog-local-backup-location.ts @@ -0,0 +1,151 @@ +import type { CSSResultGroup } from "lit"; +import { css, html, LitElement, nothing } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { fireEvent } from "../../../../common/dom/fire_event"; +import "../../../../components/ha-button"; +import { createCloseHeading } from "../../../../components/ha-dialog"; +import "../../../../components/ha-form/ha-form"; +import type { + HaFormSchema, + SchemaUnion, +} from "../../../../components/ha-form/types"; +import { extractApiErrorMessage } from "../../../../data/hassio/common"; +import { changeMountOptions } from "../../../../data/supervisor/mounts"; +import { haStyle, haStyleDialog } from "../../../../resources/styles"; +import type { HomeAssistant } from "../../../../types"; +import type { LocalBackupLocationDialogParams } from "./show-dialog-local-backup-location"; + +const SCHEMA = [ + { + name: "default_backup_mount", + required: true, + selector: { backup_location: {} }, + }, +] as const satisfies HaFormSchema[]; + +@customElement("dialog-local-backup-location") +class LocalBackupLocationDialog extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @state() private _dialogParams?: LocalBackupLocationDialogParams; + + @state() private _data?: { default_backup_mount: string | null }; + + @state() private _waiting?: boolean; + + @state() private _error?: string; + + public async showDialog( + dialogParams: LocalBackupLocationDialogParams + ): Promise { + this._dialogParams = dialogParams; + } + + public closeDialog(): void { + this._data = undefined; + this._error = undefined; + this._waiting = undefined; + this._dialogParams = undefined; + fireEvent(this, "dialog-closed", { dialog: this.localName }); + } + + protected render() { + if (!this._dialogParams) { + return nothing; + } + return html` + + ${this._error + ? html`${this._error}` + : nothing} + +

+ ${this.hass.localize( + `ui.panel.config.backup.dialogs.local_backup_location.description` + )} +

+ + + ${this.hass.localize("ui.common.cancel")} + + + ${this.hass.localize("ui.common.save")} + +
+ `; + } + + private _computeLabelCallback = ( + schema: SchemaUnion + ): string => + this.hass.localize( + `ui.panel.config.backup.dialogs.local_backup_location.options.${schema.name}.name` + ) || schema.name; + + private _valueChanged(ev: CustomEvent) { + const newLocation = ev.detail.value.default_backup_mount; + this._data = { + default_backup_mount: newLocation === "/backup" ? null : newLocation, + }; + } + + private async _changeMount() { + if (!this._data) { + return; + } + this._error = undefined; + this._waiting = true; + try { + await changeMountOptions(this.hass, this._data); + } catch (err: any) { + this._error = extractApiErrorMessage(err); + this._waiting = false; + return; + } + this.closeDialog(); + } + + static get styles(): CSSResultGroup { + return [ + haStyle, + haStyleDialog, + css` + ha-dialog { + --mdc-dialog-max-width: 500px; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "dialog-local-backup-location": LocalBackupLocationDialog; + } +} diff --git a/src/panels/config/backup/dialogs/dialog-new-backup.ts b/src/panels/config/backup/dialogs/dialog-new-backup.ts index 0ac81c454a..09d54bff8d 100644 --- a/src/panels/config/backup/dialogs/dialog-new-backup.ts +++ b/src/panels/config/backup/dialogs/dialog-new-backup.ts @@ -115,9 +115,6 @@ class DialogNewBackup extends LitElement implements HassDialog { --dialog-content-padding: 0; max-width: 500px; } - div[slot="content"] { - margin-top: -16px; - } @media all and (max-width: 450px), all and (max-height: 500px) { ha-md-dialog { max-width: none; diff --git a/src/panels/config/backup/dialogs/dialog-restore-backup-encryption-key.ts b/src/panels/config/backup/dialogs/dialog-restore-backup-encryption-key.ts index 54e8f01cd7..fca3c987ed 100644 --- a/src/panels/config/backup/dialogs/dialog-restore-backup-encryption-key.ts +++ b/src/panels/config/backup/dialogs/dialog-restore-backup-encryption-key.ts @@ -157,7 +157,11 @@ class DialogRestoreBackupEncryptionKey
Cancel - + Restore
@@ -221,10 +225,14 @@ class DialogRestoreBackupEncryptionKey ha-md-dialog { max-width: 500px; width: 100%; + --dialog-content-padding: 8px 24px; } .content p { margin: 0 0 16px; } + ha-button.danger { + --mdc-theme-primary: var(--error-color); + } `, ]; } diff --git a/src/panels/config/backup/dialogs/dialog-set-backup-encryption-key.ts b/src/panels/config/backup/dialogs/dialog-set-backup-encryption-key.ts index 2d6fe6b439..ccb5c7c87b 100644 --- a/src/panels/config/backup/dialogs/dialog-set-backup-encryption-key.ts +++ b/src/panels/config/backup/dialogs/dialog-set-backup-encryption-key.ts @@ -193,9 +193,7 @@ class DialogSetBackupEncryptionKey extends LitElement implements HassDialog { ha-md-dialog { width: 90vw; max-width: 500px; - } - div[slot="content"] { - margin-top: -16px; + --dialog-content-padding: 8px 24px; } ha-md-list { background: none; diff --git a/src/panels/config/backup/dialogs/show-dialog-backup_onboarding.ts b/src/panels/config/backup/dialogs/show-dialog-backup_onboarding.ts index 7955d29ddb..578f80bf13 100644 --- a/src/panels/config/backup/dialogs/show-dialog-backup_onboarding.ts +++ b/src/panels/config/backup/dialogs/show-dialog-backup_onboarding.ts @@ -4,7 +4,6 @@ import type { CloudStatus } from "../../../../data/cloud"; export interface BackupOnboardingDialogParams { submit?: (value: boolean) => void; cancel?: () => void; - showIntro?: boolean; cloudStatus: CloudStatus; } diff --git a/src/panels/config/backup/dialogs/show-dialog-local-backup-location.ts b/src/panels/config/backup/dialogs/show-dialog-local-backup-location.ts new file mode 100644 index 0000000000..409e49f3ad --- /dev/null +++ b/src/panels/config/backup/dialogs/show-dialog-local-backup-location.ts @@ -0,0 +1,14 @@ +import { fireEvent } from "../../../../common/dom/fire_event"; + +export interface LocalBackupLocationDialogParams {} + +export const showLocalBackupLocationDialog = ( + element: HTMLElement, + dialogParams: LocalBackupLocationDialogParams +): void => { + fireEvent(element, "show-dialog", { + dialogTag: "dialog-local-backup-location", + dialogImport: () => import("./dialog-local-backup-location"), + dialogParams, + }); +}; diff --git a/src/panels/config/backup/ha-config-backup-details.ts b/src/panels/config/backup/ha-config-backup-details.ts index 86df796b36..74a4dd74b8 100644 --- a/src/panels/config/backup/ha-config-backup-details.ts +++ b/src/panels/config/backup/ha-config-backup-details.ts @@ -119,26 +119,6 @@ class HaConfigBackupDetails extends LitElement { : !this._backup ? html`` : html` - -
- - -
-
- - Restore - -
-
@@ -159,6 +139,27 @@ class HaConfigBackupDetails extends LitElement {
+ +
+ + +
+
+ + Restore + +
+
@@ -172,6 +173,8 @@ class HaConfigBackupDetails extends LitElement { this._backup!.agent_ids! ); + const isLocal = isLocalAgent(agentId); + return html` ${isLocalAgent(agentId) @@ -204,7 +207,11 @@ class HaConfigBackupDetails extends LitElement { > - ${success ? "Backup synced" : "Backup failed"} + ${success + ? isLocal + ? "Backup created" + : "Backup uploaded" + : "Backup failed"}
${success @@ -390,6 +397,9 @@ class HaConfigBackupDetails extends LitElement { .warning ha-svg-icon { color: var(--error-color); } + ha-button.danger { + --mdc-theme-primary: var(--error-color); + } ha-backup-data-picker { display: block; } diff --git a/src/panels/config/backup/ha-config-backup-overview.ts b/src/panels/config/backup/ha-config-backup-overview.ts index afa203244f..16247dbcac 100644 --- a/src/panels/config/backup/ha-config-backup-overview.ts +++ b/src/panels/config/backup/ha-config-backup-overview.ts @@ -1,7 +1,8 @@ -import { mdiDotsVertical, mdiPlus, mdiUpload } from "@mdi/js"; +import { mdiDotsVertical, mdiHarddisk, mdiPlus, mdiUpload } from "@mdi/js"; import type { CSSResultGroup, TemplateResult } from "lit"; import { css, html, LitElement, nothing } from "lit"; import { customElement, property } from "lit/decorators"; +import { isComponentLoaded } from "../../../common/config/is_component_loaded"; import { fireEvent } from "../../../common/dom/fire_event"; import { shouldHandleRequestSelectedEvent } from "../../../common/mwc/handle-request-selected-event"; import "../../../components/ha-button"; @@ -34,6 +35,7 @@ import "./components/overview/ha-backup-overview-settings"; import "./components/overview/ha-backup-overview-summary"; import { showBackupOnboardingDialog } from "./dialogs/show-dialog-backup_onboarding"; import { showGenerateBackupDialog } from "./dialogs/show-dialog-generate-backup"; +import { showLocalBackupLocationDialog } from "./dialogs/show-dialog-local-backup-location"; import { showNewBackupDialog } from "./dialogs/show-dialog-new-backup"; import { showUploadBackupDialog } from "./dialogs/show-dialog-upload-backup"; @@ -63,15 +65,22 @@ class HaConfigBackupOverview extends LitElement { await showUploadBackupDialog(this, {}); } - private _handleOnboardingButtonClick(ev) { - ev.stopPropagation(); - this._setupAutomaticBackup(false); + private async _changeLocalLocation(ev) { + if (!shouldHandleRequestSelectedEvent(ev)) { + return; + } + + showLocalBackupLocationDialog(this, {}); } - private async _setupAutomaticBackup(showIntro: boolean) { + private _handleOnboardingButtonClick(ev) { + ev.stopPropagation(); + this._setupAutomaticBackup(); + } + + private async _setupAutomaticBackup() { const success = await showBackupOnboardingDialog(this, { cloudStatus: this.cloudStatus, - showIntro: showIntro, }); if (!success) { return; @@ -84,7 +93,7 @@ class HaConfigBackupOverview extends LitElement { private async _newBackup(): Promise { if (this._needsOnboarding) { - this._setupAutomaticBackup(true); + this._setupAutomaticBackup(); return; } @@ -125,6 +134,8 @@ class HaConfigBackupOverview extends LitElement { const backupInProgress = "state" in this.manager && this.manager.state === "in_progress"; + const isHassio = isComponentLoaded(this.hass, "hassio"); + return html` + ${isHassio + ? html` + + Change local location + ` + : nothing}

Let Home Assistant take care of your backups by creating a - scheduled backup that also removes older copies. + scheduled backup that also removes older backups.

diff --git a/src/translations/en.json b/src/translations/en.json index 3c837c68ca..18c3b1013e 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -2209,6 +2209,17 @@ }, "picker": { "search": "Search backups" + }, + "dialogs": { + "local_backup_location": { + "title": "Change local backup location", + "description": "Change the default location where local backups are stored on your Home Assistant instance.", + "options": { + "default_backup_mount": { + "name": "Default location" + } + } + } } }, "tag": {