preview-logs-pane.js 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. import PropTypes from 'prop-types'
  2. import { useTranslation } from 'react-i18next'
  3. import { Dropdown } from 'react-bootstrap'
  4. import PreviewLogsPaneEntry from './preview-logs-pane-entry'
  5. import PreviewLogsPaneMaxEntries from './preview-logs-pane-max-entries'
  6. import PreviewValidationIssue from './preview-validation-issue'
  7. import PreviewDownloadFileList from './preview-download-file-list'
  8. import PreviewError from './preview-error'
  9. import Icon from '../../../shared/components/icon'
  10. import usePersistedState from '../../../shared/hooks/use-persisted-state'
  11. import ControlledDropdown from '../../../shared/components/controlled-dropdown'
  12. const LOG_PREVIEW_LIMIT = 100
  13. function PreviewLogsPane({
  14. logEntries = { all: [], errors: [], warnings: [], typesetting: [] },
  15. rawLog = '',
  16. validationIssues = {},
  17. errors = {},
  18. outputFiles = [],
  19. isClearingCache,
  20. isCompiling = false,
  21. autoCompileHasLintingError = false,
  22. variantWithFirstErrorPopup,
  23. onLogEntryLocationClick,
  24. onClearCache,
  25. }) {
  26. const { t } = useTranslation()
  27. const {
  28. all: allCompilerIssues = [],
  29. errors: compilerErrors = [],
  30. warnings: compilerWarnings = [],
  31. typesetting: compilerTypesettingIssues = [],
  32. } = logEntries
  33. const allLogEntries = [
  34. ...compilerErrors,
  35. ...compilerWarnings,
  36. ...compilerTypesettingIssues,
  37. ]
  38. const actionsUI = (
  39. <div className="logs-pane-actions">
  40. <button
  41. className="btn btn-sm btn-danger logs-pane-actions-clear-cache"
  42. onClick={onClearCache}
  43. disabled={isClearingCache || isCompiling}
  44. >
  45. {isClearingCache ? (
  46. <Icon type="refresh" spin />
  47. ) : (
  48. <Icon type="trash-o" />
  49. )}
  50. &nbsp;
  51. <span>{t('clear_cached_files')}</span>
  52. </button>
  53. <ControlledDropdown
  54. id="dropdown-files-logs-pane"
  55. dropup
  56. pullRight
  57. disabled={isCompiling}
  58. >
  59. <Dropdown.Toggle
  60. className="btn btn-sm btn-info dropdown-toggle"
  61. title={t('other_logs_and_files')}
  62. bsStyle="info"
  63. />
  64. <Dropdown.Menu id="dropdown-files-logs-pane-list">
  65. <PreviewDownloadFileList fileList={outputFiles} />
  66. </Dropdown.Menu>
  67. </ControlledDropdown>
  68. </div>
  69. )
  70. const rawLogUI = (
  71. <PreviewLogsPaneEntry
  72. headerTitle={t('raw_logs')}
  73. rawContent={rawLog}
  74. entryAriaLabel={t('raw_logs_description')}
  75. level="raw"
  76. />
  77. )
  78. return (
  79. <div className="logs-pane">
  80. <div className="logs-pane-content">
  81. <LogsPaneInfoNotice
  82. variantWithFirstErrorPopup={variantWithFirstErrorPopup}
  83. />
  84. {autoCompileHasLintingError ? <AutoCompileLintingErrorEntry /> : null}
  85. {errors ? <PreviewErrors errors={errors} /> : null}
  86. {validationIssues ? (
  87. <PreviewValidationIssues validationIssues={validationIssues} />
  88. ) : null}
  89. {allCompilerIssues.length > 0 ? (
  90. <PreviewLogEntries
  91. logEntries={allLogEntries}
  92. onLogEntryLocationClick={onLogEntryLocationClick}
  93. />
  94. ) : null}
  95. {rawLog && rawLog !== '' ? rawLogUI : null}
  96. {actionsUI}
  97. </div>
  98. </div>
  99. )
  100. }
  101. const PreviewErrors = ({ errors }) => {
  102. const nowTS = Date.now()
  103. return Object.entries(errors).map(([name, exists], index) => {
  104. return exists && <PreviewError key={`${nowTS}-${index}`} name={name} />
  105. })
  106. }
  107. const PreviewValidationIssues = ({ validationIssues }) => {
  108. const nowTS = Date.now()
  109. return Object.keys(validationIssues).map((name, index) => (
  110. <PreviewValidationIssue
  111. key={`${nowTS}-${index}`}
  112. name={name}
  113. details={validationIssues[name]}
  114. />
  115. ))
  116. }
  117. const PreviewLogEntries = ({ logEntries, onLogEntryLocationClick }) => {
  118. const { t } = useTranslation()
  119. const nowTS = Date.now()
  120. const totalLogEntries = logEntries.length
  121. if (totalLogEntries > LOG_PREVIEW_LIMIT) {
  122. logEntries = logEntries.slice(0, 100)
  123. }
  124. logEntries = logEntries.map((logEntry, index) => (
  125. <PreviewLogsPaneEntry
  126. key={`${nowTS}-${index}`}
  127. headerTitle={logEntry.message}
  128. rawContent={logEntry.content}
  129. logType={logEntry.type}
  130. formattedContent={logEntry.humanReadableHintComponent}
  131. extraInfoURL={logEntry.extraInfoURL}
  132. level={logEntry.level}
  133. entryAriaLabel={t('log_entry_description', {
  134. level: logEntry.level,
  135. })}
  136. sourceLocation={{
  137. file: logEntry.file,
  138. line: logEntry.line,
  139. column: logEntry.column,
  140. }}
  141. onSourceLocationClick={onLogEntryLocationClick}
  142. />
  143. ))
  144. if (totalLogEntries > LOG_PREVIEW_LIMIT) {
  145. // Prepend log limit exceeded message to logs array
  146. logEntries = [
  147. <PreviewLogsPaneMaxEntries
  148. key={`${nowTS}-${LOG_PREVIEW_LIMIT}`}
  149. totalEntries={totalLogEntries}
  150. entriesShown={LOG_PREVIEW_LIMIT}
  151. />,
  152. ].concat(logEntries)
  153. }
  154. return logEntries
  155. }
  156. function AutoCompileLintingErrorEntry() {
  157. const { t } = useTranslation()
  158. return (
  159. <div className="log-entry">
  160. <div className="log-entry-header log-entry-header-error">
  161. <div className="log-entry-header-icon-container">
  162. <Icon type="exclamation-triangle" modifier="fw" />
  163. </div>
  164. <h3 className="log-entry-header-title">
  165. {t('code_check_failed_explanation')}
  166. </h3>
  167. </div>
  168. </div>
  169. )
  170. }
  171. // exported to be used by pdf-viewer during React migration
  172. export function LogsPaneInfoNotice({ variantWithFirstErrorPopup }) {
  173. const { t } = useTranslation()
  174. const [dismissedInfoNotice, setDismissedInfoNotice] = usePersistedState(
  175. `logs_pane.dismissed_info_notice`,
  176. false
  177. )
  178. const surveyLink = variantWithFirstErrorPopup
  179. ? 'https://forms.gle/AUbDDRvroQ7KFwHR9'
  180. : 'https://forms.gle/bRxevtGzBHRk8BKw8'
  181. function handleDismissButtonClick() {
  182. setDismissedInfoNotice(true)
  183. }
  184. return dismissedInfoNotice ? null : (
  185. <div className="log-entry">
  186. <div className="log-entry-header log-entry-header-raw">
  187. <div className="log-entry-header-icon-container">
  188. <span className="info-badge" />
  189. </div>
  190. <h3 className="log-entry-header-title">
  191. {t('logs_pane_info_message')}
  192. </h3>
  193. <a
  194. href={surveyLink}
  195. target="_blank"
  196. rel="noopener noreferrer"
  197. className="log-entry-header-link log-entry-header-link-raw"
  198. >
  199. <span className="log-entry-header-link-location">
  200. {t('give_feedback')}
  201. </span>
  202. </a>
  203. <button
  204. className="btn-inline-link log-entry-header-link"
  205. type="button"
  206. aria-label={t('dismiss')}
  207. onClick={handleDismissButtonClick}
  208. >
  209. <span aria-hidden="true">&times;</span>
  210. </button>
  211. </div>
  212. </div>
  213. )
  214. }
  215. PreviewErrors.propTypes = {
  216. errors: PropTypes.object.isRequired,
  217. }
  218. PreviewValidationIssues.propTypes = {
  219. validationIssues: PropTypes.object.isRequired,
  220. }
  221. PreviewLogEntries.propTypes = {
  222. logEntries: PropTypes.array.isRequired,
  223. onLogEntryLocationClick: PropTypes.func.isRequired,
  224. }
  225. LogsPaneInfoNotice.propTypes = {
  226. variantWithFirstErrorPopup: PropTypes.bool,
  227. }
  228. PreviewLogsPane.propTypes = {
  229. logEntries: PropTypes.shape({
  230. all: PropTypes.array,
  231. errors: PropTypes.array,
  232. warning: PropTypes.array,
  233. typesetting: PropTypes.array,
  234. }),
  235. autoCompileHasLintingError: PropTypes.bool,
  236. rawLog: PropTypes.string,
  237. outputFiles: PropTypes.array,
  238. isClearingCache: PropTypes.bool,
  239. isCompiling: PropTypes.bool,
  240. variantWithFirstErrorPopup: PropTypes.bool,
  241. onLogEntryLocationClick: PropTypes.func.isRequired,
  242. onClearCache: PropTypes.func.isRequired,
  243. validationIssues: PropTypes.object,
  244. errors: PropTypes.object,
  245. }
  246. export default PreviewLogsPane