diff --git a/ui/package-lock.json b/ui/package-lock.json index ebce1488..d61c94bf 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -33,6 +33,7 @@ "react-use-websocket": "^4.13.0", "react-xtermjs": "^1.0.9", "recharts": "^2.15.0", + "semver": "^7.7.3", "tailwind-merge": "^2.5.5", "usehooks-ts": "^3.1.0", "validator": "^13.12.0", @@ -5455,10 +5456,10 @@ } }, "node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", - "dev": true, + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", "bin": { "semver": "bin/semver.js" }, diff --git a/ui/package.json b/ui/package.json index a248616a..e9f31d81 100644 --- a/ui/package.json +++ b/ui/package.json @@ -43,6 +43,7 @@ "react-use-websocket": "^4.13.0", "react-xtermjs": "^1.0.9", "recharts": "^2.15.0", + "semver": "^7.7.3", "tailwind-merge": "^2.5.5", "usehooks-ts": "^3.1.0", "validator": "^13.12.0", diff --git a/ui/src/components/Button.tsx b/ui/src/components/Button.tsx index 3b7ac957..57392b79 100644 --- a/ui/src/components/Button.tsx +++ b/ui/src/components/Button.tsx @@ -1,9 +1,8 @@ import React from "react"; -import { FetcherWithComponents, Link, LinkProps, useNavigation } from "react-router-dom"; - import ExtLink from "@/components/ExtLink"; import LoadingSpinner from "@/components/LoadingSpinner"; import { cva, cx } from "@/cva.config"; +import { FetcherWithComponents, Link, LinkProps, useNavigation } from "react-router-dom"; const sizes = { XS: "h-[28px] px-2 text-xs", @@ -102,7 +101,7 @@ const iconVariants = cva({ }, }); -interface ButtonContentPropsType { +type ButtonContentPropsType = { text?: string | React.ReactNode; LeadingIcon?: React.FC<{ className: string | undefined }> | null; TrailingIcon?: React.FC<{ className: string | undefined }> | null; @@ -112,7 +111,7 @@ interface ButtonContentPropsType { size: keyof typeof sizes; theme: keyof typeof themes; loading?: boolean; -} +}; function ButtonContent(props: ButtonContentPropsType) { const { text, LeadingIcon, TrailingIcon, fullWidth, className, textAlign, loading } = @@ -211,7 +210,7 @@ export const Button = React.forwardRef( Button.displayName = "Button"; -type LinkPropsType = Pick & +type LinkPropsType = Pick & React.ComponentProps & { disabled?: boolean }; export const LinkButton = ({ to, ...props }: LinkPropsType) => { const classes = cx( @@ -224,13 +223,13 @@ export const LinkButton = ({ to, ...props }: LinkPropsType) => { if (to.toString().startsWith("http")) { return ( - + ); } else { return ( - + ); diff --git a/ui/src/components/KvmCard.tsx b/ui/src/components/KvmCard.tsx index c680a374..f5ed2e7e 100644 --- a/ui/src/components/KvmCard.tsx +++ b/ui/src/components/KvmCard.tsx @@ -2,6 +2,8 @@ import { MdConnectWithoutContact } from "react-icons/md"; import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react"; import { Link } from "react-router-dom"; import { LuEllipsisVertical } from "react-icons/lu"; +import semver from "semver"; +import { useMemo } from "react"; import Card from "@components/Card"; import { Button, LinkButton } from "@components/Button"; @@ -44,12 +46,33 @@ export default function KvmCard({ id, online, lastSeen, + appVersion, }: { title: string; id: string; online: boolean; lastSeen: Date | null; + appVersion: string; }) { + /** + * Constructs the URL for connecting to this KVM device's interface. + * + * Version 0.4.91 is the last backwards-compatible UI that works with older devices. + * Devices on v0.4.91 or below are served that version, while newer devices get + * their actual version. Unparseable versions fall back to 0.4.91 for safety. + */ + const kvmUrl = useMemo(() => { + const BACKWARDS_COMPATIBLE_VERSION = "0.4.91"; + + // Use device version if valid and >= 0.4.91, otherwise fall back to backwards-compatible version + const shouldUseDeviceVersion = + semver.valid(appVersion) && semver.gte(appVersion, BACKWARDS_COMPATIBLE_VERSION); + const version = shouldUseDeviceVersion ? appVersion : BACKWARDS_COMPATIBLE_VERSION; + + return new URL(`/v/${version}/devices/${id}`, window.location.origin).toString(); + }, [appVersion, id]); + + return (
@@ -88,7 +111,9 @@ export default function KvmCard({ text="Connect to KVM" LeadingIcon={MdConnectWithoutContact} textAlign="center" - to={`/devices/${id}`} + reloadDocument + target="_self" + to={kvmUrl} /> ) : (