diff --git a/services/web/app/src/Features/Compile/CompileController.js b/services/web/app/src/Features/Compile/CompileController.js index dd3366aa4c..98333bfac9 100644 --- a/services/web/app/src/Features/Compile/CompileController.js +++ b/services/web/app/src/Features/Compile/CompileController.js @@ -114,6 +114,15 @@ module.exports = CompileController = { stopOnFirstError, } + // temporary override to force the new compile timeout + const forceNewCompileTimeout = req.query.force_new_compile_timeout + if ( + forceNewCompileTimeout === 'active' || + forceNewCompileTimeout === 'changing' + ) { + options.forceNewCompileTimeout = forceNewCompileTimeout + } + if (req.body.rootDoc_id) { options.rootDoc_id = req.body.rootDoc_id } else if ( diff --git a/services/web/app/src/Features/Compile/CompileManager.js b/services/web/app/src/Features/Compile/CompileManager.js index 07a59a0166..29a2d2edc0 100644 --- a/services/web/app/src/Features/Compile/CompileManager.js +++ b/services/web/app/src/Features/Compile/CompileManager.js @@ -11,7 +11,11 @@ const { RateLimiter } = require('../../infrastructure/RateLimiter') const SplitTestHandler = require('../SplitTests/SplitTestHandler') const { getAnalyticsIdFromMongoUser } = require('../Analytics/AnalyticsHelper') +const NEW_COMPILE_TIMEOUT_ENFORCED_CUTOFF = new Date('2023-09-18T11:00:00.000Z') + module.exports = CompileManager = { + NEW_COMPILE_TIMEOUT_ENFORCED_CUTOFF, + compile(projectId, userId, options = {}, _callback) { const timer = new Metrics.Timer('editor.compile') const callback = function (...args) { @@ -54,6 +58,16 @@ module.exports = CompileManager = { const value = limits[key] options[key] = value } + if (options.timeout !== 20) { + // temporary override to force the new compile timeout + if (options.forceNewCompileTimeout === 'active') { + options.timeout = 20 + } else if ( + options.forceNewCompileTimeout === 'changing' + ) { + options.timeout = 60 + } + } // Put a lower limit on autocompiles for free users, based on compileGroup CompileManager._checkCompileGroupAutoCompileLimit( options.isAutoCompile, @@ -149,6 +163,7 @@ module.exports = CompileManager = { betaProgram: 1, features: 1, splitTests: 1, + signUpDate: 1, // for compile-timeout-20s }, function (err, owner) { if (err) { @@ -176,7 +191,28 @@ module.exports = CompileManager = { limits.compileBackendClass = compileBackendClass limits.showFasterCompilesFeedbackUI = showFasterCompilesFeedbackUI - callback(null, limits) + if (compileBackendClass === 'n2d' && limits.timeout <= 60) { + // project owners with faster compiles but with <= 60 compile timeout (default) + // will have a 20s compile timeout + // The compile-timeout-20s split test exists to enable a gradual rollout + SplitTestHandler.getAssignmentForMongoUser( + owner, + 'compile-timeout-20s', + (err, assignment) => { + if (err) return callback(err) + if (assignment?.variant === '20s') { + if ( + owner.signUpDate > NEW_COMPILE_TIMEOUT_ENFORCED_CUTOFF + ) { + limits.timeout = 20 + } + } + callback(null, limits) + } + ) + } else { + callback(null, limits) + } } ) } diff --git a/services/web/app/src/Features/Editor/EditorHttpController.js b/services/web/app/src/Features/Editor/EditorHttpController.js index c0fe056472..7e0947bfac 100644 --- a/services/web/app/src/Features/Editor/EditorHttpController.js +++ b/services/web/app/src/Features/Editor/EditorHttpController.js @@ -14,6 +14,10 @@ const Errors = require('../Errors/Errors') const DocstoreManager = require('../Docstore/DocstoreManager') const logger = require('@overleaf/logger') const { expressify } = require('../../util/promises') +const SplitTestHandler = require('../SplitTests/SplitTestHandler') +const { + NEW_COMPILE_TIMEOUT_ENFORCED_CUTOFF, +} = require('../Compile/CompileManager') module.exports = { joinProject: expressify(joinProject), @@ -67,6 +71,30 @@ async function joinProject(req, res, next) { if (!project) { return res.sendStatus(403) } + // Compile timeout 20s test + if (project.features?.compileTimeout <= 60) { + const compileAssignment = + await SplitTestHandler.promises.getAssignmentForMongoUser( + project.owner._id, + 'compile-backend-class-n2d' + ) + if (compileAssignment?.variant === 'n2d') { + const timeoutAssignment = + await SplitTestHandler.promises.getAssignmentForMongoUser( + project.owner._id, + 'compile-timeout-20s' + ) + if (timeoutAssignment?.variant === '20s') { + if (project.owner.signUpDate > NEW_COMPILE_TIMEOUT_ENFORCED_CUTOFF) { + // New users will see a 10s warning and compile fail at 20s + project.showNewCompileTimeoutUI = 'active' + } else { + // Older users aren't limited to 20s, but will see a notice of upcoming changes if compile >20s + project.showNewCompileTimeoutUI = 'changing' + } + } + } + } // Hide sensitive data if the user is restricted if (isRestrictedUser) { project.owner = { _id: project.owner._id } diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index ca7ba34a0c..3de17ed8ef 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -3,6 +3,7 @@ "1_4_width": "", "3_4_width": "", "a_custom_size_has_been_used_in_the_latex_code": "", + "a_fatal_compile_error_that_completely_blocks_compilation": "", "a_file_with_that_name_already_exists_and_will_be_overriden": "", "a_more_comprehensive_list_of_keyboard_shortcuts": "", "about_to_archive_projects": "", @@ -63,6 +64,7 @@ "also": "", "an_email_has_already_been_sent_to": "", "an_error_occurred_when_verifying_the_coupon_code": "", + "and_you_can_upgrade_for_plenty_more_compile_time": "", "anonymous": "", "anyone_with_link_can_edit": "", "anyone_with_link_can_view": "", @@ -102,6 +104,7 @@ "browser": "", "bulk_accept_confirm": "", "bulk_reject_confirm": "", + "but_note_that_free_compile_timeout_limit_will_be_reduced_on_x_date": "", "by_subscribing_you_agree_to_our_terms_of_service": "", "can_edit": "", "can_link_institution_email_acct_to_institution_acct": "", @@ -167,6 +170,7 @@ "comment": "", "commit": "", "common": "", + "common_causes_of_compile_timeouts_are": "", "commons_plan_tooltip": "", "compact": "", "company_name": "", @@ -177,6 +181,7 @@ "compile_larger_projects": "", "compile_mode": "", "compile_terminated_by_user": "", + "compile_timeout_will_be_reduced_project_exceeds_limit_speed_up_compile": "", "compiler": "", "compiling": "", "configure_sso": "", @@ -311,6 +316,7 @@ "emails_and_affiliations_explanation": "", "emails_and_affiliations_title": "", "enable_managed_users": "", + "enable_stop_on_first_error_under_recompile_dropdown_menu": "", "enabled_managed_users_set_up_sso": "", "enabling": "", "end_of_document": "", @@ -549,6 +555,7 @@ "labs_program_already_participating": "", "labs_program_benefits": "<0>", "labs_program_not_participating": "", + "large_or_high-resolution_images_taking_too_long": "", "last_active": "", "last_active_description": "", "last_modified": "", @@ -565,6 +572,7 @@ "learn_more": "", "learn_more_about_link_sharing": "", "learn_more_about_managed_users": "", + "learn_more_about_other_causes_of_compile_timeouts": "", "leave": "", "leave_any_group_subscriptions": "", "leave_group": "", @@ -737,6 +745,7 @@ "organize_projects": "", "other_logs_and_files": "", "other_output_files": "", + "other_ways_to_prevent_compile_timeouts": "", "output_file": "", "overall_theme": "", "overleaf": "", @@ -789,6 +798,7 @@ "please_select_a_project": "", "please_select_an_output_file": "", "please_set_main_file": "", + "plus_additional_collaborators_document_history_track_changes_and_more": "", "plus_more": "", "plus_upgraded_accounts_receive": "", "postal_code": "", @@ -841,6 +851,8 @@ "react_history_tutorial_content": "", "react_history_tutorial_title": "", "reactivate_subscription": "", + "read_more_about_fix_prevent_timeout": "", + "read_more_about_free_compile_timeouts_servers": "", "read_only": "", "read_only_token": "", "read_write_token": "", @@ -1022,8 +1034,10 @@ "sso_config_prop_help_user_id": "", "sso_explanation": "", "sso_link_error": "", + "start_a_free_trial": "", "start_by_adding_your_email": "", "start_free_trial": "", + "start_free_trial_without_exclamation": "", "stop_compile": "", "stop_on_first_error": "", "stop_on_first_error_enabled_description": "", @@ -1065,6 +1079,8 @@ "tc_switch_everyone_tip": "", "tc_switch_guests_tip": "", "tc_switch_user_tip": "", + "tell_the_project_owner_and_ask_them_to_upgrade": "", + "tell_the_project_owner_to_upgrade_plan_for_more_compile_time": "", "template_approved_by_publisher": "", "template_description": "", "template_title_taken_from_project_title": "", @@ -1088,6 +1104,8 @@ "this_could_be_because_we_cant_support_some_elements_of_the_table": "", "this_field_is_required": "", "this_grants_access_to_features_2": "", + "this_project_compiled_but_soon_might_not": "", + "this_project_exceeded_compile_timeout_limit_on_free_plan": "", "this_project_is_public": "", "this_project_is_public_read_only": "", "this_project_will_appear_in_your_dropbox_folder_at": "", @@ -1112,6 +1130,7 @@ "too_many_requests": "", "too_many_search_results": "", "too_recently_compiled": "", + "took_a_while": "", "toolbar_add_comment": "", "toolbar_bullet_list": "", "toolbar_choose_section_heading_level": "", @@ -1203,7 +1222,9 @@ "updating": "", "upgrade": "", "upgrade_cc_btn": "", + "upgrade_for_12x_more_compile_time": "", "upgrade_for_longer_compiles": "", + "upgrade_for_plenty_more_compile_time": "", "upgrade_now": "", "upgrade_to_get_feature": "", "upgrade_to_track_changes": "", @@ -1275,10 +1296,12 @@ "you_have_added_x_of_group_size_y": "", "you_have_been_invited_to_transfer_management_of_your_account": "", "you_have_been_invited_to_transfer_management_of_your_account_to": "", + "you_may_be_able_to_prevent_a_compile_timeout": "", "you_will_be_able_to_reassign_subscription": "", "youll_get_best_results_in_visual_but_can_be_used_in_source": "", "your_affiliation_is_confirmed": "", "your_browser_does_not_support_this_feature": "", + "your_compile_timed_out": "", "your_git_access_info": "", "your_git_access_info_bullet_1": "", "your_git_access_info_bullet_2": "", @@ -1290,6 +1313,9 @@ "your_new_plan": "", "your_plan": "", "your_plan_is_changing_at_term_end": "", + "your_project_compiled_but_soon_might_not": "", + "your_project_exceeded_compile_timeout_limit_on_free_plan": "", + "your_project_near_compile_timeout_limit": "", "your_projects": "", "your_subscription": "", "your_subscription_has_expired": "", diff --git a/services/web/frontend/js/features/pdf-preview/components/compile-time-warning.tsx b/services/web/frontend/js/features/pdf-preview/components/compile-time-warning.tsx index 9a9befd845..4caa170fc4 100644 --- a/services/web/frontend/js/features/pdf-preview/components/compile-time-warning.tsx +++ b/services/web/frontend/js/features/pdf-preview/components/compile-time-warning.tsx @@ -29,7 +29,7 @@ function CompileTimeWarning() { return } setDisplayStatus({ lastDisplayTime: Date.now(), dismissed: false }) - eventTracking.sendMB('compile-time-warning-displayed', {}) + eventTracking.sendMB('compile-time-warning-displayed', { time: 30 }) } }, [showCompileTimeWarning, displayStatus, setDisplayStatus]) @@ -40,6 +40,7 @@ function CompileTimeWarning() { const closeWarning = useCallback(() => { eventTracking.sendMB('compile-time-warning-dismissed', { 'time-since-displayed': getTimeSinceDisplayed(), + time: 30, }) setShowCompileTimeWarning(false) setDisplayStatus(displayStatus => ({ ...displayStatus, dismissed: true })) @@ -48,6 +49,7 @@ function CompileTimeWarning() { const handleUpgradeClick = useCallback(() => { eventTracking.sendMB('compile-time-warning-upgrade-click', { 'time-since-displayed': getTimeSinceDisplayed(), + time: 30, }) setShowCompileTimeWarning(false) setDisplayStatus(displayStatus => ({ ...displayStatus, dismissed: true })) diff --git a/services/web/frontend/js/features/pdf-preview/components/compile-timeout-messages.tsx b/services/web/frontend/js/features/pdf-preview/components/compile-timeout-messages.tsx new file mode 100644 index 0000000000..fef3ea4ad1 --- /dev/null +++ b/services/web/frontend/js/features/pdf-preview/components/compile-timeout-messages.tsx @@ -0,0 +1,250 @@ +import { memo, useCallback, useEffect, useState, useMemo } from 'react' +import * as eventTracking from '../../../infrastructure/event-tracking' +import { useDetachCompileContext } from '../../../shared/context/detach-compile-context' +import usePersistedState from '../../../shared/hooks/use-persisted-state' +import Notification from '../../../shared/components/notification' +import { Trans, useTranslation } from 'react-i18next' +import StartFreeTrialButton from '@/shared/components/start-free-trial-button' + +function CompileTimeoutMessages() { + const { + showNewCompileTimeoutUI, + isProjectOwner, + deliveryLatencies, + compiling, + showLogs, + error, + } = useDetachCompileContext() + + const { t } = useTranslation() + + const [showWarning, setShowWarning] = useState(false) + const [showChangingSoon, setShowChangingSoon] = useState(false) + const [dismissedUntilWarning, setDismissedUntilWarning] = usePersistedState< + Date | undefined + >(`has-dismissed-10s-compile-time-warning-until`) + + const segmentation = useMemo(() => { + return { + newCompileTimeout: showNewCompileTimeoutUI || 'control', + isProjectOwner, + } + }, [showNewCompileTimeoutUI, isProjectOwner]) + + const handleNewCompile = useCallback( + compileTime => { + setShowWarning(false) + setShowChangingSoon(false) + if (compileTime > 20000) { + if (showNewCompileTimeoutUI === 'changing') { + setShowChangingSoon(true) + eventTracking.sendMB('compile-time-warning-displayed', { + time: 20, + ...segmentation, + }) + } + } else if (compileTime > 10000) { + setShowChangingSoon(false) + if ( + (isProjectOwner && showNewCompileTimeoutUI === 'active') || + showNewCompileTimeoutUI === 'changing' + ) { + if ( + !dismissedUntilWarning || + new Date(dismissedUntilWarning) < new Date() + ) { + setShowWarning(true) + eventTracking.sendMB('compile-time-warning-displayed', { + time: 10, + ...segmentation, + }) + } + } else { + eventTracking.sendMB('compile-time-warning-would-display', { + time: 10, + ...segmentation, + }) + } + } + }, + [ + isProjectOwner, + showNewCompileTimeoutUI, + dismissedUntilWarning, + segmentation, + ] + ) + + const handleDismissWarning = useCallback(() => { + eventTracking.sendMB('compile-time-warning-dismissed', { + time: 10, + ...segmentation, + }) + setShowWarning(false) + const until = new Date() + until.setDate(until.getDate() + 1) // 1 day + setDismissedUntilWarning(until) + }, [setDismissedUntilWarning, segmentation]) + + const handleDismissChangingSoon = useCallback(() => { + eventTracking.sendMB('compile-time-warning-dismissed', { + time: 20, + ...segmentation, + }) + }, [segmentation]) + + useEffect(() => { + if (compiling || error || showLogs) return + window.sl_console.log( + `[compileTimeout] compiledTimeServerE2E ${ + deliveryLatencies.compileTimeServerE2E / 1000 + }s` + ) + handleNewCompile(deliveryLatencies.compileTimeServerE2E) + }, [compiling, error, showLogs, deliveryLatencies, handleNewCompile]) + + if (!window.ExposedSettings.enableSubscriptions) { + return null + } + + if (compiling || error || showLogs) { + return null + } + + if (!showWarning && !showChangingSoon) { + return null + } + + function sendInfoClickEvent() { + eventTracking.sendMB('paywall-info-click', { + 'paywall-type': 'compile-time-warning', + content: 'blog', + }) + } + + const compileTimeoutBlogLinks = [ + /* eslint-disable-next-line jsx-a11y/anchor-has-content, react/jsx-key */ + , + /* eslint-disable-next-line jsx-a11y/anchor-has-content, react/jsx-key */ + , + ] + + // if showWarning is true then the 10s warning is shown + // and if showChangingSoon is true then the 20s-60s should show + + return ( +
+ {showWarning && isProjectOwner && ( + + {t('start_free_trial_without_exclamation')} + + } + ariaLive="polite" + content={ +
+
+ + + +
+ {showNewCompileTimeoutUI === 'active' ? ( + <> + + + + {'. '} + + ) : ( + + + + )} +
+ } + type="warning" + title={t('took_a_while')} + isActionBelowContent + isDismissible + onDismiss={handleDismissWarning} + /> + )} + {showChangingSoon && + (isProjectOwner ? ( + + {t('start_free_trial_without_exclamation')} + + } + ariaLive="polite" + content={ +
+

+ {' '} + +

+
+ } + title={t('your_project_compiled_but_soon_might_not')} + type="warning" + isActionBelowContent + isDismissible + onDismiss={handleDismissChangingSoon} + /> + ) : ( + +

+ +

+

+ +

+
+ } + title={t('this_project_compiled_but_soon_might_not')} + type="warning" + isDismissible + onDismiss={handleDismissChangingSoon} + /> + ))} + + ) +} + +export default memo(CompileTimeoutMessages) diff --git a/services/web/frontend/js/features/pdf-preview/components/pdf-logs-viewer.js b/services/web/frontend/js/features/pdf-preview/components/pdf-logs-viewer.js index f41a2abd59..fd85958ea8 100644 --- a/services/web/frontend/js/features/pdf-preview/components/pdf-logs-viewer.js +++ b/services/web/frontend/js/features/pdf-preview/components/pdf-logs-viewer.js @@ -4,6 +4,7 @@ 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 TimeoutUpgradePromptNew from './timeout-upgrade-prompt-new' import PdfPreviewError from './pdf-preview-error' import PdfClearCacheButton from './pdf-clear-cache-button' import PdfDownloadFilesButton from './pdf-download-files-button' @@ -23,6 +24,7 @@ function PdfLogsViewer() { validationIssues, showLogs, stoppedOnFirstError, + showNewCompileTimeoutUI, } = useCompileContext() const { t } = useTranslation() @@ -34,9 +36,14 @@ function PdfLogsViewer() { {stoppedOnFirstError && } - {error && } - - {error === 'timedout' && } + {showNewCompileTimeoutUI && error === 'timedout' ? ( + + ) : ( + <> + {error && } + {error === 'timedout' && } + + )} {validationIssues && Object.entries(validationIssues).map(([name, issue]) => ( diff --git a/services/web/frontend/js/features/pdf-preview/components/pdf-preview-pane.js b/services/web/frontend/js/features/pdf-preview/components/pdf-preview-pane.js index 15f7cf05b9..1c038bc810 100644 --- a/services/web/frontend/js/features/pdf-preview/components/pdf-preview-pane.js +++ b/services/web/frontend/js/features/pdf-preview/components/pdf-preview-pane.js @@ -8,9 +8,10 @@ import { useDetachCompileContext as useCompileContext } from '../../../shared/co import FasterCompilesFeedback from './faster-compiles-feedback' import { PdfPreviewMessages } from './pdf-preview-messages' import CompileTimeWarning from './compile-time-warning' +import CompileTimeoutMessages from './compile-timeout-messages' function PdfPreviewPane() { - const { pdfUrl } = useCompileContext() + const { pdfUrl, showNewCompileTimeoutUI } = useCompileContext() const classes = classNames('pdf', 'full-size', { 'pdf-empty': !pdfUrl, }) @@ -18,7 +19,11 @@ function PdfPreviewPane() {
- + {showNewCompileTimeoutUI ? ( + + ) : ( + + )} }>
diff --git a/services/web/frontend/js/features/pdf-preview/components/timeout-upgrade-prompt-new.tsx b/services/web/frontend/js/features/pdf-preview/components/timeout-upgrade-prompt-new.tsx new file mode 100644 index 0000000000..545b76eb05 --- /dev/null +++ b/services/web/frontend/js/features/pdf-preview/components/timeout-upgrade-prompt-new.tsx @@ -0,0 +1,241 @@ +import { Trans, useTranslation } from 'react-i18next' +import { useDetachCompileContext } from '../../../shared/context/detach-compile-context' +import StartFreeTrialButton from '../../../shared/components/start-free-trial-button' +import { memo, useCallback } from 'react' +import PdfLogEntry from './pdf-log-entry' +import { useStopOnFirstError } from '../../../shared/hooks/use-stop-on-first-error' +import { Button } from 'react-bootstrap' +import * as eventTracking from '../../../infrastructure/event-tracking' + +function TimeoutUpgradePromptNew() { + const { + startCompile, + lastCompileOptions, + setAnimateCompileDropdownArrow, + showNewCompileTimeoutUI, + isProjectOwner, + } = useDetachCompileContext() + + const { enableStopOnFirstError } = useStopOnFirstError({ + eventSource: 'timeout-new', + }) + + const handleEnableStopOnFirstErrorClick = useCallback(() => { + enableStopOnFirstError() + startCompile({ stopOnFirstError: true }) + setAnimateCompileDropdownArrow(true) + }, [enableStopOnFirstError, startCompile, setAnimateCompileDropdownArrow]) + + if (!window.ExposedSettings.enableSubscriptions) { + return null + } + + const compileTimeChanging = showNewCompileTimeoutUI === 'changing' + + return ( + <> + + + + ) +} + +type CompileTimeoutProps = { + compileTimeChanging?: boolean + isProjectOwner: boolean +} + +function CompileTimeout({ + compileTimeChanging, + isProjectOwner, +}: CompileTimeoutProps) { + const { t } = useTranslation() + return ( + +

+ {isProjectOwner + ? t('your_project_exceeded_compile_timeout_limit_on_free_plan') + : t('this_project_exceeded_compile_timeout_limit_on_free_plan')} +

+ {isProjectOwner ? ( +

+ {compileTimeChanging ? ( + <> + {t('upgrade_for_plenty_more_compile_time')}{' '} + {t( + 'plus_additional_collaborators_document_history_track_changes_and_more' + )} + + ) : ( + <> + + + {' '} + + + )} +

+ ) : ( + , + ]} + /> + )} + + {isProjectOwner && ( +

+ + {t('start_a_free_trial')} + +

+ )} + + } + entryAriaLabel={t('your_compile_timed_out')} + level="error" + /> + ) +} + +type PreventTimeoutHelpMessageProps = { + compileTimeChanging?: boolean + lastCompileOptions: any + handleEnableStopOnFirstErrorClick: () => void +} + +function PreventTimeoutHelpMessage({ + compileTimeChanging, + lastCompileOptions, + handleEnableStopOnFirstErrorClick, +}: PreventTimeoutHelpMessageProps) { + const { t } = useTranslation() + + function sendInfoClickEvent() { + eventTracking.sendMB('paywall-info-click', { + 'paywall-type': 'compile-timeout', + content: 'blog', + }) + } + + return ( + +

+ {t('you_may_be_able_to_prevent_a_compile_timeout')} + {compileTimeChanging && ( + <> + {' '} + , + ]} + values={{ date: 'October 6 2023' }} + /> + + )} +

