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

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

GitOrigin-RevId: 8ed05c8b9c60e85605f374898b3176c7afa561a3
This commit is contained in:
Mathias Jakobsen
2025-08-07 12:41:05 +01:00
committed by Copybot
parent 9b601774fc
commit e27da3bfd8
8 changed files with 262 additions and 45 deletions

View File

@@ -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 = (

View File

@@ -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": "",

View File

@@ -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">

View File

@@ -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,

View File

@@ -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 },
},
}
),
],
}

View File

@@ -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;

View File

@@ -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);
margin-bottom: 0;
}
.pdf-error-state-info-box-list {
margin-bottom: 0;
li + li {
margin-top: var(--spacing-02);
}
}

View File

@@ -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",