Update npm packages for the UI

Upgraded most packages to current as of 2025-05-09 for almost everything.
Remove the erroneous extra dependency to old xterm package since the correct @xterm/xterm package was already included (suspect a bad merge) and it was causing issues with react 19.1.
Switched to using the hooks exposed in the usehooks-ts package (this package was already referenced, suspect a bad merge) removing our private copies of useInterval, useIsMounted, useResizeObserver which are identical.
Added import of JSX from react now needed because NPX is not in global scope in react 19.x.
Explicitly cast the ref of included elements due to change in react 19.x
This commit is contained in:
Marc Brooks 2025-05-09 16:22:53 -05:00
parent d79f359c43
commit cedad5f516
No known key found for this signature in database
GPG Key ID: 583A6AF2D6AE1DC6
14 changed files with 1631 additions and 1273 deletions

2613
ui/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -19,8 +19,8 @@
"preview": "vite preview"
},
"dependencies": {
"@headlessui/react": "^2.2.0",
"@headlessui/tailwindcss": "^0.2.1",
"@headlessui/react": "^2.2.2",
"@headlessui/tailwindcss": "^0.2.2",
"@heroicons/react": "^2.2.0",
"@vitejs/plugin-basic-ssl": "^1.2.0",
"@xterm/addon-clipboard": "^0.1.0",
@ -36,44 +36,43 @@
"framer-motion": "^11.15.0",
"lodash.throttle": "^4.1.1",
"mini-svg-data-uri": "^1.4.4",
"react": "^18.2.0",
"react": "^19.1.0",
"react-animate-height": "^3.2.3",
"react-dom": "^18.2.0",
"react-hot-toast": "^2.4.1",
"react-icons": "^5.4.0",
"react-dom": "^19.1.0",
"react-hot-toast": "^2.5.2",
"react-icons": "^5.5.0",
"react-router-dom": "^6.22.3",
"react-simple-keyboard": "^3.7.112",
"react-simple-keyboard": "^3.8.71",
"react-use-websocket": "^4.13.0",
"react-xtermjs": "^1.0.9",
"recharts": "^2.15.0",
"react-xtermjs": "^1.0.10",
"recharts": "^2.15.3",
"tailwind-merge": "^2.5.5",
"usehooks-ts": "^3.1.0",
"validator": "^13.12.0",
"xterm": "^5.3.0",
"usehooks-ts": "^3.1.1",
"validator": "^13.15.0",
"zustand": "^4.5.2"
},
"devDependencies": {
"@tailwindcss/forms": "^0.5.9",
"@tailwindcss/typography": "^0.5.15",
"@types/react": "^18.2.66",
"@types/react-dom": "^18.3.0",
"@types/semver": "^7.5.8",
"@types/validator": "^13.12.2",
"@typescript-eslint/eslint-plugin": "^8.25.0",
"@typescript-eslint/parser": "^8.25.0",
"@vitejs/plugin-react-swc": "^3.7.2",
"autoprefixer": "^10.4.20",
"eslint": "^8.20.0",
"eslint-config-prettier": "^10.0.1",
"@tailwindcss/forms": "^0.5.10",
"@tailwindcss/typography": "^0.5.16",
"@types/react": "^19.1.3",
"@types/react-dom": "^19.1.3",
"@types/semver": "^7.7.0",
"@types/validator": "^13.15.0",
"@typescript-eslint/eslint-plugin": "^8.32.0",
"@typescript-eslint/parser": "^8.32.0",
"@vitejs/plugin-react-swc": "^3.9.0",
"autoprefixer": "^10.4.21",
"eslint": "^9.26.0",
"eslint-config-prettier": "^10.1.5",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-react": "^7.37.4",
"eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-react-refresh": "^0.4.19",
"postcss": "^8.4.49",
"prettier": "^3.4.2",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"postcss": "^8.5.3",
"prettier": "^3.5.3",
"prettier-plugin-tailwindcss": "^0.6.11",
"tailwindcss": "^3.4.17",
"typescript": "^5.7.2",
"typescript": "^5.8.3",
"vite": "^5.2.0",
"vite-tsconfig-paths": "^5.1.4"
}

