preview-log-entry.js 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. import React from 'react'
  2. import PropTypes from 'prop-types'
  3. import classNames from 'classnames'
  4. import { useTranslation } from 'react-i18next'
  5. import useExpandCollapse from '../../../shared/hooks/use-expand-collapse'
  6. import Icon from '../../../shared/components/icon'
  7. function PreviewLogEntry({
  8. file,
  9. line,
  10. message,
  11. content,
  12. column,
  13. humanReadableHintComponent,
  14. extraInfoURL,
  15. level,
  16. onLogEntryLinkClick
  17. }) {
  18. const { t } = useTranslation()
  19. function handleLogEntryLinkClick() {
  20. onLogEntryLinkClick({ file, line, column })
  21. }
  22. const logEntryDescription = t('log_entry_description', {
  23. level: level
  24. })
  25. return (
  26. <div className="log-entry" aria-label={logEntryDescription}>
  27. <PreviewLogEntryHeader
  28. level={level}
  29. file={file}
  30. line={line}
  31. message={message}
  32. onLogEntryLinkClick={handleLogEntryLinkClick}
  33. />
  34. {content ? (
  35. <PreviewLogEntryContent
  36. content={content}
  37. humanReadableHintComponent={humanReadableHintComponent}
  38. extraInfoURL={extraInfoURL}
  39. />
  40. ) : null}
  41. </div>
  42. )
  43. }
  44. function PreviewLogEntryHeader({
  45. level,
  46. file,
  47. line,
  48. message,
  49. onLogEntryLinkClick
  50. }) {
  51. const { t } = useTranslation()
  52. const logEntryHeaderClasses = classNames('log-entry-header', {
  53. 'log-entry-header-error': level === 'error',
  54. 'log-entry-header-warning': level === 'warning',
  55. 'log-entry-header-typesetting': level === 'typesetting'
  56. })
  57. const headerLinkBtnTitle = t('navigate_log_source', {
  58. location: file + (line ? `, ${line}` : '')
  59. })
  60. return (
  61. <header className={logEntryHeaderClasses}>
  62. <h3 className="log-entry-header-title">{message}</h3>
  63. {file ? (
  64. <button
  65. className="btn-inline-link log-entry-header-link"
  66. type="button"
  67. title={headerLinkBtnTitle}
  68. onClick={onLogEntryLinkClick}
  69. >
  70. <Icon type="chain" />
  71. &nbsp;
  72. <span>{file}</span>
  73. {line ? <span>, {line}</span> : null}
  74. </button>
  75. ) : null}
  76. </header>
  77. )
  78. }
  79. function PreviewLogEntryContent({
  80. content,
  81. humanReadableHintComponent,
  82. extraInfoURL
  83. }) {
  84. const { isExpanded, expandableProps, toggleProps } = useExpandCollapse({
  85. collapsedSize: 150,
  86. classes: {
  87. container: 'log-entry-content-raw-expandable-container'
  88. }
  89. })
  90. const logContentClasses = classNames('log-entry-content-raw', {
  91. 'log-entry-content-raw-collapsed': !isExpanded
  92. })
  93. const buttonContainerClasses = classNames(
  94. 'log-entry-content-button-container',
  95. {
  96. 'log-entry-content-button-container-collapsed': !isExpanded
  97. }
  98. )
  99. const { t } = useTranslation()
  100. return (
  101. <div className="log-entry-content">
  102. <div {...expandableProps}>
  103. <pre className={logContentClasses}>{content.trim()}</pre>
  104. <div className={buttonContainerClasses}>
  105. <button
  106. type="button"
  107. className="btn btn-xs btn-default log-entry-btn-expand-collapse"
  108. {...toggleProps}
  109. >
  110. {isExpanded ? (
  111. <>
  112. <Icon type="angle-up" /> {t('collapse')}
  113. </>
  114. ) : (
  115. <>
  116. <Icon type="angle-down" /> {t('expand')}
  117. </>
  118. )}
  119. </button>
  120. </div>
  121. </div>
  122. {humanReadableHintComponent ? (
  123. <div className="log-entry-human-readable-hint">
  124. {humanReadableHintComponent}
  125. </div>
  126. ) : null}
  127. {extraInfoURL ? (
  128. <div className="log-entry-human-readable-hint-link">
  129. <a
  130. href={extraInfoURL}
  131. target="_blank"
  132. className="log-entry-human-readable-hint-link"
  133. >
  134. {t('log_hint_extra_info')}
  135. </a>
  136. </div>
  137. ) : null}
  138. </div>
  139. )
  140. }
  141. PreviewLogEntryHeader.propTypes = {
  142. level: PropTypes.string.isRequired,
  143. file: PropTypes.string,
  144. line: PropTypes.any,
  145. message: PropTypes.string,
  146. onLogEntryLinkClick: PropTypes.func.isRequired
  147. }
  148. PreviewLogEntryContent.propTypes = {
  149. content: PropTypes.string.isRequired,
  150. humanReadableHintComponent: PropTypes.oneOfType([
  151. PropTypes.arrayOf(PropTypes.node),
  152. PropTypes.element
  153. ]),
  154. extraInfoURL: PropTypes.string
  155. }
  156. PreviewLogEntry.propTypes = {
  157. file: PropTypes.string,
  158. // `line should be either a number or null (i.e. not required), but currently sometimes we get
  159. // an empty string (from BibTeX errors), which is why we're using `any` here. We should revert
  160. // to PropTypes.number (not required) once we fix that.
  161. line: PropTypes.any,
  162. column: PropTypes.any,
  163. message: PropTypes.string,
  164. content: PropTypes.string,
  165. humanReadableHintComponent: PropTypes.node,
  166. extraInfoURL: PropTypes.string,
  167. level: PropTypes.oneOf(['error', 'warning', 'typesetting']).isRequired,
  168. onLogEntryLinkClick: PropTypes.func.isRequired
  169. }
  170. export default PreviewLogEntry