Skip to content

Commit 4679330

Browse files
committed
feat: add expansion depth to json tree
1 parent 4d7425e commit 4679330

3 files changed

Lines changed: 146 additions & 104 deletions

File tree

‎.changeset/silent-beds-create.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@tanstack/devtools-ui': patch
3+
---
4+
5+
Improvements to the json tree component, now supports expansion length config

‎packages/devtools-ui/src/components/tree.tsx‎

Lines changed: 134 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -3,108 +3,21 @@ import clsx from 'clsx'
33
import { css, useStyles } from '../styles/use-styles'
44
import { CopiedCopier, Copier, ErrorCopier } from './icons'
55

6-
export function JsonTree(props: { value: any; copyable?: boolean }) {
7-
return <JsonValue isRoot value={props.value} copyable={props.copyable} />
6+
export function JsonTree(props: { value: any; copyable?: boolean, defaultExpansionDepth?: number }) {
7+
return <JsonValue isRoot value={props.value} copyable={props.copyable} depth={0} defaultExpansionDepth={props.defaultExpansionDepth ?? 1} />
88
}
9-
type CopyState = 'NoCopy' | 'SuccessCopy' | 'ErrorCopy'
10-
11-
const CopyButton = (props: { value: unknown }) => {
12-
const styles = useStyles()
13-
const [copyState, setCopyState] = createSignal<CopyState>('NoCopy')
149

15-
return (
16-
<button
17-
class={styles().tree.actionButton}
18-
title="Copy object to clipboard"
19-
aria-label={`${
20-
copyState() === 'NoCopy'
21-
? 'Copy object to clipboard'
22-
: copyState() === 'SuccessCopy'
23-
? 'Object copied to clipboard'
24-
: 'Error copying object to clipboard'
25-
}`}
26-
onClick={
27-
copyState() === 'NoCopy'
28-
? () => {
29-
navigator.clipboard
30-
.writeText(JSON.stringify(props.value, null, 2))
31-
.then(
32-
() => {
33-
setCopyState('SuccessCopy')
34-
setTimeout(() => {
35-
setCopyState('NoCopy')
36-
}, 1500)
37-
},
38-
(err) => {
39-
console.error('Failed to copy: ', err)
40-
setCopyState('ErrorCopy')
41-
setTimeout(() => {
42-
setCopyState('NoCopy')
43-
}, 1500)
44-
},
45-
)
46-
}
47-
: undefined
48-
}
49-
>
50-
<Switch>
51-
<Match when={copyState() === 'NoCopy'}>
52-
<Copier />
53-
</Match>
54-
<Match when={copyState() === 'SuccessCopy'}>
55-
<CopiedCopier theme={'dark'} />
56-
</Match>
57-
<Match when={copyState() === 'ErrorCopy'}>
58-
<ErrorCopier />
59-
</Match>
60-
</Switch>
61-
</button>
62-
)
63-
}
64-
65-
const Expander = (props: { expanded: boolean }) => {
66-
const styles = useStyles()
67-
return (
68-
<span
69-
class={clsx(
70-
styles().tree.expander,
71-
css`
72-
transform: rotate(${props.expanded ? 90 : 0}deg);
73-
`,
74-
props.expanded &&
75-
css`
76-
& svg {
77-
top: -1px;
78-
}
79-
`,
80-
)}
81-
>
82-
<svg
83-
width="16"
84-
height="16"
85-
viewBox="0 0 16 16"
86-
fill="none"
87-
xmlns="http://www.w3.org/2000/svg"
88-
>
89-
<path
90-
d="M6 12L10 8L6 4"
91-
stroke-width="2"
92-
stroke-linecap="round"
93-
stroke-linejoin="round"
94-
/>
95-
</svg>
96-
</span>
97-
)
98-
}
9910

10011
function JsonValue(props: {
10112
value: any
10213
keyName?: string
10314
isRoot?: boolean
10415
isLastKey?: boolean
10516
copyable?: boolean
17+
defaultExpansionDepth: number
18+
depth: number
10619
}) {
107-
const { value, keyName, isRoot = false, isLastKey, copyable } = props
20+
const { value, keyName, isRoot = false, isLastKey, copyable, defaultExpansionDepth, depth } = props
10821
const styles = useStyles()
10922

11023
return (
@@ -137,12 +50,12 @@ function JsonValue(props: {
13750
}
13851
if (Array.isArray(value)) {
13952
return (
140-
<ArrayValue copyable={copyable} keyName={keyName} value={value} />
53+
<ArrayValue defaultExpansionDepth={defaultExpansionDepth} depth={depth} copyable={copyable} keyName={keyName} value={value} />
14154
)
14255
}
14356
if (typeof value === 'object') {
14457
return (
145-
<ObjectValue copyable={copyable} keyName={keyName} value={value} />
58+
<ObjectValue defaultExpansionDepth={defaultExpansionDepth} depth={depth} copyable={copyable} keyName={keyName} value={value} />
14659
)
14760
}
14861
return <span />
@@ -161,16 +74,32 @@ const ArrayValue = ({
16174
value,
16275
keyName,
16376
copyable,
77+
defaultExpansionDepth,
78+
depth
16479
}: {
16580
value: Array<any>
16681
copyable?: boolean
16782
keyName?: string
83+
defaultExpansionDepth: number
84+
depth: number
16885
}) => {
16986
const styles = useStyles()
170-
const [expanded, setExpanded] = createSignal(true)
87+
const [expanded, setExpanded] = createSignal(depth <= defaultExpansionDepth)
88+
89+
if (value.length === 0) {
90+
return (<span class={styles().tree.expanderContainer}>
91+
{keyName && <span
92+
93+
class={clsx(styles().tree.valueKey, styles().tree.collapsible)}
94+
>
95+
&quot;{keyName}&quot;:{' '}
96+
</span>}
97+
<span class={styles().tree.valueBraces}>[]</span>
98+
</span>)
99+
}
171100
return (
172101
<span class={styles().tree.expanderContainer}>
173-
<Expander expanded={expanded()} />
102+
<Expander onClick={() => setExpanded(!expanded())} expanded={expanded()} />
174103
{keyName && (
175104
<span
176105
onclick={(e) => {
@@ -195,6 +124,8 @@ const ArrayValue = ({
195124
copyable={copyable}
196125
value={item}
197126
isLastKey={isLastKey}
127+
defaultExpansionDepth={defaultExpansionDepth}
128+
depth={depth + 1}
198129
/>
199130
)
200131
}}
@@ -222,19 +153,31 @@ const ObjectValue = ({
222153
value,
223154
keyName,
224155
copyable,
156+
defaultExpansionDepth,
157+
depth
225158
}: {
226159
value: Record<string, any>
227160
keyName?: string
228161
copyable?: boolean
162+
defaultExpansionDepth: number
163+
depth: number
229164
}) => {
230165
const styles = useStyles()
231-
const [expanded, setExpanded] = createSignal(true)
166+
const [expanded, setExpanded] = createSignal(depth <= defaultExpansionDepth)
232167
const keys = Object.keys(value)
233168
const lastKeyName = keys[keys.length - 1]
234169

170+
if (keys.length === 0) {
171+
return (<span class={styles().tree.expanderContainer}>
172+
{keyName && <span class={clsx(styles().tree.valueKey, styles().tree.collapsible)}>
173+
&quot;{keyName}&quot;:{' '}
174+
</span>}
175+
<span class={styles().tree.valueBraces}>{"{}"}</span>
176+
</span>)
177+
}
235178
return (
236179
<span class={styles().tree.expanderContainer}>
237-
{keyName && <Expander expanded={expanded()} />}
180+
{keyName && <Expander onClick={() => setExpanded(!expanded())} expanded={expanded()} />}
238181
{keyName && (
239182
<span
240183
onClick={(e) => {
@@ -259,6 +202,8 @@ const ObjectValue = ({
259202
keyName={k}
260203
isLastKey={lastKeyName === k}
261204
copyable={copyable}
205+
defaultExpansionDepth={defaultExpansionDepth}
206+
depth={depth + 1}
262207
/>
263208
</>
264209
)}
@@ -281,3 +226,94 @@ const ObjectValue = ({
281226
</span>
282227
)
283228
}
229+
230+
type CopyState = 'NoCopy' | 'SuccessCopy' | 'ErrorCopy'
231+
232+
const CopyButton = (props: { value: unknown }) => {
233+
const styles = useStyles()
234+
const [copyState, setCopyState] = createSignal<CopyState>('NoCopy')
235+
236+
return (
237+
<button
238+
class={styles().tree.actionButton}
239+
title="Copy object to clipboard"
240+
aria-label={`${copyState() === 'NoCopy'
241+
? 'Copy object to clipboard'
242+
: copyState() === 'SuccessCopy'
243+
? 'Object copied to clipboard'
244+
: 'Error copying object to clipboard'
245+
}`}
246+
onClick={
247+
copyState() === 'NoCopy'
248+
? () => {
249+
navigator.clipboard
250+
.writeText(JSON.stringify(props.value, null, 2))
251+
.then(
252+
() => {
253+
setCopyState('SuccessCopy')
254+
setTimeout(() => {
255+
setCopyState('NoCopy')
256+
}, 1500)
257+
},
258+
(err) => {
259+
console.error('Failed to copy: ', err)
260+
setCopyState('ErrorCopy')
261+
setTimeout(() => {
262+
setCopyState('NoCopy')
263+
}, 1500)
264+
},
265+
)
266+
}
267+
: undefined
268+
}
269+
>
270+
<Switch>
271+
<Match when={copyState() === 'NoCopy'}>
272+
<Copier />
273+
</Match>
274+
<Match when={copyState() === 'SuccessCopy'}>
275+
<CopiedCopier theme={'dark'} />
276+
</Match>
277+
<Match when={copyState() === 'ErrorCopy'}>
278+
<ErrorCopier />
279+
</Match>
280+
</Switch>
281+
</button>
282+
)
283+
}
284+
285+
const Expander = (props: { expanded: boolean, onClick: () => void }) => {
286+
const styles = useStyles()
287+
return (
288+
<span
289+
onClick={props.onClick}
290+
class={clsx(
291+
styles().tree.expander,
292+
css`
293+
transform: rotate(${props.expanded ? 90 : 0}deg);
294+
`,
295+
props.expanded &&
296+
css`
297+
& svg {
298+
top: -1px;
299+
}
300+
`,
301+
)}
302+
>
303+
<svg
304+
width="16"
305+
height="16"
306+
viewBox="0 0 16 16"
307+
fill="none"
308+
xmlns="http://www.w3.org/2000/svg"
309+
>
310+
<path
311+
d="M6 12L10 8L6 4"
312+
stroke-width="2"
313+
stroke-linecap="round"
314+
stroke-linejoin="round"
315+
/>
316+
</svg>
317+
</span>
318+
)
319+
}

‎packages/devtools-ui/src/styles/use-styles.ts‎

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -317,9 +317,9 @@ const stylesFactory = (theme: Theme = 'dark') => {
317317
height: ${tokens.size[1.5]};
318318
border-radius: ${tokens.border.radius.full};
319319
background-color: ${t(
320-
tokens.colors[color][500],
321-
tokens.colors[color][500],
322-
)};
320+
tokens.colors[color][500],
321+
tokens.colors[color][500],
322+
)};
323323
`,
324324
base: css`
325325
display: flex;
@@ -393,6 +393,7 @@ const stylesFactory = (theme: Theme = 'dark') => {
393393
`,
394394
expander: css`
395395
position: absolute;
396+
cursor: pointer;
396397
left: -16px;
397398
top: 3px;
398399
& path {
@@ -538,9 +539,9 @@ const stylesFactory = (theme: Theme = 'dark') => {
538539
border: 1px solid ${t(colors.gray[300], colors.gray[700])};
539540
border-radius: 0.75rem;
540541
box-shadow: ${t(
541-
'0 1px 3px rgba(0,0,0,0.06)',
542-
'0 1px 3px rgba(0,0,0,0.18)',
543-
)};
542+
'0 1px 3px rgba(0,0,0,0.06)',
543+
'0 1px 3px rgba(0,0,0,0.18)',
544+
)};
544545
`,
545546
title: css`
546547
font-size: 1.125rem;

0 commit comments

Comments
 (0)