preview-logs-pane.js 7.0 KB

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