mirror of https://github.com/jetkvm/kvm.git
Merge afa3cf2484 into d24ce1c76f
This commit is contained in:
commit
44286173f6
|
|
@ -0,0 +1,114 @@
|
|||
#!/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 ${CHECKOUT_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
|
||||
|
||||
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-xtermjs": "^1.0.10",
|
||||
"recharts": "^3.3.0",
|
||||
"semver": "^7.7.3",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tslog": "^4.10.2",
|
||||
"usehooks-ts": "^3.1.1",
|
||||
|
|
@ -6823,7 +6824,6 @@
|
|||
"version": "7.7.3",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
||||
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@
|
|||
"react-use-websocket": "^4.13.0",
|
||||
"react-xtermjs": "^1.0.10",
|
||||
"recharts": "^3.3.0",
|
||||
"semver": "^7.7.3",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tslog": "^4.10.2",
|
||||
"usehooks-ts": "^3.1.1",
|
||||
|
|
|
|||
|
|
@ -211,8 +211,8 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonPropsType>(
|
|||
|
||||
Button.displayName = "Button";
|
||||
|
||||
type LinkPropsType = Pick<LinkProps, "to"> &
|
||||
React.ComponentProps<typeof ButtonContent> & { disabled?: boolean, reloadDocument?: boolean };
|
||||
type LinkPropsType = Pick<LinkProps, "to" | "target" | "reloadDocument"> &
|
||||
React.ComponentProps<typeof ButtonContent> & { disabled?: boolean };
|
||||
export const LinkButton = ({ to, ...props }: LinkPropsType) => {
|
||||
const classes = cx(
|
||||
"group outline-hidden",
|
||||
|
|
@ -224,7 +224,7 @@ export const LinkButton = ({ to, ...props }: LinkPropsType) => {
|
|||
|
||||
if (to.toString().startsWith("http")) {
|
||||
return (
|
||||
<ExtLink href={to.toString()} className={classes}>
|
||||
<ExtLink href={to.toString()} className={classes} target={props.target}>
|
||||
<ButtonContent {...props} />
|
||||
</ExtLink>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ import { Link } from "react-router";
|
|||
import { MdConnectWithoutContact } from "react-icons/md";
|
||||
import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react";
|
||||
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";
|
||||
|
|
@ -45,12 +47,34 @@ 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.5.0";
|
||||
|
||||
// Use device version if valid and >= 0.5.0, otherwise fall back to backwards-compatible version
|
||||
let version = BACKWARDS_COMPATIBLE_VERSION;
|
||||
if (appVersion && semver.valid(appVersion) && semver.gte(appVersion, BACKWARDS_COMPATIBLE_VERSION)) {
|
||||
version = appVersion;
|
||||
}
|
||||
|
||||
return new URL(`/v/${version}/devices/${id}`, window.location.origin).toString();
|
||||
}, [appVersion, id]);
|
||||
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<div className="px-5 py-5 space-y-3">
|
||||
|
|
@ -89,7 +113,9 @@ export default function KvmCard({
|
|||
text={m.connect_to_kvm()}
|
||||
LeadingIcon={MdConnectWithoutContact}
|
||||
textAlign="center"
|
||||
to={`/devices/${id}`}
|
||||
reloadDocument
|
||||
target="_self"
|
||||
to={kvmUrl}
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
|
|
|
|||
|
|
@ -370,7 +370,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", () => {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,13 @@ import { CLOUD_API } from "@/ui.config";
|
|||
import { m } from "@localizations/messages";
|
||||
|
||||
interface LoaderData {
|
||||
devices: { id: string; name: string; online: boolean; lastSeen: string }[];
|
||||
devices: {
|
||||
id: string;
|
||||
name: string;
|
||||
online: boolean;
|
||||
lastSeen: string;
|
||||
version: string;
|
||||
}[];
|
||||
user: User;
|
||||
}
|
||||
const loader: LoaderFunction = async () => {
|
||||
|
|
@ -88,6 +94,7 @@ export default function DevicesRoute() {
|
|||
title={x.name ?? x.id}
|
||||
lastSeen={x.lastSeen ? new Date(x.lastSeen) : null}
|
||||
online={x.online}
|
||||
appVersion={x.version}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
|
|
|||
Loading…
Reference in New Issue