mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-29 13:53:40 -05:00
Migrate log entry component to new PDF preview (#5478)
* Migrate log entry component to new PDF preview * Add a test for expandable log content GitOrigin-RevId: 3e2154983c1ea03b5db44c87822e7043c4aa2cfe
This commit is contained in:
parent
f7ef2532e0
commit
12eab99990
5 changed files with 211 additions and 3 deletions
|
@ -0,0 +1,37 @@
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import PdfLogEntryRawContent from './pdf-log-entry-raw-content'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
|
export default function PdfLogEntryContent({
|
||||||
|
rawContent,
|
||||||
|
formattedContent,
|
||||||
|
extraInfoURL,
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="log-entry-content">
|
||||||
|
{formattedContent && (
|
||||||
|
<div className="log-entry-formatted-content">{formattedContent}</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{extraInfoURL && (
|
||||||
|
<div className="log-entry-content-link">
|
||||||
|
<a href={extraInfoURL} target="_blank" rel="noopener">
|
||||||
|
{t('log_hint_extra_info')}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{rawContent && (
|
||||||
|
<PdfLogEntryRawContent rawContent={rawContent} collapsedSize={150} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
PdfLogEntryContent.propTypes = {
|
||||||
|
rawContent: PropTypes.string,
|
||||||
|
formattedContent: PropTypes.node,
|
||||||
|
extraInfoURL: PropTypes.string,
|
||||||
|
}
|
|
@ -0,0 +1,66 @@
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { useResizeObserver } from '../hooks/use-resize-observer'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import classNames from 'classnames'
|
||||||
|
import { Button } from 'react-bootstrap'
|
||||||
|
import Icon from '../../../shared/components/icon'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
|
export default function PdfLogEntryRawContent({
|
||||||
|
rawContent,
|
||||||
|
collapsedSize = 0,
|
||||||
|
}) {
|
||||||
|
const [expanded, setExpanded] = useState(false)
|
||||||
|
const [needsExpander, setNeedsExpander] = useState(false)
|
||||||
|
|
||||||
|
const containerRef = useResizeObserver(entry => {
|
||||||
|
setNeedsExpander(entry.target.scrollHeight > collapsedSize)
|
||||||
|
})
|
||||||
|
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="log-entry-content-raw-container">
|
||||||
|
<div
|
||||||
|
className="expand-collapse-container"
|
||||||
|
style={{
|
||||||
|
height: expanded ? 'auto' : collapsedSize,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<pre className="log-entry-content-raw" ref={containerRef}>
|
||||||
|
{rawContent.trim()}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{needsExpander && (
|
||||||
|
<div
|
||||||
|
className={classNames('log-entry-content-button-container', {
|
||||||
|
'log-entry-content-button-container-collapsed': !expanded,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
bsSize="xs"
|
||||||
|
bsStyle="default"
|
||||||
|
className="log-entry-btn-expand-collapse"
|
||||||
|
onClick={() => setExpanded(value => !value)}
|
||||||
|
>
|
||||||
|
{expanded ? (
|
||||||
|
<>
|
||||||
|
<Icon type="angle-up" /> {t('collapse')}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Icon type="angle-down" /> {t('expand')}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
PdfLogEntryRawContent.propTypes = {
|
||||||
|
rawContent: PropTypes.string.isRequired,
|
||||||
|
collapsedSize: PropTypes.number,
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import classNames from 'classnames'
|
||||||
|
import { memo, useCallback } from 'react'
|
||||||
|
import PreviewLogEntryHeader from '../../preview/components/preview-log-entry-header'
|
||||||
|
import PdfLogEntryContent from './pdf-log-entry-content'
|
||||||
|
|
||||||
|
function PdfLogEntry({
|
||||||
|
headerTitle,
|
||||||
|
headerIcon,
|
||||||
|
rawContent,
|
||||||
|
logType,
|
||||||
|
formattedContent,
|
||||||
|
extraInfoURL,
|
||||||
|
level,
|
||||||
|
sourceLocation,
|
||||||
|
showSourceLocationLink = true,
|
||||||
|
showCloseButton = false,
|
||||||
|
entryAriaLabel = null,
|
||||||
|
customClass,
|
||||||
|
onSourceLocationClick,
|
||||||
|
onClose,
|
||||||
|
}) {
|
||||||
|
const handleLogEntryLinkClick = useCallback(
|
||||||
|
event => {
|
||||||
|
event.preventDefault()
|
||||||
|
onSourceLocationClick(sourceLocation)
|
||||||
|
},
|
||||||
|
[onSourceLocationClick, sourceLocation]
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classNames('log-entry', customClass)}
|
||||||
|
aria-label={entryAriaLabel}
|
||||||
|
>
|
||||||
|
<PreviewLogEntryHeader
|
||||||
|
level={level}
|
||||||
|
sourceLocation={sourceLocation}
|
||||||
|
headerTitle={headerTitle}
|
||||||
|
headerIcon={headerIcon}
|
||||||
|
logType={logType}
|
||||||
|
showSourceLocationLink={showSourceLocationLink}
|
||||||
|
onSourceLocationClick={handleLogEntryLinkClick}
|
||||||
|
showCloseButton={showCloseButton}
|
||||||
|
onClose={onClose}
|
||||||
|
/>
|
||||||
|
{(rawContent || formattedContent) && (
|
||||||
|
<PdfLogEntryContent
|
||||||
|
rawContent={rawContent}
|
||||||
|
formattedContent={formattedContent}
|
||||||
|
extraInfoURL={extraInfoURL}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
PdfLogEntry.propTypes = {
|
||||||
|
sourceLocation: PreviewLogEntryHeader.propTypes.sourceLocation,
|
||||||
|
headerTitle: PropTypes.string,
|
||||||
|
headerIcon: PropTypes.element,
|
||||||
|
rawContent: PropTypes.string,
|
||||||
|
logType: PropTypes.string,
|
||||||
|
formattedContent: PropTypes.node,
|
||||||
|
extraInfoURL: PropTypes.string,
|
||||||
|
level: PropTypes.oneOf(['error', 'warning', 'typesetting', 'raw', 'success'])
|
||||||
|
.isRequired,
|
||||||
|
customClass: PropTypes.string,
|
||||||
|
showSourceLocationLink: PropTypes.bool,
|
||||||
|
showCloseButton: PropTypes.bool,
|
||||||
|
entryAriaLabel: PropTypes.string,
|
||||||
|
onSourceLocationClick: PropTypes.func,
|
||||||
|
onClose: PropTypes.func,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(PdfLogEntry)
|
|
@ -1,8 +1,8 @@
|
||||||
import { memo, useCallback } from 'react'
|
import { memo, useCallback } from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import PreviewLogsPaneEntry from '../../preview/components/preview-logs-pane-entry'
|
|
||||||
import PreviewLogsPaneMaxEntries from '../../preview/components/preview-logs-pane-max-entries'
|
import PreviewLogsPaneMaxEntries from '../../preview/components/preview-logs-pane-max-entries'
|
||||||
|
import PdfLogEntry from './pdf-log-entry'
|
||||||
|
|
||||||
const LOG_PREVIEW_LIMIT = 100
|
const LOG_PREVIEW_LIMIT = 100
|
||||||
|
|
||||||
|
@ -23,13 +23,12 @@ function PdfLogsEntries({ entries }) {
|
||||||
<>
|
<>
|
||||||
{entries.length > LOG_PREVIEW_LIMIT && (
|
{entries.length > LOG_PREVIEW_LIMIT && (
|
||||||
<PreviewLogsPaneMaxEntries
|
<PreviewLogsPaneMaxEntries
|
||||||
key={`${entries.length}-${LOG_PREVIEW_LIMIT}`}
|
|
||||||
totalEntries={entries.length}
|
totalEntries={entries.length}
|
||||||
entriesShown={LOG_PREVIEW_LIMIT}
|
entriesShown={LOG_PREVIEW_LIMIT}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{logEntries.map(logEntry => (
|
{logEntries.map(logEntry => (
|
||||||
<PreviewLogsPaneEntry
|
<PdfLogEntry
|
||||||
key={logEntry.key}
|
key={logEntry.key}
|
||||||
headerTitle={logEntry.message}
|
headerTitle={logEntry.message}
|
||||||
rawContent={logEntry.content}
|
rawContent={logEntry.content}
|
||||||
|
|
|
@ -148,6 +148,7 @@ describe('<PdfPreview/>', function () {
|
||||||
// xhrMock.teardown()
|
// xhrMock.teardown()
|
||||||
fetchMock.reset()
|
fetchMock.reset()
|
||||||
localStorage.clear()
|
localStorage.clear()
|
||||||
|
sinon.restore()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('renders the PDF preview', async function () {
|
it('renders the PDF preview', async function () {
|
||||||
|
@ -270,6 +271,35 @@ describe('<PdfPreview/>', function () {
|
||||||
expect(fetchMock.called('express:/build/:file')).to.be.false // TODO: actual path
|
expect(fetchMock.called('express:/build/:file')).to.be.false // TODO: actual path
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('displays expandable raw logs', async function () {
|
||||||
|
mockCompile()
|
||||||
|
mockBuildFile()
|
||||||
|
|
||||||
|
// pretend that the content is large enough to trigger a "collapse"
|
||||||
|
// (in jsdom these values are always zero)
|
||||||
|
sinon.stub(HTMLElement.prototype, 'scrollHeight').value(500)
|
||||||
|
sinon.stub(HTMLElement.prototype, 'scrollWidth').value(500)
|
||||||
|
|
||||||
|
renderWithEditorContext(<PdfPreview />, { scope })
|
||||||
|
|
||||||
|
// wait for "compile on load" to finish
|
||||||
|
await screen.findByRole('button', { name: 'Compiling…' })
|
||||||
|
await screen.findByRole('button', { name: 'Recompile' })
|
||||||
|
|
||||||
|
const logsButton = screen.getByRole('button', { name: 'View logs' })
|
||||||
|
logsButton.click()
|
||||||
|
|
||||||
|
await screen.findByRole('button', { name: 'View PDF' })
|
||||||
|
|
||||||
|
// expand the log
|
||||||
|
const expandButton = screen.getByRole('button', { name: 'Expand' })
|
||||||
|
expandButton.click()
|
||||||
|
|
||||||
|
// collapse the log
|
||||||
|
const collapseButton = screen.getByRole('button', { name: 'Collapse' })
|
||||||
|
collapseButton.click()
|
||||||
|
})
|
||||||
|
|
||||||
it('displays error messages if there were validation problems', async function () {
|
it('displays error messages if there were validation problems', async function () {
|
||||||
const validationProblems = {
|
const validationProblems = {
|
||||||
sizeCheck: {
|
sizeCheck: {
|
||||||
|
|
Loading…
Reference in a new issue