@tanstack/query-devtools 5.0.0-alpha.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/LICENSE +21 -0
  2. package/build/.tsbuildinfo +1 -0
  3. package/build/cjs/index.js +7966 -0
  4. package/build/cjs/index.js.map +1 -0
  5. package/build/esm/index.js +7964 -0
  6. package/build/esm/index.js.map +1 -0
  7. package/build/source/Context.js +10 -0
  8. package/build/source/Devtools.jsx +1485 -0
  9. package/build/source/Explorer.jsx +266 -0
  10. package/build/source/__tests__/devtools.test.jsx +6 -0
  11. package/build/source/fonts.js +7 -0
  12. package/build/source/icons/index.jsx +344 -0
  13. package/build/source/index.jsx +64 -0
  14. package/build/source/theme.js +304 -0
  15. package/build/source/utils.jsx +74 -0
  16. package/build/types/Context.d.ts +29 -0
  17. package/build/types/Context.d.ts.map +1 -0
  18. package/build/types/Devtools.d.ts +23 -0
  19. package/build/types/Devtools.d.ts.map +1 -0
  20. package/build/types/Explorer.d.ts +22 -0
  21. package/build/types/Explorer.d.ts.map +1 -0
  22. package/build/types/__tests__/devtools.test.d.ts +1 -0
  23. package/build/types/__tests__/devtools.test.d.ts.map +1 -0
  24. package/build/types/fonts.d.ts +2 -0
  25. package/build/types/fonts.d.ts.map +1 -0
  26. package/build/types/icons/index.d.ts +12 -0
  27. package/build/types/icons/index.d.ts.map +1 -0
  28. package/build/types/index.d.ts +27 -0
  29. package/build/types/index.d.ts.map +1 -0
  30. package/build/types/theme.d.ts +297 -0
  31. package/build/types/theme.d.ts.map +1 -0
  32. package/build/types/utils.d.ts +22 -0
  33. package/build/types/utils.d.ts.map +1 -0
  34. package/build/umd/index.js +872 -0
  35. package/build/umd/index.js.map +1 -0
  36. package/package.json +53 -0
  37. package/src/Context.ts +41 -0
  38. package/src/Devtools.tsx +1890 -0
  39. package/src/Explorer.tsx +348 -0
  40. package/src/__tests__/devtools.test.tsx +5 -0
  41. package/src/fonts.ts +7 -0
  42. package/src/icons/index.tsx +1065 -0
  43. package/src/index.tsx +113 -0
  44. package/src/theme.ts +321 -0
  45. package/src/utils.tsx +103 -0
