mirror of https://github.com/jetkvm/kvm.git
feat: enhance FailSafeModeOverlay with tooltip and log download improvements
This commit is contained in:
parent
72f29df835
commit
82ad2a467f
|
|
@ -1,9 +1,10 @@
|
|||
import { useState } from "react";
|
||||
import { ExclamationTriangleIcon } from "@heroicons/react/24/solid";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { LuInfo } from "react-icons/lu";
|
||||
|
||||
import { Button } from "@/components/Button";
|
||||
import { GridCard } from "@components/Card";
|
||||
import Card, { GridCard } from "@components/Card";
|
||||
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
import { useDeviceUiNavigation } from "@/hooks/useAppNavigation";
|
||||
import { useVersion } from "@/hooks/useVersion";
|
||||
|
|
@ -30,19 +31,47 @@ function OverlayContent({ children }: OverlayContentProps) {
|
|||
);
|
||||
}
|
||||
|
||||
interface TooltipProps {
|
||||
readonly children: React.ReactNode;
|
||||
readonly text: string;
|
||||
readonly show: boolean;
|
||||
}
|
||||
|
||||
function Tooltip({ children, text, show }: TooltipProps) {
|
||||
if (!show) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div className="group/tooltip relative">
|
||||
{children}
|
||||
<div className="pointer-events-none absolute bottom-full left-1/2 mb-2 hidden -translate-x-1/2 opacity-0 transition-opacity group-hover/tooltip:block group-hover/tooltip:opacity-100">
|
||||
<Card>
|
||||
<div className="whitespace-nowrap px-2 py-1 text-xs flex items-center gap-1 justify-center">
|
||||
<LuInfo className="h-3 w-3 text-slate-700 dark:text-slate-300" />
|
||||
{text}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function FailSafeModeOverlay({ reason }: FailSafeModeOverlayProps) {
|
||||
const { send } = useJsonRpc();
|
||||
const { navigateTo } = useDeviceUiNavigation();
|
||||
const { appVersion } = useVersion();
|
||||
const { systemVersion } = useDeviceStore();
|
||||
const [isDownloadingLogs, setIsDownloadingLogs] = useState(false);
|
||||
const [hasDownloadedLogs, setHasDownloadedLogs] = useState(false);
|
||||
|
||||
const getReasonCopy = () => {
|
||||
switch (reason) {
|
||||
case "video":
|
||||
return {
|
||||
message:
|
||||
"We've detected an issue with the video capture process. Your device is still running and accessible, but video streaming is temporarily unavailable. You can reboot to attempt recovery, report the issue, or downgrade to the last stable version.",
|
||||
"We've detected an issue with the video capture process. Your device is still running and accessible, but video streaming is temporarily unavailable.",
|
||||
};
|
||||
default:
|
||||
return {
|
||||
|
|
@ -80,13 +109,14 @@ export function FailSafeModeOverlay({ reason }: FailSafeModeOverlayProps) {
|
|||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
notifications.success("Recovery logs downloaded successfully");
|
||||
notifications.success("Crash logs downloaded successfully");
|
||||
setHasDownloadedLogs(true);
|
||||
|
||||
// Open GitHub issue
|
||||
const issueBody = `## Issue Description
|
||||
The ${reason} process encountered an error and recovery mode was activated.
|
||||
The \`${reason}\` process encountered an error and fail safe mode was activated.
|
||||
|
||||
**Reason:** ${reason}
|
||||
**Reason:** \`${reason}\`
|
||||
**Timestamp:** ${new Date().toISOString()}
|
||||
**App Version:** ${appVersion || "Unknown"}
|
||||
**System Version:** ${systemVersion || "Unknown"}
|
||||
|
|
@ -95,6 +125,9 @@ The ${reason} process encountered an error and recovery mode was activated.
|
|||
Please attach the recovery logs file that was downloaded to your computer:
|
||||
\`${filename}\`
|
||||
|
||||
> [!NOTE]
|
||||
> Please omit any sensitive information from the logs.
|
||||
|
||||
## Additional Context
|
||||
[Please describe what you were doing when this occurred]`;
|
||||
|
||||
|
|
@ -114,7 +147,7 @@ Please attach the recovery logs file that was downloaded to your computer:
|
|||
return (
|
||||
<AnimatePresence>
|
||||
<motion.div
|
||||
className="aspect-video h-full w-full"
|
||||
className="aspect-video h-full w-full isolate"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0, transition: { duration: 0 } }}
|
||||
|
|
@ -132,6 +165,7 @@ Please attach the recovery logs file that was downloaded to your computer:
|
|||
<h2 className="text-xl font-bold">Fail safe mode activated</h2>
|
||||
<p className="text-sm">{message}</p>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Button
|
||||
onClick={handleReportAndDownloadLogs}
|
||||
|
|
@ -139,22 +173,32 @@ Please attach the recovery logs file that was downloaded to your computer:
|
|||
size="SM"
|
||||
disabled={isDownloadingLogs}
|
||||
LeadingIcon={GitHubIcon}
|
||||
text={isDownloadingLogs ? "Downloading Logs..." : "Report Issue & Download Logs"}
|
||||
text={isDownloadingLogs ? "Downloading Logs..." : "Download Logs & Report Issue"}
|
||||
/>
|
||||
|
||||
<div className="h-8 w-px bg-slate-200 dark:bg-slate-700 block" />
|
||||
<Tooltip text="Download logs first to unlock" show={!hasDownloadedLogs}>
|
||||
<Button
|
||||
onClick={() => navigateTo("/settings/general/reboot")}
|
||||
theme="light"
|
||||
size="SM"
|
||||
text="Reboot Device"
|
||||
disabled={!hasDownloadedLogs}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip text="Download logs first to unlock" show={!hasDownloadedLogs}>
|
||||
<Button
|
||||
size="SM"
|
||||
onClick={handleDowngrade}
|
||||
theme="light"
|
||||
text="Downgrade to v0.4.8"
|
||||
disabled={!hasDownloadedLogs}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,28 +1,39 @@
|
|||
import { useNavigate } from "react-router";
|
||||
import { useCallback } from "react";
|
||||
import { useCallback, useState } from "react";
|
||||
|
||||
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
import { Button } from "@components/Button";
|
||||
|
||||
import LoadingSpinner from "../components/LoadingSpinner";
|
||||
import { useDeviceUiNavigation } from "../hooks/useAppNavigation";
|
||||
|
||||
export default function SettingsGeneralRebootRoute() {
|
||||
const navigate = useNavigate();
|
||||
const { send } = useJsonRpc();
|
||||
const [isRebooting, setIsRebooting] = useState(false);
|
||||
const { navigateTo } = useDeviceUiNavigation();
|
||||
|
||||
const onConfirmUpdate = useCallback(() => {
|
||||
const onConfirmUpdate = useCallback(async () => {
|
||||
setIsRebooting(true);
|
||||
// This is where we send the RPC to the golang binary
|
||||
send("reboot", { force: true });
|
||||
}, [send]);
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||
navigateTo("/");
|
||||
}, [navigateTo, send]);
|
||||
|
||||
{
|
||||
/* 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 isRebooting={isRebooting} onClose={() => navigate("..")} onConfirmUpdate={onConfirmUpdate} />;
|
||||
}
|
||||
|
||||
export function Dialog({
|
||||
isRebooting,
|
||||
onClose,
|
||||
onConfirmUpdate,
|
||||
}: {
|
||||
isRebooting: boolean;
|
||||
onClose: () => void;
|
||||
onConfirmUpdate: () => void;
|
||||
}) {
|
||||
|
|
@ -31,6 +42,7 @@ export function Dialog({
|
|||
<div className="pointer-events-auto relative mx-auto text-left">
|
||||
<div>
|
||||
<ConfirmationBox
|
||||
isRebooting={isRebooting}
|
||||
onYes={onConfirmUpdate}
|
||||
onNo={onClose}
|
||||
/>
|
||||
|
|
@ -40,9 +52,11 @@ export function Dialog({
|
|||
}
|
||||
|
||||
function ConfirmationBox({
|
||||
isRebooting,
|
||||
onYes,
|
||||
onNo,
|
||||
}: {
|
||||
isRebooting: boolean;
|
||||
onYes: () => void;
|
||||
onNo: () => void;
|
||||
}) {
|
||||
|
|
@ -55,11 +69,16 @@ function ConfirmationBox({
|
|||
<p className="text-sm text-slate-600 dark:text-slate-300">
|
||||
Do you want to proceed with rebooting the system?
|
||||
</p>
|
||||
|
||||
{isRebooting ? (
|
||||
<div className="mt-4 flex items-center justify-center">
|
||||
<LoadingSpinner className="h-6 w-6 text-blue-700 dark:text-blue-500" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-4 flex gap-x-2">
|
||||
<Button size="SM" theme="light" text="Yes" onClick={onYes} />
|
||||
<Button size="SM" theme="blank" text="No" onClick={onNo} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -483,10 +483,6 @@ export default function KvmIdRoute() {
|
|||
const rpcDataChannel = pc.createDataChannel("rpc");
|
||||
rpcDataChannel.onopen = () => {
|
||||
setRpcDataChannel(rpcDataChannel);
|
||||
|
||||
// setTimeout(() => {
|
||||
// useFailsafeModeStore.setState({ isFailsafeMode: true, reason: "video" });
|
||||
// }, 1000);
|
||||
};
|
||||
|
||||
const rpcHidChannel = pc.createDataChannel("hidrpc");
|
||||
|
|
@ -863,10 +859,9 @@ export default function KvmIdRoute() {
|
|||
className="animate-slideUpFade pointer-events-none absolute inset-0 flex items-center justify-center p-4"
|
||||
>
|
||||
<div className="relative h-full max-h-[720px] w-full max-w-[1280px] rounded-md">
|
||||
{!!ConnectionStatusElement && ConnectionStatusElement}
|
||||
{isFailsafeMode && failsafeReason && (
|
||||
{isFailsafeMode && failsafeReason ? (
|
||||
<FailSafeModeOverlay reason={failsafeReason} />
|
||||
)}
|
||||
) : !!ConnectionStatusElement && ConnectionStatusElement}
|
||||
</div>
|
||||
</div>
|
||||
<SidebarContainer sidebarView={sidebarView} />
|
||||
|
|
|
|||
Loading…
Reference in New Issue