pdf-preview.stories.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567
  1. import { withContextRoot } from './utils/with-context-root'
  2. import { useCallback, useEffect, useMemo, useState } from 'react'
  3. import useFetchMock from './hooks/use-fetch-mock'
  4. import { setupContext } from './fixtures/context'
  5. import { Button } from 'react-bootstrap'
  6. import PdfPreviewPane from '../js/features/pdf-preview/components/pdf-preview-pane'
  7. import PdfPreview from '../js/features/pdf-preview/components/pdf-preview'
  8. import PdfPreviewToolbar from '../js/features/pdf-preview/components/pdf-preview-toolbar'
  9. import PdfFileList from '../js/features/pdf-preview/components/pdf-file-list'
  10. import { buildFileList } from '../js/features/pdf-preview/util/file-list'
  11. import PdfLogsViewer from '../js/features/pdf-preview/components/pdf-logs-viewer'
  12. import examplePdf from './fixtures/storybook-example.pdf'
  13. import PdfPreviewError from '../js/features/pdf-preview/components/pdf-preview-error'
  14. import PdfPreviewHybridToolbar from '../js/features/pdf-preview/components/pdf-preview-hybrid-toolbar'
  15. import { useCompileContext } from '../js/shared/context/compile-context'
  16. setupContext()
  17. export default {
  18. title: 'PDF Preview',
  19. component: PdfPreview,
  20. subcomponents: {
  21. PdfPreviewToolbar,
  22. PdfPreviewHybridToolbar,
  23. PdfFileList,
  24. PdfPreviewError,
  25. },
  26. }
  27. const project = {
  28. _id: 'a-project',
  29. name: 'A Project',
  30. features: {},
  31. tokens: {},
  32. owner: {
  33. _id: 'a-user',
  34. email: 'stories@overleaf.com',
  35. },
  36. members: [],
  37. invites: [],
  38. }
  39. const scope = {
  40. project,
  41. settings: {
  42. syntaxValidation: true,
  43. },
  44. hasLintingError: false,
  45. $applyAsync: () => {},
  46. editor: {
  47. sharejs_doc: {
  48. doc_id: 'test-doc',
  49. getSnapshot: () => 'some doc content',
  50. },
  51. },
  52. }
  53. const dispatchProjectJoined = () => {
  54. window.dispatchEvent(new CustomEvent('project:joined', { detail: project }))
  55. }
  56. const dispatchDocChanged = () => {
  57. window.dispatchEvent(
  58. new CustomEvent('doc:changed', { detail: { doc_id: 'foo' } })
  59. )
  60. }
  61. const outputFiles = [
  62. {
  63. path: 'output.pdf',
  64. build: '123',
  65. url: '/build/output.pdf',
  66. type: 'pdf',
  67. },
  68. {
  69. path: 'output.bbl',
  70. build: '123',
  71. url: '/build/output.bbl',
  72. type: 'bbl',
  73. },
  74. {
  75. path: 'output.bib',
  76. build: '123',
  77. url: '/build/output.bib',
  78. type: 'bib',
  79. },
  80. {
  81. path: 'example.txt',
  82. build: '123',
  83. url: '/build/example.txt',
  84. type: 'txt',
  85. },
  86. {
  87. path: 'output.log',
  88. build: '123',
  89. url: '/build/output.log',
  90. type: 'log',
  91. },
  92. {
  93. path: 'output.blg',
  94. build: '123',
  95. url: '/build/output.blg',
  96. type: 'blg',
  97. },
  98. ]
  99. const mockCompile = (fetchMock, delay = 1000) =>
  100. fetchMock.post(
  101. 'express:/project/:projectId/compile',
  102. {
  103. body: {
  104. status: 'success',
  105. clsiServerId: 'foo',
  106. compileGroup: 'priority',
  107. pdfDownloadDomain: '',
  108. outputFiles,
  109. },
  110. },
  111. { delay }
  112. )
  113. const mockCompileError = (fetchMock, status = 'success', delay = 1000) =>
  114. fetchMock.post(
  115. 'express:/project/:projectId/compile',
  116. {
  117. body: {
  118. status,
  119. clsiServerId: 'foo',
  120. compileGroup: 'priority',
  121. },
  122. },
  123. { delay, overwriteRoutes: true }
  124. )
  125. const mockCompileValidationIssues = (
  126. fetchMock,
  127. validationProblems,
  128. delay = 1000
  129. ) =>
  130. fetchMock.post(
  131. 'express:/project/:projectId/compile',
  132. () => {
  133. return {
  134. body: {
  135. status: 'validation-problems',
  136. validationProblems,
  137. clsiServerId: 'foo',
  138. compileGroup: 'priority',
  139. },
  140. }
  141. },
  142. { delay, overwriteRoutes: true }
  143. )
  144. const mockClearCache = fetchMock =>
  145. fetchMock.delete('express:/project/:projectId/output', 204, {
  146. delay: 1000,
  147. })
  148. const mockBuildFile = fetchMock =>
  149. fetchMock.get(
  150. 'express:/build/:file',
  151. (url, options, request) => {
  152. const { pathname } = new URL(url, 'https://example.com')
  153. switch (pathname) {
  154. case '/build/output.blg':
  155. return 'This is BibTeX, Version 4.0' // FIXME
  156. case '/build/output.log':
  157. return `
  158. The LaTeX compiler output
  159. * With a lot of details
  160. Wrapped in an HTML <pre> element with
  161. preformatted text which is to be presented exactly
  162. as written in the HTML file
  163. (whitespace included™)
  164. The text is typically rendered using a non-proportional ("monospace") font.
  165. LaTeX Font Info: External font \`cmex10' loaded for size
  166. (Font) <7> on input line 18.
  167. LaTeX Font Info: External font \`cmex10' loaded for size
  168. (Font) <5> on input line 18.
  169. ! Undefined control sequence.
  170. <recently read> \\Zlpha
  171. main.tex, line 23
  172. `
  173. case '/build/output.pdf':
  174. return new Promise(resolve => {
  175. const xhr = new XMLHttpRequest()
  176. xhr.addEventListener('load', () => {
  177. resolve({
  178. status: 200,
  179. headers: {
  180. 'Content-Length': xhr.getResponseHeader('Content-Length'),
  181. 'Content-Type': xhr.getResponseHeader('Content-Type'),
  182. },
  183. body: xhr.response,
  184. })
  185. })
  186. xhr.open('GET', examplePdf)
  187. xhr.responseType = 'arraybuffer'
  188. xhr.send()
  189. })
  190. default:
  191. return 404
  192. }
  193. },
  194. { sendAsJson: false }
  195. )
  196. export const Interactive = () => {
  197. useFetchMock(fetchMock => {
  198. mockCompile(fetchMock)
  199. mockBuildFile(fetchMock)
  200. mockClearCache(fetchMock)
  201. })
  202. useEffect(() => {
  203. dispatchProjectJoined()
  204. }, [])
  205. const Inner = () => {
  206. const context = useCompileContext()
  207. const { setHasLintingError } = context
  208. const toggleLintingError = useCallback(() => {
  209. setHasLintingError(value => !value)
  210. }, [setHasLintingError])
  211. const values = useMemo(() => {
  212. const entries = Object.entries(context).sort((a, b) => {
  213. return a[0].localeCompare(b[0])
  214. })
  215. const values = { boolean: [], other: [] }
  216. for (const entry of entries) {
  217. const type = typeof entry[1]
  218. if (type === 'boolean') {
  219. values.boolean.push(entry)
  220. } else if (type !== 'function') {
  221. values.other.push(entry)
  222. }
  223. }
  224. return values
  225. }, [context])
  226. return (
  227. <div
  228. style={{
  229. padding: 20,
  230. background: 'white',
  231. float: 'left',
  232. zIndex: 10,
  233. position: 'absolute',
  234. top: 60,
  235. bottom: 60,
  236. right: 20,
  237. left: 400,
  238. overflow: 'hidden',
  239. boxShadow: '0px 2px 5px #ccc',
  240. borderRadius: 3,
  241. }}
  242. >
  243. <div
  244. style={{
  245. display: 'flex',
  246. fontSize: 14,
  247. gap: 20,
  248. height: '100%',
  249. overflow: 'hidden',
  250. }}
  251. >
  252. <div style={{ height: '100%', overflow: 'auto', flexShrink: 0 }}>
  253. <table>
  254. <tbody>
  255. {values.boolean.map(([key, value]) => {
  256. return (
  257. <tr key={key} style={{ border: '1px solid #ddd' }}>
  258. <td style={{ padding: 5 }}>{value ? '🟢' : '🔴'}</td>
  259. <th style={{ padding: 5 }}>{key}</th>
  260. </tr>
  261. )
  262. })}
  263. </tbody>
  264. </table>
  265. <div>
  266. <div
  267. style={{
  268. display: 'flex',
  269. flexDirection: 'column',
  270. gap: 10,
  271. margin: '10px 0',
  272. }}
  273. >
  274. <Button onClick={dispatchDocChanged}>trigger doc change</Button>
  275. <Button onClick={toggleLintingError}>
  276. toggle linting error
  277. </Button>
  278. </div>
  279. </div>
  280. </div>
  281. <div style={{ height: '100%', overflow: 'auto' }}>
  282. <table
  283. style={{
  284. width: '100%',
  285. overflow: 'hidden',
  286. }}
  287. >
  288. <tbody>
  289. {values.other.map(([key, value]) => {
  290. return (
  291. <tr
  292. key={key}
  293. style={{
  294. width: '100%',
  295. overflow: 'hidden',
  296. border: '1px solid #ddd',
  297. }}
  298. >
  299. <th
  300. style={{
  301. verticalAlign: 'top',
  302. padding: 5,
  303. }}
  304. >
  305. {key}
  306. </th>
  307. <td
  308. style={{
  309. overflow: 'auto',
  310. padding: 5,
  311. }}
  312. >
  313. <pre
  314. style={{
  315. margin: '0 10px',
  316. fontSize: 10,
  317. }}
  318. >
  319. {JSON.stringify(value, null, 2)}
  320. </pre>
  321. </td>
  322. </tr>
  323. )
  324. })}
  325. </tbody>
  326. </table>
  327. </div>
  328. </div>
  329. </div>
  330. )
  331. }
  332. return withContextRoot(
  333. <div className="pdf-viewer">
  334. <PdfPreviewPane />
  335. <Inner />
  336. </div>,
  337. scope
  338. )
  339. }
  340. const compileStatuses = [
  341. 'autocompile-backoff',
  342. 'clear-cache',
  343. 'clsi-maintenance',
  344. 'compile-in-progress',
  345. 'exited',
  346. 'failure',
  347. 'generic',
  348. 'project-too-large',
  349. 'rate-limited',
  350. 'success',
  351. 'terminated',
  352. 'timedout',
  353. 'too-recently-compiled',
  354. 'unavailable',
  355. 'validation-problems',
  356. 'foo',
  357. ]
  358. export const CompileError = () => {
  359. const [status, setStatus] = useState('success')
  360. useFetchMock(fetchMock => {
  361. mockCompileError(fetchMock, status, 0)
  362. mockBuildFile(fetchMock)
  363. })
  364. const Inner = () => {
  365. const { startCompile } = useCompileContext()
  366. const handleStatusChange = useCallback(
  367. event => {
  368. setStatus(event.target.value)
  369. window.setTimeout(() => {
  370. startCompile()
  371. }, 0)
  372. },
  373. [startCompile]
  374. )
  375. return (
  376. <div
  377. style={{
  378. position: 'absolute',
  379. bottom: 10,
  380. left: 10,
  381. background: 'white',
  382. padding: 10,
  383. }}
  384. >
  385. <label>
  386. {'status: '}
  387. <select value={status} onInput={handleStatusChange}>
  388. {compileStatuses.map(status => (
  389. <option key={status}>{status}</option>
  390. ))}
  391. </select>
  392. </label>
  393. </div>
  394. )
  395. }
  396. return withContextRoot(
  397. <>
  398. <PdfPreviewPane />
  399. <Inner />
  400. </>,
  401. scope
  402. )
  403. }
  404. const compileErrors = [
  405. 'autocompile-backoff',
  406. 'clear-cache',
  407. 'clsi-maintenance',
  408. 'compile-in-progress',
  409. 'exited',
  410. 'failure',
  411. 'generic',
  412. 'project-too-large',
  413. 'rate-limited',
  414. 'success',
  415. 'terminated',
  416. 'timedout',
  417. 'too-recently-compiled',
  418. 'unavailable',
  419. 'validation-problems',
  420. 'foo',
  421. ]
  422. export const DisplayError = () => {
  423. return withContextRoot(
  424. <>
  425. {compileErrors.map(error => (
  426. <div
  427. key={error}
  428. style={{ background: '#5d6879', padding: 10, margin: 5 }}
  429. >
  430. <div style={{ fontFamily: 'monospace', color: 'white' }}>{error}</div>
  431. <PdfPreviewError error={error} />
  432. </div>
  433. ))}
  434. </>,
  435. scope
  436. )
  437. }
  438. export const Toolbar = () => {
  439. useFetchMock(fetchMock => mockCompile(fetchMock, 500))
  440. return withContextRoot(
  441. <div className="pdf">
  442. <PdfPreviewToolbar />
  443. </div>,
  444. scope
  445. )
  446. }
  447. export const HybridToolbar = () => {
  448. useFetchMock(fetchMock => {
  449. mockCompile(fetchMock, 500)
  450. mockBuildFile(fetchMock)
  451. })
  452. return withContextRoot(
  453. <div className="pdf">
  454. <PdfPreviewHybridToolbar />
  455. </div>,
  456. scope
  457. )
  458. }
  459. export const FileList = () => {
  460. const fileList = useMemo(() => {
  461. return buildFileList(outputFiles)
  462. }, [])
  463. return (
  464. <div className="dropdown open">
  465. <div className="dropdown-menu">
  466. <PdfFileList fileList={fileList} />
  467. </div>
  468. </div>
  469. )
  470. }
  471. export const Logs = () => {
  472. useFetchMock(fetchMock => {
  473. mockCompileError(fetchMock, 400, 0)
  474. mockBuildFile(fetchMock)
  475. mockClearCache(fetchMock)
  476. })
  477. return withContextRoot(
  478. <div className="pdf">
  479. <PdfLogsViewer />
  480. </div>,
  481. scope
  482. )
  483. }
  484. const validationProblems = {
  485. sizeCheck: {
  486. resources: [
  487. { path: 'foo/bar', kbSize: 76221 },
  488. { path: 'bar/baz', kbSize: 2342 },
  489. ],
  490. },
  491. mainFile: true,
  492. conflictedPaths: [
  493. {
  494. path: 'foo/bar',
  495. },
  496. {
  497. path: 'foo/baz',
  498. },
  499. ],
  500. }
  501. export const ValidationIssues = () => {
  502. useFetchMock(fetchMock => {
  503. mockCompileValidationIssues(fetchMock, validationProblems, 0)
  504. mockBuildFile(fetchMock)
  505. })
  506. useEffect(() => {
  507. dispatchProjectJoined()
  508. }, [])
  509. return withContextRoot(<PdfPreviewPane />, scope)
  510. }