preview-logs-pane.js 6.2 KB

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