From a5b5e61ed4653d9aaaad6873313135af513cc9d2 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 23 May 2023 13:35:31 +0200 Subject: [PATCH] Add backup location selector (#16567) Co-authored-by: Paul Bottein --- src/components/ha-addon-picker.ts | 39 ++-- src/components/ha-mount-picker.ts | 179 ++++++++++++++++++ .../ha-selector-backup-location.ts | 46 +++++ src/components/ha-selector/ha-selector.ts | 1 + src/data/selector.ts | 5 + src/translations/en.json | 18 +- 6 files changed, 259 insertions(+), 29 deletions(-) create mode 100644 src/components/ha-mount-picker.ts create mode 100644 src/components/ha-selector/ha-selector-backup-location.ts diff --git a/src/components/ha-addon-picker.ts b/src/components/ha-addon-picker.ts index 3667300952..87a989d18f 100644 --- a/src/components/ha-addon-picker.ts +++ b/src/components/ha-addon-picker.ts @@ -5,13 +5,15 @@ import { isComponentLoaded } from "../common/config/is_component_loaded"; import { fireEvent } from "../common/dom/fire_event"; import { stringCompare } from "../common/string/compare"; import { fetchHassioAddonsInfo, HassioAddonInfo } from "../data/hassio/addon"; -import { showAlertDialog } from "../dialogs/generic/show-dialog-box"; -import { ValueChangedEvent, HomeAssistant } from "../types"; -import { HaComboBox } from "./ha-combo-box"; +import { HomeAssistant, ValueChangedEvent } from "../types"; +import "./ha-alert"; +import "./ha-combo-box"; +import type { HaComboBox } from "./ha-combo-box"; +import "./ha-list-item"; const rowRenderer: ComboBoxLitRenderer = ( item -) => html` +) => html` ${item.name} ${item.slug} ${item.icon @@ -21,7 +23,7 @@ const rowRenderer: ComboBoxLitRenderer = ( .src="/api/hassio/addons/${item.slug}/icon" />` : ""} -`; +`; @customElement("ha-addon-picker") class HaAddonPicker extends LitElement { @@ -41,6 +43,8 @@ class HaAddonPicker extends LitElement { @query("ha-combo-box") private _comboBox!: HaComboBox; + @state() private _error?: string; + public open() { this._comboBox?.open(); } @@ -57,6 +61,9 @@ class HaAddonPicker extends LitElement { if (!this._addons) { return nothing; } + if (this._error) { + return html`${this._error}`; + } return html` ${this._error}`; + } + const dataDiskOption = html` + ${this.hass.localize("ui.components.mount-picker.use_datadisk")} + + `; + return html` + + ${this.usage !== SupervisorMountUsage.MEDIA && + (!this._mounts.default_backup_mount || + this._mounts.default_backup_mount === __BACKUP_DATA_DISK__) + ? dataDiskOption + : nothing} + ${this._filterMounts(this._mounts, this.usage).map( + (mount) => html` + ${mount.name} + ${mount.server}${mount.port + ? `:${mount.port}` + : nothing}${mount.type === SupervisorMountType.NFS + ? mount.path + : ` :${mount.share}`} + + ` + )} + ${this.usage !== SupervisorMountUsage.MEDIA && + this._mounts.default_backup_mount + ? dataDiskOption + : nothing} + + `; + } + + private _filterMounts = memoizeOne( + (mounts: SupervisorMounts, usage: this["usage"]) => { + let filteredMounts = mounts.mounts.filter((mount) => + [SupervisorMountType.CIFS, SupervisorMountType.NFS].includes(mount.type) + ); + if (usage) { + filteredMounts = mounts.mounts.filter((mount) => mount.usage === usage); + } + return filteredMounts.sort((mountA, mountB) => { + if (mountA.name === mounts.default_backup_mount) { + return -1; + } + if (mountB.name === mounts.default_backup_mount) { + return 1; + } + return caseInsensitiveStringCompare( + mountA.name, + mountB.name, + this.hass.locale.language + ); + }); + } + ); + + private async _getMounts() { + try { + if (isComponentLoaded(this.hass, "hassio")) { + this._mounts = await fetchSupervisorMounts(this.hass); + } else { + this._error = this.hass.localize( + "ui.components.mount-picker.error.no_supervisor" + ); + } + } catch (err: any) { + this._error = this.hass.localize( + "ui.components.mount-picker.error.fetch_mounts" + ); + } + } + + private get _value() { + return this.value || ""; + } + + private _mountChanged(ev: Event) { + ev.stopPropagation(); + const target = ev.target as HaSelect; + const newValue = target.value; + + if (newValue !== this._value) { + this._setValue(newValue); + } + } + + private _setValue(value: string) { + this.value = value; + setTimeout(() => { + fireEvent(this, "value-changed", { value }); + fireEvent(this, "change"); + }, 0); + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-mount-picker": HaMountPicker; + } +} diff --git a/src/components/ha-selector/ha-selector-backup-location.ts b/src/components/ha-selector/ha-selector-backup-location.ts new file mode 100644 index 0000000000..f20c447a7b --- /dev/null +++ b/src/components/ha-selector/ha-selector-backup-location.ts @@ -0,0 +1,46 @@ +import { css, html, LitElement } from "lit"; +import { customElement, property } from "lit/decorators"; +import { BackupLocationSelector } from "../../data/selector"; +import { HomeAssistant } from "../../types"; +import "../ha-mount-picker"; + +@customElement("ha-selector-backup_location") +export class HaBackupLocationSelector extends LitElement { + @property() public hass!: HomeAssistant; + + @property() public selector!: BackupLocationSelector; + + @property() public value?: any; + + @property() public label?: string; + + @property() public helper?: string; + + @property({ type: Boolean }) public disabled = false; + + @property({ type: Boolean }) public required = true; + + protected render() { + return html``; + } + + static styles = css` + ha-mount-picker { + width: 100%; + } + `; +} + +declare global { + interface HTMLElementTagNameMap { + "ha-selector-backup-location": HaBackupLocationSelector; + } +} diff --git a/src/components/ha-selector/ha-selector.ts b/src/components/ha-selector/ha-selector.ts index 3f732c212d..2e7e5143d8 100644 --- a/src/components/ha-selector/ha-selector.ts +++ b/src/components/ha-selector/ha-selector.ts @@ -33,6 +33,7 @@ const LOAD_ELEMENTS = { object: () => import("./ha-selector-object"), select: () => import("./ha-selector-select"), state: () => import("./ha-selector-state"), + backup_location: () => import("./ha-selector-backup-location"), stt: () => import("./ha-selector-stt"), target: () => import("./ha-selector-target"), template: () => import("./ha-selector-template"), diff --git a/src/data/selector.ts b/src/data/selector.ts index 8250172c80..7c75a806bf 100644 --- a/src/data/selector.ts +++ b/src/data/selector.ts @@ -300,6 +300,11 @@ export interface StateSelector { } | null; } +export interface BackupLocationSelector { + // eslint-disable-next-line @typescript-eslint/ban-types + backup_location: {} | null; +} + export interface StringSelector { text: { multiline?: boolean; diff --git a/src/translations/en.json b/src/translations/en.json index 883c6542bd..8422f0d143 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -453,14 +453,16 @@ "addon-picker": { "addon": "Add-on", "error": { - "no_supervisor": { - "title": "No Supervisor", - "description": "Add-ons are not supported." - }, - "fetch_addons": { - "title": "Error loading add-ons", - "description": "There was an error loading add-ons." - } + "no_supervisor": "Add-ons are not supported on your installation.", + "fetch_addons": "There was an error loading add-ons." + } + }, + "mount-picker": { + "mount": "Location", + "use_datadisk": "Use data disk for backup", + "error": { + "no_supervisor": "Storage is not supported on your installation.", + "fetch_mounts": "There was an error loading the locations." } }, "stt-picker": {