preview-log-entry.js 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  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. showLineAndNoLink = true,
  17. showCloseButton = false,
  18. onLogEntryLocationClick,
  19. onClose
  20. }) {
  21. const { t } = useTranslation()
  22. function handleLogEntryLinkClick() {
  23. onLogEntryLocationClick({ file, line, column })
  24. }
  25. const logEntryDescription =
  26. level === 'raw'
  27. ? t('raw_logs_description')
  28. : t('log_entry_description', {
  29. level: level
  30. })
  31. return (
  32. <div className="log-entry" aria-label={logEntryDescription}>
  33. <PreviewLogEntryHeader
  34. level={level}
  35. file={file}
  36. line={line}
  37. message={message}
  38. showLineAndNoLink={showLineAndNoLink}
  39. onLogEntryLocationClick={handleLogEntryLinkClick}
  40. showCloseButton={showCloseButton}
  41. onClose={onClose}
  42. />
  43. {content ? (
  44. <PreviewLogEntryContent
  45. content={content}
  46. humanReadableHintComponent={humanReadableHintComponent}
  47. extraInfoURL={extraInfoURL}
  48. />
  49. ) : null}
  50. </div>
  51. )
  52. }
  53. function PreviewLogEntryHeader({
  54. level,
  55. file,
  56. line,
  57. message,
  58. showLineAndNoLink = true,
  59. showCloseButton = false,
  60. onLogEntryLocationClick,
  61. onClose
  62. }) {
  63. const { t } = useTranslation()
  64. const logEntryHeaderClasses = classNames('log-entry-header', {
  65. 'log-entry-header-error': level === 'error',
  66. 'log-entry-header-warning': level === 'warning',
  67. 'log-entry-header-typesetting': level === 'typesetting',
  68. 'log-entry-header-raw': level === 'raw'
  69. })
  70. const headerLogLocationTitle = t('navigate_log_source', {
  71. location: file + (line ? `, ${line}` : '')
  72. })
  73. return (
  74. <header className={logEntryHeaderClasses}>
  75. <h3 className="log-entry-header-title">{message}</h3>
  76. {showLineAndNoLink && file ? (
  77. <button
  78. className="btn-inline-link log-entry-header-link"
  79. type="button"
  80. title={headerLogLocationTitle}
  81. onClick={onLogEntryLocationClick}
  82. >
  83. <Icon type="chain" />
  84. &nbsp;
  85. <span>{file}</span>
  86. {line ? <span>, {line}</span> : null}
  87. </button>
  88. ) : null}
  89. {showCloseButton && file ? (
  90. <button
  91. className="btn-inline-link log-entry-header-link"
  92. type="button"
  93. aria-label={t('dismiss_error_popup')}
  94. onClick={onClose}
  95. >
  96. <span aria-hidden="true">&times;</span>
  97. </button>
  98. ) : null}
  99. </header>
  100. )
  101. }
  102. function PreviewLogEntryContent({
  103. content,
  104. humanReadableHintComponent,
  105. extraInfoURL
  106. }) {
  107. const { isExpanded, expandableProps, toggleProps } = useExpandCollapse({
  108. collapsedSize: 150,
  109. classes: {
  110. container: 'log-entry-content-raw-expandable-container'
  111. }
  112. })
  113. const logContentClasses = classNames('log-entry-content-raw', {
  114. 'log-entry-content-raw-collapsed': !isExpanded
  115. })
  116. const buttonContainerClasses = classNames(
  117. 'log-entry-content-button-container',
  118. {
  119. 'log-entry-content-button-container-collapsed': !isExpanded
  120. }
  121. )
  122. const { t } = useTranslation()
  123. return (
  124. <div className="log-entry-content">
  125. <div {...expandableProps}>
  126. <pre className={logContentClasses}>{content.trim()}</pre>
  127. <div className={buttonContainerClasses}>
  128. <button
  129. type="button"
  130. className="btn btn-xs btn-default log-entry-btn-expand-collapse"
  131. {...toggleProps}
  132. >
  133. {isExpanded ? (
  134. <>
  135. <Icon type="angle-up" /> {t('collapse')}
  136. </>
  137. ) : (
  138. <>
  139. <Icon type="angle-down" /> {t('expand')}
  140. </>
  141. )}
  142. </button>
  143. </div>
  144. </div>
  145. {humanReadableHintComponent ? (
  146. <div className="log-entry-human-readable-hint">
  147. {humanReadableHintComponent}
  148. </div>
  149. ) : null}
  150. {extraInfoURL ? (
  151. <div className="log-entry-human-readable-hint-link">
  152. <a
  153. href={extraInfoURL}
  154. target="_blank"
  155. className="log-entry-human-readable-hint-link"
  156. >
  157. {t('log_hint_extra_info')}
  158. </a>
  159. </div>
  160. ) : null}
  161. </div>
  162. )
  163. }
  164. PreviewLogEntryHeader.propTypes = {
  165. level: PropTypes.string.isRequired,
  166. file: PropTypes.string,
  167. line: PropTypes.any,
  168. message: PropTypes.string,
  169. showLineAndNoLink: PropTypes.bool,
  170. showCloseButton: PropTypes.bool,
  171. onLogEntryLocationClick: PropTypes.func,
  172. onClose: PropTypes.func
  173. }
  174. PreviewLogEntryContent.propTypes = {
  175. content: PropTypes.string.isRequired,
  176. humanReadableHintComponent: PropTypes.oneOfType([
  177. PropTypes.arrayOf(PropTypes.node),
  178. PropTypes.element
  179. ]),
  180. extraInfoURL: PropTypes.string
  181. }
  182. PreviewLogEntry.propTypes = {
  183. file: PropTypes.string,
  184. // `line should be either a number or null (i.e. not required), but currently sometimes we get
  185. // an empty string (from BibTeX errors), which is why we're using `any` here. We should revert
  186. // to PropTypes.number (not required) once we fix that.
  187. line: PropTypes.any,
  188. column: PropTypes.any,
  189. message: PropTypes.string,
  190. content: PropTypes.string,
  191. humanReadableHintComponent: PropTypes.node,
  192. extraInfoURL: PropTypes.string,
  193. level: PropTypes.oneOf(['error', 'warning', 'typesetting', 'raw']).isRequired,
  194. showLineAndNoLink: PropTypes.bool,
  195. showCloseButton: PropTypes.bool,
  196. onLogEntryLocationClick: PropTypes.func,
  197. onClose: PropTypes.func
  198. }
  199. export default PreviewLogEntry