Просмотр исходного кода

Merge pull request #27640 from overleaf/mj-ide-compile-errors

[web] Handle premium timeout and running compilation in redesigned editor

GitOrigin-RevId: 8ed05c8b9c60e85605f374898b3176c7afa561a3
Mathias Jakobsen 1 год назад
Родитель
Сommit
e27da3bfd8

+ 7 - 0
services/web/.storybook/utils/with-split-tests.tsx

@@ -5,6 +5,13 @@ import { SplitTestContext } from '../../frontend/js/shared/context/split-test-co
 export const splitTestsArgTypes = {
   // to be able to use this utility, you need to add the argTypes for each split test in this object
   // Check the original implementation for an example: https://github.com/overleaf/internal/pull/17809
+  'editor-redesign': {
+    description: 'Enable the new editor redesign',
+    control: {
+      type: 'select' as const,
+    },
+    options: ['enabled'],
+  },
 }
 
 export const withSplitTests = (

+ 2 - 0
services/web/frontend/extracted-translations.json

@@ -1342,9 +1342,11 @@
   "project_search_result_count": "",
   "project_search_result_count_plural": "",
   "project_synchronisation": "",
+  "project_timed_out_common_causes": "",
   "project_timed_out_enable_stop_on_first_error": "",
   "project_timed_out_fatal_error": "",
   "project_timed_out_intro": "",
+  "project_timed_out_intro_short": "",
   "project_timed_out_learn_more": "",
   "project_timed_out_optimize_images": "",
   "project_title_options": "",

+ 132 - 13
services/web/frontend/js/features/ide-redesign/components/pdf-preview/pdf-error-state.tsx

@@ -1,28 +1,61 @@
 import OLButton from '@/features/ui/components/ol/ol-button'
 import MaterialIcon from '@/shared/components/material-icon'
-import { useTranslation } from 'react-i18next'
+import { Trans, useTranslation } from 'react-i18next'
 import { useRailContext } from '../../contexts/rail-context'
 import { usePdfPreviewContext } from '@/features/pdf-preview/components/pdf-preview-provider'
 import { useDetachCompileContext as useCompileContext } from '@/shared/context/detach-compile-context'
 import { useIsNewEditorEnabled } from '../../utils/new-editor-utils'
 import { upgradePlan } from '@/main/account-upgrade'
 import classNames from 'classnames'
+import { useStopOnFirstError } from '@/shared/hooks/use-stop-on-first-error'
+import { useCallback } from 'react'
 
+// AvailableStates
+// - rendering-error-expected
+// - rendering-error
+// - clsi-maintenance
+// - clsi-unavailable
+// - too-recently-compiled
+// - terminated
+// - rate-limited
+// - compile-in-progress
+// - autocompile-disabled
+// - project-too-large
+// - timedout
+// - failure
+// - clear-cache
+// - pdf-viewer-loading-error
+// - validation-problems
 function PdfErrorState() {
   const { loadingError } = usePdfPreviewContext()
   // TODO ide-redesign-cleanup: rename showLogs to something else and check usages
   const { hasShortCompileTimeout, error, showLogs } = useCompileContext()
   const newEditor = useIsNewEditorEnabled()
+  const { t } = useTranslation()
 
   if (!newEditor || (!loadingError && !showLogs)) {
     return null
   }
 
-  if (hasShortCompileTimeout && error === 'timedout') {
-    return <CompileTimeoutErrorState />
+  switch (error) {
+    case 'timedout': {
+      if (hasShortCompileTimeout) {
+        return <CompileTimeoutErrorState />
+      } else {
+        return <LongCompileTimeoutErrorState />
+      }
+    }
+    case 'compile-in-progress':
+      return (
+        <ErrorState
+          title={t('pdf_compile_in_progress_error')}
+          description={t('pdf_compile_try_again')}
+          iconType="warning"
+        />
+      )
+    default:
+      return <GeneralErrorState />
   }
-
-  return <GeneralErrorState />
 }
 
 const GeneralErrorState = () => {
@@ -34,7 +67,6 @@ const GeneralErrorState = () => {
       title={t('pdf_couldnt_compile')}
       description={t('we_are_unable_to_generate_the_pdf_at_this_time')}
       iconType="warning"
-      iconClassName="pdf-error-state-warning-icon"
       actions={
         <OLButton
           variant="secondary"
@@ -52,11 +84,13 @@ const GeneralErrorState = () => {
             <MaterialIcon type="info" unfilled />
             {t('why_might_this_happen')}
           </div>
-          <ul className="pdf-error-state-info-box-text">
-            <li>{t('there_is_an_unrecoverable_latex_error_check_logs')}</li>
-            <li>{t('the_document_environment_contains_no_content')}</li>
-            <li>{t('this_project_contains_a_file_called_output')}</li>
-          </ul>
+          <div className="pdf-error-state-info-box-text">
+            <ul className="pdf-error-state-info-box-list">
+              <li>{t('there_is_an_unrecoverable_latex_error_check_logs')}</li>
+              <li>{t('the_document_environment_contains_no_content')}</li>
+              <li>{t('this_project_contains_a_file_called_output')}</li>
+            </ul>
+          </div>
         </div>
       }
     />
@@ -84,6 +118,85 @@ const CompileTimeoutErrorState = () => {
   )
 }
 
+const LongCompileTimeoutErrorState = () => {
+  const { t } = useTranslation()
+
+  const { enableStopOnFirstError } = useStopOnFirstError({
+    eventSource: 'timeout',
+  })
+  const { startCompile, lastCompileOptions, setAnimateCompileDropdownArrow } =
+    useCompileContext()
+
+  const handleEnableStopOnFirstErrorClick = useCallback(() => {
+    enableStopOnFirstError()
+    startCompile({ stopOnFirstError: true })
+    setAnimateCompileDropdownArrow(true)
+  }, [enableStopOnFirstError, startCompile, setAnimateCompileDropdownArrow])
+
+  return (
+    <ErrorState
+      title={t('compile_limit_reached')}
+      description={t('project_timed_out_intro_short')}
+      iconType="running_with_errors"
+      extraContent={
+        <div className="pdf-error-state-info-box">
+          <div className="pdf-error-state-info-box-title">
+            <MaterialIcon type="info" unfilled />
+            {t('project_timed_out_common_causes')}
+          </div>
+          <div className="pdf-error-state-info-box-text">
+            <ul className="pdf-error-state-info-box-list">
+              <li>
+                <Trans
+                  i18nKey="project_timed_out_optimize_images"
+                  components={[
+                    // eslint-disable-next-line jsx-a11y/anchor-has-content, react/jsx-key
+                    <a href="https://www.overleaf.com/learn/how-to/Optimising_very_large_image_files" />,
+                  ]}
+                />
+              </li>
+              <li>
+                <Trans
+                  i18nKey="project_timed_out_fatal_error"
+                  components={[
+                    // eslint-disable-next-line jsx-a11y/anchor-has-content, react/jsx-key
+                    <a href="https://www.overleaf.com/learn/how-to/Why_do_I_keep_getting_the_compile_timeout_error_message%3F#Fatal_compile_errors_blocking_the_compilation" />,
+                  ]}
+                />
+                {!lastCompileOptions.stopOnFirstError && (
+                  <>
+                    {' '}
+                    <Trans
+                      i18nKey="project_timed_out_enable_stop_on_first_error"
+                      components={[
+                        // eslint-disable-next-line react/jsx-key
+                        <OLButton
+                          variant="primary"
+                          size="sm"
+                          onClick={handleEnableStopOnFirstErrorClick}
+                        />,
+                      ]}
+                    />
+                  </>
+                )}
+              </li>
+            </ul>
+            <p className="mb-0">
+              <Trans
+                i18nKey="project_timed_out_learn_more"
+                components={[
+                  // eslint-disable-next-line jsx-a11y/anchor-has-content, react/jsx-key
+                  <a href="https://www.overleaf.com/learn/how-to/Why_do_I_keep_getting_the_compile_timeout_error_message%3F" />,
+                ]}
+              />
+            </p>
+          </div>
+        </div>
+      }
+    />
+  )
+}
+
 const ErrorState = ({
   title,
   description,
@@ -95,14 +208,20 @@ const ErrorState = ({
   title: string
   description: string
   iconType: string
-  actions: React.ReactNode
+  actions?: React.ReactNode
   iconClassName?: string
   extraContent?: React.ReactNode
 }) => {
   return (
     <div className="pdf-error-state">
       <div className="pdf-error-state-top-section">
-        <div className={classNames('pdf-error-state-icon', iconClassName)}>
+        <div
+          className={classNames(
+            'pdf-error-state-icon',
+            'pdf-error-state-warning-icon',
+            iconClassName
+          )}
+        >
           <MaterialIcon type={iconType} />
         </div>
         <div className="pdf-error-state-text">

+ 1 - 1
services/web/frontend/stories/decorators/scope.tsx

@@ -21,7 +21,7 @@ import { ReactContextRoot } from '@/features/ide-react/context/react-context-roo
 
 const scopeWatchers: [string, (value: any) => void][] = []
 
-const user: User = {
+export const user: User = {
   id: 'story-user' as UserId,
   email: 'story-user@example.com',
   allowedFreeTrial: true,

+ 90 - 0
services/web/frontend/stories/pdf-error-state.stories.tsx

@@ -0,0 +1,90 @@
+import useFetchMock from './hooks/use-fetch-mock'
+import { mockCompile } from './fixtures/compile'
+import { ScopeDecorator, user } from './decorators/scope'
+import PdfErrorState from '@/features/ide-redesign/components/pdf-preview/pdf-error-state'
+import { useDetachCompileContext as useCompileContext } from '@/shared/context/detach-compile-context'
+import { useEffect } from 'react'
+import PdfPreview from '@/features/pdf-preview/components/pdf-preview'
+
+export default {
+  title: 'Editor / PDF Error States',
+  component: PdfErrorState,
+}
+
+const compileErrors = [
+  'autocompile-backoff',
+  'clear-cache',
+  'clsi-maintenance',
+  'compile-in-progress',
+  'exited',
+  'failure',
+  'generic',
+  'project-too-large',
+  'rate-limited',
+  'success',
+  'terminated',
+  'timedout',
+  'too-recently-compiled',
+  'unavailable',
+  'validation-problems',
+  'foo',
+] as const
+
+const ErrorPane = ({ error }: { error: (typeof compileErrors)[number] }) => {
+  window.metaAttributesCache.set('ol-splitTestVariants', {
+    'editor-redesign': 'enabled',
+  })
+  useFetchMock(fetchMock => {
+    mockCompile(fetchMock)
+  })
+  const { setError } = useCompileContext()
+  useEffect(() => {
+    setError(error)
+  }, [setError, error])
+
+  return (
+    <div className="ide-redesign-main">
+      <span style={{ fontFamily: 'monospace' }}>{error}</span>
+      <PdfPreview />
+    </div>
+  )
+}
+
+const story = {
+  render: (args: { error: (typeof compileErrors)[number] }) => {
+    return <ErrorPane error={args.error} />
+  },
+  argTypes: {
+    error: {
+      options: compileErrors,
+      control: {
+        type: 'select',
+      },
+    },
+  },
+  args: {
+    error: compileErrors[0],
+  },
+}
+
+export const PremiumUser = {
+  ...story,
+  decorators: [ScopeDecorator],
+}
+
+export const FreeUser = {
+  ...story,
+  decorators: [
+    (Story: any) =>
+      ScopeDecorator(
+        Story,
+        { mockCompileOnLoad: false },
+        {
+          'ol-user': {
+            ...user,
+            features: { ...user.features, compileTimeout: 20 },
+          },
+        }
+      ),
+  ],
+}

+ 6 - 0
services/web/frontend/stylesheets/abstracts/mixins.scss

@@ -156,6 +156,12 @@
   --link-visited-color: var(--link-visited-color-dark);
 }
 
+@mixin themed-links {
+  --link-color: var(--link-ui-themed);
+  --link-hover-color: var(--link-ui-hover-themed);
+  --link-visited-color: var(--link-ui-visited-themed);
+}
+
 @mixin triangle($direction, $width, $height, $color) {
   position: absolute;
   border-color: transparent;

+ 21 - 30
services/web/frontend/stylesheets/pages/editor/pdf-error-state.scss

@@ -1,19 +1,3 @@
-:root {
-  --pdf-error-state-info-box-color: var(--content-primary-dark);
-  --pdf-error-state-info-box-background: var(--bg-dark-primary);
-  --pdf-error-state-info-box-border: var(--border-divider-dark);
-  --pdf-error-state-label-color: var(--content-primary-dark);
-  --pdf-error-state-description-color: var(--content-secondary-dark);
-}
-
-@include theme('light') {
-  --pdf-error-state-info-box-color: var(--content-primary);
-  --pdf-error-state-info-box-background: var(--bg-light-primary);
-  --pdf-error-state-info-box-border: var(--border-divider);
-  --pdf-error-state-label-color: var(--content-primary);
-  --pdf-error-state-description-color: var(--content-secondary);
-}
-
 .pdf-error-state {
   position: absolute;
   inset: var(--toolbar-small-height) 0 0 0;
@@ -34,6 +18,7 @@
 }
 
 .pdf-error-state-icon {
+  color: var(--content-primary-themed);
   width: 80px;
   height: 80px;
   display: flex;
@@ -44,14 +29,14 @@
   .material-symbols {
     font-size: 80px;
   }
-}
 
-.pdf-error-state-warning-icon {
-  background-color: var(--bg-danger-03);
-  color: var(--content-danger);
+  &.pdf-error-state-warning-icon {
+    background-color: var(--bg-danger-03);
+    color: var(--content-danger);
 
-  .material-symbols {
-    font-size: 32px;
+    .material-symbols {
+      font-size: 32px;
+    }
   }
 }
 
@@ -64,23 +49,25 @@
 
 .pdf-error-state-label {
   font-size: var(--font-size-02);
-  color: var(--pdf-error-state-label-color);
+  color: var(--content-primary-themed);
   font-weight: 600;
   margin-bottom: 0;
 }
 
 .pdf-error-state-description {
-  color: var(--pdf-error-state-description-color);
+  color: var(--content-secondary-themed);
   font-size: var(--font-size-02);
   margin-bottom: 0;
 }
 
 .pdf-error-state-info-box {
-  background-color: var(--pdf-error-state-info-box-background);
-  color: var(--pdf-error-state-info-box-color);
+  background-color: var(--bg-primary-themed);
+  color: var(--content-primary-themed);
   padding: var(--spacing-06);
-  border: 1px solid var(--pdf-error-state-info-box-border);
+  border: 1px solid var(--border-divider-themed);
   border-radius: var(--border-radius-base);
+
+  @include themed-links;
 }
 
 .pdf-error-state-info-box-title {
@@ -94,8 +81,12 @@
 
 .pdf-error-state-info-box-text {
   font-size: var(--font-size-02);
-  display: flex;
-  flex-direction: column;
-  gap: var(--spacing-02);
+}
+
+.pdf-error-state-info-box-list {
   margin-bottom: 0;
+
+  li + li {
+    margin-top: var(--spacing-02);
+  }
 }

+ 2 - 0
services/web/locales/en.json

@@ -1753,9 +1753,11 @@
   "project_search_result_count": "__count__ result",
   "project_search_result_count_plural": "__count__ results",
   "project_synchronisation": "Project synchronisation",
+  "project_timed_out_common_causes": "Most common causes of timeouts are:",
   "project_timed_out_enable_stop_on_first_error": "<0>Enable “Stop on first error”</0> to help you find and fix errors right away.",
   "project_timed_out_fatal_error": "A <0>fatal compile error</0> may be completely blocking compilation.",
   "project_timed_out_intro": "Sorry, your compile took too long to run and timed out. The most common causes of timeouts are:",
+  "project_timed_out_intro_short": "Sorry, your compile took too long to run and timed out",
   "project_timed_out_learn_more": "<0>Learn more</0> about other causes of compile timeouts and how to fix them.",
   "project_timed_out_optimize_images": "Large or high-resolution images are taking too long to process. You may be able to <0>optimize them</0>.",
   "project_too_large": "Project too large",