mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-28 03:36:44 +00:00
Backup retention by location (#25144)
This commit is contained in:
parent
672fbc6007
commit
1b79869c87
@ -2,6 +2,7 @@ import { memoize } from "@fullcalendar/core/internal";
|
|||||||
import { setHours, setMinutes } from "date-fns";
|
import { setHours, setMinutes } from "date-fns";
|
||||||
import type { HassConfig } from "home-assistant-js-websocket";
|
import type { HassConfig } from "home-assistant-js-websocket";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
|
import checkValidDate from "../common/datetime/check_valid_date";
|
||||||
import {
|
import {
|
||||||
formatDateTime,
|
formatDateTime,
|
||||||
formatDateTimeNumeric,
|
formatDateTimeNumeric,
|
||||||
@ -10,11 +11,10 @@ import { formatTime } from "../common/datetime/format_time";
|
|||||||
import type { LocalizeFunc } from "../common/translations/localize";
|
import type { LocalizeFunc } from "../common/translations/localize";
|
||||||
import type { HomeAssistant } from "../types";
|
import type { HomeAssistant } from "../types";
|
||||||
import { fileDownload } from "../util/file_download";
|
import { fileDownload } from "../util/file_download";
|
||||||
|
import { handleFetchPromise } from "../util/hass-call-api";
|
||||||
|
import type { BackupManagerState, ManagerStateEvent } from "./backup_manager";
|
||||||
import { domainToName } from "./integration";
|
import { domainToName } from "./integration";
|
||||||
import type { FrontendLocaleData } from "./translation";
|
import type { FrontendLocaleData } from "./translation";
|
||||||
import type { BackupManagerState, ManagerStateEvent } from "./backup_manager";
|
|
||||||
import checkValidDate from "../common/datetime/check_valid_date";
|
|
||||||
import { handleFetchPromise } from "../util/hass-call-api";
|
|
||||||
|
|
||||||
export const enum BackupScheduleRecurrence {
|
export const enum BackupScheduleRecurrence {
|
||||||
NEVER = "never",
|
NEVER = "never",
|
||||||
@ -37,6 +37,11 @@ export const BACKUP_DAYS: BackupDay[] = [
|
|||||||
export const sortWeekdays = (weekdays) =>
|
export const sortWeekdays = (weekdays) =>
|
||||||
weekdays.sort((a, b) => BACKUP_DAYS.indexOf(a) - BACKUP_DAYS.indexOf(b));
|
weekdays.sort((a, b) => BACKUP_DAYS.indexOf(a) - BACKUP_DAYS.indexOf(b));
|
||||||
|
|
||||||
|
export interface Retention {
|
||||||
|
copies?: number | null;
|
||||||
|
days?: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
export interface BackupConfig {
|
export interface BackupConfig {
|
||||||
automatic_backups_configured: boolean;
|
automatic_backups_configured: boolean;
|
||||||
last_attempted_automatic_backup: string | null;
|
last_attempted_automatic_backup: string | null;
|
||||||
@ -52,10 +57,7 @@ export interface BackupConfig {
|
|||||||
name: string | null;
|
name: string | null;
|
||||||
password: string | null;
|
password: string | null;
|
||||||
};
|
};
|
||||||
retention: {
|
retention: Retention;
|
||||||
copies?: number | null;
|
|
||||||
days?: number | null;
|
|
||||||
};
|
|
||||||
schedule: {
|
schedule: {
|
||||||
recurrence: BackupScheduleRecurrence;
|
recurrence: BackupScheduleRecurrence;
|
||||||
time?: string | null;
|
time?: string | null;
|
||||||
@ -75,10 +77,7 @@ export interface BackupMutableConfig {
|
|||||||
name?: string | null;
|
name?: string | null;
|
||||||
password?: string | null;
|
password?: string | null;
|
||||||
};
|
};
|
||||||
retention?: {
|
retention?: Retention;
|
||||||
copies?: number | null;
|
|
||||||
days?: number | null;
|
|
||||||
};
|
|
||||||
schedule?: {
|
schedule?: {
|
||||||
recurrence: BackupScheduleRecurrence;
|
recurrence: BackupScheduleRecurrence;
|
||||||
time?: string | null;
|
time?: string | null;
|
||||||
@ -90,7 +89,8 @@ export interface BackupMutableConfig {
|
|||||||
export type BackupAgentsConfig = Record<string, BackupAgentConfig>;
|
export type BackupAgentsConfig = Record<string, BackupAgentConfig>;
|
||||||
|
|
||||||
export interface BackupAgentConfig {
|
export interface BackupAgentConfig {
|
||||||
protected: boolean;
|
protected?: boolean;
|
||||||
|
retention?: Retention | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BackupAgent {
|
export interface BackupAgent {
|
||||||
|
@ -0,0 +1,274 @@
|
|||||||
|
import { css, html, LitElement, nothing, type PropertyValues } from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||||
|
import { clamp } from "../../../../../common/number/clamp";
|
||||||
|
import "../../../../../components/ha-expansion-panel";
|
||||||
|
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-md-textfield";
|
||||||
|
import type { BackupConfig, Retention } from "../../../../../data/backup";
|
||||||
|
import type { HomeAssistant } from "../../../../../types";
|
||||||
|
|
||||||
|
export type BackupConfigSchedule = Pick<BackupConfig, "schedule" | "retention">;
|
||||||
|
|
||||||
|
const MIN_VALUE = 1;
|
||||||
|
const MAX_VALUE = 9999; // because of input width
|
||||||
|
|
||||||
|
export enum RetentionPreset {
|
||||||
|
GLOBAL = "global",
|
||||||
|
COPIES_3 = "copies_3",
|
||||||
|
FOREVER = "forever",
|
||||||
|
CUSTOM = "custom",
|
||||||
|
}
|
||||||
|
|
||||||
|
const PRESET_MAP: Record<
|
||||||
|
Exclude<RetentionPreset, RetentionPreset.CUSTOM>,
|
||||||
|
Retention | null
|
||||||
|
> = {
|
||||||
|
copies_3: { copies: 3, days: null },
|
||||||
|
forever: { copies: null, days: null },
|
||||||
|
global: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface RetentionData {
|
||||||
|
type: "copies" | "days" | "forever";
|
||||||
|
value: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
@customElement("ha-backup-config-retention")
|
||||||
|
class HaBackupConfigRetention extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public retention?: Retention | null;
|
||||||
|
|
||||||
|
@property() public headline?: string;
|
||||||
|
|
||||||
|
@property({ type: Boolean, attribute: "location-specific" })
|
||||||
|
public locationSpecific = false;
|
||||||
|
|
||||||
|
@state() private _preset: RetentionPreset = RetentionPreset.COPIES_3;
|
||||||
|
|
||||||
|
@state() private _type: "copies" | "days" = "copies";
|
||||||
|
|
||||||
|
@state() private _value = 3;
|
||||||
|
|
||||||
|
private presetOptions = [
|
||||||
|
RetentionPreset.COPIES_3,
|
||||||
|
RetentionPreset.FOREVER,
|
||||||
|
RetentionPreset.CUSTOM,
|
||||||
|
];
|
||||||
|
|
||||||
|
public willUpdate(properties: PropertyValues) {
|
||||||
|
super.willUpdate(properties);
|
||||||
|
|
||||||
|
if (!this.hasUpdated) {
|
||||||
|
if (!this.retention) {
|
||||||
|
this._preset = RetentionPreset.GLOBAL;
|
||||||
|
} else if (
|
||||||
|
this.retention?.days === null &&
|
||||||
|
this.retention?.copies === null
|
||||||
|
) {
|
||||||
|
this._preset = RetentionPreset.FOREVER;
|
||||||
|
} else {
|
||||||
|
this._value = this.retention.copies || this.retention.days || 3;
|
||||||
|
if (
|
||||||
|
this.retention.days ||
|
||||||
|
this.locationSpecific ||
|
||||||
|
this.retention.copies !== 3
|
||||||
|
) {
|
||||||
|
this._preset = RetentionPreset.CUSTOM;
|
||||||
|
this._type = this.retention?.copies ? "copies" : "days";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.locationSpecific) {
|
||||||
|
this.presetOptions = [
|
||||||
|
RetentionPreset.GLOBAL,
|
||||||
|
RetentionPreset.FOREVER,
|
||||||
|
RetentionPreset.CUSTOM,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
return html`
|
||||||
|
<ha-md-list-item>
|
||||||
|
<span slot="headline">
|
||||||
|
${this.headline ??
|
||||||
|
this.hass.localize(`ui.panel.config.backup.schedule.retention`)}
|
||||||
|
</span>
|
||||||
|
<span slot="supporting-text">
|
||||||
|
${this.hass.localize(
|
||||||
|
`ui.panel.config.backup.schedule.retention_description`
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<ha-md-select
|
||||||
|
slot="end"
|
||||||
|
@change=${this._retentionPresetChanged}
|
||||||
|
.value=${this._preset}
|
||||||
|
>
|
||||||
|
${this.presetOptions.map(
|
||||||
|
(option) => html`
|
||||||
|
<ha-md-select-option .value=${option}>
|
||||||
|
<div slot="headline">
|
||||||
|
${this.hass.localize(
|
||||||
|
`ui.panel.config.backup.schedule.retention_presets.${option}`
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</ha-md-select-option>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</ha-md-select>
|
||||||
|
</ha-md-list-item>
|
||||||
|
|
||||||
|
${this._preset === RetentionPreset.CUSTOM
|
||||||
|
? html`<ha-expansion-panel
|
||||||
|
expanded
|
||||||
|
.header=${this.hass.localize(
|
||||||
|
"ui.panel.config.backup.schedule.custom_retention"
|
||||||
|
)}
|
||||||
|
outlined
|
||||||
|
>
|
||||||
|
<ha-md-list-item>
|
||||||
|
<span slot="headline">
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.backup.schedule.custom_retention_label"
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<ha-md-textfield
|
||||||
|
slot="end"
|
||||||
|
@change=${this._retentionValueChanged}
|
||||||
|
.value=${this._value.toString()}
|
||||||
|
id="value"
|
||||||
|
type="number"
|
||||||
|
.min=${MIN_VALUE.toString()}
|
||||||
|
.max=${MAX_VALUE.toString()}
|
||||||
|
step="1"
|
||||||
|
>
|
||||||
|
</ha-md-textfield>
|
||||||
|
<ha-md-select
|
||||||
|
slot="end"
|
||||||
|
@change=${this._retentionTypeChanged}
|
||||||
|
.value=${this._type}
|
||||||
|
id="type"
|
||||||
|
>
|
||||||
|
<ha-md-select-option value="days">
|
||||||
|
<div slot="headline">
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.backup.schedule.retention_units.days"
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</ha-md-select-option>
|
||||||
|
<ha-md-select-option value="copies">
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.backup.schedule.retention_units.copies"
|
||||||
|
)}
|
||||||
|
</ha-md-select-option>
|
||||||
|
</ha-md-select>
|
||||||
|
</ha-md-list-item></ha-expansion-panel
|
||||||
|
> `
|
||||||
|
: nothing}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _retentionPresetChanged(ev) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
const target = ev.currentTarget as HaMdSelect;
|
||||||
|
let value = target.value as RetentionPreset;
|
||||||
|
|
||||||
|
if (
|
||||||
|
value === RetentionPreset.CUSTOM &&
|
||||||
|
(this.locationSpecific || this._preset === RetentionPreset.FOREVER)
|
||||||
|
) {
|
||||||
|
this._preset = value;
|
||||||
|
// custom needs to have a type of days or copies, set it to default copies 3
|
||||||
|
value = RetentionPreset.COPIES_3;
|
||||||
|
} else {
|
||||||
|
this._preset = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.locationSpecific || value !== RetentionPreset.CUSTOM) {
|
||||||
|
const retention = PRESET_MAP[value];
|
||||||
|
|
||||||
|
fireEvent(this, "value-changed", {
|
||||||
|
value: retention,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _retentionValueChanged(ev) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
const target = ev.currentTarget as HaMdSelect;
|
||||||
|
const value = parseInt(target.value);
|
||||||
|
const clamped = clamp(value, MIN_VALUE, MAX_VALUE);
|
||||||
|
target.value = clamped.toString();
|
||||||
|
|
||||||
|
fireEvent(this, "value-changed", {
|
||||||
|
value: {
|
||||||
|
copies: this._type === "copies" ? clamped : null,
|
||||||
|
days: this._type === "days" ? clamped : null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _retentionTypeChanged(ev) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
const target = ev.currentTarget as HaMdSelect;
|
||||||
|
const type = target.value as "copies" | "days";
|
||||||
|
|
||||||
|
fireEvent(this, "value-changed", {
|
||||||
|
value: {
|
||||||
|
copies: type === "copies" ? this._value : null,
|
||||||
|
days: type === "days" ? this._value : null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = css`
|
||||||
|
ha-md-list-item {
|
||||||
|
--md-item-overflow: visible;
|
||||||
|
--md-list-item-leading-space: 0;
|
||||||
|
--md-list-item-trailing-space: 0;
|
||||||
|
}
|
||||||
|
@media all and (max-width: 450px) {
|
||||||
|
ha-md-select {
|
||||||
|
min-width: 160px;
|
||||||
|
width: 160px;
|
||||||
|
--md-filled-field-content-space: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ha-md-textfield#value {
|
||||||
|
min-width: 70px;
|
||||||
|
}
|
||||||
|
ha-md-select#type {
|
||||||
|
min-width: 100px;
|
||||||
|
}
|
||||||
|
@media all and (max-width: 450px) {
|
||||||
|
ha-md-textfield#value {
|
||||||
|
min-width: 60px;
|
||||||
|
margin: 0 -8px;
|
||||||
|
}
|
||||||
|
ha-md-select#type {
|
||||||
|
min-width: 120px;
|
||||||
|
width: 120px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ha-expansion-panel {
|
||||||
|
--expansion-panel-summary-padding: 0 16px;
|
||||||
|
--expansion-panel-content-padding: 0 16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
ha-md-list-item.days {
|
||||||
|
--md-item-align-items: flex-start;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-backup-config-retention": HaBackupConfigRetention;
|
||||||
|
}
|
||||||
|
}
|
@ -1,10 +1,8 @@
|
|||||||
import type { PropertyValues } from "lit";
|
|
||||||
import { css, html, LitElement, nothing } from "lit";
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { formatTime } from "../../../../../common/datetime/format_time";
|
import { formatTime } from "../../../../../common/datetime/format_time";
|
||||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||||
import { clamp } from "../../../../../common/number/clamp";
|
|
||||||
import "../../../../../components/ha-checkbox";
|
import "../../../../../components/ha-checkbox";
|
||||||
import type { HaCheckbox } from "../../../../../components/ha-checkbox";
|
import type { HaCheckbox } from "../../../../../components/ha-checkbox";
|
||||||
import "../../../../../components/ha-expansion-panel";
|
import "../../../../../components/ha-expansion-panel";
|
||||||
@ -15,10 +13,13 @@ import "../../../../../components/ha-md-select";
|
|||||||
import type { HaMdSelect } from "../../../../../components/ha-md-select";
|
import type { HaMdSelect } from "../../../../../components/ha-md-select";
|
||||||
import "../../../../../components/ha-md-select-option";
|
import "../../../../../components/ha-md-select-option";
|
||||||
import "../../../../../components/ha-md-textfield";
|
import "../../../../../components/ha-md-textfield";
|
||||||
import "../../../../../components/ha-switch";
|
|
||||||
import "../../../../../components/ha-time-input";
|
import "../../../../../components/ha-time-input";
|
||||||
import "../../../../../components/ha-tip";
|
import "../../../../../components/ha-tip";
|
||||||
import type { BackupConfig, BackupDay } from "../../../../../data/backup";
|
import type {
|
||||||
|
BackupConfig,
|
||||||
|
BackupDay,
|
||||||
|
Retention,
|
||||||
|
} from "../../../../../data/backup";
|
||||||
import {
|
import {
|
||||||
BACKUP_DAYS,
|
BACKUP_DAYS,
|
||||||
BackupScheduleRecurrence,
|
BackupScheduleRecurrence,
|
||||||
@ -29,76 +30,32 @@ import {
|
|||||||
import type { SupervisorUpdateConfig } from "../../../../../data/supervisor/update";
|
import type { SupervisorUpdateConfig } from "../../../../../data/supervisor/update";
|
||||||
import type { HomeAssistant } from "../../../../../types";
|
import type { HomeAssistant } from "../../../../../types";
|
||||||
import { documentationUrl } from "../../../../../util/documentation-url";
|
import { documentationUrl } from "../../../../../util/documentation-url";
|
||||||
|
import "./ha-backup-config-retention";
|
||||||
|
|
||||||
export type BackupConfigSchedule = Pick<BackupConfig, "schedule" | "retention">;
|
export type BackupConfigSchedule = Pick<BackupConfig, "schedule" | "retention">;
|
||||||
|
|
||||||
const MIN_VALUE = 1;
|
|
||||||
const MAX_VALUE = 50;
|
|
||||||
|
|
||||||
enum RetentionPreset {
|
|
||||||
COPIES_3 = "copies_3",
|
|
||||||
FOREVER = "forever",
|
|
||||||
CUSTOM = "custom",
|
|
||||||
}
|
|
||||||
|
|
||||||
enum BackupScheduleTime {
|
enum BackupScheduleTime {
|
||||||
DEFAULT = "default",
|
DEFAULT = "default",
|
||||||
CUSTOM = "custom",
|
CUSTOM = "custom",
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RetentionData {
|
|
||||||
type: "copies" | "days" | "forever";
|
|
||||||
value: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const RETENTION_PRESETS: Record<
|
|
||||||
Exclude<RetentionPreset, RetentionPreset.CUSTOM>,
|
|
||||||
RetentionData
|
|
||||||
> = {
|
|
||||||
copies_3: { type: "copies", value: 3 },
|
|
||||||
forever: { type: "forever", value: 0 },
|
|
||||||
};
|
|
||||||
|
|
||||||
const SCHEDULE_OPTIONS = [
|
const SCHEDULE_OPTIONS = [
|
||||||
BackupScheduleRecurrence.NEVER,
|
BackupScheduleRecurrence.NEVER,
|
||||||
BackupScheduleRecurrence.DAILY,
|
BackupScheduleRecurrence.DAILY,
|
||||||
BackupScheduleRecurrence.CUSTOM_DAYS,
|
BackupScheduleRecurrence.CUSTOM_DAYS,
|
||||||
] as const satisfies BackupScheduleRecurrence[];
|
] as const satisfies BackupScheduleRecurrence[];
|
||||||
|
|
||||||
const RETENTION_PRESETS_OPTIONS = [
|
|
||||||
RetentionPreset.COPIES_3,
|
|
||||||
RetentionPreset.FOREVER,
|
|
||||||
RetentionPreset.CUSTOM,
|
|
||||||
] as const satisfies RetentionPreset[];
|
|
||||||
|
|
||||||
const SCHEDULE_TIME_OPTIONS = [
|
const SCHEDULE_TIME_OPTIONS = [
|
||||||
BackupScheduleTime.DEFAULT,
|
BackupScheduleTime.DEFAULT,
|
||||||
BackupScheduleTime.CUSTOM,
|
BackupScheduleTime.CUSTOM,
|
||||||
] as const satisfies BackupScheduleTime[];
|
] as const satisfies BackupScheduleTime[];
|
||||||
|
|
||||||
const computeRetentionPreset = (
|
|
||||||
data: RetentionData
|
|
||||||
): RetentionPreset | undefined => {
|
|
||||||
for (const [key, value] of Object.entries(RETENTION_PRESETS)) {
|
|
||||||
if (
|
|
||||||
value.type === data.type &&
|
|
||||||
(value.type === RetentionPreset.FOREVER || value.value === data.value)
|
|
||||||
) {
|
|
||||||
return key as RetentionPreset;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return RetentionPreset.CUSTOM;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface FormData {
|
interface FormData {
|
||||||
recurrence: BackupScheduleRecurrence;
|
recurrence: BackupScheduleRecurrence;
|
||||||
time_option: BackupScheduleTime;
|
time_option: BackupScheduleTime;
|
||||||
time?: string | null;
|
time?: string | null;
|
||||||
days: BackupDay[];
|
days: BackupDay[];
|
||||||
retention: {
|
retention: Retention;
|
||||||
type: "copies" | "days" | "forever";
|
|
||||||
value: number;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const INITIAL_FORM_DATA: FormData = {
|
const INITIAL_FORM_DATA: FormData = {
|
||||||
@ -106,8 +63,7 @@ const INITIAL_FORM_DATA: FormData = {
|
|||||||
time_option: BackupScheduleTime.DEFAULT,
|
time_option: BackupScheduleTime.DEFAULT,
|
||||||
days: [],
|
days: [],
|
||||||
retention: {
|
retention: {
|
||||||
type: "copies",
|
copies: 3,
|
||||||
value: 3,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -122,17 +78,6 @@ class HaBackupConfigSchedule extends LitElement {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public supervisorUpdateConfig?: SupervisorUpdateConfig;
|
public supervisorUpdateConfig?: SupervisorUpdateConfig;
|
||||||
|
|
||||||
@state() private _retentionPreset?: RetentionPreset;
|
|
||||||
|
|
||||||
protected willUpdate(changedProperties: PropertyValues): void {
|
|
||||||
if (changedProperties.has("value")) {
|
|
||||||
if (this._retentionPreset !== RetentionPreset.CUSTOM) {
|
|
||||||
const data = this._getData(this.value);
|
|
||||||
this._retentionPreset = computeRetentionPreset(data.retention);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _getData = memoizeOne((value?: BackupConfigSchedule): FormData => {
|
private _getData = memoizeOne((value?: BackupConfigSchedule): FormData => {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return INITIAL_FORM_DATA;
|
return INITIAL_FORM_DATA;
|
||||||
@ -150,15 +95,7 @@ class HaBackupConfigSchedule extends LitElement {
|
|||||||
config.schedule.recurrence === BackupScheduleRecurrence.CUSTOM_DAYS
|
config.schedule.recurrence === BackupScheduleRecurrence.CUSTOM_DAYS
|
||||||
? config.schedule.days
|
? config.schedule.days
|
||||||
: [],
|
: [],
|
||||||
retention: {
|
retention: config.retention,
|
||||||
type:
|
|
||||||
config.retention.days === null && config.retention.copies === null
|
|
||||||
? "forever"
|
|
||||||
: config.retention.days != null
|
|
||||||
? "days"
|
|
||||||
: "copies",
|
|
||||||
value: config.retention.days ?? config.retention.copies ?? 3,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -173,12 +110,7 @@ class HaBackupConfigSchedule extends LitElement {
|
|||||||
? data.days
|
? data.days
|
||||||
: [],
|
: [],
|
||||||
},
|
},
|
||||||
retention:
|
retention: data.retention,
|
||||||
data.retention.type === "forever"
|
|
||||||
? { days: null, copies: null }
|
|
||||||
: data.retention.type === "days"
|
|
||||||
? { days: data.retention.value, copies: null }
|
|
||||||
: { copies: data.retention.value, days: null },
|
|
||||||
};
|
};
|
||||||
|
|
||||||
fireEvent(this, "value-changed", { value: this.value });
|
fireEvent(this, "value-changed", { value: this.value });
|
||||||
@ -377,81 +309,11 @@ class HaBackupConfigSchedule extends LitElement {
|
|||||||
`
|
`
|
||||||
: nothing}
|
: nothing}
|
||||||
|
|
||||||
<ha-md-list-item>
|
<ha-backup-config-retention
|
||||||
<span slot="headline">
|
.hass=${this.hass}
|
||||||
${this.hass.localize(`ui.panel.config.backup.schedule.retention`)}
|
.retention=${data.retention}
|
||||||
</span>
|
@value-changed=${this._retentionChanged}
|
||||||
<span slot="supporting-text">
|
></ha-backup-config-retention>
|
||||||
${this.hass.localize(
|
|
||||||
`ui.panel.config.backup.schedule.retention_description`
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
<ha-md-select
|
|
||||||
slot="end"
|
|
||||||
@change=${this._retentionPresetChanged}
|
|
||||||
.value=${this._retentionPreset ?? ""}
|
|
||||||
>
|
|
||||||
${RETENTION_PRESETS_OPTIONS.map(
|
|
||||||
(option) => html`
|
|
||||||
<ha-md-select-option .value=${option}>
|
|
||||||
<div slot="headline">
|
|
||||||
${this.hass.localize(
|
|
||||||
`ui.panel.config.backup.schedule.retention_presets.${option}`
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</ha-md-select-option>
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
</ha-md-select>
|
|
||||||
</ha-md-list-item>
|
|
||||||
|
|
||||||
${this._retentionPreset === RetentionPreset.CUSTOM
|
|
||||||
? html`<ha-expansion-panel
|
|
||||||
expanded
|
|
||||||
.header=${this.hass.localize(
|
|
||||||
"ui.panel.config.backup.schedule.custom_retention"
|
|
||||||
)}
|
|
||||||
outlined
|
|
||||||
>
|
|
||||||
<ha-md-list-item>
|
|
||||||
<span slot="headline">
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.backup.schedule.custom_retention_label"
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
<ha-md-textfield
|
|
||||||
slot="end"
|
|
||||||
@change=${this._retentionValueChanged}
|
|
||||||
.value=${data.retention.value.toString()}
|
|
||||||
id="value"
|
|
||||||
type="number"
|
|
||||||
.min=${MIN_VALUE.toString()}
|
|
||||||
.max=${MAX_VALUE.toString()}
|
|
||||||
step="1"
|
|
||||||
>
|
|
||||||
</ha-md-textfield>
|
|
||||||
<ha-md-select
|
|
||||||
slot="end"
|
|
||||||
@change=${this._retentionTypeChanged}
|
|
||||||
.value=${data.retention.type}
|
|
||||||
id="type"
|
|
||||||
>
|
|
||||||
<ha-md-select-option value="days">
|
|
||||||
<div slot="headline">
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.backup.schedule.retention_units.days"
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</ha-md-select-option>
|
|
||||||
<ha-md-select-option value="copies">
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.backup.schedule.retention_units.copies"
|
|
||||||
)}
|
|
||||||
</ha-md-select-option>
|
|
||||||
</ha-md-select>
|
|
||||||
</ha-md-list-item></ha-expansion-panel
|
|
||||||
> `
|
|
||||||
: nothing}
|
|
||||||
<ha-tip .hass=${this.hass}
|
<ha-tip .hass=${this.hass}
|
||||||
>${this.hass.localize("ui.panel.config.backup.schedule.tip", {
|
>${this.hass.localize("ui.panel.config.backup.schedule.tip", {
|
||||||
backup_create: html`<a
|
backup_create: html`<a
|
||||||
@ -543,65 +405,18 @@ class HaBackupConfigSchedule extends LitElement {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private _retentionPresetChanged(ev) {
|
private _retentionChanged(ev: CustomEvent<{ value: Retention }>) {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
const target = ev.currentTarget as HaMdSelect;
|
const retention = ev.detail.value;
|
||||||
let value = target.value as RetentionPreset;
|
|
||||||
|
|
||||||
// custom needs to have a type of days or copies, set it to default copies 3
|
|
||||||
if (
|
|
||||||
value === RetentionPreset.CUSTOM &&
|
|
||||||
this._retentionPreset === RetentionPreset.FOREVER
|
|
||||||
) {
|
|
||||||
this._retentionPreset = value;
|
|
||||||
value = RetentionPreset.COPIES_3;
|
|
||||||
} else {
|
|
||||||
this._retentionPreset = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value !== RetentionPreset.CUSTOM) {
|
|
||||||
const data = this._getData(this.value);
|
|
||||||
const retention = RETENTION_PRESETS[value];
|
|
||||||
// Ensure we have at least 1 in default value because user can't select 0
|
|
||||||
if (value !== RetentionPreset.FOREVER) {
|
|
||||||
retention.value = Math.max(retention.value, 1);
|
|
||||||
}
|
|
||||||
this._setData({
|
|
||||||
...data,
|
|
||||||
retention,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _retentionValueChanged(ev) {
|
|
||||||
ev.stopPropagation();
|
|
||||||
const target = ev.currentTarget as HaMdSelect;
|
|
||||||
const value = parseInt(target.value);
|
|
||||||
const clamped = clamp(value, MIN_VALUE, MAX_VALUE);
|
|
||||||
const data = this._getData(this.value);
|
|
||||||
target.value = clamped.toString();
|
|
||||||
this._setData({
|
|
||||||
...data,
|
|
||||||
retention: {
|
|
||||||
...data.retention,
|
|
||||||
value: clamped,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private _retentionTypeChanged(ev) {
|
|
||||||
ev.stopPropagation();
|
|
||||||
const target = ev.currentTarget as HaMdSelect;
|
|
||||||
const value = target.value as "copies" | "days";
|
|
||||||
|
|
||||||
const data = this._getData(this.value);
|
const data = this._getData(this.value);
|
||||||
this._setData({
|
|
||||||
|
const newData = {
|
||||||
...data,
|
...data,
|
||||||
retention: {
|
retention,
|
||||||
...data.retention,
|
};
|
||||||
type: value,
|
|
||||||
},
|
this._setData(newData);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static styles = css`
|
static styles = css`
|
||||||
@ -631,25 +446,7 @@ class HaBackupConfigSchedule extends LitElement {
|
|||||||
width: 145px;
|
width: 145px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ha-md-textfield#value {
|
|
||||||
min-width: 70px;
|
|
||||||
}
|
|
||||||
ha-md-select#type {
|
|
||||||
min-width: 100px;
|
|
||||||
}
|
|
||||||
@media all and (max-width: 450px) {
|
|
||||||
ha-md-textfield#value {
|
|
||||||
min-width: 60px;
|
|
||||||
margin: 0 -8px;
|
|
||||||
}
|
|
||||||
ha-md-select#type {
|
|
||||||
min-width: 120px;
|
|
||||||
width: 120px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ha-expansion-panel {
|
ha-expansion-panel {
|
||||||
--expansion-panel-summary-padding: 0 16px;
|
|
||||||
--expansion-panel-content-padding: 0 16px;
|
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
ha-tip {
|
ha-tip {
|
||||||
|
@ -1,32 +1,35 @@
|
|||||||
import type { PropertyValues } from "lit";
|
import type { PropertyValues } from "lit";
|
||||||
import { css, html, LitElement, nothing } from "lit";
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import { fireEvent } from "../../../common/dom/fire_event";
|
||||||
import "../../../components/ha-alert";
|
import "../../../components/ha-alert";
|
||||||
import "../../../components/ha-button";
|
import "../../../components/ha-button";
|
||||||
import "../../../components/ha-switch";
|
|
||||||
import "../../../components/ha-button-menu";
|
import "../../../components/ha-button-menu";
|
||||||
import "../../../components/ha-card";
|
import "../../../components/ha-card";
|
||||||
import "../../../components/ha-fade-in";
|
import "../../../components/ha-fade-in";
|
||||||
import "../../../components/ha-spinner";
|
|
||||||
import "../../../components/ha-icon-button";
|
import "../../../components/ha-icon-button";
|
||||||
import "../../../components/ha-md-list";
|
import "../../../components/ha-md-list";
|
||||||
import "../../../components/ha-md-list-item";
|
import "../../../components/ha-md-list-item";
|
||||||
|
import "../../../components/ha-spinner";
|
||||||
|
import "../../../components/ha-switch";
|
||||||
import type {
|
import type {
|
||||||
BackupAgent,
|
BackupAgent,
|
||||||
BackupAgentConfig,
|
BackupAgentConfig,
|
||||||
BackupConfig,
|
BackupConfig,
|
||||||
|
Retention,
|
||||||
} from "../../../data/backup";
|
} from "../../../data/backup";
|
||||||
import {
|
import {
|
||||||
CLOUD_AGENT,
|
CLOUD_AGENT,
|
||||||
computeBackupAgentName,
|
computeBackupAgentName,
|
||||||
fetchBackupAgentsInfo,
|
fetchBackupAgentsInfo,
|
||||||
|
isLocalAgent,
|
||||||
updateBackupConfig,
|
updateBackupConfig,
|
||||||
} from "../../../data/backup";
|
} from "../../../data/backup";
|
||||||
import "../../../layouts/hass-subpage";
|
import "../../../layouts/hass-subpage";
|
||||||
import type { HomeAssistant } from "../../../types";
|
import type { HomeAssistant } from "../../../types";
|
||||||
import "./components/ha-backup-data-picker";
|
|
||||||
import { showConfirmationDialog } from "../../lovelace/custom-card-helpers";
|
import { showConfirmationDialog } from "../../lovelace/custom-card-helpers";
|
||||||
import { fireEvent } from "../../../common/dom/fire_event";
|
import "./components/config/ha-backup-config-retention";
|
||||||
|
import "./components/ha-backup-data-picker";
|
||||||
|
|
||||||
@customElement("ha-config-backup-location")
|
@customElement("ha-config-backup-location")
|
||||||
class HaConfigBackupDetails extends LitElement {
|
class HaConfigBackupDetails extends LitElement {
|
||||||
@ -61,18 +64,21 @@ class HaConfigBackupDetails extends LitElement {
|
|||||||
|
|
||||||
const encrypted = this._isEncryptionTurnedOn();
|
const encrypted = this._isEncryptionTurnedOn();
|
||||||
|
|
||||||
|
const agentName =
|
||||||
|
(this._agent &&
|
||||||
|
computeBackupAgentName(
|
||||||
|
this.hass.localize,
|
||||||
|
this.agentId,
|
||||||
|
this.agents
|
||||||
|
)) ||
|
||||||
|
this.hass.localize("ui.panel.config.backup.location.header");
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<hass-subpage
|
<hass-subpage
|
||||||
back-path="/config/backup/settings"
|
back-path="/config/backup/settings"
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.narrow=${this.narrow}
|
.narrow=${this.narrow}
|
||||||
.header=${(this._agent &&
|
.header=${agentName}
|
||||||
computeBackupAgentName(
|
|
||||||
this.hass.localize,
|
|
||||||
this.agentId,
|
|
||||||
this.agents
|
|
||||||
)) ||
|
|
||||||
this.hass.localize("ui.panel.config.backup.location.header")}
|
|
||||||
>
|
>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
${this._error &&
|
${this._error &&
|
||||||
@ -96,14 +102,14 @@ class HaConfigBackupDetails extends LitElement {
|
|||||||
><ha-spinner></ha-spinner
|
><ha-spinner></ha-spinner
|
||||||
></ha-fade-in>`
|
></ha-fade-in>`
|
||||||
: html`
|
: html`
|
||||||
${CLOUD_AGENT === this.agentId
|
<ha-card>
|
||||||
? html`
|
<div class="card-header">
|
||||||
<ha-card>
|
${this.hass.localize(
|
||||||
<div class="card-header">
|
"ui.panel.config.backup.location.configuration.title"
|
||||||
${this.hass.localize(
|
)}
|
||||||
"ui.panel.config.backup.location.configuration.title"
|
</div>
|
||||||
)}
|
${CLOUD_AGENT === this.agentId
|
||||||
</div>
|
? html`
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<p>
|
<p>
|
||||||
${this.hass.localize(
|
${this.hass.localize(
|
||||||
@ -111,9 +117,21 @@ class HaConfigBackupDetails extends LitElement {
|
|||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</ha-card>
|
`
|
||||||
`
|
: this.config?.agents[this.agentId]
|
||||||
: nothing}
|
? html`<ha-backup-config-retention
|
||||||
|
location-specific
|
||||||
|
.headline=${this.hass.localize(
|
||||||
|
`ui.panel.config.backup.location.retention_for_${isLocalAgent(this.agentId) ? "this_system" : "location"}`,
|
||||||
|
{ location: agentName }
|
||||||
|
)}
|
||||||
|
.hass=${this.hass}
|
||||||
|
.retention=${this.config?.agents[this.agentId]
|
||||||
|
?.retention}
|
||||||
|
@value-changed=${this._retentionChanged}
|
||||||
|
></ha-backup-config-retention>`
|
||||||
|
: nothing}
|
||||||
|
</ha-card>
|
||||||
<ha-card>
|
<ha-card>
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
${this.hass.localize(
|
${this.hass.localize(
|
||||||
@ -247,18 +265,37 @@ class HaConfigBackupDetails extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _updateAgentEncryption(value: boolean) {
|
private async _updateAgentConfig(config: Partial<BackupAgentConfig>) {
|
||||||
const agentsConfig = {
|
try {
|
||||||
...this.config?.agents,
|
const agents = this.config?.agents || {};
|
||||||
[this.agentId]: {
|
agents[this.agentId] = {
|
||||||
...this.config?.agents[this.agentId],
|
...(agents[this.agentId] || {}),
|
||||||
protected: value,
|
...config,
|
||||||
},
|
};
|
||||||
};
|
|
||||||
await updateBackupConfig(this.hass, {
|
await updateBackupConfig(this.hass, {
|
||||||
agents: agentsConfig,
|
agents,
|
||||||
|
});
|
||||||
|
fireEvent(this, "ha-refresh-backup-config");
|
||||||
|
} catch (err: any) {
|
||||||
|
this._error = this.hass.localize(
|
||||||
|
"ui.panel.config.backup.location.save_error",
|
||||||
|
{ error: err.message }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _retentionChanged(ev: CustomEvent<{ value: Retention }>) {
|
||||||
|
const retention = ev.detail.value;
|
||||||
|
this._updateAgentConfig({
|
||||||
|
retention,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _updateAgentEncryption(value: boolean) {
|
||||||
|
this._updateAgentConfig({
|
||||||
|
protected: value,
|
||||||
});
|
});
|
||||||
fireEvent(this, "ha-refresh-backup-config");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _turnOnEncryption() {
|
private _turnOnEncryption() {
|
||||||
@ -363,6 +400,10 @@ class HaConfigBackupDetails extends LitElement {
|
|||||||
ha-spinner {
|
ha-spinner {
|
||||||
margin: 24px auto;
|
margin: 24px auto;
|
||||||
}
|
}
|
||||||
|
ha-backup-config-retention {
|
||||||
|
display: block;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2533,6 +2533,7 @@
|
|||||||
"custom_retention_label": "Keep only",
|
"custom_retention_label": "Keep only",
|
||||||
"retention_description": "Based on the maximum number of backups or how many days they should be kept.",
|
"retention_description": "Based on the maximum number of backups or how many days they should be kept.",
|
||||||
"retention_presets": {
|
"retention_presets": {
|
||||||
|
"global": "Use global settings",
|
||||||
"copies_3": "3 backups",
|
"copies_3": "3 backups",
|
||||||
"forever": "Forever",
|
"forever": "Forever",
|
||||||
"custom": "Custom"
|
"custom": "Custom"
|
||||||
@ -2762,6 +2763,9 @@
|
|||||||
},
|
},
|
||||||
"location": {
|
"location": {
|
||||||
"header": "Location",
|
"header": "Location",
|
||||||
|
"save_error": "Error saving configuration: {error}",
|
||||||
|
"retention_for_this_system": "Retention for this system",
|
||||||
|
"retention_for_location": "Retention for {location}",
|
||||||
"not_found": "Not found",
|
"not_found": "Not found",
|
||||||
"not_found_description": "Location matching ''{backupId}'' not found",
|
"not_found_description": "Location matching ''{backupId}'' not found",
|
||||||
"error": "Could not fetch location details",
|
"error": "Could not fetch location details",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user