Skip to content

Commit c463e10

Browse files
feat: add expansion depth to json tree (#132)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent bb671a7 commit c463e10

File tree

4 files changed

+190
-97
lines changed

4 files changed

+190
-97
lines changed

.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

.changeset/tricky-cloths-joke.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@tanstack/devtools': patch
3+
'@tanstack/devtools-vite': patch
4+
---
5+
6+
improve open-source by using location origin

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

Lines changed: 178 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -3,97 +3,19 @@ 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} />
8-
}
9-
type CopyState = 'NoCopy' | 'SuccessCopy' | 'ErrorCopy'
10-
11-
const CopyButton = (props: { value: unknown }) => {
12-
const styles = useStyles()
13-
const [copyState, setCopyState] = createSignal<CopyState>('NoCopy')
14-
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()
6+
export function JsonTree(props: {
7+
value: any
8+
copyable?: boolean
9+
defaultExpansionDepth?: number
10+
}) {
6711
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>
12+
<JsonValue
13+
isRoot
14+
value={props.value}
15+
copyable={props.copyable}
16+
depth={0}
17+
defaultExpansionDepth={props.defaultExpansionDepth ?? 1}
18+
/>
9719
)
9820
}
9921

@@ -103,8 +25,18 @@ function JsonValue(props: {
10325
isRoot?: boolean
10426
isLastKey?: boolean
10527
copyable?: boolean
28+
defaultExpansionDepth: number
29+
depth: number
10630
}) {
107-
const { value, keyName, isRoot = false, isLastKey, copyable } = props
31+
const {
32+
value,
33+
keyName,
34+
isRoot = false,
35+
isLastKey,
36+
copyable,
37+
defaultExpansionDepth,
38+
depth,
39+
} = props
10840
const styles = useStyles()
10941

11042
return (
@@ -137,12 +69,24 @@ function JsonValue(props: {
13769
}
13870
if (Array.isArray(value)) {
13971
return (
140-
<ArrayValue copyable={copyable} keyName={keyName} value={value} />
72+
<ArrayValue
73+
defaultExpansionDepth={defaultExpansionDepth}
74+
depth={depth}
75+
copyable={copyable}
76+
keyName={keyName}
77+
value={value}
78+
/>
14179
)
14280
}
14381
if (typeof value === 'object') {
14482
return (
145-
<ObjectValue copyable={copyable} keyName={keyName} value={value} />
83+
<ObjectValue
84+
defaultExpansionDepth={defaultExpansionDepth}
85+
depth={depth}
86+
copyable={copyable}
87+
keyName={keyName}
88+
value={value}
89+
/>
14690
)
14791
}
14892
return <span />
@@ -161,16 +105,36 @@ const ArrayValue = ({
161105
value,
162106
keyName,
163107
copyable,
108+
defaultExpansionDepth,
109+
depth,
164110
}: {
165111
value: Array<any>
166112
copyable?: boolean
167113
keyName?: string
114+
defaultExpansionDepth: number
115+
depth: number
168116
}) => {
169117
const styles = useStyles()
170-
const [expanded, setExpanded] = createSignal(true)
118+
const [expanded, setExpanded] = createSignal(depth <= defaultExpansionDepth)
119+
120+
if (value.length === 0) {
121+
return (
122+
<span class={styles().tree.expanderContainer}>
123+
{keyName && (
124+
<span class={clsx(styles().tree.valueKey, styles().tree.collapsible)}>
125+
&quot;{keyName}&quot;:{' '}
126+
</span>
127+
)}
128+
<span class={styles().tree.valueBraces}>[]</span>
129+
</span>
130+
)
131+
}
171132
return (
172133
<span class={styles().tree.expanderContainer}>
173-
<Expander expanded={expanded()} />
134+
<Expander
135+
onClick={() => setExpanded(!expanded())}
136+
expanded={expanded()}
137+
/>
174138
{keyName && (
175139
<span
176140
onclick={(e) => {
@@ -195,6 +159,8 @@ const ArrayValue = ({
195159
copyable={copyable}
196160
value={item}
197161
isLastKey={isLastKey}
162+
defaultExpansionDepth={defaultExpansionDepth}
163+
depth={depth + 1}
198164
/>
199165
)
200166
}}
@@ -222,19 +188,40 @@ const ObjectValue = ({
222188
value,
223189
keyName,
224190
copyable,
191+
defaultExpansionDepth,
192+
depth,
225193
}: {
226194
value: Record<string, any>
227195
keyName?: string
228196
copyable?: boolean
197+
defaultExpansionDepth: number
198+
depth: number
229199
}) => {
230200
const styles = useStyles()
231-
const [expanded, setExpanded] = createSignal(true)
201+
const [expanded, setExpanded] = createSignal(depth <= defaultExpansionDepth)
232202
const keys = Object.keys(value)
233203
const lastKeyName = keys[keys.length - 1]
234204

205+
if (keys.length === 0) {
206+
return (
207+
<span class={styles().tree.expanderContainer}>
208+
{keyName && (
209+
<span class={clsx(styles().tree.valueKey, styles().tree.collapsible)}>
210+
&quot;{keyName}&quot;:{' '}
211+
</span>
212+
)}
213+
<span class={styles().tree.valueBraces}>{'{}'}</span>
214+
</span>
215+
)
216+
}
235217
return (
236218
<span class={styles().tree.expanderContainer}>
237-
{keyName && <Expander expanded={expanded()} />}
219+
{keyName && (
220+
<Expander
221+
onClick={() => setExpanded(!expanded())}
222+
expanded={expanded()}
223+
/>
224+
)}
238225
{keyName && (
239226
<span
240227
onClick={(e) => {
@@ -259,6 +246,8 @@ const ObjectValue = ({
259246
keyName={k}
260247
isLastKey={lastKeyName === k}
261248
copyable={copyable}
249+
defaultExpansionDepth={defaultExpansionDepth}
250+
depth={depth + 1}
262251
/>
263252
</>
264253
)}
@@ -281,3 +270,95 @@ const ObjectValue = ({
281270
</span>
282271
)
283272
}
273+
274+
type CopyState = 'NoCopy' | 'SuccessCopy' | 'ErrorCopy'
275+
276+
const CopyButton = (props: { value: unknown }) => {
277+
const styles = useStyles()
278+
const [copyState, setCopyState] = createSignal<CopyState>('NoCopy')
279+
280+
return (
281+
<button
282+
class={styles().tree.actionButton}
283+
title="Copy object to clipboard"
284+
aria-label={`${
285+
copyState() === 'NoCopy'
286+
? 'Copy object to clipboard'
287+
: copyState() === 'SuccessCopy'
288+
? 'Object copied to clipboard'
289+
: 'Error copying object to clipboard'
290+
}`}
291+
onClick={
292+
copyState() === 'NoCopy'
293+
? () => {
294+
navigator.clipboard
295+
.writeText(JSON.stringify(props.value, null, 2))
296+
.then(
297+
() => {
298+
setCopyState('SuccessCopy')
299+
setTimeout(() => {
300+
setCopyState('NoCopy')
301+
}, 1500)
302+
},
303+
(err) => {
304+
console.error('Failed to copy: ', err)
305+
setCopyState('ErrorCopy')
306+
setTimeout(() => {
307+
setCopyState('NoCopy')
308+
}, 1500)
309+
},
310+
)
311+
}
312+
: undefined
313+
}
314+
>
315+
<Switch>
316+
<Match when={copyState() === 'NoCopy'}>
317+
<Copier />
318+
</Match>
319+
<Match when={copyState() === 'SuccessCopy'}>
320+
<CopiedCopier theme={'dark'} />
321+
</Match>
322+
<Match when={copyState() === 'ErrorCopy'}>
323+
<ErrorCopier />
324+
</Match>
325+
</Switch>
326+
</button>
327+
)
328+
}
329+
330+
const Expander = (props: { expanded: boolean; onClick: () => void }) => {
331+
const styles = useStyles()
332+
return (
333+
<span
334+
onClick={props.onClick}
335+
class={clsx(
336+
styles().tree.expander,
337+
css`
338+
transform: rotate(${props.expanded ? 90 : 0}deg);
339+
`,
340+
props.expanded &&
341+
css`
342+
& svg {
343+
top: -1px;
344+
}
345+
`,
346+
)}
347+
>
348+
<svg
349+
width="16"
350+
height="16"
351+
viewBox="0 0 16 16"
352+
fill="none"
353+
xmlns="http://www.w3.org/2000/svg"
354+
>
355+
<path
356+
d="M6 12L10 8L6 4"
357+
stroke-width="2"
358+
stroke-linecap="round"
359+
stroke-linejoin="round"
360+
/>
361+
</svg>
362+
</span>
363+
)
364+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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 {

0 commit comments

Comments
 (0)