From 94773e898e69388db3f7e198a7effc5234983e2f Mon Sep 17 00:00:00 2001 From: June Kelly Date: Wed, 6 Oct 2021 09:33:18 +0100 Subject: [PATCH] Merge pull request #5280 from overleaf/hb-max-log-entries-display Display only a max of 100 log entries GitOrigin-RevId: 7a9a9d6824eda72dd6c19024d1e0ff6d25bebf49 --- .../web/frontend/extracted-translations.json | 3 + .../components/preview-log-entry-header.js | 142 ++++++++++++++++++ .../components/preview-logs-pane-entry.js | 138 +---------------- .../preview-logs-pane-max-entries.js | 33 ++++ .../preview/components/preview-logs-pane.js | 25 ++- services/web/locales/en.json | 4 + .../components/preview-logs-pane.test.js | 29 ++++ 7 files changed, 237 insertions(+), 137 deletions(-) create mode 100644 services/web/frontend/js/features/preview/components/preview-log-entry-header.js create mode 100644 services/web/frontend/js/features/preview/components/preview-logs-pane-max-entries.js 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>Tip: 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: {