backport: versioning cloud app (0.3.8)

This commit is contained in:
Siyuan Miao 2025-11-24 13:03:34 +01:00
parent 43bf322c75
commit 55ff16c4a2
6 changed files with 44 additions and 11 deletions

9
ui/package-lock.json generated
View File

@ -32,7 +32,7 @@
"react-simple-keyboard": "^3.7.112", "react-simple-keyboard": "^3.7.112",
"react-xtermjs": "^1.0.9", "react-xtermjs": "^1.0.9",
"recharts": "^2.15.1", "recharts": "^2.15.1",
"semver": "^7.7.1", "semver": "^7.7.3",
"tailwind-merge": "^2.5.5", "tailwind-merge": "^2.5.5",
"usehooks-ts": "^3.1.1", "usehooks-ts": "^3.1.1",
"validator": "^13.12.0", "validator": "^13.12.0",
@ -5336,9 +5336,10 @@
} }
}, },
"node_modules/semver": { "node_modules/semver": {
"version": "7.7.1", "version": "7.7.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
"integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"license": "ISC",
"bin": { "bin": {
"semver": "bin/semver.js" "semver": "bin/semver.js"
}, },

View File

@ -41,7 +41,7 @@
"react-simple-keyboard": "^3.7.112", "react-simple-keyboard": "^3.7.112",
"react-xtermjs": "^1.0.9", "react-xtermjs": "^1.0.9",
"recharts": "^2.15.1", "recharts": "^2.15.1",
"semver": "^7.7.1", "semver": "^7.7.3",
"tailwind-merge": "^2.5.5", "tailwind-merge": "^2.5.5",
"usehooks-ts": "^3.1.1", "usehooks-ts": "^3.1.1",
"validator": "^13.12.0", "validator": "^13.12.0",

View File

@ -210,7 +210,7 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonPropsType>(
Button.displayName = "Button"; Button.displayName = "Button";
type LinkPropsType = Pick<LinkProps, "to"> & type LinkPropsType = Pick<LinkProps, "to" | "target" | "reloadDocument"> &
React.ComponentProps<typeof ButtonContent> & { disabled?: boolean }; React.ComponentProps<typeof ButtonContent> & { disabled?: boolean };
export const LinkButton = ({ to, ...props }: LinkPropsType) => { export const LinkButton = ({ to, ...props }: LinkPropsType) => {
const classes = cx( const classes = cx(
@ -223,13 +223,13 @@ export const LinkButton = ({ to, ...props }: LinkPropsType) => {
if (to.toString().startsWith("http")) { if (to.toString().startsWith("http")) {
return ( return (
<ExtLink href={to.toString()} className={classes}> <ExtLink href={to.toString()} className={classes} target={props.target}>
<ButtonContent {...props} /> <ButtonContent {...props} />
</ExtLink> </ExtLink>
); );
} else { } else {
return ( return (
<Link to={to} className={classes}> <Link to={to} className={classes} target={props.target} reloadDocument={props.reloadDocument}>
<ButtonContent {...props} /> <ButtonContent {...props} />
</Link> </Link>
); );

View File

@ -4,6 +4,8 @@ import { MdConnectWithoutContact } from "react-icons/md";
import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react"; import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { LuEllipsisVertical } from "react-icons/lu"; import { LuEllipsisVertical } from "react-icons/lu";
import semver from "semver";
import { useMemo } from "react";
function getRelativeTimeString(date: Date | number, lang = navigator.language): string { function getRelativeTimeString(date: Date | number, lang = navigator.language): string {
// Allow dates or times to be passed // Allow dates or times to be passed
@ -43,12 +45,33 @@ export default function KvmCard({
id, id,
online, online,
lastSeen, lastSeen,
appVersion,
}: { }: {
title: string; title: string;
id: string; id: string;
online: boolean; online: boolean;
lastSeen: Date | null; 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 ( return (
<Card> <Card>
<div className="px-5 py-5 space-y-3"> <div className="px-5 py-5 space-y-3">
@ -87,7 +110,9 @@ export default function KvmCard({
text="Connect to KVM" text="Connect to KVM"
LeadingIcon={MdConnectWithoutContact} LeadingIcon={MdConnectWithoutContact}
textAlign="center" textAlign="center"
to={`/devices/${id}`} reloadDocument
target="_self"
to={kvmUrl}
/> />
) : ( ) : (
<Button <Button

View File

@ -302,7 +302,8 @@ if (isOnDevice) {
}, },
], ],
}, },
]); // We only need to do this for the cloud deployment, because the device doesn't need to be versioned
], { basename: import.meta.env.BASE_URL });
} }
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {

View File

@ -12,7 +12,13 @@ import { ArrowRightIcon } from "@heroicons/react/16/solid";
import { CLOUD_API } from "@/ui.config"; import { CLOUD_API } from "@/ui.config";
interface LoaderData { interface LoaderData {
devices: { id: string; name: string; online: boolean; lastSeen: string }[]; devices: {
id: string;
name: string;
online: boolean;
lastSeen: string;
version: string;
}[];
user: User; user: User;
} }