mirror of
https://github.com/bitburner-official/bitburner-src.git
synced 2026-05-03 06:17:04 +02:00
FEATURE: Add "Recent Errors" tab and improved error modal (#2169)
This commit is contained in:
committed by
GitHub
parent
cf72937faf
commit
18f84396e2
@@ -0,0 +1,62 @@
|
||||
import { Router } from "../ui/GameRoot";
|
||||
import { SimplePage } from "@enums";
|
||||
import { errorModalsAreSuppressed, ErrorRecord, ErrorState } from "./ErrorState";
|
||||
|
||||
let currentId = 0;
|
||||
|
||||
export const DisplayError = (
|
||||
message: string,
|
||||
errorType: string,
|
||||
scriptName = "",
|
||||
hostname: string = "",
|
||||
pid: number = -1,
|
||||
) => {
|
||||
const errorPageOpen = Router.page() === SimplePage.RecentErrors;
|
||||
if (!errorPageOpen) {
|
||||
ErrorState.UnreadErrors++;
|
||||
}
|
||||
const prior = findExistingErrorCopy(message, hostname);
|
||||
if (prior) {
|
||||
prior.occurrences++;
|
||||
prior.time = new Date();
|
||||
if (pid !== -1) {
|
||||
prior.pid = pid;
|
||||
}
|
||||
prior.server = hostname;
|
||||
prior.message = message;
|
||||
|
||||
updateActiveError(prior);
|
||||
} else {
|
||||
ErrorState.Errors.unshift({
|
||||
id: currentId++,
|
||||
server: hostname,
|
||||
errorType,
|
||||
scriptName,
|
||||
message,
|
||||
pid,
|
||||
occurrences: 1,
|
||||
time: new Date(),
|
||||
unread: !errorPageOpen,
|
||||
});
|
||||
while (ErrorState.Errors.length > 100) {
|
||||
ErrorState.Errors.pop();
|
||||
}
|
||||
updateActiveError(ErrorState.Errors[0]);
|
||||
}
|
||||
};
|
||||
|
||||
function findExistingErrorCopy(message: string, hostname: string): ErrorRecord | null {
|
||||
const serverAgnosticMessage = message.replaceAll(hostname, "<server>");
|
||||
return (
|
||||
ErrorState.Errors.find(
|
||||
(e) => e.message.replaceAll(e.server, "<server>") === serverAgnosticMessage || e.message === message,
|
||||
) ?? null
|
||||
);
|
||||
}
|
||||
|
||||
function updateActiveError(error: ErrorRecord): void {
|
||||
if (!ErrorState.ActiveError && !errorModalsAreSuppressed()) {
|
||||
ErrorState.ActiveError = error;
|
||||
ErrorState.ErrorUpdate.emit(ErrorState.ActiveError);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { makeStyles } from "tss-react/mui";
|
||||
import { Modal } from "../ui/React/Modal";
|
||||
import { Box, Button, Typography } from "@mui/material/";
|
||||
import { errorModalsAreSuppressed, type ErrorRecord, ErrorState, toggleSuppressErrorModals } from "./ErrorState";
|
||||
import { Router } from "../ui/GameRoot";
|
||||
import { SimplePage, ToastVariant } from "@enums";
|
||||
import { useRerender } from "../ui/React/hooks";
|
||||
import { OptionSwitch } from "../ui/React/OptionSwitch";
|
||||
import { LogBoxEvents } from "../ui/React/LogBoxManager";
|
||||
import { recentScripts } from "../Netscript/RecentScripts";
|
||||
import { SnackbarEvents } from "../ui/React/Snackbar";
|
||||
import { Settings } from "../Settings/Settings";
|
||||
|
||||
export function ErrorModal(): React.ReactElement {
|
||||
const { classes } = useStyles();
|
||||
const rerender = useRerender();
|
||||
const [error, setError] = useState<ErrorRecord | null>(ErrorState.ActiveError);
|
||||
|
||||
useEffect(() => {
|
||||
const listener = (newError: ErrorRecord) => {
|
||||
if (newError.force || (Router.page() !== SimplePage.RecentErrors && !errorModalsAreSuppressed())) {
|
||||
setError(newError);
|
||||
rerender();
|
||||
} else {
|
||||
ErrorState.ActiveError = null;
|
||||
setError(null);
|
||||
}
|
||||
};
|
||||
return ErrorState.ErrorUpdate.subscribe(listener);
|
||||
}, [rerender]);
|
||||
|
||||
const onClose = (force = false): void => {
|
||||
ErrorState.ActiveError && (ErrorState.ActiveError.unread = false);
|
||||
|
||||
if (force || errorModalsAreSuppressed()) {
|
||||
ErrorState.ActiveError = null;
|
||||
setError(null);
|
||||
} else {
|
||||
const nextError = ErrorState.Errors.find((e) => e.unread) ?? null;
|
||||
ErrorState.ActiveError = nextError;
|
||||
setError(nextError);
|
||||
}
|
||||
ErrorState.UnreadErrors = ErrorState.Errors.filter((e) => e.unread).length;
|
||||
};
|
||||
|
||||
const viewLogs = (): void => {
|
||||
if (error === null) {
|
||||
return;
|
||||
}
|
||||
const recentScript = recentScripts.find((script) => script.runningScript.pid === error.pid);
|
||||
if (!recentScript) {
|
||||
SnackbarEvents.emit(`No recent script found with pid ${error.pid}`, ToastVariant.INFO, 2000);
|
||||
return;
|
||||
}
|
||||
onClose();
|
||||
LogBoxEvents.emit(recentScript.runningScript);
|
||||
};
|
||||
|
||||
const goToErrorPage = () => {
|
||||
onClose(true);
|
||||
Router.toPage(SimplePage.RecentErrors);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal open={!!error} onClose={() => onClose()}>
|
||||
{error && (
|
||||
<>
|
||||
<Typography component="div">
|
||||
<h2>{error.errorType} ERROR</h2>
|
||||
{/* Add a zero-width space after each slash to allow clean wrapping. */}
|
||||
<p style={{ whiteSpace: "pre-wrap" }}>{error.message.replaceAll("/", "/\u200B")}</p>
|
||||
<p>
|
||||
Script: {error.scriptName}
|
||||
<br />
|
||||
PID: {error.pid}
|
||||
</p>
|
||||
{!Settings.SuppressErrorModals && (
|
||||
<OptionSwitch
|
||||
checked={errorModalsAreSuppressed()}
|
||||
onChange={(newValue) => toggleSuppressErrorModals(newValue)}
|
||||
text="Suppress error modals (5 min)"
|
||||
tooltip={
|
||||
<>
|
||||
If this is set, no error modals will be shown for the next five minutes, and only log errors to the
|
||||
Recent Errors page.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Typography>
|
||||
<Box className={classes.inlineFlexBox}>
|
||||
<Button onClick={() => onClose()}>Close</Button>
|
||||
<div>
|
||||
<Button disabled={error.pid === -1} onClick={viewLogs}>
|
||||
View Script Logs
|
||||
</Button>
|
||||
<Button onClick={goToErrorPage}>Errors Page</Button>
|
||||
</div>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
const useStyles = makeStyles()(() => ({
|
||||
inlineFlexBox: {
|
||||
display: "inline-flex",
|
||||
flexDirection: "row",
|
||||
width: "100%",
|
||||
justifyContent: "space-between",
|
||||
},
|
||||
}));
|
||||
@@ -0,0 +1,35 @@
|
||||
import { EventEmitter } from "../utils/EventEmitter";
|
||||
|
||||
export type ErrorRecord = {
|
||||
id: number;
|
||||
server: string;
|
||||
errorType: string;
|
||||
message: string;
|
||||
scriptName: string;
|
||||
pid: number;
|
||||
occurrences: number;
|
||||
time: Date;
|
||||
unread: boolean;
|
||||
force?: boolean;
|
||||
};
|
||||
|
||||
export const ErrorState = {
|
||||
ErrorUpdate: new EventEmitter<[ErrorRecord]>(),
|
||||
ActiveError: null as ErrorRecord | null,
|
||||
Errors: [] as ErrorRecord[],
|
||||
UnreadErrors: 0,
|
||||
PreventModalsUntil: new Date(),
|
||||
};
|
||||
|
||||
export function errorModalsAreSuppressed(): boolean {
|
||||
return ErrorState.PreventModalsUntil.getTime() > Date.now();
|
||||
}
|
||||
|
||||
export function toggleSuppressErrorModals(newValue: boolean, indefinite = false): void {
|
||||
if (newValue) {
|
||||
ErrorState.PreventModalsUntil = new Date(indefinite ? new Date("3000-01-01") : Date.now() + 1000 * 60 * 5);
|
||||
ErrorState.ActiveError = null;
|
||||
} else {
|
||||
ErrorState.PreventModalsUntil = new Date();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { makeStyles } from "tss-react/mui";
|
||||
import { type ErrorRecord, ErrorState } from "./ErrorState";
|
||||
import { useRerender } from "../ui/React/hooks";
|
||||
import { Typography, Tooltip } from "@mui/material";
|
||||
import { Theme } from "@mui/material/styles";
|
||||
|
||||
export function RecentErrorsPage(): React.ReactElement {
|
||||
const rerender = useRerender();
|
||||
React.useEffect(() => {
|
||||
const clearSubscription = ErrorState.ErrorUpdate.subscribe(rerender);
|
||||
ErrorState.UnreadErrors = 0;
|
||||
return () => {
|
||||
clearSubscription();
|
||||
ErrorState.UnreadErrors = 0;
|
||||
};
|
||||
}, [rerender]);
|
||||
|
||||
useEffect(() => {
|
||||
ErrorState.Errors.forEach((error) => {
|
||||
error.unread = false; // Mark all errors as read when the page is loaded
|
||||
});
|
||||
}, []);
|
||||
|
||||
const { classes } = useStyles();
|
||||
|
||||
const showError = (error: ErrorRecord): void => {
|
||||
ErrorState.ErrorUpdate.emit({ ...error, force: true });
|
||||
};
|
||||
|
||||
const formatMessage = (message: string): string => {
|
||||
/**
|
||||
* - Add a zero-width space after each slash to allow clean wrapping.
|
||||
* - Replace 2+ newline characters with only 1 newline character to reduce the number of empty lines.
|
||||
*/
|
||||
return message.replaceAll("/", "/\u200B").replaceAll(/\n{2,}/g, "\n");
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Typography component="div" sx={{ height: "100vh", overflowY: "auto", scrollbarWidth: "thin" }}>
|
||||
<table className={classes.errorTable}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th className={classes.cellText}>Count</th>
|
||||
<th className={classes.cellText}>Type</th>
|
||||
<th className={classes.cellText}>Message</th>
|
||||
<th className={classes.cellText}>Script</th>
|
||||
<th className={classes.cellText}>Time</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{ErrorState.Errors.map((e, i) => (
|
||||
<tr key={i} className={classes.errorRow} onClick={() => showError(e)}>
|
||||
<td className={classes.cellText}>
|
||||
<div className={classes.xsmall}>{e.occurrences}</div>
|
||||
</td>
|
||||
<td className={classes.cellText}>
|
||||
<div className={classes.xsmall}>{e.errorType}</div>
|
||||
</td>
|
||||
<td>
|
||||
<div className={classes.errorText} key={i}>
|
||||
{formatMessage(e.message)}
|
||||
</div>
|
||||
</td>
|
||||
<td className={classes.cellText}>
|
||||
<div className={classes.small}>
|
||||
<Tooltip title={<>{formatMessage(e.scriptName)}</>}>
|
||||
<div style={{ textOverflow: "ellipsis", overflow: "auto" }}>{formatMessage(e.scriptName)}</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</td>
|
||||
<td className={classes.cellText}>
|
||||
<div className={classes.xsmall}>{e.time.toLocaleString()}</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</Typography>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const useStyles = makeStyles()((theme: Theme) => ({
|
||||
errorTable: {
|
||||
width: "100%",
|
||||
maxWidth: "100%",
|
||||
borderCollapse: "collapse",
|
||||
},
|
||||
errorRow: {
|
||||
borderTop: `1px solid ${theme.colors.button}`,
|
||||
"&:hover": {
|
||||
backgroundColor: theme.colors.button,
|
||||
},
|
||||
},
|
||||
cellText: {
|
||||
verticalAlign: "top",
|
||||
padding: "4px",
|
||||
textAlign: "left",
|
||||
},
|
||||
errorText: {
|
||||
margin: "4px",
|
||||
color: "white",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "pre-wrap",
|
||||
lineClamp: "6",
|
||||
lineHeight: 1.1,
|
||||
overflowX: "auto",
|
||||
maxHeight: "200px",
|
||||
},
|
||||
xsmall: {
|
||||
maxWidth: "110px",
|
||||
fontSize: "14px",
|
||||
lineHeight: 1.2,
|
||||
},
|
||||
small: {
|
||||
maxWidth: "200px",
|
||||
},
|
||||
}));
|
||||
Reference in New Issue
Block a user