diff --git a/ui/.prettierrc b/ui/.prettierrc index 65b362d..43fe6c4 100644 --- a/ui/.prettierrc +++ b/ui/.prettierrc @@ -6,6 +6,7 @@ "arrowParens": "avoid", "singleQuote": false, "plugins": ["prettier-plugin-tailwindcss"], - "tailwindFunctions": ["clsx"], - "printWidth": 90 + "tailwindFunctions": ["clsx", "cx"], + "printWidth": 90, + "tailwindStylesheet": "./src/index.css" } diff --git a/ui/src/components/AutoHeight.tsx b/ui/src/components/AutoHeight.tsx index fbcf9cf..75fda8a 100644 --- a/ui/src/components/AutoHeight.tsx +++ b/ui/src/components/AutoHeight.tsx @@ -22,7 +22,7 @@ const AutoHeight = ({ children, ...props }: { children: React.ReactNode }) => { {...props} height={height} duration={300} - contentClassName="h-fit" + contentClassName="h-fit p-px" contentRef={contentDiv} disableDisplayNone > diff --git a/ui/src/components/Checkbox.tsx b/ui/src/components/Checkbox.tsx index cdf29c5..cf9855d 100644 --- a/ui/src/components/Checkbox.tsx +++ b/ui/src/components/Checkbox.tsx @@ -12,7 +12,7 @@ const sizes = { const checkboxVariants = cva({ base: cx( - "block rounded", + "form-checkbox block rounded", // Colors "border-slate-300 dark:border-slate-600 bg-slate-50 dark:bg-slate-800 checked:accent-blue-700 checked:dark:accent-blue-500 transition-colors", diff --git a/ui/src/components/DhcpLeaseCard.tsx b/ui/src/components/DhcpLeaseCard.tsx new file mode 100644 index 0000000..8a6e59c --- /dev/null +++ b/ui/src/components/DhcpLeaseCard.tsx @@ -0,0 +1,212 @@ +import { LuRefreshCcw } from "react-icons/lu"; + +import { Button } from "@/components/Button"; +import { GridCard } from "@/components/Card"; +import { LifeTimeLabel } from "@/routes/devices.$id.settings.network"; +import { NetworkState } from "@/hooks/stores"; + +export default function DhcpLeaseCard({ + networkState, + setShowRenewLeaseConfirm, +}: { + networkState: NetworkState; + setShowRenewLeaseConfirm: (show: boolean) => void; +}) { + return ( + +
+
+

+ DHCP Lease Information +

+ +
+
+ {networkState?.dhcp_lease?.ip && ( +
+ + IP Address + + + {networkState?.dhcp_lease?.ip} + +
+ )} + + {networkState?.dhcp_lease?.netmask && ( +
+ + Subnet Mask + + + {networkState?.dhcp_lease?.netmask} + +
+ )} + + {networkState?.dhcp_lease?.dns && ( +
+ + DNS Servers + + + {networkState?.dhcp_lease?.dns.map(dns =>
{dns}
)} +
+
+ )} + + {networkState?.dhcp_lease?.broadcast && ( +
+ + Broadcast + + + {networkState?.dhcp_lease?.broadcast} + +
+ )} + + {networkState?.dhcp_lease?.domain && ( +
+ + Domain + + + {networkState?.dhcp_lease?.domain} + +
+ )} + + {networkState?.dhcp_lease?.ntp_servers && + networkState?.dhcp_lease?.ntp_servers.length > 0 && ( +
+
+ NTP Servers +
+
+ {networkState?.dhcp_lease?.ntp_servers.map(server => ( +
{server}
+ ))} +
+
+ )} + + {networkState?.dhcp_lease?.hostname && ( +
+ + Hostname + + + {networkState?.dhcp_lease?.hostname} + +
+ )} +
+ +
+ {networkState?.dhcp_lease?.routers && + networkState?.dhcp_lease?.routers.length > 0 && ( +
+ + Gateway + + + {networkState?.dhcp_lease?.routers.map(router => ( +
{router}
+ ))} +
+
+ )} + + {networkState?.dhcp_lease?.server_id && ( +
+ + DHCP Server + + + {networkState?.dhcp_lease?.server_id} + +
+ )} + + {networkState?.dhcp_lease?.lease_expiry && ( +
+ + Lease Expires + + + + +
+ )} + + {networkState?.dhcp_lease?.mtu && ( +
+ MTU + + {networkState?.dhcp_lease?.mtu} + +
+ )} + + {networkState?.dhcp_lease?.ttl && ( +
+ TTL + + {networkState?.dhcp_lease?.ttl} + +
+ )} + + {networkState?.dhcp_lease?.bootp_next_server && ( +
+ + Boot Next Server + + + {networkState?.dhcp_lease?.bootp_next_server} + +
+ )} + + {networkState?.dhcp_lease?.bootp_server_name && ( +
+ + Boot Server Name + + + {networkState?.dhcp_lease?.bootp_server_name} + +
+ )} + + {networkState?.dhcp_lease?.bootp_file && ( +
+ + Boot File + + + {networkState?.dhcp_lease?.bootp_file} + +
+ )} +
+
+ +
+
+
+
+
+ ); +} diff --git a/ui/src/components/Ipv6NetworkCard.tsx b/ui/src/components/Ipv6NetworkCard.tsx new file mode 100644 index 0000000..a31b78e --- /dev/null +++ b/ui/src/components/Ipv6NetworkCard.tsx @@ -0,0 +1,93 @@ +import { NetworkState } from "../hooks/stores"; +import { LifeTimeLabel } from "../routes/devices.$id.settings.network"; + +import { GridCard } from "./Card"; + +export default function Ipv6NetworkCard({ + networkState, +}: { + networkState: NetworkState; +}) { + return ( + +
+
+

+ IPv6 Information +

+ +
+ {networkState?.dhcp_lease?.ip && ( +
+ + Link-local + + + {networkState?.ipv6_link_local} + +
+ )} +
+ +
+ {networkState?.ipv6_addresses && networkState?.ipv6_addresses.length > 0 && ( +
+

IPv6 Addresses

+ {networkState.ipv6_addresses.map( + addr => ( +
+
+
+ + Address + + {addr.address} +
+ + {addr.valid_lifetime && ( +
+ + Valid Lifetime + + + {addr.valid_lifetime === "" ? ( + + N/A + + ) : ( + + )} + +
+ )} + {addr.preferred_lifetime && ( +
+ + Preferred Lifetime + + + {addr.preferred_lifetime === "" ? ( + + N/A + + ) : ( + + )} + +
+ )} +
+
+ ), + )} +
+ )} +
+
+
+
+ ); +} diff --git a/ui/src/components/Modal.tsx b/ui/src/components/Modal.tsx index 1772011..d9433df 100644 --- a/ui/src/components/Modal.tsx +++ b/ui/src/components/Modal.tsx @@ -20,7 +20,9 @@ const Modal = React.memo(function Modal({ transition className="fixed inset-0 bg-gray-500/75 transition-opacity data-closed:opacity-0 data-enter:duration-500 data-leave:duration-200 data-enter:ease-out data-leave:ease-in dark:bg-slate-900/90" /> -
+
{/* TODO: This doesn't work well with other-sessions */}
) : ( - +
{/* Control Buttons */}
diff --git a/ui/src/components/extensions/DCPowerControl.tsx b/ui/src/components/extensions/DCPowerControl.tsx index e51b5bd..3fcb7dc 100644 --- a/ui/src/components/extensions/DCPowerControl.tsx +++ b/ui/src/components/extensions/DCPowerControl.tsx @@ -63,7 +63,7 @@ export function DCPowerControl() { ) : ( - +
{/* Power Controls */}
diff --git a/ui/src/components/extensions/SerialConsole.tsx b/ui/src/components/extensions/SerialConsole.tsx index 1c8a16c..544d3fd 100644 --- a/ui/src/components/extensions/SerialConsole.tsx +++ b/ui/src/components/extensions/SerialConsole.tsx @@ -58,7 +58,7 @@ export function SerialConsole() { description="Configure your serial console settings" /> - +
{/* Open Console Button */}
diff --git a/ui/src/components/popovers/ExtensionPopover.tsx b/ui/src/components/popovers/ExtensionPopover.tsx index 2dc0204..10ee2ea 100644 --- a/ui/src/components/popovers/ExtensionPopover.tsx +++ b/ui/src/components/popovers/ExtensionPopover.tsx @@ -92,7 +92,7 @@ export default function ExtensionPopover() { {renderActiveExtension()}
- +
{AVAILABLE_EXTENSIONS.map(extension => (
((_props, ref) => { ) : null}
((_props, ref) => { {!remoteVirtualMediaState && (
- +
{storedDevices.map((device, index) => (
@@ -63,7 +63,7 @@ export default function DeviceList({
- +
@@ -35,7 +35,7 @@ export default function EmptyStateCard({
`...)ms` */ +/* If we don't ignore this, Prettier will add a space between the value and the `ms`. Rendering the utility invalid. */ +/* prettier-ignore */ @utility animation-delay-* { - animation-delay: --value(integer) ms; + animation-delay: --value(integer)ms; +} + +/* If we don't ignore this, Prettier will add a space between the value and the `ms`. Rendering the utility invalid. */ +/* prettier-ignore */ +@utility animation-duration-* { + animation-duration: --value(integer)ms; } html { diff --git a/ui/src/routes/devices.$id.mount.tsx b/ui/src/routes/devices.$id.mount.tsx index 475b57b..7ac519c 100644 --- a/ui/src/routes/devices.$id.mount.tsx +++ b/ui/src/routes/devices.$id.mount.tsx @@ -7,7 +7,7 @@ import { LuCheck, LuUpload, } from "react-icons/lu"; -import { PlusCircleIcon , ExclamationTriangleIcon } from "@heroicons/react/20/solid"; +import { PlusCircleIcon, ExclamationTriangleIcon } from "@heroicons/react/20/solid"; import { TrashIcon } from "@heroicons/react/16/solid"; import { useNavigate } from "react-router-dom"; @@ -38,7 +38,6 @@ import { useRTCStore, } from "../hooks/stores"; - export default function MountRoute() { const navigate = useNavigate(); { @@ -283,8 +282,8 @@ function ModeSelectionView({ return (
-
-

+
+

Virtual Media Source

@@ -320,7 +319,7 @@ function ModeSelectionView({ ].map(({ label, description, value: mode, icon: Icon, tag, disabled }, index) => (
disabled ? null : setSelectedMode(mode as "browser" | "url" | "device") } @@ -365,7 +364,7 @@ function ModeSelectionView({ value={mode} disabled={disabled} checked={selectedMode === mode} - className="absolute right-4 top-4 h-4 w-4 text-blue-700" + className="absolute top-4 right-4 form-radio h-4 w-4 rounded-full text-blue-700" />
@@ -373,7 +372,7 @@ function ModeSelectionView({ ))}
- +
{selectedFile ? ( <>
-

+

{formatters.truncateMiddle(selectedFile.name, 40)}

@@ -460,7 +459,7 @@ function BrowserFileView({ ) : (

-

+

Click to select a file

@@ -483,7 +482,7 @@ function BrowserFileView({

Popular images

- + {popularImages.map((image, index) => (
{`${image.name}
-

+

{formatters.truncateMiddle(image.name, 40)}

{image.description && ( @@ -797,7 +796,7 @@ function DeviceFileView({ description="Select an image to mount from the JetKVM storage" />
-

+

No images available

@@ -827,7 +826,7 @@ function DeviceFileView({

) : ( -
+
{currentFiles.map((file, index) => ( 0 ? (
) : (
0 && (
@@ -1282,7 +1281,7 @@ function UploadFileView({
-

+

{incompleteFileName ? `Click to select "${incompleteFileName.replace(".incomplete", "")}"` : "Click to select a file"} @@ -1336,7 +1335,7 @@ function UploadFileView({

-

+

Upload successful

@@ -1365,7 +1364,7 @@ function UploadFileView({ {/* Display upload error if present */} {uploadError && (

Error: {uploadError} @@ -1373,7 +1372,7 @@ function UploadFileView({ )}
-

Mount Error

+

Mount Error

An error occurred while attempting to mount the media. Please try again. @@ -1481,8 +1480,8 @@ function PreUploadedImageItem({ }} >

-
-
+
+
{formatters.truncateMiddle(name, 45)}
@@ -1494,7 +1493,7 @@ function PreUploadedImageItem({
-
+
e.stopPropagation()} // Prevent double-firing of onSelect /> ) : ( @@ -1540,7 +1539,7 @@ function PreUploadedImageItem({ function ViewHeader({ title, description }: { title: string; description: string }) { return (
-

+

{title}

@@ -1558,7 +1557,7 @@ function UsbModeSelector({ setUsbMode: (mode: RemoteVirtualMediaState["mode"]) => void; }) { return ( -
+
diff --git a/ui/src/routes/devices.$id.settings.network.tsx b/ui/src/routes/devices.$id.settings.network.tsx index aebcd03..167cf0b 100644 --- a/ui/src/routes/devices.$id.settings.network.tsx +++ b/ui/src/routes/devices.$id.settings.network.tsx @@ -1,7 +1,7 @@ import { useCallback, useEffect, useRef, useState } from "react"; import dayjs from "dayjs"; import relativeTime from "dayjs/plugin/relativeTime"; -import { ArrowPathIcon } from "@heroicons/react/24/outline"; +import { LuEthernetPort } from "react-icons/lu"; import { IPv4Mode, @@ -16,13 +16,18 @@ import { import { useJsonRpc } from "@/hooks/useJsonRpc"; import { Button } from "@components/Button"; import { GridCard } from "@components/Card"; -import InputField from "@components/InputField"; +import InputField, { InputFieldWithLabel } from "@components/InputField"; import { SelectMenuBasic } from "@/components/SelectMenuBasic"; import { SettingsPageHeader } from "@/components/SettingsPageheader"; import Fieldset from "@/components/Fieldset"; import { ConfirmDialog } from "@/components/ConfirmDialog"; import notifications from "@/notifications"; +import Ipv6NetworkCard from "../components/Ipv6NetworkCard"; +import EmptyCard from "../components/EmptyCard"; +import AutoHeight from "../components/AutoHeight"; +import DhcpLeaseCard from "../components/DhcpLeaseCard"; + import { SettingsItem } from "./devices.$id.settings"; dayjs.extend(relativeTime); @@ -56,15 +61,11 @@ export function LifeTimeLabel({ lifetime }: { lifetime: string }) { return ( <> - {dayjs(lifetime).format("YYYY-MM-DD HH:mm")} - {remaining && ( - <> - {" "} - - ({remaining}) - - - )} + {remaining && <> {remaining}} + + {" "} + ({dayjs(lifetime).format("YYYY-MM-DD HH:mm")}) + ); } @@ -114,6 +115,14 @@ export default function SettingsNetworkRoute() { }); }, [send]); + const getNetworkState = useCallback(() => { + send("getNetworkState", {}, resp => { + if ("error" in resp) return; + console.log(resp.result); + setNetworkState(resp.result as NetworkState); + }); + }, [send, setNetworkState]); + const setNetworkSettingsRemote = useCallback( (settings: NetworkSettings) => { setNetworkSettingsLoaded(false); @@ -129,21 +138,14 @@ export default function SettingsNetworkRoute() { // We need to update the firstNetworkSettings ref to the new settings so we can use it to determine if the settings have changed firstNetworkSettings.current = resp.result as NetworkSettings; setNetworkSettings(resp.result as NetworkSettings); + getNetworkState(); setNetworkSettingsLoaded(true); notifications.success("Network settings saved"); }); }, - [send], + [getNetworkState, send], ); - const getNetworkState = useCallback(() => { - send("getNetworkState", {}, resp => { - if ("error" in resp) return; - console.log(resp.result); - setNetworkState(resp.result as NetworkState); - }); - }, [send, setNetworkState]); - const handleRenewLease = useCallback(() => { send("renewDHCPLease", {}, resp => { if ("error" in resp) { @@ -171,10 +173,6 @@ export default function SettingsNetworkRoute() { setNetworkSettings({ ...networkSettings, lldp_mode: value as LLDPMode }); }; - // const handleLldpTxTlvsChange = (value: string[]) => { - // setNetworkSettings({ ...networkSettings, lldp_tx_tlvs: value }); - // }; - const handleMdnsModeChange = (value: mDNSMode | string) => { setNetworkSettings({ ...networkSettings, mdns_mode: value as mDNSMode }); }; @@ -258,7 +256,7 @@ export default function SettingsNetworkRoute() {
-
+
{selectedDomainOption === "custom" && ( -
- + setCustomDomain(e.target.value)} - /> -
)} @@ -359,209 +355,35 @@ export default function SettingsNetworkRoute() { ])} /> - {networkState?.dhcp_lease && ( - -
-
-

- DHCP Lease -

- -
-
- {networkState?.dhcp_lease?.ip && ( -
- - IP Address - - - {networkState?.dhcp_lease?.ip} - -
- )} - - {networkState?.dhcp_lease?.netmask && ( -
- - Subnet Mask - - - {networkState?.dhcp_lease?.netmask} - -
- )} - - {networkState?.dhcp_lease?.dns && ( -
- - DNS Servers - - - {networkState?.dhcp_lease?.dns.map(dns => ( -
{dns}
- ))} -
-
- )} - - {networkState?.dhcp_lease?.broadcast && ( -
- - Broadcast - - - {networkState?.dhcp_lease?.broadcast} - -
- )} - - {networkState?.dhcp_lease?.domain && ( -
- - Domain - - - {networkState?.dhcp_lease?.domain} - -
- )} - - {networkState?.dhcp_lease?.ntp_servers && - networkState?.dhcp_lease?.ntp_servers.length > 0 && ( -
-
- NTP Servers -
-
- {networkState?.dhcp_lease?.ntp_servers.map(server => ( -
{server}
- ))} -
-
- )} - - {networkState?.dhcp_lease?.hostname && ( -
- - Hostname - - - {networkState?.dhcp_lease?.hostname} - -
- )} + + {!networkSettingsLoaded ? ( + +
+
+

+ DHCP Lease Information +

+
+
+
+
- -
- {networkState?.dhcp_lease?.routers && - networkState?.dhcp_lease?.routers.length > 0 && ( -
- - Gateway - - - {networkState?.dhcp_lease?.routers.map(router => ( -
{router}
- ))} -
-
- )} - - {networkState?.dhcp_lease?.server_id && ( -
- - DHCP Server - - - {networkState?.dhcp_lease?.server_id} - -
- )} - - {networkState?.dhcp_lease?.lease_expiry && ( -
- - Lease Expires - - - - -
- )} - - {networkState?.dhcp_lease?.mtu && ( -
- - MTU - - - {networkState?.dhcp_lease?.mtu} - -
- )} - - {networkState?.dhcp_lease?.ttl && ( -
- - TTL - - - {networkState?.dhcp_lease?.ttl} - -
- )} - - {networkState?.dhcp_lease?.bootp_next_server && ( -
- - Boot Next Server - - - {networkState?.dhcp_lease?.bootp_next_server} - -
- )} - - {networkState?.dhcp_lease?.bootp_server_name && ( -
- - Boot Server Name - - - {networkState?.dhcp_lease?.bootp_server_name} - -
- )} - - {networkState?.dhcp_lease?.bootp_file && ( -
- - Boot File - - - {networkState?.dhcp_lease?.bootp_file} - -
- )} -
-
- -
-
-
- - )} + + ) : networkState?.dhcp_lease && networkState.dhcp_lease.ip ? ( + + ) : ( + + )} +
@@ -579,93 +401,32 @@ export default function SettingsNetworkRoute() { ])} /> - {networkState?.ipv6_addresses && ( - -
-
-

- IPv6 Information -

- -
- {networkState?.dhcp_lease?.ip && ( -
- - Link-local - - - {networkState?.ipv6_link_local} - -
- )} -
- -
- {networkState?.ipv6_addresses && - networkState?.ipv6_addresses.length > 0 && ( -
-

IPv6 Addresses

- {networkState.ipv6_addresses.map(addr => ( -
-
-
- - Address - - - {addr.address} - -
- - {addr.valid_lifetime && ( -
- - Valid Lifetime - - - {addr.valid_lifetime === "" ? ( - - N/A - - ) : ( - - )} - -
- )} - {addr.preferred_lifetime && ( -
- - Preferred Lifetime - - - {addr.preferred_lifetime === "" ? ( - - N/A - - ) : ( - - )} - -
- )} -
-
- ))} -
- )} + + {!networkSettingsLoaded ? ( + +
+
+

+ IPv6 Information +

+
+
+
+
+
-
- - )} + + ) : networkState?.ipv6_addresses && networkState.ipv6_addresses.length > 0 ? ( + + ) : ( + + )} +
{ setSelectedMode(mode as "password" | "noPassword"); }} - className="absolute top-2 right-2 h-4 w-4 text-blue-600" + className="form-radio absolute top-2 right-2 h-4 w-4 text-blue-600" />