Skip to content

Commit

Permalink
fix: unify code block display (#4182)
Browse files Browse the repository at this point in the history
- Improve formatting to make things more consistent
- Removes `highlight.js` and just reuses `codemirror` everywhere.
- New component for showing code with a title
- Better light mode handling
- New `maxHeight` option

BEFORE:
![Screenshot 2025-01-24 at 1 09
02 PM](https://github.com/user-attachments/assets/3834ea23-b040-4f02-9926-3ea620474da6)
![Screenshot 2025-01-24 at 1 09
10 PM](https://github.com/user-attachments/assets/585c6cb4-e705-4094-acd6-2341e09cdbf8)

AFTER:
![Screenshot 2025-01-24 at 1 04
29 PM](https://github.com/user-attachments/assets/7bc9e342-3114-467f-95ae-e4e6b0df43a6)
![Screenshot 2025-01-24 at 1 04
36 PM](https://github.com/user-attachments/assets/4ecd901f-3978-4a63-8c83-0f8077debb96)
  • Loading branch information
wesbillman authored Jan 24, 2025
1 parent 965232d commit 57cf1b9
Show file tree
Hide file tree
Showing 16 changed files with 112 additions and 320 deletions.
9 changes: 2 additions & 7 deletions frontend/console/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,9 @@
"build-storybook": "storybook build"
},
"lint-staged": {
"*.(js|cjs|tsx|ts)": [
"pnpm run lint:fix"
]
"*.(js|cjs|tsx|ts)": ["pnpm run lint:fix"]
},
"browserslist": [
"> 2%"
],
"browserslist": ["> 2%"],
"source": "index.html",
"dependencies": {
"@bufbuild/protobuf": "^1.10.0",
Expand All @@ -57,7 +53,6 @@
"dagre": "^0.8.5",
"fnv1a": "^1.1.1",
"fuse.js": "^7.0.0",
"highlight.js": "^11.8.0",
"hugeicons-react": "^0.3.0",
"json-schema-faker": "0.5.8",
"please-use-pnpm": "^1.1.0",
Expand Down
72 changes: 43 additions & 29 deletions frontend/console/src/components/CodeBlock.tsx
Original file line number Diff line number Diff line change
@@ -1,44 +1,58 @@
import hljs from 'highlight.js/lib/core'
import go from 'highlight.js/lib/languages/go'
import graphql from 'highlight.js/lib/languages/graphql'
import json from 'highlight.js/lib/languages/json'
import plaintext from 'highlight.js/lib/languages/plaintext'
import 'highlight.js/styles/atom-one-dark.css'
import { json } from '@codemirror/lang-json'
import { EditorState } from '@codemirror/state'
import { EditorView } from '@codemirror/view'
import { atomone } from '@uiw/codemirror-theme-atomone'
import { githubLight } from '@uiw/codemirror-theme-github'
import { useEffect, useRef } from 'react'
import { useUserPreferences } from '../providers/user-preferences-provider'

interface Props {
code: string
language: string
maxHeight?: number
}

export const CodeBlock = ({ code, language, maxHeight }: Props) => {
const codeRef = useRef<HTMLElement>(null)
export const CodeBlock = ({ code, maxHeight = 300 }: Props) => {
const editorContainerRef = useRef<HTMLDivElement>(null)
const editorViewRef = useRef<EditorView | null>(null)
const { isDarkMode } = useUserPreferences()

useEffect(() => {
hljs.configure({ ignoreUnescapedHTML: true })
hljs.registerLanguage('graphql', graphql)
hljs.registerLanguage('json', json)
hljs.registerLanguage('go', go)
hljs.registerLanguage('plaintext', plaintext)
if (editorContainerRef.current) {
const state = EditorState.create({
doc: code,
extensions: [
EditorState.readOnly.of(true),
isDarkMode ? atomone : githubLight,
json(),
EditorView.theme({
'&': {
minHeight: '2em',
backgroundColor: isDarkMode ? '#282c34' : '#f6f8fa',
},
'.cm-scroller': {
overflow: 'auto',
padding: '6px',
maxHeight: maxHeight ? `${maxHeight}px` : 'none',
},
'.cm-content': {
minHeight: '2em',
},
}),
],
})

if (codeRef.current) {
codeRef.current.removeAttribute('data-highlighted')
hljs.highlightElement(codeRef.current)
}
const view = new EditorView({
state,
parent: editorContainerRef.current,
})

editorViewRef.current = view

return () => {
if (codeRef.current) {
codeRef.current.removeAttribute('data-highlighted')
return () => {
view.destroy()
}
}
}, [code, language])
}, [code, isDarkMode, maxHeight])

return (
<pre style={{ maxHeight: maxHeight ? `${maxHeight}px` : 'auto' }}>
<code ref={codeRef} className={`language-${language} text-xs`}>
{code}
</code>
</pre>
)
return <div ref={editorContainerRef} className='rounded-md overflow-hidden border border-gray-200 dark:border-gray-600/20' />
}
27 changes: 27 additions & 0 deletions frontend/console/src/components/CodeBlockWithTitle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { CodeBlock } from './CodeBlock'

interface Props {
title: string
code: string
maxHeight?: number
}

export const CodeBlockWithTitle = ({ title, code, maxHeight }: Props) => {
const formattedCode = (() => {
try {
// If it's already a JSON string, parse and re-stringify to ensure consistent formatting
const parsed = JSON.parse(code)
return JSON.stringify(parsed, null, 2)
} catch {
// If it's not valid JSON, return as-is
return code
}
})()

return (
<div className='mt-4'>
<div className='text-xs font-medium text-gray-600 dark:text-gray-400 mb-1 uppercase tracking-wider'>{title}</div>
<CodeBlock code={formattedCode} maxHeight={maxHeight} />
</div>
)
}
131 changes: 0 additions & 131 deletions frontend/console/src/components/CodeEditorV2.tsx

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { AttributeBadge } from '../../../components/AttributeBadge'
import { CodeBlock } from '../../../components/CodeBlock'
import { CodeBlockWithTitle } from '../../../components/CodeBlockWithTitle'
import type { AsyncExecuteEvent, Event } from '../../../protos/xyz/block/ftl/timeline/v1/event_pb'
import { formatDuration } from '../../../utils/date.utils'
import { DeploymentCard } from '../../deployments/DeploymentCard'
Expand All @@ -19,12 +19,7 @@ export const TimelineAsyncExecuteDetails = ({ event }: { event: Event }) => {
<TraceGraph requestKey={asyncEvent.requestKey} selectedEventId={event.id} />
</div>

{asyncEvent.error && (
<>
<h3>Error</h3>
<CodeBlock code={asyncEvent.error} language='text' />
</>
)}
{asyncEvent.error && <CodeBlockWithTitle title='Error' code={asyncEvent.error} />}
<DeploymentCard deploymentKey={asyncEvent.deploymentKey} />
<ul className='pt-4 space-y-2'>
{asyncEvent.requestKey && (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { AttributeBadge } from '../../../components/AttributeBadge'
import { CodeBlock } from '../../../components/CodeBlock'
import { CodeBlockWithTitle } from '../../../components/CodeBlockWithTitle'
import type { CallEvent, Event } from '../../../protos/xyz/block/ftl/timeline/v1/event_pb'
import { formatDuration } from '../../../utils/date.utils'
import { DeploymentCard } from '../../deployments/DeploymentCard'
Expand All @@ -17,28 +17,13 @@ export const TimelineCallDetails = ({ event }: { event: Event }) => {
<TraceGraph requestKey={call.requestKey} selectedEventId={event.id} />
</div>

<div className='text-sm pt-2'>Request</div>
<CodeBlock code={call.request ? JSON.stringify(JSON.parse(call.request || '{}'), null, 2) : ''} language='json' />
{call.request && <CodeBlockWithTitle title='Request' code={JSON.stringify(JSON.parse(call.request || '{}'), null, 2)} />}

{call.response !== 'null' && (
<>
<div className='text-sm pt-2'>Response</div>
<CodeBlock code={JSON.stringify(JSON.parse(call.response || '{}'), null, 2)} language='json' />
</>
)}
{call.response && <CodeBlockWithTitle title='Response' code={JSON.stringify(JSON.parse(call.response || '{}'), null, 2)} />}

{call.error && (
<>
<h3 className='pt-4'>Error</h3>
<CodeBlock code={call.error} language='text' />
{call.stack && (
<>
<h3 className='pt-4'>Stack</h3>
<CodeBlock code={call.stack} language='text' />
</>
)}
</>
)}
{call.error && <CodeBlockWithTitle title='Error' code={call.error} />}

{call.stack && <CodeBlockWithTitle title='Stack' code={call.stack} />}

<DeploymentCard className='mt-4' deploymentKey={call.deploymentKey} />

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { AttributeBadge } from '../../../components/AttributeBadge'
import { CodeBlock } from '../../../components/CodeBlock'
import { CodeBlockWithTitle } from '../../../components/CodeBlockWithTitle'
import type { CronScheduledEvent, Event } from '../../../protos/xyz/block/ftl/timeline/v1/event_pb'
import { formatDuration, formatTimestampShort } from '../../../utils/date.utils'
import { DeploymentCard } from '../../deployments/DeploymentCard'
Expand All @@ -11,13 +11,7 @@ export const TimelineCronScheduledDetails = ({ event }: { event: Event }) => {
return (
<>
<div className='p-4'>
{cron.error && (
<>
<h3>Error</h3>
<CodeBlock code={cron.error} language='text' />
</>
)}

{cron.error && <CodeBlockWithTitle title='Error' code={cron.error} />}
<DeploymentCard deploymentKey={cron.deploymentKey} />

<ul className='pt-4 space-y-2'>
Expand Down
Loading

0 comments on commit 57cf1b9

Please sign in to comment.