mirror of
https://github.com/home-assistant/frontend.git
synced 2026-08-15 10:58:38 +00:00
Compare commits
1
Commits
dev
...
local-disk-mounts
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e4f54540c2 |
@@ -11,6 +11,7 @@ import {
|
||||
fetchSupervisorMounts,
|
||||
SupervisorMountType,
|
||||
SupervisorMountUsage,
|
||||
supervisorMountDescription,
|
||||
} from "../data/supervisor/mounts";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-alert";
|
||||
@@ -58,9 +59,7 @@ class HaMountPicker extends LitElement {
|
||||
).map((mount) => ({
|
||||
value: mount.name,
|
||||
label: mount.name,
|
||||
secondary: `${mount.server}${mount.port ? `:${mount.port}` : ""}${
|
||||
mount.type === SupervisorMountType.NFS ? mount.path : `:${mount.share}`
|
||||
}`,
|
||||
secondary: supervisorMountDescription(mount),
|
||||
iconPath:
|
||||
mount.usage === SupervisorMountUsage.MEDIA
|
||||
? mdiPlayBox
|
||||
@@ -108,10 +107,16 @@ class HaMountPicker extends LitElement {
|
||||
private _filterMounts = memoizeOne(
|
||||
(mounts: SupervisorMounts, usage: this["usage"]) => {
|
||||
let filteredMounts = mounts.mounts.filter((mount) =>
|
||||
[SupervisorMountType.CIFS, SupervisorMountType.NFS].includes(mount.type)
|
||||
[
|
||||
SupervisorMountType.CIFS,
|
||||
SupervisorMountType.DISK,
|
||||
SupervisorMountType.NFS,
|
||||
].includes(mount.type)
|
||||
);
|
||||
if (usage) {
|
||||
filteredMounts = mounts.mounts.filter((mount) => mount.usage === usage);
|
||||
filteredMounts = filteredMounts.filter(
|
||||
(mount) => mount.usage === usage
|
||||
);
|
||||
}
|
||||
return filteredMounts.sort((mountA, mountB) => {
|
||||
if (mountA.name === mounts.default_backup_mount) {
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { HomeAssistant } from "../../types";
|
||||
export enum SupervisorMountType {
|
||||
BIND = "bind",
|
||||
CIFS = "cifs",
|
||||
DISK = "disk",
|
||||
NFS = "nfs",
|
||||
}
|
||||
|
||||
@@ -28,26 +29,40 @@ interface SupervisorMountBase {
|
||||
name: string;
|
||||
usage: SupervisorMountUsage;
|
||||
type: SupervisorMountType;
|
||||
server: string;
|
||||
port: number;
|
||||
read_only?: boolean;
|
||||
}
|
||||
|
||||
export interface SupervisorMountResponse extends SupervisorMountBase {
|
||||
state: SupervisorMountState | null;
|
||||
}
|
||||
|
||||
export interface SupervisorNFSMount extends SupervisorMountResponse {
|
||||
// Supervisor omits port when the mount uses the protocol default.
|
||||
interface SupervisorNetworkMount extends SupervisorMountResponse {
|
||||
server: string;
|
||||
port?: number;
|
||||
}
|
||||
|
||||
export interface SupervisorNFSMount extends SupervisorNetworkMount {
|
||||
type: SupervisorMountType.NFS;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export interface SupervisorCIFSMount extends SupervisorMountResponse {
|
||||
export interface SupervisorCIFSMount extends SupervisorNetworkMount {
|
||||
type: SupervisorMountType.CIFS;
|
||||
share: string;
|
||||
version?: CIFSVersion;
|
||||
}
|
||||
|
||||
export type SupervisorMount = SupervisorNFSMount | SupervisorCIFSMount;
|
||||
// A disk mount is identified by device on the way in, but Supervisor resolves
|
||||
// that to a UUID and only ever reports uuid and filesystem back.
|
||||
export interface SupervisorDiskMount extends SupervisorMountResponse {
|
||||
type: SupervisorMountType.DISK;
|
||||
uuid: string;
|
||||
filesystem?: string;
|
||||
}
|
||||
|
||||
export type SupervisorMount =
|
||||
SupervisorNFSMount | SupervisorCIFSMount | SupervisorDiskMount;
|
||||
|
||||
export type SupervisorNFSMountRequestParams = SupervisorNFSMount;
|
||||
|
||||
@@ -57,14 +72,67 @@ export interface SupervisorCIFSMountRequestParams extends SupervisorCIFSMount {
|
||||
version?: CIFSVersion;
|
||||
}
|
||||
|
||||
interface SupervisorDiskMountRequestParamsBase {
|
||||
name: string;
|
||||
usage: SupervisorMountUsage;
|
||||
type: SupervisorMountType.DISK;
|
||||
read_only?: boolean;
|
||||
}
|
||||
|
||||
// Supervisor accepts exactly one identifier: device when creating from a
|
||||
// candidate, uuid when round-tripping a mount it already resolved.
|
||||
export type SupervisorDiskMountRequestParams =
|
||||
| (SupervisorDiskMountRequestParamsBase & { device: string; uuid?: never })
|
||||
| (SupervisorDiskMountRequestParamsBase & { uuid: string; device?: never });
|
||||
|
||||
export type SupervisorMountRequestParams =
|
||||
SupervisorNFSMountRequestParams | SupervisorCIFSMountRequestParams;
|
||||
| SupervisorNFSMountRequestParams
|
||||
| SupervisorCIFSMountRequestParams
|
||||
| SupervisorDiskMountRequestParams;
|
||||
|
||||
export interface SupervisorMounts {
|
||||
default_backup_mount: string | null;
|
||||
mounts: SupervisorMount[];
|
||||
}
|
||||
|
||||
// Null when UDisks2 cannot attribute the device to a drive, which also happens
|
||||
// when the drive is unplugged between enumeration and lookup.
|
||||
export interface SupervisorMountCandidateDrive {
|
||||
vendor: string;
|
||||
model: string;
|
||||
serial: string;
|
||||
id: string;
|
||||
size: number;
|
||||
connection_bus: string;
|
||||
removable: boolean;
|
||||
ejectable: boolean;
|
||||
}
|
||||
|
||||
export interface SupervisorMountCandidate {
|
||||
device: string;
|
||||
uuid: string;
|
||||
label: string;
|
||||
filesystem: string;
|
||||
size: number;
|
||||
read_only: boolean;
|
||||
drive: SupervisorMountCandidateDrive | null;
|
||||
}
|
||||
|
||||
export interface SupervisorMountCandidates {
|
||||
candidates: SupervisorMountCandidate[];
|
||||
}
|
||||
|
||||
// Identifies a mount in a list row. A disk mount has no server, share or path,
|
||||
// so it is described by what Supervisor does report for it.
|
||||
export const supervisorMountDescription = (mount: SupervisorMount): string => {
|
||||
if (mount.type === SupervisorMountType.DISK) {
|
||||
return [mount.filesystem, mount.uuid].filter(Boolean).join(" • ");
|
||||
}
|
||||
return `${mount.server}${mount.port ? `:${mount.port}` : ""}${
|
||||
mount.type === SupervisorMountType.NFS ? mount.path : `:${mount.share}`
|
||||
}`;
|
||||
};
|
||||
|
||||
export const fetchSupervisorMounts = async (
|
||||
hass: HomeAssistant
|
||||
): Promise<SupervisorMounts> =>
|
||||
@@ -75,6 +143,18 @@ export const fetchSupervisorMounts = async (
|
||||
timeout: null,
|
||||
});
|
||||
|
||||
// Returns an empty list on a host without UDisks2. A Supervisor predating disk
|
||||
// mounts answers 404, which callers use to hide the feature.
|
||||
export const fetchSupervisorMountCandidates = async (
|
||||
hass: HomeAssistant
|
||||
): Promise<SupervisorMountCandidates> =>
|
||||
hass.callWS({
|
||||
type: "supervisor/api",
|
||||
endpoint: `/mounts/candidates`,
|
||||
method: "get",
|
||||
timeout: null,
|
||||
});
|
||||
|
||||
export const createSupervisorMount = async (
|
||||
hass: HomeAssistant,
|
||||
data: SupervisorMountRequestParams
|
||||
|
||||
@@ -14,28 +14,92 @@ import type { SchemaUnion } from "../../../components/ha-form/types";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-dialog";
|
||||
import { extractApiErrorMessage } from "../../../data/hassio/common";
|
||||
import type { SupervisorMountRequestParams } from "../../../data/supervisor/mounts";
|
||||
import type {
|
||||
SupervisorMountCandidate,
|
||||
SupervisorMountRequestParams,
|
||||
} from "../../../data/supervisor/mounts";
|
||||
import {
|
||||
createSupervisorMount,
|
||||
fetchSupervisorMountCandidates,
|
||||
removeSupervisorMount,
|
||||
SupervisorMountType,
|
||||
SupervisorMountUsage,
|
||||
updateSupervisorMount,
|
||||
} from "../../../data/supervisor/mounts";
|
||||
import { bytesToString } from "../../../util/bytes-to-string";
|
||||
import { DirtyStateProviderMixin } from "../../../mixins/dirty-state-provider-mixin";
|
||||
import { haStyle, haStyleDialog } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { documentationUrl } from "../../../util/documentation-url";
|
||||
import type { MountViewDialogParams } from "./show-dialog-view-mount";
|
||||
|
||||
// Describes a device by the drive it belongs to, falling back to what UDisks2
|
||||
// did report: an unattributed device has no drive, and an unformatted-label
|
||||
// partition has no label.
|
||||
const mountCandidateLabel = (candidate: SupervisorMountCandidate): string => {
|
||||
const drive = [candidate.drive?.vendor, candidate.drive?.model]
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
const identity = candidate.label || candidate.device;
|
||||
const size = bytesToString(candidate.size);
|
||||
return drive ? `${drive} — ${identity}, ${size}` : `${identity}, ${size}`;
|
||||
};
|
||||
|
||||
const mountSchema = memoizeOne(
|
||||
(
|
||||
localize: LocalizeFunc,
|
||||
existing?: boolean,
|
||||
mountType?: SupervisorMountType,
|
||||
showCIFSVersion?: boolean
|
||||
) =>
|
||||
[
|
||||
showCIFSVersion?: boolean,
|
||||
showDisk?: boolean,
|
||||
candidates?: SupervisorMountCandidate[],
|
||||
diskIdentity?: string,
|
||||
readOnlyForced?: boolean,
|
||||
allowBackupUsage = true
|
||||
) => {
|
||||
// Supervisor rejects a read-only mount used for backups, so a device that
|
||||
// can only be mounted read-only is not offered for one.
|
||||
const usageOptions: [string, string][] = allowBackupUsage
|
||||
? [
|
||||
[
|
||||
SupervisorMountUsage.BACKUP,
|
||||
localize(
|
||||
"ui.panel.config.storage.network_mounts.mount_usage.backup"
|
||||
),
|
||||
],
|
||||
]
|
||||
: [];
|
||||
usageOptions.push(
|
||||
[
|
||||
SupervisorMountUsage.MEDIA,
|
||||
localize("ui.panel.config.storage.network_mounts.mount_usage.media"),
|
||||
],
|
||||
[
|
||||
SupervisorMountUsage.SHARE,
|
||||
localize("ui.panel.config.storage.network_mounts.mount_usage.share"),
|
||||
]
|
||||
);
|
||||
|
||||
const typeOptions: [string, string][] = [
|
||||
[
|
||||
SupervisorMountType.CIFS,
|
||||
localize("ui.panel.config.storage.network_mounts.mount_type.cifs"),
|
||||
],
|
||||
[
|
||||
SupervisorMountType.NFS,
|
||||
localize("ui.panel.config.storage.network_mounts.mount_type.nfs"),
|
||||
],
|
||||
];
|
||||
// Hidden on a Supervisor that does not support disk mounts, but always
|
||||
// offered when editing one that already exists.
|
||||
if (showDisk || mountType === SupervisorMountType.DISK) {
|
||||
typeOptions.push([
|
||||
SupervisorMountType.DISK,
|
||||
localize("ui.panel.config.storage.network_mounts.mount_type.disk"),
|
||||
]);
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
name: "name",
|
||||
required: true,
|
||||
@@ -46,57 +110,34 @@ const mountSchema = memoizeOne(
|
||||
name: "usage",
|
||||
required: true,
|
||||
type: "select",
|
||||
options: [
|
||||
[
|
||||
SupervisorMountUsage.BACKUP,
|
||||
localize(
|
||||
"ui.panel.config.storage.network_mounts.mount_usage.backup"
|
||||
),
|
||||
],
|
||||
[
|
||||
SupervisorMountUsage.MEDIA,
|
||||
localize(
|
||||
"ui.panel.config.storage.network_mounts.mount_usage.media"
|
||||
),
|
||||
],
|
||||
[
|
||||
SupervisorMountUsage.SHARE,
|
||||
localize(
|
||||
"ui.panel.config.storage.network_mounts.mount_usage.share"
|
||||
),
|
||||
],
|
||||
] as const,
|
||||
},
|
||||
{
|
||||
name: "server",
|
||||
required: true,
|
||||
selector: { text: {} },
|
||||
options: usageOptions,
|
||||
},
|
||||
{
|
||||
name: "type",
|
||||
required: true,
|
||||
type: "select",
|
||||
options: [
|
||||
[
|
||||
SupervisorMountType.CIFS,
|
||||
localize("ui.panel.config.storage.network_mounts.mount_type.cifs"),
|
||||
],
|
||||
[
|
||||
SupervisorMountType.NFS,
|
||||
localize("ui.panel.config.storage.network_mounts.mount_type.nfs"),
|
||||
],
|
||||
],
|
||||
options: typeOptions,
|
||||
},
|
||||
...(mountType === "nfs"
|
||||
...(mountType === SupervisorMountType.NFS
|
||||
? ([
|
||||
{
|
||||
name: "server",
|
||||
required: true,
|
||||
selector: { text: {} },
|
||||
},
|
||||
{
|
||||
name: "path",
|
||||
required: true,
|
||||
selector: { text: {} },
|
||||
},
|
||||
] as const)
|
||||
: mountType === "cifs"
|
||||
: mountType === SupervisorMountType.CIFS
|
||||
? ([
|
||||
{
|
||||
name: "server",
|
||||
required: true,
|
||||
selector: { text: {} },
|
||||
},
|
||||
...(showCIFSVersion
|
||||
? ([
|
||||
{
|
||||
@@ -148,8 +189,44 @@ const mountSchema = memoizeOne(
|
||||
selector: { text: { type: "password" } },
|
||||
},
|
||||
] as const)
|
||||
: ([] as const)),
|
||||
] as const
|
||||
: mountType === SupervisorMountType.DISK
|
||||
? existing
|
||||
? // Supervisor excludes a mounted device from the candidates, so
|
||||
// an existing mount can only show what it resolved to.
|
||||
([
|
||||
{
|
||||
name: "device_identity",
|
||||
type: "constant",
|
||||
value: diskIdentity,
|
||||
},
|
||||
{
|
||||
name: "read_only",
|
||||
selector: { boolean: {} },
|
||||
},
|
||||
] as const)
|
||||
: ([
|
||||
{
|
||||
name: "device",
|
||||
required: true,
|
||||
selector: {
|
||||
select: {
|
||||
options: (candidates ?? []).map((candidate) => ({
|
||||
value: candidate.device,
|
||||
label: mountCandidateLabel(candidate),
|
||||
})),
|
||||
mode: "dropdown",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "read_only",
|
||||
disabled: readOnlyForced,
|
||||
selector: { boolean: {} },
|
||||
},
|
||||
] as const)
|
||||
: ([] as const)),
|
||||
] as const;
|
||||
}
|
||||
);
|
||||
|
||||
@customElement("dialog-mount-view")
|
||||
@@ -172,6 +249,12 @@ class ViewMountDialog extends DirtyStateProviderMixin<
|
||||
|
||||
@state() private _showCIFSVersion?: boolean;
|
||||
|
||||
@state() private _candidates?: SupervisorMountCandidate[];
|
||||
|
||||
@state() private _diskSupported = false;
|
||||
|
||||
@state() private _diskIdentity?: string;
|
||||
|
||||
@state() private _reloadMounts?: () => void;
|
||||
|
||||
@state() private _open = false;
|
||||
@@ -190,13 +273,36 @@ class ViewMountDialog extends DirtyStateProviderMixin<
|
||||
) {
|
||||
this._showCIFSVersion = true;
|
||||
}
|
||||
if (dialogParams.mount?.type === SupervisorMountType.DISK) {
|
||||
this._diskIdentity = [
|
||||
dialogParams.mount.filesystem,
|
||||
dialogParams.mount.uuid,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" • ");
|
||||
}
|
||||
this._initDirtyTracking({ type: "deep" }, this._data ?? {});
|
||||
this._loadCandidates();
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._open = false;
|
||||
}
|
||||
|
||||
private async _loadCandidates(): Promise<void> {
|
||||
try {
|
||||
const { candidates } = await fetchSupervisorMountCandidates(this.hass);
|
||||
this._candidates = candidates;
|
||||
this._diskSupported = true;
|
||||
} catch (_err: any) {
|
||||
// A Supervisor predating disk mounts answers 404. Any other failure
|
||||
// leaves us unable to offer a device either, so in both cases the option
|
||||
// is hidden rather than shown as broken.
|
||||
this._candidates = [];
|
||||
this._diskSupported = false;
|
||||
}
|
||||
}
|
||||
|
||||
private _dialogClosed(): void {
|
||||
this._data = undefined;
|
||||
this._waiting = undefined;
|
||||
@@ -205,6 +311,9 @@ class ViewMountDialog extends DirtyStateProviderMixin<
|
||||
this._validationWarning = undefined;
|
||||
this._existing = undefined;
|
||||
this._showCIFSVersion = undefined;
|
||||
this._candidates = undefined;
|
||||
this._diskSupported = false;
|
||||
this._diskIdentity = undefined;
|
||||
this._reloadMounts = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
@@ -249,6 +358,15 @@ class ViewMountDialog extends DirtyStateProviderMixin<
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
this._showNoCandidates
|
||||
? html`<ha-alert alert-type="info">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.storage.network_mounts.no_disk_candidates"
|
||||
)}
|
||||
</ha-alert>`
|
||||
: nothing
|
||||
}
|
||||
<ha-form
|
||||
autofocus
|
||||
.data=${this._data}
|
||||
@@ -256,7 +374,12 @@ class ViewMountDialog extends DirtyStateProviderMixin<
|
||||
this.hass.localize,
|
||||
this._existing,
|
||||
this._data?.type,
|
||||
this._showCIFSVersion
|
||||
this._showCIFSVersion,
|
||||
this._diskSupported,
|
||||
this._candidates,
|
||||
this._diskIdentity,
|
||||
this._readOnlyForced,
|
||||
this._allowBackupUsage
|
||||
)}
|
||||
.error=${this._validationError}
|
||||
.warning=${this._validationWarning}
|
||||
@@ -308,6 +431,33 @@ class ViewMountDialog extends DirtyStateProviderMixin<
|
||||
`;
|
||||
}
|
||||
|
||||
// The device the mount already uses is excluded from candidates, so an empty
|
||||
// list is only worth mentioning while creating one.
|
||||
private get _showNoCandidates(): boolean {
|
||||
return (
|
||||
!this._existing &&
|
||||
this._data?.type === SupervisorMountType.DISK &&
|
||||
this._candidates?.length === 0
|
||||
);
|
||||
}
|
||||
|
||||
private get _readOnlyForced(): boolean {
|
||||
if (this._existing || this._data?.type !== SupervisorMountType.DISK) {
|
||||
return false;
|
||||
}
|
||||
const { device } = this._data;
|
||||
return !!this._candidates?.find((candidate) => candidate.device === device)
|
||||
?.read_only;
|
||||
}
|
||||
|
||||
// Backup usage is impossible for a read-only mount, whether the device forced
|
||||
// that or the user chose it.
|
||||
private get _allowBackupUsage(): boolean {
|
||||
return !(
|
||||
this._data?.type === SupervisorMountType.DISK && this._data.read_only
|
||||
);
|
||||
}
|
||||
|
||||
private _computeLabelCallback = (
|
||||
// @ts-ignore
|
||||
schema: SchemaUnion<ReturnType<typeof mountSchema>>
|
||||
@@ -353,6 +503,15 @@ class ViewMountDialog extends DirtyStateProviderMixin<
|
||||
) {
|
||||
this._validationWarning.version = "not_recomeded_cifs_version";
|
||||
}
|
||||
// A device the host reports as read-only cannot be mounted writable.
|
||||
if (this._readOnlyForced) {
|
||||
this._data!.read_only = true;
|
||||
}
|
||||
// Picking such a device while backup was selected leaves a combination
|
||||
// Supervisor refuses, so drop the usage and make the user choose again.
|
||||
if (!this._allowBackupUsage && this._data?.usage === "backup") {
|
||||
delete (this._data as Partial<SupervisorMountRequestParams>).usage;
|
||||
}
|
||||
this._updateDirtyState(this._data ?? {});
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
SupervisorMountUsage,
|
||||
fetchSupervisorMounts,
|
||||
reloadSupervisorMount,
|
||||
supervisorMountDescription,
|
||||
} from "../../../data/supervisor/mounts";
|
||||
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import "../../../layouts/hass-subpage";
|
||||
@@ -74,7 +75,11 @@ class HaConfigSectionStorage extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
const validMounts = this._mountsInfo?.mounts.filter((mount) =>
|
||||
[SupervisorMountType.CIFS, SupervisorMountType.NFS].includes(mount.type)
|
||||
[
|
||||
SupervisorMountType.CIFS,
|
||||
SupervisorMountType.DISK,
|
||||
SupervisorMountType.NFS,
|
||||
].includes(mount.type)
|
||||
);
|
||||
const isHAOS = this._hostInfo?.features.includes("haos");
|
||||
return html`
|
||||
@@ -193,13 +198,7 @@ class HaConfigSectionStorage extends LitElement {
|
||||
${mount.name}
|
||||
</span>
|
||||
<span slot="secondary">
|
||||
${mount.server}${
|
||||
mount.port ? `:${mount.port}` : nothing
|
||||
}${
|
||||
mount.type === SupervisorMountType.NFS
|
||||
? mount.path
|
||||
: `:${mount.share}`
|
||||
}
|
||||
${supervisorMountDescription(mount)}
|
||||
</span>
|
||||
${
|
||||
mount.state !== SupervisorMountState.ACTIVE
|
||||
|
||||
+27
-13
@@ -3445,7 +3445,7 @@
|
||||
"agents": {
|
||||
"cloud_agent_description": "Note: It stores only the most recent backup, regardless of your retention settings, with a maximum size of 5 GB.",
|
||||
"cloud_agent_no_subcription": "You currently do not have an active Home Assistant Cloud subscription.",
|
||||
"network_mount_agent_description": "Network storage",
|
||||
"network_mount_agent_description": "Additional storage",
|
||||
"unavailable_agents": "Unavailable locations",
|
||||
"no_agents": "No locations configured",
|
||||
"encryption_turned_off": "Encryption turned off",
|
||||
@@ -3730,7 +3730,7 @@
|
||||
"no_location": "No location selected",
|
||||
"no_location_description": "You have to select at least one location to create a backup.",
|
||||
"more_locations": "Explore more locations",
|
||||
"manage_network_storage": "Manage network storage",
|
||||
"manage_network_storage": "Manage storage",
|
||||
"ha_cloud_backup": "{home_assistant_cloud} backup",
|
||||
"ha_cloud_description": "Stores an encrypted backup offsite. This is ideal in case your local backup becomes inaccessible. The cloud stores one backup with a maximum size of 5 GB, and older backups are automatically deleted."
|
||||
},
|
||||
@@ -8638,7 +8638,7 @@
|
||||
"join_chat": "Chat",
|
||||
"join_blog": "Blog",
|
||||
"join_newsletter": "Newsletter",
|
||||
"media_storage": "You can add network storage to your Home Assistant instance in the {storage} panel."
|
||||
"media_storage": "You can add additional storage to your Home Assistant instance in the {storage} panel."
|
||||
},
|
||||
"analytics": {
|
||||
"caption": "Analytics",
|
||||
@@ -8901,15 +8901,16 @@
|
||||
"move": "Move"
|
||||
},
|
||||
"network_mounts": {
|
||||
"title": "Network storage",
|
||||
"add_title": "Add network storage",
|
||||
"update_title": "Update network storage",
|
||||
"no_mounts": "No connected network storage",
|
||||
"title": "Additional storage",
|
||||
"add_title": "Add storage",
|
||||
"update_title": "Update storage",
|
||||
"no_mounts": "No additional storage configured",
|
||||
"no_disk_candidates": "No suitable disks were found. Connect a disk with a supported file system, then reopen this dialog.",
|
||||
"documentation": "Documentation",
|
||||
"not_supported": {
|
||||
"title": "The operating system does not support network storage",
|
||||
"supervised": "Network storage is not supported on this host",
|
||||
"os": "To use network storage you need to run at least Home Assistant OS {version}",
|
||||
"title": "The operating system does not support additional storage",
|
||||
"supervised": "Additional storage is not supported on this host",
|
||||
"os": "To use additional storage you need to run at least Home Assistant OS {version}",
|
||||
"navigate_to_updates": "Go to updates"
|
||||
},
|
||||
"mount_usage": {
|
||||
@@ -8919,7 +8920,8 @@
|
||||
},
|
||||
"mount_type": {
|
||||
"nfs": "Network File System (NFS)",
|
||||
"cifs": "Samba/Windows (CIFS)"
|
||||
"cifs": "Samba/Windows (CIFS)",
|
||||
"disk": "Local disk"
|
||||
},
|
||||
"cifs_versions": {
|
||||
"auto": "Auto (2.1+)",
|
||||
@@ -8947,13 +8949,25 @@
|
||||
"description": "This is the path of the remote share on your storage server"
|
||||
},
|
||||
"type": {
|
||||
"title": "Protocol",
|
||||
"description": "This determines how to communicate with the storage server"
|
||||
"title": "Type",
|
||||
"description": "This determines the kind of storage you want to add"
|
||||
},
|
||||
"usage": {
|
||||
"title": "Usage",
|
||||
"description": "This determines how the share is intended to be used"
|
||||
},
|
||||
"device": {
|
||||
"title": "Disk",
|
||||
"description": "This is the disk connected to your system that you want to use"
|
||||
},
|
||||
"device_identity": {
|
||||
"title": "Disk",
|
||||
"description": "This is the disk this storage was set up with, which cannot be changed"
|
||||
},
|
||||
"read_only": {
|
||||
"title": "Read-only",
|
||||
"description": "Home Assistant will not be able to write to this storage. Backup storage cannot be read-only"
|
||||
},
|
||||
"version": {
|
||||
"title": "Samba/Windows (CIFS) version",
|
||||
"description": "This chooses the version of the protocol to use"
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type {
|
||||
SupervisorCIFSMount,
|
||||
SupervisorDiskMount,
|
||||
SupervisorNFSMount,
|
||||
} from "../../src/data/supervisor/mounts";
|
||||
import {
|
||||
SupervisorMountState,
|
||||
SupervisorMountType,
|
||||
SupervisorMountUsage,
|
||||
supervisorMountDescription,
|
||||
} from "../../src/data/supervisor/mounts";
|
||||
|
||||
const nfsMount = (
|
||||
overrides: Partial<SupervisorNFSMount> = {}
|
||||
): SupervisorNFSMount => ({
|
||||
name: "my_nfs",
|
||||
type: SupervisorMountType.NFS,
|
||||
usage: SupervisorMountUsage.MEDIA,
|
||||
state: SupervisorMountState.ACTIVE,
|
||||
server: "nas.local",
|
||||
path: "/export/media",
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const cifsMount = (
|
||||
overrides: Partial<SupervisorCIFSMount> = {}
|
||||
): SupervisorCIFSMount => ({
|
||||
name: "my_share",
|
||||
type: SupervisorMountType.CIFS,
|
||||
usage: SupervisorMountUsage.MEDIA,
|
||||
state: SupervisorMountState.ACTIVE,
|
||||
server: "nas.local",
|
||||
share: "media",
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const diskMount = (
|
||||
overrides: Partial<SupervisorDiskMount> = {}
|
||||
): SupervisorDiskMount => ({
|
||||
name: "media_disk",
|
||||
type: SupervisorMountType.DISK,
|
||||
usage: SupervisorMountUsage.MEDIA,
|
||||
state: SupervisorMountState.ACTIVE,
|
||||
uuid: "e3f1a2b4-5c6d-7e8f-9a0b-1c2d3e4f5a6b",
|
||||
filesystem: "ext4",
|
||||
...overrides,
|
||||
});
|
||||
|
||||
describe("supervisorMountDescription", () => {
|
||||
it("describes an NFS mount by server and path", () => {
|
||||
expect(supervisorMountDescription(nfsMount())).toBe(
|
||||
"nas.local/export/media"
|
||||
);
|
||||
});
|
||||
|
||||
it("describes a CIFS mount by server and share", () => {
|
||||
expect(supervisorMountDescription(cifsMount())).toBe("nas.local:media");
|
||||
});
|
||||
|
||||
it("includes the port only when Supervisor reports one", () => {
|
||||
expect(supervisorMountDescription(nfsMount({ port: 2049 }))).toBe(
|
||||
"nas.local:2049/export/media"
|
||||
);
|
||||
expect(supervisorMountDescription(nfsMount({ port: undefined }))).toBe(
|
||||
"nas.local/export/media"
|
||||
);
|
||||
});
|
||||
|
||||
it("describes a disk mount by filesystem and uuid", () => {
|
||||
expect(supervisorMountDescription(diskMount())).toBe(
|
||||
"ext4 • e3f1a2b4-5c6d-7e8f-9a0b-1c2d3e4f5a6b"
|
||||
);
|
||||
});
|
||||
|
||||
it("omits the filesystem when Supervisor has not resolved one", () => {
|
||||
expect(
|
||||
supervisorMountDescription(diskMount({ filesystem: undefined }))
|
||||
).toBe("e3f1a2b4-5c6d-7e8f-9a0b-1c2d3e4f5a6b");
|
||||
});
|
||||
|
||||
it("never describes a disk mount using network fields", () => {
|
||||
// A disk mount has no server, share or path, which previously rendered as
|
||||
// "undefined" in the storage panel and the mount picker.
|
||||
expect(supervisorMountDescription(diskMount())).not.toContain("undefined");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user