Просмотр исходного кода

Merge pull request #25939 from overleaf/dp-error-logs

Update error logs designs for new editor

GitOrigin-RevId: 0de3a54446a0ff114a1debb7b5f274d3a8f19c42
David 1 год назад
Родитель
Сommit
db98f5132b
32 измененных файлов с 1021 добавлено и 267 удалено
  1. 1 0
      services/web/app/src/Features/Project/ProjectController.js
  2. 1 0
      services/web/config/settings.defaults.js
  3. 3 1
      services/web/frontend/extracted-translations.json
  4. BIN
      services/web/frontend/fonts/material-symbols/MaterialSymbolsRoundedUnfilledPartialSlice.woff2
  5. 1 0
      services/web/frontend/fonts/material-symbols/unfilled-symbols.mjs
  6. 1 1
      services/web/frontend/js/features/ide-redesign/components/chat/chat.tsx
  7. 2 12
      services/web/frontend/js/features/ide-redesign/components/error-logs/error-indicator.tsx
  8. 98 0
      services/web/frontend/js/features/ide-redesign/components/error-logs/error-logs-header.tsx
  9. 14 0
      services/web/frontend/js/features/ide-redesign/components/error-logs/error-logs-panel.tsx
  10. 133 0
      services/web/frontend/js/features/ide-redesign/components/error-logs/error-logs.tsx
  11. 153 0
      services/web/frontend/js/features/ide-redesign/components/error-logs/log-entry-header.tsx
  12. 109 0
      services/web/frontend/js/features/ide-redesign/components/error-logs/log-entry.tsx
  13. 10 0
      services/web/frontend/js/features/ide-redesign/components/error-logs/old-error-pane.tsx
  14. 1 1
      services/web/frontend/js/features/ide-redesign/components/integrations-panel/integrations-panel.tsx
  15. 31 0
      services/web/frontend/js/features/ide-redesign/components/rail-panel-header.tsx
  16. 19 22
      services/web/frontend/js/features/ide-redesign/components/rail.tsx
  17. 7 1
      services/web/frontend/js/features/pdf-preview/components/pdf-log-entry-content.tsx
  18. 8 4
      services/web/frontend/js/features/pdf-preview/components/pdf-log-entry-raw-content.tsx
  19. 29 10
      services/web/frontend/js/features/pdf-preview/components/pdf-log-entry.tsx
  20. 174 138
      services/web/frontend/js/features/pdf-preview/components/pdf-preview-error.tsx
  21. 7 3
      services/web/frontend/js/features/pdf-preview/components/timeout-upgrade-paywall-prompt.tsx
  22. 0 14
      services/web/frontend/js/features/preview/components/preview-log-entry-header.tsx
  23. 7 2
      services/web/frontend/js/shared/context/local-compile-context.tsx
  24. 0 1
      services/web/frontend/stories/pdf-log-entry.stories.tsx
  25. 198 46
      services/web/frontend/stylesheets/bootstrap-5/pages/editor/logs.scss
  26. 11 5
      services/web/frontend/stylesheets/bootstrap-5/pages/editor/rail.scss
  27. 0 1
      services/web/locales/da.json
  28. 0 1
      services/web/locales/de.json
  29. 3 1
      services/web/locales/en.json
  30. 0 1
      services/web/locales/fr.json
  31. 0 1
      services/web/locales/sv.json
  32. 0 1
      services/web/locales/zh-CN.json

+ 1 - 0
services/web/app/src/Features/Project/ProjectController.js

@@ -352,6 +352,7 @@ const _ProjectController = {
       'overleaf-assist-bundle',
       'word-count-client',
       'editor-popup-ux-survey',
+      'new-editor-error-logs-redesign',
     ].filter(Boolean)
 
     const getUserValues = async userId =>

+ 1 - 0
services/web/config/settings.defaults.js

