@tanstack/query-devtools 5.0.5 → 5.4.2

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.
package/src/Devtools.tsx CHANGED
@@ -15,16 +15,18 @@ import { TransitionGroup } from 'solid-transition-group'
15
15
  import { Key } from '@solid-primitives/keyed'
16
16
  import { createLocalStorage } from '@solid-primitives/storage'
17
17
  import { createResizeObserver } from '@solid-primitives/resize-observer'
18
- import { DropdownMenu } from '@kobalte/core'
18
+ import { DropdownMenu, RadioGroup } from '@kobalte/core'
19
19
  import { tokens } from './theme'
20
20
  import {
21
21
  convertRemToPixels,
22
22
  displayValue,
23
+ getMutationStatusColor,
23
24
  getPreferredColorScheme,
24
25
  getQueryStatusColor,
25
26
  getQueryStatusColorByLabel,
26
27
  getQueryStatusLabel,
27
28
  getSidedProp,
29
+ mutationSortFns,
28
30
  sortFns,
29
31
  } from './utils'
30
32
  import {
@@ -32,16 +34,20 @@ import {
32
34
  ArrowLeft,
33
35
  ArrowRight,
34
36
  ArrowUp,
37
+ CheckCircle,
35
38
  ChevronDown,
39
+ LoadingCircle,
36
40
  Monitor,
37
41
  Moon,
38
42
  Offline,
43
+ PauseCircle,
39
44
  Search,
40
45
  Settings,
41
46
  Sun,
42
47
  TanstackLogo,
43
48
  Trash,
44
49
  Wifi,
50
+ XCircle,
45
51
  } from './icons'
46
52
  import Explorer from './Explorer'
47
53
  import {
@@ -57,7 +63,13 @@ import type {
57
63
  DevtoolsPosition,
58
64
  QueryDevtoolsProps,
59
65
  } from './Context'
60
- import type { Query, QueryCache, QueryState } from '@tanstack/query-core'
66
+ import type {
67
+ Mutation,
68
+ MutationCache,
69
+ Query,
70
+ QueryCache,
71
+ QueryState,
72
+ } from '@tanstack/query-core'
61
73
  import type { StorageObject, StorageSetter } from '@solid-primitives/storage'
62
74
  import type { Accessor, Component, JSX, Setter } from 'solid-js'
63
75
 
@@ -68,7 +80,7 @@ interface DevtoolsPanelProps {
68
80
 
69
81
  interface QueryStatusProps {
70
82
  label: string
71
- color: 'green' | 'yellow' | 'gray' | 'blue' | 'purple'
83
+ color: 'green' | 'yellow' | 'gray' | 'blue' | 'purple' | 'red'
72
84
  count: number
73
85
  }
74
86
 
@@ -84,10 +96,14 @@ const DEFAULT_HEIGHT = 500
84
96
  const DEFAULT_WIDTH = 500
85
97
  const DEFAULT_SORT_FN_NAME = Object.keys(sortFns)[0]
86
98
  const DEFAULT_SORT_ORDER = 1
99
+ const DEFAULT_MUTATION_SORT_FN_NAME = Object.keys(mutationSortFns)[0]
87
100
 
88
101
  const [selectedQueryHash, setSelectedQueryHash] = createSignal<string | null>(
89
102
  null,
90
103
  )
104
+ const [selectedMutationId, setSelectedMutationId] = createSignal<number | null>(
105
+ null,
106
+ )
91
107
  const [panelWidth, setPanelWidth] = createSignal(0)
92
108
 
93
109
  export const DevtoolsComponent: Component<QueryDevtoolsProps> = (props) => {
@@ -145,8 +161,9 @@ export const Devtools: Component<DevtoolsPanelProps> = (props) => {
145
161
  )
146
162
  })
147
163
 
164
+ let transitionsContainerRef!: HTMLDivElement
148
165
  createEffect(() => {
149
- const root = document.querySelector('.tsqd-parent-container') as HTMLElement
166
+ const root = transitionsContainerRef.parentElement as HTMLElement
150
167
  const height = props.localStore.height || DEFAULT_HEIGHT
151
168
  const width = props.localStore.width || DEFAULT_WIDTH
152
169
  const panelPosition = position()
@@ -160,6 +177,23 @@ export const Devtools: Component<DevtoolsPanelProps> = (props) => {
160
177
  )
161
178
  })
162
179
 
180
+ // Calculates the inherited font size of the parent and sets it as a CSS variable
181
+ // All the design tokens are calculated based on this variable
182
+ onMount(() => {
183
+ // This is to make sure that the font size is updated when the stylesheet is updated
184
+ // and the user focuses back on the window
185
+ const onFocus = () => {
186
+ const root = transitionsContainerRef.parentElement as HTMLElement
187
+ const fontSize = getComputedStyle(root).fontSize
188
+ root.style.setProperty('--tsqd-font-size', fontSize)
189
+ }
190
+ onFocus()
191
+ window.addEventListener('focus', onFocus)
192
+ onCleanup(() => {
193
+ window.removeEventListener('focus', onFocus)
194
+ })
195
+ })
196
+
163
197
  return (
164
198
  <div
165
199
  // styles for animating the panel in and out
@@ -193,6 +227,7 @@ export const Devtools: Component<DevtoolsPanelProps> = (props) => {
193
227
  `,
194
228
  'tsqd-transitions-container',
195
229
  )}
230
+ ref={transitionsContainerRef}
196
231
  >
197
232
  <TransitionGroup name="tsqd-panel-transition">
198
233
  <Show when={isOpen()}>
@@ -234,13 +269,6 @@ export const DevtoolsPanel: Component<DevtoolsPanelProps> = (props) => {
234
269
 
235
270
  const [isResizing, setIsResizing] = createSignal(false)
236
271
 
237
- const sort = createMemo(() => props.localStore.sort || DEFAULT_SORT_FN_NAME)
238
- const sortOrder = createMemo(
239
- () => Number(props.localStore.sortOrder) || DEFAULT_SORT_ORDER,
240
- ) as () => 1 | -1
241
-
242
- const [offline, setOffline] = createSignal(false)
243
-
244
272
  const position = createMemo(
245
273
  () =>
246
274
  (props.localStore.position ||
@@ -248,41 +276,6 @@ export const DevtoolsPanel: Component<DevtoolsPanelProps> = (props) => {
248
276
  POSITION) as DevtoolsPosition,
249
277
  )
250
278
 
251
- const sortFn = createMemo(() => sortFns[sort() as string])
252
-
253
- const onlineManager = createMemo(
254
- () => useQueryDevtoolsContext().onlineManager,
255
- )
256
-
257
- const cache = createMemo(() => {
258
- return useQueryDevtoolsContext().client.getQueryCache()
259
- })
260
-
261
- const queryCount = createSubscribeToQueryCacheBatcher((queryCache) => {
262
- return queryCache().getAll().length
263
- }, false)
264
-
265
- const queries = createMemo(
266
- on(
267
- () => [queryCount(), props.localStore.filter, sort(), sortOrder()],
268
- () => {
269
- const curr = cache().getAll()
270
-
271
- const filtered = props.localStore.filter
272
- ? curr.filter(
273
- (item) =>
274
- rankItem(item.queryHash, props.localStore.filter || '').passed,
275
- )
276
- : [...curr]
277
-
278
- const sorted = sortFn()
279
- ? filtered.sort((a, b) => sortFn()!(a, b) * sortOrder())
280
- : filtered
281
- return sorted
282
- },
283
- ),
284
- )
285
-
286
279
  const handleDragStart: JSX.EventHandler<HTMLDivElement, MouseEvent> = (
287
280
  event,
288
281
  ) => {
@@ -344,9 +337,6 @@ export const DevtoolsPanel: Component<DevtoolsPanelProps> = (props) => {
344
337
  document.addEventListener('mouseup', unsub, false)
345
338
  }
346
339
 
347
- setupQueryCacheSubscription()
348
-
349
- let queriesContainerRef!: HTMLDivElement
350
340
  let panelRef!: HTMLDivElement
351
341
 
352
342
  onMount(() => {
@@ -357,10 +347,6 @@ export const DevtoolsPanel: Component<DevtoolsPanelProps> = (props) => {
357
347
  })
358
348
  })
359
349
 
360
- const setDevtoolsPosition = (pos: DevtoolsPosition) => {
361
- props.setLocalStore('position', pos)
362
- }
363
-
364
350
  createEffect(() => {
365
351
  const rootContainer = panelRef.parentElement?.parentElement?.parentElement
366
352
  if (!rootContainer) return
@@ -465,51 +451,212 @@ export const DevtoolsPanel: Component<DevtoolsPanelProps> = (props) => {
465
451
  >
466
452
  <ChevronDown />
467
453
  </button>
454
+ <ContentView
455
+ localStore={props.localStore}
456
+ setLocalStore={props.setLocalStore}
457
+ />
458
+ </aside>
459
+ )
460
+ }
461
+
462
+ const ContentView: Component<DevtoolsPanelProps> = (props) => {
463
+ setupQueryCacheSubscription()
464
+ setupMutationCacheSubscription()
465
+
466
+ let containerRef!: HTMLDivElement
467
+ const theme = useTheme()
468
+ const styles = createMemo(() => {
469
+ return theme() === 'dark' ? darkStyles : lightStyles
470
+ })
471
+
472
+ const [selectedView, setSelectedView] = createSignal<'queries' | 'mutations'>(
473
+ 'queries',
474
+ )
475
+
476
+ const sort = createMemo(() => props.localStore.sort || DEFAULT_SORT_FN_NAME)
477
+ const sortOrder = createMemo(
478
+ () => Number(props.localStore.sortOrder) || DEFAULT_SORT_ORDER,
479
+ ) as () => 1 | -1
480
+
481
+ const mutationSort = createMemo(
482
+ () => props.localStore.mutationSort || DEFAULT_MUTATION_SORT_FN_NAME,
483
+ )
484
+ const mutationSortOrder = createMemo(
485
+ () => Number(props.localStore.mutationSortOrder) || DEFAULT_SORT_ORDER,
486
+ ) as () => 1 | -1
487
+
488
+ const [offline, setOffline] = createSignal(false)
489
+
490
+ const sortFn = createMemo(() => sortFns[sort() as string])
491
+ const mutationSortFn = createMemo(
492
+ () => mutationSortFns[mutationSort() as string],
493
+ )
494
+
495
+ const onlineManager = createMemo(
496
+ () => useQueryDevtoolsContext().onlineManager,
497
+ )
498
+
499
+ const query_cache = createMemo(() => {
500
+ return useQueryDevtoolsContext().client.getQueryCache()
501
+ })
502
+
503
+ const mutation_cache = createMemo(() => {
504
+ return useQueryDevtoolsContext().client.getMutationCache()
505
+ })
506
+
507
+ const queryCount = createSubscribeToQueryCacheBatcher((queryCache) => {
508
+ return queryCache().getAll().length
509
+ }, false)
510
+
511
+ const queries = createMemo(
512
+ on(
513
+ () => [queryCount(), props.localStore.filter, sort(), sortOrder()],
514
+ () => {
515
+ const curr = query_cache().getAll()
516
+
517
+ const filtered = props.localStore.filter
518
+ ? curr.filter(
519
+ (item) =>
520
+ rankItem(item.queryHash, props.localStore.filter || '').passed,
521
+ )
522
+ : [...curr]
523
+
524
+ const sorted = sortFn()
525
+ ? filtered.sort((a, b) => sortFn()!(a, b) * sortOrder())
526
+ : filtered
527
+ return sorted
528
+ },
529
+ ),
530
+ )
531
+
532
+ const mutationCount = createSubscribeToMutationCacheBatcher(
533
+ (mutationCache) => {
534
+ return mutationCache().getAll().length
535
+ },
536
+ false,
537
+ )
538
+
539
+ const mutations = createMemo(
540
+ on(
541
+ () => [
542
+ mutationCount(),
543
+ props.localStore.mutationFilter,
544
+ mutationSort(),
545
+ mutationSortOrder(),
546
+ ],
547
+ () => {
548
+ const curr = mutation_cache().getAll()
549
+
550
+ const filtered = props.localStore.mutationFilter
551
+ ? curr.filter((item) => {
552
+ const value = `${
553
+ item.options.mutationKey
554
+ ? JSON.stringify(item.options.mutationKey) + ' - '
555
+ : ''
556
+ }${new Date(item.state.submittedAt).toLocaleString()}`
557
+ return rankItem(value, props.localStore.mutationFilter || '')
558
+ .passed
559
+ })
560
+ : [...curr]
561
+
562
+ const sorted = mutationSortFn()
563
+ ? filtered.sort(
564
+ (a, b) => mutationSortFn()!(a, b) * mutationSortOrder(),
565
+ )
566
+ : filtered
567
+ return sorted
568
+ },
569
+ ),
570
+ )
571
+
572
+ const setDevtoolsPosition = (pos: DevtoolsPosition) => {
573
+ props.setLocalStore('position', pos)
574
+ }
575
+
576
+ // Sets the Font Size variable on portal menu elements since they will be outside
577
+ // the main panel container
578
+ const setComputedVariables = (el: HTMLDivElement) => {
579
+ const computedStyle = getComputedStyle(containerRef)
580
+ const variable = computedStyle.getPropertyValue('--tsqd-font-size')
581
+ el.style.setProperty('--tsqd-font-size', variable)
582
+ }
583
+ return (
584
+ <>
468
585
  <div
469
- ref={queriesContainerRef}
470
586
  // When the panels are stacked we use the height style
471
587
  // to divide the panels into two equal parts
472
588
  class={cx(
473
589
  styles().queriesContainer,
474
590
  panelWidth() < secondBreakpoint &&
475
- selectedQueryHash() &&
591
+ (selectedQueryHash() || selectedMutationId()) &&
476
592
  css`
477
593
  height: 50%;
478
594
  max-height: 50%;
479
595
  `,
480
596
  'tsqd-queries-container',
481
597
  )}
598
+ ref={containerRef}
482
599
  >
483
600
  <div class={cx(styles().row, 'tsqd-header')}>
484
- <button
485
- class={cx(styles().logo, 'tsqd-text-logo-container')}
486
- onClick={() => props.setLocalStore('open', 'false')}
487
- aria-label="Close Tanstack query devtools"
488
- >
489
- <span class={cx(styles().tanstackLogo, 'tsqd-text-logo-tanstack')}>
490
- TANSTACK
491
- </span>
492
- <span
493
- class={cx(
494
- styles().queryFlavorLogo,
495
- 'tsqd-text-logo-query-flavor',
496
- )}
601
+ <div class={styles().logoAndToggleContainer}>
602
+ <button
603
+ class={cx(styles().logo, 'tsqd-text-logo-container')}
604
+ onClick={() => props.setLocalStore('open', 'false')}
605
+ aria-label="Close Tanstack query devtools"
497
606
  >
498
- {useQueryDevtoolsContext().queryFlavor} v
499
- {useQueryDevtoolsContext().version}
500
- </span>
501
- </button>
502
- <QueryStatusCount />
607
+ <span
608
+ class={cx(styles().tanstackLogo, 'tsqd-text-logo-tanstack')}
609
+ >
610
+ TANSTACK
611
+ </span>
612
+ <span
613
+ class={cx(
614
+ styles().queryFlavorLogo,
615
+ 'tsqd-text-logo-query-flavor',
616
+ )}
617
+ >
618
+ {useQueryDevtoolsContext().queryFlavor} v
619
+ {useQueryDevtoolsContext().version}
620
+ </span>
621
+ </button>
622
+ <RadioGroup.Root
623
+ class={cx(styles().viewToggle)}
624
+ value={selectedView()}
625
+ onChange={(value) => {
626
+ setSelectedView(value as 'queries' | 'mutations')
627
+ setSelectedQueryHash(null)
628
+ setSelectedMutationId(null)
629
+ }}
630
+ >
631
+ <RadioGroup.Item value="queries" class="tsqd-radio-toggle">
632
+ <RadioGroup.ItemInput />
633
+ <RadioGroup.ItemControl>
634
+ <RadioGroup.ItemIndicator />
635
+ </RadioGroup.ItemControl>
636
+ <RadioGroup.ItemLabel title="Toggle Queries View">
637
+ Queries
638
+ </RadioGroup.ItemLabel>
639
+ </RadioGroup.Item>
640
+ <RadioGroup.Item value="mutations" class="tsqd-radio-toggle">
641
+ <RadioGroup.ItemInput />
642
+ <RadioGroup.ItemControl>
643
+ <RadioGroup.ItemIndicator />
644
+ </RadioGroup.ItemControl>
645
+ <RadioGroup.ItemLabel title="Toggle Mutations View">
646
+ Mutations
647
+ </RadioGroup.ItemLabel>
648
+ </RadioGroup.Item>
649
+ </RadioGroup.Root>
650
+ </div>
651
+
652
+ <Show when={selectedView() === 'queries'}>
653
+ <QueryStatusCount />
654
+ </Show>
655
+ <Show when={selectedView() === 'mutations'}>
656
+ <MutationStatusCount />
657
+ </Show>
503
658
  </div>
504
- <div
505
- class={cx(
506
- styles().row,
507
- css`
508
- gap: ${tokens.size[2.5]};
509
- `,
510
- 'tsqd-filters-actions-container',
511
- )}
512
- >
659
+ <div class={cx(styles().row, 'tsqd-filters-actions-container')}>
513
660
  <div class={cx(styles().filtersContainer, 'tsqd-filters-container')}>
514
661
  <div
515
662
  class={cx(
@@ -522,11 +669,19 @@ export const DevtoolsPanel: Component<DevtoolsPanelProps> = (props) => {
522
669
  aria-label="Filter queries by query key"
523
670
  type="text"
524
671
  placeholder="Filter"
525
- onInput={(e) =>
526
- props.setLocalStore('filter', e.currentTarget.value)
527
- }
672
+ onInput={(e) => {
673
+ if (selectedView() === 'queries') {
674
+ props.setLocalStore('filter', e.currentTarget.value)
675
+ } else {
676
+ props.setLocalStore('mutationFilter', e.currentTarget.value)
677
+ }
678
+ }}
528
679
  class="tsqd-query-filter-textfield"
529
- value={props.localStore.filter || ''}
680
+ value={
681
+ selectedView() === 'queries'
682
+ ? props.localStore.filter || ''
683
+ : props.localStore.mutationFilter || ''
684
+ }
530
685
  />
531
686
  </div>
532
687
  <div
@@ -535,33 +690,74 @@ export const DevtoolsPanel: Component<DevtoolsPanelProps> = (props) => {
535
690
  'tsqd-query-filter-sort-container',
536
691
  )}
537
692
  >
538
- <select
539
- value={sort()}
540
- onChange={(e) =>
541
- props.setLocalStore('sort', e.currentTarget.value)
542
- }
543
- >
544
- {Object.keys(sortFns).map((key) => (
545
- <option value={key}>Sort by {key}</option>
546
- ))}
547
- </select>
693
+ <Show when={selectedView() === 'queries'}>
694
+ <select
695
+ value={sort()}
696
+ onChange={(e) => {
697
+ props.setLocalStore('sort', e.currentTarget.value)
698
+ }}
699
+ >
700
+ {Object.keys(sortFns).map((key) => (
701
+ <option value={key}>Sort by {key}</option>
702
+ ))}
703
+ </select>
704
+ </Show>
705
+ <Show when={selectedView() === 'mutations'}>
706
+ <select
707
+ value={mutationSort()}
708
+ onChange={(e) => {
709
+ props.setLocalStore('mutationSort', e.currentTarget.value)
710
+ }}
711
+ >
712
+ {Object.keys(mutationSortFns).map((key) => (
713
+ <option value={key}>Sort by {key}</option>
714
+ ))}
715
+ </select>
716
+ </Show>
548
717
  <ChevronDown />
549
718
  </div>
550
719
  <button
551
720
  onClick={() => {
552
- props.setLocalStore('sortOrder', String(sortOrder() * -1))
721
+ if (selectedView() === 'queries') {
722
+ props.setLocalStore('sortOrder', String(sortOrder() * -1))
723
+ } else {
724
+ props.setLocalStore(
725
+ 'mutationSortOrder',
726
+ String(mutationSortOrder() * -1),
727
+ )
728
+ }
553
729
  }}
554
730
  aria-label={`Sort order ${
555
- sortOrder() === -1 ? 'descending' : 'ascending'
731
+ (selectedView() === 'queries'
732
+ ? sortOrder()
733
+ : mutationSortOrder()) === -1
734
+ ? 'descending'
735
+ : 'ascending'
556
736
  }`}
557
- aria-pressed={sortOrder() === -1}
737
+ aria-pressed={
738
+ (selectedView() === 'queries'
739
+ ? sortOrder()
740
+ : mutationSortOrder()) === -1
741
+ }
558
742
  class="tsqd-query-filter-sort-order-btn"
559
743
  >
560
- <Show when={sortOrder() === 1}>
744
+ <Show
745
+ when={
746
+ (selectedView() === 'queries'
747
+ ? sortOrder()
748
+ : mutationSortOrder()) === 1
749
+ }
750
+ >
561
751
  <span>Asc</span>
562
752
  <ArrowUp />
563
753
  </Show>
564
- <Show when={sortOrder() === -1}>
754
+ <Show
755
+ when={
756
+ (selectedView() === 'queries'
757
+ ? sortOrder()
758
+ : mutationSortOrder()) === -1
759
+ }
760
+ >
565
761
  <span>Desc</span>
566
762
  <ArrowDown />
567
763
  </Show>
@@ -571,7 +767,11 @@ export const DevtoolsPanel: Component<DevtoolsPanelProps> = (props) => {
571
767
  <div class={cx(styles().actionsContainer, 'tsqd-actions-container')}>
572
768
  <button
573
769
  onClick={() => {
574
- cache().clear()
770
+ if (selectedView() === 'queries') {
771
+ query_cache().clear()
772
+ } else {
773
+ mutation_cache().clear()
774
+ }
575
775
  }}
576
776
  class={cx(
577
777
  styles().actionsBtn,
@@ -579,7 +779,7 @@ export const DevtoolsPanel: Component<DevtoolsPanelProps> = (props) => {
579
779
  'tsqd-action-clear-cache',
580
780
  )}
581
781
  aria-label="Clear query cache"
582
- title="Clear query cache"
782
+ title={`Clear ${selectedView()} cache`}
583
783
  >
584
784
  <Trash />
585
785
  </button>
@@ -624,7 +824,9 @@ export const DevtoolsPanel: Component<DevtoolsPanelProps> = (props) => {
624
824
  >
625
825
  <Settings />
626
826
  </DropdownMenu.Trigger>
627
- <DropdownMenu.Portal>
827
+ <DropdownMenu.Portal
828
+ ref={(el) => setComputedVariables(el as HTMLDivElement)}
829
+ >
628
830
  <DropdownMenu.Content
629
831
  class={cx(styles().settingsMenu, 'tsqd-settings-menu')}
630
832
  >
@@ -647,7 +849,9 @@ export const DevtoolsPanel: Component<DevtoolsPanelProps> = (props) => {
647
849
  <span>Position</span>
648
850
  <ChevronDown />
649
851
  </DropdownMenu.SubTrigger>
650
- <DropdownMenu.Portal>
852
+ <DropdownMenu.Portal
853
+ ref={(el) => setComputedVariables(el as HTMLDivElement)}
854
+ >
651
855
  <DropdownMenu.SubContent
652
856
  class={cx(
653
857
  styles().settingsMenu,
@@ -724,7 +928,9 @@ export const DevtoolsPanel: Component<DevtoolsPanelProps> = (props) => {
724
928
  <span>Theme</span>
725
929
  <ChevronDown />
726
930
  </DropdownMenu.SubTrigger>
727
- <DropdownMenu.Portal>
931
+ <DropdownMenu.Portal
932
+ ref={(el) => setComputedVariables(el as HTMLDivElement)}
933
+ >
728
934
  <DropdownMenu.SubContent
729
935
  class={cx(
730
936
  styles().settingsMenu,
@@ -787,23 +993,43 @@ export const DevtoolsPanel: Component<DevtoolsPanelProps> = (props) => {
787
993
  </DropdownMenu.Root>
788
994
  </div>
789
995
  </div>
790
- <div
791
- class={cx(
792
- styles().overflowQueryContainer,
793
- 'tsqd-queries-overflow-container',
794
- )}
795
- >
796
- <div class="tsqd-queries-container">
797
- <Key by={(q) => q.queryHash} each={queries()}>
798
- {(query) => <QueryRow query={query()} />}
799
- </Key>
996
+ <Show when={selectedView() === 'queries'}>
997
+ <div
998
+ class={cx(
999
+ styles().overflowQueryContainer,
1000
+ 'tsqd-queries-overflow-container',
1001
+ )}
1002
+ >
1003
+ <div class="tsqd-queries-container">
1004
+ <Key by={(q) => q.queryHash} each={queries()}>
1005
+ {(query) => <QueryRow query={query()} />}
1006
+ </Key>
1007
+ </div>
800
1008
  </div>
801
- </div>
1009
+ </Show>
1010
+ <Show when={selectedView() === 'mutations'}>
1011
+ <div
1012
+ class={cx(
1013
+ styles().overflowQueryContainer,
1014
+ 'tsqd-mutations-overflow-container',
1015
+ )}
1016
+ >
1017
+ <div class="tsqd-mutations-container">
1018
+ <Key by={(m) => m.mutationId} each={mutations()}>
1019
+ {(mutation) => <MutationRow mutation={mutation()} />}
1020
+ </Key>
1021
+ </div>
1022
+ </div>
1023
+ </Show>
802
1024
  </div>
803
- <Show when={selectedQueryHash()}>
1025
+ <Show when={selectedView() === 'queries' && selectedQueryHash()}>
804
1026
  <QueryDetails />
805
1027
  </Show>
806
- </aside>
1028
+
1029
+ <Show when={selectedView() === 'mutations' && selectedMutationId()}>
1030
+ <MutationDetails />
1031
+ </Show>
1032
+ </>
807
1033
  )
808
1034
  }
809
1035
 
@@ -907,6 +1133,114 @@ export const QueryRow: Component<{ query: Query }> = (props) => {
907
1133
  )
908
1134
  }
909
1135
 
1136
+ export const MutationRow: Component<{ mutation: Mutation }> = (props) => {
1137
+ const theme = useTheme()
1138
+ const styles = createMemo(() => {
1139
+ return theme() === 'dark' ? darkStyles : lightStyles
1140
+ })
1141
+
1142
+ const { colors, alpha } = tokens
1143
+ const t = (light: string, dark: string) => (theme() === 'dark' ? dark : light)
1144
+
1145
+ const mutationState = createSubscribeToMutationCacheBatcher(
1146
+ (mutationCache) => {
1147
+ const mutations = mutationCache().getAll()
1148
+ const mutation = mutations.find(
1149
+ (m) => m.mutationId === props.mutation.mutationId,
1150
+ )
1151
+ return mutation?.state
1152
+ },
1153
+ )
1154
+
1155
+ const isPaused = createSubscribeToMutationCacheBatcher((mutationCache) => {
1156
+ const mutations = mutationCache().getAll()
1157
+ const mutation = mutations.find(
1158
+ (m) => m.mutationId === props.mutation.mutationId,
1159
+ )
1160
+ if (!mutation) return false
1161
+ return mutation.state.isPaused
1162
+ })
1163
+
1164
+ const status = createSubscribeToMutationCacheBatcher((mutationCache) => {
1165
+ const mutations = mutationCache().getAll()
1166
+ const mutation = mutations.find(
1167
+ (m) => m.mutationId === props.mutation.mutationId,
1168
+ )
1169
+ if (!mutation) return 'idle'
1170
+ return mutation.state.status
1171
+ })
1172
+
1173
+ const color = createMemo(() =>
1174
+ getMutationStatusColor({
1175
+ isPaused: isPaused(),
1176
+ status: status(),
1177
+ }),
1178
+ )
1179
+
1180
+ const getObserverCountColorStyles = () => {
1181
+ if (color() === 'gray') {
1182
+ return css`
1183
+ background-color: ${t(colors[color()][200], colors[color()][700])};
1184
+ color: ${t(colors[color()][700], colors[color()][300])};
1185
+ `
1186
+ }
1187
+
1188
+ return css`
1189
+ background-color: ${t(
1190
+ colors[color()][200] + alpha[80],
1191
+ colors[color()][900],
1192
+ )};
1193
+ color: ${t(colors[color()][800], colors[color()][300])};
1194
+ `
1195
+ }
1196
+
1197
+ return (
1198
+ <Show when={mutationState()}>
1199
+ <button
1200
+ onClick={() => {
1201
+ setSelectedMutationId(
1202
+ props.mutation.mutationId === selectedMutationId()
1203
+ ? null
1204
+ : props.mutation.mutationId,
1205
+ )
1206
+ }}
1207
+ class={cx(
1208
+ styles().queryRow,
1209
+ selectedMutationId() === props.mutation.mutationId &&
1210
+ styles().selectedQueryRow,
1211
+ 'tsqd-query-row',
1212
+ )}
1213
+ aria-label={`Mutation submitted at ${new Date(
1214
+ props.mutation.state.submittedAt,
1215
+ ).toLocaleString()}`}
1216
+ >
1217
+ <div
1218
+ class={cx(getObserverCountColorStyles(), 'tsqd-query-observer-count')}
1219
+ >
1220
+ <Show when={color() === 'purple'}>
1221
+ <PauseCircle />
1222
+ </Show>
1223
+ <Show when={color() === 'green'}>
1224
+ <CheckCircle />
1225
+ </Show>
1226
+ <Show when={color() === 'red'}>
1227
+ <XCircle />
1228
+ </Show>
1229
+ <Show when={color() === 'yellow'}>
1230
+ <LoadingCircle />
1231
+ </Show>
1232
+ </div>
1233
+ <code class="tsqd-query-hash">
1234
+ <Show when={props.mutation.options.mutationKey}>
1235
+ {JSON.stringify(props.mutation.options.mutationKey)} -{' '}
1236
+ </Show>
1237
+ {new Date(props.mutation.state.submittedAt).toLocaleString()}
1238
+ </code>
1239
+ </button>
1240
+ </Show>
1241
+ )
1242
+ }
1243
+
910
1244
  export const QueryStatusCount: Component = () => {
911
1245
  const stale = createSubscribeToQueryCacheBatcher(
912
1246
  (queryCache) =>
@@ -961,6 +1295,76 @@ export const QueryStatusCount: Component = () => {
961
1295
  )
962
1296
  }
963
1297
 
1298
+ export const MutationStatusCount: Component = () => {
1299
+ const success = createSubscribeToMutationCacheBatcher(
1300
+ (mutationCache) =>
1301
+ mutationCache()
1302
+ .getAll()
1303
+ .filter(
1304
+ (m) =>
1305
+ getMutationStatusColor({
1306
+ isPaused: m.state.isPaused,
1307
+ status: m.state.status,
1308
+ }) === 'green',
1309
+ ).length,
1310
+ )
1311
+
1312
+ const pending = createSubscribeToMutationCacheBatcher(
1313
+ (mutationCache) =>
1314
+ mutationCache()
1315
+ .getAll()
1316
+ .filter(
1317
+ (m) =>
1318
+ getMutationStatusColor({
1319
+ isPaused: m.state.isPaused,
1320
+ status: m.state.status,
1321
+ }) === 'yellow',
1322
+ ).length,
1323
+ )
1324
+
1325
+ const paused = createSubscribeToMutationCacheBatcher(
1326
+ (mutationCache) =>
1327
+ mutationCache()
1328
+ .getAll()
1329
+ .filter(
1330
+ (m) =>
1331
+ getMutationStatusColor({
1332
+ isPaused: m.state.isPaused,
1333
+ status: m.state.status,
1334
+ }) === 'purple',
1335
+ ).length,
1336
+ )
1337
+
1338
+ const error = createSubscribeToMutationCacheBatcher(
1339
+ (mutationCache) =>
1340
+ mutationCache()
1341
+ .getAll()
1342
+ .filter(
1343
+ (m) =>
1344
+ getMutationStatusColor({
1345
+ isPaused: m.state.isPaused,
1346
+ status: m.state.status,
1347
+ }) === 'red',
1348
+ ).length,
1349
+ )
1350
+
1351
+ const theme = useTheme()
1352
+ const styles = createMemo(() => {
1353
+ return theme() === 'dark' ? darkStyles : lightStyles
1354
+ })
1355
+
1356
+ return (
1357
+ <div
1358
+ class={cx(styles().queryStatusContainer, 'tsqd-query-status-container')}
1359
+ >
1360
+ <QueryStatus label="Paused" color="purple" count={paused()} />
1361
+ <QueryStatus label="Pending" color="yellow" count={pending()} />
1362
+ <QueryStatus label="Success" color="green" count={success()} />
1363
+ <QueryStatus label="Error" color="red" count={error()} />
1364
+ </div>
1365
+ )
1366
+ }
1367
+
964
1368
  export const QueryStatus: Component<QueryStatusProps> = (props) => {
965
1369
  const theme = useTheme()
966
1370
  const styles = createMemo(() => {
@@ -1430,7 +1834,7 @@ const QueryDetails = () => {
1430
1834
  </div>
1431
1835
  <div
1432
1836
  style={{
1433
- padding: '0.5rem',
1837
+ padding: tokens.size[2],
1434
1838
  }}
1435
1839
  class="tsqd-query-details-explorer-container tsqd-query-details-data-explorer"
1436
1840
  >
@@ -1447,7 +1851,7 @@ const QueryDetails = () => {
1447
1851
  </div>
1448
1852
  <div
1449
1853
  style={{
1450
- padding: '0.5rem',
1854
+ padding: tokens.size[2],
1451
1855
  }}
1452
1856
  class="tsqd-query-details-explorer-container tsqd-query-details-query-explorer"
1453
1857
  >
@@ -1462,7 +1866,170 @@ const QueryDetails = () => {
1462
1866
  )
1463
1867
  }
1464
1868
 
1465
- const signalsMap = new Map<(q: Accessor<QueryCache>) => any, Setter<any>>()
1869
+ const MutationDetails = () => {
1870
+ const theme = useTheme()
1871
+ const styles = createMemo(() => {
1872
+ return theme() === 'dark' ? darkStyles : lightStyles
1873
+ })
1874
+
1875
+ const { colors } = tokens
1876
+ const t = (light: string, dark: string) => (theme() === 'dark' ? dark : light)
1877
+
1878
+ const isPaused = createSubscribeToMutationCacheBatcher((mutationCache) => {
1879
+ const mutations = mutationCache().getAll()
1880
+ const mutation = mutations.find(
1881
+ (m) => m.mutationId === selectedMutationId(),
1882
+ )
1883
+ if (!mutation) return false
1884
+ return mutation.state.isPaused
1885
+ })
1886
+
1887
+ const status = createSubscribeToMutationCacheBatcher((mutationCache) => {
1888
+ const mutations = mutationCache().getAll()
1889
+ const mutation = mutations.find(
1890
+ (m) => m.mutationId === selectedMutationId(),
1891
+ )
1892
+ if (!mutation) return 'idle'
1893
+ return mutation.state.status
1894
+ })
1895
+
1896
+ const color = createMemo(() =>
1897
+ getMutationStatusColor({
1898
+ isPaused: isPaused(),
1899
+ status: status(),
1900
+ }),
1901
+ )
1902
+
1903
+ const activeMutation = createSubscribeToMutationCacheBatcher(
1904
+ (mutationCache) =>
1905
+ mutationCache()
1906
+ .getAll()
1907
+ .find((mutation) => mutation.mutationId === selectedMutationId()),
1908
+ false,
1909
+ )
1910
+
1911
+ const getQueryStatusColors = () => {
1912
+ if (color() === 'gray') {
1913
+ return css`
1914
+ background-color: ${t(colors[color()][200], colors[color()][700])};
1915
+ color: ${t(colors[color()][700], colors[color()][300])};
1916
+ border-color: ${t(colors[color()][400], colors[color()][600])};
1917
+ `
1918
+ }
1919
+ return css`
1920
+ background-color: ${t(colors[color()][100], colors[color()][900])};
1921
+ color: ${t(colors[color()][700], colors[color()][300])};
1922
+ border-color: ${t(colors[color()][400], colors[color()][600])};
1923
+ `
1924
+ }
1925
+
1926
+ return (
1927
+ <Show when={activeMutation()}>
1928
+ <div
1929
+ class={cx(styles().detailsContainer, 'tsqd-query-details-container')}
1930
+ >
1931
+ <div class={cx(styles().detailsHeader, 'tsqd-query-details-header')}>
1932
+ Mutation Details
1933
+ </div>
1934
+ <div
1935
+ class={cx(
1936
+ styles().detailsBody,
1937
+ 'tsqd-query-details-summary-container',
1938
+ )}
1939
+ >
1940
+ <div class="tsqd-query-details-summary">
1941
+ <pre>
1942
+ <code>
1943
+ <Show
1944
+ when={activeMutation()!.options.mutationKey}
1945
+ fallback={'No mutationKey found'}
1946
+ >
1947
+ {displayValue(activeMutation()!.options.mutationKey, true)}
1948
+ </Show>
1949
+ </code>
1950
+ </pre>
1951
+ <span
1952
+ class={cx(styles().queryDetailsStatus, getQueryStatusColors())}
1953
+ >
1954
+ <Show when={color() === 'purple'}>pending</Show>
1955
+ <Show when={color() !== 'purple'}>{status()}</Show>
1956
+ </span>
1957
+ </div>
1958
+ <div class="tsqd-query-details-last-updated">
1959
+ <span>Submitted At:</span>
1960
+ <span>
1961
+ {new Date(
1962
+ activeMutation()!.state.submittedAt,
1963
+ ).toLocaleTimeString()}
1964
+ </span>
1965
+ </div>
1966
+ </div>
1967
+ <div class={cx(styles().detailsHeader, 'tsqd-query-details-header')}>
1968
+ Variables Details
1969
+ </div>
1970
+ <div
1971
+ style={{
1972
+ padding: tokens.size[2],
1973
+ }}
1974
+ class="tsqd-query-details-explorer-container tsqd-query-details-query-explorer"
1975
+ >
1976
+ <Explorer
1977
+ label="Variables"
1978
+ defaultExpanded={['Variables']}
1979
+ value={activeMutation()!.state.variables}
1980
+ />
1981
+ </div>
1982
+ <div class={cx(styles().detailsHeader, 'tsqd-query-details-header')}>
1983
+ Context Details
1984
+ </div>
1985
+ <div
1986
+ style={{
1987
+ padding: tokens.size[2],
1988
+ }}
1989
+ class="tsqd-query-details-explorer-container tsqd-query-details-query-explorer"
1990
+ >
1991
+ <Explorer
1992
+ label="Context"
1993
+ defaultExpanded={['Context']}
1994
+ value={activeMutation()!.state.context}
1995
+ />
1996
+ </div>
1997
+ <div class={cx(styles().detailsHeader, 'tsqd-query-details-header')}>
1998
+ Data Explorer
1999
+ </div>
2000
+ <div
2001
+ style={{
2002
+ padding: tokens.size[2],
2003
+ }}
2004
+ class="tsqd-query-details-explorer-container tsqd-query-details-query-explorer"
2005
+ >
2006
+ <Explorer
2007
+ label="Data"
2008
+ defaultExpanded={['Data']}
2009
+ value={activeMutation()!.state.data}
2010
+ />
2011
+ </div>
2012
+ <div class={cx(styles().detailsHeader, 'tsqd-query-details-header')}>
2013
+ Mutations Explorer
2014
+ </div>
2015
+ <div
2016
+ style={{
2017
+ padding: tokens.size[2],
2018
+ }}
2019
+ class="tsqd-query-details-explorer-container tsqd-query-details-query-explorer"
2020
+ >
2021
+ <Explorer
2022
+ label="Mutation"
2023
+ defaultExpanded={['Mutation']}
2024
+ value={activeMutation()}
2025
+ />
2026
+ </div>
2027
+ </div>
2028
+ </Show>
2029
+ )
2030
+ }
2031
+
2032
+ const queryCacheMap = new Map<(q: Accessor<QueryCache>) => any, Setter<any>>()
1466
2033
 
1467
2034
  const setupQueryCacheSubscription = () => {
1468
2035
  const queryCache = createMemo(() => {
@@ -1471,7 +2038,7 @@ const setupQueryCacheSubscription = () => {
1471
2038
  })
1472
2039
 
1473
2040
  const unsub = queryCache().subscribe(() => {
1474
- for (const [callback, setter] of signalsMap.entries()) {
2041
+ for (const [callback, setter] of queryCacheMap.entries()) {
1475
2042
  queueMicrotask(() => {
1476
2043
  setter(callback(queryCache))
1477
2044
  })
@@ -1479,7 +2046,7 @@ const setupQueryCacheSubscription = () => {
1479
2046
  })
1480
2047
 
1481
2048
  onCleanup(() => {
1482
- signalsMap.clear()
2049
+ queryCacheMap.clear()
1483
2050
  unsub()
1484
2051
  })
1485
2052
 
@@ -1505,11 +2072,67 @@ const createSubscribeToQueryCacheBatcher = <T,>(
1505
2072
  })
1506
2073
 
1507
2074
  // @ts-ignore
1508
- signalsMap.set(callback, setValue)
2075
+ queryCacheMap.set(callback, setValue)
1509
2076
 
1510
2077
  onCleanup(() => {
1511
2078
  // @ts-ignore
1512
- signalsMap.delete(callback)
2079
+ queryCacheMap.delete(callback)
2080
+ })
2081
+
2082
+ return value
2083
+ }
2084
+
2085
+ const mutationCacheMap = new Map<
2086
+ (q: Accessor<MutationCache>) => any,
2087
+ Setter<any>
2088
+ >()
2089
+
2090
+ const setupMutationCacheSubscription = () => {
2091
+ const mutationCache = createMemo(() => {
2092
+ const client = useQueryDevtoolsContext().client
2093
+ return client.getMutationCache()
2094
+ })
2095
+
2096
+ const unsub = mutationCache().subscribe(() => {
2097
+ for (const [callback, setter] of mutationCacheMap.entries()) {
2098
+ queueMicrotask(() => {
2099
+ setter(callback(mutationCache))
2100
+ })
2101
+ }
2102
+ })
2103
+
2104
+ onCleanup(() => {
2105
+ mutationCacheMap.clear()
2106
+ unsub()
2107
+ })
2108
+
2109
+ return unsub
2110
+ }
2111
+
2112
+ const createSubscribeToMutationCacheBatcher = <T,>(
2113
+ callback: (queryCache: Accessor<MutationCache>) => Exclude<T, Function>,
2114
+ equalityCheck: boolean = true,
2115
+ ) => {
2116
+ const mutationCache = createMemo(() => {
2117
+ const client = useQueryDevtoolsContext().client
2118
+ return client.getMutationCache()
2119
+ })
2120
+
2121
+ const [value, setValue] = createSignal<T>(
2122
+ callback(mutationCache),
2123
+ !equalityCheck ? { equals: false } : undefined,
2124
+ )
2125
+
2126
+ createEffect(() => {
2127
+ setValue(callback(mutationCache))
2128
+ })
2129
+
2130
+ // @ts-ignore
2131
+ mutationCacheMap.set(callback, setValue)
2132
+
2133
+ onCleanup(() => {
2134
+ // @ts-ignore
2135
+ mutationCacheMap.delete(callback)
1513
2136
  })
1514
2137
 
1515
2138
  return value
@@ -1606,7 +2229,7 @@ const stylesFactory = (theme: 'light' | 'dark') => {
1606
2229
  right: 0;
1607
2230
  left: 0;
1608
2231
  max-height: 90%;
1609
- min-height: 3.5rem;
2232
+ min-height: ${size[14]};
1610
2233
  border-bottom: ${t(colors.gray[400], colors.darkGray[300])} 1px solid;
1611
2234
  `,
1612
2235
  'panel-position-bottom': css`
@@ -1614,7 +2237,7 @@ const stylesFactory = (theme: 'light' | 'dark') => {
1614
2237
  right: 0;
1615
2238
  left: 0;
1616
2239
  max-height: 90%;
1617
- min-height: 3.5rem;
2240
+ min-height: ${size[14]};
1618
2241
  border-top: ${t(colors.gray[400], colors.darkGray[300])} 1px solid;
1619
2242
  `,
1620
2243
  'panel-position-right': css`
@@ -1788,7 +2411,7 @@ const stylesFactory = (theme: 'light' | 'dark') => {
1788
2411
  justify-content: space-between;
1789
2412
  align-items: center;
1790
2413
  padding: ${tokens.size[2]} ${tokens.size[2.5]};
1791
- gap: ${tokens.size[3]};
2414
+ gap: ${tokens.size[2.5]};
1792
2415
  border-bottom: ${t(colors.gray[300], colors.darkGray[500])} 1px solid;
1793
2416
  align-items: center;
1794
2417
  & > button {
@@ -1800,8 +2423,19 @@ const stylesFactory = (theme: 'light' | 'dark') => {
1800
2423
  flex-direction: column;
1801
2424
  }
1802
2425
  `,
2426
+ logoAndToggleContainer: css`
2427
+ display: flex;
2428
+ gap: ${tokens.size[3]};
2429
+ align-items: center;
2430
+ `,
1803
2431
  logo: css`
1804
2432
  cursor: pointer;
2433
+ display: flex;
2434
+ flex-direction: column;
2435
+ background-color: transparent;
2436
+ border: none;
2437
+ gap: ${tokens.size[0.5]};
2438
+ padding: 0px;
1805
2439
  &:hover {
1806
2440
  opacity: 0.7;
1807
2441
  }
@@ -1935,6 +2569,8 @@ const stylesFactory = (theme: 'light' | 'dark') => {
1935
2569
  outline: 2px solid ${colors.blue[800]};
1936
2570
  }
1937
2571
  & svg {
2572
+ width: ${tokens.size[3]};
2573
+ height: ${tokens.size[3]};
1938
2574
  color: ${t(colors.gray[500], colors.gray[400])};
1939
2575
  }
1940
2576
  }
@@ -2020,8 +2656,8 @@ const stylesFactory = (theme: 'light' | 'dark') => {
2020
2656
  border-radius: ${tokens.border.radius.sm};
2021
2657
  background-color: ${t(colors.gray[100], colors.darkGray[400])};
2022
2658
  border: 1px solid ${t(colors.gray[300], colors.darkGray[200])};
2023
- width: 1.625rem;
2024
- height: 1.625rem;
2659
+ width: ${tokens.size[6.5]};
2660
+ height: ${tokens.size[6.5]};
2025
2661
  justify-content: center;
2026
2662
  display: flex;
2027
2663
  align-items: center;
@@ -2173,6 +2809,8 @@ const stylesFactory = (theme: 'light' | 'dark') => {
2173
2809
 
2174
2810
  & pre {
2175
2811
  margin: 0;
2812
+ display: flex;
2813
+ align-items: center;
2176
2814
  }
2177
2815
  `,
2178
2816
  queryDetailsStatus: css`
@@ -2353,6 +2991,56 @@ const stylesFactory = (theme: 'light' | 'dark') => {
2353
2991
  background-color: ${t(colors.purple[100], colors.purple[900])};
2354
2992
  }
2355
2993
  `,
2994
+ viewToggle: css`
2995
+ border-radius: ${tokens.border.radius.sm};
2996
+ background-color: ${t(colors.gray[200], colors.darkGray[600])};
2997
+ border: 1px solid ${t(colors.gray[300], colors.darkGray[200])};
2998
+ display: flex;
2999
+ padding: 0;
3000
+ font-size: ${font.size.xs};
3001
+ color: ${t(colors.gray[700], colors.gray[300])};
3002
+ overflow: hidden;
3003
+
3004
+ &:has(:focus-visible) {
3005
+ outline: 2px solid ${colors.blue[800]};
3006
+ }
3007
+
3008
+ & .tsqd-radio-toggle {
3009
+ opacity: 0.5;
3010
+ display: flex;
3011
+ & label {
3012
+ display: flex;
3013
+ align-items: center;
3014
+ cursor: pointer;
3015
+ line-height: ${font.lineHeight.md};
3016
+ }
3017
+
3018
+ & label:hover {
3019
+ background-color: ${t(colors.gray[100], colors.darkGray[500])};
3020
+ }
3021
+ }
3022
+
3023
+ & > [data-checked] {
3024
+ opacity: 1;
3025
+ background-color: ${t(colors.gray[100], colors.darkGray[400])};
3026
+ & label:hover {
3027
+ background-color: ${t(colors.gray[100], colors.darkGray[400])};
3028
+ }
3029
+ }
3030
+
3031
+ & .tsqd-radio-toggle:first-child {
3032
+ & label {
3033
+ padding: 0 ${tokens.size[1.5]} 0 ${tokens.size[2]};
3034
+ }
3035
+ border-right: 1px solid ${t(colors.gray[300], colors.darkGray[200])};
3036
+ }
3037
+
3038
+ & .tsqd-radio-toggle:nth-child(2) {
3039
+ & label {
3040
+ padding: 0 ${tokens.size[2]} 0 ${tokens.size[1.5]};
3041
+ }
3042
+ }
3043
+ `,
2356
3044
  }
2357
3045
  }
2358
3046