From 2d6531b8dc6bb69888c37ff1155aff1c849ea5ed Mon Sep 17 00:00:00 2001 From: Kira Pilot Date: Thu, 19 May 2022 15:16:05 +0000 Subject: [PATCH 1/6] added error boundary and error ui components --- .vscode/settings.json | 5 +- site/package.json | 1 + site/src/app.tsx | 17 ++-- site/src/components/CodeBlock/CodeBlock.tsx | 28 +++++-- site/src/components/CopyButton/CopyButton.tsx | 25 ++++-- .../ErrorBoundary/ErrorBoundary.tsx | 31 +++++++ .../RuntimeErrorState/ReportButtons.tsx | 70 ++++++++++++++++ .../RuntimeErrorState/RuntimeErrorReport.tsx | 80 +++++++++++++++++++ .../RuntimeErrorState/RuntimeErrorState.tsx | 74 +++++++++++++++++ site/src/components/Section/Section.tsx | 14 +++- site/src/components/Stack/Stack.tsx | 2 + site/src/theme/palettes.ts | 3 + 12 files changed, 327 insertions(+), 23 deletions(-) create mode 100644 site/src/components/ErrorBoundary/ErrorBoundary.tsx create mode 100644 site/src/components/RuntimeErrorState/ReportButtons.tsx create mode 100644 site/src/components/RuntimeErrorState/RuntimeErrorReport.tsx create mode 100644 site/src/components/RuntimeErrorState/RuntimeErrorState.tsx diff --git a/.vscode/settings.json b/.vscode/settings.json index a04dc17791f5f..3564a8ef964df 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -51,6 +51,7 @@ "rpty", "sdkproto", "Signup", + "sourcemapped", "stretchr", "TCGETS", "tcpip", @@ -76,7 +77,7 @@ }, { "match": "provisionerd/proto/provisionerd.proto", - "cmd": "make provisionerd/proto/provisionerd.pb.go", + "cmd": "make provisionerd/proto/provisionerd.pb.go" } ] }, @@ -104,5 +105,5 @@ }, // We often use a version of TypeScript that's ahead of the version shipped // with VS Code. - "typescript.tsdk": "./site/node_modules/typescript/lib", + "typescript.tsdk": "./site/node_modules/typescript/lib" } diff --git a/site/package.json b/site/package.json index 50ad5f0c74a76..00b84da9d61b3 100644 --- a/site/package.json +++ b/site/package.json @@ -41,6 +41,7 @@ "react": "17.0.2", "react-dom": "17.0.2", "react-router-dom": "6.3.0", + "sourcemapped-stacktrace": "1.1.11", "swr": "1.2.2", "uuid": "8.3.2", "xstate": "4.32.1", diff --git a/site/src/app.tsx b/site/src/app.tsx index a4f0bfa5b5b69..dd210ef8f022a 100644 --- a/site/src/app.tsx +++ b/site/src/app.tsx @@ -4,6 +4,7 @@ import React from "react" import { BrowserRouter as Router } from "react-router-dom" import { SWRConfig } from "swr" import { AppRouter } from "./AppRouter" +import { ErrorBoundary } from "./components/ErrorBoundary/ErrorBoundary" import { GlobalSnackbar } from "./components/GlobalSnackbar/GlobalSnackbar" import { dark } from "./theme" import "./theme/globalFonts" @@ -30,13 +31,15 @@ export const App: React.FC = () => { }, }} > - - - - - - - + + + + + + + + + ) diff --git a/site/src/components/CodeBlock/CodeBlock.tsx b/site/src/components/CodeBlock/CodeBlock.tsx index 278315980237d..37a9e2b4e1b33 100644 --- a/site/src/components/CodeBlock/CodeBlock.tsx +++ b/site/src/components/CodeBlock/CodeBlock.tsx @@ -5,20 +5,30 @@ import { combineClasses } from "../../util/combineClasses" export interface CodeBlockProps { lines: string[] + ctas?: React.ReactElement[] className?: string } -export const CodeBlock: React.FC = ({ lines, className = "" }) => { +export const CodeBlock: React.FC = ({ lines, ctas, className = "" }) => { const styles = useStyles() return ( -
- {lines.map((line, idx) => ( -
- {line} + <> +
+ {lines.map((line, idx) => ( +
+ {line} +
+ ))} +
+ {ctas && ctas.length && ( +
+ {ctas.map((cta, i) => { + return {cta} + })}
- ))} -
+ )} + ) } @@ -36,4 +46,8 @@ const useStyles = makeStyles((theme) => ({ line: { whiteSpace: "pre-wrap", }, + ctaBar: { + display: "flex", + justifyContent: "space-between", + }, })) diff --git a/site/src/components/CopyButton/CopyButton.tsx b/site/src/components/CopyButton/CopyButton.tsx index 0ddd865f27023..fd16ed7dd487d 100644 --- a/site/src/components/CopyButton/CopyButton.tsx +++ b/site/src/components/CopyButton/CopyButton.tsx @@ -3,17 +3,25 @@ import { makeStyles } from "@material-ui/core/styles" import Tooltip from "@material-ui/core/Tooltip" import Check from "@material-ui/icons/Check" import React, { useState } from "react" +import { combineClasses } from "../../util/combineClasses" import { FileCopyIcon } from "../Icons/FileCopyIcon" interface CopyButtonProps { text: string - className?: string + ctaCopy?: string + wrapperClassName?: string + buttonClassName?: string } /** * Copy button used inside the CodeBlock component internally */ -export const CopyButton: React.FC = ({ className = "", text }) => { +export const CopyButton: React.FC = ({ + text, + ctaCopy, + wrapperClassName = "", + buttonClassName = "", +}) => { const styles = useStyles() const [isCopied, setIsCopied] = useState(false) @@ -36,9 +44,16 @@ export const CopyButton: React.FC = ({ className = "", text }) return ( -
-
diff --git a/site/src/components/ErrorBoundary/ErrorBoundary.tsx b/site/src/components/ErrorBoundary/ErrorBoundary.tsx new file mode 100644 index 0000000000000..1e54293b8ee87 --- /dev/null +++ b/site/src/components/ErrorBoundary/ErrorBoundary.tsx @@ -0,0 +1,31 @@ +import React from "react" +import { RuntimeErrorState } from "../RuntimeErrorState/RuntimeErrorState" + +type ErrorBoundaryProps = Record + +interface ErrorBoundaryState { + error: Error | null +} + +/** + * Our app's Error Boundary + * Read more about React Error Boundaries: https://reactjs.org/docs/error-boundaries.html + */ +export class ErrorBoundary extends React.Component { + constructor(props: ErrorBoundaryProps) { + super(props) + this.state = { error: null } + } + + static getDerivedStateFromError(error: Error): { error: Error } { + return { error } + } + + render(): React.ReactNode { + if (this.state.error) { + return + } + + return this.props.children + } +} diff --git a/site/src/components/RuntimeErrorState/ReportButtons.tsx b/site/src/components/RuntimeErrorState/ReportButtons.tsx new file mode 100644 index 0000000000000..88466ab8944f7 --- /dev/null +++ b/site/src/components/RuntimeErrorState/ReportButtons.tsx @@ -0,0 +1,70 @@ +import Button from "@material-ui/core/Button" +import { makeStyles } from "@material-ui/core/styles" +import RefreshIcon from "@material-ui/icons/Refresh" +import React from "react" +import { CopyButton } from "../CopyButton/CopyButton" + +const Language = { + reloadApp: "Reload Application", + copyReport: "Copy Report", +} + +/** + * A wrapper component for a full-width copy button + */ +const CopyStackButton = ({ text }: { text: string }): React.ReactElement => { + const styles = useStyles() + + return ( + + ) +} + +/** + * A button that reloads our application + */ +const ReloadAppButton = (): React.ReactElement => { + const styles = useStyles() + + return ( + + ) +} + +/** + * createCtas generates an array of buttons to be used with our error boundary UI + */ +export const createCtas = (codeBlock: string[]): React.ReactElement[] => { + // REMARK: we don't have to worry about key order changing + // eslint-disable-next-line react/jsx-key + return [, ] +} + +const useStyles = makeStyles((theme) => ({ + buttonWrapper: { + marginTop: theme.spacing(1), + marginLeft: 0, + flex: theme.spacing(1), + textTransform: "uppercase", + }, + + copyButton: { + width: "100%", + marginRight: theme.spacing(1), + backgroundColor: theme.palette.primary.main, + textTransform: "uppercase", + }, +})) diff --git a/site/src/components/RuntimeErrorState/RuntimeErrorReport.tsx b/site/src/components/RuntimeErrorState/RuntimeErrorReport.tsx new file mode 100644 index 0000000000000..e9687d6be93d1 --- /dev/null +++ b/site/src/components/RuntimeErrorState/RuntimeErrorReport.tsx @@ -0,0 +1,80 @@ +import { makeStyles } from "@material-ui/core/styles" +import React from "react" +import { CodeBlock } from "../CodeBlock/CodeBlock" +import { createCtas } from "./ReportButtons" + +const Language = { + reportLoading: "Generating crash report...", +} + +interface ReportState { + error: Error + mappedStack: string[] | null +} + +interface StackTraceAvailableMsg { + type: "stackTraceAvailable" + stackTrace: string[] +} + +/** + * stackTraceUnavailable is a Msg describing a stack trace not being available + */ +export const stackTraceUnavailable = { + type: "stackTraceUnavailable", +} as const + +type ReportMessage = StackTraceAvailableMsg | typeof stackTraceUnavailable + +export const stackTraceAvailable = (stackTrace: string[]): StackTraceAvailableMsg => { + return { + type: "stackTraceAvailable", + stackTrace, + } +} + +const setStackTrace = (model: ReportState, mappedStack: string[]): ReportState => { + return { + ...model, + mappedStack, + } +} + +export const reducer = (model: ReportState, msg: ReportMessage): ReportState => { + switch (msg.type) { + case "stackTraceAvailable": + return setStackTrace(model, msg.stackTrace) + case "stackTraceUnavailable": + return setStackTrace(model, ["Unable to get stack trace"]) + } +} + +/** + * A code block component that contains the error stack resulting from an error boundary trigger + */ +export const RuntimeErrorReport = ({ error, mappedStack }: ReportState): React.ReactElement => { + const styles = useStyles() + + if (!mappedStack) { + return + } + + const codeBlock = [ + "======================= STACK TRACE ========================", + "", + error.message, + ...mappedStack, + "", + "============================================================", + ] + + return +} + +const useStyles = makeStyles(() => ({ + codeBlock: { + minHeight: "auto", + userSelect: "all", + width: "100%", + }, +})) diff --git a/site/src/components/RuntimeErrorState/RuntimeErrorState.tsx b/site/src/components/RuntimeErrorState/RuntimeErrorState.tsx new file mode 100644 index 0000000000000..3c44a4f2d7eb4 --- /dev/null +++ b/site/src/components/RuntimeErrorState/RuntimeErrorState.tsx @@ -0,0 +1,74 @@ +import Box from "@material-ui/core/Box" +import { makeStyles } from "@material-ui/core/styles" +import ErrorOutlineIcon from "@material-ui/icons/ErrorOutline" +import React, { useEffect, useReducer } from "react" +import { mapStackTrace } from "sourcemapped-stacktrace" +import { Margins } from "../Margins/Margins" +import { Section } from "../Section/Section" +import { reducer, RuntimeErrorReport, stackTraceAvailable, stackTraceUnavailable } from "./RuntimeErrorReport" + +const Language = { + title: "Coder encountered an error", +} + +interface RuntimeErrorStateProps { + error: Error +} + +/** + * A title for our error boundary UI + */ +const ErrorStateTitle = () => { + const styles = useStyles() + + return ( + + + {Language.title} + + ) +} + +/** + * An error UI that is displayed when our error boundary (ErrorBoundary.tsx) is triggered + */ +export const RuntimeErrorState: React.FC = ({ error }) => { + const styles = useStyles() + const [reportState, dispatch] = useReducer(reducer, { error, mappedStack: null }) + + useEffect(() => { + try { + mapStackTrace(error.stack, (mappedStack) => dispatch(stackTraceAvailable(mappedStack))) + } catch { + dispatch(stackTraceUnavailable) + } + }, [error]) + + return ( + + +
}> + +
+
+
+ ) +} + +const useStyles = makeStyles((theme) => ({ + title: { + "& span": { + paddingLeft: theme.spacing(1), + }, + + "& .MuiSvgIcon-root": { + color: theme.palette.error.main, + }, + }, + + reportContainer: { + display: "flex", + justifyContent: "center", + marginTop: theme.spacing(5), + }, +})) diff --git a/site/src/components/Section/Section.tsx b/site/src/components/Section/Section.tsx index 97dc042be944d..22bf0dd101a73 100644 --- a/site/src/components/Section/Section.tsx +++ b/site/src/components/Section/Section.tsx @@ -2,6 +2,7 @@ import { makeStyles } from "@material-ui/core/styles" import { fade } from "@material-ui/core/styles/colorManipulator" import Typography from "@material-ui/core/Typography" import React from "react" +import { combineClasses } from "../../util/combineClasses" import { SectionAction } from "../SectionAction/SectionAction" type SectionLayout = "fixed" | "fluid" @@ -12,15 +13,24 @@ export interface SectionProps { toolbar?: React.ReactNode alert?: React.ReactNode layout?: SectionLayout + className?: string children?: React.ReactNode } type SectionFC = React.FC & { Action: typeof SectionAction } -export const Section: SectionFC = ({ title, description, toolbar, alert, children, layout = "fixed" }) => { +export const Section: SectionFC = ({ + title, + description, + toolbar, + alert, + className = "", + children, + layout = "fixed", +}) => { const styles = useStyles({ layout }) return ( -
+
{(title || description) && (
diff --git a/site/src/components/Stack/Stack.tsx b/site/src/components/Stack/Stack.tsx index ed1015d9815de..82a231dc42a9c 100644 --- a/site/src/components/Stack/Stack.tsx +++ b/site/src/components/Stack/Stack.tsx @@ -14,6 +14,8 @@ const useStyles = makeStyles((theme) => ({ })) export const Stack: React.FC = ({ children, spacing = 2 }) => { + throw new Error("uh oh") + const styles = useStyles({ spacing }) return
{children}
} diff --git a/site/src/theme/palettes.ts b/site/src/theme/palettes.ts index ac740f446e5ae..6c223a4c85312 100644 --- a/site/src/theme/palettes.ts +++ b/site/src/theme/palettes.ts @@ -37,4 +37,7 @@ export const darkPalette: PaletteOptions = { success: { main: "#6BBE00", }, + error: { + main: "#DD4764", + }, } From 59be45a0a14419d95e8e5972fa8ad66216223971 Mon Sep 17 00:00:00 2001 From: Kira Pilot Date: Thu, 19 May 2022 16:10:10 +0000 Subject: [PATCH 2/6] add body txt and standardize btn size --- site/src/components/CodeBlock/CodeBlock.tsx | 2 ++ .../RuntimeErrorState/ReportButtons.tsx | 2 ++ .../RuntimeErrorState/RuntimeErrorState.tsx | 34 +++++++++++++++++-- site/src/components/Stack/Stack.tsx | 2 -- 4 files changed, 35 insertions(+), 5 deletions(-) diff --git a/site/src/components/CodeBlock/CodeBlock.tsx b/site/src/components/CodeBlock/CodeBlock.tsx index 37a9e2b4e1b33..eb5f6face2d62 100644 --- a/site/src/components/CodeBlock/CodeBlock.tsx +++ b/site/src/components/CodeBlock/CodeBlock.tsx @@ -35,6 +35,8 @@ export const CodeBlock: React.FC = ({ lines, ctas, className = " const useStyles = makeStyles((theme) => ({ root: { minHeight: 156, + maxHeight: 240, + overflowY: "scroll", background: theme.palette.background.default, color: theme.palette.text.primary, fontFamily: MONOSPACE_FONT_FAMILY, diff --git a/site/src/components/RuntimeErrorState/ReportButtons.tsx b/site/src/components/RuntimeErrorState/ReportButtons.tsx index 88466ab8944f7..a1192d3524a26 100644 --- a/site/src/components/RuntimeErrorState/ReportButtons.tsx +++ b/site/src/components/RuntimeErrorState/ReportButtons.tsx @@ -59,6 +59,7 @@ const useStyles = makeStyles((theme) => ({ marginLeft: 0, flex: theme.spacing(1), textTransform: "uppercase", + fontSize: theme.typography.button.fontSize, }, copyButton: { @@ -66,5 +67,6 @@ const useStyles = makeStyles((theme) => ({ marginRight: theme.spacing(1), backgroundColor: theme.palette.primary.main, textTransform: "uppercase", + fontSize: theme.typography.button.fontSize, }, })) diff --git a/site/src/components/RuntimeErrorState/RuntimeErrorState.tsx b/site/src/components/RuntimeErrorState/RuntimeErrorState.tsx index 3c44a4f2d7eb4..b78be9b3abbd7 100644 --- a/site/src/components/RuntimeErrorState/RuntimeErrorState.tsx +++ b/site/src/components/RuntimeErrorState/RuntimeErrorState.tsx @@ -2,13 +2,17 @@ import Box from "@material-ui/core/Box" import { makeStyles } from "@material-ui/core/styles" import ErrorOutlineIcon from "@material-ui/icons/ErrorOutline" import React, { useEffect, useReducer } from "react" +import { Link } from "react-router-dom" import { mapStackTrace } from "sourcemapped-stacktrace" import { Margins } from "../Margins/Margins" import { Section } from "../Section/Section" +import { Typography } from "../Typography/Typography" import { reducer, RuntimeErrorReport, stackTraceAvailable, stackTraceUnavailable } from "./RuntimeErrorReport" const Language = { title: "Coder encountered an error", + body: "Please copy the crash log using the button below and", + link: "send it to us.", } interface RuntimeErrorStateProps { @@ -20,7 +24,6 @@ interface RuntimeErrorStateProps { */ const ErrorStateTitle = () => { const styles = useStyles() - return ( @@ -29,6 +32,28 @@ const ErrorStateTitle = () => { ) } +/** + * A description for our error boundary UI + */ +const ErrorStateDescription = () => { + const styles = useStyles() + return ( + + {Language.body}  + { + window.location.href = "mailto:support@coder.com" + e.preventDefault() + }} + className={styles.link} + > + {Language.link} + + + ) +} + /** * An error UI that is displayed when our error boundary (ErrorBoundary.tsx) is triggered */ @@ -47,7 +72,7 @@ export const RuntimeErrorState: React.FC = ({ error }) = return ( -
}> +
} description={}>
@@ -65,7 +90,10 @@ const useStyles = makeStyles((theme) => ({ color: theme.palette.error.main, }, }, - + link: { + textDecoration: "none", + color: theme.palette.primary.main, + }, reportContainer: { display: "flex", justifyContent: "center", diff --git a/site/src/components/Stack/Stack.tsx b/site/src/components/Stack/Stack.tsx index 82a231dc42a9c..ed1015d9815de 100644 --- a/site/src/components/Stack/Stack.tsx +++ b/site/src/components/Stack/Stack.tsx @@ -14,8 +14,6 @@ const useStyles = makeStyles((theme) => ({ })) export const Stack: React.FC = ({ children, spacing = 2 }) => { - throw new Error("uh oh") - const styles = useStyles({ spacing }) return
{children}
} From dfa43fb1a8f0f9e01ca47d144f7ae3317c1c7b48 Mon Sep 17 00:00:00 2001 From: Kira Pilot Date: Thu, 19 May 2022 17:41:08 +0000 Subject: [PATCH 3/6] added story --- site/src/components/CopyButton/CopyButton.tsx | 15 +++++----- .../RuntimeErrorState/RuntimeErrorReport.tsx | 2 +- .../RuntimeErrorState.stories.tsx | 30 +++++++++++++++++++ .../RuntimeErrorState/RuntimeErrorState.tsx | 2 +- .../{ReportButtons.tsx => createCtas.tsx} | 4 +-- site/src/theme/palettes.ts | 3 -- 6 files changed, 42 insertions(+), 14 deletions(-) create mode 100644 site/src/components/RuntimeErrorState/RuntimeErrorState.stories.tsx rename site/src/components/RuntimeErrorState/{ReportButtons.tsx => createCtas.tsx} (94%) diff --git a/site/src/components/CopyButton/CopyButton.tsx b/site/src/components/CopyButton/CopyButton.tsx index fd16ed7dd487d..80b53d1a7e413 100644 --- a/site/src/components/CopyButton/CopyButton.tsx +++ b/site/src/components/CopyButton/CopyButton.tsx @@ -1,4 +1,4 @@ -import Button from "@material-ui/core/Button" +import IconButton from "@material-ui/core/Button" import { makeStyles } from "@material-ui/core/styles" import Tooltip from "@material-ui/core/Tooltip" import Check from "@material-ui/icons/Check" @@ -45,16 +45,14 @@ export const CopyButton: React.FC = ({ return (
- + {isCopied ? : } + {ctaCopy &&
{ctaCopy}
} +
) @@ -80,4 +78,7 @@ const useStyles = makeStyles((theme) => ({ width: 20, height: 20, }, + buttonCopy: { + marginLeft: theme.spacing(1), + }, })) diff --git a/site/src/components/RuntimeErrorState/RuntimeErrorReport.tsx b/site/src/components/RuntimeErrorState/RuntimeErrorReport.tsx index e9687d6be93d1..0ab929d371acb 100644 --- a/site/src/components/RuntimeErrorState/RuntimeErrorReport.tsx +++ b/site/src/components/RuntimeErrorState/RuntimeErrorReport.tsx @@ -1,7 +1,7 @@ import { makeStyles } from "@material-ui/core/styles" import React from "react" import { CodeBlock } from "../CodeBlock/CodeBlock" -import { createCtas } from "./ReportButtons" +import { createCtas } from "./createCtas" const Language = { reportLoading: "Generating crash report...", diff --git a/site/src/components/RuntimeErrorState/RuntimeErrorState.stories.tsx b/site/src/components/RuntimeErrorState/RuntimeErrorState.stories.tsx new file mode 100644 index 0000000000000..5466459d91c3a --- /dev/null +++ b/site/src/components/RuntimeErrorState/RuntimeErrorState.stories.tsx @@ -0,0 +1,30 @@ +import { ComponentMeta, Story } from "@storybook/react" +import React from "react" +import { RuntimeErrorState, RuntimeErrorStateProps } from "./RuntimeErrorState" + +const error = new Error("An error occurred") + +export default { + title: "components/RuntimeErrorState", + component: RuntimeErrorState, + argTypes: { + error: { + defaultValue: error, + }, + }, +} as ComponentMeta + +const Template: Story = (args) => + +export const Errored = Template.bind({}) +Errored.parameters = { + // The RuntimeErrorState is noisy for chromatic, because it renders an actual error + // along with the stacktrace - and the stacktrace includes the full URL of + // scripts in the stack. This is problematic, because every deployment uses + // a different URL, causing the validation to fail. + chromatic: { disableSnapshot: true }, +} + +Errored.args = { + error, +} diff --git a/site/src/components/RuntimeErrorState/RuntimeErrorState.tsx b/site/src/components/RuntimeErrorState/RuntimeErrorState.tsx index b78be9b3abbd7..5d83eedc24e3d 100644 --- a/site/src/components/RuntimeErrorState/RuntimeErrorState.tsx +++ b/site/src/components/RuntimeErrorState/RuntimeErrorState.tsx @@ -15,7 +15,7 @@ const Language = { link: "send it to us.", } -interface RuntimeErrorStateProps { +export interface RuntimeErrorStateProps { error: Error } diff --git a/site/src/components/RuntimeErrorState/ReportButtons.tsx b/site/src/components/RuntimeErrorState/createCtas.tsx similarity index 94% rename from site/src/components/RuntimeErrorState/ReportButtons.tsx rename to site/src/components/RuntimeErrorState/createCtas.tsx index a1192d3524a26..3be72455ec4d8 100644 --- a/site/src/components/RuntimeErrorState/ReportButtons.tsx +++ b/site/src/components/RuntimeErrorState/createCtas.tsx @@ -59,7 +59,7 @@ const useStyles = makeStyles((theme) => ({ marginLeft: 0, flex: theme.spacing(1), textTransform: "uppercase", - fontSize: theme.typography.button.fontSize, + fontSize: theme.typography.fontSize, }, copyButton: { @@ -67,6 +67,6 @@ const useStyles = makeStyles((theme) => ({ marginRight: theme.spacing(1), backgroundColor: theme.palette.primary.main, textTransform: "uppercase", - fontSize: theme.typography.button.fontSize, + fontSize: theme.typography.fontSize, }, })) diff --git a/site/src/theme/palettes.ts b/site/src/theme/palettes.ts index 6c223a4c85312..ac740f446e5ae 100644 --- a/site/src/theme/palettes.ts +++ b/site/src/theme/palettes.ts @@ -37,7 +37,4 @@ export const darkPalette: PaletteOptions = { success: { main: "#6BBE00", }, - error: { - main: "#DD4764", - }, } From d12d8fc08db340c59a3b404321784eecd14fa464 Mon Sep 17 00:00:00 2001 From: Kira Pilot Date: Thu, 19 May 2022 20:04:55 +0000 Subject: [PATCH 4/6] feat: added error boundary closes #1013 --- .../RuntimeErrorState.test.tsx | 43 +++++++++++++++++++ .../RuntimeErrorState/RuntimeErrorState.tsx | 13 ++---- .../RuntimeErrorState/createCtas.tsx | 2 +- 3 files changed, 47 insertions(+), 11 deletions(-) create mode 100644 site/src/components/RuntimeErrorState/RuntimeErrorState.test.tsx diff --git a/site/src/components/RuntimeErrorState/RuntimeErrorState.test.tsx b/site/src/components/RuntimeErrorState/RuntimeErrorState.test.tsx new file mode 100644 index 0000000000000..0c923d43892f5 --- /dev/null +++ b/site/src/components/RuntimeErrorState/RuntimeErrorState.test.tsx @@ -0,0 +1,43 @@ +import { screen } from "@testing-library/react" +import React from "react" +import { render } from "../../testHelpers/renderHelpers" +import { Language as ButtonLanguage } from "./createCtas" +import { Language as RuntimeErrorStateLanguage, RuntimeErrorState } from "./RuntimeErrorState" + +describe("RuntimeErrorState", () => { + beforeEach(() => { + // Given + const errorText = "broken!" + const errorStateProps = { + error: new Error(errorText), + } + + // When + render() + }) + + it("should show stack when encountering runtime error", () => { + // Then + const reportError = screen.getByText("broken!") + expect(reportError).toBeDefined() + + // Despite appearances, this is the stack trace + const stackTrace = screen.getByText("Unable to get stack trace") + expect(stackTrace).toBeDefined() + }) + + it("should have a button bar", () => { + // Then + const copyCta = screen.getByText(ButtonLanguage.copyReport) + expect(copyCta).toBeDefined() + + const reloadCta = screen.getByText(ButtonLanguage.reloadApp) + expect(reloadCta).toBeDefined() + }) + + it("should have an email link", () => { + // Then + const emailLink = screen.getByText(RuntimeErrorStateLanguage.link) + expect(emailLink.closest("a")).toHaveAttribute("href", "mailto:support@coder.com") + }) +}) diff --git a/site/src/components/RuntimeErrorState/RuntimeErrorState.tsx b/site/src/components/RuntimeErrorState/RuntimeErrorState.tsx index 5d83eedc24e3d..bab209186d9d7 100644 --- a/site/src/components/RuntimeErrorState/RuntimeErrorState.tsx +++ b/site/src/components/RuntimeErrorState/RuntimeErrorState.tsx @@ -1,15 +1,15 @@ import Box from "@material-ui/core/Box" +import Link from "@material-ui/core/Link" import { makeStyles } from "@material-ui/core/styles" import ErrorOutlineIcon from "@material-ui/icons/ErrorOutline" import React, { useEffect, useReducer } from "react" -import { Link } from "react-router-dom" import { mapStackTrace } from "sourcemapped-stacktrace" import { Margins } from "../Margins/Margins" import { Section } from "../Section/Section" import { Typography } from "../Typography/Typography" import { reducer, RuntimeErrorReport, stackTraceAvailable, stackTraceUnavailable } from "./RuntimeErrorReport" -const Language = { +export const Language = { title: "Coder encountered an error", body: "Please copy the crash log using the button below and", link: "send it to us.", @@ -40,14 +40,7 @@ const ErrorStateDescription = () => { return ( {Language.body}  - { - window.location.href = "mailto:support@coder.com" - e.preventDefault() - }} - className={styles.link} - > + {Language.link} diff --git a/site/src/components/RuntimeErrorState/createCtas.tsx b/site/src/components/RuntimeErrorState/createCtas.tsx index 3be72455ec4d8..e41b5a4fdf9f1 100644 --- a/site/src/components/RuntimeErrorState/createCtas.tsx +++ b/site/src/components/RuntimeErrorState/createCtas.tsx @@ -4,7 +4,7 @@ import RefreshIcon from "@material-ui/icons/Refresh" import React from "react" import { CopyButton } from "../CopyButton/CopyButton" -const Language = { +export const Language = { reloadApp: "Reload Application", copyReport: "Copy Report", } From a5f1aa9a7c2572ca663fb04236714b6222462831 Mon Sep 17 00:00:00 2001 From: Kira Pilot Date: Thu, 19 May 2022 21:07:09 +0000 Subject: [PATCH 5/6] committing lockfile --- site/yarn.lock | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/site/yarn.lock b/site/yarn.lock index cd898202a62f6..0ab541c8bc229 100644 --- a/site/yarn.lock +++ b/site/yarn.lock @@ -11991,6 +11991,11 @@ source-map-url@^0.4.0: resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.1.tgz#0af66605a745a5a2f91cf1bbf8a7afbc283dec56" integrity sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw== +source-map@0.5.6: + version "0.5.6" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412" + integrity sha1-dc449SvwczxafwwRjYEzSiu19BI= + source-map@^0.5.0, source-map@^0.5.6, source-map@^0.5.7: version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" @@ -12006,6 +12011,13 @@ source-map@^0.7.3, source-map@~0.7.2: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== +sourcemapped-stacktrace@1.1.11: + version "1.1.11" + resolved "https://registry.yarnpkg.com/sourcemapped-stacktrace/-/sourcemapped-stacktrace-1.1.11.tgz#e2dede7fc148599c52a4f883276e527f8452657d" + integrity sha512-O0pcWjJqzQFVsisPlPXuNawJHHg9N9UgpJ/aDmvi9+vnS3x1C0NhwkVFzzZ1VN0Xo+bekyweoqYvBw5ZBKiNnQ== + dependencies: + source-map "0.5.6" + space-separated-tokens@^1.0.0: version "1.1.5" resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz#85f32c3d10d9682007e917414ddc5c26d1aa6899" From bea96b41652c54cc8e29fd8c2687523a2c063b44 Mon Sep 17 00:00:00 2001 From: Kira Pilot Date: Fri, 20 May 2022 14:30:13 +0000 Subject: [PATCH 6/6] added email body to help link --- .../RuntimeErrorState/RuntimeErrorReport.tsx | 23 +++++++++------- .../RuntimeErrorState.test.tsx | 2 +- .../RuntimeErrorState/RuntimeErrorState.tsx | 27 ++++++++++++++++--- 3 files changed, 37 insertions(+), 15 deletions(-) diff --git a/site/src/components/RuntimeErrorState/RuntimeErrorReport.tsx b/site/src/components/RuntimeErrorState/RuntimeErrorReport.tsx index 0ab929d371acb..7206060e3906d 100644 --- a/site/src/components/RuntimeErrorState/RuntimeErrorReport.tsx +++ b/site/src/components/RuntimeErrorState/RuntimeErrorReport.tsx @@ -49,6 +49,17 @@ export const reducer = (model: ReportState, msg: ReportMessage): ReportState => } } +export const createFormattedStackTrace = (error: Error, mappedStack: string[] | null): string[] => { + return [ + "======================= STACK TRACE ========================", + "", + error.message, + ...(mappedStack ? mappedStack : []), + "", + "============================================================", + ] +} + /** * A code block component that contains the error stack resulting from an error boundary trigger */ @@ -59,16 +70,8 @@ export const RuntimeErrorReport = ({ error, mappedStack }: ReportState): React.R return } - const codeBlock = [ - "======================= STACK TRACE ========================", - "", - error.message, - ...mappedStack, - "", - "============================================================", - ] - - return + const formattedStackTrace = createFormattedStackTrace(error, mappedStack) + return } const useStyles = makeStyles(() => ({ diff --git a/site/src/components/RuntimeErrorState/RuntimeErrorState.test.tsx b/site/src/components/RuntimeErrorState/RuntimeErrorState.test.tsx index 0c923d43892f5..19a09cdde4d86 100644 --- a/site/src/components/RuntimeErrorState/RuntimeErrorState.test.tsx +++ b/site/src/components/RuntimeErrorState/RuntimeErrorState.test.tsx @@ -38,6 +38,6 @@ describe("RuntimeErrorState", () => { it("should have an email link", () => { // Then const emailLink = screen.getByText(RuntimeErrorStateLanguage.link) - expect(emailLink.closest("a")).toHaveAttribute("href", "mailto:support@coder.com") + expect(emailLink.closest("a")).toHaveAttribute("href", expect.stringContaining("mailto:support@coder.com")) }) }) diff --git a/site/src/components/RuntimeErrorState/RuntimeErrorState.tsx b/site/src/components/RuntimeErrorState/RuntimeErrorState.tsx index bab209186d9d7..9f2c7ec5e9790 100644 --- a/site/src/components/RuntimeErrorState/RuntimeErrorState.tsx +++ b/site/src/components/RuntimeErrorState/RuntimeErrorState.tsx @@ -7,7 +7,13 @@ import { mapStackTrace } from "sourcemapped-stacktrace" import { Margins } from "../Margins/Margins" import { Section } from "../Section/Section" import { Typography } from "../Typography/Typography" -import { reducer, RuntimeErrorReport, stackTraceAvailable, stackTraceUnavailable } from "./RuntimeErrorReport" +import { + createFormattedStackTrace, + reducer, + RuntimeErrorReport, + stackTraceAvailable, + stackTraceUnavailable, +} from "./RuntimeErrorReport" export const Language = { title: "Coder encountered an error", @@ -35,12 +41,17 @@ const ErrorStateTitle = () => { /** * A description for our error boundary UI */ -const ErrorStateDescription = () => { +const ErrorStateDescription = ({ emailBody }: { emailBody?: string }) => { const styles = useStyles() return ( {Language.body}  - + {Language.link} @@ -65,7 +76,15 @@ export const RuntimeErrorState: React.FC = ({ error }) = return ( -
} description={}> +
} + description={ + + } + >