@@ -966,6 +966,7 @@ module.exports = {
     editorToolbarButtons: [],
     sourceEditorExtensions: [],
     sourceEditorComponents: [],
+    pdfLogEntryHeaderActionComponents: [],
     pdfLogEntryComponents: [],
     pdfLogEntriesComponents: [],
     pdfPreviewPromotions: [],

+ 3 - 1
services/web/frontend/extracted-translations.json

@@ -411,7 +411,6 @@
   "discount": "",
   "discount_of": "",
   "discover_the_fastest_way_to_search_and_cite": "",
-  "dismiss_error_popup": "",
   "display": "",
   "display_deleted_user": "",
   "display_math": "",
@@ -680,6 +679,7 @@
   "go_page": "",
   "go_prev_page": "",
   "go_to_account_settings": "",
+  "go_to_code_location": "",
   "go_to_code_location_in_pdf": "",
   "go_to_overleaf": "",
   "go_to_pdf_location_in_code": "",
@@ -969,6 +969,7 @@
   "login_count": "",
   "login_to_accept_invitation": "",
   "login_with_service": "",
+  "logs": "",
   "logs_and_output_files": "",
   "looking_multiple_licenses": "",
   "looks_like_youre_at": "",
@@ -1039,6 +1040,7 @@
   "more_compile_time": "",
   "more_editor_toolbar_item": "",
   "more_info": "",
+  "more_logs_and_files": "",
   "more_options": "",
   "my_library": "",
   "n_items": "",

BIN
services/web/frontend/fonts/material-symbols/MaterialSymbolsRoundedUnfilledPartialSlice.woff2


+ 1 - 0
services/web/frontend/fonts/material-symbols/unfilled-symbols.mjs

@@ -4,6 +4,7 @@
 // You may need to hard reload your browser window to see the changes.
 
 export default /** @type {const} */ ([
+  'auto_delete',
   'book_5',
   'brush',
   'code',

+ 1 - 1
services/web/frontend/js/features/ide-redesign/components/chat/chat.tsx

@@ -9,8 +9,8 @@ import { useUserContext } from '@/shared/context/user-context'
 import { lazy, Suspense, useEffect } from 'react'
 import { useTranslation } from 'react-i18next'
 import classNames from 'classnames'
-import { RailPanelHeader } from '../rail'
 import { RailIndicator } from '../rail-indicator'
+import RailPanelHeader from '../rail-panel-header'
 
 const MessageList = lazy(() => import('../../../chat/components/message-list'))
 

+ 2 - 12
services/web/frontend/js/features/ide-redesign/components/errors.tsx → services/web/frontend/js/features/ide-redesign/components/error-logs/error-indicator.tsx

@@ -1,9 +1,7 @@
-import PdfLogsViewer from '@/features/pdf-preview/components/pdf-logs-viewer'
-import { PdfPreviewProvider } from '@/features/pdf-preview/components/pdf-preview-provider'
 import { useDetachCompileContext as useCompileContext } from '@/shared/context/detach-compile-context'
-import { RailIndicator } from './rail-indicator'
+import { RailIndicator } from '../rail-indicator'
 
-export const ErrorIndicator = () => {
+export default function ErrorIndicator() {
   const { logEntries } = useCompileContext()
 
   if (!logEntries) {
@@ -25,11 +23,3 @@ export const ErrorIndicator = () => {
     />
   )
 }
-
-export const ErrorPane = () => {
-  return (
-    <PdfPreviewProvider>
-      <PdfLogsViewer alwaysVisible />
-    </PdfPreviewProvider>
-  )
-}

+ 98 - 0
services/web/frontend/js/features/ide-redesign/components/error-logs/error-logs-header.tsx

@@ -0,0 +1,98 @@
+import { useTranslation } from 'react-i18next'
+import RailPanelHeader from '../rail-panel-header'
+import OLIconButton from '@/features/ui/components/ol/ol-icon-button'
+import { useDetachCompileContext as useCompileContext } from '@/shared/context/detach-compile-context'
+import {
+  Dropdown,
+  DropdownMenu,
+  DropdownToggle,
+} from '@/features/ui/components/bootstrap-5/dropdown-menu'
+import PdfFileList from '@/features/pdf-preview/components/pdf-file-list'
+import { forwardRef } from 'react'
+import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
+
+export default function ErrorLogsHeader() {
+  const { t } = useTranslation()
+
+  return (
+    <RailPanelHeader
+      title={t('logs')}
+      actions={[
+        <DownloadFileDropdown key="download-files" />,
+        <ClearCacheButton key="clear-cache" />,
+      ]}
+    />
+  )
+}
+
+const ClearCacheButton = () => {
+  const { compiling, clearCache, clearingCache } = useCompileContext()
+  const { t } = useTranslation()
+
+  return (
+    <OLTooltip
+      id="clear-cache"
+      description={t('clear_cached_files')}
+      overlayProps={{ placement: 'bottom' }}
+    >
+      <OLIconButton
+        unfilled
+        onClick={() => clearCache()}
+        className="rail-panel-header-button-subdued"
+        icon="auto_delete"
+        isLoading={clearingCache}
+        disabled={clearingCache || compiling}
+        accessibilityLabel={t('clear_cached_files')}
+        size="sm"
+      />
+    </OLTooltip>
+  )
+}
+
+const DownloadFileDropdown = () => {
+  const { fileList } = useCompileContext()
+
+  const { t } = useTranslation()
+
+  return (
+    <Dropdown align="end">
+      <DropdownToggle
+        as={DownloadFileDropdownToggleButton}
+        id="dropdown-files-dropdown-toggle"
+      >
+        {t('other_logs_and_files')}
+      </DropdownToggle>
+      {fileList && (
+        <DropdownMenu id="dropdown-files-logs-pane-list">
+          <PdfFileList fileList={fileList} />
+        </DropdownMenu>
+      )}
+    </Dropdown>
+  )
+}
+
+const DownloadFileDropdownToggleButton = forwardRef<
+  HTMLButtonElement,
+  { onClick: React.MouseEventHandler }
+>(function DownloadFileDropdownToggleButton({ onClick }, ref) {
+  const { compiling, fileList } = useCompileContext()
+  const { t } = useTranslation()
+
+  return (
+    <OLTooltip
+      id="more-logs-and-files"
+      description={t('more_logs_and_files')}
+      overlayProps={{ placement: 'bottom' }}
+    >
+      <OLIconButton
+        ref={ref}
+        onClick={onClick}
+        className="rail-panel-header-button-subdued"
+        icon="download"
+        disabled={compiling || !fileList}
+        accessibilityLabel={t('other_logs_and_files')}
+        size="sm"
+      />
+    </OLTooltip>
+  )
+})

+ 14 - 0
services/web/frontend/js/features/ide-redesign/components/error-logs/error-logs-panel.tsx

@@ -0,0 +1,14 @@
+import { PdfPreviewProvider } from '@/features/pdf-preview/components/pdf-preview-provider'
+import ErrorLogs from './error-logs'
+import ErrorLogsHeader from './error-logs-header'
+
+export default function ErrorLogsPanel() {
+  return (
+    <PdfPreviewProvider>
+      <div className="error-logs-panel">
+        <ErrorLogsHeader />
+        <ErrorLogs />
+      </div>
+    </PdfPreviewProvider>
+  )
+}

+ 133 - 0
services/web/frontend/js/features/ide-redesign/components/error-logs/error-logs.tsx

@@ -0,0 +1,133 @@
+import { useTranslation } from 'react-i18next'
+import { memo, useMemo, useState } from 'react'
+import { usePdfPreviewContext } from '@/features/pdf-preview/components/pdf-preview-provider'
+import StopOnFirstErrorPrompt from '@/features/pdf-preview/components/stop-on-first-error-prompt'
+import PdfPreviewError from '@/features/pdf-preview/components/pdf-preview-error'
+import PdfValidationIssue from '@/features/pdf-preview/components/pdf-validation-issue'
+import PdfLogsEntries from '@/features/pdf-preview/components/pdf-logs-entries'
+import PdfPreviewErrorBoundaryFallback from '@/features/pdf-preview/components/pdf-preview-error-boundary-fallback'
+import withErrorBoundary from '@/infrastructure/error-boundary'
+import { useDetachCompileContext as useCompileContext } from '@/shared/context/detach-compile-context'
+import { Nav, NavLink, TabContainer, TabContent } from 'react-bootstrap'
+import { LogEntry as LogEntryData } from '@/features/pdf-preview/util/types'
+import LogEntry from './log-entry'
+
+type ErrorLogTab = {
+  key: string
+  label: string
+  entries: LogEntryData[] | undefined
+}
+
+function ErrorLogs() {
+  const { error, logEntries, rawLog, validationIssues, stoppedOnFirstError } =
+    useCompileContext()
+
+  const tabs = useMemo(() => {
+    return [
+      { key: 'all', label: 'All', entries: logEntries?.all },
+      { key: 'errors', label: 'Errors', entries: logEntries?.errors },
+      { key: 'warnings', label: 'Warnings', entries: logEntries?.warnings },
+      { key: 'info', label: 'Info', entries: logEntries?.typesetting },
+    ]
+  }, [logEntries])
+
+  const { loadingError } = usePdfPreviewContext()
+
+  const { t } = useTranslation()
+
+  const [activeTab, setActiveTab] = useState<string | null>('all')
+
+  const entries = useMemo(() => {
+    return tabs.find(tab => tab.key === activeTab)?.entries || []
+  }, [activeTab, tabs])
+
+  const includeErrors = activeTab === 'all' || activeTab === 'errors'
+  const includeWarnings = activeTab === 'all' || activeTab === 'warnings'
+  const includeInfo = activeTab === 'all' || activeTab === 'info'
+
+  return (
+    <TabContainer onSelect={setActiveTab} defaultActiveKey={activeTab ?? 'all'}>
+      <Nav defaultActiveKey="all" className="error-logs-tabs">
+        {tabs.map(tab => (
+          <TabHeader key={tab.key} tab={tab} active={activeTab === tab.key} />
+        ))}
+      </Nav>
+      <TabContent className="error-logs">
+        <div className="logs-pane-content">
+          {stoppedOnFirstError && includeErrors && <StopOnFirstErrorPrompt />}
+
+          {loadingError && (
+            <PdfPreviewError
+              error="pdf-viewer-loading-error"
+              includeErrors={includeErrors}
+              includeWarnings={includeWarnings}
+            />
+          )}
+
+          {error && (
+            <PdfPreviewError
+              error={error}
+              includeErrors={includeErrors}
+              includeWarnings={includeWarnings}
+            />
+          )}
+
+          {includeErrors &&
+            validationIssues &&
+            Object.entries(validationIssues).map(([name, issue]) => (
+              <PdfValidationIssue key={name} name={name} issue={issue} />
+            ))}
+
+          {entries && (
+            <PdfLogsEntries
+              entries={entries}
+              hasErrors={includeErrors && entries.length > 0}
+            />
+          )}
+
+          {rawLog && includeInfo && (
+            <LogEntry
+              headerTitle={t('raw_logs')}
+              rawContent={rawLog}
+              entryAriaLabel={t('raw_logs_description')}
+              level="raw"
+              alwaysExpandRawContent
+            />
+          )}
+        </div>
+      </TabContent>
+    </TabContainer>
+  )
+}
+
+function formatErrorNumber(num: number | undefined) {
+  if (num === undefined) {
+    return undefined
+  }
+
+  if (num > 99) {
+    return '99+'
+  }
+
+  return Math.floor(num).toString()
+}
+
+const TabHeader = ({ tab, active }: { tab: ErrorLogTab; active: boolean }) => {
+  return (
+    <NavLink
+      eventKey={tab.key}
+      className="error-logs-tab-header"
+      active={active}
+    >
+      {tab.label}
+      <div className="error-logs-tab-count">
+        {/* TODO: it would be nice if this number included custom errors */}
+        {formatErrorNumber(tab.entries?.length)}
+      </div>
+    </NavLink>
+  )
+}
+
+export default withErrorBoundary(memo(ErrorLogs), () => (
+  <PdfPreviewErrorBoundaryFallback type="logs" />
+))

+ 153 - 0
services/web/frontend/js/features/ide-redesign/components/error-logs/log-entry-header.tsx

@@ -0,0 +1,153 @@
+import classNames from 'classnames'
+import { useState, useRef, MouseEventHandler, ElementType } from 'react'
+import { useTranslation } from 'react-i18next'
+import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
+import {
+  ErrorLevel,
+  SourceLocation,
+  LogEntry as LogEntryData,
+} from '@/features/pdf-preview/util/types'
+import useResizeObserver from '@/features/preview/hooks/use-resize-observer'
+import OLIconButton from '@/features/ui/components/ol/ol-icon-button'
+import importOverleafModules from '../../../../../macros/import-overleaf-module.macro'
+
+const actionComponents = importOverleafModules(
+  'pdfLogEntryHeaderActionComponents'
+) as {
+  import: { default: ElementType }
+  path: string
+}[]
+
+function LogEntryHeader({
+  sourceLocation,
+  level,
+  headerTitle,
+  logType,
+  showSourceLocationLink = true,
+  onSourceLocationClick,
+  collapsed,
+  onToggleCollapsed,
+  id,
+  logEntry,
+}: {
+  headerTitle: string | React.ReactNode
+  level: ErrorLevel
+  logType?: string
+  sourceLocation?: SourceLocation
+  showSourceLocationLink?: boolean
+  onSourceLocationClick?: MouseEventHandler<HTMLButtonElement>
+  collapsed: boolean
+  onToggleCollapsed: () => void
+  id?: string
+  logEntry?: LogEntryData
+}) {
+  const { t } = useTranslation()
+  const logLocationSpanRef = useRef<HTMLSpanElement>(null)
+  const [locationSpanOverflown, setLocationSpanOverflown] = useState(false)
+
+  useResizeObserver(
+    logLocationSpanRef,
+    locationSpanOverflown,
+    checkLocationSpanOverflow
+  )
+
+  const file = sourceLocation ? sourceLocation.file : null
+  const line = sourceLocation ? sourceLocation.line : null
+  const logEntryHeaderTextClasses = classNames('log-entry-header-text', {
+    'log-entry-header-text-error': level === 'error',
+    'log-entry-header-text-warning': level === 'warning',
+    'log-entry-header-text-info':
+      level === 'info' || level === 'typesetting' || level === 'raw',
+    'log-entry-header-text-success': level === 'success',
+  })
+
+  function checkLocationSpanOverflow(observedElement: ResizeObserverEntry) {
+    const spanEl = observedElement.target
+    const isOverflowing = spanEl.scrollWidth > spanEl.clientWidth
+    setLocationSpanOverflown(isOverflowing)
+  }
+
+  const locationText =
+    showSourceLocationLink && file ? `${file}${line ? `, ${line}` : ''}` : null
+
+  // Because we want an ellipsis on the left-hand side (e.g. "...longfilename.tex"), the
+  // `log-entry-location` class has text laid out from right-to-left using the CSS
+  // rule `direction: rtl;`.
+  // This works most of the times, except when the first character of the filename is considered
+  // a punctuation mark, like `/` (e.g. `/foo/bar/baz.sty`). In this case, because of
+  // right-to-left writing rules, the punctuation mark is moved to the right-side of the string,
+  // resulting in `...bar/baz.sty/` instead of `...bar/baz.sty`.
+  // To avoid this edge-case, we wrap the `logLocationLinkText` in two directional formatting
+  // characters:
+  //   * \u202A LEFT-TO-RIGHT EMBEDDING Treat the following text as embedded left-to-right.
+  //   * \u202C POP DIRECTIONAL FORMATTING End the scope of the last LRE, RLE, RLO, or LRO.
+  // This essentially tells the browser that, althought the text is laid out from right-to-left,
+  // the wrapped portion of text should follow left-to-right writing rules.
+  const formattedLocationText = locationText ? (
+    <span ref={logLocationSpanRef} className="log-entry-location">
+      {`\u202A${locationText}\u202C`}
+    </span>
+  ) : null
+
+  const headerTitleText = logType ? `${logType} ${headerTitle}` : headerTitle
+
+  return (
+    <header className="log-entry-header-card">
+      <OLTooltip
+        id={`expand-collapse-${locationText}`}
+        description={collapsed ? t('expand') : t('collapse')}
+        overlayProps={{ placement: 'bottom' }}
+      >
+        <OLIconButton
+          size="sm"
+          variant="ghost"
+          icon={collapsed ? 'chevron_right' : 'expand_more'}
+          onClick={onToggleCollapsed}
+          accessibilityLabel={collapsed ? t('expand') : t('collapse')}
+        />
+      </OLTooltip>
+      <div className="log-entry-header-content">
+        <h3 className={logEntryHeaderTextClasses}>{headerTitleText}</h3>
+        {locationSpanOverflown && formattedLocationText && locationText ? (
+          <OLTooltip
+            id={locationText}
+            description={locationText}
+            overlayProps={{ placement: 'left' }}
+            tooltipProps={{ className: 'log-location-tooltip' }}
+          >
+            {formattedLocationText}
+          </OLTooltip>
+        ) : (
+          formattedLocationText
+        )}
+      </div>
+      <div className="log-entry-header-actions">
+        {showSourceLocationLink && (
+          <OLTooltip
+            id={`go-to-location-${locationText}`}
+            description={t('go_to_code_location')}
+            overlayProps={{ placement: 'bottom' }}
+          >
+            <OLIconButton
+              onClick={onSourceLocationClick}
+              variant="ghost"
+              icon="my_location"
+              accessibilityLabel={t('go_to_code_location')}
+            />
+          </OLTooltip>
+        )}
+        {actionComponents.map(({ import: { default: Component }, path }) => (
+          <Component
+            key={path}
+            collapsed={collapsed}
+            onToggleCollapsed={onToggleCollapsed}
+            logEntry={logEntry}
+            id={id}
+          />
+        ))}
+      </div>
+    </header>
+  )
+}
+
+export default LogEntryHeader

+ 109 - 0
services/web/frontend/js/features/ide-redesign/components/error-logs/log-entry.tsx

@@ -0,0 +1,109 @@
+import { memo, MouseEventHandler, useCallback, useState } from 'react'
+import HumanReadableLogsHints from '../../../../ide/human-readable-logs/HumanReadableLogsHints'
+import { sendMB } from '@/infrastructure/event-tracking'
+import {
+  ErrorLevel,
+  LogEntry as LogEntryData,
+  SourceLocation,
+} from '@/features/pdf-preview/util/types'
+import LogEntryHeader from './log-entry-header'
+import PdfLogEntryContent from '@/features/pdf-preview/components/pdf-log-entry-content'
+
+function LogEntry({
+  ruleId,
+  headerTitle,
+  rawContent,
+  logType,
+  formattedContent,
+  extraInfoURL,
+  level,
+  sourceLocation,
+  showSourceLocationLink = true,
+  entryAriaLabel = undefined,
+  contentDetails,
+  onSourceLocationClick,
+  index,
+  logEntry,
+  id,
+  alwaysExpandRawContent = false,
+}: {
+  headerTitle: string | React.ReactNode
+  level: ErrorLevel
+  ruleId?: string
+  rawContent?: string
+  logType?: string
+  formattedContent?: React.ReactNode
+  extraInfoURL?: string | null
+  sourceLocation?: SourceLocation
+  showSourceLocationLink?: boolean
+  entryAriaLabel?: string
+  contentDetails?: string[]
+  onSourceLocationClick?: (sourceLocation: SourceLocation) => void
+  index?: number
+  logEntry?: LogEntryData
+  id?: string
+  alwaysExpandRawContent?: boolean
+}) {
+  const [collapsed, setCollapsed] = useState(true)
+
+  if (ruleId && HumanReadableLogsHints[ruleId]) {
+    const hint = HumanReadableLogsHints[ruleId]
+    formattedContent = hint.formattedContent(contentDetails)
+    extraInfoURL = hint.extraInfoURL
+  }
+
+  const handleLogEntryLinkClick: MouseEventHandler<HTMLButtonElement> =
+    useCallback(
+      event => {
+        event.preventDefault()
+
+        if (onSourceLocationClick && sourceLocation) {
+          onSourceLocationClick(sourceLocation)
+
+          const parts = sourceLocation?.file?.split('.')
+          const extension =
+            parts?.length && parts?.length > 1 ? parts.pop() : ''
+          sendMB('log-entry-link-click', { level, ruleId, extension })
+        }
+      },
+      [level, onSourceLocationClick, ruleId, sourceLocation]
+    )
+
+  return (
+    <div
+      className="log-entry"
+      data-ruleid={ruleId}
+      data-log-entry-id={id}
+      aria-label={entryAriaLabel}
+    >
+      <LogEntryHeader
+        level={level}
+        sourceLocation={sourceLocation}
+        headerTitle={headerTitle}
+        logType={logType}
+        showSourceLocationLink={showSourceLocationLink}
+        onSourceLocationClick={handleLogEntryLinkClick}
+        collapsed={collapsed}
+        onToggleCollapsed={() => setCollapsed(collapsed => !collapsed)}
+        id={id}
+        logEntry={logEntry}
+      />
+
+      {!collapsed && (
+        <>
+          <div className="horizontal-divider" />
+          <PdfLogEntryContent
+            alwaysExpandRawContent={alwaysExpandRawContent}
+            rawContent={rawContent}
+            formattedContent={formattedContent}
+            extraInfoURL={extraInfoURL}
+            index={index}
+            logEntry={logEntry}
+          />
+        </>
+      )}
+    </div>
+  )
+}
+
+export default memo(LogEntry)

+ 10 - 0
services/web/frontend/js/features/ide-redesign/components/error-logs/old-error-pane.tsx

@@ -0,0 +1,10 @@
+import PdfLogsViewer from '@/features/pdf-preview/components/pdf-logs-viewer'
+import { PdfPreviewProvider } from '@/features/pdf-preview/components/pdf-preview-provider'
+
+export default function OldErrorPane() {
+  return (
+    <PdfPreviewProvider>
+      <PdfLogsViewer alwaysVisible />
+    </PdfPreviewProvider>
+  )
+}

+ 1 - 1
services/web/frontend/js/features/ide-redesign/components/integrations-panel/integrations-panel.tsx

@@ -1,7 +1,7 @@
 import { ElementType } from 'react'
 import importOverleafModules from '../../../../../macros/import-overleaf-module.macro'
-import { RailPanelHeader } from '../rail'
 import { useTranslation } from 'react-i18next'
+import RailPanelHeader from '../rail-panel-header'
 
 const integrationPanelComponents = importOverleafModules(
   'integrationPanelComponents'

+ 31 - 0
services/web/frontend/js/features/ide-redesign/components/rail-panel-header.tsx

@@ -0,0 +1,31 @@
+import { useTranslation } from 'react-i18next'
+import { useRailContext } from '../contexts/rail-context'
+import OLIconButton from '@/features/ui/components/ol/ol-icon-button'
+import React from 'react'
+
+export default function RailPanelHeader({
+  title,
+  actions,
+}: {
+  title: string
+  actions?: React.ReactNode[]
+}) {
+  const { t } = useTranslation()
+  const { handlePaneCollapse } = useRailContext()
+  return (
+    <header className="rail-panel-header">
+      <h4 className="rail-panel-title">{title}</h4>
+
+      <div className="rail-panel-header-actions">
+        {actions}
+        <OLIconButton
+          onClick={handlePaneCollapse}
+          className="rail-panel-header-button-subdued"
+          icon="close"
+          accessibilityLabel={t('close')}
+          size="sm"
+        />
+      </div>
+    </header>
+  )
+}

+ 19 - 22
services/web/frontend/js/features/ide-redesign/components/rail.tsx

@@ -6,7 +6,7 @@ import MaterialIcon, {
 } from '@/shared/components/material-icon'
 import { Panel } from 'react-resizable-panels'
 import { useLayoutContext } from '@/shared/context/layout-context'
-import { ErrorIndicator, ErrorPane } from './errors'
+import ErrorIndicator from './error-logs/error-indicator'
 import {
   RailModalKey,
   RailTabKey,
@@ -39,6 +39,10 @@ import {
   hasFullProjectSearch,
 } from './full-project-search-panel'
 import { sendSearchEvent } from '@/features/event-tracking/search-events'
+import ErrorLogsPanel from './error-logs/error-logs-panel'
+import { useDetachCompileContext as useCompileContext } from '@/shared/context/detach-compile-context'
+import OldErrorPane from './error-logs/old-error-pane'
+import { useFeatureFlag } from '@/shared/context/split-test-context'
 
 type RailElement = {
   icon: AvailableUnfilledIcon
@@ -47,6 +51,7 @@ type RailElement = {
   indicator?: ReactElement
   title: string
   hide?: boolean
+  disabled?: boolean
 }
 
 type RailActionButton = {
@@ -96,6 +101,8 @@ export const RailLayout = () => {
     togglePane,
     setResizing,
   } = useRailContext()
+  const { logEntries } = useCompileContext()
+  const errorLogsDisabled = !logEntries
 
   const { view, setLeftMenuShown } = useLayoutContext()
 
@@ -103,6 +110,8 @@ export const RailLayout = () => {
 
   const isHistoryView = view === 'history'
 
+  const newErrorlogs = useFeatureFlag('new-editor-error-logs-redesign')
+
   const railTabs: RailElement[] = useMemo(
     () => [
       {
@@ -142,11 +151,12 @@ export const RailLayout = () => {
         key: 'errors',
         icon: 'report',
         title: t('error_log'),
-        component: <ErrorPane />,
+        component: newErrorlogs ? <ErrorLogsPanel /> : <OldErrorPane />,
         indicator: <ErrorIndicator />,
+        disabled: errorLogsDisabled,
       },
     ],
-    [t]
+    [t, errorLogsDisabled, newErrorlogs]
   )
 
   const railActions: RailAction[] = useMemo(
@@ -217,7 +227,7 @@ export const RailLayout = () => {
         <Nav activeKey={selectedTab} className="ide-rail-tabs-nav">
           {railTabs
             .filter(({ hide }) => !hide)
-            .map(({ icon, key, indicator, title }) => (
+            .map(({ icon, key, indicator, title, disabled }) => (
               <RailTab
                 open={isOpen && selectedTab === key}
                 key={key}
@@ -225,6 +235,7 @@ export const RailLayout = () => {
                 icon={icon}
                 indicator={indicator}
                 title={title}
+                disabled={disabled}
               />
             ))}
           <div className="flex-grow-1" />
@@ -251,7 +262,7 @@ export const RailLayout = () => {
             hidden: isHistoryView,
           })}
         >
-          <Tab.Content>
+          <Tab.Content className="ide-rail-tab-content">
             {railTabs
               .filter(({ hide }) => !hide)
               .map(({ key, component }) => (
@@ -291,12 +302,14 @@ const RailTab = ({
   open,
   indicator,
   title,
+  disabled = false,
 }: {
   icon: AvailableUnfilledIcon
   eventKey: string
   open: boolean
   indicator?: ReactElement
   title: string
+  disabled?: boolean
 }) => {
   return (
     <OLTooltip
@@ -309,6 +322,7 @@ const RailTab = ({
         className={classNames('ide-rail-tab-link', {
           'open-rail': open,
         })}
+        disabled={disabled}
       >
         {open ? (
           <MaterialIcon
@@ -382,23 +396,6 @@ const RailActionElement = ({ action }: { action: RailAction }) => {
   }
 }
 
-export const RailPanelHeader: FC<{ title: string }> = ({ title }) => {
-  const { t } = useTranslation()
-  const { handlePaneCollapse } = useRailContext()
-  return (
-    <header className="rail-panel-header">
-      <h4 className="rail-panel-title">{title}</h4>
-      <OLIconButton
-        onClick={handlePaneCollapse}
-        className="rail-panel-header-button-subdued"
-        icon="close"
-        accessibilityLabel={t('close')}
-        size="sm"
-      />
-    </header>
-  )
-}
-
 const RailHelpDropdown = () => {
   const showSupport = getMeta('ol-showSupport')
   const { t } = useTranslation()

+ 7 - 1
services/web/frontend/js/features/pdf-preview/components/pdf-log-entry-content.tsx

@@ -17,12 +17,14 @@ export default function PdfLogEntryContent({
   extraInfoURL,
   index,
   logEntry,
+  alwaysExpandRawContent = false,
 }: {
   rawContent?: string
   formattedContent?: React.ReactNode
   extraInfoURL?: string | null
   index?: number
   logEntry?: LogEntry
+  alwaysExpandRawContent?: boolean
 }) {
   const { t } = useTranslation()
 
@@ -48,7 +50,11 @@ export default function PdfLogEntryContent({
         )}
 
       {rawContent && (
-        <PdfLogEntryRawContent rawContent={rawContent} collapsedSize={150} />
+        <PdfLogEntryRawContent
+          rawContent={rawContent}
+          collapsedSize={150}
+          alwaysExpanded={alwaysExpandRawContent}
+        />
       )}
     </div>
   )

+ 8 - 4
services/web/frontend/js/features/pdf-preview/components/pdf-log-entry-raw-content.tsx

@@ -8,20 +8,24 @@ import Icon from '../../../shared/components/icon'
 export default function PdfLogEntryRawContent({
   rawContent,
   collapsedSize = 0,
+  alwaysExpanded = false,
 }: {
   rawContent: string
   collapsedSize?: number
+  alwaysExpanded?: boolean
 }) {
-  const [expanded, setExpanded] = useState(false)
-  const [needsExpander, setNeedsExpander] = useState(true)
+  const [expanded, setExpanded] = useState(alwaysExpanded)
+  const [needsExpander, setNeedsExpander] = useState(!alwaysExpanded)
 
   const { elementRef } = useResizeObserver(
     useCallback(
       (element: Element) => {
         if (element.scrollHeight === 0) return // skip update when logs-pane is closed
-        setNeedsExpander(element.scrollHeight > collapsedSize)
+        setNeedsExpander(
+          !alwaysExpanded && element.scrollHeight > collapsedSize
+        )
       },
-      [collapsedSize]
+      [collapsedSize, alwaysExpanded]
     )
   )
 

+ 29 - 10
services/web/frontend/js/features/pdf-preview/components/pdf-log-entry.tsx

@@ -1,4 +1,3 @@
-import classNames from 'classnames'
 import { memo, MouseEventHandler, useCallback } from 'react'
 import PreviewLogEntryHeader from '../../preview/components/preview-log-entry-header'
 import PdfLogEntryContent from './pdf-log-entry-content'
@@ -6,6 +5,9 @@ import HumanReadableLogsHints from '../../../ide/human-readable-logs/HumanReadab
 import { sendMB } from '@/infrastructure/event-tracking'
 import getMeta from '@/utils/meta'
 import { ErrorLevel, LogEntry, SourceLocation } from '../util/types'
+import { useIsNewEditorEnabled } from '@/features/ide-redesign/utils/new-editor-utils'
+import NewLogEntry from '@/features/ide-redesign/components/error-logs/log-entry'
+import { useFeatureFlag } from '@/shared/context/split-test-context'
 
 function PdfLogEntry({
   ruleId,
@@ -18,12 +20,9 @@ function PdfLogEntry({
   level,
   sourceLocation,
   showSourceLocationLink = true,
-  showCloseButton = false,
   entryAriaLabel = undefined,
-  customClass,
   contentDetails,
   onSourceLocationClick,
-  onClose,
   index,
   logEntry,
   id,
@@ -38,12 +37,9 @@ function PdfLogEntry({
   extraInfoURL?: string | null
   sourceLocation?: SourceLocation
   showSourceLocationLink?: boolean
-  showCloseButton?: boolean
   entryAriaLabel?: string
-  customClass?: string
   contentDetails?: string[]
   onSourceLocationClick?: (sourceLocation: SourceLocation) => void
-  onClose?: () => void
   index?: number
   logEntry?: LogEntry
   id?: string
@@ -73,9 +69,34 @@ function PdfLogEntry({
       [level, onSourceLocationClick, ruleId, sourceLocation]
     )
 
+  const newEditor = useIsNewEditorEnabled()
+  const newErrorlogs = useFeatureFlag('new-editor-error-logs-redesign')
+
+  if (newEditor && newErrorlogs) {
+    return (
+      <NewLogEntry
+        index={index}
+        id={id}
+        logEntry={logEntry}
+        ruleId={ruleId}
+        headerTitle={headerTitle}
+        formattedContent={formattedContent}
+        rawContent={rawContent}
+        logType={logType}
+        level={level}
+        contentDetails={contentDetails}
+        entryAriaLabel={entryAriaLabel}
+        sourceLocation={sourceLocation}
+        onSourceLocationClick={onSourceLocationClick}
+        showSourceLocationLink={showSourceLocationLink}
+        extraInfoURL={extraInfoURL}
+      />
+    )
+  }
+
   return (
     <div
-      className={classNames('log-entry', customClass)}
+      className="log-entry"
       aria-label={entryAriaLabel}
       data-ruleid={ruleId}
       data-log-entry-id={id}
@@ -88,8 +109,6 @@ function PdfLogEntry({
         logType={logType}
         showSourceLocationLink={showSourceLocationLink}
         onSourceLocationClick={handleLogEntryLinkClick}
-        showCloseButton={showCloseButton}
-        onClose={onClose}
       />
 
       {(rawContent || formattedContent || showAiErrorAssistant) && (

+ 174 - 138
services/web/frontend/js/features/pdf-preview/components/pdf-preview-error.tsx

@@ -6,7 +6,15 @@ import { useDetachCompileContext as useCompileContext } from '../../../shared/co
 import { useStopOnFirstError } from '../../../shared/hooks/use-stop-on-first-error'
 import getMeta from '../../../utils/meta'
 
-function PdfPreviewError({ error }: { error: string }) {
+function PdfPreviewError({
+  error,
+  includeWarnings = true,
+  includeErrors = true,
+}: {
+  error: string
+  includeWarnings?: boolean
+  includeErrors?: boolean
+}) {
   const { t } = useTranslation()
 
   const { startCompile } = useCompileContext()
@@ -14,191 +22,217 @@ function PdfPreviewError({ error }: { error: string }) {
   switch (error) {
     case 'rendering-error-expected':
       return (
-        <PdfLogEntry
-          headerTitle={t('pdf_rendering_error')}
-          formattedContent={
-            <>
-              <Trans
-                i18nKey="something_went_wrong_rendering_pdf_expected"
-                components={[
-                  // eslint-disable-next-line react/jsx-key
-                  <OLButton
-                    variant="primary"
-                    size="sm"
-                    onClick={() => startCompile()}
-                  />,
-                ]}
-              />
-              <br />
-              <br />
-              <Trans
-                i18nKey="last_resort_trouble_shooting_guide"
-                components={[
-                  // eslint-disable-next-line jsx-a11y/anchor-has-content
-                  <a
-                    href="/learn/how-to/Resolving_access%2C_loading%2C_and_display_problems"
-                    target="_blank"
-                    key="troubleshooting-link"
-                  />,
-                ]}
-              />
-            </>
-          }
-          level="warning"
-        />
+        includeWarnings && (
+          <PdfLogEntry
+            headerTitle={t('pdf_rendering_error')}
+            formattedContent={
+              <>
+                <Trans
+                  i18nKey="something_went_wrong_rendering_pdf_expected"
+                  components={[
+                    // eslint-disable-next-line react/jsx-key
+                    <OLButton
+                      variant="primary"
+                      size="sm"
+                      onClick={() => startCompile()}
+                    />,
+                  ]}
+                />
+                <br />
+                <br />
+                <Trans
+                  i18nKey="last_resort_trouble_shooting_guide"
+                  components={[
+                    // eslint-disable-next-line jsx-a11y/anchor-has-content
+                    <a
+                      href="/learn/how-to/Resolving_access%2C_loading%2C_and_display_problems"
+                      target="_blank"
+                      key="troubleshooting-link"
+                    />,
+                  ]}
+                />
+              </>
+            }
+            level="warning"
+          />
+        )
       )
 
     case 'rendering-error':
       return (
-        <ErrorLogEntry title={t('pdf_rendering_error')}>
-          {t('something_went_wrong_rendering_pdf')}
-          &nbsp;
-          <Trans
-            i18nKey="try_recompile_project_or_troubleshoot"
-            components={[
-              // eslint-disable-next-line jsx-a11y/anchor-has-content
-              <a
-                href="/learn/how-to/Resolving_access%2C_loading%2C_and_display_problems"
-                target="_blank"
-                key="troubleshooting-link"
-              />,
-            ]}
-          />
-          {getMeta('ol-compilesUserContentDomain') && (
-            <>
-              <br />
-              <br />
-              <Trans
-                i18nKey="new_compile_domain_notice"
-                values={{
-                  compilesUserContentDomain: new URL(
-                    getMeta('ol-compilesUserContentDomain')
-                  ).hostname,
-                }}
-                shouldUnescape
-                tOptions={{ interpolation: { escapeValue: true } }}
-                components={[
-                  <code key="domain" />,
-                  /* eslint-disable-next-line jsx-a11y/anchor-has-content */
-                  <a
-                    href="/learn/how-to/Resolving_access%2C_loading%2C_and_display_problems"
-                    target="_blank"
-                    key="troubleshooting-link"
-                  />,
-                ]}
-              />
-            </>
-          )}
-        </ErrorLogEntry>
+        includeErrors && (
+          <ErrorLogEntry title={t('pdf_rendering_error')}>
+            {t('something_went_wrong_rendering_pdf')}
+            &nbsp;
+            <Trans
+              i18nKey="try_recompile_project_or_troubleshoot"
+              components={[
+                // eslint-disable-next-line jsx-a11y/anchor-has-content
+                <a
+                  href="/learn/how-to/Resolving_access%2C_loading%2C_and_display_problems"
+                  target="_blank"
+                  key="troubleshooting-link"
+                />,
+              ]}
+            />
+            {getMeta('ol-compilesUserContentDomain') && (
+              <>
+                <br />
+                <br />
+                <Trans
+                  i18nKey="new_compile_domain_notice"
+                  values={{
+                    compilesUserContentDomain: new URL(
+                      getMeta('ol-compilesUserContentDomain')
+                    ).hostname,
+                  }}
+                  shouldUnescape
+                  tOptions={{ interpolation: { escapeValue: true } }}
+                  components={[
+                    <code key="domain" />,
+                    /* eslint-disable-next-line jsx-a11y/anchor-has-content */
+                    <a
+                      href="/learn/how-to/Resolving_access%2C_loading%2C_and_display_problems"
+                      target="_blank"
+                      key="troubleshooting-link"
+                    />,
+                  ]}
+                />
+              </>
+            )}
+          </ErrorLogEntry>
+        )
       )
 
     case 'clsi-maintenance':
       return (
-        <ErrorLogEntry title={t('server_error')}>
-          {t('clsi_maintenance')}
-        </ErrorLogEntry>
+        includeErrors && (
+          <ErrorLogEntry title={t('server_error')}>
+            {t('clsi_maintenance')}
+          </ErrorLogEntry>
+        )
       )
 
     case 'clsi-unavailable':
       return (
-        <ErrorLogEntry title={t('server_error')}>
-          {t('clsi_unavailable')}
-        </ErrorLogEntry>
+        includeErrors && (
+          <ErrorLogEntry title={t('server_error')}>
+            {t('clsi_unavailable')}
+          </ErrorLogEntry>
+        )
       )
 
     case 'too-recently-compiled':
       return (
-        <ErrorLogEntry title={t('server_error')}>
-          {t('too_recently_compiled')}
-        </ErrorLogEntry>
+        includeErrors && (
+          <ErrorLogEntry title={t('server_error')}>
+            {t('too_recently_compiled')}
+          </ErrorLogEntry>
+        )
       )
 
     case 'terminated':
       return (
-        <ErrorLogEntry title={t('terminated')}>
-          {t('compile_terminated_by_user')}
-        </ErrorLogEntry>
+        includeErrors && (
+          <ErrorLogEntry title={t('terminated')}>
+            {t('compile_terminated_by_user')}
+          </ErrorLogEntry>
+        )
       )
 
     case 'rate-limited':
       return (
-        <ErrorLogEntry title={t('pdf_compile_rate_limit_hit')}>
-          {t('project_flagged_too_many_compiles')}
-        </ErrorLogEntry>
+        includeErrors && (
+          <ErrorLogEntry title={t('pdf_compile_rate_limit_hit')}>
+            {t('project_flagged_too_many_compiles')}
+          </ErrorLogEntry>
+        )
       )
 
     case 'compile-in-progress':
       return (
-        <ErrorLogEntry title={t('pdf_compile_in_progress_error')}>
-          {t('pdf_compile_try_again')}
-        </ErrorLogEntry>
+        includeErrors && (
+          <ErrorLogEntry title={t('pdf_compile_in_progress_error')}>
+            {t('pdf_compile_try_again')}
+          </ErrorLogEntry>
+        )
       )
 
     case 'autocompile-disabled':
       return (
-        <ErrorLogEntry title={t('autocompile_disabled')}>
-          {t('autocompile_disabled_reason')}
-        </ErrorLogEntry>
+        includeErrors && (
+          <ErrorLogEntry title={t('autocompile_disabled')}>
+            {t('autocompile_disabled_reason')}
+          </ErrorLogEntry>
+        )
       )
 
     case 'project-too-large':
       return (
-        <ErrorLogEntry title={t('project_too_large')}>
-          {t('project_too_much_editable_text')}
-        </ErrorLogEntry>
+        includeErrors && (
+          <ErrorLogEntry title={t('project_too_large')}>
+            {t('project_too_much_editable_text')}
+          </ErrorLogEntry>
+        )
       )
 
     case 'timedout':
-      return <TimedOutLogEntry />
+      return includeErrors && <TimedOutLogEntry />
 
     case 'failure':
       return (
-        <ErrorLogEntry title={t('no_pdf_error_title')}>
-          {t('no_pdf_error_explanation')}
+        includeErrors && (
+          <ErrorLogEntry title={t('no_pdf_error_title')}>
+            {t('no_pdf_error_explanation')}
 
-          <ul className="my-1 ps-3">
-            <li>{t('no_pdf_error_reason_unrecoverable_error')}</li>
-            <li>
-              <Trans
-                i18nKey="no_pdf_error_reason_no_content"
-                components={{ code: <code /> }}
-              />
-            </li>
-            <li>
-              <Trans
-                i18nKey="no_pdf_error_reason_output_pdf_already_exists"
-                components={{ code: <code /> }}
-              />
-            </li>
-          </ul>
-        </ErrorLogEntry>
+            <ul className="my-1 ps-3">
+              <li>{t('no_pdf_error_reason_unrecoverable_error')}</li>
+              <li>
+                <Trans
+                  i18nKey="no_pdf_error_reason_no_content"
+                  components={{ code: <code /> }}
+                />
+              </li>
+              <li>
+                <Trans
+                  i18nKey="no_pdf_error_reason_output_pdf_already_exists"
+                  components={{ code: <code /> }}
+                />
+              </li>
+            </ul>
+          </ErrorLogEntry>
+        )
       )
 
     case 'clear-cache':
       return (
-        <ErrorLogEntry title={t('server_error')}>
-          {t('somthing_went_wrong_compiling')}
-        </ErrorLogEntry>
+        includeErrors && (
+          <ErrorLogEntry title={t('server_error')}>
+            {t('somthing_went_wrong_compiling')}
+          </ErrorLogEntry>
+        )
       )
 
     case 'pdf-viewer-loading-error':
       return (
-        <ErrorLogEntry title={t('pdf_rendering_error')}>
-          <Trans
-            i18nKey="something_went_wrong_loading_pdf_viewer"
-            components={[
-              <strong key="strong-" />,
-              // eslint-disable-next-line jsx-a11y/anchor-has-content
-              <a
-                href="/learn/how-to/Resolving_access%2C_loading%2C_and_display_problems"
-                target="_blank"
-                key="troubleshooting-link"
-              />,
-              // eslint-disable-next-line jsx-a11y/anchor-has-content
-              <a key="contact-link" target="_blank" href="/contact" />,
-            ]}
-          />
-        </ErrorLogEntry>
+        includeErrors && (
+          <ErrorLogEntry title={t('pdf_rendering_error')}>
+            <Trans
+              i18nKey="something_went_wrong_loading_pdf_viewer"
+              components={[
+                <strong key="strong-" />,
+                // eslint-disable-next-line jsx-a11y/anchor-has-content
+                <a
+                  href="/learn/how-to/Resolving_access%2C_loading%2C_and_display_problems"
+                  target="_blank"
+                  key="troubleshooting-link"
+                />,
+                // eslint-disable-next-line jsx-a11y/anchor-has-content
+                <a key="contact-link" target="_blank" href="/contact" />,
+              ]}
+            />
+          </ErrorLogEntry>
+        )
       )
 
     case 'validation-problems':
@@ -207,9 +241,11 @@ function PdfPreviewError({ error }: { error: string }) {
     case 'error':
     default:
       return (
-        <ErrorLogEntry title={t('server_error')}>
-          {t('somthing_went_wrong_compiling')}
-        </ErrorLogEntry>
+        includeErrors && (
+          <ErrorLogEntry title={t('server_error')}>
+            {t('somthing_went_wrong_compiling')}
+          </ErrorLogEntry>
+        )
       )
   }
 }

+ 7 - 3
services/web/frontend/js/features/pdf-preview/components/timeout-upgrade-paywall-prompt.tsx

@@ -17,7 +17,7 @@ type Segmentation = Record<
 >
 
 interface TimeoutUpgradePaywallPromptProps {
-  setIsShowingPrimary: Dispatch<SetStateAction<boolean>>
+  setIsShowingPrimary?: Dispatch<SetStateAction<boolean>>
 }
 
 function TimeoutUpgradePaywallPrompt({
@@ -40,7 +40,9 @@ function TimeoutUpgradePaywallPrompt({
   function onClose() {
     sendPaywallEvent('paywall-dismiss')
     setIsPaywallDismissed(true)
-    setIsShowingPrimary(false)
+    if (setIsShowingPrimary) {
+      setIsShowingPrimary(false)
+    }
   }
 
   function onClickInfoLink() {
@@ -57,7 +59,9 @@ function TimeoutUpgradePaywallPrompt({
     sendPaywallEvent('paywall-prompt', {
       plan: isStudent ? 'student' : 'collaborator',
     })
-    setIsShowingPrimary(true)
+    if (setIsShowingPrimary) {
+      setIsShowingPrimary(true)
+    }
   }, [isStudent, setIsShowingPrimary])
 
   return (

+ 0 - 14
services/web/frontend/js/features/preview/components/preview-log-entry-header.tsx

@@ -14,9 +14,7 @@ function PreviewLogEntryHeader({
   headerIcon,
   logType,
   showSourceLocationLink = true,
-  showCloseButton = false,
   onSourceLocationClick,
-  onClose,
 }: {
   headerTitle: string | React.ReactNode
   level: ErrorLevel
@@ -24,9 +22,7 @@ function PreviewLogEntryHeader({
   logType?: string
   sourceLocation?: SourceLocation
   showSourceLocationLink?: boolean
-  showCloseButton?: boolean
   onSourceLocationClick?: MouseEventHandler<HTMLButtonElement>
-  onClose?: () => void
 }) {
   const { t } = useTranslation()
   const logLocationSpanRef = useRef<HTMLSpanElement>(null)
@@ -116,16 +112,6 @@ function PreviewLogEntryHeader({
       ) : (
         locationLink
       )}
-      {showCloseButton ? (
-        <OLButton
-          variant="link"
-          className="btn-inline-link log-entry-header-link"
-          aria-label={t('dismiss_error_popup')}
-          onClick={onClose}
-        >
-          <span aria-hidden="true">&times;</span>
-        </OLButton>
-      ) : null}
     </header>
   )
 }

+ 7 - 2
services/web/frontend/js/shared/context/local-compile-context.tsx

@@ -45,7 +45,7 @@ import {
   PdfScrollPosition,
   usePdfScrollPosition,
 } from '@/shared/hooks/use-pdf-scroll-position'
-import { PdfFileDataList } from '@/features/pdf-preview/util/types'
+import { LogEntry, PdfFileDataList } from '@/features/pdf-preview/util/types'
 import { isSplitTestEnabled } from '@/utils/splitTestUtils'
 import { captureException } from '@/infrastructure/error-reporter'
 import OError from '@overleaf/o-error'
@@ -69,7 +69,12 @@ export type CompileContext = {
   hasShortCompileTimeout: boolean
   highlights?: Record<string, any>[]
   isProjectOwner: boolean
-  logEntries?: Record<string, any>
+  logEntries?: {
+    all: LogEntry[]
+    errors: LogEntry[]
+    warnings: LogEntry[]
+    typesetting: LogEntry[]
+  }
   logEntryAnnotations?: Record<string, any>
   outputFilesArchive?: string
   pdfDownloadUrl?: string

+ 0 - 1
services/web/frontend/stories/pdf-log-entry.stories.tsx

@@ -34,7 +34,6 @@ const fakeArgs = {
   formattedContent: 'This is a log entry',
   level: 'error' as const,
   extraInfoURL: 'https://example.com',
-  showCloseButton: true,
   showSourceLocationLink: true,
   rawContent: 'This is a raw log entry',
   contentDetails: ['detail 1', 'detail 2'],

+ 198 - 46
services/web/frontend/stylesheets/bootstrap-5/pages/editor/logs.scss

@@ -1,16 +1,168 @@
 :root {
   --logs-pane-bg: var(--bg-dark-secondary);
-}
-
-.ide-redesign-main {
-  --logs-pane-bg: var(--bg-dark-primary);
+  --logs-info-color: var(--blue-40);
 }
 
 @include theme('light') {
   --logs-pane-bg: var(--bg-light-secondary);
+  --logs-info-color: var(--blue-60);
+}
+
+.ide-redesign-main {
+  --logs-pane-bg: var(--bg-primary-themed);
+
+  .error-logs-panel {
+    display: flex;
+    flex-direction: column;
+    height: 100%;
+  }
+
+  // TODO $ide-redesign-cleanup: Unnest everything from inside .error-logs
+  //  when we remove the styles for the old pane
+  .error-logs {
+    --logs-pane-bg: var(--bg-secondary-themed);
+
+    overflow-y: auto;
+    background-color: var(--logs-pane-bg);
+    flex: 1;
+
+    .logs-pane-content {
+      display: flex;
+      flex-direction: column;
+      padding: var(--spacing-02);
+      gap: var(--spacing-02);
+    }
+
+    .icon-button,
+    .icon-button-small {
+      @include ol-button-variant(
+        var(--content-secondary-themed),
+        transparent,
+        transparent,
+        var(--bg-secondary-themed),
+        transparent,
+        true
+      );
+    }
+
+    .log-entry-content {
+      color: var(--content-primary-themed);
+      background-color: var(--bg-primary-themed);
+      padding: 0;
+    }
+
+    .log-entry-content-raw {
+      color: var(--content-primary-themed);
+      background-color: var(--bg-secondary-themed);
+    }
+
+    .log-entry-content-raw-container {
+      background-color: var(--bg-secondary-themed);
+    }
+
+    .log-entry {
+      background-color: var(--bg-primary-themed);
+      width: 100%;
+      border-radius: var(--border-radius-base);
+      padding: var(--spacing-05) var(--spacing-04);
+
+      .horizontal-divider {
+        margin: var(--spacing-04) 0;
+        border-top: 1px solid var(--border-divider-themed);
+      }
+    }
+
+    .log-entry-header-card {
+      background-color: var(--bg-primary-themed);
+      width: 100%;
+      display: flex;
+      gap: var(--spacing-03);
+      align-items: center;
+      border: none;
+    }
+
+    .log-entry-header-content {
+      display: flex;
+      flex-direction: column;
+      text-align: left;
+      gap: var(--spacing-02);
+      flex: 1;
+      overflow: hidden;
+    }
+
+    .log-entry-header-text {
+      font-weight: 600;
+      font-size: var(--font-size-02);
+      line-height: var(--line-height-02);
+      margin-bottom: 0;
+    }
+
+    .log-entry-header-text-error {
+      color: var(--content-danger-themed);
+    }
+
+    .log-entry-header-text-warning {
+      color: var(--content-warning-themed);
+    }
+
+    .log-entry-header-text-info {
+      color: var(--logs-info-color);
+    }
+
+    .log-entry-header-text-success {
+      color: var(--content-positive-themed);
+    }
+
+    .log-entry-location {
+      white-space: nowrap;
+      direction: rtl;
+      text-overflow: ellipsis;
+      overflow: hidden;
+      font-size: var(--font-size-01);
+      line-height: var(--line-height-01);
+      color: var(--content-secondary-themed);
+    }
+
+    .log-entry-header-actions {
+      display: flex;
+      gap: var(--spacing-03);
+    }
+
+    .log-entry-content-button-container {
+      background-image: linear-gradient(
+        0deg,
+        var(--bg-secondary-themed) 0%,
+        transparent 100%
+      );
+    }
+  }
 
-  .ide-redesign-main {
-    --logs-pane-bg: #fff;
+  .error-logs-tabs {
+    display: flex;
+  }
+
+  .error-logs-tab-header {
+    display: flex;
+    align-items: center;
+    gap: var(--spacing-04);
+    color: var(--content-secondary-themed);
+    padding: var(--spacing-03) var(--spacing-04);
+    font-size: var(--font-size-02);
+    border-bottom: 3px solid transparent;
+
+    &[aria-selected='true'] {
+      color: var(--content-primary-themed);
+      border-bottom: 3px solid var(--bg-accent-01);
+    }
+  }
+
+  .error-logs-tab-count {
+    background-color: var(--bg-secondary-themed);
+    border-radius: 100%;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    padding: 0 var(--spacing-02);
   }
 }
 
@@ -148,53 +300,53 @@
     text-overflow: ellipsis;
     overflow: hidden;
   }
+}
 
-  .log-entry-content {
-    &:empty {
-      display: none;
-    }
-
-    background-color: var(--bg-light-primary);
-    color: var(--content-primary);
-    padding: var(--spacing-04);
+.log-entry-content {
+  &:empty {
+    display: none;
   }
 
-  .log-entry-content-raw {
-    @include body-xs;
+  background-color: var(--bg-light-primary);
+  color: var(--content-primary);
+  padding: var(--spacing-04);
+}
 
-    color: var(--content-secondary);
-    padding: var(--spacing-03);
-    margin: 0;
-    white-space: pre-wrap;
-  }
+.log-entry-content-raw {
+  @include body-xs;
 
-  .log-entry-content-button-container {
-    position: relative;
-    height: 40px;
-    margin-top: 0;
-    transition:
-      margin 0.15s ease-in-out,
-      opacity 0.15s ease-in-out;
-    padding-bottom: var(--spacing-04);
-    text-align: center;
-    background-image: linear-gradient(
-      0deg,
-      var(--bg-light-tertiary) 0%,
-      transparent 100%
-    );
-    border-radius: 0 0 var(--border-radius-base) var(--border-radius-base);
-  }
+  color: var(--content-secondary);
+  padding: var(--spacing-03);
+  margin: 0;
+  white-space: pre-wrap;
+}
 
-  .log-entry-content-button-container-collapsed {
-    margin-top: -40px;
-  }
+.log-entry-content-button-container {
+  position: relative;
+  height: 40px;
+  margin-top: 0;
+  transition:
+    margin 0.15s ease-in-out,
+    opacity 0.15s ease-in-out;
+  padding-bottom: var(--spacing-04);
+  text-align: center;
+  background-image: linear-gradient(
+    0deg,
+    var(--bg-light-tertiary) 0%,
+    transparent 100%
+  );
+  border-radius: 0 0 var(--border-radius-base) var(--border-radius-base);
+}
 
-  .log-entry-content-raw-container {
-    background-color: var(--bg-light-tertiary);
-    border-radius: var(--border-radius-base);
-    overflow: hidden;
-    margin-top: var(--spacing-03);
-  }
+.log-entry-content-button-container-collapsed {
+  margin-top: -40px;
+}
+
+.log-entry-content-raw-container {
+  background-color: var(--bg-light-tertiary);
+  border-radius: var(--border-radius-base);
+  overflow: hidden;
+  margin-top: var(--spacing-03);
 }
 
 .log-entry-formatted-content,

+ 11 - 5
services/web/frontend/stylesheets/bootstrap-5/pages/editor/rail.scss

@@ -35,6 +35,12 @@ body {
   }
 }
 
+.rail-panel-header-actions {
+  display: flex;
+  align-items: center;
+  gap: var(--spacing-01);
+}
+
 .rail-panel-title {
   font-size: var(--font-size-02);
   color: var(--ide-rail-color);
@@ -127,12 +133,12 @@ body {
   .tab-pane {
     height: 100%;
   }
+}
 
-  .tab-content {
-    height: 100%;
-    width: 100%;
-    position: relative;
-  }
+.ide-rail-tab-content {
+  height: 100%;
+  width: 100%;
+  position: relative;
 }
 
 .ide-rail-tabs-nav {

+ 0 - 1
services/web/locales/da.json

@@ -432,7 +432,6 @@
   "disconnected": "Forbindelsen blev afbrudt",
   "discount_of": "Rabat på __amount__",
   "discover_latex_templates_and_examples": "Opdag LaTeX skabeloner og eksempler til at hjælpe med alt fra at skrive en artikel til at bruge en specifik LaTeX pakke.",
-  "dismiss_error_popup": "Afvis første fejlmeddelelse",
   "display_deleted_user": "Vis slettede brugere",
   "do_not_have_acct_or_do_not_want_to_link": "Hvis du ikke har en <b>__appName__</b>-konto, eller hvis du ikke vil kæde den sammen med din <b>__institutionName__</b>-konto, klik venligst <b>__clickText__</b>.",
   "do_not_link_accounts": "Kæd ikke kontoer sammen",

+ 0 - 1
services/web/locales/de.json

@@ -312,7 +312,6 @@
   "disable_stop_on_first_error": "„Anhalten beim ersten Fehler“ deaktivieren",
   "disconnected": "Nicht verbunden",
   "discount_of": "__amount__ Rabatt",
-  "dismiss_error_popup": "Erste Fehlermeldung schließen",
   "do_not_have_acct_or_do_not_want_to_link": "Wenn du kein <b>__appName__</b>-Konto hast oder nicht mit deinem <b>__institutionName__</b>-Konto verknüpfen möchtest, klicke auf <b>„__clickText__“</b>.",
   "do_not_link_accounts": "Konten nicht verknüpfen",
   "do_you_want_to_change_your_primary_email_address_to": "Willst Du deine primäre E-Mail-Adresse in <b>__email__</b> ändern?",

+ 3 - 1
services/web/locales/en.json

@@ -536,7 +536,6 @@
   "discover_latex_templates_and_examples": "Discover LaTeX templates and examples to help with everything from writing a journal article to using a specific LaTeX package.",
   "discover_the_fastest_way_to_search_and_cite": "Discover the fastest way to search and cite",
   "discover_why_over_people_worldwide_trust_overleaf": "Discover why over __count__ million people worldwide trust Overleaf with their work.",
-  "dismiss_error_popup": "Dismiss first error alert",
   "display": "Display",
   "display_deleted_user": "Display deleted users",
   "display_math": "Display math",
@@ -889,6 +888,7 @@
   "go_page": "Go to page __page__",
   "go_prev_page": "Go to Previous Page",
   "go_to_account_settings": "Go to account settings",
+  "go_to_code_location": "Go to code location",
   "go_to_code_location_in_pdf": "Go to code location in PDF",
   "go_to_first_page": "Go to first page",
   "go_to_last_page": "Go to last page",
@@ -1273,6 +1273,7 @@
   "login_to_overleaf": "Log in to Overleaf",
   "login_with_service": "Log in with __service__",
   "login_with_sso": "Log in to Overleaf with SSO",
+  "logs": "Logs",
   "logs_and_output_files": "Logs and output files",
   "longer_compile_timeout": "Longer <0>compile timeout</0>",
   "longer_compile_timeout_on_faster_servers": "Longer compile timeout on faster servers",
@@ -1357,6 +1358,7 @@
   "more_compile_time": "More compile time",
   "more_editor_toolbar_item": "More editor toolbar items",
   "more_info": "More Info",
+  "more_logs_and_files": "More logs and files",
   "more_options": "More options",
   "more_project_collaborators": "<0>More</0> project <0>collaborators</0>",
   "more_than_one_kind_of_snippet_was_requested": "The link to open this content on Overleaf included some invalid parameters. If this keeps happening for links on a particular site, please report this to them.",

+ 0 - 1
services/web/locales/fr.json

@@ -344,7 +344,6 @@
   "disable_stop_on_first_error": "Désactiver “Arrêter à la première erreur”",
   "disconnected": "Déconnecté",
   "discount_of": "Remise de __amount__",
-  "dismiss_error_popup": "Ignorer l’alerte de première erreur",
   "do_not_have_acct_or_do_not_want_to_link": "Si vous n’avez pas de compte <b>__appName__</b> ou si vous ne souhaitez pas le lier à votre compte <b>__institutionName__</b>, veuillez cliquer <b>__clickText__</b>.",
   "do_not_link_accounts": "Ne pas lier les comptes",
   "do_you_want_to_change_your_primary_email_address_to": "Voulez-vous définir <b>__email__</b> comme votre adresse email principale ?",

+ 0 - 1
services/web/locales/sv.json

@@ -208,7 +208,6 @@
   "dictionary": "Ordbok",
   "disable_stop_on_first_error": "Inaktivera \"Stopp vid första fel\"",
   "disconnected": "Frånkopplad",
-  "dismiss_error_popup": "Avfärda varning om första fel",
   "do_not_have_acct_or_do_not_want_to_link": "Om du inte har ett <b>__appName__</b>-konto, eller om du inte vill länka till ditt <b>__institutionName__</b>-konto, vänligen klicka på <b>__clickText__</b>.",
   "do_not_link_accounts": "Länka ej konton",
   "documentation": "Dokumentation",

+ 0 - 1
services/web/locales/zh-CN.json

@@ -518,7 +518,6 @@
   "discover_latex_templates_and_examples": "探索 LaTeX 模板和示例,以帮助完成从撰写期刊文章到使用特定 LaTeX 包的所有工作。",
   "discover_the_fastest_way_to_search_and_cite": "探索搜索和引用的最快方法",
   "discover_why_over_people_worldwide_trust_overleaf": "了解为什么全世界有超过__count__万人信任 Overleaf 并把工作交给它。",
-  "dismiss_error_popup": "忽略第一个错误提示",
   "display": "显示",
   "display_deleted_user": "显示已删除的用户",
   "display_math": "显示数学公式",