Преглед изворни кода

Merge pull request #8297 from overleaf/em-halt-on-error-logs

Stop on first error info box in logs pane

GitOrigin-RevId: cf11f65d582d98bea93c6506393940d9a6144c0d
Eric Mc Sween пре 4 година
родитељ
комит
cb657d1f1c

+ 16 - 17
services/clsi/app/js/CompileController.js

@@ -64,16 +64,17 @@ function compile(req, res, next) {
                 'error running compile'
               )
             } else {
-              let file
-              status = 'failure'
-              for (file of outputFiles) {
-                if (file.path === 'output.pdf' && file.size > 0) {
-                  status = 'success'
-                  lastSuccessfulCompileTimestamp = Date.now()
-                }
-              }
-
-              if (status === 'failure') {
+              if (
+                outputFiles.some(
+                  file => file.path === 'output.pdf' && file.size > 0
+                )
+              ) {
+                status = 'success'
+                lastSuccessfulCompileTimestamp = Date.now()
+              } else if (request.stopOnFirstError) {
+                status = 'stopped-on-first-error'
+              } else {
+                status = 'failure'
                 logger.warn(
                   { project_id: request.project_id, outputFiles },
                   'project failed to compile successfully, no output.pdf generated'
@@ -81,13 +82,11 @@ function compile(req, res, next) {
               }
 
               // log an error if any core files are found
-              for (file of outputFiles) {
-                if (file.path === 'core') {
-                  logger.error(
-                    { project_id: request.project_id, req, outputFiles },
-                    'core file found in output'
-                  )
-                }
+              if (outputFiles.some(file => file.path === 'core')) {
+                logger.error(
+                  { project_id: request.project_id, req, outputFiles },
+                  'core file found in output'
+                )
               }
             }
 

+ 3 - 1
services/web/frontend/extracted-translations.json

@@ -89,6 +89,7 @@
   "demonstrating_git_integration": "",
   "department": "",
   "description": "",
+  "disable_stop_on_first_error": "",
   "dismiss": "",
   "dismiss_error_popup": "",
   "doesnt_match": "",
@@ -252,7 +253,6 @@
   "log_viewer_error": "",
   "login_with_service": "",
   "logs_and_output_files": "",
-  "logs_pane_info_message": "",
   "main_file_not_found": "",
   "make_email_primary_description": "",
   "make_primary": "",
@@ -435,6 +435,8 @@
   "start_free_trial": "",
   "stop_compile": "",
   "stop_on_first_error": "",
+  "stop_on_first_error_enabled_description": "",
+  "stop_on_first_error_enabled_title": "",
   "stop_on_validation_error": "",
   "store_your_work": "",
   "subject": "",

+ 3 - 2
services/web/frontend/js/features/pdf-preview/components/pdf-hybrid-logs-button.tsx

@@ -6,7 +6,8 @@ import Icon from '../../../shared/components/icon'
 import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context'
 
 function PdfHybridLogsButton() {
-  const { error, logEntries, toggleLogs, showLogs } = useCompileContext()
+  const { error, logEntries, toggleLogs, showLogs, stoppedOnFirstError } =
+    useCompileContext()
 
   const { t } = useTranslation()
 
@@ -26,7 +27,7 @@ function PdfHybridLogsButton() {
     >
       <Button
         bsStyle="link"
-        disabled={Boolean(error)}
+        disabled={Boolean(error || stoppedOnFirstError)}
         active={showLogs}
         className="toolbar-item log-btn"
         onClick={handleClick}

+ 8 - 2
services/web/frontend/js/features/pdf-preview/components/pdf-log-entry.js

@@ -72,8 +72,14 @@ PdfLogEntry.propTypes = {
   logType: PropTypes.string,
   formattedContent: PropTypes.node,
   extraInfoURL: PropTypes.string,
-  level: PropTypes.oneOf(['error', 'warning', 'typesetting', 'raw', 'success'])
-    .isRequired,
+  level: PropTypes.oneOf([
+    'error',
+    'warning',
+    'info',
+    'typesetting',
+    'raw',
+    'success',
+  ]).isRequired,
   customClass: PropTypes.string,
   showSourceLocationLink: PropTypes.bool,
   showCloseButton: PropTypes.bool,

+ 5 - 0
services/web/frontend/js/features/pdf-preview/components/pdf-logs-viewer.js

@@ -2,6 +2,7 @@ import { useTranslation } from 'react-i18next'
 import { memo } from 'react'
 import classnames from 'classnames'
 import PdfValidationIssue from './pdf-validation-issue'
+import StopOnFirstErrorPrompt from './stop-on-first-error-prompt'
 import TimeoutUpgradePrompt from './timeout-upgrade-prompt'
 import PdfPreviewError from './pdf-preview-error'
 import PdfClearCacheButton from './pdf-clear-cache-button'
@@ -21,6 +22,8 @@ function PdfLogsViewer() {
     rawLog,
     validationIssues,
     showLogs,
+    stopOnFirstError,
+    stoppedOnFirstError,
   } = useCompileContext()
 
   const { t } = useTranslation()
@@ -30,6 +33,8 @@ function PdfLogsViewer() {
       <div className="logs-pane-content">
         {codeCheckFailed && <PdfCodeCheckFailedNotice />}
 
+        {stopOnFirstError && stoppedOnFirstError && <StopOnFirstErrorPrompt />}
+
         {error && <PdfPreviewError error={error} />}
 
         {error === 'timedout' && <TimeoutUpgradePrompt />}

+ 34 - 0
services/web/frontend/js/features/pdf-preview/components/stop-on-first-error-prompt.js

@@ -0,0 +1,34 @@
+import { useTranslation, Trans } from 'react-i18next'
+import { Button } from 'react-bootstrap'
+import PdfLogEntry from './pdf-log-entry'
+import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context'
+
+export default function StopOnFirstErrorPrompt() {
+  const { t } = useTranslation()
+  const { setStopOnFirstError, startCompile } = useCompileContext()
+  return (
+    <PdfLogEntry
+      headerTitle={t('stop_on_first_error_enabled_title')}
+      formattedContent={
+        <>
+          <Trans
+            i18nKey="stop_on_first_error_enabled_description"
+            // eslint-disable-next-line react/jsx-key
+            components={[<strong />]}
+          />{' '}
+          <Button
+            bsSize="xs"
+            bsStyle="info"
+            onClick={() => {
+              startCompile({ stopOnFirstError: false })
+              setStopOnFirstError(false)
+            }}
+          >
+            {t('disable_stop_on_first_error')}
+          </Button>
+        </>
+      }
+      level="info"
+    />
+  )
+}

+ 11 - 6
services/web/frontend/js/features/pdf-preview/util/compiler.js

@@ -41,7 +41,10 @@ export default class DocumentCompiler {
     this.currentDoc = null
     this.error = undefined
     this.timer = 0
-    this.stopOnFirstError = false
+    this.defaultOptions = {
+      draft: false,
+      stopOnFirstError: false,
+    }
 
     this.debouncedAutoCompile = debounce(
       () => {
@@ -57,9 +60,7 @@ export default class DocumentCompiler {
   // The main "compile" function.
   // Call this directly to run a compile now, otherwise call debouncedAutoCompile.
   async compile(options = {}) {
-    if (!options) {
-      options = {}
-    }
+    options = { ...this.defaultOptions, ...options }
 
     // set "compiling" to true (in the React component's state), and return if it was already true
     const wasCompiling = this.compilingRef.current
@@ -85,14 +86,14 @@ export default class DocumentCompiler {
 
       const body = {
         rootDoc_id: this.getRootDocOverrideId(),
-        draft: this.draft,
+        draft: options.draft,
         check: 'silent', // NOTE: 'error' and 'validate' are possible, but unused
         // use incremental compile for all users but revert to a full compile
         // if there was previously a server error
         incrementalCompilesEnabled: !this.error,
       }
       if (getMeta('ol-showStopOnFirstError')) {
-        body.stopOnFirstError = this.stopOnFirstError
+        body.stopOnFirstError = options.stopOnFirstError
       }
       const data = await postJSON(
         `/project/${this.projectId}/compile?${params}`,
@@ -202,4 +203,8 @@ export default class DocumentCompiler {
       this.setError('clear-cache')
     })
   }
+
+  setOption(option, value) {
+    this.defaultOptions[option] = value
+  }
 }

+ 2 - 0
services/web/frontend/js/features/preview/components/preview-log-entry-header.js

@@ -32,6 +32,7 @@ function PreviewLogEntryHeader({
   const logEntryHeaderClasses = classNames('log-entry-header', {
     'log-entry-header-error': level === 'error',
     'log-entry-header-warning': level === 'warning',
+    'log-entry-header-info': level === 'info',
     'log-entry-header-typesetting': level === 'typesetting',
     'log-entry-header-raw': level === 'raw',
     'log-entry-header-success': level === 'success',
@@ -41,6 +42,7 @@ function PreviewLogEntryHeader({
     'log-entry-header-link-warning': level === 'warning',
     'log-entry-header-link-typesetting': level === 'typesetting',
     'log-entry-header-link-raw': level === 'raw',
+    'log-entry-header-link-info': level === 'info',
     'log-entry-header-link-success': level === 'success',
   })
   const headerLogLocationTitle = t('navigate_log_source', {

+ 5 - 1
services/web/frontend/js/ide/log-parser/latex-log-parser.js

@@ -79,7 +79,11 @@ export default class LatexParser {
   }
 
   currentLineIsError() {
-    return this.currentLine[0] === '!'
+    return (
+      this.currentLine[0] === '!' &&
+      this.currentLine !==
+        '!  ==> Fatal error occurred, no output PDF file produced!'
+    )
   }
 
   currentLineIsFileLineError() {

+ 9 - 2
services/web/frontend/js/shared/context/detach-compile-context.js

@@ -50,6 +50,7 @@ export function DetachCompileProvider({ children }) {
     showLogs: _showLogs,
     stopOnFirstError: _stopOnFirstError,
     stopOnValidationError: _stopOnValidationError,
+    stoppedOnFirstError: _stoppedOnFirstError,
     uncompiled: _uncompiled,
     validationIssues: _validationIssues,
     firstRenderDone: _firstRenderDone,
@@ -172,6 +173,12 @@ export function DetachCompileProvider({ children }) {
     'detacher',
     'detached'
   )
+  const [stoppedOnFirstError] = useDetachStateWatcher(
+    'stoppedOnFirstError',
+    _stoppedOnFirstError,
+    'detacher',
+    'detached'
+  )
   const [uncompiled] = useDetachStateWatcher(
     'uncompiled',
     _uncompiled,
@@ -239,14 +246,12 @@ export function DetachCompileProvider({ children }) {
     'detached',
     'detacher'
   )
-
   const setStopOnFirstError = useDetachAction(
     'setStopOnFirstError',
     _setStopOnFirstError,
     'detached',
     'detacher'
   )
-
   const setStopOnValidationError = useDetachAction(
     'setStopOnValidationError',
     _setStopOnValidationError,
@@ -333,6 +338,7 @@ export function DetachCompileProvider({ children }) {
       stopCompile,
       stopOnFirstError,
       stopOnValidationError,
+      stoppedOnFirstError,
       uncompiled,
       validationIssues,
       firstRenderDone,
@@ -375,6 +381,7 @@ export function DetachCompileProvider({ children }) {
       stopCompile,
       stopOnFirstError,
       stopOnValidationError,
+      stoppedOnFirstError,
       uncompiled,
       validationIssues,
       firstRenderDone,

+ 15 - 2
services/web/frontend/js/shared/context/local-compile-context.js

@@ -62,6 +62,7 @@ export const CompileContextPropTypes = {
     showLogs: PropTypes.bool.isRequired,
     stopOnFirstError: PropTypes.bool.isRequired,
     stopOnValidationError: PropTypes.bool.isRequired,
+    stoppedOnFirstError: PropTypes.bool.isRequired,
     uncompiled: PropTypes.bool,
     validationIssues: PropTypes.object,
     firstRenderDone: PropTypes.func,
@@ -164,6 +165,9 @@ export function LocalCompileProvider({ children }) {
     true
   )
 
+  // whether the last compiles stopped on first error
+  const [stoppedOnFirstError, setStoppedOnFirstError] = useState(false)
+
   // whether compiling should be prevented if there are linting errors
   const [stopOnValidationError, setStopOnValidationError] = usePersistedState(
     `stop_on_validation_error:${projectId}`,
@@ -221,12 +225,12 @@ export function LocalCompileProvider({ children }) {
 
   // keep draft setting in sync with the compiler
   useEffect(() => {
-    compiler.draft = draft
+    compiler.setOption('draft', draft)
   }, [compiler, draft])
 
   // keep stop on first error setting in sync with the compiler
   useEffect(() => {
-    compiler.stopOnFirstError = stopOnFirstError
+    compiler.setOption('stopOnFirstError', stopOnFirstError)
   }, [compiler, stopOnFirstError])
 
   // pass the "uncompiled" value up into the scope for use outside this context provider
@@ -311,6 +315,11 @@ export function LocalCompileProvider({ children }) {
           setShowLogs(false)
           break
 
+        case 'stopped-on-first-error':
+          setError(undefined)
+          setShowLogs(true)
+          break
+
         case 'clsi-maintenance':
         case 'compile-in-progress':
         case 'exited':
@@ -354,6 +363,8 @@ export function LocalCompileProvider({ children }) {
           setError('error')
           break
       }
+
+      setStoppedOnFirstError(data.status === 'stopped-on-first-error')
     }
 
     return () => {
@@ -484,6 +495,7 @@ export function LocalCompileProvider({ children }) {
       stopCompile,
       stopOnFirstError,
       stopOnValidationError,
+      stoppedOnFirstError,
       uncompiled,
       validationIssues,
       firstRenderDone,
@@ -523,6 +535,7 @@ export function LocalCompileProvider({ children }) {
       stopCompile,
       stopOnFirstError,
       stopOnValidationError,
+      stoppedOnFirstError,
       uncompiled,
       validationIssues,
       firstRenderDone,

+ 4 - 2
services/web/frontend/stylesheets/app/editor/logs.less

@@ -68,11 +68,13 @@
   .btn-alert-variant(@ol-blue);
 }
 
-.log-entry-header-raw {
+.log-entry-header-raw,
+.log-entry-header-info {
   background-color: @ol-blue-gray-4;
 }
 
-.log-entry-header-link-raw {
+.log-entry-header-link-raw,
+.log-entry-header-link-info {
   .btn-alert-variant(@ol-blue-gray-4);
 }
 

+ 4 - 1
services/web/locales/en.json

@@ -1752,5 +1752,8 @@
   "email_or_password_wrong_try_again_or_reset": "Your email or password is incorrect. Please try again, or <0>set or reset your password</0>.",
   "compile_error_handling": "Compile Error Handling",
   "stop_on_first_error": "Stop on first error",
-  "try_to_compile_despite_errors": "Try to compile despite errors"
+  "try_to_compile_despite_errors": "Try to compile despite errors",
+  "stop_on_first_error_enabled_title": "No PDF: Stop on first error enabled",
+  "stop_on_first_error_enabled_description": "<0>“Stop on first error” is enabled.</0> Disabling it may allow the compiler to produce a PDF (but your project will still have errors).",
+  "disable_stop_on_first_error": "Disable “Stop on first error”"
 }