diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json
index 44fd025aa0..1fde7d0468 100644
--- a/services/web/frontend/extracted-translations.json
+++ b/services/web/frontend/extracted-translations.json
@@ -173,6 +173,9 @@
"loading": "",
"loading_recent_github_commits": "",
"log_entry_description": "",
+ "log_entry_maximum_entries": "",
+ "log_entry_maximum_entries_message": "",
+ "log_entry_maximum_entries_title": "",
"log_hint_extra_info": "",
"logs_pane_info_message": "",
"logs_pane_info_message_popup": "",
diff --git a/services/web/frontend/js/features/preview/components/preview-log-entry-header.js b/services/web/frontend/js/features/preview/components/preview-log-entry-header.js
new file mode 100644
index 0000000000..d5cf82d7f2
--- /dev/null
+++ b/services/web/frontend/js/features/preview/components/preview-log-entry-header.js
@@ -0,0 +1,142 @@
+import PropTypes from 'prop-types'
+import classNames from 'classnames'
+import { useState, useRef } from 'react'
+import { useTranslation } from 'react-i18next'
+import { OverlayTrigger, Tooltip } from 'react-bootstrap'
+import useResizeObserver from '../hooks/use-resize-observer'
+import Icon from '../../../shared/components/icon'
+
+function PreviewLogEntryHeader({
+ sourceLocation,
+ level,
+ headerTitle,
+ headerIcon,
+ logType,
+ showSourceLocationLink = true,
+ showCloseButton = false,
+ onSourceLocationClick,
+ onClose,
+}) {
+ const { t } = useTranslation()
+ const logLocationSpanRef = useRef()
+ const [locationSpanOverflown, setLocationSpanOverflown] = useState(false)
+
+ useResizeObserver(
+ logLocationSpanRef,
+ locationSpanOverflown,
+ checkLocationSpanOverflow
+ )
+
+ const file = sourceLocation ? sourceLocation.file : null
+ const line = sourceLocation ? sourceLocation.line : null
+ const logEntryHeaderClasses = classNames('log-entry-header', {
+ 'log-entry-header-error': level === 'error',
+ 'log-entry-header-warning': level === 'warning',
+ 'log-entry-header-typesetting': level === 'typesetting',
+ 'log-entry-header-raw': level === 'raw',
+ 'log-entry-header-success': level === 'success',
+ })
+ const logEntryLocationBtnClasses = classNames('log-entry-header-link', {
+ 'log-entry-header-link-error': level === 'error',
+ '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-success': level === 'success',
+ })
+ const headerLogLocationTitle = t('navigate_log_source', {
+ location: file + (line ? `, ${line}` : ''),
+ })
+
+ function checkLocationSpanOverflow(observedElement) {
+ const spanEl = observedElement.target
+ const isOverflowing = spanEl.scrollWidth > spanEl.clientWidth
+ setLocationSpanOverflown(isOverflowing)
+ }
+
+ const locationLinkText =
+ showSourceLocationLink && file ? `${file}${line ? `, ${line}` : ''}` : null
+
+ // Because we want an ellipsis on the left-hand side (e.g. "...longfilename.tex"), the
+ // `log-entry-header-link-location` class has text laid out from right-to-left using the CSS
+ // rule `direction: rtl;`.
+ // This works most of the times, except when the first character of the filename is considered
+ // a punctuation mark, like `/` (e.g. `/foo/bar/baz.sty`). In this case, because of
+ // right-to-left writing rules, the punctuation mark is moved to the right-side of the string,
+ // resulting in `...bar/baz.sty/` instead of `...bar/baz.sty`.
+ // To avoid this edge-case, we wrap the `logLocationLinkText` in two directional formatting
+ // characters:
+ // * \u202A LEFT-TO-RIGHT EMBEDDING Treat the following text as embedded left-to-right.
+ // * \u202C POP DIRECTIONAL FORMATTING End the scope of the last LRE, RLE, RLO, or LRO.
+ // This essentially tells the browser that, althought the text is laid out from right-to-left,
+ // the wrapped portion of text should follow left-to-right writing rules.
+ const locationLink = locationLinkText ? (
+
+ ) : null
+
+ const locationTooltip =
+ locationSpanOverflown && locationLinkText ? (
+
+ {locationLinkText}
+
+ ) : null
+
+ var headerTitleText = logType ? `${logType} ${headerTitle}` : headerTitle
+
+ return (
+
+ {headerIcon ? (
+ {headerIcon}
+ ) : null}
+ {headerTitleText}
+ {locationTooltip ? (
+
+ {locationLink}
+
+ ) : (
+ locationLink
+ )}
+ {showCloseButton ? (
+
+ ) : null}
+
+ )
+}
+
+PreviewLogEntryHeader.propTypes = {
+ sourceLocation: PropTypes.shape({
+ file: PropTypes.string,
+ // `line should be either a number or null (i.e. not required), but currently sometimes we get
+ // an empty string (from BibTeX errors), which is why we're using `any` here. We should revert
+ // to PropTypes.number (not required) once we fix that.
+ line: PropTypes.any,
+ column: PropTypes.any,
+ }),
+ level: PropTypes.string.isRequired,
+ headerTitle: PropTypes.string,
+ headerIcon: PropTypes.element,
+ logType: PropTypes.string,
+ showSourceLocationLink: PropTypes.bool,
+ showCloseButton: PropTypes.bool,
+ onSourceLocationClick: PropTypes.func,
+ onClose: PropTypes.func,
+}
+
+export default PreviewLogEntryHeader
diff --git a/services/web/frontend/js/features/preview/components/preview-logs-pane-entry.js b/services/web/frontend/js/features/preview/components/preview-logs-pane-entry.js
index 04e0f2ebab..ff5baf43bc 100644
--- a/services/web/frontend/js/features/preview/components/preview-logs-pane-entry.js
+++ b/services/web/frontend/js/features/preview/components/preview-logs-pane-entry.js
@@ -1,12 +1,11 @@
-import { useState, useRef } from 'react'
import PropTypes from 'prop-types'
import classNames from 'classnames'
import { useTranslation } from 'react-i18next'
-import { OverlayTrigger, Tooltip } from 'react-bootstrap'
import useExpandCollapse from '../../../shared/hooks/use-expand-collapse'
-import useResizeObserver from '../hooks/use-resize-observer'
import Icon from '../../../shared/components/icon'
+import PreviewLogEntryHeader from './preview-log-entry-header'
+
function PreviewLogsPaneEntry({
headerTitle,
headerIcon,
@@ -54,120 +53,6 @@ function PreviewLogsPaneEntry({
)
}
-function PreviewLogEntryHeader({
- sourceLocation,
- level,
- headerTitle,
- headerIcon,
- logType,
- showSourceLocationLink = true,
- showCloseButton = false,
- onSourceLocationClick,
- onClose,
-}) {
- const { t } = useTranslation()
- const logLocationSpanRef = useRef()
- const [locationSpanOverflown, setLocationSpanOverflown] = useState(false)
-
- useResizeObserver(
- logLocationSpanRef,
- locationSpanOverflown,
- checkLocationSpanOverflow
- )
-
- const file = sourceLocation ? sourceLocation.file : null
- const line = sourceLocation ? sourceLocation.line : null
- const logEntryHeaderClasses = classNames('log-entry-header', {
- 'log-entry-header-error': level === 'error',
- 'log-entry-header-warning': level === 'warning',
- 'log-entry-header-typesetting': level === 'typesetting',
- 'log-entry-header-raw': level === 'raw',
- 'log-entry-header-success': level === 'success',
- })
- const logEntryLocationBtnClasses = classNames('log-entry-header-link', {
- 'log-entry-header-link-error': level === 'error',
- '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-success': level === 'success',
- })
- const headerLogLocationTitle = t('navigate_log_source', {
- location: file + (line ? `, ${line}` : ''),
- })
-
- function checkLocationSpanOverflow(observedElement) {
- const spanEl = observedElement.target
- const isOverflowing = spanEl.scrollWidth > spanEl.clientWidth
- setLocationSpanOverflown(isOverflowing)
- }
-
- const locationLinkText =
- showSourceLocationLink && file ? `${file}${line ? `, ${line}` : ''}` : null
-
- // Because we want an ellipsis on the left-hand side (e.g. "...longfilename.tex"), the
- // `log-entry-header-link-location` class has text laid out from right-to-left using the CSS
- // rule `direction: rtl;`.
- // This works most of the times, except when the first character of the filename is considered
- // a punctuation mark, like `/` (e.g. `/foo/bar/baz.sty`). In this case, because of
- // right-to-left writing rules, the punctuation mark is moved to the right-side of the string,
- // resulting in `...bar/baz.sty/` instead of `...bar/baz.sty`.
- // To avoid this edge-case, we wrap the `logLocationLinkText` in two directional formatting
- // characters:
- // * \u202A LEFT-TO-RIGHT EMBEDDING Treat the following text as embedded left-to-right.
- // * \u202C POP DIRECTIONAL FORMATTING End the scope of the last LRE, RLE, RLO, or LRO.
- // This essentially tells the browser that, althought the text is laid out from right-to-left,
- // the wrapped portion of text should follow left-to-right writing rules.
- const locationLink = locationLinkText ? (
-
- ) : null
-
- const locationTooltip =
- locationSpanOverflown && locationLinkText ? (
-
- {locationLinkText}
-
- ) : null
-
- var headerTitleText = logType ? `${logType} ${headerTitle}` : headerTitle
-
- return (
-
- {headerIcon ? (
- {headerIcon}
- ) : null}
- {headerTitleText}
- {locationTooltip ? (
-
- {locationLink}
-
- ) : (
- locationLink
- )}
- {showCloseButton ? (
-
- ) : null}
-
- )
-}
-
function PreviewLogEntryContent({
rawContent,
formattedContent,
@@ -233,25 +118,6 @@ function PreviewLogEntryContent({
)
}
-PreviewLogEntryHeader.propTypes = {
- sourceLocation: PropTypes.shape({
- file: PropTypes.string,
- // `line should be either a number or null (i.e. not required), but currently sometimes we get
- // an empty string (from BibTeX errors), which is why we're using `any` here. We should revert
- // to PropTypes.number (not required) once we fix that.
- line: PropTypes.any,
- column: PropTypes.any,
- }),
- level: PropTypes.string.isRequired,
- headerTitle: PropTypes.string,
- headerIcon: PropTypes.element,
- logType: PropTypes.string,
- showSourceLocationLink: PropTypes.bool,
- showCloseButton: PropTypes.bool,
- onSourceLocationClick: PropTypes.func,
- onClose: PropTypes.func,
-}
-
PreviewLogEntryContent.propTypes = {
rawContent: PropTypes.string,
formattedContent: PropTypes.node,
diff --git a/services/web/frontend/js/features/preview/components/preview-logs-pane-max-entries.js b/services/web/frontend/js/features/preview/components/preview-logs-pane-max-entries.js
new file mode 100644
index 0000000000..6e588cd7ff
--- /dev/null
+++ b/services/web/frontend/js/features/preview/components/preview-logs-pane-max-entries.js
@@ -0,0 +1,33 @@
+import PropTypes from 'prop-types'
+import { Trans, useTranslation } from 'react-i18next'
+import PreviewLogEntryHeader from './preview-log-entry-header'
+import Icon from '../../../shared/components/icon'
+
+function PreviewLogsPaneMaxEntries({ totalEntries, entriesShown }) {
+ const { t } = useTranslation()
+
+ const title = t('log_entry_maximum_entries_title', {
+ total: totalEntries,
+ displayed: entriesShown,
+ })
+
+ return (
+
+
+
+ {' '}
+ ]}
+ />
+
+
+ )
+}
+
+PreviewLogsPaneMaxEntries.propTypes = {
+ totalEntries: PropTypes.number,
+ entriesShown: PropTypes.number,
+}
+
+export default PreviewLogsPaneMaxEntries
diff --git a/services/web/frontend/js/features/preview/components/preview-logs-pane.js b/services/web/frontend/js/features/preview/components/preview-logs-pane.js
index 4256b3a791..d9f9c0d6b0 100644
--- a/services/web/frontend/js/features/preview/components/preview-logs-pane.js
+++ b/services/web/frontend/js/features/preview/components/preview-logs-pane.js
@@ -2,6 +2,7 @@ import PropTypes from 'prop-types'
import { useTranslation } from 'react-i18next'
import { Dropdown } from 'react-bootstrap'
import PreviewLogsPaneEntry from './preview-logs-pane-entry'
+import PreviewLogsPaneMaxEntries from './preview-logs-pane-max-entries'
import PreviewValidationIssue from './preview-validation-issue'
import PreviewDownloadFileList from './preview-download-file-list'
import PreviewError from './preview-error'
@@ -9,6 +10,8 @@ import Icon from '../../../shared/components/icon'
import usePersistedState from '../../../shared/hooks/use-persisted-state'
import ControlledDropdown from '../../../shared/components/controlled-dropdown'
+const LOG_PREVIEW_LIMIT = 100
+
function PreviewLogsPane({
logEntries = { all: [], errors: [], warnings: [], typesetting: [] },
rawLog = '',
@@ -121,7 +124,14 @@ const PreviewValidationIssues = ({ validationIssues }) => {
const PreviewLogEntries = ({ logEntries, onLogEntryLocationClick }) => {
const { t } = useTranslation()
const nowTS = Date.now()
- return logEntries.map((logEntry, index) => (
+
+ const totalLogEntries = logEntries.length
+
+ if (totalLogEntries > LOG_PREVIEW_LIMIT) {
+ logEntries = logEntries.slice(0, 100)
+ }
+
+ logEntries = logEntries.map((logEntry, index) => (
{
onSourceLocationClick={onLogEntryLocationClick}
/>
))
+
+ if (totalLogEntries > LOG_PREVIEW_LIMIT) {
+ // Prepend log limit exceeded message to logs array
+ logEntries = [
+ ,
+ ].concat(logEntries)
+ }
+
+ return logEntries
}
function AutoCompileLintingErrorEntry() {
diff --git a/services/web/locales/en.json b/services/web/locales/en.json
index bf6f99292d..d21003bced 100644
--- a/services/web/locales/en.json
+++ b/services/web/locales/en.json
@@ -27,6 +27,10 @@
"view_error": "View error",
"view_error_plural": "View all errors",
"log_entry_description": "Log entry with level: __level__",
+ "log_entry_maximum_entries": "Maximum log entries limit hit",
+ "log_entry_maximum_entries_title": "__total__ issues total. Showing the first __displayed__",
+ "log_entry_maximum_entries_message": "<0>Tip0>: Try to fix the first error and recompile. Often one error causes many later error messages",
+ "log_entry_description": "Log entry with level: __level__",
"navigate_log_source": "Navigate to log position in source code: __location__",
"other_output_files": "Download other output files",
"refresh": "Refresh",
diff --git a/services/web/test/frontend/features/preview/components/preview-logs-pane.test.js b/services/web/test/frontend/features/preview/components/preview-logs-pane.test.js
index e4fec64547..ceee93a3ae 100644
--- a/services/web/test/frontend/features/preview/components/preview-logs-pane.test.js
+++ b/services/web/test/frontend/features/preview/components/preview-logs-pane.test.js
@@ -113,6 +113,35 @@ entering extended mode
})
})
+ describe('with over 100 log entries', function () {
+ it('renders only 100 with a warning message', function () {
+ const errors = Array(200).fill(sampleError1)
+ const logEntries = {
+ all: [...errors, ...warnings, ...typesetting],
+ errors,
+ warnings,
+ typesetting,
+ }
+
+ renderWithEditorContext(
+
+ )
+
+ const displayedLogEntries = screen.getAllByLabelText(
+ `Log entry with level`,
+ { exact: false }
+ )
+ screen.getByLabelText(`Maximum log entries limit hit`)
+ // should only show the first 100 errors and stop
+ expect(displayedLogEntries).to.have.lengthOf(100)
+ })
+ })
+
describe('with validation issues', function () {
const sampleValidationIssues = {
sizeCheck: {