error-logs.tsx 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. import { useTranslation } from 'react-i18next'
  2. import { ElementType, 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. import importOverleafModules from '../../../../macros/import-overleaf-module.macro'
  21. // todo: quota clean-up remove unneeded old paywall component
  22. const logsComponents: Array<{
  23. import: { default: ElementType }
  24. path: string
  25. }> = importOverleafModules('errorLogsComponents')
  26. type ErrorLogTab = {
  27. key: string
  28. label: string
  29. entries: LogEntryData[] | undefined
  30. }
  31. function ErrorLogs({
  32. includeActionButtons,
  33. }: {
  34. includeActionButtons?: boolean
  35. }) {
  36. const { error, logEntries, rawLog, validationIssues, stoppedOnFirstError } =
  37. useCompileContext()
  38. const { compileTimeout } = getMeta('ol-compileSettings')
  39. const { t } = useTranslation()
  40. const tabs = useMemo(() => {
  41. return [
  42. {
  43. key: 'all',
  44. label: t('all_logs'),
  45. entries: logEntries?.all,
  46. },
  47. { key: 'errors', label: t('errors'), entries: logEntries?.errors },
  48. { key: 'warnings', label: t('warnings'), entries: logEntries?.warnings },
  49. { key: 'info', label: t('info'), entries: logEntries?.typesetting },
  50. ]
  51. }, [logEntries, t])
  52. const { loadingError } = usePdfPreviewContext()
  53. const [activeTab, setActiveTab] = useState<string | null>('all')
  54. const changeTab = useCallback(
  55. (key: string | null) => {
  56. if (tabs.some(tab => tab.key === key)) {
  57. setActiveTab(key)
  58. }
  59. },
  60. [tabs]
  61. )
  62. const entries = useMemo(() => {
  63. return tabs.find(tab => tab.key === activeTab)?.entries || []
  64. }, [activeTab, tabs])
  65. const includeErrors = activeTab === 'all' || activeTab === 'errors'
  66. const includeWarnings = activeTab === 'all' || activeTab === 'warnings'
  67. return (
  68. <TabContainer onSelect={changeTab} defaultActiveKey={activeTab ?? 'all'}>
  69. <Nav defaultActiveKey="all" className="error-logs-tabs">
  70. {tabs.map(tab => (
  71. <TabHeader key={tab.key} tab={tab} active={activeTab === tab.key} />
  72. ))}
  73. </Nav>
  74. {logsComponents.map(({ import: { default: Component }, path }) => (
  75. <Component key={path} />
  76. ))}
  77. <ErrorAssistantAiPaywallNotification />
  78. <TabContent className="error-logs new-error-logs">
  79. <div className="logs-pane-content">
  80. <RollingBuildSelectedReminder />
  81. {stoppedOnFirstError && includeErrors && <StopOnFirstErrorPrompt />}
  82. {loadingError && (
  83. <PdfPreviewError
  84. error="pdf-viewer-loading-error"
  85. includeErrors={includeErrors}
  86. includeWarnings={includeWarnings}
  87. />
  88. )}
  89. {compileTimeout < 60 && error === 'timedout' ? (
  90. <TimeoutUpgradePromptNew />
  91. ) : (
  92. <>{error && <PdfPreviewError error={error} />}</>
  93. )}
  94. {includeErrors &&
  95. validationIssues &&
  96. Object.entries(validationIssues).map(([name, issue]) => (
  97. <PdfValidationIssue key={name} name={name} issue={issue} />
  98. ))}
  99. {entries && (
  100. <PdfLogsEntries
  101. entries={entries}
  102. hasErrors={
  103. includeErrors &&
  104. logEntries?.errors &&
  105. logEntries?.errors.length > 0
  106. }
  107. />
  108. )}
  109. {rawLog && activeTab === 'all' && (
  110. <LogEntry
  111. headerTitle={t('raw_logs')}
  112. rawContent={rawLog}
  113. entryAriaLabel={t('raw_logs_description')}
  114. level="raw"
  115. alwaysExpandRawContent
  116. showSourceLocationLink={false}
  117. />
  118. )}
  119. {includeActionButtons && (
  120. <div className="logs-pane-actions">
  121. <PdfClearCacheButton />
  122. <PdfDownloadFilesButton />
  123. </div>
  124. )}
  125. </div>
  126. </TabContent>
  127. </TabContainer>
  128. )
  129. }
  130. function formatErrorNumber(num: number | undefined) {
  131. if (num === undefined) {
  132. return undefined
  133. }
  134. if (num > 99) {
  135. return '99+'
  136. }
  137. return Math.floor(num).toString()
  138. }
  139. const TabHeader = ({ tab, active }: { tab: ErrorLogTab; active: boolean }) => {
  140. return (
  141. <NavLink
  142. eventKey={tab.key}
  143. className="error-logs-tab-header"
  144. active={active}
  145. >
  146. {tab.label}
  147. <div className="error-logs-tab-count">
  148. {/* TODO: it would be nice if this number included custom errors */}
  149. {formatErrorNumber(tab.entries?.length)}
  150. </div>
  151. </NavLink>
  152. )
  153. }
  154. export default withErrorBoundary(memo(ErrorLogs), () => (
  155. <PdfPreviewErrorBoundaryFallback type="logs" />
  156. ))