Multiple backup adjustments (#23361)

* Always show welcome screen during onboarding

* Fix content overflow in dialog

* Rename copies to backups, drop 7 days

* Remove including new and learn more button

* Some other fixes

* Allow changing default local location from new backup page

* Dont show addon version in settings and onboarding

* Add margin for ios

* Display old encryption key before change

* Refactor descriptipn
This commit is contained in:
Paul Bottein 2024-12-20 15:52:55 +01:00 committed by GitHub
parent c66f5e2d8a
commit fb228dc918
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 324 additions and 86 deletions

View File

@ -1,5 +1,6 @@
import { mdiBackupRestore, mdiFolder, mdiHarddisk, mdiPlayBox } from "@mdi/js"; 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 { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../common/config/is_component_loaded"; import { isComponentLoaded } from "../common/config/is_component_loaded";
@ -173,6 +174,16 @@ class HaMountPicker extends LitElement {
fireEvent(this, "change"); fireEvent(this, "change");
}, 0); }, 0);
} }
static get styles(): CSSResultGroup {
return [
css`
ha-select {
width: 100%;
}
`,
];
}
} }
declare global { declare global {

View File

@ -68,7 +68,8 @@ export type Selector =
| TTSVoiceSelector | TTSVoiceSelector
| UiActionSelector | UiActionSelector
| UiColorSelector | UiColorSelector
| UiStateContentSelector; | UiStateContentSelector
| BackupLocationSelector;
export interface ActionSelector { export interface ActionSelector {
// eslint-disable-next-line @typescript-eslint/ban-types // eslint-disable-next-line @typescript-eslint/ban-types

View File

@ -48,6 +48,13 @@ class HaBackupConfigAgents extends LitElement {
return this.value ?? DEFAULT_AGENTS; 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() { protected render() {
return html` return html`
${this._agentIds.length > 0 ${this._agentIds.length > 0
@ -60,6 +67,7 @@ class HaBackupConfigAgents extends LitElement {
agentId, agentId,
this._agentIds this._agentIds
); );
const description = this._description(agentId);
return html` return html`
<ha-md-list-item> <ha-md-list-item>
${isLocalAgent(agentId) ${isLocalAgent(agentId)
@ -82,13 +90,8 @@ class HaBackupConfigAgents extends LitElement {
/> />
`} `}
<div slot="headline">${name}</div> <div slot="headline">${name}</div>
${agentId === CLOUD_AGENT ${description
? html` ? html`<div slot="supporting-text">${description}</div>`
<div slot="supporting-text">
It stores one backup. The oldest backups are
deleted.
</div>
`
: nothing} : nothing}
<ha-switch <ha-switch
slot="end" slot="end"

View File

@ -60,6 +60,9 @@ class HaBackupConfigData extends LitElement {
@property({ type: Boolean, attribute: "force-home-assistant" }) @property({ type: Boolean, attribute: "force-home-assistant" })
public forceHomeAssistant = false; public forceHomeAssistant = false;
@property({ attribute: "hide-addon-version", type: Boolean })
public hideAddonVersion = false;
@state() private value?: BackupConfigData; @state() private value?: BackupConfigData;
@state() private _addons: BackupAddonItem[] = []; @state() private _addons: BackupAddonItem[] = [];
@ -156,7 +159,7 @@ class HaBackupConfigData extends LitElement {
The bare minimum needed to restore your system. The bare minimum needed to restore your system.
</span> </span>
${this.forceHomeAssistant ${this.forceHomeAssistant
? html`<ha-button slot="end">Learn more</ha-button>` ? nothing
: html` : html`
<ha-switch <ha-switch
id="homeassistant" id="homeassistant"
@ -253,7 +256,7 @@ class HaBackupConfigData extends LitElement {
.value=${data.addons_mode} .value=${data.addons_mode}
> >
<ha-md-select-option value="all"> <ha-md-select-option value="all">
<div slot="headline">All, including new</div> <div slot="headline">All</div>
</ha-md-select-option> </ha-md-select-option>
<ha-md-select-option value="none"> <ha-md-select-option value="none">
<div slot="headline">None</div> <div slot="headline">None</div>
@ -276,6 +279,7 @@ class HaBackupConfigData extends LitElement {
.value=${data.addons} .value=${data.addons}
@value-changed=${this._addonsChanged} @value-changed=${this._addonsChanged}
.addons=${this._addons} .addons=${this._addons}
.hideVersion=${this.hideAddonVersion}
></ha-backup-addons-picker> ></ha-backup-addons-picker>
</ha-expansion-panel> </ha-expansion-panel>
` `

View File

@ -23,7 +23,6 @@ const MAX_VALUE = 50;
enum RetentionPreset { enum RetentionPreset {
COPIES_3 = "copies_3", COPIES_3 = "copies_3",
DAYS_7 = "days_7",
FOREVER = "forever", FOREVER = "forever",
CUSTOM = "custom", CUSTOM = "custom",
} }
@ -38,7 +37,6 @@ const RETENTION_PRESETS: Record<
RetentionData RetentionData
> = { > = {
copies_3: { type: "copies", value: 3 }, copies_3: { type: "copies", value: 3 },
days_7: { type: "days", value: 7 },
forever: { type: "days", value: 0 }, forever: { type: "days", value: 0 },
}; };
@ -176,7 +174,7 @@ class HaBackupConfigSchedule extends LitElement {
</ha-md-select> </ha-md-select>
</ha-md-list-item> </ha-md-list-item>
<ha-md-list-item> <ha-md-list-item>
<span slot="headline">Maximum copies</span> <span slot="headline">Backups to keep</span>
<span slot="supporting-text"> <span slot="supporting-text">
The number of backups that are saved The number of backups that are saved
</span> </span>
@ -186,13 +184,10 @@ class HaBackupConfigSchedule extends LitElement {
.value=${this._retentionPreset} .value=${this._retentionPreset}
> >
<ha-md-select-option .value=${RetentionPreset.COPIES_3}> <ha-md-select-option .value=${RetentionPreset.COPIES_3}>
<div slot="headline">Latest 3 copies</div> <div slot="headline">3 backups</div>
</ha-md-select-option>
<ha-md-select-option .value=${RetentionPreset.DAYS_7}>
<div slot="headline">Keep 7 days</div>
</ha-md-select-option> </ha-md-select-option>
<ha-md-select-option .value=${RetentionPreset.FOREVER}> <ha-md-select-option .value=${RetentionPreset.FOREVER}>
<div slot="headline">Keep forever</div> <div slot="headline">All backups</div>
</ha-md-select-option> </ha-md-select-option>
<ha-md-select-option .value=${RetentionPreset.CUSTOM}> <ha-md-select-option .value=${RetentionPreset.CUSTOM}>
<div slot="headline">Custom</div> <div slot="headline">Custom</div>
@ -223,7 +218,7 @@ class HaBackupConfigSchedule extends LitElement {
<div slot="headline">days</div> <div slot="headline">days</div>
</ha-md-select-option> </ha-md-select-option>
<ha-md-select-option .value=${"copies"}> <ha-md-select-option .value=${"copies"}>
<div slot="headline">copies</div> <div slot="headline">backups</div>
</ha-md-select-option> </ha-md-select-option>
</ha-md-select> </ha-md-select>
</ha-md-list-item> </ha-md-list-item>

View File

@ -26,6 +26,9 @@ export class HaBackupAddonsPicker extends LitElement {
@property({ attribute: false }) public value?: string[]; @property({ attribute: false }) public value?: string[];
@property({ attribute: "hide-version", type: Boolean })
public hideVersion = false;
protected render() { protected render() {
return html` return html`
<div class="items"> <div class="items">
@ -35,7 +38,7 @@ export class HaBackupAddonsPicker extends LitElement {
<ha-backup-formfield-label <ha-backup-formfield-label
slot="label" slot="label"
.label=${item.name} .label=${item.name}
.version=${item.version} .version=${this.hideVersion ? undefined : item.version}
.iconPath=${item.iconPath || mdiPuzzle} .iconPath=${item.iconPath || mdiPuzzle}
.imageUrl=${this.addons?.find((a) => a.slug === item.slug)?.icon .imageUrl=${this.addons?.find((a) => a.slug === item.slug)?.icon
? `/api/hassio/addons/${item.slug}/icon` ? `/api/hassio/addons/${item.slug}/icon`

View File

@ -43,7 +43,7 @@ class SupervisorFormfieldLabel extends LitElement {
margin-right: 4px; margin-right: 4px;
margin-inline-end: 4px; margin-inline-end: 4px;
margin-inline-start: initial; margin-inline-start: initial;
font-size: 16px; font-size: 14px;
font-weight: 400; font-weight: 400;
line-height: 24px; line-height: 24px;
letter-spacing: 0.5px; letter-spacing: 0.5px;

View File

@ -102,8 +102,7 @@ class HaBackupOverviewBackups extends LitElement {
gap: 24px; gap: 24px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
margin-bottom: 24px; margin-bottom: calc(72px + env(safe-area-inset-bottom));
margin-bottom: 72px;
} }
.card-actions { .card-actions {
display: flex; display: flex;

View File

@ -38,7 +38,7 @@ class HaBackupOverviewBackups extends LitElement {
Backups are essential to a reliable smart home. They protect your Backups are essential to a reliable smart home. They protect your
setup against failures and allows you to quickly have a working setup against failures and allows you to quickly have a working
system again. It is recommended to create a daily backup and keep 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. them is off-site.
</p> </p>
</div> </div>

View File

@ -33,7 +33,7 @@ class HaBackupBackupsSummary extends LitElement {
let copiesText = "and keep all backups"; let copiesText = "and keep all backups";
if (copies) { if (copies) {
copiesText = `and keep the latest ${copies} copie(s)`; copiesText = `and keep the latest ${copies} backup(s)`;
} else if (days) { } else if (days) {
copiesText = `and keep backups for ${days} day(s)`; copiesText = `and keep backups for ${days} day(s)`;
} }
@ -69,7 +69,7 @@ class HaBackupBackupsSummary extends LitElement {
private _addonsDescription(config: BackupConfig): string { private _addonsDescription(config: BackupConfig): string {
if (config.create_backup.include_all_addons) { if (config.create_backup.include_all_addons) {
return "All add-ons, including new"; return "All add-ons";
} }
if (config.create_backup.include_addons?.length) { if (config.create_backup.include_addons?.length) {
return `${config.create_backup.include_addons.length} add-ons`; return `${config.create_backup.include_addons.length} add-ons`;

View File

@ -82,8 +82,6 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
@state() private _step?: Step; @state() private _step?: Step;
@state() private _steps: Step[] = [];
@state() private _params?: BackupOnboardingDialogParams; @state() private _params?: BackupOnboardingDialogParams;
@query("ha-md-dialog") private _dialog!: HaMdDialog; @query("ha-md-dialog") private _dialog!: HaMdDialog;
@ -92,8 +90,7 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
public showDialog(params: BackupOnboardingDialogParams): void { public showDialog(params: BackupOnboardingDialogParams): void {
this._params = params; this._params = params;
this._steps = params.showIntro ? STEPS.concat() : STEPS.slice(1); this._step = STEPS[0];
this._step = this._steps[0];
this._config = RECOMMENDED_CONFIG; this._config = RECOMMENDED_CONFIG;
const agents: string[] = []; const agents: string[] = [];
@ -128,7 +125,6 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
} }
this._opened = false; this._opened = false;
this._step = undefined; this._step = undefined;
this._steps = [];
this._config = undefined; this._config = undefined;
this._params = undefined; this._params = undefined;
} }
@ -170,19 +166,19 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
} }
private _previousStep() { private _previousStep() {
const index = this._steps.indexOf(this._step!); const index = STEPS.indexOf(this._step!);
if (index === 0) { if (index === 0) {
return; return;
} }
this._step = this._steps[index - 1]; this._step = STEPS[index - 1];
} }
private _nextStep() { private _nextStep() {
const index = this._steps.indexOf(this._step!); const index = STEPS.indexOf(this._step!);
if (index === this._steps.length - 1) { if (index === STEPS.length - 1) {
return; return;
} }
this._step = this._steps[index + 1]; this._step = STEPS[index + 1];
} }
protected render() { protected render() {
@ -190,8 +186,8 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
return nothing; return nothing;
} }
const isLastStep = this._step === this._steps[this._steps.length - 1]; const isLastStep = this._step === STEPS[STEPS.length - 1];
const isFirstStep = this._step === this._steps[0]; const isFirstStep = this._step === STEPS[0];
return html` return html`
<ha-md-dialog disable-cancel-action open @closed=${this.closeDialog}> <ha-md-dialog disable-cancel-action open @closed=${this.closeDialog}>
@ -224,7 +220,7 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
@click=${this._done} @click=${this._done}
.disabled=${!this._isStepValid()} .disabled=${!this._isStepValid()}
> >
Save Save and create backup
</ha-button> </ha-button>
` `
: html` : html`
@ -296,7 +292,7 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
Backups are essential to a reliable smart home. They protect your Backups are essential to a reliable smart home. They protect your
setup against failures and allows you to quickly have a working setup against failures and allows you to quickly have a working
system again. It is recommended to create a daily backup and keep 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. them is off-site.
</p> </p>
</div> </div>
@ -331,7 +327,7 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
case "setup": case "setup":
return html` return html`
<p> <p>
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. last 3 days on two different locations. And one of them is off-site.
</p> </p>
<ha-md-list class="full"> <ha-md-list class="full">
@ -355,7 +351,7 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
return html` return html`
<p> <p>
Let Home Assistant take care of your backups by creating a scheduled Let Home Assistant take care of your backups by creating a scheduled
backup that also removes older copies. backup that also removes older backups.
</p> </p>
<ha-backup-config-schedule <ha-backup-config-schedule
.hass=${this.hass} .hass=${this.hass}
@ -374,6 +370,7 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
.value=${this._dataConfig(this._config)} .value=${this._dataConfig(this._config)}
@value-changed=${this._dataChanged} @value-changed=${this._dataChanged}
force-home-assistant force-home-assistant
hide-addon-version
></ha-backup-config-data> ></ha-backup-config-data>
`; `;
case "locations": case "locations":
@ -470,9 +467,7 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
ha-md-dialog { ha-md-dialog {
width: 90vw; width: 90vw;
max-width: 560px; max-width: 560px;
} --dialog-content-padding: 8px 24px;
div[slot="content"] {
margin-top: -16px;
} }
ha-md-list { ha-md-list {
background: none; background: none;

View File

@ -141,6 +141,13 @@ class DialogChangeBackupEncryptionKey extends LitElement implements HassDialog {
have access to all your current backups. All next backups will use have access to all your current backups. All next backups will use
the new encryption key. the new encryption key.
</p> </p>
<div class="encryption-key">
<p>${this._params?.currentKey}</p>
<ha-icon-button
.path=${mdiContentCopy}
@click=${this._copyOldKeyToClipboard}
></ha-icon-button>
</div>
<ha-md-list> <ha-md-list>
<ha-md-list-item> <ha-md-list-item>
<span slot="headline">Download old emergency kit</span> <span slot="headline">Download old emergency kit</span>
@ -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() { private _downloadOld() {
if (!this._params?.currentKey) { if (!this._params?.currentKey) {
return; return;
@ -240,9 +257,7 @@ class DialogChangeBackupEncryptionKey extends LitElement implements HassDialog {
ha-md-dialog { ha-md-dialog {
width: 90vw; width: 90vw;
max-width: 560px; max-width: 560px;
} --dialog-content-padding: 8px 24px;
div[slot="content"] {
margin-top: -16px;
} }
ha-md-list { ha-md-list {
background: none; background: none;

View File

@ -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<void> {
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`
<ha-dialog
open
scrimClickAction
escapeKeyAction
.heading=${createCloseHeading(
this.hass,
this.hass.localize(
`ui.panel.config.backup.dialogs.local_backup_location.title`
)
)}
@closed=${this.closeDialog}
>
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: nothing}
<p>
${this.hass.localize(
`ui.panel.config.backup.dialogs.local_backup_location.description`
)}
</p>
<ha-form
.hass=${this.hass}
.data=${this._data}
.schema=${SCHEMA}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
dialogInitialFocus
></ha-form>
<ha-button
slot="secondaryAction"
@click=${this.closeDialog}
dialogInitialFocus
>
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button
.disabled=${this._waiting || !this._data}
slot="primaryAction"
@click=${this._changeMount}
>
${this.hass.localize("ui.common.save")}
</ha-button>
</ha-dialog>
`;
}
private _computeLabelCallback = (
schema: SchemaUnion<typeof SCHEMA>
): 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;
}
}

View File

@ -115,9 +115,6 @@ class DialogNewBackup extends LitElement implements HassDialog {
--dialog-content-padding: 0; --dialog-content-padding: 0;
max-width: 500px; max-width: 500px;
} }
div[slot="content"] {
margin-top: -16px;
}
@media all and (max-width: 450px), all and (max-height: 500px) { @media all and (max-width: 450px), all and (max-height: 500px) {
ha-md-dialog { ha-md-dialog {
max-width: none; max-width: none;

View File

@ -157,7 +157,11 @@ class DialogRestoreBackupEncryptionKey
</div> </div>
<div slot="actions"> <div slot="actions">
<ha-button @click=${this.closeDialog}>Cancel</ha-button> <ha-button @click=${this.closeDialog}>Cancel</ha-button>
<ha-button @click=${this._submit} .disabled=${!this._getKey()}> <ha-button
@click=${this._submit}
class="danger"
.disabled=${!this._getKey()}
>
Restore Restore
</ha-button> </ha-button>
</div> </div>
@ -221,10 +225,14 @@ class DialogRestoreBackupEncryptionKey
ha-md-dialog { ha-md-dialog {
max-width: 500px; max-width: 500px;
width: 100%; width: 100%;
--dialog-content-padding: 8px 24px;
} }
.content p { .content p {
margin: 0 0 16px; margin: 0 0 16px;
} }
ha-button.danger {
--mdc-theme-primary: var(--error-color);
}
`, `,
]; ];
} }

View File

@ -193,9 +193,7 @@ class DialogSetBackupEncryptionKey extends LitElement implements HassDialog {
ha-md-dialog { ha-md-dialog {
width: 90vw; width: 90vw;
max-width: 500px; max-width: 500px;
} --dialog-content-padding: 8px 24px;
div[slot="content"] {
margin-top: -16px;
} }
ha-md-list { ha-md-list {
background: none; background: none;

View File

@ -4,7 +4,6 @@ import type { CloudStatus } from "../../../../data/cloud";
export interface BackupOnboardingDialogParams { export interface BackupOnboardingDialogParams {
submit?: (value: boolean) => void; submit?: (value: boolean) => void;
cancel?: () => void; cancel?: () => void;
showIntro?: boolean;
cloudStatus: CloudStatus; cloudStatus: CloudStatus;
} }

View File

@ -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,
});
};

View File

@ -119,26 +119,6 @@ class HaConfigBackupDetails extends LitElement {
: !this._backup : !this._backup
? html`<ha-circular-progress active></ha-circular-progress>` ? html`<ha-circular-progress active></ha-circular-progress>`
: html` : html`
<ha-card header="Select what to restore">
<div class="card-content">
<ha-backup-data-picker
.hass=${this.hass}
.data=${this._backup}
.value=${this._selectedBackup}
@value-changed=${this._selectedBackupChanged}
.addonsInfo=${this._addonsInfo}
>
</ha-backup-data-picker>
</div>
<div class="card-actions">
<ha-button
@click=${this._restore}
.disabled=${this._isRestoreDisabled()}
>
Restore
</ha-button>
</div>
</ha-card>
<ha-card header="Backup"> <ha-card header="Backup">
<div class="card-content"> <div class="card-content">
<ha-md-list> <ha-md-list>
@ -159,6 +139,27 @@ class HaConfigBackupDetails extends LitElement {
</ha-md-list> </ha-md-list>
</div> </div>
</ha-card> </ha-card>
<ha-card header="Select what to restore">
<div class="card-content">
<ha-backup-data-picker
.hass=${this.hass}
.data=${this._backup}
.value=${this._selectedBackup}
@value-changed=${this._selectedBackupChanged}
.addonsInfo=${this._addonsInfo}
>
</ha-backup-data-picker>
</div>
<div class="card-actions">
<ha-button
@click=${this._restore}
.disabled=${this._isRestoreDisabled()}
class="danger"
>
Restore
</ha-button>
</div>
</ha-card>
<ha-card header="Locations"> <ha-card header="Locations">
<div class="card-content"> <div class="card-content">
<ha-md-list> <ha-md-list>
@ -172,6 +173,8 @@ class HaConfigBackupDetails extends LitElement {
this._backup!.agent_ids! this._backup!.agent_ids!
); );
const isLocal = isLocalAgent(agentId);
return html` return html`
<ha-md-list-item> <ha-md-list-item>
${isLocalAgent(agentId) ${isLocalAgent(agentId)
@ -204,7 +207,11 @@ class HaConfigBackupDetails extends LitElement {
> >
</span> </span>
<span> <span>
${success ? "Backup synced" : "Backup failed"} ${success
? isLocal
? "Backup created"
: "Backup uploaded"
: "Backup failed"}
</span> </span>
</div> </div>
${success ${success
@ -390,6 +397,9 @@ class HaConfigBackupDetails extends LitElement {
.warning ha-svg-icon { .warning ha-svg-icon {
color: var(--error-color); color: var(--error-color);
} }
ha-button.danger {
--mdc-theme-primary: var(--error-color);
}
ha-backup-data-picker { ha-backup-data-picker {
display: block; display: block;
} }

View File

@ -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 type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit"; import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
import { shouldHandleRequestSelectedEvent } from "../../../common/mwc/handle-request-selected-event"; import { shouldHandleRequestSelectedEvent } from "../../../common/mwc/handle-request-selected-event";
import "../../../components/ha-button"; import "../../../components/ha-button";
@ -34,6 +35,7 @@ import "./components/overview/ha-backup-overview-settings";
import "./components/overview/ha-backup-overview-summary"; import "./components/overview/ha-backup-overview-summary";
import { showBackupOnboardingDialog } from "./dialogs/show-dialog-backup_onboarding"; import { showBackupOnboardingDialog } from "./dialogs/show-dialog-backup_onboarding";
import { showGenerateBackupDialog } from "./dialogs/show-dialog-generate-backup"; 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 { showNewBackupDialog } from "./dialogs/show-dialog-new-backup";
import { showUploadBackupDialog } from "./dialogs/show-dialog-upload-backup"; import { showUploadBackupDialog } from "./dialogs/show-dialog-upload-backup";
@ -63,15 +65,22 @@ class HaConfigBackupOverview extends LitElement {
await showUploadBackupDialog(this, {}); await showUploadBackupDialog(this, {});
} }
private _handleOnboardingButtonClick(ev) { private async _changeLocalLocation(ev) {
ev.stopPropagation(); if (!shouldHandleRequestSelectedEvent(ev)) {
this._setupAutomaticBackup(false); return;
}
showLocalBackupLocationDialog(this, {});
} }
private async _setupAutomaticBackup(showIntro: boolean) { private _handleOnboardingButtonClick(ev) {
ev.stopPropagation();
this._setupAutomaticBackup();
}
private async _setupAutomaticBackup() {
const success = await showBackupOnboardingDialog(this, { const success = await showBackupOnboardingDialog(this, {
cloudStatus: this.cloudStatus, cloudStatus: this.cloudStatus,
showIntro: showIntro,
}); });
if (!success) { if (!success) {
return; return;
@ -84,7 +93,7 @@ class HaConfigBackupOverview extends LitElement {
private async _newBackup(): Promise<void> { private async _newBackup(): Promise<void> {
if (this._needsOnboarding) { if (this._needsOnboarding) {
this._setupAutomaticBackup(true); this._setupAutomaticBackup();
return; return;
} }
@ -125,6 +134,8 @@ class HaConfigBackupOverview extends LitElement {
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");
return html` return html`
<hass-subpage <hass-subpage
back-path="/config/system" back-path="/config/system"
@ -139,6 +150,18 @@ class HaConfigBackupOverview extends LitElement {
.label=${this.hass.localize("ui.common.menu")} .label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical} .path=${mdiDotsVertical}
></ha-icon-button> ></ha-icon-button>
${isHassio
? html`<ha-list-item
graphic="icon"
@request-selected=${this._changeLocalLocation}
>
<ha-svg-icon
slot="graphic"
.path=${mdiHarddisk}
></ha-svg-icon>
Change local location
</ha-list-item>`
: nothing}
<ha-list-item <ha-list-item
graphic="icon" graphic="icon"
@request-selected=${this._uploadBackup} @request-selected=${this._uploadBackup}

View File

@ -56,7 +56,7 @@ class HaConfigBackupSettings extends LitElement {
<div class="card-content"> <div class="card-content">
<p> <p>
Let Home Assistant take care of your backups by creating a 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.
</p> </p>
<ha-backup-config-schedule <ha-backup-config-schedule
.hass=${this.hass} .hass=${this.hass}
@ -73,6 +73,7 @@ class HaConfigBackupSettings extends LitElement {
.value=${this._dataConfig} .value=${this._dataConfig}
@value-changed=${this._dataConfigChanged} @value-changed=${this._dataConfigChanged}
force-home-assistant force-home-assistant
hide-addon-version
></ha-backup-config-data> ></ha-backup-config-data>
</div> </div>
</ha-card> </ha-card>

View File

@ -2209,6 +2209,17 @@
}, },
"picker": { "picker": {
"search": "Search backups" "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": { "tag": {