error-logs.tsx 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. import { useTranslation } from 'react-i18next'
  2. import { memo, useCallback, useMemo, useState } from 'react'
  3. import { usePdfPreviewContext } from '@/features/pdf-preview/components/pdf-preview-provider'
  4. import StopOnFirstErrorPrompt from '@/features/pdf-preview/components/stop-on-first-error-prompt'
  5. import PdfPreviewError from '@/features/pdf-preview/components/pdf-preview-error'
  6. import PdfValidationIssue from '@/features/pdf-preview/components/pdf-validation-issue'
  7. import PdfLogsEntries from '@/features/pdf-preview/components/pdf-logs-entries'
  8. import PdfPreviewErrorBoundaryFallback from '@/features/pdf-preview/components/pdf-preview-error-boundary-fallback'
  9. import withErrorBoundary from '@/infrastructure/error-boundary'
  10. import { useDetachCompileContext as useCompileContext } from '@/shared/context/detach-compile-context'
  11. import { Nav, NavLink, TabContainer, TabContent } from 'react-bootstrap'
  12. import { LogEntry as LogEntryData } from '@/features/pdf-preview/util/types'
  13. import LogEntry from './log-entry'
  14. import TimeoutUpgradePromptNew from '@/features/pdf-preview/components/timeout-upgrade-prompt-new'
  15. import getMeta from '@/utils/meta'
  16. import PdfClearCacheButton from '@/features/pdf-preview/components/pdf-clear-cache-button'
  17. import PdfDownloadFilesButton from '@/features/pdf-preview/components/pdf-download-files-button'
  18. import RollingBuildSelectedReminder from './rolling-build-selected-reminder'
  19. import ErrorAssistantAiPaywallNotification from './error-assistant-ai-paywall-notification'
  20. type ErrorLogTab = {
  21. key: string
  22. label: string
  23. entries: LogEntryData[] | undefined
  24. }
  25. function ErrorLogs({
  26. includeActionButtons,
  27. }: {
  28. includeActionButtons?: boolean
  29. }) {
  30. const { error, logEntries, rawLog, validationIssues, stoppedOnFirstError } =
  31. useCompileContext()
  32. const { compileTimeout } = getMeta('ol-compileSettings')
  33. const { t } = useTranslation()
  34. const tabs = useMemo(() => {
  35. return [
  36. {
  37. key: 'all',
  38. label: t('all_logs'),
  39. entries: logEntries?.all,
  40. },
  41. { key: 'errors', label: t('errors'), entries: logEntries?.errors },
  42. { key: 'warnings', label: t('warnings'), entries: logEntries?.warnings },
  43. { key: 'info', label: t('info'), entries: logEntries?.typesetting },
  44. ]
  45. }, [logEntries, t])
  46. const { loadingError } = usePdfPreviewContext()
  47. const [activeTab, setActiveTab] = useState<string | null>('all')
  48. const changeTab = useCallback(
  49. (key: string | null) => {
  50. if (tabs.some(tab => tab.key === key)) {
  51. setActiveTab(key)
  52. }
  53. },
  54. [tabs]
  55. )
  56. const entries = useMemo(() => {
  57. return tabs.find(tab => tab.key === activeTab)?.entries || []
  58. }, [activeTab, tabs])
  59. const includeErrors = activeTab === 'all' || activeTab === 'errors'
  60. const includeWarnings = activeTab === 'all' || activeTab === 'warnings'
  61. return (
  62. <TabContainer onSelect={changeTab} defaultActiveKey={activeTab ?? 'all'}>
  63. <Nav defaultActiveKey="all" className="error-logs-tabs">
  64. {tabs.map(tab => (
  65. <TabHeader key={tab.key} tab={tab} active={activeTab === tab.key} />
  66. ))}
  67. </Nav>
  68. <ErrorAssistantAiPaywallNotification />
  69. <TabContent className="error-logs new-error-logs">
  70. <div className="logs-pane-content">
  71. <RollingBuildSelectedReminder />
  72. {stoppedOnFirstError && includeErrors && <StopOnFirstErrorPrompt />}
  73. {loadingError && (
  74. <PdfPreviewError
  75. error="pdf-viewer-loading-error"
  76. includeErrors={includeErrors}
  77. includeWarnings={includeWarnings}
  78. />
  79. )}
  80. {compileTimeout < 60 && error === 'timedout' ? (
  81. <TimeoutUpgradePromptNew />
  82. ) : (
  83. <>{error && <PdfPreviewError error={error} />}</>
  84. )}
  85. {includeErrors &&
  86. validationIssues &&
  87. Object.entries(validationIssues).map(([name, issue]) => (
  88. <PdfValidationIssue key={name} name={name} issue={issue} />
  89. ))}
  90. {entries && (
  91. <PdfLogsEntries
  92. entries={entries}
  93. hasErrors={
  94. includeErrors &&
  95. logEntries?.errors &&
  96. logEntries?.errors.length > 0
  97. }
  98. />
  99. )}
  100. {rawLog && activeTab === 'all' && (
  101. <LogEntry
  102. headerTitle={t('raw_logs')}
  103. rawContent={rawLog}
  104. entryAriaLabel={t('raw_logs_description')}
  105. level="raw"
  106. alwaysExpandRawContent
  107. showSourceLocationLink={false}
  108. />
  109. )}
  110. {includeActionButtons && (
  111. <div className="logs-pane-actions">
  112. <PdfClearCacheButton />
  113. <PdfDownloadFilesButton />
  114. </div>
  115. )}
  116. </div>
  117. </TabContent>
  118. </TabContainer>
  119. )
  120. }
  121. function formatErrorNumber(num: number | undefined) {
  122. if (num === undefined) {
  123. return undefined
  124. }
  125. if (num > 99) {
  126. return '99+'
  127. }
  128. return Math.floor(num).toString()
  129. }
  130. const TabHeader = ({ tab, active }: { tab: ErrorLogTab; active: boolean }) => {
  131. return (
  132. <NavLink
  133. eventKey={tab.key}
  134. className="error-logs-tab-header"
  135. active={active}
  136. as="button"
  137. >
  138. {tab.label}
  139. <div className="error-logs-tab-count">
  140. {/* TODO: it would be nice if this number included custom errors */}
  141. {formatErrorNumber(tab.entries?.length)}
  142. </div>
  143. </NavLink>
  144. )
  145. }
  146. export default withErrorBoundary(memo(ErrorLogs), () => (
  147. <PdfPreviewErrorBoundaryFallback type="logs" />
  148. ))