View File

@ -1,4 +1,4 @@
import React from "react";
import React, { JSX } from "react";
import { FetcherWithComponents, Link, LinkProps, useNavigation } from "react-router-dom";
import ExtLink from "@/components/ExtLink";

View File

@ -1,5 +1,5 @@
import type { Ref } from "react";
import React, { forwardRef } from "react";
import React, { forwardRef, JSX } from "react";
import clsx from "clsx";
import FieldLabel from "@/components/FieldLabel";

View File

@ -1,5 +1,5 @@
import type { Ref } from "react";
import React, { forwardRef } from "react";
import React, { forwardRef, JSX } from "react";
import clsx from "clsx";
import FieldLabel from "@/components/FieldLabel";

View File

@ -1,4 +1,4 @@
import React from "react";
import React, { JSX } from "react";
import clsx from "clsx";
import FieldLabel from "@/components/FieldLabel";

View File

@ -79,10 +79,11 @@ function Terminal({
return () => {
setDisableKeyboardFocusTrap(false);
};
}, [enableTerminal, instance, ref, setDisableKeyboardFocusTrap, type]);
}, [ref, instance, enableTerminal, setDisableKeyboardFocusTrap, type]);
const readyState = dataChannel.readyState;
useEffect(() => {
if (!instance) return;
if (readyState !== "open") return;
const abortController = new AbortController();
@ -93,11 +94,10 @@ function Terminal({
// Handle binary data differently based on browser implementation
// Firefox sends data as blobs, chrome sends data as arraybuffer
if (binaryType === "arraybuffer") {
instance?.write(new Uint8Array(e.data));
instance.write(new Uint8Array(e.data));
} else if (binaryType === "blob") {
const reader = new FileReader();
reader.onload = () => {
if (!instance) return;
if (!reader.result) return;
instance.write(new Uint8Array(reader.result as ArrayBuffer));
};
@ -107,12 +107,12 @@ function Terminal({
{ signal: abortController.signal },
);
const onDataHandler = instance?.onData(data => {
const onDataHandler = instance.onData(data => {
dataChannel.send(data);
});
// Setup escape key handler
const onKeyHandler = instance?.onKey(e => {
const onKeyHandler = instance.onKey(e => {
const { domEvent } = e;
if (domEvent.key === "Escape") {
setTerminalType("none");
@ -123,32 +123,32 @@ function Terminal({
// Send initial terminal size
if (dataChannel.readyState === "open") {
dataChannel.send(JSON.stringify({ rows: instance?.rows, cols: instance?.cols }));
dataChannel.send(JSON.stringify({ rows: instance.rows, cols: instance.cols }));
}
return () => {
abortController.abort();
onDataHandler?.dispose();
onKeyHandler?.dispose();
onDataHandler.dispose();
onKeyHandler.dispose();
};
}, [dataChannel, instance, readyState, setDisableKeyboardFocusTrap, setTerminalType]);
}, [instance, dataChannel, readyState, setDisableKeyboardFocusTrap, setTerminalType]);
useEffect(() => {
if (!instance) return;
// Load the fit addon
const fitAddon = new FitAddon();
instance?.loadAddon(fitAddon);
instance.loadAddon(fitAddon);
instance?.loadAddon(new ClipboardAddon());
instance?.loadAddon(new Unicode11Addon());
instance?.loadAddon(new WebLinksAddon());
instance.loadAddon(new ClipboardAddon());
instance.loadAddon(new Unicode11Addon());
instance.loadAddon(new WebLinksAddon());
instance.unicode.activeVersion = "11";
if (isWebGl2Supported) {
const webGl2Addon = new WebglAddon();
webGl2Addon.onContextLoss(() => webGl2Addon.dispose());
instance?.loadAddon(webGl2Addon);
instance.loadAddon(webGl2Addon);
}
const handleResize = () => fitAddon.fit();
@ -158,13 +158,11 @@ function Terminal({
return () => {
window.removeEventListener("resize", handleResize);
};
}, [ref, instance, dataChannel]);
}, [ref, instance]);
return (
<div
onKeyDown={e => {
e.stopPropagation();
}}
onKeyDown={e => e.stopPropagation()}
onKeyUp={e => e.stopPropagation()}
>
<div>

View File

@ -1,4 +1,4 @@
import React from "react";
import React, { JSX } from "react";
import clsx from "clsx";
import FieldLabel from "@/components/FieldLabel";

View File

@ -10,7 +10,7 @@ import {
useVideoStore,
} from "@/hooks/stores";
import { keys, modifiers } from "@/keyboardMappings";
import { useResizeObserver } from "@/hooks/useResizeObserver";
import { useResizeObserver } from "usehooks-ts";
import { cx } from "@/cva.config";
import VirtualKeyboard from "@components/VirtualKeyboard";
import Actionbar from "@components/ActionBar";
@ -67,7 +67,7 @@ export default function WebRTCVideo() {
// Video-related
useResizeObserver({
ref: videoElm,
ref: videoElm as React.RefObject<HTMLElement>,
onResize: ({ width, height }) => {
// This is actually client size, not videoSize
if (width && height) {

View File

@ -1,21 +0,0 @@
import { useEffect, useRef } from "react";
export default function useInterval(callback: () => void, delay: number) {
const savedCallback = useRef<typeof callback>();
// Save the callback directly in the useRef object
savedCallback.current = callback;
// Set up the interval.
useEffect(() => {
function tick() {
if (!savedCallback.current) return;
savedCallback.current();
}
if (delay !== null) {
const id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
}

View File

@ -1,26 +0,0 @@
import { useCallback, useEffect, useRef } from "react";
/**
* Custom hook that determines if the component is currently mounted.
* @returns {() => boolean} A function that returns a boolean value indicating whether the component is mounted.
* @public
* @see [Documentation](https://usehooks-ts.com/react-hook/use-is-mounted)
* @example
* ```tsx
* const isComponentMounted = useIsMounted();
* // Use isComponentMounted() to check if the component is currently mounted before performing certain actions.
* ```
*/
export function useIsMounted(): () => boolean {
const isMounted = useRef(false);
useEffect(() => {
isMounted.current = true;
return () => {
isMounted.current = false;
};
}, []);
return useCallback(() => isMounted.current, []);
}

View File

@ -1,131 +0,0 @@
import { useEffect, useRef, useState } from "react";
import type { RefObject } from "react";
import { useIsMounted } from "./useIsMounted";
/** The size of the observed element. */
interface Size {
/** The width of the observed element. */
width: number | undefined;
/** The height of the observed element. */
height: number | undefined;
}
/** The options for the ResizeObserver. */
interface UseResizeObserverOptions<T extends HTMLElement = HTMLElement> {
/** The ref of the element to observe. */
ref: RefObject<T>;
/**
* When using `onResize`, the hook doesn't re-render on element size changes; it delegates handling to the provided callback.
* @default undefined
*/
onResize?: (size: Size) => void;
/**
* The box model to use for the ResizeObserver.
* @default 'content-box'
*/
box?: "border-box" | "content-box" | "device-pixel-content-box";
}
const initialSize: Size = {
width: undefined,
height: undefined,
};
/**
* Custom hook that observes the size of an element using the ResizeObserver API.
* @template T - The type of the element to observe.
* @param {UseResizeObserverOptions<T>} options - The options for the ResizeObserver.
* @returns {Size} - The size of the observed element.
* @public
* @see [Documentation](https://usehooks-ts.com/react-hook/use-resize-observer)
* @see [MDN ResizeObserver API](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver)
* @example
* ```tsx
* const myRef = useRef(null);
* const { width = 0, height = 0 } = useResizeObserver({
* ref: myRef,
* box: 'content-box',
* });
*
* <div ref={myRef}>Hello, world!</div>
* ```
*/
export function useResizeObserver<T extends HTMLElement = HTMLElement>(
options: UseResizeObserverOptions<T>,
): Size {
const { ref, box = "content-box" } = options;
const [{ width, height }, setSize] = useState<Size>(initialSize);
const isMounted = useIsMounted();
const previousSize = useRef<Size>({ ...initialSize });
const onResize = useRef<((size: Size) => void) | undefined>(undefined);
onResize.current = options.onResize;
useEffect(() => {
if (!ref.current) return;
if (typeof window === "undefined" || !("ResizeObserver" in window)) return;
const observer = new ResizeObserver(([entry]) => {
const boxProp =
box === "border-box"
? "borderBoxSize"
: box === "device-pixel-content-box"
? "devicePixelContentBoxSize"
: "contentBoxSize";
const newWidth = extractSize(entry, boxProp, "inlineSize");
const newHeight = extractSize(entry, boxProp, "blockSize");
const hasChanged =
previousSize.current.width !== newWidth ||
previousSize.current.height !== newHeight;
if (hasChanged) {
const newSize: Size = { width: newWidth, height: newHeight };
previousSize.current.width = newWidth;
previousSize.current.height = newHeight;
if (onResize.current) {
onResize.current(newSize);
} else {
if (isMounted()) {
setSize(newSize);
}
}
}
});
observer.observe(ref.current, { box });
return () => {
observer.disconnect();
};
}, [box, isMounted, ref]);
return { width, height };
}
/** @private */
type BoxSizesKey = keyof Pick<
ResizeObserverEntry,
"borderBoxSize" | "contentBoxSize" | "devicePixelContentBoxSize"
>;
function extractSize(
entry: ResizeObserverEntry,
box: BoxSizesKey,
sizeType: keyof ResizeObserverSize,
): number | undefined {
if (!entry[box]) {
if (box === "contentBoxSize") {
return entry.contentRect[sizeType === "inlineSize" ? "width" : "height"];
}
return undefined;
}
return Array.isArray(entry[box])
? entry[box][0][sizeType]
: // @ts-expect-error Support Firefox's non-standard behavior
(entry[box][sizeType] as number);
}

View File

@ -19,7 +19,7 @@ import { LinkButton } from "../components/Button";
import { cx } from "../cva.config";
import { useUiStore } from "../hooks/stores";
import useKeyboard from "../hooks/useKeyboard";
import { useResizeObserver } from "../hooks/useResizeObserver";
import { useResizeObserver } from "usehooks-ts";
import LoadingSpinner from "../components/LoadingSpinner";
/* TODO: Migrate to using URLs instead of the global state. To simplify the refactoring, we'll keep the global state for now. */
@ -30,7 +30,7 @@ export default function SettingsRoute() {
const scrollContainerRef = useRef<HTMLDivElement>(null);
const [showLeftGradient, setShowLeftGradient] = useState(false);
const [showRightGradient, setShowRightGradient] = useState(false);
const { width } = useResizeObserver({ ref: scrollContainerRef });
const { width = 0 } = useResizeObserver({ ref: scrollContainerRef as React.RefObject<HTMLDivElement> });
// Handle scroll position to show/hide gradients
const handleScroll = () => {

View File

@ -5,7 +5,7 @@ import { ArrowRightIcon } from "@heroicons/react/16/solid";
import DashboardNavbar from "@components/Header";
import { LinkButton } from "@components/Button";
import KvmCard from "@components/KvmCard";
import useInterval from "@/hooks/useInterval";
import { useInterval } from "usehooks-ts";
import { checkAuth } from "@/main";
import { User } from "@/hooks/stores";
import EmptyCard from "@components/EmptyCard";