TopErrorBoundary.tsx 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  1. import React from "react";
  2. import * as Sentry from "@sentry/browser";
  3. import { resetCursor } from "../utils";
  4. import { t } from "../i18n";
  5. interface TopErrorBoundaryState {
  6. hasError: boolean;
  7. sentryEventId: string;
  8. localStorage: string;
  9. }
  10. export class TopErrorBoundary extends React.Component<
  11. any,
  12. TopErrorBoundaryState
  13. > {
  14. state: TopErrorBoundaryState = {
  15. hasError: false,
  16. sentryEventId: "",
  17. localStorage: "",
  18. };
  19. render() {
  20. return this.state.hasError ? this.errorSplash() : this.props.children;
  21. }
  22. componentDidCatch(error: Error, errorInfo: any) {
  23. resetCursor();
  24. const _localStorage: any = {};
  25. for (const [key, value] of Object.entries({ ...localStorage })) {
  26. try {
  27. _localStorage[key] = JSON.parse(value);
  28. } catch (error) {
  29. _localStorage[key] = value;
  30. }
  31. }
  32. Sentry.withScope((scope) => {
  33. scope.setExtras(errorInfo);
  34. const eventId = Sentry.captureException(error);
  35. this.setState((state) => ({
  36. hasError: true,
  37. sentryEventId: eventId,
  38. localStorage: JSON.stringify(_localStorage),
  39. }));
  40. });
  41. }
  42. private selectTextArea(event: React.MouseEvent<HTMLTextAreaElement>) {
  43. if (event.target !== document.activeElement) {
  44. event.preventDefault();
  45. (event.target as HTMLTextAreaElement).select();
  46. }
  47. }
  48. private async createGithubIssue() {
  49. let body = "";
  50. try {
  51. const templateStrFn = (await import("../bug-issue-template")).default;
  52. body = encodeURIComponent(templateStrFn(this.state.sentryEventId));
  53. } catch (error) {
  54. console.error(error);
  55. }
  56. window.open(
  57. `https://github.com/excalidraw/excalidraw/issues/new?body=${body}`,
  58. );
  59. }
  60. private errorSplash() {
  61. return (
  62. <div className="ErrorSplash">
  63. <div className="ErrorSplash-messageContainer">
  64. <div className="ErrorSplash-paragraph bigger align-center">
  65. {t("errorSplash.headingMain_pre")}
  66. <button onClick={() => window.location.reload()}>
  67. {t("errorSplash.headingMain_button")}
  68. </button>
  69. </div>
  70. <div className="ErrorSplash-paragraph align-center">
  71. {t("errorSplash.clearCanvasMessage")}
  72. <button
  73. onClick={() => {
  74. try {
  75. localStorage.clear();
  76. window.location.reload();
  77. } catch (error) {
  78. console.error(error);
  79. }
  80. }}
  81. >
  82. {t("errorSplash.clearCanvasMessage_button")}
  83. </button>
  84. <br />
  85. <div className="smaller">
  86. <span role="img" aria-label="warning">
  87. ⚠️
  88. </span>
  89. {t("errorSplash.clearCanvasCaveat")}
  90. <span role="img" aria-hidden="true">
  91. ⚠️
  92. </span>
  93. </div>
  94. </div>
  95. <div>
  96. <div className="ErrorSplash-paragraph">
  97. {t("errorSplash.trackedToSentry_pre")}
  98. {this.state.sentryEventId}
  99. {t("errorSplash.trackedToSentry_post")}
  100. </div>
  101. <div className="ErrorSplash-paragraph">
  102. {t("errorSplash.openIssueMessage_pre")}
  103. <button onClick={() => this.createGithubIssue()}>
  104. {t("errorSplash.openIssueMessage_button")}
  105. </button>
  106. {t("errorSplash.openIssueMessage_post")}
  107. </div>
  108. <div className="ErrorSplash-paragraph">
  109. <div className="ErrorSplash-details">
  110. <label>{t("errorSplash.sceneContent")}</label>
  111. <textarea
  112. rows={5}
  113. onPointerDown={this.selectTextArea}
  114. readOnly={true}
  115. value={this.state.localStorage}
  116. />
  117. </div>
  118. </div>
  119. </div>
  120. </div>
  121. </div>
  122. );
  123. }
  124. }