+

{t('common_causes_of_compile_timeouts_are')}:

+
    +
  • + , + ]} + /> +
  • +
  • + , + ]} + /> + {!lastCompileOptions.stopOnFirstError && ( + <> + {' '} + , + // eslint-disable-next-line react/jsx-key + , + ]} + />{' '} + + )} +
  • +
+

+ , + ]} + /> +

+ + } + entryAriaLabel={t('other_ways_to_prevent_compile_timeouts')} + level="raw" + /> + ) +} + +export default memo(TimeoutUpgradePromptNew) diff --git a/services/web/frontend/js/features/pdf-preview/util/compiler.js b/services/web/frontend/js/features/pdf-preview/util/compiler.js index a5a2c757b7..5ac589cbae 100644 --- a/services/web/frontend/js/features/pdf-preview/util/compiler.js +++ b/services/web/frontend/js/features/pdf-preview/util/compiler.js @@ -184,6 +184,14 @@ export default class DocumentCompiler { params.file_line_errors = 'true' } + // temporary override to force the new compile timeout + const newCompileTimeoutOverride = new URLSearchParams( + window.location.search + ).get('force_new_compile_timeout') + if (newCompileTimeoutOverride) { + params.set('force_new_compile_timeout', newCompileTimeoutOverride) + } + return params } diff --git a/services/web/frontend/js/shared/context/detach-compile-context.js b/services/web/frontend/js/shared/context/detach-compile-context.js index fd743fe5a2..c280a7fc3d 100644 --- a/services/web/frontend/js/shared/context/detach-compile-context.js +++ b/services/web/frontend/js/shared/context/detach-compile-context.js @@ -34,6 +34,7 @@ export function DetachCompileProvider({ children }) { fileList: _fileList, hasChanges: _hasChanges, highlights: _highlights, + isProjectOwner: _isProjectOwner, lastCompileOptions: _lastCompileOptions, logEntries: _logEntries, logEntryAnnotations: _logEntryAnnotations, @@ -55,6 +56,7 @@ export function DetachCompileProvider({ children }) { setStopOnValidationError: _setStopOnValidationError, showLogs: _showLogs, showCompileTimeWarning: _showCompileTimeWarning, + showNewCompileTimeoutUI: _showNewCompileTimeoutUI, showFasterCompilesFeedbackUI: _showFasterCompilesFeedbackUI, stopOnFirstError: _stopOnFirstError, stopOnValidationError: _stopOnValidationError, @@ -135,6 +137,12 @@ export function DetachCompileProvider({ children }) { 'detacher', 'detached' ) + const [isProjectOwner] = useDetachStateWatcher( + 'isProjectOwner', + _isProjectOwner, + 'detacher', + 'detached' + ) const [lastCompileOptions] = useDetachStateWatcher( 'lastCompileOptions', _lastCompileOptions, @@ -189,6 +197,12 @@ export function DetachCompileProvider({ children }) { 'detacher', 'detached' ) + const [showNewCompileTimeoutUI] = useDetachStateWatcher( + 'showNewCompileTimeoutUI', + _showNewCompileTimeoutUI, + 'detacher', + 'detached' + ) const [showFasterCompilesFeedbackUI] = useDetachStateWatcher( 'showFasterCompilesFeedbackUI', _showFasterCompilesFeedbackUI, @@ -384,6 +398,7 @@ export function DetachCompileProvider({ children }) { fileList, hasChanges, highlights, + isProjectOwner, lastCompileOptions, logEntryAnnotations, logEntries, @@ -409,6 +424,7 @@ export function DetachCompileProvider({ children }) { setStopOnValidationError, showLogs, showCompileTimeWarning, + showNewCompileTimeoutUI, showFasterCompilesFeedbackUI, startCompile, stopCompile, @@ -437,6 +453,7 @@ export function DetachCompileProvider({ children }) { fileList, hasChanges, highlights, + isProjectOwner, lastCompileOptions, logEntryAnnotations, logEntries, @@ -460,6 +477,7 @@ export function DetachCompileProvider({ children }) { setStopOnValidationError, showCompileTimeWarning, showLogs, + showNewCompileTimeoutUI, showFasterCompilesFeedbackUI, startCompile, stopCompile, diff --git a/services/web/frontend/js/shared/context/local-compile-context.js b/services/web/frontend/js/shared/context/local-compile-context.js index fc50661c48..058396bfde 100644 --- a/services/web/frontend/js/shared/context/local-compile-context.js +++ b/services/web/frontend/js/shared/context/local-compile-context.js @@ -66,6 +66,7 @@ export const CompileContextPropTypes = { setStopOnValidationError: PropTypes.func.isRequired, showCompileTimeWarning: PropTypes.bool.isRequired, showLogs: PropTypes.bool.isRequired, + showNewCompileTimeoutUI: PropTypes.string, showFasterCompilesFeedbackUI: PropTypes.bool.isRequired, stopOnFirstError: PropTypes.bool.isRequired, stopOnValidationError: PropTypes.bool.isRequired, @@ -84,7 +85,11 @@ export function LocalCompileProvider({ children }) { const { hasPremiumCompile, isProjectOwner } = useEditorContext() - const { _id: projectId, rootDocId } = useProjectContext() + const { + _id: projectId, + rootDocId, + showNewCompileTimeoutUI, + } = useProjectContext() const { pdfPreviewOpen } = useLayoutContext() @@ -555,6 +560,7 @@ export function LocalCompileProvider({ children }) { fileList, hasChanges, highlights, + isProjectOwner, lastCompileOptions, logEntryAnnotations, logEntries, @@ -580,6 +586,7 @@ export function LocalCompileProvider({ children }) { setStopOnFirstError, setStopOnValidationError, showLogs, + showNewCompileTimeoutUI, showFasterCompilesFeedbackUI, startCompile, stopCompile, @@ -609,6 +616,7 @@ export function LocalCompileProvider({ children }) { fileList, hasChanges, highlights, + isProjectOwner, lastCompileOptions, logEntries, logEntryAnnotations, @@ -629,6 +637,7 @@ export function LocalCompileProvider({ children }) { setStopOnValidationError, showCompileTimeWarning, showLogs, + showNewCompileTimeoutUI, showFasterCompilesFeedbackUI, startCompile, stopCompile, diff --git a/services/web/frontend/js/shared/context/project-context.js b/services/web/frontend/js/shared/context/project-context.js index 807c799fba..a1eafe2507 100644 --- a/services/web/frontend/js/shared/context/project-context.js +++ b/services/web/frontend/js/shared/context/project-context.js @@ -33,6 +33,7 @@ export const projectShape = { _id: PropTypes.string.isRequired, email: PropTypes.string.isRequired, }), + useNewCompileTimeoutUI: PropTypes.string, } ProjectContext.Provider.propTypes = { @@ -79,8 +80,20 @@ export function ProjectProvider({ children }) { features, publicAccesLevel: publicAccessLevel, owner, + showNewCompileTimeoutUI, } = project || projectFallback + // temporary override for new compile timeout + const forceNewCompileTimeout = new URLSearchParams( + window.location.search + ).get('force_new_compile_timeout') + const newCompileTimeoutOverride = + forceNewCompileTimeout === 'active' + ? 'active' + : forceNewCompileTimeout === 'changing' + ? 'changing' + : undefined + const value = useMemo(() => { return { _id, @@ -91,6 +104,8 @@ export function ProjectProvider({ children }) { features, publicAccessLevel, owner, + showNewCompileTimeoutUI: + newCompileTimeoutOverride || showNewCompileTimeoutUI, } }, [ _id, @@ -101,6 +116,8 @@ export function ProjectProvider({ children }) { features, publicAccessLevel, owner, + showNewCompileTimeoutUI, + newCompileTimeoutOverride, ]) return ( diff --git a/services/web/frontend/stylesheets/app/editor.less b/services/web/frontend/stylesheets/app/editor.less index 0cd536455d..8f1da9570f 100644 --- a/services/web/frontend/stylesheets/app/editor.less +++ b/services/web/frontend/stylesheets/app/editor.less @@ -756,6 +756,7 @@ CodeMirror .pdf-preview-messages { position: absolute; right: @margin-sm; + left: @margin-sm; top: @margin-xl; z-index: @zindex-popover; } diff --git a/services/web/frontend/stylesheets/app/editor/pdf.less b/services/web/frontend/stylesheets/app/editor/pdf.less index c3b70f0a49..efc961e0ce 100644 --- a/services/web/frontend/stylesheets/app/editor/pdf.less +++ b/services/web/frontend/stylesheets/app/editor/pdf.less @@ -589,3 +589,10 @@ align-items: center; justify-content: center; } + +.btn-secondary-compile-timeout-override { + color: #1b222c; + background-color: #ffffff; + border-color: #677283; + border-width: 1px; +} diff --git a/services/web/frontend/stylesheets/components/buttons.less b/services/web/frontend/stylesheets/components/buttons.less index 8bcbd42909..2e35fdd290 100755 --- a/services/web/frontend/stylesheets/components/buttons.less +++ b/services/web/frontend/stylesheets/components/buttons.less @@ -156,6 +156,35 @@ background-color: @red-10; } } +.btn-info-ghost when (@is-new-css = true) { + .btn-borderless(@blue-50, @btn-info-ghost-bg, @blue-10); +} +// Info Ghost appear as info blue with no border +.btn-info-ghost when (@is-new-css = false) { + .button-variant(@btn-info-ghost-color; @btn-info-ghost-bg; @btn-info-ghost-border); + // hover for ghost acts different from typical variants, as it's default state has no bg + &:hover { + background-color: @blue-10; + } +} +// Inline button to fit text, without link styling. +// TODO: generic class for other styles +.btn-info-ghost-inline when (@is-new-css = true) { + .btn-borderless(@blue-50, @btn-info-ghost-bg, @blue-10); + padding: 0 !important; + font-size: inherit !important; + vertical-align: inherit; +} +.btn-info-ghost-inline when (@is-new-css = false) { + .button-variant(@btn-info-ghost-color; @btn-info-ghost-bg; @btn-info-ghost-border); + // hover for ghost acts different from typical variants, as it's default state has no bg + &:hover { + background-color: @blue-10; + } + padding: 0 !important; + font-size: inherit !important; + vertical-align: inherit; +} .btn-danger-ghost when (@is-new-css = true) { .btn-borderless(@red-50, @btn-danger-ghost-bg, @red-10); } diff --git a/services/web/frontend/stylesheets/core/variables.less b/services/web/frontend/stylesheets/core/variables.less index 75db0a2bcd..836f6fff39 100644 --- a/services/web/frontend/stylesheets/core/variables.less +++ b/services/web/frontend/stylesheets/core/variables.less @@ -241,6 +241,10 @@ @btn-info-bg: @ol-blue; @btn-info-border: transparent; +@btn-info-ghost-color: @blue-50; +@btn-info-ghost-bg: transparent; +@btn-info-ghost-border: transparent; + @btn-warning-color: #fff; @btn-warning-bg: @orange; @btn-warning-border: transparent; diff --git a/services/web/frontend/stylesheets/variables/all.less b/services/web/frontend/stylesheets/variables/all.less index 1567f05d8d..b131d684c7 100644 --- a/services/web/frontend/stylesheets/variables/all.less +++ b/services/web/frontend/stylesheets/variables/all.less @@ -171,6 +171,10 @@ @btn-info-bg: @blue; @btn-info-border: transparent; +@btn-info-ghost-color: @blue-50; +@btn-info-ghost-bg: transparent; +@btn-info-ghost-border: transparent; + @btn-warning-color: #fff; @btn-warning-bg: @orange; @btn-warning-border: transparent; diff --git a/services/web/locales/en.json b/services/web/locales/en.json index 4e4b377d70..551a9d2d03 100644 --- a/services/web/locales/en.json +++ b/services/web/locales/en.json @@ -12,6 +12,7 @@ "Terms": "Terms", "Universities": "Universities", "a_custom_size_has_been_used_in_the_latex_code": "A custom size has been used in the LaTeX code.", + "a_fatal_compile_error_that_completely_blocks_compilation": "A <0>fatal compile error that completely blocks the compilation.", "a_file_with_that_name_already_exists_and_will_be_overriden": "A file with that name already exists. That file will be overwritten.", "a_more_comprehensive_list_of_keyboard_shortcuts": "A more comprehensive list of keyboard shortcuts can be found in <0>this __appName__ project template", "about": "About", @@ -100,6 +101,7 @@ "an_email_has_already_been_sent_to": "An email has already been sent to <0>__email__. Please wait and try again later.", "an_error_occurred_when_verifying_the_coupon_code": "An error occurred when verifying the coupon code", "and": "and", + "and_you_can_upgrade_for_plenty_more_compile_time": "And you can upgrade to get plenty more compile time.", "annual": "Annual", "annual_billing_enabled": "Annual billing enabled", "anonymous": "Anonymous", @@ -178,6 +180,7 @@ "built_in": "Built-In", "bulk_accept_confirm": "Are you sure you want to accept the selected __nChanges__ changes?", "bulk_reject_confirm": "Are you sure you want to reject the selected __nChanges__ changes?", + "but_note_that_free_compile_timeout_limit_will_be_reduced_on_x_date": "But note that the free compile timeout limit <0>will be reduced on __date__.", "buy_now_no_exclamation_mark": "Buy now", "by": "by", "by_registering_you_agree_to_our_terms_of_service": "By registering, you agree to our <0>terms of service.", @@ -270,6 +273,7 @@ "comment": "Comment", "commit": "Commit", "common": "Common", + "common_causes_of_compile_timeouts_are": "Common causes of compile timeouts are", "commons_plan_tooltip": "You’re on the __plan__ plan because of your affiliation with __institution__. Click to find out how to make the most of your Overleaf premium features.", "compact": "Compact", "company_name": "Company Name", @@ -284,6 +288,7 @@ "compile_timeout": "Compile timeout (minutes)", "compile_timeout_short": "Compile timeout", "compile_timeout_short_info": "This is how much time you get to compile your project on the Overleaf servers. For short and simple projects, 1 minute should be enough, but you may need longer for complex or longer projects", + "compile_timeout_will_be_reduced_project_exceeds_limit_speed_up_compile": "The compile timeout limit on our free plan <0>will be reduced on __date__ and this project currently exceeds the new limit. You may be able to fix issues to <1>speed up the compile.", "compiler": "Compiler", "compiling": "Compiling", "complete": "Complete", @@ -485,6 +490,7 @@ "empty_zip_file": "Zip doesn’t contain any file", "en": "English", "enable_managed_users": "Enable Managed Users", + "enable_stop_on_first_error_under_recompile_dropdown_menu": "Enable <0>“Stop on first error” under the <1>Recompile drop-down menu to help you find and fix errors right away.", "enabled_managed_users_set_up_sso": "You need to enable Managed Users to set up SSO.", "enabling": "Enabling", "end_of_document": "End of document", @@ -886,6 +892,7 @@ "labs_program_benefits": "__appName__ is always looking for new ways to help users work more quickly and effectively. By joining Overleaf Labs, you can participate in experiments that explore innovative ideas in the space of collaborative writing and publishing.", "labs_program_not_participating": "You are not enrolled in Labs", "language": "Language", + "large_or_high-resolution_images_taking_too_long": "Large or high-resolution images taking too long to process. You may be able to <0>optimize them.", "last_active": "Last Active", "last_active_description": "Last time a project was opened.", "last_modified": "Last Modified", @@ -915,6 +922,7 @@ "learn_more_about_emails": "<0>Learn more about managing your __appName__ emails.", "learn_more_about_link_sharing": "Learn more about Link Sharing", "learn_more_about_managed_users": "Learn more about Managed Users.", + "learn_more_about_other_causes_of_compile_timeouts": "<0>Learn more about other causes of compile timeouts and how to fix them.", "learn_more_lowercase": "learn more", "leave": "Leave", "leave_any_group_subscriptions": "Leave any group subscriptions other than the one that will be managing your account. <0>Leave them from the Subscription page.", @@ -1186,6 +1194,7 @@ "other_logs_and_files": "Other logs and files", "other_output_files": "Download other output files", "other_sessions": "Other Sessions", + "other_ways_to_prevent_compile_timeouts": "Other ways to prevent compile timeouts", "our_values": "Our values", "output_file": "Output file", "over": "over", @@ -1269,6 +1278,7 @@ "please_select_an_output_file": "Please Select an Output File", "please_set_a_password": "Please set a password", "please_set_main_file": "Please choose the main file for this project in the project menu. ", + "plus_additional_collaborators_document_history_track_changes_and_more": "(plus additional collaborators, document history, track changes, and more).", "plus_more": "plus more", "plus_upgraded_accounts_receive": "Plus with an upgraded account you get", "popular_tags": "Popular Tags", @@ -1342,6 +1352,8 @@ "react_history_tutorial_content": "To compare a range of versions, use the <0> on the versions you want at the start and end of the range. To add a label or to download a version use the options in the three-dot menu. <1>Learn more about using Overleaf History.", "react_history_tutorial_title": "History actions have a new home", "reactivate_subscription": "Reactivate your subscription", + "read_more_about_fix_prevent_timeout": "Read more about how to fix and prevent timeouts", + "read_more_about_free_compile_timeouts_servers": "Read more about changes to free compile timeouts and servers", "read_only": "Read Only", "read_only_token": "Read-Only Token", "read_write_token": "Read-Write Token", @@ -1612,8 +1624,10 @@ "sso_not_linked": "You have not linked your account to __provider__. Please log in to your account another way and link your __provider__ account via your account settings.", "sso_user_denied_access": "Cannot log in because __appName__ was not granted access to your __provider__ account. Please try again.", "standard": "Standard", + "start_a_free_trial": "Start a free trial", "start_by_adding_your_email": "Start by adding your email address.", "start_free_trial": "Start Free Trial!", + "start_free_trial_without_exclamation": "Start Free Trial", "start_using_latex_now": "start using LaTeX right now", "start_using_sl_now": "Start using __appName__ now", "state": "State", @@ -1674,6 +1688,8 @@ "tc_switch_everyone_tip": "Toggle track-changes for everyone", "tc_switch_guests_tip": "Toggle track-changes for all link-sharing guests", "tc_switch_user_tip": "Toggle track-changes for this user", + "tell_the_project_owner_and_ask_them_to_upgrade": "<0>Tell the project owner and ask them to upgrade their Overleaf plan if you need more compile time.", + "tell_the_project_owner_to_upgrade_plan_for_more_compile_time": "Tell the project owner and ask them to upgrade their Overleaf plan if you need more compile time.", "template": "Template", "template_approved_by_publisher": "This template has been approved by the publisher", "template_description": "Template Description", @@ -1722,6 +1738,8 @@ "this_field_is_required": "This field is required", "this_grants_access_to_features_2": "This grants you access to <0>__appName__ <0>__featureType__ features.", "this_is_your_template": "This is your template from your project", + "this_project_compiled_but_soon_might_not": "This project compiled, but soon it might not", + "this_project_exceeded_compile_timeout_limit_on_free_plan": "This project exceeded the compile timeout limit on our free plan.", "this_project_is_public": "This project is public and can be edited by anyone with the URL.", "this_project_is_public_read_only": "This project is public and can be viewed but not edited by anyone with the URL", "this_project_will_appear_in_your_dropbox_folder_at": "This project will appear in your Dropbox folder at ", @@ -1751,6 +1769,7 @@ "too_many_requests": "Too many requests were received in a short space of time. Please wait for a few moments and try again.", "too_many_search_results": "There are more than 100 results. Please refine your search.", "too_recently_compiled": "This project was compiled very recently, so this compile has been skipped.", + "took_a_while": "That took a while...", "toolbar_add_comment": "Add Comment", "toolbar_bullet_list": "Bullet List", "toolbar_choose_section_heading_level": "Choose section heading level", @@ -1867,7 +1886,9 @@ "updating_site": "Updating Site", "upgrade": "Upgrade", "upgrade_cc_btn": "Upgrade now, pay after 7 days", + "upgrade_for_12x_more_compile_time": "Upgrade to get 12x more compile time", "upgrade_for_longer_compiles": "Upgrade to increase your timeout limit.", + "upgrade_for_plenty_more_compile_time": "Upgrade to get plenty more compile time.", "upgrade_now": "Upgrade Now", "upgrade_to_get_feature": "Upgrade to get __feature__, plus:", "upgrade_to_track_changes": "Upgrade to Track Changes", @@ -1977,6 +1998,7 @@ "you_have_been_invited_to_transfer_management_of_your_account_to": "You have been invited to transfer management of your account to __groupName__.", "you_introed_high_number": " You’ve introduced <0>__numberOfPeople__ people to __appName__. Good job!", "you_introed_small_number": " You’ve introduced <0>__numberOfPeople__ person to __appName__. Good job, but can you get some more?", + "you_may_be_able_to_prevent_a_compile_timeout": "You may be able to prevent a compile timeout using the following tips.", "you_not_introed_anyone_to_sl": "You’ve not introduced anyone to __appName__ yet. Get sharing!", "you_plus_1": "You + 1", "you_plus_10": "You + 10", @@ -1987,6 +2009,7 @@ "your_account_is_managed_by_admin_cant_join_additional_group": "Your __appName__ account is managed by your current group admin (__admin__). This means you can’t join additional group subscriptions. <0>Read more about Managed Users.", "your_affiliation_is_confirmed": "Your <0>__institutionName__ affiliation is confirmed.", "your_browser_does_not_support_this_feature": "Sorry, your browser doesn’t support this feature. Please update your browser to its latest version.", + "your_compile_timed_out": "Your compile timed out", "your_git_access_info": "Your Git authentication tokens should be entered whenever you’re prompted for a password.", "your_git_access_info_bullet_1": "You can have up to 10 tokens.", "your_git_access_info_bullet_2": "If you reach the maximum limit, you’ll need to delete a token before you can generate a new one.", @@ -1999,6 +2022,9 @@ "your_password_has_been_successfully_changed": "Your password has been successfully changed", "your_plan": "Your plan", "your_plan_is_changing_at_term_end": "Your plan is changing to <0>__pendingPlanName__ at the end of the current billing period.", + "your_project_compiled_but_soon_might_not": "Your project compiled, but soon it might not", + "your_project_exceeded_compile_timeout_limit_on_free_plan": "Your project exceeded the compile timeout limit on our free plan.", + "your_project_near_compile_timeout_limit": "Your project is near the compile timeout limit for our free plan.", "your_projects": "Your Projects", "your_sessions": "Your Sessions", "your_subscription": "Your Subscription", diff --git a/services/web/test/unit/src/Compile/CompileManagerTests.js b/services/web/test/unit/src/Compile/CompileManagerTests.js index cf9b05163f..0ef54edc5d 100644 --- a/services/web/test/unit/src/Compile/CompileManagerTests.js +++ b/services/web/test/unit/src/Compile/CompileManagerTests.js @@ -215,6 +215,7 @@ describe('CompileManager', function () { betaProgram: 1, features: 1, splitTests: 1, + signUpDate: 1, }) .should.equal(true) }) @@ -232,6 +233,96 @@ describe('CompileManager', function () { }) }) + describe('getProjectCompileLimits with reduced compile timeout', function () { + beforeEach(function () { + this.getAssignmentForMongoUser.callsFake((user, test, cb) => { + if (test === 'compile-backend-class-n2d') { + cb(null, { variant: 'n2d' }) + } + if (test === 'compile-timeout-20s') { + cb(null, { variant: '20s' }) + } + }) + this.features = { + compileTimeout: (this.timeout = 60), + compileGroup: (this.group = 'standard'), + } + this.ProjectGetter.getProject = sinon + .stub() + .callsArgWith( + 2, + null, + (this.project = { owner_ref: (this.owner_id = 'owner-id-123') }) + ) + this.UserGetter.getUser = sinon + .stub() + .callsArgWith( + 2, + null, + (this.user = { features: this.features, analyticsId: 'abc' }) + ) + this.CompileManager.getProjectCompileLimits( + this.project_id, + this.callback + ) + }) + + describe('user is in the n2d group and compile-timeout-20s split test variant', function () { + describe('user has a timeout of more than 60s', function () { + beforeEach(function () { + this.features.compileTimeout = 120 + }) + it('should keep the users compile timeout', function () { + this.CompileManager.getProjectCompileLimits( + this.project_id, + this.callback + ) + this.callback + .calledWith(null, sinon.match({ timeout: 120 })) + .should.equal(true) + }) + }) + describe('user registered before the cut off date', function () { + beforeEach(function () { + this.features.compileTimeout = 60 + const signUpDate = new Date( + this.CompileManager.NEW_COMPILE_TIMEOUT_ENFORCED_CUTOFF + ) + signUpDate.setDate(signUpDate.getDate() - 1) + this.user.signUpDate = signUpDate + }) + it('should keep the users compile timeout', function () { + this.CompileManager.getProjectCompileLimits( + this.project_id, + this.callback + ) + this.callback + .calledWith(null, sinon.match({ timeout: 60 })) + .should.equal(true) + }) + }) + describe('user registered after the cut off date', function () { + beforeEach(function () { + this.timeout = 60 + const signUpDate = new Date( + this.CompileManager.NEW_COMPILE_TIMEOUT_ENFORCED_CUTOFF + ) + signUpDate.setDate(signUpDate.getDate() + 1) + this.user.signUpDate = signUpDate + }) + it('should reduce compile timeout to 20s', function () { + this.CompileManager.getProjectCompileLimits( + this.project_id, + this.callback + ) + this.callback + .calledWith(null, sinon.match({ timeout: 20 })) + .should.equal(true) + }) + }) + }) + }) + describe('compileBackendClass', function () { beforeEach(function () { this.features = { diff --git a/services/web/test/unit/src/Editor/EditorHttpControllerTests.js b/services/web/test/unit/src/Editor/EditorHttpControllerTests.js index e8a443018f..5c935d39db 100644 --- a/services/web/test/unit/src/Editor/EditorHttpControllerTests.js +++ b/services/web/test/unit/src/Editor/EditorHttpControllerTests.js @@ -131,6 +131,13 @@ describe('EditorHttpController', function () { notFound: sinon.stub(), unprocessableEntity: sinon.stub(), } + this.SplitTestHandler = { + promises: { + getAssignmentForMongoUser: sinon + .stub() + .resolves({ variant: 'default' }), + }, + } this.EditorHttpController = SandboxedModule.require(MODULE_PATH, { requires: { '../Project/ProjectDeleter': this.ProjectDeleter, @@ -150,6 +157,8 @@ describe('EditorHttpController', function () { this.ProjectEntityUpdateHandler, '../Docstore/DocstoreManager': this.DocstoreManager, '../Errors/HttpErrorHandler': this.HttpErrorHandler, + '../SplitTests/SplitTestHandler': this.SplitTestHandler, + '../Compile/CompileManager': {}, }, }) }) diff --git a/services/web/types/window.ts b/services/web/types/window.ts index cae4ae2db4..f9dd073ffa 100644 --- a/services/web/types/window.ts +++ b/services/web/types/window.ts @@ -8,6 +8,9 @@ declare global { // eslint-disable-next-line no-unused-vars interface Window { csrfToken: string + sl_console: { + log: (message: string) => void + } sl_debugging: boolean user: User user_id?: string