mirror of
https://github.com/bitburner-official/bitburner-src.git
synced 2024-11-22 23:53:48 +01:00
Add information to the recovery page
Adds error & environment information to the recovery page when available. The info will be displayed when the error boundary catches an error only. Otherwise, does a few minor tweaks to the UI of the page. - Add DevPage button to throw an uncaught error to go into recovery - Add "Delete Save" button in recovery from Game Options (refactored into its own component) - Use "Soft Reset" button from Game Options (refactored into its own component) - The "Soft Reset" & "Delete Save" buttons now have confirmations - Add tooltip on "Disable Recovery Mode" button - Add timestamp to the RECOVERY.json filename - Add textarea containing markdown with the current error details, if available - Error - Page - Version - Environment - Platform - UserAgent - Features - Source - Stack Trace - Change GitHub new issue link to contain default body & title, if possible - Change links to not take the full width (they were clickable by mistake) - Fix "Disable Recovery Mode" not resetting the ErrorBoundary's state, making going back to terminal impossible
This commit is contained in:
parent
8b69fd7faa
commit
65964c84b2
@ -1,4 +1,4 @@
|
|||||||
import React from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
|
|
||||||
import Accordion from "@mui/material/Accordion";
|
import Accordion from "@mui/material/Accordion";
|
||||||
import AccordionSummary from "@mui/material/AccordionSummary";
|
import AccordionSummary from "@mui/material/AccordionSummary";
|
||||||
@ -17,6 +17,8 @@ interface IProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function General(props: IProps): React.ReactElement {
|
export function General(props: IProps): React.ReactElement {
|
||||||
|
const [error, setError] = useState(false);
|
||||||
|
|
||||||
function addMoney(n: number) {
|
function addMoney(n: number) {
|
||||||
return function () {
|
return function () {
|
||||||
props.player.gainMoney(n, "other");
|
props.player.gainMoney(n, "other");
|
||||||
@ -43,6 +45,10 @@ export function General(props: IProps): React.ReactElement {
|
|||||||
props.router.toBitVerse(false, false);
|
props.router.toBitVerse(false, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (error) throw new ReferenceError('Manually thrown error');
|
||||||
|
}, [error]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Accordion TransitionProps={{ unmountOnExit: true }}>
|
<Accordion TransitionProps={{ unmountOnExit: true }}>
|
||||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||||
@ -81,6 +87,7 @@ export function General(props: IProps): React.ReactElement {
|
|||||||
<Button onClick={b1tflum3}>Run b1t_flum3.exe</Button>
|
<Button onClick={b1tflum3}>Run b1t_flum3.exe</Button>
|
||||||
<Button onClick={quickHackW0r1dD43m0n}>Quick w0rld_d34m0n</Button>
|
<Button onClick={quickHackW0r1dD43m0n}>Quick w0rld_d34m0n</Button>
|
||||||
<Button onClick={hackW0r1dD43m0n}>Hack w0rld_d34m0n</Button>
|
<Button onClick={hackW0r1dD43m0n}>Hack w0rld_d34m0n</Button>
|
||||||
|
<Button onClick={() => setError(true)}>Throw Error</Button>
|
||||||
</AccordionDetails>
|
</AccordionDetails>
|
||||||
</Accordion>
|
</Accordion>
|
||||||
);
|
);
|
||||||
|
@ -1,29 +1,58 @@
|
|||||||
import React, { ErrorInfo } from "react";
|
import React, { ErrorInfo } from "react";
|
||||||
|
|
||||||
|
import { IErrorData, getErrorForDisplay } from "../utils/ErrorHelper";
|
||||||
import { RecoveryRoot } from "./React/RecoveryRoot";
|
import { RecoveryRoot } from "./React/RecoveryRoot";
|
||||||
import { IRouter } from "./Router";
|
import { IRouter, Page } from "./Router";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
router: IRouter;
|
router: IRouter;
|
||||||
softReset: () => void;
|
softReset: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
error?: Error;
|
||||||
|
errorInfo?: React.ErrorInfo;
|
||||||
|
page?: Page;
|
||||||
|
hasError: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ErrorBoundary extends React.Component<IProps, IState> {
|
||||||
|
state: IState
|
||||||
|
|
||||||
export class ErrorBoundary extends React.Component<IProps> {
|
|
||||||
state: { hasError: boolean }
|
|
||||||
constructor(props: IProps) {
|
constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = { hasError: false };
|
this.state = { hasError: false } as IState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reset(): void {
|
||||||
|
this.setState( { hasError: false } as IState);
|
||||||
|
}
|
||||||
|
|
||||||
componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
|
componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
|
||||||
|
this.setState({
|
||||||
|
errorInfo,
|
||||||
|
page: this.props.router.page(),
|
||||||
|
});
|
||||||
console.error(error, errorInfo);
|
console.error(error, errorInfo);
|
||||||
}
|
}
|
||||||
render(): React.ReactNode {
|
render(): React.ReactNode {
|
||||||
if (this.state.hasError) {
|
if (this.state.hasError) {
|
||||||
return <RecoveryRoot router={this.props.router} softReset={this.props.softReset} />;
|
let errorData: IErrorData | undefined;
|
||||||
|
if (this.state.error) {
|
||||||
|
try {
|
||||||
|
// We don't want recursive errors, so in case this fails, it's in a try catch.
|
||||||
|
errorData = getErrorForDisplay(this.state.error, this.state.errorInfo, this.state.page);
|
||||||
|
} catch (ex) {
|
||||||
|
console.error(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return <RecoveryRoot router={this.props.router} softReset={this.props.softReset}
|
||||||
|
errorData={errorData} resetError={() => this.reset()} />;
|
||||||
}
|
}
|
||||||
return this.props.children;
|
return this.props.children;
|
||||||
}
|
}
|
||||||
static getDerivedStateFromError(): { hasError: true} {
|
static getDerivedStateFromError(error: Error): IState {
|
||||||
return { hasError: true };
|
return { hasError: true, error};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
32
src/ui/React/DeleteGameButton.tsx
Normal file
32
src/ui/React/DeleteGameButton.tsx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { deleteGame } from "../../db";
|
||||||
|
import { ConfirmationModal } from "./ConfirmationModal";
|
||||||
|
import Button from "@mui/material/Button";
|
||||||
|
import { Tooltip } from '@mui/material';
|
||||||
|
|
||||||
|
import DeleteIcon from '@mui/icons-material/Delete';
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
color?: "primary" | "warning" | "error";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DeleteGameButton({ color = "primary" }: IProps): React.ReactElement {
|
||||||
|
const [modalOpened, setModalOpened] = useState(false);
|
||||||
|
|
||||||
|
return (<>
|
||||||
|
<Tooltip title="This will permanently delete your local save game. Did you export it before?">
|
||||||
|
<Button startIcon={<DeleteIcon />} color={color} onClick={() => setModalOpened(true)}>Delete Save</Button>
|
||||||
|
</Tooltip>
|
||||||
|
<ConfirmationModal
|
||||||
|
onConfirm={() => {
|
||||||
|
setModalOpened(false);
|
||||||
|
deleteGame()
|
||||||
|
.then(() => setTimeout(() => location.reload(), 1000))
|
||||||
|
.catch((r) => console.error(`Could not delete game: ${r}`));
|
||||||
|
}}
|
||||||
|
open={modalOpened}
|
||||||
|
onClose={() => setModalOpened(false)}
|
||||||
|
confirmationText={"Really delete your game? (It's permanent!)"}
|
||||||
|
/>
|
||||||
|
</>)
|
||||||
|
}
|
@ -21,6 +21,7 @@ import TextField from "@mui/material/TextField";
|
|||||||
|
|
||||||
import DownloadIcon from "@mui/icons-material/Download";
|
import DownloadIcon from "@mui/icons-material/Download";
|
||||||
import UploadIcon from "@mui/icons-material/Upload";
|
import UploadIcon from "@mui/icons-material/Upload";
|
||||||
|
import SaveIcon from '@mui/icons-material/Save';
|
||||||
|
|
||||||
import { FileDiagnosticModal } from "../../Diagnostic/FileDiagnosticModal";
|
import { FileDiagnosticModal } from "../../Diagnostic/FileDiagnosticModal";
|
||||||
import { dialogBoxCreate } from "./DialogBox";
|
import { dialogBoxCreate } from "./DialogBox";
|
||||||
@ -31,9 +32,11 @@ import { StyleEditorModal } from "./StyleEditorModal";
|
|||||||
import { SnackbarEvents } from "./Snackbar";
|
import { SnackbarEvents } from "./Snackbar";
|
||||||
|
|
||||||
import { Settings } from "../../Settings/Settings";
|
import { Settings } from "../../Settings/Settings";
|
||||||
import { save, deleteGame } from "../../db";
|
import { save } from "../../db";
|
||||||
import { formatTime } from "../../utils/helpers/formatTime";
|
import { formatTime } from "../../utils/helpers/formatTime";
|
||||||
import { OptionSwitch } from "./OptionSwitch";
|
import { OptionSwitch } from "./OptionSwitch";
|
||||||
|
import { DeleteGameButton } from "./DeleteGameButton";
|
||||||
|
import { SoftResetButton } from "./SoftResetButton";
|
||||||
|
|
||||||
const useStyles = makeStyles((theme: Theme) =>
|
const useStyles = makeStyles((theme: Theme) =>
|
||||||
createStyles({
|
createStyles({
|
||||||
@ -71,10 +74,8 @@ export function GameOptionsRoot(props: IProps): React.ReactElement {
|
|||||||
const [timestampFormat, setTimestampFormat] = useState(Settings.TimestampsFormat);
|
const [timestampFormat, setTimestampFormat] = useState(Settings.TimestampsFormat);
|
||||||
const [locale, setLocale] = useState(Settings.Locale);
|
const [locale, setLocale] = useState(Settings.Locale);
|
||||||
const [diagnosticOpen, setDiagnosticOpen] = useState(false);
|
const [diagnosticOpen, setDiagnosticOpen] = useState(false);
|
||||||
const [deleteGameOpen, setDeleteOpen] = useState(false);
|
|
||||||
const [themeEditorOpen, setThemeEditorOpen] = useState(false);
|
const [themeEditorOpen, setThemeEditorOpen] = useState(false);
|
||||||
const [styleEditorOpen, setStyleEditorOpen] = useState(false);
|
const [styleEditorOpen, setStyleEditorOpen] = useState(false);
|
||||||
const [softResetOpen, setSoftResetOpen] = useState(false);
|
|
||||||
const [importSaveOpen, setImportSaveOpen] = useState(false);
|
const [importSaveOpen, setImportSaveOpen] = useState(false);
|
||||||
const [importData, setImportData] = useState<ImportData | null>(null);
|
const [importData, setImportData] = useState<ImportData | null>(null);
|
||||||
|
|
||||||
@ -194,14 +195,6 @@ export function GameOptionsRoot(props: IProps): React.ReactElement {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function doSoftReset(): void {
|
|
||||||
if (!Settings.SuppressBuyAugmentationConfirmation) {
|
|
||||||
setSoftResetOpen(true);
|
|
||||||
} else {
|
|
||||||
props.softReset();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classes.root} style={{ width: "90%" }}>
|
<div className={classes.root} style={{ width: "90%" }}>
|
||||||
<Typography variant="h4" gutterBottom>
|
<Typography variant="h4" gutterBottom>
|
||||||
@ -531,19 +524,19 @@ export function GameOptionsRoot(props: IProps): React.ReactElement {
|
|||||||
</Grid>
|
</Grid>
|
||||||
<Box sx={{ display: 'grid', width: 'fit-content', height: 'fit-content' }}>
|
<Box sx={{ display: 'grid', width: 'fit-content', height: 'fit-content' }}>
|
||||||
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr 1fr' }}>
|
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr 1fr' }}>
|
||||||
<Button onClick={() => props.save()}>Save Game</Button>
|
<Button onClick={() => props.save()} startIcon={<SaveIcon />} >
|
||||||
<Button onClick={() => setDeleteOpen(true)}>Delete Game</Button>
|
Save Game
|
||||||
|
</Button>
|
||||||
|
<DeleteGameButton />
|
||||||
</Box>
|
</Box>
|
||||||
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr 1fr' }}>
|
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr 1fr' }}>
|
||||||
<Tooltip title={<Typography>Export your game to a text file.</Typography>}>
|
<Tooltip title={<Typography>Export your game to a text file.</Typography>}>
|
||||||
<Button onClick={() => props.export()}>
|
<Button onClick={() => props.export()} startIcon={<DownloadIcon />}>
|
||||||
<DownloadIcon color="primary" />
|
|
||||||
Export Game
|
Export Game
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip title={<Typography>Import your game from a text file.<br />This will <strong>overwrite</strong> your current game. Back it up first!</Typography>}>
|
<Tooltip title={<Typography>Import your game from a text file.<br />This will <strong>overwrite</strong> your current game. Back it up first!</Typography>}>
|
||||||
<Button onClick={startImport}>
|
<Button onClick={startImport} startIcon={<UploadIcon />}>
|
||||||
<UploadIcon color="primary" />
|
|
||||||
Import Game
|
Import Game
|
||||||
<input ref={importInput} id="import-game-file-selector" type="file" hidden onChange={onImport} />
|
<input ref={importInput} id="import-game-file-selector" type="file" hidden onChange={onImport} />
|
||||||
</Button>
|
</Button>
|
||||||
@ -587,21 +580,7 @@ export function GameOptionsRoot(props: IProps): React.ReactElement {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Box>
|
</Box>
|
||||||
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr 1fr' }}>
|
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr 1fr' }}>
|
||||||
<Tooltip
|
<SoftResetButton noConfirmation={Settings.SuppressBuyAugmentationConfirmation} onTriggered={props.softReset} />
|
||||||
title={
|
|
||||||
<Typography>
|
|
||||||
Perform a soft reset. Resets everything as if you had just purchased an Augmentation.
|
|
||||||
</Typography>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Button onClick={doSoftReset}>Soft Reset</Button>
|
|
||||||
</Tooltip>
|
|
||||||
<ConfirmationModal
|
|
||||||
open={softResetOpen}
|
|
||||||
onClose={() => setSoftResetOpen(false)}
|
|
||||||
onConfirm={props.softReset}
|
|
||||||
confirmationText={"This will perform the same action as installing Augmentations, are you sure?"}
|
|
||||||
/>
|
|
||||||
<Tooltip
|
<Tooltip
|
||||||
title={
|
title={
|
||||||
<Typography>
|
<Typography>
|
||||||
@ -640,17 +619,6 @@ export function GameOptionsRoot(props: IProps): React.ReactElement {
|
|||||||
</Box>
|
</Box>
|
||||||
</Grid>
|
</Grid>
|
||||||
<FileDiagnosticModal open={diagnosticOpen} onClose={() => setDiagnosticOpen(false)} />
|
<FileDiagnosticModal open={diagnosticOpen} onClose={() => setDiagnosticOpen(false)} />
|
||||||
<ConfirmationModal
|
|
||||||
onConfirm={() => {
|
|
||||||
setDeleteOpen(false);
|
|
||||||
deleteGame()
|
|
||||||
.then(() => setTimeout(() => location.reload(), 1000))
|
|
||||||
.catch((r) => console.error(`Could not delete game: ${r}`));
|
|
||||||
}}
|
|
||||||
open={deleteGameOpen}
|
|
||||||
onClose={() => setDeleteOpen(false)}
|
|
||||||
confirmationText={"Really delete your game? (It's permanent!)"}
|
|
||||||
/>
|
|
||||||
<ThemeEditorModal open={themeEditorOpen} onClose={() => setThemeEditorOpen(false)} />
|
<ThemeEditorModal open={themeEditorOpen} onClose={() => setThemeEditorOpen(false)} />
|
||||||
<StyleEditorModal open={styleEditorOpen} onClose={() => setStyleEditorOpen(false)} />
|
<StyleEditorModal open={styleEditorOpen} onClose={() => setStyleEditorOpen(false)} />
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,11 +1,16 @@
|
|||||||
import React from "react";
|
import React, { useEffect } from "react";
|
||||||
import Typography from "@mui/material/Typography";
|
|
||||||
import Link from "@mui/material/Link";
|
import { Typography, Link, Button, ButtonGroup, Tooltip, Box, Paper, TextField } from "@mui/material";
|
||||||
import Button from "@mui/material/Button";
|
|
||||||
import { Settings } from "../../Settings/Settings";
|
import { Settings } from "../../Settings/Settings";
|
||||||
import { load } from "../../db";
|
import { load } from "../../db";
|
||||||
import { IRouter } from "../Router";
|
import { IRouter } from "../Router";
|
||||||
import { download } from "../../SaveObject";
|
import { download } from "../../SaveObject";
|
||||||
|
import { IErrorData, newIssueUrl } from "../../utils/ErrorHelper";
|
||||||
|
import { DeleteGameButton } from "./DeleteGameButton";
|
||||||
|
import { SoftResetButton } from "./SoftResetButton";
|
||||||
|
|
||||||
|
import DirectionsRunIcon from '@mui/icons-material/DirectionsRun';
|
||||||
|
import GitHubIcon from "@mui/icons-material/GitHub";
|
||||||
|
|
||||||
export let RecoveryMode = false;
|
export let RecoveryMode = false;
|
||||||
|
|
||||||
@ -16,40 +21,79 @@ export function ActivateRecoveryMode(): void {
|
|||||||
interface IProps {
|
interface IProps {
|
||||||
router: IRouter;
|
router: IRouter;
|
||||||
softReset: () => void;
|
softReset: () => void;
|
||||||
|
errorData?: IErrorData;
|
||||||
|
resetError?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RecoveryRoot({ router, softReset }: IProps): React.ReactElement {
|
export function RecoveryRoot({ router, softReset, errorData, resetError }: IProps): React.ReactElement {
|
||||||
function recover(): void {
|
function recover(): void {
|
||||||
|
if (resetError) resetError();
|
||||||
RecoveryMode = false;
|
RecoveryMode = false;
|
||||||
router.toTerminal();
|
router.toTerminal();
|
||||||
}
|
}
|
||||||
Settings.AutosaveInterval = 0;
|
Settings.AutosaveInterval = 0;
|
||||||
load().then((content) => {
|
|
||||||
download("RECOVERY.json", content);
|
useEffect(() => {
|
||||||
});
|
load().then((content) => {
|
||||||
|
const epochTime = Math.round(Date.now() / 1000);
|
||||||
|
const filename = `RECOVERY_BITBURNER_${epochTime}.json`;
|
||||||
|
download(filename, content);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Box sx={{ padding: "8px 16px", minHeight: "100vh", maxWidth: '1200px', boxSizing: "border-box",}}>
|
||||||
<Typography variant="h3">RECOVERY MODE ACTIVATED</Typography>
|
<Typography variant="h3">RECOVERY MODE ACTIVATED</Typography>
|
||||||
<Typography>
|
<Typography>
|
||||||
There was an error loading your save file and the game went into recovery mode. In this mode saving is disabled
|
There was an error with your save file and the game went into recovery mode. In this mode saving is disabled
|
||||||
and the game will automatically export your save file (to prevent corruption).
|
and the game will automatically export your save file (to prevent corruption).
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography>At this point it is recommended to alert a developer.</Typography>
|
<Typography>At this point it is recommended to alert a developer.</Typography>
|
||||||
<Link href="https://github.com/danielyxie/bitburner/issues/new" target="_blank">
|
<Typography>
|
||||||
<Typography>File an issue on github</Typography>
|
<Link href={errorData?.issueUrl ?? newIssueUrl} target="_blank">File an issue on github</Link>
|
||||||
</Link>
|
</Typography>
|
||||||
<Link href="https://www.reddit.com/r/Bitburner/" target="_blank">
|
<Typography>
|
||||||
<Typography>Make a reddit post</Typography>
|
<Link href="https://www.reddit.com/r/Bitburner/" target="_blank">Make a reddit post</Link>
|
||||||
</Link>
|
</Typography>
|
||||||
<Link href="https://discord.gg/TFc3hKD" target="_blank">
|
<Typography>
|
||||||
<Typography>Post in the #bug-report channel on Discord.</Typography>
|
<Link href="https://discord.gg/TFc3hKD" target="_blank">Post in the #bug-report channel on Discord.</Link>
|
||||||
</Link>
|
</Typography>
|
||||||
<Typography>Please include your save file.</Typography>
|
<Typography>Please include your save file.</Typography>
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
<Typography>You can disable recovery mode now. But chances are the game will not work correctly.</Typography>
|
<Typography>You can disable recovery mode now. But chances are the game will not work correctly.</Typography>
|
||||||
<Button onClick={recover}>DISABLE RECOVERY MODE</Button>
|
<ButtonGroup sx={{ my: 2 }}>
|
||||||
<Button onClick={softReset}>PERFORM SOFT RESET</Button>
|
<Tooltip title="Disables the recovery mode & attempt to head back to the terminal page. This may or may not work. Ensure you have saved the recovery file.">
|
||||||
</>
|
<Button onClick={recover} startIcon={<DirectionsRunIcon />}>Disable Recovery Mode</Button>
|
||||||
|
</Tooltip>
|
||||||
|
<SoftResetButton color="warning" onTriggered={softReset} />
|
||||||
|
<DeleteGameButton color="error" />
|
||||||
|
</ButtonGroup>
|
||||||
|
|
||||||
|
{errorData && (
|
||||||
|
<Paper sx={{ px: 2, pt: 1, pb: 2, mt: 2}}>
|
||||||
|
<Typography variant="h5">
|
||||||
|
{errorData.title}
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{ my: 2 }}>
|
||||||
|
<TextField
|
||||||
|
label="Bug Report Text"
|
||||||
|
value={errorData.body}
|
||||||
|
variant="outlined"
|
||||||
|
color="secondary"
|
||||||
|
multiline
|
||||||
|
fullWidth
|
||||||
|
rows={12}
|
||||||
|
sx={{ "& .MuiOutlinedInput-root": { color: Settings.theme.secondary }}} />
|
||||||
|
</Box>
|
||||||
|
<Tooltip title="Submitting an issue to GitHub really help us improve the game!">
|
||||||
|
<Button component={Link} startIcon={<GitHubIcon />} color="info" sx={{ px: 2 }}
|
||||||
|
href={errorData.issueUrl ?? newIssueUrl} target={"_blank"} >
|
||||||
|
Submit Issue to GitHub
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</Paper>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
36
src/ui/React/SoftResetButton.tsx
Normal file
36
src/ui/React/SoftResetButton.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
import { ConfirmationModal } from "./ConfirmationModal";
|
||||||
|
import Button from "@mui/material/Button";
|
||||||
|
import { Tooltip } from '@mui/material';
|
||||||
|
import RestartAltIcon from '@mui/icons-material/RestartAlt';
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
color?: "primary" | "warning" | "error";
|
||||||
|
noConfirmation?: boolean;
|
||||||
|
onTriggered: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SoftResetButton({ color = "primary", noConfirmation = false, onTriggered }: IProps): React.ReactElement {
|
||||||
|
const [modalOpened, setModalOpened] = useState(false);
|
||||||
|
|
||||||
|
function handleButtonClick(): void {
|
||||||
|
if (noConfirmation) {
|
||||||
|
onTriggered();
|
||||||
|
} else {
|
||||||
|
setModalOpened(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (<>
|
||||||
|
<Tooltip title="Perform a soft reset. Resets everything as if you had just purchased an Augmentation.">
|
||||||
|
<Button startIcon={<RestartAltIcon />} color={color} onClick={handleButtonClick}>Soft Reset</Button>
|
||||||
|
</Tooltip>
|
||||||
|
<ConfirmationModal
|
||||||
|
onConfirm={onTriggered}
|
||||||
|
open={modalOpened}
|
||||||
|
onClose={() => setModalOpened(false)}
|
||||||
|
confirmationText={"This will perform the same action as installing Augmentations, are you sure?"}
|
||||||
|
/>
|
||||||
|
</>)
|
||||||
|
}
|
135
src/utils/ErrorHelper.ts
Normal file
135
src/utils/ErrorHelper.ts
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { Page } from "../ui/Router";
|
||||||
|
import { hash } from "../hash/hash";
|
||||||
|
import { CONSTANTS } from "../Constants";
|
||||||
|
|
||||||
|
enum GameEnv {
|
||||||
|
Production,
|
||||||
|
Development,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Platform {
|
||||||
|
Browser,
|
||||||
|
Steam,
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GameVersion {
|
||||||
|
version: string;
|
||||||
|
hash: string;
|
||||||
|
|
||||||
|
toDisplay: () => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BrowserFeatures {
|
||||||
|
userAgent: string;
|
||||||
|
language: string;
|
||||||
|
cookiesEnabled: boolean;
|
||||||
|
doNotTrack: string | null;
|
||||||
|
indexedDb: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IErrorMetadata {
|
||||||
|
error: Error;
|
||||||
|
errorInfo?: React.ErrorInfo;
|
||||||
|
page?: Page;
|
||||||
|
|
||||||
|
environment: GameEnv;
|
||||||
|
platform: Platform;
|
||||||
|
version: GameVersion;
|
||||||
|
features: BrowserFeatures;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IErrorData {
|
||||||
|
metadata: IErrorMetadata;
|
||||||
|
|
||||||
|
title: string;
|
||||||
|
body: string;
|
||||||
|
|
||||||
|
features: string;
|
||||||
|
fileName?: string;
|
||||||
|
|
||||||
|
issueUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const newIssueUrl = `https://github.com/danielyxie/bitburner/issues/new`;
|
||||||
|
|
||||||
|
function getErrorMetadata(error: Error, errorInfo?: React.ErrorInfo, page?: Page): IErrorMetadata {
|
||||||
|
const isElectron = navigator.userAgent.toLowerCase().indexOf(" electron/") > -1;
|
||||||
|
const env = process.env.NODE_ENV === "development" ? GameEnv.Development : GameEnv.Production;
|
||||||
|
const version: GameVersion = {
|
||||||
|
version: CONSTANTS.VersionString,
|
||||||
|
hash: hash(),
|
||||||
|
toDisplay: () => `v${CONSTANTS.VersionString} (${hash()})`,
|
||||||
|
}
|
||||||
|
const features: BrowserFeatures = {
|
||||||
|
userAgent: navigator.userAgent,
|
||||||
|
|
||||||
|
language: navigator.language,
|
||||||
|
cookiesEnabled: navigator.cookieEnabled,
|
||||||
|
doNotTrack: navigator.doNotTrack,
|
||||||
|
indexedDb: (!!window.indexedDB),
|
||||||
|
}
|
||||||
|
const metadata: IErrorMetadata = {
|
||||||
|
platform: isElectron ? Platform.Steam : Platform.Browser,
|
||||||
|
environment: env,
|
||||||
|
version, features,
|
||||||
|
error, errorInfo, page,
|
||||||
|
}
|
||||||
|
return metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getErrorForDisplay(error: Error, errorInfo?: React.ErrorInfo, page?: Page): IErrorData {
|
||||||
|
const metadata = getErrorMetadata(error, errorInfo, page);
|
||||||
|
const fileName = (metadata.error as any).fileName;
|
||||||
|
const features = `lang=${metadata.features.language} cookiesEnabled=${metadata.features.cookiesEnabled.toString()}` +
|
||||||
|
` doNotTrack=${metadata.features.doNotTrack} indexedDb=${metadata.features.indexedDb.toString()}`;
|
||||||
|
|
||||||
|
const title = `${metadata.error.name}: ${metadata.error.message}${metadata.page && ` (at "${Page[metadata.page]}")`}`;
|
||||||
|
const body = `
|
||||||
|
## ${title}
|
||||||
|
|
||||||
|
### How did this happen?
|
||||||
|
|
||||||
|
Please fill this information with details if relevant.
|
||||||
|
|
||||||
|
- [ ] Save file
|
||||||
|
- [ ] Minimal scripts to reproduce the issue
|
||||||
|
- [ ] Steps to reproduce
|
||||||
|
|
||||||
|
### Environment
|
||||||
|
|
||||||
|
* Error: ${metadata.error?.toString() ?? 'n/a'}
|
||||||
|
* Page: ${metadata.page ? Page[metadata.page] : 'n/a'}
|
||||||
|
* Version: ${metadata.version.toDisplay()}
|
||||||
|
* Environment: ${GameEnv[metadata.environment]}
|
||||||
|
* Platform: ${Platform[metadata.platform]}
|
||||||
|
* UserAgent: ${navigator.userAgent}
|
||||||
|
* Features: ${features}
|
||||||
|
* Source: ${fileName ?? 'n/a'}
|
||||||
|
|
||||||
|
${metadata.environment === GameEnv.Development ? `
|
||||||
|
### Stack Trace
|
||||||
|
\`\`\`
|
||||||
|
${metadata.errorInfo?.componentStack.toString().trim()}
|
||||||
|
\`\`\`
|
||||||
|
` : ''}
|
||||||
|
### Save
|
||||||
|
\`\`\`
|
||||||
|
Copy your save here if possible
|
||||||
|
\`\`\`
|
||||||
|
`.trim();
|
||||||
|
|
||||||
|
const issueUrl = `${newIssueUrl}?title=${encodeURIComponent(title)}&body=${encodeURIComponent(body)}`;
|
||||||
|
|
||||||
|
const data: IErrorData = {
|
||||||
|
metadata,
|
||||||
|
fileName,
|
||||||
|
features,
|
||||||
|
title,
|
||||||
|
body,
|
||||||
|
issueUrl,
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user