mirror of https://github.com/jetkvm/kvm.git
backport: versioning cloud app (0.3.9)
This commit is contained in:
parent
5452d7c721
commit
9ce77b333e
|
|
@ -33,6 +33,7 @@
|
||||||
"react-use-websocket": "^4.13.0",
|
"react-use-websocket": "^4.13.0",
|
||||||
"react-xtermjs": "^1.0.9",
|
"react-xtermjs": "^1.0.9",
|
||||||
"recharts": "^2.15.0",
|
"recharts": "^2.15.0",
|
||||||
|
"semver": "^7.7.3",
|
||||||
"tailwind-merge": "^2.5.5",
|
"tailwind-merge": "^2.5.5",
|
||||||
"usehooks-ts": "^3.1.0",
|
"usehooks-ts": "^3.1.0",
|
||||||
"validator": "^13.12.0",
|
"validator": "^13.12.0",
|
||||||
|
|
@ -5455,10 +5456,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==",
|
||||||
"dev": true,
|
"license": "ISC",
|
||||||
"bin": {
|
"bin": {
|
||||||
"semver": "bin/semver.js"
|
"semver": "bin/semver.js"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@
|
||||||
"react-use-websocket": "^4.13.0",
|
"react-use-websocket": "^4.13.0",
|
||||||
"react-xtermjs": "^1.0.9",
|
"react-xtermjs": "^1.0.9",
|
||||||
"recharts": "^2.15.0",
|
"recharts": "^2.15.0",
|
||||||
|
"semver": "^7.7.3",
|
||||||
"tailwind-merge": "^2.5.5",
|
"tailwind-merge": "^2.5.5",
|
||||||
"usehooks-ts": "^3.1.0",
|
"usehooks-ts": "^3.1.0",
|
||||||
"validator": "^13.12.0",
|
"validator": "^13.12.0",
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { FetcherWithComponents, Link, LinkProps, useNavigation } from "react-router-dom";
|
|
||||||
|
|
||||||
import ExtLink from "@/components/ExtLink";
|
import ExtLink from "@/components/ExtLink";
|
||||||
import LoadingSpinner from "@/components/LoadingSpinner";
|
import LoadingSpinner from "@/components/LoadingSpinner";
|
||||||
import { cva, cx } from "@/cva.config";
|
import { cva, cx } from "@/cva.config";
|
||||||
|
import { FetcherWithComponents, Link, LinkProps, useNavigation } from "react-router-dom";
|
||||||
|
|
||||||
const sizes = {
|
const sizes = {
|
||||||
XS: "h-[28px] px-2 text-xs",
|
XS: "h-[28px] px-2 text-xs",
|
||||||
|
|
@ -102,7 +101,7 @@ const iconVariants = cva({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
interface ButtonContentPropsType {
|
type ButtonContentPropsType = {
|
||||||
text?: string | React.ReactNode;
|
text?: string | React.ReactNode;
|
||||||
LeadingIcon?: React.FC<{ className: string | undefined }> | null;
|
LeadingIcon?: React.FC<{ className: string | undefined }> | null;
|
||||||
TrailingIcon?: React.FC<{ className: string | undefined }> | null;
|
TrailingIcon?: React.FC<{ className: string | undefined }> | null;
|
||||||
|
|
@ -112,7 +111,7 @@ interface ButtonContentPropsType {
|
||||||
size: keyof typeof sizes;
|
size: keyof typeof sizes;
|
||||||
theme: keyof typeof themes;
|
theme: keyof typeof themes;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
}
|
};
|
||||||
|
|
||||||
function ButtonContent(props: ButtonContentPropsType) {
|
function ButtonContent(props: ButtonContentPropsType) {
|
||||||
const { text, LeadingIcon, TrailingIcon, fullWidth, className, textAlign, loading } =
|
const { text, LeadingIcon, TrailingIcon, fullWidth, className, textAlign, loading } =
|
||||||
|
|
@ -211,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(
|
||||||
|
|
@ -224,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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,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";
|
||||||
|
|
||||||
import Card from "@components/Card";
|
import Card from "@components/Card";
|
||||||
import { Button, LinkButton } from "@components/Button";
|
import { Button, LinkButton } from "@components/Button";
|
||||||
|
|
@ -44,12 +46,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">
|
||||||
|
|
@ -88,7 +111,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
|
||||||
|
|
|
||||||
|
|
@ -304,7 +304,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", () => {
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,13 @@ import EmptyCard from "@components/EmptyCard";
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue