mirror of https://github.com/jetkvm/kvm.git
Compare commits
4 Commits
d71c887dc7
...
3632bfff5d
| Author | SHA1 | Date |
|---|---|---|
|
|
3632bfff5d | |
|
|
78d4a0275b | |
|
|
4ff617a679 | |
|
|
39b23b8bd5 |
|
|
@ -213,7 +213,7 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonPropsType>(
|
||||||
Button.displayName = "Button";
|
Button.displayName = "Button";
|
||||||
|
|
||||||
type LinkPropsType = Pick<LinkProps, "to"> &
|
type LinkPropsType = Pick<LinkProps, "to"> &
|
||||||
React.ComponentProps<typeof ButtonContent> & { disabled?: boolean };
|
React.ComponentProps<typeof ButtonContent> & { disabled?: boolean, reloadDocument?: 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",
|
||||||
|
|
@ -231,7 +231,7 @@ export const LinkButton = ({ to, ...props }: LinkPropsType) => {
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<Link to={to} className={classes}>
|
<Link to={to} reloadDocument={props.reloadDocument} className={classes}>
|
||||||
<ButtonContent {...props} />
|
<ButtonContent {...props} />
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -98,7 +98,7 @@ export default function SettingsAccessIndexRoute() {
|
||||||
}
|
}
|
||||||
|
|
||||||
getCloudState();
|
getCloudState();
|
||||||
// In cloud mode, we need to navigate to the device overview page, as we don't a connection anymore
|
// In cloud mode, we need to navigate to the device overview page, as we don't have a connection anymore
|
||||||
if (!isOnDevice) navigate("/");
|
if (!isOnDevice) navigate("/");
|
||||||
return;
|
return;
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,12 @@ import { Button } from "@components/Button";
|
||||||
export default function SettingsGeneralRebootRoute() {
|
export default function SettingsGeneralRebootRoute() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { send } = useJsonRpc();
|
const { send } = useJsonRpc();
|
||||||
|
|
||||||
|
const onClose = useCallback(() => {
|
||||||
|
navigate(".."); // back to the devices.$id.settings page
|
||||||
|
window.location.reload(); // force a full reload to ensure the current device/cloud UI version is loaded
|
||||||
|
}, [navigate]);
|
||||||
|
|
||||||
|
|
||||||
const onConfirmUpdate = useCallback(() => {
|
const onConfirmUpdate = useCallback(() => {
|
||||||
// This is where we send the RPC to the golang binary
|
// This is where we send the RPC to the golang binary
|
||||||
|
|
@ -16,7 +22,7 @@ export default function SettingsGeneralRebootRoute() {
|
||||||
{
|
{
|
||||||
/* TODO: Migrate to using URLs instead of the global state. To simplify the refactoring, we'll keep the global state for now. */
|
/* TODO: Migrate to using URLs instead of the global state. To simplify the refactoring, we'll keep the global state for now. */
|
||||||
}
|
}
|
||||||
return <Dialog onClose={() => navigate("..")} onConfirmUpdate={onConfirmUpdate} />;
|
return <Dialog onClose={onClose} onConfirmUpdate={onConfirmUpdate} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Dialog({
|
export function Dialog({
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useLocation, useNavigate } from "react-router";
|
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
import { useLocation, useNavigate } from "react-router";
|
||||||
import { CheckCircleIcon } from "@heroicons/react/20/solid";
|
import { CheckCircleIcon } from "@heroicons/react/20/solid";
|
||||||
import { MdConnectWithoutContact, MdRestartAlt } from "react-icons/md";
|
import { MdConnectWithoutContact, MdRestartAlt } from "react-icons/md";
|
||||||
|
|
||||||
|
|
@ -19,6 +19,11 @@ export default function SettingsGeneralUpdateRoute() {
|
||||||
const { setModalView, otaState } = useUpdateStore();
|
const { setModalView, otaState } = useUpdateStore();
|
||||||
const { send } = useJsonRpc();
|
const { send } = useJsonRpc();
|
||||||
|
|
||||||
|
const onClose = useCallback(() => {
|
||||||
|
navigate(".."); // back to the devices.$id.settings page
|
||||||
|
window.location.reload(); // force a full reload to ensure the current device/cloud UI version is loaded
|
||||||
|
}, [navigate]);
|
||||||
|
|
||||||
const onConfirmUpdate = useCallback(() => {
|
const onConfirmUpdate = useCallback(() => {
|
||||||
send("tryUpdate", {});
|
send("tryUpdate", {});
|
||||||
setModalView("updating");
|
setModalView("updating");
|
||||||
|
|
@ -39,7 +44,7 @@ export default function SettingsGeneralUpdateRoute() {
|
||||||
{
|
{
|
||||||
/* TODO: Migrate to using URLs instead of the global state. To simplify the refactoring, we'll keep the global state for now. */
|
/* TODO: Migrate to using URLs instead of the global state. To simplify the refactoring, we'll keep the global state for now. */
|
||||||
}
|
}
|
||||||
return <Dialog onClose={() => navigate("..")} onConfirmUpdate={onConfirmUpdate} />;
|
return <Dialog onClose={onClose} onConfirmUpdate={onConfirmUpdate} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Dialog({
|
export function Dialog({
|
||||||
|
|
@ -223,7 +228,7 @@ function UpdatingDeviceState({
|
||||||
100,
|
100,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// System: 10% download, 90% update
|
// System: 10% download, 10% verification, 80% update
|
||||||
return Math.min(
|
return Math.min(
|
||||||
downloadProgress * 0.1 + verificationProgress * 0.1 + updateProgress * 0.8,
|
downloadProgress * 0.1 + verificationProgress * 0.1 + updateProgress * 0.8,
|
||||||
100,
|
100,
|
||||||
|
|
@ -287,17 +292,19 @@ function UpdatingDeviceState({
|
||||||
<span className="font-medium text-black dark:text-white">
|
<span className="font-medium text-black dark:text-white">
|
||||||
Rebooting the device to complete the update...
|
Rebooting the device to complete the update...
|
||||||
</span>
|
</span>
|
||||||
<p>
|
<p className="font-medium text-black dark:text-white">
|
||||||
This may take a few minutes. The device will automatically
|
This may take a few minutes. The device will automatically
|
||||||
reconnect once it is back online. If it doesn{"'"}t, you can
|
reconnect once it is back online.<br/>
|
||||||
manually reconnect.
|
If it doesn{"'"}t reconnect automatically, you can manually
|
||||||
|
reconnect by clicking here:
|
||||||
<LinkButton
|
<LinkButton
|
||||||
size="SM"
|
size="XS"
|
||||||
theme="light"
|
theme="light"
|
||||||
text="Reconnect to KVM"
|
text="Reconnect to KVM"
|
||||||
LeadingIcon={MdConnectWithoutContact}
|
LeadingIcon={MdConnectWithoutContact}
|
||||||
textAlign="center"
|
textAlign="center"
|
||||||
to={".."}
|
reloadDocument={true}
|
||||||
|
to={"/"}
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -307,15 +314,17 @@ function UpdatingDeviceState({
|
||||||
<span className="font-medium text-black dark:text-white">
|
<span className="font-medium text-black dark:text-white">
|
||||||
Device reboot is pending...
|
Device reboot is pending...
|
||||||
</span>
|
</span>
|
||||||
<p>
|
<p className="font-medium text-black dark:text-white">
|
||||||
The JetKVM is preparing to reboot. This may take a while. If it doesn{"'"}t automatically reboot
|
The JetKVM is preparing to reboot. This may take a while.<br/>
|
||||||
after a few minutes, you can manually request a reboot.
|
If it doesn{"'"}t automatically reboot after a few minutes, you
|
||||||
|
can manually request a reboot by clicking here:
|
||||||
<LinkButton
|
<LinkButton
|
||||||
size="SM"
|
size="XS"
|
||||||
theme="light"
|
theme="light"
|
||||||
text="Reboot the KVM"
|
text="Reboot the KVM"
|
||||||
LeadingIcon={MdRestartAlt}
|
LeadingIcon={MdRestartAlt}
|
||||||
textAlign="center"
|
textAlign="center"
|
||||||
|
reloadDocument={true}
|
||||||
to={"../reboot"}
|
to={"../reboot"}
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
|
|
|
||||||
|
|
@ -146,6 +146,7 @@ export default function KvmIdRoute() {
|
||||||
const { otaState, setOtaState, setModalView } = useUpdateStore();
|
const { otaState, setOtaState, setModalView } = useUpdateStore();
|
||||||
|
|
||||||
const [loadingMessage, setLoadingMessage] = useState("Connecting to device...");
|
const [loadingMessage, setLoadingMessage] = useState("Connecting to device...");
|
||||||
|
|
||||||
const cleanupAndStopReconnecting = useCallback(
|
const cleanupAndStopReconnecting = useCallback(
|
||||||
function cleanupAndStopReconnecting() {
|
function cleanupAndStopReconnecting() {
|
||||||
console.log("Closing peer connection");
|
console.log("Closing peer connection");
|
||||||
|
|
@ -182,11 +183,11 @@ export default function KvmIdRoute() {
|
||||||
pc: RTCPeerConnection,
|
pc: RTCPeerConnection,
|
||||||
remoteDescription: RTCSessionDescriptionInit,
|
remoteDescription: RTCSessionDescriptionInit,
|
||||||
) {
|
) {
|
||||||
setLoadingMessage("Setting remote description");
|
setLoadingMessage("Setting remote description type:" + remoteDescription.type);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await pc.setRemoteDescription(new RTCSessionDescription(remoteDescription));
|
await pc.setRemoteDescription(new RTCSessionDescription(remoteDescription));
|
||||||
console.log("[setRemoteSessionDescription] Remote description set successfully");
|
console.log("[setRemoteSessionDescription] Remote description set successfully to: " + remoteDescription.sdp);
|
||||||
setLoadingMessage("Establishing secure connection...");
|
setLoadingMessage("Establishing secure connection...");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(
|
console.error(
|
||||||
|
|
@ -231,9 +232,15 @@ export default function KvmIdRoute() {
|
||||||
const ignoreOffer = useRef(false);
|
const ignoreOffer = useRef(false);
|
||||||
const isSettingRemoteAnswerPending = useRef(false);
|
const isSettingRemoteAnswerPending = useRef(false);
|
||||||
const makingOffer = useRef(false);
|
const makingOffer = useRef(false);
|
||||||
|
const reconnectAttemptsRef = useRef(20);
|
||||||
|
|
||||||
const wsProtocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
const wsProtocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||||
|
|
||||||
|
const reconnectInterval = (attempt: number) => {
|
||||||
|
// Exponential backoff with a max of 10 seconds between attempts
|
||||||
|
return Math.min(500 * 2 ** attempt, 10000);
|
||||||
|
}
|
||||||
|
|
||||||
const { sendMessage, getWebSocket } = useWebSocket(
|
const { sendMessage, getWebSocket } = useWebSocket(
|
||||||
isOnDevice
|
isOnDevice
|
||||||
? `${wsProtocol}//${window.location.host}/webrtc/signaling/client`
|
? `${wsProtocol}//${window.location.host}/webrtc/signaling/client`
|
||||||
|
|
@ -241,17 +248,16 @@ export default function KvmIdRoute() {
|
||||||
{
|
{
|
||||||
heartbeat: true,
|
heartbeat: true,
|
||||||
retryOnError: true,
|
retryOnError: true,
|
||||||
reconnectAttempts: 15,
|
reconnectAttempts: reconnectAttemptsRef.current,
|
||||||
reconnectInterval: 1000,
|
reconnectInterval: reconnectInterval,
|
||||||
onReconnectStop: () => {
|
onReconnectStop: (attempt: number) => {
|
||||||
console.debug("Reconnect stopped");
|
console.debug("Reconnect stopped after ", attempt, "attempts");
|
||||||
cleanupAndStopReconnecting();
|
cleanupAndStopReconnecting();
|
||||||
},
|
},
|
||||||
|
|
||||||
shouldReconnect(event) {
|
shouldReconnect(event) {
|
||||||
console.debug("[Websocket] shouldReconnect", event);
|
console.debug("[Websocket] shouldReconnect", event);
|
||||||
// TODO: Why true?
|
return !connectionFailed; // we always want to try to reconnect unless we're explicitly stopped
|
||||||
return true;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
onClose(event) {
|
onClose(event) {
|
||||||
|
|
@ -284,6 +290,7 @@ export default function KvmIdRoute() {
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const parsedMessage = JSON.parse(message.data);
|
const parsedMessage = JSON.parse(message.data);
|
||||||
|
|
||||||
if (parsedMessage.type === "device-metadata") {
|
if (parsedMessage.type === "device-metadata") {
|
||||||
const { deviceVersion } = parsedMessage.data;
|
const { deviceVersion } = parsedMessage.data;
|
||||||
console.debug("[Websocket] Received device-metadata message");
|
console.debug("[Websocket] Received device-metadata message");
|
||||||
|
|
@ -300,10 +307,12 @@ export default function KvmIdRoute() {
|
||||||
console.log("[Websocket] Device is using new signaling");
|
console.log("[Websocket] Device is using new signaling");
|
||||||
isLegacySignalingEnabled.current = false;
|
isLegacySignalingEnabled.current = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
setupPeerConnection();
|
setupPeerConnection();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!peerConnection) return;
|
if (!peerConnection) return;
|
||||||
|
|
||||||
if (parsedMessage.type === "answer") {
|
if (parsedMessage.type === "answer") {
|
||||||
console.debug("[Websocket] Received answer");
|
console.debug("[Websocket] Received answer");
|
||||||
const readyForOffer =
|
const readyForOffer =
|
||||||
|
|
@ -594,7 +603,9 @@ export default function KvmIdRoute() {
|
||||||
api.POST(`${CLOUD_API}/webrtc/turn_activity`, {
|
api.POST(`${CLOUD_API}/webrtc/turn_activity`, {
|
||||||
bytesReceived: bytesReceivedDelta,
|
bytesReceived: bytesReceivedDelta,
|
||||||
bytesSent: bytesSentDelta,
|
bytesSent: bytesSentDelta,
|
||||||
});
|
}).catch(()=>{
|
||||||
|
// we don't care about errors here, but we don't want unhandled promise rejections
|
||||||
|
});
|
||||||
}, 10000);
|
}, 10000);
|
||||||
|
|
||||||
const { setNetworkState} = useNetworkStateStore();
|
const { setNetworkState} = useNetworkStateStore();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue