mirror of https://github.com/jetkvm/kvm.git
feat: add versioning to cloud app (#965)
Co-authored-by: Siyuan Miao <i@xswan.net>
This commit is contained in:
parent
322a22303c
commit
8d801fdb3d
|
|
@ -0,0 +1,116 @@
|
||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
SCRIPT_PATH=$(realpath "$(dirname $(realpath "${BASH_SOURCE[0]}"))")
|
||||||
|
source ${SCRIPT_PATH}/build_utils.sh
|
||||||
|
|
||||||
|
function show_help() {
|
||||||
|
echo "Usage: $0 [options]"
|
||||||
|
echo "Options:"
|
||||||
|
echo " -b, --branch <branch> Checkout branch"
|
||||||
|
echo " --set-as-default Set as default"
|
||||||
|
echo " --skip-confirmation Skip confirmation"
|
||||||
|
echo " --help Show help"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Parse command line arguments
|
||||||
|
CHECKOUT_BRANCH=
|
||||||
|
SET_AS_DEFAULT=false
|
||||||
|
SKIP_CONFIRMATION=false
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case $1 in
|
||||||
|
-b|--branch)
|
||||||
|
CHECKOUT_BRANCH="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--set-as-default)
|
||||||
|
SET_AS_DEFAULT=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--skip-confirmation)
|
||||||
|
SKIP_CONFIRMATION=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--help)
|
||||||
|
show_help
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown option: $1"
|
||||||
|
show_help
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
|
||||||
|
# Checkout current branch in a new temporary directory
|
||||||
|
# only popd when exiting the script
|
||||||
|
TMP_DIR=$(mktemp -d)
|
||||||
|
trap 'popd > /dev/null && rm -rf ${TMP_DIR}' EXIT
|
||||||
|
msg_info "Copying repository to a new temporary directory ${TMP_DIR} ..."
|
||||||
|
# git fetch origin ${CH}ECKOUT_BRANCH:${CHECKOUT_BRANCH}
|
||||||
|
git clone . ${TMP_DIR}
|
||||||
|
cp ${SCRIPT_PATH}/versioned.patch ${TMP_DIR}
|
||||||
|
msg_info "Checking out branch ${CHECKOUT_BRANCH} ..."
|
||||||
|
pushd ${TMP_DIR} > /dev/null
|
||||||
|
git checkout ${CHECKOUT_BRANCH}
|
||||||
|
|
||||||
|
|
||||||
|
# CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
||||||
|
# # Verify branch name matches release/x.x.x or release/x.x.x-dev...
|
||||||
|
# if [[ ! $CURRENT_BRANCH =~ ^(release|release-cloud-app)/[0-9]+\.[0-9]+\.[0-9]+(-dev[0-9]+)?$ ]]; then
|
||||||
|
# msg_err "Current branch '$CURRENT_BRANCH' does not match required pattern"
|
||||||
|
# msg_err "Expected: release/x.x.x OR release/x.x.x-dev20241104123632"
|
||||||
|
# exit 1
|
||||||
|
# fi
|
||||||
|
|
||||||
|
CURRENT_BRANCH=release/0.5.0
|
||||||
|
|
||||||
|
GIT_COMMIT=$(git rev-parse HEAD)
|
||||||
|
BUILD_TIMESTAMP=$(date -u +%FT%T%z)
|
||||||
|
VERSION=${CURRENT_BRANCH#release/}
|
||||||
|
VERSION=${VERSION#release-cloud-app/}
|
||||||
|
if [[ ! $VERSION =~ ^[0-9]+\.[0-9]+\.[0-9]+(-dev[0-9]+)?$ ]]; then
|
||||||
|
msg_err "Version '$VERSION' does not match required pattern"
|
||||||
|
msg_err "Expected: x.x.x OR x.x.x-dev20241104123632"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Change to ui directory
|
||||||
|
cd ui
|
||||||
|
|
||||||
|
if [ "$SET_AS_DEFAULT" = true ]; then
|
||||||
|
# Build for root dist
|
||||||
|
msg_info "Building for root dist..."
|
||||||
|
npm ci
|
||||||
|
npm run build:prod
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Build for versioned dist/v/VERSION
|
||||||
|
msg_info "Building for dist/v/${VERSION}..."
|
||||||
|
npm ci
|
||||||
|
npm run build:prod -- --base=/v/${VERSION}/ --outDir dist/v/${VERSION}
|
||||||
|
|
||||||
|
# Ask for confirmation
|
||||||
|
if [ "$SKIP_CONFIRMATION" = false ]; then
|
||||||
|
read -p "Do you want to deploy the cloud app to production? (y/N): " -n 1 -r
|
||||||
|
echo ""
|
||||||
|
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||||
|
msg_err "Deployment cancelled."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Deploy to production
|
||||||
|
msg_info "Deploying to r2://jetkvm-cloud-app..."
|
||||||
|
rclone copyto \
|
||||||
|
--progress \
|
||||||
|
--stats=1s \
|
||||||
|
--header-upload="x-amz-meta-jetkvm-version: ${VERSION}" \
|
||||||
|
--header-upload="x-amz-meta-jetkvm-build-ref: ${GIT_COMMIT}" \
|
||||||
|
--header-upload="x-amz-meta-jetkvm-build-timestamp: ${BUILD_TIMESTAMP}" \
|
||||||
|
dist \
|
||||||
|
r2://jetkvm-cloud-app
|
||||||
|
|
||||||
|
msg_ok "Successfully deployed v${VERSION} to production"
|
||||||
|
|
@ -36,6 +36,7 @@
|
||||||
"react-use-websocket": "^4.13.0",
|
"react-use-websocket": "^4.13.0",
|
||||||
"react-xtermjs": "^1.0.10",
|
"react-xtermjs": "^1.0.10",
|
||||||
"recharts": "^3.3.0",
|
"recharts": "^3.3.0",
|
||||||
|
"semver": "^7.7.3",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"tslog": "^4.10.2",
|
"tslog": "^4.10.2",
|
||||||
"usehooks-ts": "^3.1.1",
|
"usehooks-ts": "^3.1.1",
|
||||||
|
|
@ -6823,7 +6824,6 @@
|
||||||
"version": "7.7.3",
|
"version": "7.7.3",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
||||||
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
|
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"bin": {
|
"bin": {
|
||||||
"semver": "bin/semver.js"
|
"semver": "bin/semver.js"
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,7 @@
|
||||||
"react-use-websocket": "^4.13.0",
|
"react-use-websocket": "^4.13.0",
|
||||||
"react-xtermjs": "^1.0.10",
|
"react-xtermjs": "^1.0.10",
|
||||||
"recharts": "^3.3.0",
|
"recharts": "^3.3.0",
|
||||||
|
"semver": "^7.7.3",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"tslog": "^4.10.2",
|
"tslog": "^4.10.2",
|
||||||
"usehooks-ts": "^3.1.1",
|
"usehooks-ts": "^3.1.1",
|
||||||
|
|
|
||||||
|
|
@ -211,8 +211,8 @@ 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, reloadDocument?: 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(
|
||||||
"group outline-hidden",
|
"group outline-hidden",
|
||||||
|
|
@ -224,7 +224,7 @@ 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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,13 @@ import { Link } from "react-router";
|
||||||
import { MdConnectWithoutContact } from "react-icons/md";
|
import { MdConnectWithoutContact } from "react-icons/md";
|
||||||
import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react";
|
import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react";
|
||||||
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";
|
||||||
import { m } from "@localizations/messages.js";
|
import { m } from "@localizations/messages.js";
|
||||||
|
import { CLOUD_BACKWARDS_COMPATIBLE_VERSION, CLOUD_ENABLE_VERSIONED_UI } from "@/ui.config";
|
||||||
|
|
||||||
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
|
||||||
|
|
@ -45,12 +48,38 @@ 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.
|
||||||
|
*
|
||||||
|
* CLOUD_BACKWARDS_COMPATIBLE_VERSION is the last backwards-compatible UI that works with older devices.
|
||||||
|
* Devices on CLOUD_BACKWARDS_COMPATIBLE_VERSION or below are served that version, while newer devices get
|
||||||
|
* their actual version. Unparseable versions fall back to CLOUD_BACKWARDS_COMPATIBLE_VERSION for safety.
|
||||||
|
*/
|
||||||
|
const kvmUrl = useMemo(() => {
|
||||||
|
let uri = `/devices/${id}`;
|
||||||
|
|
||||||
|
// Only use versioned path if versioned UI is enabled
|
||||||
|
if (CLOUD_ENABLE_VERSIONED_UI) {
|
||||||
|
// Use device version if valid and >= 0.5.0, otherwise fall back to backwards-compatible version
|
||||||
|
let version = CLOUD_BACKWARDS_COMPATIBLE_VERSION;
|
||||||
|
if (appVersion && semver.valid(appVersion) && semver.gte(appVersion, CLOUD_BACKWARDS_COMPATIBLE_VERSION)) {
|
||||||
|
version = appVersion;
|
||||||
|
}
|
||||||
|
uri = `/v/${version}${uri}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new URL(uri, 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">
|
||||||
|
|
@ -89,7 +118,9 @@ export default function KvmCard({
|
||||||
text={m.connect_to_kvm()}
|
text={m.connect_to_kvm()}
|
||||||
LeadingIcon={MdConnectWithoutContact}
|
LeadingIcon={MdConnectWithoutContact}
|
||||||
textAlign="center"
|
textAlign="center"
|
||||||
to={`/devices/${id}`}
|
reloadDocument
|
||||||
|
target="_self"
|
||||||
|
to={kvmUrl}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
|
|
|
||||||
181
ui/src/main.tsx
181
ui/src/main.tsx
|
|
@ -4,13 +4,14 @@ import {
|
||||||
createBrowserRouter,
|
createBrowserRouter,
|
||||||
isRouteErrorResponse,
|
isRouteErrorResponse,
|
||||||
redirect,
|
redirect,
|
||||||
|
type RouteObject,
|
||||||
RouterProvider,
|
RouterProvider,
|
||||||
useRouteError,
|
useRouteError,
|
||||||
} from "react-router";
|
} from "react-router";
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
import { ExclamationTriangleIcon } from "@heroicons/react/16/solid";
|
import { ExclamationTriangleIcon } from "@heroicons/react/16/solid";
|
||||||
|
|
||||||
import { CLOUD_API, DEVICE_API } from "@/ui.config";
|
import { CLOUD_API, CLOUD_ENABLE_VERSIONED_UI, DEVICE_API } from "@/ui.config";
|
||||||
import api from "@/api";
|
import api from "@/api";
|
||||||
import Root from "@/root";
|
import Root from "@/root";
|
||||||
import { m } from "@localizations/messages.js";
|
import { m } from "@localizations/messages.js";
|
||||||
|
|
@ -89,36 +90,12 @@ export async function checkAuth() {
|
||||||
return isOnDevice ? checkDeviceAuth() : checkCloudAuth();
|
return isOnDevice ? checkDeviceAuth() : checkCloudAuth();
|
||||||
}
|
}
|
||||||
|
|
||||||
let router;
|
let router: ReturnType<typeof createBrowserRouter>;
|
||||||
if (isOnDevice) {
|
|
||||||
router = createBrowserRouter([
|
const getDeviceRoute = (r: Omit<RouteObject, "children" | "index">): RouteObject => ({
|
||||||
{
|
|
||||||
path: "/welcome/mode",
|
|
||||||
element: <WelcomeLocalModeRoute />,
|
|
||||||
action: WelcomeLocalModeRoute.action,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "/welcome/password",
|
|
||||||
element: <WelcomeLocalPasswordRoute />,
|
|
||||||
action: WelcomeLocalPasswordRoute.action,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "/welcome",
|
|
||||||
element: <WelcomeRoute />,
|
|
||||||
loader: WelcomeRoute.loader,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "/login-local",
|
|
||||||
element: <LoginLocalRoute />,
|
|
||||||
action: LoginLocalRoute.action,
|
|
||||||
loader: LoginLocalRoute.loader,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "/",
|
|
||||||
errorElement: <ErrorBoundary />,
|
|
||||||
element: <DeviceRoute />,
|
element: <DeviceRoute />,
|
||||||
HydrateFallback: () => <div className="p-4">{m.loading()}</div>,
|
|
||||||
loader: DeviceRoute.loader,
|
loader: DeviceRoute.loader,
|
||||||
|
...r,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: "other-session",
|
path: "other-session",
|
||||||
|
|
@ -143,6 +120,7 @@ if (isOnDevice) {
|
||||||
index: true,
|
index: true,
|
||||||
element: <SettingsGeneralIndexRoute />,
|
element: <SettingsGeneralIndexRoute />,
|
||||||
},
|
},
|
||||||
|
// was previously only present on device routes
|
||||||
{
|
{
|
||||||
path: "reboot",
|
path: "reboot",
|
||||||
element: <SettingsGeneralRebootRoute />,
|
element: <SettingsGeneralRebootRoute />,
|
||||||
|
|
@ -215,7 +193,36 @@ if (isOnDevice) {
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isOnDevice) {
|
||||||
|
router = createBrowserRouter([
|
||||||
|
{
|
||||||
|
path: "/welcome/mode",
|
||||||
|
element: <WelcomeLocalModeRoute />,
|
||||||
|
action: WelcomeLocalModeRoute.action,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/welcome/password",
|
||||||
|
element: <WelcomeLocalPasswordRoute />,
|
||||||
|
action: WelcomeLocalPasswordRoute.action,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/welcome",
|
||||||
|
element: <WelcomeRoute />,
|
||||||
|
loader: WelcomeRoute.loader,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/login-local",
|
||||||
|
element: <LoginLocalRoute />,
|
||||||
|
action: LoginLocalRoute.action,
|
||||||
|
loader: LoginLocalRoute.loader,
|
||||||
|
},
|
||||||
|
getDeviceRoute({
|
||||||
|
path: "/",
|
||||||
|
errorElement: <ErrorBoundary />,
|
||||||
|
HydrateFallback: () => <div className="p-4">{m.loading()}</div>,
|
||||||
|
}),
|
||||||
{
|
{
|
||||||
path: "/adopt",
|
path: "/adopt",
|
||||||
element: <AdoptRoute />,
|
element: <AdoptRoute />,
|
||||||
|
|
@ -224,7 +231,7 @@ if (isOnDevice) {
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
} else {
|
} else {
|
||||||
router = createBrowserRouter([
|
const routeObjects: RouteObject[] = [
|
||||||
{
|
{
|
||||||
errorElement: <ErrorBoundary />,
|
errorElement: <ErrorBoundary />,
|
||||||
children: [
|
children: [
|
||||||
|
|
@ -241,7 +248,6 @@ if (isOnDevice) {
|
||||||
return redirect(`/devices`);
|
return redirect(`/devices`);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
path: "devices/:id/setup",
|
path: "devices/:id/setup",
|
||||||
element: <SetupRoute />,
|
element: <SetupRoute />,
|
||||||
|
|
@ -252,103 +258,9 @@ if (isOnDevice) {
|
||||||
path: "devices/already-adopted",
|
path: "devices/already-adopted",
|
||||||
element: <DevicesAlreadyAdopted />,
|
element: <DevicesAlreadyAdopted />,
|
||||||
},
|
},
|
||||||
{
|
getDeviceRoute({
|
||||||
path: "devices/:id",
|
path: "devices/:id",
|
||||||
element: <DeviceRoute />,
|
}),
|
||||||
loader: DeviceRoute.loader,
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: "other-session",
|
|
||||||
element: <OtherSessionRoute />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "mount",
|
|
||||||
element: <MountRoute />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "settings",
|
|
||||||
element: <SettingsRoute />,
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
index: true,
|
|
||||||
loader: SettingsIndexRoute.loader,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "general",
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
index: true,
|
|
||||||
element: <SettingsGeneralIndexRoute />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "update",
|
|
||||||
element: <SettingsGeneralUpdateRoute />,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "mouse",
|
|
||||||
element: <SettingsMouseRoute />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "keyboard",
|
|
||||||
element: <SettingsKeyboardRoute />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "advanced",
|
|
||||||
element: <SettingsAdvancedRoute />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "hardware",
|
|
||||||
element: <SettingsHardwareRoute />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "network",
|
|
||||||
element: <SettingsNetworkRoute />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "access",
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
index: true,
|
|
||||||
element: <SettingsAccessIndexRoute />,
|
|
||||||
loader: SettingsAccessIndexRoute.loader,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "local-auth",
|
|
||||||
element: <SecurityAccessLocalAuthRoute />,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "video",
|
|
||||||
element: <SettingsVideoRoute />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "appearance",
|
|
||||||
element: <SettingsAppearanceRoute />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "macros",
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
index: true,
|
|
||||||
element: <SettingsMacrosRoute />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "add",
|
|
||||||
element: <SettingsMacrosAddRoute />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: ":macroId/edit",
|
|
||||||
element: <SettingsMacrosEditRoute />,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: "devices/:id/deregister",
|
path: "devices/:id/deregister",
|
||||||
element: <DevicesIdDeregister />,
|
element: <DevicesIdDeregister />,
|
||||||
|
|
@ -370,7 +282,20 @@ if (isOnDevice) {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]);
|
];
|
||||||
|
|
||||||
|
// if versioned UI is not enabled, we need to add a route that redirects to the non-versioned route
|
||||||
|
if (!CLOUD_ENABLE_VERSIONED_UI) {
|
||||||
|
routeObjects.unshift({
|
||||||
|
path: "v/:version/*",
|
||||||
|
element: <Root />,
|
||||||
|
loader: async ({ params }) => {
|
||||||
|
throw redirect(`/${params['*']}`);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
router = createBrowserRouter(routeObjects, { basename: import.meta.env.BASE_URL });
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,13 @@ import { CLOUD_API } from "@/ui.config";
|
||||||
import { m } from "@localizations/messages";
|
import { m } from "@localizations/messages";
|
||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
const loader: LoaderFunction = async () => {
|
const loader: LoaderFunction = async () => {
|
||||||
|
|
@ -88,6 +94,7 @@ export default function DevicesRoute() {
|
||||||
title={x.name ?? x.id}
|
title={x.name ?? x.id}
|
||||||
lastSeen={x.lastSeen ? new Date(x.lastSeen) : null}
|
lastSeen={x.lastSeen ? new Date(x.lastSeen) : null}
|
||||||
online={x.online}
|
online={x.online}
|
||||||
|
appVersion={x.version}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,13 @@
|
||||||
|
const toBoolean = (value: string | undefined) => {
|
||||||
|
if (!value) return false;
|
||||||
|
return ["1", "true", "yes", "y"].includes(value.toLowerCase().trim());
|
||||||
|
}
|
||||||
export const CLOUD_API = import.meta.env.VITE_CLOUD_API;
|
export const CLOUD_API = import.meta.env.VITE_CLOUD_API;
|
||||||
|
|
||||||
|
export const CLOUD_BACKWARDS_COMPATIBLE_VERSION = import.meta.env.VITE_CLOUD_BACKWARDS_COMPATIBLE_VERSION || "0.5.0";
|
||||||
|
|
||||||
|
export const CLOUD_ENABLE_VERSIONED_UI = toBoolean(import.meta.env.VITE_CLOUD_ENABLE_VERSIONED_UI);
|
||||||
|
|
||||||
export const DOWNGRADE_VERSION = import.meta.env.VITE_DOWNGRADE_VERSION || "0.4.8";
|
export const DOWNGRADE_VERSION = import.meta.env.VITE_DOWNGRADE_VERSION || "0.4.8";
|
||||||
|
|
||||||
// In device mode, an empty string uses the current hostname (the JetKVM device's IP) as the API endpoint
|
// In device mode, an empty string uses the current hostname (the JetKVM device's IP) as the API endpoint
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue