diff --git a/scripts/deploy_cloud_app.sh b/scripts/deploy_cloud_app.sh new file mode 100755 index 00000000..129dc667 --- /dev/null +++ b/scripts/deploy_cloud_app.sh @@ -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 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" diff --git a/ui/package-lock.json b/ui/package-lock.json index bd1d54ef..c4e1c96c 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -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" diff --git a/ui/package.json b/ui/package.json index 70eaa662..fc4ad519 100644 --- a/ui/package.json +++ b/ui/package.json @@ -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", diff --git a/ui/src/components/Button.tsx b/ui/src/components/Button.tsx index 6ae358f2..04f536f3 100644 --- a/ui/src/components/Button.tsx +++ b/ui/src/components/Button.tsx @@ -211,8 +211,8 @@ export const Button = React.forwardRef( Button.displayName = "Button"; -type LinkPropsType = Pick & - React.ComponentProps & { disabled?: boolean, reloadDocument?: boolean }; +type LinkPropsType = Pick & + React.ComponentProps & { 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 ( - + ); diff --git a/ui/src/components/KvmCard.tsx b/ui/src/components/KvmCard.tsx index c44fea38..8d79d0e1 100644 --- a/ui/src/components/KvmCard.tsx +++ b/ui/src/components/KvmCard.tsx @@ -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 (
@@ -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} /> ) : (