mirror of https://github.com/jetkvm/kvm.git
prettifying
This commit is contained in:
parent
e9096c36f7
commit
03fd7508de
|
@ -5,27 +5,27 @@ import {
|
||||||
useUiStore,
|
useUiStore,
|
||||||
useUpdateStore,
|
useUpdateStore,
|
||||||
} from "@/hooks/stores";
|
} from "@/hooks/stores";
|
||||||
import {Checkbox} from "@components/Checkbox";
|
import { Checkbox } from "@components/Checkbox";
|
||||||
import {Button, LinkButton} from "@components/Button";
|
import { Button, LinkButton } from "@components/Button";
|
||||||
import {TextAreaWithLabel} from "@components/TextArea";
|
import { TextAreaWithLabel } from "@components/TextArea";
|
||||||
import {SectionHeader} from "@components/SectionHeader";
|
import { SectionHeader } from "@components/SectionHeader";
|
||||||
import {GridCard} from "@components/Card";
|
import { GridCard } from "@components/Card";
|
||||||
import {InputFieldWithLabel} from "@components/InputField";
|
import { InputFieldWithLabel } from "@components/InputField";
|
||||||
import {CheckCircleIcon} from "@heroicons/react/20/solid";
|
import { CheckCircleIcon } from "@heroicons/react/20/solid";
|
||||||
import {cx} from "@/cva.config";
|
import { cx } from "@/cva.config";
|
||||||
import React, {useCallback, useEffect, useRef, useState} from "react";
|
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import {isOnDevice} from "@/main";
|
import { isOnDevice } from "@/main";
|
||||||
import PointingFinger from "@/assets/pointing-finger.svg";
|
import PointingFinger from "@/assets/pointing-finger.svg";
|
||||||
import MouseIcon from "@/assets/mouse-icon.svg";
|
import MouseIcon from "@/assets/mouse-icon.svg";
|
||||||
import {useJsonRpc} from "@/hooks/useJsonRpc";
|
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
||||||
import {SelectMenuBasic} from "../SelectMenuBasic";
|
import { SelectMenuBasic } from "../SelectMenuBasic";
|
||||||
import {SystemVersionInfo} from "@components/UpdateDialog";
|
import { SystemVersionInfo } from "@components/UpdateDialog";
|
||||||
import notifications from "@/notifications";
|
import notifications from "@/notifications";
|
||||||
import api from "../../api";
|
import api from "../../api";
|
||||||
import LocalAuthPasswordDialog from "@/components/LocalAuthPasswordDialog";
|
import LocalAuthPasswordDialog from "@/components/LocalAuthPasswordDialog";
|
||||||
import {LocalDevice} from "@routes/devices.$id";
|
import { LocalDevice } from "@routes/devices.$id";
|
||||||
import {useRevalidator} from "react-router-dom";
|
import { useRevalidator } from "react-router-dom";
|
||||||
import {ShieldCheckIcon} from "@heroicons/react/20/solid";
|
import { ShieldCheckIcon } from "@heroicons/react/20/solid";
|
||||||
|
|
||||||
export function SettingsItem({
|
export function SettingsItem({
|
||||||
title,
|
title,
|
||||||
|
@ -120,7 +120,7 @@ export default function SettingsSidebar() {
|
||||||
|
|
||||||
const handleUsbEmulationToggle = useCallback(
|
const handleUsbEmulationToggle = useCallback(
|
||||||
(enabled: boolean) => {
|
(enabled: boolean) => {
|
||||||
send("setUsbEmulationState", {enabled: enabled}, resp => {
|
send("setUsbEmulationState", { enabled: enabled }, resp => {
|
||||||
if ("error" in resp) {
|
if ("error" in resp) {
|
||||||
notifications.error(
|
notifications.error(
|
||||||
`Failed to ${enabled ? "enable" : "disable"} USB emulation: ${resp.error.data || "Unknown error"}`,
|
`Failed to ${enabled ? "enable" : "disable"} USB emulation: ${resp.error.data || "Unknown error"}`,
|
||||||
|
@ -156,7 +156,7 @@ export default function SettingsSidebar() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleStreamQualityChange = (factor: string) => {
|
const handleStreamQualityChange = (factor: string) => {
|
||||||
send("setStreamQualityFactor", {factor: Number(factor)}, resp => {
|
send("setStreamQualityFactor", { factor: Number(factor) }, resp => {
|
||||||
if ("error" in resp) {
|
if ("error" in resp) {
|
||||||
notifications.error(
|
notifications.error(
|
||||||
`Failed to set stream quality: ${resp.error.data || "Unknown error"}`,
|
`Failed to set stream quality: ${resp.error.data || "Unknown error"}`,
|
||||||
|
@ -168,7 +168,7 @@ export default function SettingsSidebar() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAutoUpdateChange = (enabled: boolean) => {
|
const handleAutoUpdateChange = (enabled: boolean) => {
|
||||||
send("setAutoUpdateState", {enabled}, resp => {
|
send("setAutoUpdateState", { enabled }, resp => {
|
||||||
if ("error" in resp) {
|
if ("error" in resp) {
|
||||||
notifications.error(
|
notifications.error(
|
||||||
`Failed to set auto-update: ${resp.error.data || "Unknown error"}`,
|
`Failed to set auto-update: ${resp.error.data || "Unknown error"}`,
|
||||||
|
@ -180,7 +180,7 @@ export default function SettingsSidebar() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDevChannelChange = (enabled: boolean) => {
|
const handleDevChannelChange = (enabled: boolean) => {
|
||||||
send("setDevChannelState", {enabled}, resp => {
|
send("setDevChannelState", { enabled }, resp => {
|
||||||
if ("error" in resp) {
|
if ("error" in resp) {
|
||||||
notifications.error(
|
notifications.error(
|
||||||
`Failed to set dev channel state: ${resp.error.data || "Unknown error"}`,
|
`Failed to set dev channel state: ${resp.error.data || "Unknown error"}`,
|
||||||
|
@ -192,7 +192,7 @@ export default function SettingsSidebar() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleJigglerChange = (enabled: boolean) => {
|
const handleJigglerChange = (enabled: boolean) => {
|
||||||
send("setJigglerState", {enabled}, resp => {
|
send("setJigglerState", { enabled }, resp => {
|
||||||
if ("error" in resp) {
|
if ("error" in resp) {
|
||||||
notifications.error(
|
notifications.error(
|
||||||
`Failed to set jiggler state: ${resp.error.data || "Unknown error"}`,
|
`Failed to set jiggler state: ${resp.error.data || "Unknown error"}`,
|
||||||
|
@ -204,7 +204,7 @@ export default function SettingsSidebar() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEDIDChange = (newEdid: string) => {
|
const handleEDIDChange = (newEdid: string) => {
|
||||||
send("setEDID", {edid: newEdid}, resp => {
|
send("setEDID", { edid: newEdid }, resp => {
|
||||||
if ("error" in resp) {
|
if ("error" in resp) {
|
||||||
notifications.error(`Failed to set EDID: ${resp.error.data || "Unknown error"}`);
|
notifications.error(`Failed to set EDID: ${resp.error.data || "Unknown error"}`);
|
||||||
return;
|
return;
|
||||||
|
@ -216,7 +216,7 @@ export default function SettingsSidebar() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUsbConfigChange = useCallback((usbConfig: object) => {
|
const handleUsbConfigChange = useCallback((usbConfig: object) => {
|
||||||
send("setUsbConfig", {usbConfig}, resp => {
|
send("setUsbConfig", { usbConfig }, resp => {
|
||||||
if ("error" in resp) {
|
if ("error" in resp) {
|
||||||
notifications.error(
|
notifications.error(
|
||||||
`Failed to update USB Config: ${resp.error.data || "Unknown error"}`,
|
`Failed to update USB Config: ${resp.error.data || "Unknown error"}`,
|
||||||
|
@ -232,7 +232,7 @@ export default function SettingsSidebar() {
|
||||||
|
|
||||||
const handleDevModeChange = useCallback(
|
const handleDevModeChange = useCallback(
|
||||||
(developerMode: boolean) => {
|
(developerMode: boolean) => {
|
||||||
send("setDevModeState", {enabled: developerMode}, resp => {
|
send("setDevModeState", { enabled: developerMode }, resp => {
|
||||||
if ("error" in resp) {
|
if ("error" in resp) {
|
||||||
notifications.error(
|
notifications.error(
|
||||||
`Failed to set dev mode: ${resp.error.data || "Unknown error"}`,
|
`Failed to set dev mode: ${resp.error.data || "Unknown error"}`,
|
||||||
|
@ -241,7 +241,7 @@ export default function SettingsSidebar() {
|
||||||
}
|
}
|
||||||
setDeveloperMode(developerMode);
|
setDeveloperMode(developerMode);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
sidebarRef.current?.scrollTo({top: 5000, behavior: "smooth"});
|
sidebarRef.current?.scrollTo({ top: 5000, behavior: "smooth" });
|
||||||
}, 0);
|
}, 0);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -249,7 +249,7 @@ export default function SettingsSidebar() {
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleUpdateSSHKey = useCallback(() => {
|
const handleUpdateSSHKey = useCallback(() => {
|
||||||
send("setSSHKeyState", {sshKey}, resp => {
|
send("setSSHKeyState", { sshKey }, resp => {
|
||||||
if ("error" in resp) {
|
if ("error" in resp) {
|
||||||
notifications.error(
|
notifications.error(
|
||||||
`Failed to update SSH key: ${resp.error.data || "Unknown error"}`,
|
`Failed to update SSH key: ${resp.error.data || "Unknown error"}`,
|
||||||
|
@ -261,26 +261,26 @@ export default function SettingsSidebar() {
|
||||||
}, [send, sshKey]);
|
}, [send, sshKey]);
|
||||||
|
|
||||||
const handleUsbProductIdChange = (productId: string) => {
|
const handleUsbProductIdChange = (productId: string) => {
|
||||||
setUsbConfig({...usbConfig, usb_product_id: productId})
|
setUsbConfig({... usbConfig, usb_product_id: productId})
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUsbVendorIdChange = (vendorId: string) => {
|
const handleUsbVendorIdChange = (vendorId: string) => {
|
||||||
setUsbConfig({...usbConfig, usb_vendor_id: vendorId})
|
setUsbConfig({... usbConfig, usb_vendor_id: vendorId})
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUsbSerialChange = (serialNumber: string) => {
|
const handleUsbSerialChange = (serialNumber: string) => {
|
||||||
setUsbConfig({...usbConfig, usb_serial_number: serialNumber})
|
setUsbConfig({... usbConfig, usb_serial_number: serialNumber})
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUsbName = (name: string) => {
|
const handleUsbName = (name: string) => {
|
||||||
setUsbConfig({...usbConfig, usb_name: name})
|
setUsbConfig({... usbConfig, usb_name: name})
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUsbManufacturer = (manufacturer: string) => {
|
const handleUsbManufacturer = (manufacturer: string) => {
|
||||||
setUsbConfig({...usbConfig, usb_manufacturer: manufacturer})
|
setUsbConfig({... usbConfig, usb_manufacturer: manufacturer})
|
||||||
};
|
};
|
||||||
|
|
||||||
const {setIsUpdateDialogOpen, setModalView, otaState} = useUpdateStore();
|
const { setIsUpdateDialogOpen, setModalView, otaState } = useUpdateStore();
|
||||||
const handleCheckForUpdates = () => {
|
const handleCheckForUpdates = () => {
|
||||||
if (otaState.updating) {
|
if (otaState.updating) {
|
||||||
setModalView("updating");
|
setModalView("updating");
|
||||||
|
@ -379,7 +379,7 @@ export default function SettingsSidebar() {
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const {setModalView: setLocalAuthModalView} = useLocalAuthModalStore();
|
const { setModalView: setLocalAuthModalView } = useLocalAuthModalStore();
|
||||||
const [isLocalAuthDialogOpen, setIsLocalAuthDialogOpen] = useState(false);
|
const [isLocalAuthDialogOpen, setIsLocalAuthDialogOpen] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -437,7 +437,7 @@ export default function SettingsSidebar() {
|
||||||
onKeyDown={e => e.stopPropagation()}
|
onKeyDown={e => e.stopPropagation()}
|
||||||
onKeyUp={e => e.stopPropagation()}
|
onKeyUp={e => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
<SidebarHeader title="Settings" setSidebarView={setSidebarView}/>
|
<SidebarHeader title="Settings" setSidebarView={setSidebarView} />
|
||||||
<div
|
<div
|
||||||
className="h-full px-4 py-2 space-y-4 overflow-y-scroll bg-white dark:bg-slate-900"
|
className="h-full px-4 py-2 space-y-4 overflow-y-scroll bg-white dark:bg-slate-900"
|
||||||
ref={sidebarRef}
|
ref={sidebarRef}
|
||||||
|
@ -450,7 +450,7 @@ export default function SettingsSidebar() {
|
||||||
currentVersions ? (
|
currentVersions ? (
|
||||||
<>
|
<>
|
||||||
App: {currentVersions.appVersion}
|
App: {currentVersions.appVersion}
|
||||||
<br/>
|
<br />
|
||||||
System: {currentVersions.systemVersion}
|
System: {currentVersions.systemVersion}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
@ -467,7 +467,7 @@ export default function SettingsSidebar() {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-[1px] w-full bg-slate-800/10 dark:bg-slate-300/20"/>
|
<div className="h-[1px] w-full bg-slate-800/10 dark:bg-slate-300/20" />
|
||||||
<SectionHeader
|
<SectionHeader
|
||||||
title="Mouse"
|
title="Mouse"
|
||||||
description="Customize mouse behavior and interaction settings"
|
description="Customize mouse behavior and interaction settings"
|
||||||
|
@ -497,7 +497,7 @@ export default function SettingsSidebar() {
|
||||||
/>
|
/>
|
||||||
</SettingsItem>
|
</SettingsItem>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<SettingsItem title="Modes" description="Choose the mouse input mode"/>
|
<SettingsItem title="Modes" description="Choose the mouse input mode" />
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<button
|
<button
|
||||||
className="block group grow"
|
className="block group grow"
|
||||||
|
@ -519,7 +519,7 @@ export default function SettingsSidebar() {
|
||||||
Most convenient
|
Most convenient
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<CheckCircleIcon className="w-4 h-4 text-blue-700 dark:text-blue-500"/>
|
<CheckCircleIcon className="w-4 h-4 text-blue-700 dark:text-blue-500" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</GridCard>
|
</GridCard>
|
||||||
|
@ -530,7 +530,7 @@ export default function SettingsSidebar() {
|
||||||
>
|
>
|
||||||
<GridCard>
|
<GridCard>
|
||||||
<div className="flex items-center px-4 py-3 gap-x-4">
|
<div className="flex items-center px-4 py-3 gap-x-4">
|
||||||
<img className="w-6 shrink-0" src={MouseIcon} alt="Mouse icon"/>
|
<img className="w-6 shrink-0" src={MouseIcon} alt="Mouse icon" />
|
||||||
<div className="flex items-center justify-between grow">
|
<div className="flex items-center justify-between grow">
|
||||||
<div className="text-left">
|
<div className="text-left">
|
||||||
<h3 className="text-sm font-semibold text-black dark:text-white">
|
<h3 className="text-sm font-semibold text-black dark:text-white">
|
||||||
|
@ -548,7 +548,7 @@ export default function SettingsSidebar() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-[1px] w-full bg-slate-800/10 dark:bg-slate-300/20"/>
|
<div className="h-[1px] w-full bg-slate-800/10 dark:bg-slate-300/20" />
|
||||||
<div className="pb-2 space-y-4">
|
<div className="pb-2 space-y-4">
|
||||||
<SectionHeader
|
<SectionHeader
|
||||||
title="Video"
|
title="Video"
|
||||||
|
@ -564,9 +564,9 @@ export default function SettingsSidebar() {
|
||||||
label=""
|
label=""
|
||||||
value={streamQuality}
|
value={streamQuality}
|
||||||
options={[
|
options={[
|
||||||
{value: "1", label: "High"},
|
{ value: "1", label: "High" },
|
||||||
{value: "0.5", label: "Medium"},
|
{ value: "0.5", label: "Medium" },
|
||||||
{value: "0.1", label: "Low"},
|
{ value: "0.1", label: "Low" },
|
||||||
]}
|
]}
|
||||||
onChange={e => handleStreamQualityChange(e.target.value)}
|
onChange={e => handleStreamQualityChange(e.target.value)}
|
||||||
/>
|
/>
|
||||||
|
@ -588,7 +588,7 @@ export default function SettingsSidebar() {
|
||||||
handleEDIDChange(e.target.value as string);
|
handleEDIDChange(e.target.value as string);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
options={[...edids, {value: "custom", label: "Custom"}]}
|
options={[...edids, { value: "custom", label: "Custom" }]}
|
||||||
/>
|
/>
|
||||||
</SettingsItem>
|
</SettingsItem>
|
||||||
{customEdidValue !== null && (
|
{customEdidValue !== null && (
|
||||||
|
@ -627,7 +627,7 @@ export default function SettingsSidebar() {
|
||||||
</div>
|
</div>
|
||||||
{isOnDevice && (
|
{isOnDevice && (
|
||||||
<>
|
<>
|
||||||
<div className="h-[1px] w-full bg-slate-800/10 dark:bg-slate-300/20"/>
|
<div className="h-[1px] w-full bg-slate-800/10 dark:bg-slate-300/20" />
|
||||||
<div className="pb-4 space-y-4">
|
<div className="pb-4 space-y-4">
|
||||||
<SectionHeader
|
<SectionHeader
|
||||||
title="JetKVM Cloud"
|
title="JetKVM Cloud"
|
||||||
|
@ -636,8 +636,7 @@ export default function SettingsSidebar() {
|
||||||
|
|
||||||
<GridCard>
|
<GridCard>
|
||||||
<div className="flex items-start p-4 gap-x-4">
|
<div className="flex items-start p-4 gap-x-4">
|
||||||
<ShieldCheckIcon
|
<ShieldCheckIcon className="w-8 h-8 mt-1 text-blue-600 shrink-0 dark:text-blue-500" />
|
||||||
className="w-8 h-8 mt-1 text-blue-600 shrink-0 dark:text-blue-500"/>
|
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<h3 className="text-base font-bold text-slate-900 dark:text-white">
|
<h3 className="text-base font-bold text-slate-900 dark:text-white">
|
||||||
|
@ -665,7 +664,7 @@ export default function SettingsSidebar() {
|
||||||
.
|
.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<hr className="block w-full dark:border-slate-600"/>
|
<hr className="block w-full dark:border-slate-600" />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<LinkButton
|
<LinkButton
|
||||||
|
@ -726,7 +725,7 @@ export default function SettingsSidebar() {
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<div className="h-[1px] w-full bg-slate-800/10 dark:bg-slate-300/20"/>
|
<div className="h-[1px] w-full bg-slate-800/10 dark:bg-slate-300/20" />
|
||||||
{isOnDevice ? (
|
{isOnDevice ? (
|
||||||
<>
|
<>
|
||||||
<div className="pb-2 space-y-4">
|
<div className="pb-2 space-y-4">
|
||||||
|
@ -781,7 +780,7 @@ export default function SettingsSidebar() {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-[1px] w-full bg-slate-800/10 dark:bg-slate-300/20"/>
|
<div className="h-[1px] w-full bg-slate-800/10 dark:bg-slate-300/20" />
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
<div className="pb-2 space-y-4">
|
<div className="pb-2 space-y-4">
|
||||||
|
@ -815,7 +814,7 @@ export default function SettingsSidebar() {
|
||||||
</SettingsItem>
|
</SettingsItem>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-[1px] w-full bg-slate-800/10 dark:bg-slate-300/20"/>
|
<div className="h-[1px] w-full bg-slate-800/10 dark:bg-slate-300/20" />
|
||||||
|
|
||||||
<SectionHeader
|
<SectionHeader
|
||||||
title="Appearance"
|
title="Appearance"
|
||||||
|
@ -827,9 +826,9 @@ export default function SettingsSidebar() {
|
||||||
label=""
|
label=""
|
||||||
value={currentTheme}
|
value={currentTheme}
|
||||||
options={[
|
options={[
|
||||||
{value: "system", label: "System"},
|
{ value: "system", label: "System" },
|
||||||
{value: "light", label: "Light"},
|
{ value: "light", label: "Light" },
|
||||||
{value: "dark", label: "Dark"},
|
{ value: "dark", label: "Dark" },
|
||||||
]}
|
]}
|
||||||
onChange={e => {
|
onChange={e => {
|
||||||
setCurrentTheme(e.target.value);
|
setCurrentTheme(e.target.value);
|
||||||
|
@ -837,7 +836,7 @@ export default function SettingsSidebar() {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</SettingsItem>
|
</SettingsItem>
|
||||||
<div className="h-[1px] w-full bg-slate-800/10 dark:bg-slate-300/20"/>
|
<div className="h-[1px] w-full bg-slate-800/10 dark:bg-slate-300/20" />
|
||||||
<div className="pb-2 space-y-4">
|
<div className="pb-2 space-y-4">
|
||||||
<SectionHeader
|
<SectionHeader
|
||||||
title="Advanced"
|
title="Advanced"
|
||||||
|
@ -915,9 +914,7 @@ export default function SettingsSidebar() {
|
||||||
theme="primary"
|
theme="primary"
|
||||||
text="Update USB Config"
|
text="Update USB Config"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (Object.values(usbConfig).every(function (i) {
|
if (Object.values(usbConfig).every(function(i) { return Boolean(i); })) {
|
||||||
return Boolean(i);
|
|
||||||
})) {
|
|
||||||
handleUsbConfigChange(usbConfig);
|
handleUsbConfigChange(usbConfig);
|
||||||
notifications.success("Successfully updated USB Config")
|
notifications.success("Successfully updated USB Config")
|
||||||
} else {
|
} else {
|
||||||
|
|
Loading…
Reference in New Issue