@@ -0,0 +1,1890 @@
1
+ import type { Accessor, Component, JSX, Setter } from 'solid-js'
2
+ import { For } from 'solid-js'
3
+ import {
4
+ createEffect,
5
+ createMemo,
6
+ createSignal,
7
+ on,
8
+ onCleanup,
9
+ onMount,
10
+ Show,
11
+ } from 'solid-js'
12
+ import { rankItem } from '@tanstack/match-sorter-utils'
13
+ import { css, cx } from '@emotion/css'
14
+ import { tokens } from './theme'
15
+ import type { Query, QueryCache, QueryState } from '@tanstack/query-core'
16
+ import {
17
+ getQueryStatusLabel,
18
+ getQueryStatusColor,
19
+ displayValue,
20
+ getQueryStatusColorByLabel,
21
+ sortFns,
22
+ convertRemToPixels,
23
+ } from './utils'
24
+ import {
25
+ ArrowDown,
26
+ ArrowUp,
27
+ ChevronDown,
28
+ Offline,
29
+ Search,
30
+ Settings,
31
+ TanstackLogo,
32
+ Wifi,
33
+ } from './icons'
34
+ import Explorer from './Explorer'
35
+ import type {
36
+ QueryDevtoolsProps,
37
+ DevtoolsPosition,
38
+ DevtoolsButtonPosition,
39
+ DevToolsErrorType,
40
+ } from './Context'
41
+ import { QueryDevtoolsContext, useQueryDevtoolsContext } from './Context'
42
+ import { TransitionGroup } from 'solid-transition-group'
43
+ import { loadFonts } from './fonts'
44
+ import { Key } from '@solid-primitives/keyed'
45
+ import type { StorageObject, StorageSetter } from '@solid-primitives/storage'
46
+ import { createLocalStorage } from '@solid-primitives/storage'
47
+ import { createResizeObserver } from '@solid-primitives/resize-observer'
48
+
49
+ interface DevtoolsPanelProps {
50
+ localStore: StorageObject<string>
51
+ setLocalStore: StorageSetter<string, unknown>
52
+ }
53
+
54
+ interface QueryStatusProps {
55
+ label: string
56
+ color: 'green' | 'yellow' | 'gray' | 'blue' | 'purple'
57
+ count: number
58
+ }
59
+
60
+ const firstBreakpoint = 1024
61
+ const secondBreakpoint = 796
62
+ const thirdBreakpoint = 700
63
+
64
+ const BUTTON_POSITION: DevtoolsButtonPosition = 'bottom-right'
65
+ const POSITION: DevtoolsPosition = 'bottom'
66
+ const INITIAL_IS_OPEN = false
67
+ const DEFAULT_HEIGHT = 500
68
+ const DEFAULT_WIDTH = 500
69
+ const DEFAULT_SORT_FN_NAME = Object.keys(sortFns)[0]
70
+ const DEFAULT_SORT_ORDER = 1
71
+
72
+ const [selectedQueryHash, setSelectedQueryHash] = createSignal<string | null>(
73
+ null,
74
+ )
75
+ const [panelWidth, setPanelWidth] = createSignal(0)
76
+
77
+ export const DevtoolsComponent: Component<QueryDevtoolsProps> = (props) => {
78
+ return (
79
+ <QueryDevtoolsContext.Provider value={props}>
80
+ <Devtools />
81
+ </QueryDevtoolsContext.Provider>
82
+ )
83
+ }
84
+
85
+ export const Devtools = () => {
86
+ loadFonts()
87
+
88
+ const styles = getStyles()
89
+
90
+ const [localStore, setLocalStore] = createLocalStorage({
91
+ prefix: 'TanstackQueryDevtools',
92
+ })
93
+
94
+ const buttonPosition = createMemo(() => {
95
+ return useQueryDevtoolsContext().buttonPosition || BUTTON_POSITION
96
+ })
97
+
98
+ const isOpen = createMemo(() => {
99
+ return localStore.open === 'true'
100
+ ? true
101
+ : localStore.open === 'false'
102
+ ? false
103
+ : useQueryDevtoolsContext().initialIsOpen || INITIAL_IS_OPEN
104
+ })
105
+
106
+ const position = createMemo(() => {
107
+ return localStore.position || useQueryDevtoolsContext().position || POSITION
108
+ })
109
+
110
+ return (
111
+ <div
112
+ // styles for animating the panel in and out
113
+ class={css`
114
+ & .TSQD-panel-exit-active,
115
+ & .TSQD-panel-enter-active {
116
+ transition: opacity 0.3s, transform 0.3s;
117
+ }
118
+
119
+ & .TSQD-panel-exit-to,
120
+ & .TSQD-panel-enter {
121
+ ${position() === 'top'
122
+ ? `transform: translateY(-${Number(
123
+ localStore.height || DEFAULT_HEIGHT,
124
+ )}px);`
125
+ : position() === 'left'
126
+ ? `transform: translateX(-${Number(
127
+ localStore.width || DEFAULT_WIDTH,
128
+ )}px);`
129
+ : position() === 'right'
130
+ ? `transform: translateX(${Number(
131
+ localStore.width || DEFAULT_WIDTH,
132
+ )}px);`
133
+ : `transform: translateY(${Number(
134
+ localStore.height || DEFAULT_HEIGHT,
135
+ )}px);`}
136
+ }
137
+
138
+ & .TSQD-button-exit-active,
139
+ & .TSQD-button-enter-active {
140
+ transition: opacity 0.3s, transform 0.3s;
141
+ }
142
+
143
+ & .TSQD-button-exit-to,
144
+ & .TSQD-button-enter {
145
+ transform: ${buttonPosition() === 'top-left'
146
+ ? `translateX(-72px);`
147
+ : buttonPosition() === 'top-right'
148
+ ? `translateX(72px);`
149
+ : `translateY(72px);`};
150
+ }
151
+ `}
152
+ >
153
+ <TransitionGroup name="TSQD-panel">
154
+ <Show when={isOpen()}>
155
+ <DevtoolsPanel
156
+ localStore={localStore}
157
+ setLocalStore={setLocalStore}
158
+ />
159
+ </Show>
160
+ </TransitionGroup>
161
+ <TransitionGroup name="TSQD-button">
162
+ <Show when={!isOpen()}>
163
+ <div
164
+ class={cx(
165
+ styles.devtoolsBtn,
166
+ styles[`devtoolsBtn-position-${buttonPosition()}`],
167
+ )}
168
+ >
169
+ <div aria-hidden="true">
170
+ <TanstackLogo />
171
+ </div>
172
+ <button
173
+ aria-label="Open Tanstack query devtools"
174
+ onClick={() => setLocalStore('open', 'true')}
175
+ >
176
+ <TanstackLogo />
177
+ </button>
178
+ </div>
179
+ </Show>
180
+ </TransitionGroup>
181
+ </div>
182
+ )
183
+ }
184
+
185
+ export const DevtoolsPanel: Component<DevtoolsPanelProps> = (props) => {
186
+ const styles = getStyles()
187
+ const [isResizing, setIsResizing] = createSignal(false)
188
+
189
+ const sort = createMemo(() => props.localStore.sort || DEFAULT_SORT_FN_NAME)
190
+ const sortOrder = createMemo(
191
+ () => Number(props.localStore.sortOrder) || DEFAULT_SORT_ORDER,
192
+ ) as () => 1 | -1
193
+
194
+ const [offline, setOffline] = createSignal(false)
195
+ const [settingsOpen, setSettingsOpen] = createSignal(false)
196
+
197
+ const position = createMemo(
198
+ () =>
199
+ (props.localStore.position ||
200
+ useQueryDevtoolsContext().position ||
201
+ POSITION) as DevtoolsPosition,
202
+ )
203
+
204
+ const sortFn = createMemo(() => sortFns[sort() as string])
205
+
206
+ const onlineManager = createMemo(
207
+ () => useQueryDevtoolsContext().onlineManager,
208
+ )
209
+
210
+ const cache = createMemo(() => {
211
+ return useQueryDevtoolsContext().client.getQueryCache()
212
+ })
213
+
214
+ const queryCount = createSubscribeToQueryCacheBatcher((queryCache) => {
215
+ return queryCache().getAll().length
216
+ }, false)
217
+
218
+ const queries = createMemo(
219
+ on(
220
+ () => [queryCount(), props.localStore.filter, sort(), sortOrder()],
221
+ () => {
222
+ const curr = cache().getAll()
223
+
224
+ const filtered = props.localStore.filter
225
+ ? curr.filter(
226
+ (item) =>
227
+ rankItem(item.queryHash, props.localStore.filter || '').passed,
228
+ )
229
+ : [...curr]
230
+
231
+ const sorted = sortFn()
232
+ ? filtered.sort((a, b) => sortFn()!(a, b) * sortOrder())
233
+ : filtered
234
+ return sorted
235
+ },
236
+ ),
237
+ )
238
+
239
+ const handleDragStart: JSX.EventHandler<HTMLDivElement, MouseEvent> = (
240
+ event,
241
+ ) => {
242
+ const panelElement = event.currentTarget.parentElement
243
+ if (!panelElement) return
244
+ setIsResizing(true)
245
+ const { height, width } = panelElement.getBoundingClientRect()
246
+ const startX = event.clientX
247
+ const startY = event.clientY
248
+ let newSize = 0
249
+ const minHeight = convertRemToPixels(3.5)
250
+ const minWidth = convertRemToPixels(12)
251
+ const runDrag = (moveEvent: MouseEvent) => {
252
+ moveEvent.preventDefault()
253
+
254
+ if (position() === 'left' || position() === 'right') {
255
+ const valToAdd =
256
+ position() === 'right'
257
+ ? startX - moveEvent.clientX
258
+ : moveEvent.clientX - startX
259
+ newSize = Math.round(width + valToAdd)
260
+ if (newSize < minWidth) {
261
+ newSize = minWidth
262
+ }
263
+ props.setLocalStore('width', String(Math.round(newSize)))
264
+
265
+ const newWidth = panelElement.getBoundingClientRect().width
266
+ // If the panel size didn't decrease, this means we have reached the minimum width
267
+ // of the panel so we restore the original width in local storage
268
+ // Restoring the width helps in smooth open/close transitions
269
+ if (Number(props.localStore.width) < newWidth) {
270
+ props.setLocalStore('width', String(newWidth))
271
+ }
272
+ } else {
273
+ const valToAdd =
274
+ position() === 'bottom'
275
+ ? startY - moveEvent.clientY
276
+ : moveEvent.clientY - startY
277
+ newSize = Math.round(height + valToAdd)
278
+ // If the panel size is less than the minimum height,
279
+ // we set the size to the minimum height
280
+ if (newSize < minHeight) {
281
+ newSize = minHeight
282
+ setSelectedQueryHash(null)
283
+ }
284
+ props.setLocalStore('height', String(Math.round(newSize)))
285
+ }
286
+ }
287
+
288
+ const unsub = () => {
289
+ if (isResizing()) {
290
+ setIsResizing(false)
291
+ }
292
+ document.removeEventListener('mousemove', runDrag, false)
293
+ document.removeEventListener('mouseUp', unsub, false)
294
+ }
295
+
296
+ document.addEventListener('mousemove', runDrag, false)
297
+ document.addEventListener('mouseup', unsub, false)
298
+ }
299
+
300
+ setupQueryCacheSubscription()
301
+
302
+ let queriesContainerRef!: HTMLDivElement
303
+ let panelRef!: HTMLDivElement
304
+
305
+ onMount(() => {
306
+ createResizeObserver(panelRef, ({ width }, el) => {
307
+ if (el === panelRef) {
308
+ setPanelWidth(width)
309
+ }
310
+ })
311
+ })
312
+
313
+ const setDevtoolsPosition = (pos: DevtoolsPosition) => {
314
+ props.setLocalStore('position', pos)
315
+ setSettingsOpen(false)
316
+ }
317
+
318
+ return (
319
+ <aside
320
+ // Some context for styles here
321
+ // background-color - Changes to a lighter color create a harder contrast
322
+ // between the queries and query detail panel
323
+ // -
324
+ // min-width - When the panel is in the left or right position, the panel
325
+ // width is set to min-content to allow the panel to shrink to the lowest possible width
326
+ class={`${styles.panel} ${styles[`panel-position-${position()}`]} ${css`
327
+ flex-direction: ${panelWidth() < secondBreakpoint ? 'column' : 'row'};
328
+ background-color: ${panelWidth() < secondBreakpoint
329
+ ? tokens.colors.gray[600]
330
+ : tokens.colors.darkGray[900]};
331
+ ${panelWidth() < thirdBreakpoint &&
332
+ (position() === 'right' || position() === 'left')
333
+ ? `
334
+ min-width: min-content;
335
+ `
336
+ : ''}
337
+ `}`}
338
+ style={{
339
+ height:
340
+ position() === 'bottom' || position() === 'top'
341
+ ? `${props.localStore.height || DEFAULT_HEIGHT}px`
342
+ : 'auto',
343
+ width:
344
+ position() === 'right' || position() === 'left'
345
+ ? `${props.localStore.width || DEFAULT_WIDTH}px`
346
+ : 'auto',
347
+ }}
348
+ ref={panelRef}
349
+ aria-label="Tanstack query devtools"
350
+ >
351
+ <div
352
+ class={cx(
353
+ styles.dragHandle,
354
+ styles[`dragHandle-position-${position()}`],
355
+ )}
356
+ onMouseDown={handleDragStart}
357
+ ></div>
358
+ <button
359
+ aria-label="Close tanstack query devtools"
360
+ class={cx(styles.closeBtn, styles[`closeBtn-position-${position()}`])}
361
+ onClick={() => props.setLocalStore('open', 'false')}
362
+ >
363
+ <ChevronDown />
364
+ </button>
365
+ <div
366
+ ref={queriesContainerRef}
367
+ // When the panels are stacked we use the height style
368
+ // to divide the panels into two equal parts
369
+ class={`${styles.queriesContainer} ${css`
370
+ ${panelWidth() < secondBreakpoint && selectedQueryHash()
371
+ ? `
372
+ height: 50%;
373
+ max-height: 50%;
374
+ `
375
+ : ''}
376
+ `}`}
377
+ >
378
+ <div class={cx(styles.row)}>
379
+ <button
380
+ class={styles.logo}
381
+ onClick={() => props.setLocalStore('open', 'false')}
382
+ aria-label="Close Tanstack query devtools"
383
+ >
384
+ <span class={styles.tanstackLogo}>TANSTACK</span>
385
+ <span class={styles.queryFlavorLogo}>
386
+ {useQueryDevtoolsContext().queryFlavor} v
387
+ {useQueryDevtoolsContext().version}
388
+ </span>
389
+ </button>
390
+ <QueryStatusCount />
391
+ </div>
392
+ <div
393
+ class={cx(
394
+ styles.row,
395
+ css`
396
+ gap: ${tokens.size[2.5]};
397
+ `,
398
+ )}
399
+ >
400
+ <div class={styles.filtersContainer}>
401
+ <div class={styles.filterInput}>
402
+ <Search />
403
+ <input
404
+ aria-label="Filter queries by query key"
405
+ type="text"
406
+ placeholder="Filter"
407
+ onInput={(e) =>
408
+ props.setLocalStore('filter', e.currentTarget.value)
409
+ }
410
+ value={props.localStore.filter || ''}
411
+ />
412
+ </div>
413
+ <div class={styles.filterSelect}>
414
+ <select
415
+ value={sort()}
416
+ onChange={(e) =>
417
+ props.setLocalStore('sort', e.currentTarget.value)
418
+ }
419
+ >
420
+ {Object.keys(sortFns).map((key) => (
421
+ <option value={key}>Sort by {key}</option>
422
+ ))}
423
+ </select>
424
+ <ChevronDown />
425
+ </div>
426
+ <button
427
+ onClick={() => {
428
+ props.setLocalStore('sortOrder', String(sortOrder() * -1))
429
+ }}
430
+ aria-label={`Sort order ${
431
+ sortOrder() === -1 ? 'descending' : 'ascending'
432
+ }`}
433
+ aria-pressed={sortOrder() === -1}
434
+ >
435
+ <Show when={sortOrder() === 1}>
436
+ <span>Asc</span>
437
+ <ArrowUp />
438
+ </Show>
439
+ <Show when={sortOrder() === -1}>
440
+ <span>Desc</span>
441
+ <ArrowDown />
442
+ </Show>
443
+ </button>
444
+ </div>
445
+
446
+ <div class={styles.actionsContainer}>
447
+ <button
448
+ onClick={() => {
449
+ if (offline()) {
450
+ onlineManager().setOnline(undefined)
451
+ setOffline(false)
452
+ window.dispatchEvent(new Event('online'))
453
+ } else {
454
+ onlineManager().setOnline(false)
455
+ setOffline(true)
456
+ }
457
+ }}
458
+ class={styles.actionsBtn}
459
+ aria-label={`${
460
+ offline()
461
+ ? 'Unset offline mocking behavior'
462
+ : 'Mock offline behavior'
463
+ }`}
464
+ aria-pressed={offline()}
465
+ >
466
+ {offline() ? <Offline /> : <Wifi />}
467
+ </button>
468
+ <div style={{ position: 'relative' }}>
469
+ <button
470
+ onClick={() => setSettingsOpen((prev) => !prev)}
471
+ class={styles.actionsBtn}
472
+ id="TSQD-settings-menu-btn"
473
+ aria-label={`${
474
+ settingsOpen() ? 'Close' : 'Open'
475
+ } settings menu`}
476
+ aria-haspopup="true"
477
+ aria-controls="TSQD-settings-menu"
478
+ >
479
+ <Settings />
480
+ </button>
481
+ <Show when={settingsOpen()}>
482
+ <div
483
+ role="menu"
484
+ tabindex="-1"
485
+ aria-labelledby="TSQD-settings-menu-btn"
486
+ id="TSQD-settings-menu"
487
+ class={styles.settingsMenu}
488
+ >
489
+ <div class={styles.settingsMenuHeader}>Position</div>
490
+ <div class={styles.settingsMenuSection}>
491
+ <button
492
+ onClick={() => {
493
+ setDevtoolsPosition('top')
494
+ }}
495
+ aria-label="Position top"
496
+ >
497
+ <ArrowUp />
498
+ <span>Top</span>
499
+ </button>
500
+ <button
501
+ onClick={() => {
502
+ setDevtoolsPosition('bottom')
503
+ }}
504
+ aria-label="Position bottom"
505
+ >
506
+ <ArrowDown />
507
+ <span>Bottom</span>
508
+ </button>
509
+ <button
510
+ onClick={() => {
511
+ setDevtoolsPosition('left')
512
+ }}
513
+ aria-label="Position left"
514
+ >
515
+ <ArrowDown />
516
+ <span>Left</span>
517
+ </button>
518
+ <button
519
+ onClick={() => {
520
+ setDevtoolsPosition('right')
521
+ }}
522
+ aria-label="Position right"
523
+ >
524
+ <ArrowDown />
525
+ <span>Right</span>
526
+ </button>
527
+ </div>
528
+ </div>
529
+ </Show>
530
+ </div>
531
+ </div>
532
+ </div>
533
+ <div class={styles.overflowQueryContainer}>
534
+ <div>
535
+ <Key by={(q) => q.queryHash} each={queries()}>
536
+ {(query) => <QueryRow query={query()} />}
537
+ </Key>
538
+ </div>
539
+ </div>
540
+ </div>
541
+ <Show when={selectedQueryHash()}>
542
+ <QueryDetails />
543
+ </Show>
544
+ </aside>
545
+ )
546
+ }
547
+
548
+ export const QueryRow: Component<{ query: Query }> = (props) => {
549
+ const styles = getStyles()
550
+
551
+ const queryState = createSubscribeToQueryCacheBatcher(
552
+ (queryCache) =>
553
+ queryCache().find({
554
+ queryKey: props.query.queryKey,
555
+ })?.state,
556
+ )
557
+
558
+ const isDisabled = createSubscribeToQueryCacheBatcher(
559
+ (queryCache) =>
560
+ queryCache()
561
+ .find({
562
+ queryKey: props.query.queryKey,
563
+ })
564
+ ?.isDisabled() ?? false,
565
+ )
566
+
567
+ const isStale = createSubscribeToQueryCacheBatcher(
568
+ (queryCache) =>
569
+ queryCache()
570
+ .find({
571
+ queryKey: props.query.queryKey,
572
+ })
573
+ ?.isStale() ?? false,
574
+ )
575
+
576
+ const observers = createSubscribeToQueryCacheBatcher(
577
+ (queryCache) =>
578
+ queryCache()
579
+ .find({
580
+ queryKey: props.query.queryKey,
581
+ })
582
+ ?.getObserversCount() ?? 0,
583
+ )
584
+
585
+ const color = createMemo(() =>
586
+ getQueryStatusColor({
587
+ queryState: queryState()!,
588
+ observerCount: observers(),
589
+ isStale: isStale(),
590
+ }),
591
+ )
592
+
593
+ return (
594
+ <Show when={queryState()}>
595
+ <button
596
+ onClick={() =>
597
+ setSelectedQueryHash(
598
+ props.query.queryHash === selectedQueryHash()
599
+ ? null
600
+ : props.query.queryHash,
601
+ )
602
+ }
603
+ class={cx(
604
+ styles.queryRow,
605
+ selectedQueryHash() === props.query.queryHash &&
606
+ styles.selectedQueryRow,
607
+ )}
608
+ aria-label={`Query key ${props.query.queryHash}`}
609
+ >
610
+ <div
611
+ class={cx(
612
+ 'TSQDObserverCount',
613
+ color() === 'gray'
614
+ ? css`
615
+ background-color: ${tokens.colors[color()][700]};
616
+ color: ${tokens.colors[color()][300]};
617
+ `
618
+ : css`
619
+ background-color: ${tokens.colors[color()][900]};
620
+ color: ${tokens.colors[color()][300]} !important;
621
+ `,
622
+ )}
623
+ >
624
+ {observers()}
625
+ </div>
626
+ <code class="TSQDQueryHash">{props.query.queryHash}</code>
627
+ <Show when={isDisabled()}>
628
+ <div class="TSQDQueryDisabled">disabled</div>
629
+ </Show>
630
+ </button>
631
+ </Show>
632
+ )
633
+ }
634
+
635
+ export const QueryStatusCount: Component = () => {
636
+ const stale = createSubscribeToQueryCacheBatcher(
637
+ (queryCache) =>
638
+ queryCache()
639
+ .getAll()
640
+ .filter((q) => getQueryStatusLabel(q) === 'stale').length,
641
+ )
642
+
643
+ const fresh = createSubscribeToQueryCacheBatcher(
644
+ (queryCache) =>
645
+ queryCache()
646
+ .getAll()
647
+ .filter((q) => getQueryStatusLabel(q) === 'fresh').length,
648
+ )
649
+
650
+ const fetching = createSubscribeToQueryCacheBatcher(
651
+ (queryCache) =>
652
+ queryCache()
653
+ .getAll()
654
+ .filter((q) => getQueryStatusLabel(q) === 'fetching').length,
655
+ )
656
+
657
+ const paused = createSubscribeToQueryCacheBatcher(
658
+ (queryCache) =>
659
+ queryCache()
660
+ .getAll()
661
+ .filter((q) => getQueryStatusLabel(q) === 'paused').length,
662
+ )
663
+
664
+ const inactive = createSubscribeToQueryCacheBatcher(
665
+ (queryCache) =>
666
+ queryCache()
667
+ .getAll()
668
+ .filter((q) => getQueryStatusLabel(q) === 'inactive').length,
669
+ )
670
+
671
+ const styles = getStyles()
672
+
673
+ return (
674
+ <div class={styles.queryStatusContainer}>
675
+ <QueryStatus label="Fresh" color="green" count={fresh()} />
676
+ <QueryStatus label="Fetching" color="blue" count={fetching()} />
677
+ <QueryStatus label="Paused" color="purple" count={paused()} />
678
+ <QueryStatus label="Stale" color="yellow" count={stale()} />
679
+ <QueryStatus label="Inactive" color="gray" count={inactive()} />
680
+ </div>
681
+ )
682
+ }
683
+
684
+ export const QueryStatus: Component<QueryStatusProps> = (props) => {
685
+ const styles = getStyles()
686
+
687
+ let tagRef!: HTMLButtonElement
688
+
689
+ const [mouseOver, setMouseOver] = createSignal(false)
690
+ const [focused, setFocused] = createSignal(false)
691
+
692
+ const showLabel = createMemo(() => {
693
+ if (selectedQueryHash()) {
694
+ if (panelWidth() < firstBreakpoint && panelWidth() > secondBreakpoint) {
695
+ return false
696
+ }
697
+ }
698
+ if (panelWidth() < thirdBreakpoint) {
699
+ return false
700
+ }
701
+
702
+ return true
703
+ })
704
+
705
+ return (
706
+ <button
707
+ onFocus={() => setFocused(true)}
708
+ onBlur={() => setFocused(false)}
709
+ onMouseEnter={() => setMouseOver(true)}
710
+ onMouseLeave={() => {
711
+ setMouseOver(false)
712
+ setFocused(false)
713
+ }}
714
+ disabled={showLabel()}
715
+ ref={tagRef}
716
+ class={cx(
717
+ styles.queryStatusTag,
718
+ !showLabel()
719
+ ? css`
720
+ cursor: pointer;
721
+ &:hover {
722
+ background: ${tokens.colors.darkGray[400]}${tokens.alpha[80]};
723
+ }
724
+ `
725
+ : null,
726
+ )}
727
+ {...(mouseOver() || focused()
728
+ ? {
729
+ 'aria-describedby': 'TSQD-status-tooltip',
730
+ }
731
+ : {})}
732
+ >
733
+ <Show when={!showLabel() && (mouseOver() || focused())}>
734
+ <div
735
+ role="tooltip"
736
+ id="TSQD-status-tooltip"
737
+ class={cx(styles.statusTooltip)}
738
+ >
739
+ {props.label}
740
+ </div>
741
+ </Show>
742
+ <span
743
+ class={css`
744
+ width: ${tokens.size[2]};
745
+ height: ${tokens.size[2]};
746
+ border-radius: ${tokens.border.radius.full};
747
+ background-color: ${tokens.colors[props.color][500]};
748
+ `}
749
+ />
750
+ <Show when={showLabel()}>
751
+ <span>{props.label}</span>
752
+ </Show>
753
+ <span
754
+ class={cx(
755
+ styles.queryStatusCount,
756
+ props.count > 0 && props.color !== 'gray'
757
+ ? css`
758
+ background-color: ${tokens.colors[props.color][900]};
759
+ color: ${tokens.colors[props.color][300]} !important;
760
+ `
761
+ : css`
762
+ color: ${tokens.colors['gray'][400]} !important;
763
+ `,
764
+ )}
765
+ >
766
+ {props.count}
767
+ </span>
768
+ </button>
769
+ )
770
+ }
771
+
772
+ const QueryDetails = () => {
773
+ const styles = getStyles()
774
+ const queryClient = useQueryDevtoolsContext().client
775
+
776
+ const [restoringLoading, setRestoringLoading] = createSignal(false)
777
+
778
+ const errorTypes = createMemo(() => {
779
+ return useQueryDevtoolsContext().errorTypes || []
780
+ })
781
+
782
+ const activeQuery = createSubscribeToQueryCacheBatcher(
783
+ (queryCache) =>
784
+ queryCache()
785
+ .getAll()
786
+ .find((query) => query.queryHash === selectedQueryHash()),
787
+ false,
788
+ )
789
+
790
+ const activeQueryFresh = createSubscribeToQueryCacheBatcher((queryCache) => {
791
+ return queryCache()
792
+ .getAll()
793
+ .find((query) => query.queryHash === selectedQueryHash())
794
+ }, false)
795
+
796
+ const activeQueryState = createSubscribeToQueryCacheBatcher(
797
+ (queryCache) =>
798
+ queryCache()
799
+ .getAll()
800
+ .find((query) => query.queryHash === selectedQueryHash())?.state,
801
+ false,
802
+ )
803
+
804
+ const activeQueryStateData = createSubscribeToQueryCacheBatcher(
805
+ (queryCache) => {
806
+ return queryCache()
807
+ .getAll()
808
+ .find((query) => query.queryHash === selectedQueryHash())?.state.data
809
+ },
810
+ false,
811
+ )
812
+
813
+ const statusLabel = createSubscribeToQueryCacheBatcher((queryCache) => {
814
+ const query = queryCache()
815
+ .getAll()
816
+ .find((q) => q.queryHash === selectedQueryHash())
817
+ if (!query) return 'inactive'
818
+ return getQueryStatusLabel(query)
819
+ })
820
+
821
+ const queryStatus = createSubscribeToQueryCacheBatcher((queryCache) => {
822
+ const query = queryCache()
823
+ .getAll()
824
+ .find((q) => q.queryHash === selectedQueryHash())
825
+ if (!query) return 'pending'
826
+ return query.state.status
827
+ })
828
+
829
+ const observerCount = createSubscribeToQueryCacheBatcher(
830
+ (queryCache) =>
831
+ queryCache()
832
+ .getAll()
833
+ .find((query) => query.queryHash === selectedQueryHash())
834
+ ?.getObserversCount() ?? 0,
835
+ )
836
+
837
+ const color = createMemo(() => getQueryStatusColorByLabel(statusLabel()))
838
+
839
+ const handleRefetch = () => {
840
+ const promise = activeQuery()?.fetch()
841
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
842
+ promise?.catch(() => {})
843
+ }
844
+
845
+ const triggerError = (errorType?: DevToolsErrorType) => {
846
+ const error =
847
+ errorType?.initializer(activeQuery()!) ??
848
+ new Error('Unknown error from devtools')
849
+
850
+ const __previousQueryOptions = activeQuery()!.options
851
+
852
+ activeQuery()!.setState({
853
+ status: 'error',
854
+ error,
855
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
856
+ fetchMeta: {
857
+ ...activeQuery()!.state.fetchMeta,
858
+ __previousQueryOptions,
859
+ } as any,
860
+ } as QueryState<unknown, Error>)
861
+ }
862
+
863
+ const restoreQueryAfterLoadingOrError = () => {
864
+ activeQuery()?.fetch(
865
+ (activeQuery()?.state.fetchMeta as any).__previousQueryOptions,
866
+ {
867
+ // Make sure this fetch will cancel the previous one
868
+ cancelRefetch: true,
869
+ },
870
+ )
871
+ }
872
+
873
+ createEffect(() => {
874
+ if (statusLabel() !== 'fetching') {
875
+ setRestoringLoading(false)
876
+ }
877
+ })
878
+
879
+ return (
880
+ <Show when={activeQuery() && activeQueryState()}>
881
+ <div class={styles.detailsContainer}>
882
+ <div class={styles.detailsHeader}>Query Details</div>
883
+ <div class={styles.detailsBody}>
884
+ <div>
885
+ <pre>
886
+ <code>{displayValue(activeQuery()!.queryKey, true)}</code>
887
+ </pre>
888
+ <span
889
+ class={cx(
890
+ styles.queryDetailsStatus,
891
+ color() === 'gray'
892
+ ? css`
893
+ background-color: ${tokens.colors[color()][700]};
894
+ color: ${tokens.colors[color()][300]};
895
+ border-color: ${tokens.colors[color()][600]};
896
+ `
897
+ : css`
898
+ background-color: ${tokens.colors[color()][900]};
899
+ color: ${tokens.colors[color()][300]};
900
+ border-color: ${tokens.colors[color()][600]};
901
+ `,
902
+ )}
903
+ >
904
+ {statusLabel()}
905
+ </span>
906
+ </div>
907
+ <div>
908
+ <span>Observers:</span>
909
+ <span>{observerCount()}</span>
910
+ </div>
911
+ <div>
912
+ <span>Last Updated:</span>
913
+ <span>
914
+ {new Date(activeQueryState()!.dataUpdatedAt).toLocaleTimeString()}
915
+ </span>
916
+ </div>
917
+ </div>
918
+ <div class={styles.detailsHeader}>Actions</div>
919
+ <div class={styles.actionsBody}>
920
+ <button
921
+ class={css`
922
+ color: ${tokens.colors.blue[400]};
923
+ `}
924
+ onClick={handleRefetch}
925
+ disabled={statusLabel() === 'fetching'}
926
+ >
927
+ <span
928
+ class={css`
929
+ background-color: ${tokens.colors.blue[400]};
930
+ `}
931
+ ></span>
932
+ Refetch
933
+ </button>
934
+ <button
935
+ class={css`
936
+ color: ${tokens.colors.yellow[400]};
937
+ `}
938
+ onClick={() => queryClient.invalidateQueries(activeQuery())}
939
+ >
940
+ <span
941
+ class={css`
942
+ background-color: ${tokens.colors.yellow[400]};
943
+ `}
944
+ ></span>
945
+ Invalidate
946
+ </button>
947
+ <button
948
+ class={css`
949
+ color: ${tokens.colors.gray[300]};
950
+ `}
951
+ onClick={() => queryClient.resetQueries(activeQuery())}
952
+ >
953
+ <span
954
+ class={css`
955
+ background-color: ${tokens.colors.gray[400]};
956
+ `}
957
+ ></span>
958
+ Reset
959
+ </button>
960
+ <button
961
+ class={css`
962
+ color: ${tokens.colors.cyan[400]};
963
+ `}
964
+ disabled={restoringLoading()}
965
+ onClick={() => {
966
+ if (activeQuery()?.state.data === undefined) {
967
+ setRestoringLoading(true)
968
+ restoreQueryAfterLoadingOrError()
969
+ } else {
970
+ const activeQueryVal = activeQuery()
971
+ if (!activeQueryVal) return
972
+ const __previousQueryOptions = activeQueryVal.options
973
+ // Trigger a fetch in order to trigger suspense as well.
974
+ activeQueryVal.fetch({
975
+ ...__previousQueryOptions,
976
+ queryFn: () => {
977
+ return new Promise(() => {
978
+ // Never resolve
979
+ })
980
+ },
981
+ gcTime: -1,
982
+ })
983
+ activeQueryVal.setState({
984
+ data: undefined,
985
+ status: 'pending',
986
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
987
+ fetchMeta: {
988
+ ...activeQueryVal.state.fetchMeta,
989
+ __previousQueryOptions,
990
+ } as any,
991
+ } as QueryState<unknown, Error>)
992
+ }
993
+ }}
994
+ >
995
+ <span
996
+ class={css`
997
+ background-color: ${tokens.colors.cyan[400]};
998
+ `}
999
+ ></span>
1000
+ {statusLabel() === 'fetching' ? 'Restore' : 'Trigger'} Loading
1001
+ </button>
1002
+ <Show when={errorTypes().length === 0 || queryStatus() === 'error'}>
1003
+ <button
1004
+ class={css`
1005
+ color: ${tokens.colors.red[400]};
1006
+ `}
1007
+ onClick={() => {
1008
+ if (!activeQuery()!.state.error) {
1009
+ triggerError()
1010
+ } else {
1011
+ queryClient.resetQueries(activeQuery())
1012
+ }
1013
+ }}
1014
+ >
1015
+ <span
1016
+ class={css`
1017
+ background-color: ${tokens.colors.red[400]};
1018
+ `}
1019
+ ></span>
1020
+ {queryStatus() === 'error' ? 'Restore' : 'Trigger'} Error
1021
+ </button>
1022
+ </Show>
1023
+ <Show
1024
+ when={!(errorTypes().length === 0 || queryStatus() === 'error')}
1025
+ >
1026
+ <div class={styles.actionsSelect}>
1027
+ <span
1028
+ class={css`
1029
+ background-color: ${tokens.colors.red[400]};
1030
+ `}
1031
+ ></span>
1032
+ Trigger Error
1033
+ <select
1034
+ disabled={queryStatus() === 'pending'}
1035
+ onChange={(e) => {
1036
+ const errorType = errorTypes().find(
1037
+ (t) => t.name === e.currentTarget.value,
1038
+ )
1039
+
1040
+ triggerError(errorType)
1041
+ }}
1042
+ >
1043
+ <option value="" disabled selected></option>
1044
+ <For each={errorTypes()}>
1045
+ {(errorType) => (
1046
+ <option value={errorType.name}>{errorType.name}</option>
1047
+ )}
1048
+ </For>
1049
+ </select>
1050
+ <ChevronDown />
1051
+ </div>
1052
+ </Show>
1053
+ </div>
1054
+ <div class={styles.detailsHeader}>Data Explorer</div>
1055
+ <div
1056
+ style={{
1057
+ padding: '0.5rem',
1058
+ }}
1059
+ >
1060
+ <Explorer
1061
+ label="Data"
1062
+ defaultExpanded={['Data']}
1063
+ value={activeQueryStateData()}
1064
+ copyable={true}
1065
+ />
1066
+ </div>
1067
+ <div class={styles.detailsHeader}>Query Explorer</div>
1068
+ <div
1069
+ style={{
1070
+ padding: '0.5rem',
1071
+ }}
1072
+ >
1073
+ <Explorer
1074
+ label="Query"
1075
+ defaultExpanded={['Query', 'queryKey']}
1076
+ value={activeQueryFresh()}
1077
+ />
1078
+ </div>
1079
+ </div>
1080
+ </Show>
1081
+ )
1082
+ }
1083
+
1084
+ const signalsMap = new Map<(q: Accessor<QueryCache>) => any, Setter<any>>()
1085
+
1086
+ const setupQueryCacheSubscription = () => {
1087
+ const queryCache = createMemo(() => {
1088
+ const client = useQueryDevtoolsContext().client
1089
+ return client.getQueryCache()
1090
+ })
1091
+
1092
+ const unsub = queryCache().subscribe(() => {
1093
+ for (const [callback, setter] of signalsMap.entries()) {
1094
+ queueMicrotask(() => {
1095
+ setter(callback(queryCache))
1096
+ })
1097
+ }
1098
+ })
1099
+
1100
+ onCleanup(() => {
1101
+ signalsMap.clear()
1102
+ unsub()
1103
+ })
1104
+
1105
+ return unsub
1106
+ }
1107
+
1108
+ const createSubscribeToQueryCacheBatcher = <T,>(
1109
+ callback: (queryCache: Accessor<QueryCache>) => Exclude<T, Function>,
1110
+ equalityCheck: boolean = true,
1111
+ ) => {
1112
+ const queryCache = createMemo(() => {
1113
+ const client = useQueryDevtoolsContext().client
1114
+ return client.getQueryCache()
1115
+ })
1116
+
1117
+ const [value, setValue] = createSignal<T>(
1118
+ callback(queryCache),
1119
+ !equalityCheck ? { equals: false } : undefined,
1120
+ )
1121
+
1122
+ createEffect(() => {
1123
+ setValue(callback(queryCache))
1124
+ })
1125
+
1126
+ // @ts-ignore
1127
+ signalsMap.set(callback, setValue)
1128
+
1129
+ onCleanup(() => {
1130
+ // @ts-ignore
1131
+ signalsMap.delete(callback)
1132
+ })
1133
+
1134
+ return value
1135
+ }
1136
+
1137
+ const getStyles = () => {
1138
+ const { colors, font, size, alpha, shadow, border } = tokens
1139
+
1140
+ return {
1141
+ devtoolsBtn: css`
1142
+ z-index: 100000;
1143
+ position: fixed;
1144
+ padding: 4px;
1145
+
1146
+ display: flex;
1147
+ align-items: center;
1148
+ justify-content: center;
1149
+ border-radius: 9999px;
1150
+ box-shadow: ${shadow.md()};
1151
+ overflow: hidden;
1152
+
1153
+ & div {
1154
+ position: absolute;
1155
+ top: -8px;
1156
+ left: -8px;
1157
+ right: -8px;
1158
+ bottom: -8px;
1159
+ border-radius: 9999px;
1160
+
1161
+ & svg {
1162
+ position: absolute;
1163
+ width: 100%;
1164
+ height: 100%;
1165
+ }
1166
+ filter: blur(6px) saturate(1.2) contrast(1.1);
1167
+ }
1168
+
1169
+ &:focus-within {
1170
+ outline-offset: 2px;
1171
+ outline: 3px solid ${colors.green[600]};
1172
+ }
1173
+
1174
+ & button {
1175
+ position: relative;
1176
+ z-index: 1;
1177
+ padding: 0;
1178
+ border-radius: 9999px;
1179
+ background-color: transparent;
1180
+ border: none;
1181
+ height: 40px;
1182
+ display: flex;
1183
+ width: 40px;
1184
+ overflow: hidden;
1185
+ cursor: pointer;
1186
+ outline: none;
1187
+ & svg {
1188
+ position: absolute;
1189
+ width: 100%;
1190
+ height: 100%;
1191
+ }
1192
+ }
1193
+ `,
1194
+ panel: css`
1195
+ position: fixed;
1196
+ z-index: 9999;
1197
+ display: flex;
1198
+ gap: ${tokens.size[0.5]};
1199
+ & * {
1200
+ font-family: 'Inter', sans-serif;
1201
+ color: ${colors.gray[300]};
1202
+ box-sizing: border-box;
1203
+ }
1204
+ `,
1205
+ 'devtoolsBtn-position-bottom-right': css`
1206
+ bottom: 12px;
1207
+ right: 12px;
1208
+ `,
1209
+ 'devtoolsBtn-position-bottom-left': css`
1210
+ bottom: 12px;
1211
+ left: 12px;
1212
+ `,
1213
+ 'devtoolsBtn-position-top-left': css`
1214
+ top: 12px;
1215
+ left: 12px;
1216
+ `,
1217
+ 'devtoolsBtn-position-top-right': css`
1218
+ top: 12px;
1219
+ right: 12px;
1220
+ `,
1221
+ 'panel-position-top': css`
1222
+ top: 0;
1223
+ right: 0;
1224
+ left: 0;
1225
+ max-height: 90%;
1226
+ min-height: 3.5rem;
1227
+ border-bottom: ${colors.darkGray[300]} 1px solid;
1228
+ `,
1229
+ 'panel-position-bottom': css`
1230
+ bottom: 0;
1231
+ right: 0;
1232
+ left: 0;
1233
+ max-height: 90%;
1234
+ min-height: 3.5rem;
1235
+ border-top: ${colors.darkGray[300]} 1px solid;
1236
+ `,
1237
+ 'panel-position-right': css`
1238
+ bottom: 0;
1239
+ right: 0;
1240
+ top: 0;
1241
+ border-left: ${colors.darkGray[300]} 1px solid;
1242
+ max-width: 90%;
1243
+ `,
1244
+ 'panel-position-left': css`
1245
+ bottom: 0;
1246
+ left: 0;
1247
+ top: 0;
1248
+ border-right: ${colors.darkGray[300]} 1px solid;
1249
+ max-width: 90%;
1250
+ `,
1251
+ closeBtn: css`
1252
+ position: absolute;
1253
+ cursor: pointer;
1254
+ z-index: 5;
1255
+ display: flex;
1256
+ align-items: center;
1257
+ justify-content: center;
1258
+ outline: none;
1259
+ &:hover {
1260
+ background-color: ${colors.darkGray[500]};
1261
+ }
1262
+ &:focus-visible {
1263
+ outline: 2px solid ${colors.blue[600]};
1264
+ }
1265
+ `,
1266
+ 'closeBtn-position-top': css`
1267
+ bottom: 0;
1268
+ right: ${size[3]};
1269
+ transform: translate(0, 100%);
1270
+ background-color: ${colors.darkGray[700]};
1271
+ border-right: ${colors.darkGray[300]} 1px solid;
1272
+ border-left: ${colors.darkGray[300]} 1px solid;
1273
+ border-top: none;
1274
+ border-bottom: ${colors.darkGray[300]} 1px solid;
1275
+ border-radius: 0px 0px ${border.radius.sm} ${border.radius.sm};
1276
+ padding: ${size[1.5]} ${size[2.5]} ${size[2]} ${size[2.5]};
1277
+
1278
+ &::after {
1279
+ content: ' ';
1280
+ position: absolute;
1281
+ bottom: 100%;
1282
+ left: -${size[2.5]};
1283
+ height: ${size[1.5]};
1284
+ width: calc(100% + ${size[5]});
1285
+ }
1286
+
1287
+ & svg {
1288
+ transform: rotate(180deg);
1289
+ }
1290
+ `,
1291
+ 'closeBtn-position-bottom': css`
1292
+ top: 0;
1293
+ right: ${size[3]};
1294
+ transform: translate(0, -100%);
1295
+ background-color: ${colors.darkGray[700]};
1296
+ border-right: ${colors.darkGray[300]} 1px solid;
1297
+ border-left: ${colors.darkGray[300]} 1px solid;
1298
+ border-top: ${colors.darkGray[300]} 1px solid;
1299
+ border-bottom: none;
1300
+ border-radius: ${border.radius.sm} ${border.radius.sm} 0px 0px;
1301
+ padding: ${size[2]} ${size[2.5]} ${size[1.5]} ${size[2.5]};
1302
+
1303
+ &::after {
1304
+ content: ' ';
1305
+ position: absolute;
1306
+ top: 100%;
1307
+ left: -${size[2.5]};
1308
+ height: ${size[1.5]};
1309
+ width: calc(100% + ${size[5]});
1310
+ }
1311
+ `,
1312
+ 'closeBtn-position-right': css`
1313
+ bottom: ${size[3]};
1314
+ left: 0;
1315
+ transform: translate(-100%, 0);
1316
+ background-color: ${colors.darkGray[700]};
1317
+ border-right: none;
1318
+ border-left: ${colors.darkGray[300]} 1px solid;
1319
+ border-top: ${colors.darkGray[300]} 1px solid;
1320
+ border-bottom: ${colors.darkGray[300]} 1px solid;
1321
+ border-radius: ${border.radius.sm} 0px 0px ${border.radius.sm};
1322
+ padding: ${size[2.5]} ${size[1]} ${size[2.5]} ${size[1.5]};
1323
+
1324
+ &::after {
1325
+ content: ' ';
1326
+ position: absolute;
1327
+ left: 100%;
1328
+ height: calc(100% + ${size[5]});
1329
+ width: ${size[1.5]};
1330
+ }
1331
+
1332
+ & svg {
1333
+ transform: rotate(-90deg);
1334
+ }
1335
+ `,
1336
+ 'closeBtn-position-left': css`
1337
+ bottom: ${size[3]};
1338
+ right: 0;
1339
+ transform: translate(100%, 0);
1340
+ background-color: ${colors.darkGray[700]};
1341
+ border-left: none;
1342
+ border-right: ${colors.darkGray[300]} 1px solid;
1343
+ border-top: ${colors.darkGray[300]} 1px solid;
1344
+ border-bottom: ${colors.darkGray[300]} 1px solid;
1345
+ border-radius: 0px ${border.radius.sm} ${border.radius.sm} 0px;
1346
+ padding: ${size[2.5]} ${size[1.5]} ${size[2.5]} ${size[1]};
1347
+
1348
+ &::after {
1349
+ content: ' ';
1350
+ position: absolute;
1351
+ right: 100%;
1352
+ height: calc(100% + ${size[5]});
1353
+ width: ${size[1.5]};
1354
+ }
1355
+
1356
+ & svg {
1357
+ transform: rotate(90deg);
1358
+ }
1359
+ `,
1360
+ queriesContainer: css`
1361
+ flex: 1 1 700px;
1362
+ background-color: ${colors.darkGray[700]};
1363
+ display: flex;
1364
+ flex-direction: column;
1365
+ `,
1366
+ dragHandle: css`
1367
+ position: absolute;
1368
+ transition: background-color 0.125s ease;
1369
+ &:hover {
1370
+ background-color: ${colors.gray[400]}${alpha[90]};
1371
+ }
1372
+ z-index: 4;
1373
+ `,
1374
+ 'dragHandle-position-top': css`
1375
+ bottom: 0;
1376
+ width: 100%;
1377
+ height: ${tokens.size[1]};
1378
+ cursor: ns-resize;
1379
+ `,
1380
+ 'dragHandle-position-bottom': css`
1381
+ top: 0;
1382
+ width: 100%;
1383
+ height: ${tokens.size[1]};
1384
+ cursor: ns-resize;
1385
+ `,
1386
+ 'dragHandle-position-right': css`
1387
+ left: 0;
1388
+ width: ${tokens.size[1]};
1389
+ height: 100%;
1390
+ cursor: ew-resize;
1391
+ `,
1392
+ 'dragHandle-position-left': css`
1393
+ right: 0;
1394
+ width: ${tokens.size[1]};
1395
+ height: 100%;
1396
+ cursor: ew-resize;
1397
+ `,
1398
+ row: css`
1399
+ display: flex;
1400
+ justify-content: space-between;
1401
+ padding: ${tokens.size[2.5]} ${tokens.size[3]};
1402
+ gap: ${tokens.size[4]};
1403
+ border-bottom: ${colors.darkGray[500]} 1px solid;
1404
+ align-items: center;
1405
+ & > button {
1406
+ padding: 0;
1407
+ background: transparent;
1408
+ border: none;
1409
+ display: flex;
1410
+ flex-direction: column;
1411
+ }
1412
+ `,
1413
+ logo: css`
1414
+ cursor: pointer;
1415
+ &:hover {
1416
+ opacity: 0.7;
1417
+ }
1418
+ &:focus-visible {
1419
+ outline-offset: 4px;
1420
+ border-radius: ${border.radius.xs};
1421
+ outline: 2px solid ${colors.blue[800]};
1422
+ }
1423
+ `,
1424
+ tanstackLogo: css`
1425
+ font-size: ${font.size.lg};
1426
+ font-weight: ${font.weight.extrabold};
1427
+ line-height: ${font.lineHeight.sm};
1428
+ white-space: nowrap;
1429
+ `,
1430
+ queryFlavorLogo: css`
1431
+ font-weight: ${font.weight.semibold};
1432
+ font-size: ${font.size.sm};
1433
+ background: linear-gradient(to right, #dd524b, #e9a03b);
1434
+ background-clip: text;
1435
+ line-height: ${font.lineHeight.xs};
1436
+ -webkit-text-fill-color: transparent;
1437
+ white-space: nowrap;
1438
+ `,
1439
+ queryStatusContainer: css`
1440
+ display: flex;
1441
+ gap: ${tokens.size[2]};
1442
+ height: min-content;
1443
+ `,
1444
+ queryStatusTag: css`
1445
+ display: flex;
1446
+ gap: ${tokens.size[1.5]};
1447
+ background: ${colors.darkGray[500]};
1448
+ border-radius: ${tokens.border.radius.md};
1449
+ font-size: ${font.size.sm};
1450
+ padding: ${tokens.size[1]};
1451
+ padding-left: ${tokens.size[2.5]};
1452
+ align-items: center;
1453
+ line-height: ${font.lineHeight.md};
1454
+ font-weight: ${font.weight.medium};
1455
+ border: none;
1456
+ user-select: none;
1457
+ position: relative;
1458
+ &:focus-visible {
1459
+ outline-offset: 2px;
1460
+ outline: 2px solid ${colors.blue[800]};
1461
+ }
1462
+ & span:nth-child(2) {
1463
+ color: ${colors.gray[300]}${alpha[80]};
1464
+ }
1465
+ `,
1466
+ statusTooltip: css`
1467
+ position: absolute;
1468
+ z-index: 1;
1469
+ background-color: ${colors.darkGray[500]};
1470
+ top: 100%;
1471
+ left: 50%;
1472
+ transform: translate(-50%, calc(${tokens.size[2]}));
1473
+ padding: ${tokens.size[0.5]} ${tokens.size[3]};
1474
+ border-radius: ${tokens.border.radius.md};
1475
+ font-size: ${font.size.sm};
1476
+ border: 2px solid ${colors.gray[600]};
1477
+ color: ${tokens.colors['gray'][300]};
1478
+
1479
+ &::before {
1480
+ top: 0px;
1481
+ content: ' ';
1482
+ display: block;
1483
+ left: 50%;
1484
+ transform: translate(-50%, -100%);
1485
+ position: absolute;
1486
+ border-color: transparent transparent ${colors.gray[600]} transparent;
1487
+ border-style: solid;
1488
+ border-width: 7px;
1489
+ /* transform: rotate(180deg); */
1490
+ }
1491
+
1492
+ &::after {
1493
+ top: 0px;
1494
+ content: ' ';
1495
+ display: block;
1496
+ left: 50%;
1497
+ transform: translate(-50%, calc(-100% + 2.5px));
1498
+ position: absolute;
1499
+ border-color: transparent transparent ${colors.darkGray[500]}
1500
+ transparent;
1501
+ border-style: solid;
1502
+ border-width: 7px;
1503
+ }
1504
+ `,
1505
+ selectedQueryRow: css`
1506
+ background-color: ${colors.darkGray[500]};
1507
+ `,
1508
+ queryStatusCount: css`
1509
+ padding: 0 8px;
1510
+ display: flex;
1511
+ align-items: center;
1512
+ justify-content: center;
1513
+ color: ${colors.gray[400]};
1514
+ background-color: ${colors.darkGray[300]};
1515
+ border-radius: 3px;
1516
+ font-variant-numeric: tabular-nums;
1517
+ `,
1518
+ filtersContainer: css`
1519
+ display: flex;
1520
+ gap: ${tokens.size[2.5]};
1521
+ & > button {
1522
+ cursor: pointer;
1523
+ padding: ${tokens.size[1.5]} ${tokens.size[2.5]};
1524
+ padding-right: ${tokens.size[1.5]};
1525
+ border-radius: ${tokens.border.radius.md};
1526
+ background-color: ${colors.darkGray[400]};
1527
+ font-size: ${font.size.sm};
1528
+ display: flex;
1529
+ align-items: center;
1530
+ line-height: ${font.lineHeight.sm};
1531
+ gap: ${tokens.size[1.5]};
1532
+ max-width: 160px;
1533
+ border: 1px solid ${colors.darkGray[200]};
1534
+ &:focus-visible {
1535
+ outline-offset: 2px;
1536
+ border-radius: ${border.radius.xs};
1537
+ outline: 2px solid ${colors.blue[800]};
1538
+ }
1539
+ }
1540
+ `,
1541
+ filterInput: css`
1542
+ padding: ${tokens.size[1.5]} ${tokens.size[2.5]};
1543
+ border-radius: ${tokens.border.radius.md};
1544
+ background-color: ${colors.darkGray[400]};
1545
+ display: flex;
1546
+ box-sizing: content-box;
1547
+ align-items: center;
1548
+ gap: ${tokens.size[1.5]};
1549
+ max-width: 160px;
1550
+ min-width: 100px;
1551
+ border: 1px solid ${colors.darkGray[200]};
1552
+ height: min-content;
1553
+ & > svg {
1554
+ width: ${tokens.size[3.5]};
1555
+ height: ${tokens.size[3.5]};
1556
+ }
1557
+ & input {
1558
+ font-size: ${font.size.sm};
1559
+ width: 100%;
1560
+ background-color: ${colors.darkGray[400]};
1561
+ border: none;
1562
+ padding: 0;
1563
+ line-height: ${font.lineHeight.sm};
1564
+ color: ${colors.gray[300]};
1565
+ &::placeholder {
1566
+ color: ${colors.gray[300]};
1567
+ }
1568
+ &:focus {
1569
+ outline: none;
1570
+ }
1571
+ }
1572
+
1573
+ &:focus-within {
1574
+ outline-offset: 2px;
1575
+ border-radius: ${border.radius.xs};
1576
+ outline: 2px solid ${colors.blue[800]};
1577
+ }
1578
+ `,
1579
+ filterSelect: css`
1580
+ padding: ${tokens.size[1.5]} ${tokens.size[2.5]};
1581
+ border-radius: ${tokens.border.radius.md};
1582
+ background-color: ${colors.darkGray[400]};
1583
+ display: flex;
1584
+ align-items: center;
1585
+ gap: ${tokens.size[1.5]};
1586
+ box-sizing: content-box;
1587
+ max-width: 160px;
1588
+ border: 1px solid ${colors.darkGray[200]};
1589
+ height: min-content;
1590
+ & > svg {
1591
+ width: ${tokens.size[3]};
1592
+ height: ${tokens.size[3]};
1593
+ }
1594
+ & > select {
1595
+ appearance: none;
1596
+ min-width: 100px;
1597
+ line-height: ${font.lineHeight.sm};
1598
+ font-size: ${font.size.sm};
1599
+ background-color: ${colors.darkGray[400]};
1600
+ border: none;
1601
+ &:focus {
1602
+ outline: none;
1603
+ }
1604
+ }
1605
+ &:focus-within {
1606
+ outline-offset: 2px;
1607
+ border-radius: ${border.radius.xs};
1608
+ outline: 2px solid ${colors.blue[800]};
1609
+ }
1610
+ `,
1611
+ actionsContainer: css`
1612
+ display: flex;
1613
+ gap: ${tokens.size[2.5]};
1614
+ `,
1615
+ actionsBtn: css`
1616
+ border-radius: ${tokens.border.radius.md};
1617
+ background-color: ${colors.darkGray[400]};
1618
+ width: 2.125rem; // 34px
1619
+ height: 2.125rem; // 34px
1620
+ justify-content: center;
1621
+ display: flex;
1622
+ align-items: center;
1623
+ gap: ${tokens.size[1.5]};
1624
+ max-width: 160px;
1625
+ border: 1px solid ${colors.darkGray[200]};
1626
+ cursor: pointer;
1627
+ &:hover {
1628
+ background-color: ${colors.darkGray[500]};
1629
+ }
1630
+ & svg {
1631
+ width: ${tokens.size[4]};
1632
+ height: ${tokens.size[4]};
1633
+ }
1634
+ &:focus-visible {
1635
+ outline-offset: 2px;
1636
+ border-radius: ${border.radius.xs};
1637
+ outline: 2px solid ${colors.blue[800]};
1638
+ }
1639
+ `,
1640
+ overflowQueryContainer: css`
1641
+ flex: 1;
1642
+ overflow-y: auto;
1643
+ & > div {
1644
+ display: flex;
1645
+ flex-direction: column;
1646
+ }
1647
+ `,
1648
+ queryRow: css`
1649
+ display: flex;
1650
+ align-items: center;
1651
+ padding: 0;
1652
+ background-color: inherit;
1653
+ border: none;
1654
+ cursor: pointer;
1655
+ &:focus-visible {
1656
+ outline-offset: -2px;
1657
+ border-radius: ${border.radius.xs};
1658
+ outline: 2px solid ${colors.blue[800]};
1659
+ }
1660
+ &:hover .TSQDQueryHash {
1661
+ background-color: ${colors.darkGray[600]};
1662
+ }
1663
+
1664
+ & .TSQDObserverCount {
1665
+ padding: 0 ${tokens.size[1]};
1666
+ user-select: none;
1667
+ min-width: ${tokens.size[8]};
1668
+ align-self: stretch !important;
1669
+ display: flex;
1670
+ align-items: center;
1671
+ justify-content: center;
1672
+ font-size: ${font.size.sm};
1673
+ font-weight: ${font.weight.medium};
1674
+ border-bottom: 1px solid ${colors.darkGray[700]};
1675
+ }
1676
+ & .TSQDQueryHash {
1677
+ user-select: text;
1678
+ font-size: ${font.size.sm};
1679
+ display: flex;
1680
+ align-items: center;
1681
+ min-height: ${tokens.size[8]};
1682
+ flex: 1;
1683
+ padding: ${tokens.size[1]} ${tokens.size[2]};
1684
+ font-family: 'Menlo', 'Fira Code', monospace !important;
1685
+ border-bottom: 1px solid ${colors.darkGray[400]};
1686
+ text-align: left;
1687
+ text-overflow: clip;
1688
+ word-break: break-word;
1689
+ }
1690
+
1691
+ & .TSQDQueryDisabled {
1692
+ align-self: stretch;
1693
+ align-self: stretch !important;
1694
+ display: flex;
1695
+ align-items: center;
1696
+ padding: 0 ${tokens.size[3]};
1697
+ color: ${colors.gray[300]};
1698
+ background-color: ${colors.darkGray[600]};
1699
+ border-bottom: 1px solid ${colors.darkGray[400]};
1700
+ font-size: ${font.size.sm};
1701
+ }
1702
+ `,
1703
+ detailsContainer: css`
1704
+ flex: 1 1 700px;
1705
+ background-color: ${colors.darkGray[700]};
1706
+ display: flex;
1707
+ flex-direction: column;
1708
+ overflow-y: auto;
1709
+ display: flex;
1710
+ `,
1711
+ detailsHeader: css`
1712
+ position: sticky;
1713
+ top: 0;
1714
+ z-index: 2;
1715
+ background-color: ${colors.darkGray[600]};
1716
+ padding: ${tokens.size[2]} ${tokens.size[2]};
1717
+ font-weight: ${font.weight.medium};
1718
+ font-size: ${font.size.sm};
1719
+ `,
1720
+ detailsBody: css`
1721
+ margin: ${tokens.size[2]} 0px ${tokens.size[3]} 0px;
1722
+ & > div {
1723
+ display: flex;
1724
+ align-items: stretch;
1725
+ padding: 0 ${tokens.size[2]};
1726
+ line-height: ${font.lineHeight.sm};
1727
+ justify-content: space-between;
1728
+ & > span {
1729
+ font-size: ${font.size.sm};
1730
+ }
1731
+ & > span:nth-child(2) {
1732
+ font-variant-numeric: tabular-nums;
1733
+ }
1734
+ }
1735
+
1736
+ & > div:first-child {
1737
+ margin-bottom: ${tokens.size[2]};
1738
+ }
1739
+
1740
+ & code {
1741
+ font-family: 'Menlo', 'Fira Code', monospace !important;
1742
+ margin: 0;
1743
+ font-size: ${font.size.sm};
1744
+ line-height: ${font.lineHeight.sm};
1745
+ }
1746
+ `,
1747
+ queryDetailsStatus: css`
1748
+ border: 1px solid ${colors.darkGray[200]};
1749
+ border-radius: ${tokens.border.radius.md};
1750
+ font-weight: ${font.weight.medium};
1751
+ padding: ${tokens.size[1]} ${tokens.size[2.5]};
1752
+ `,
1753
+ actionsBody: css`
1754
+ flex-wrap: wrap;
1755
+ margin: ${tokens.size[3]} 0px ${tokens.size[3]} 0px;
1756
+ display: flex;
1757
+ gap: ${tokens.size[2]};
1758
+ padding: 0px ${tokens.size[2]};
1759
+ & > button {
1760
+ font-size: ${font.size.sm};
1761
+ padding: ${tokens.size[2]} ${tokens.size[2]};
1762
+ display: flex;
1763
+ border-radius: ${tokens.border.radius.md};
1764
+ border: 1px solid ${colors.darkGray[400]};
1765
+ background-color: ${colors.darkGray[600]};
1766
+ align-items: center;
1767
+ gap: ${tokens.size[2]};
1768
+ font-weight: ${font.weight.medium};
1769
+ line-height: ${font.lineHeight.sm};
1770
+ cursor: pointer;
1771
+ &:focus-visible {
1772
+ outline-offset: 2px;
1773
+ border-radius: ${border.radius.xs};
1774
+ outline: 2px solid ${colors.blue[800]};
1775
+ }
1776
+ &:hover {
1777
+ background-color: ${colors.darkGray[500]};
1778
+ }
1779
+
1780
+ &:disabled {
1781
+ opacity: 0.6;
1782
+ cursor: not-allowed;
1783
+ }
1784
+
1785
+ & > span {
1786
+ width: ${size[2]};
1787
+ height: ${size[2]};
1788
+ border-radius: ${tokens.border.radius.full};
1789
+ }
1790
+ }
1791
+ `,
1792
+ actionsSelect: css`
1793
+ font-size: ${font.size.sm};
1794
+ padding: ${tokens.size[2]} ${tokens.size[2]};
1795
+ display: flex;
1796
+ border-radius: ${tokens.border.radius.md};
1797
+ overflow: hidden;
1798
+ border: 1px solid ${colors.darkGray[400]};
1799
+ background-color: ${colors.darkGray[600]};
1800
+ align-items: center;
1801
+ gap: ${tokens.size[2]};
1802
+ font-weight: ${font.weight.medium};
1803
+ line-height: ${font.lineHeight.sm};
1804
+ color: ${tokens.colors.red[400]};
1805
+ cursor: pointer;
1806
+ position: relative;
1807
+ &:hover {
1808
+ background-color: ${colors.darkGray[500]};
1809
+ }
1810
+ & > span {
1811
+ width: ${size[2]};
1812
+ height: ${size[2]};
1813
+ border-radius: ${tokens.border.radius.full};
1814
+ }
1815
+ &:focus-within {
1816
+ outline-offset: 2px;
1817
+ border-radius: ${border.radius.xs};
1818
+ outline: 2px solid ${colors.blue[800]};
1819
+ }
1820
+ & select {
1821
+ position: absolute;
1822
+ top: 0;
1823
+ left: 0;
1824
+ width: 100%;
1825
+ height: 100%;
1826
+ appearance: none;
1827
+ background-color: transparent;
1828
+ border: none;
1829
+ color: transparent;
1830
+ outline: none;
1831
+ }
1832
+
1833
+ & svg path {
1834
+ stroke: ${tokens.colors.red[400]} !important;
1835
+ }
1836
+ `,
1837
+ settingsMenu: css`
1838
+ position: absolute;
1839
+ top: calc(100% + ${tokens.size[2]});
1840
+ border-radius: ${tokens.border.radius.lg};
1841
+ border: 1px solid ${colors.gray[600]};
1842
+ right: 0;
1843
+ min-width: ${tokens.size[44]};
1844
+ background-color: ${colors.darkGray[400]};
1845
+ font-size: ${font.size.sm};
1846
+ color: ${colors.gray[500]};
1847
+ z-index: 2;
1848
+ `,
1849
+ settingsMenuHeader: css`
1850
+ padding: ${tokens.size[1.5]} ${tokens.size[2.5]};
1851
+ color: ${colors.gray[300]};
1852
+ font-weight: ${font.weight.medium};
1853
+ `,
1854
+ settingsMenuSection: css`
1855
+ border-top: 1px solid ${colors.gray[600]};
1856
+ display: flex;
1857
+ flex-direction: column;
1858
+ padding: ${tokens.size[1]} ${tokens.size[1]};
1859
+
1860
+ & > button {
1861
+ cursor: pointer;
1862
+ background-color: transparent;
1863
+ border: none;
1864
+ padding: ${tokens.size[2]} ${tokens.size[1.5]};
1865
+ font-size: ${font.size.sm};
1866
+ display: flex;
1867
+ align-items: center;
1868
+ justify-content: flex-start;
1869
+ gap: ${tokens.size[2]};
1870
+ border-radius: ${tokens.border.radius.md};
1871
+ &:hover {
1872
+ background-color: ${colors.darkGray[500]};
1873
+ }
1874
+
1875
+ &:focus-visible {
1876
+ outline-offset: 2px;
1877
+ outline: 2px solid ${colors.blue[800]};
1878
+ }
1879
+ }
1880
+
1881
+ & button:nth-child(4) svg {
1882
+ transform: rotate(-90deg);
1883
+ }
1884
+
1885
+ & button:nth-child(3) svg {
1886
+ transform: rotate(90deg);
1887
+ }
1888
+ `,
1889
+ }
1890
+ }