@tanstack/query-devtools 5.0.5 → 5.1.0

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) => {
@@ -234,13 +250,6 @@ export const DevtoolsPanel: Component<DevtoolsPanelProps> = (props) => {
234
250
 
235
251
  const [isResizing, setIsResizing] = createSignal(false)
236
252
 
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
253
  const position = createMemo(
245
254
  () =>
246
255
  (props.localStore.position ||
@@ -248,41 +257,6 @@ export const DevtoolsPanel: Component<DevtoolsPanelProps> = (props) => {
248
257
  POSITION) as DevtoolsPosition,
249
258
  )
250
259
 
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
260
  const handleDragStart: JSX.EventHandler<HTMLDivElement, MouseEvent> = (
287
261
  event,
288
262
  ) => {
@@ -344,9 +318,6 @@ export const DevtoolsPanel: Component<DevtoolsPanelProps> = (props) => {
344
318
  document.addEventListener('mouseup', unsub, false)
345
319
  }
346
320
 
347
- setupQueryCacheSubscription()
348
-
349
- let queriesContainerRef!: HTMLDivElement
350
321
  let panelRef!: HTMLDivElement
351
322
 
352
323
  onMount(() => {
@@ -357,10 +328,6 @@ export const DevtoolsPanel: Component<DevtoolsPanelProps> = (props) => {
357
328
  })
358
329
  })
359
330
 
360
- const setDevtoolsPosition = (pos: DevtoolsPosition) => {
361
- props.setLocalStore('position', pos)
362
- }
363
-
364
331
  createEffect(() => {
365
332
  const rootContainer = panelRef.parentElement?.parentElement?.parentElement
366
333
  if (!rootContainer) return
@@ -465,14 +432,136 @@ export const DevtoolsPanel: Component<DevtoolsPanelProps> = (props) => {
465
432
  >
466
433
  <ChevronDown />
467
434
  </button>
435
+ <ContentView
436
+ localStore={props.localStore}
437
+ setLocalStore={props.setLocalStore}
438
+ />
439
+ </aside>
440
+ )
441
+ }
442
+
443
+ const ContentView: Component<DevtoolsPanelProps> = (props) => {
444
+ setupQueryCacheSubscription()
445
+ setupMutationCacheSubscription()
446
+
447
+ const theme = useTheme()
448
+ const styles = createMemo(() => {
449
+ return theme() === 'dark' ? darkStyles : lightStyles
450
+ })
451
+
452
+ const [selectedView, setSelectedView] = createSignal<'queries' | 'mutations'>(
453
+ 'queries',
454
+ )
455
+
456
+ const sort = createMemo(() => props.localStore.sort || DEFAULT_SORT_FN_NAME)
457
+ const sortOrder = createMemo(
458
+ () => Number(props.localStore.sortOrder) || DEFAULT_SORT_ORDER,
459
+ ) as () => 1 | -1
460
+
461
+ const mutationSort = createMemo(
462
+ () => props.localStore.mutationSort || DEFAULT_MUTATION_SORT_FN_NAME,
463
+ )
464
+ const mutationSortOrder = createMemo(
465
+ () => Number(props.localStore.mutationSortOrder) || DEFAULT_SORT_ORDER,
466
+ ) as () => 1 | -1
467
+
468
+ const [offline, setOffline] = createSignal(false)
469
+
470
+ const sortFn = createMemo(() => sortFns[sort() as string])
471
+ const mutationSortFn = createMemo(
472
+ () => mutationSortFns[mutationSort() as string],
473
+ )
474
+
475
+ const onlineManager = createMemo(
476
+ () => useQueryDevtoolsContext().onlineManager,
477
+ )
478
+
479
+ const query_cache = createMemo(() => {
480
+ return useQueryDevtoolsContext().client.getQueryCache()
481
+ })
482
+
483
+ const mutation_cache = createMemo(() => {
484
+ return useQueryDevtoolsContext().client.getMutationCache()
485
+ })
486
+
487
+ const queryCount = createSubscribeToQueryCacheBatcher((queryCache) => {
488
+ return queryCache().getAll().length
489
+ }, false)
490
+
491
+ const queries = createMemo(
492
+ on(
493
+ () => [queryCount(), props.localStore.filter, sort(), sortOrder()],
494
+ () => {
495
+ const curr = query_cache().getAll()
496
+
497
+ const filtered = props.localStore.filter
498
+ ? curr.filter(
499
+ (item) =>
500
+ rankItem(item.queryHash, props.localStore.filter || '').passed,
501
+ )
502
+ : [...curr]
503
+
504
+ const sorted = sortFn()
505
+ ? filtered.sort((a, b) => sortFn()!(a, b) * sortOrder())
506
+ : filtered
507
+ return sorted
508
+ },
509
+ ),
510
+ )
511
+
512
+ const mutationCount = createSubscribeToMutationCacheBatcher(
513
+ (mutationCache) => {
514
+ return mutationCache().getAll().length
515
+ },
516
+ false,
517
+ )
518
+
519
+ const mutations = createMemo(
520
+ on(
521
+ () => [
522
+ mutationCount(),
523
+ props.localStore.mutationFilter,
524
+ mutationSort(),
525
+ mutationSortOrder(),
526
+ ],
527
+ () => {
528
+ const curr = mutation_cache().getAll()
529
+
530
+ const filtered = props.localStore.mutationFilter
531
+ ? curr.filter((item) => {
532
+ const value = `${
533
+ item.options.mutationKey
534
+ ? JSON.stringify(item.options.mutationKey) + ' - '
535
+ : ''
536
+ }${new Date(item.state.submittedAt).toLocaleString()}`
537
+ return rankItem(value, props.localStore.mutationFilter || '')
538
+ .passed
539
+ })
540
+ : [...curr]
541
+
542
+ const sorted = mutationSortFn()
543
+ ? filtered.sort(
544
+ (a, b) => mutationSortFn()!(a, b) * mutationSortOrder(),
545
+ )
546
+ : filtered
547
+ return sorted
548
+ },
549
+ ),
550
+ )
551
+
552
+ const setDevtoolsPosition = (pos: DevtoolsPosition) => {
553
+ props.setLocalStore('position', pos)
554
+ }
555
+
556
+ return (
557
+ <>
468
558
  <div
469
- ref={queriesContainerRef}
470
559
  // When the panels are stacked we use the height style
471
560
  // to divide the panels into two equal parts
472
561
  class={cx(
473
562
  styles().queriesContainer,
474
563
  panelWidth() < secondBreakpoint &&
475
- selectedQueryHash() &&
564
+ (selectedQueryHash() || selectedMutationId()) &&
476
565
  css`
477
566
  height: 50%;
478
567
  max-height: 50%;
@@ -481,35 +570,65 @@ export const DevtoolsPanel: Component<DevtoolsPanelProps> = (props) => {
481
570
  )}
482
571
  >
483
572
  <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
- )}
573
+ <div class={styles().logoAndToggleContainer}>
574
+ <button
575
+ class={cx(styles().logo, 'tsqd-text-logo-container')}
576
+ onClick={() => props.setLocalStore('open', 'false')}
577
+ aria-label="Close Tanstack query devtools"
497
578
  >
498
- {useQueryDevtoolsContext().queryFlavor} v
499
- {useQueryDevtoolsContext().version}
500
- </span>
501
- </button>
502
- <QueryStatusCount />
579
+ <span
580
+ class={cx(styles().tanstackLogo, 'tsqd-text-logo-tanstack')}
581
+ >
582
+ TANSTACK
583
+ </span>
584
+ <span
585
+ class={cx(
586
+ styles().queryFlavorLogo,
587
+ 'tsqd-text-logo-query-flavor',
588
+ )}
589
+ >
590
+ {useQueryDevtoolsContext().queryFlavor} v
591
+ {useQueryDevtoolsContext().version}
592
+ </span>
593
+ </button>
594
+ <RadioGroup.Root
595
+ class={cx(styles().viewToggle)}
596
+ value={selectedView()}
597
+ onChange={(value) => {
598
+ setSelectedView(value as 'queries' | 'mutations')
599
+ setSelectedQueryHash(null)
600
+ setSelectedMutationId(null)
601
+ }}
602
+ >
603
+ <RadioGroup.Item value="queries" class="tsqd-radio-toggle">
604
+ <RadioGroup.ItemInput />
605
+ <RadioGroup.ItemControl>
606
+ <RadioGroup.ItemIndicator />
607
+ </RadioGroup.ItemControl>
608
+ <RadioGroup.ItemLabel title="Toggle Queries View">
609
+ Queries
610
+ </RadioGroup.ItemLabel>
611
+ </RadioGroup.Item>
612
+ <RadioGroup.Item value="mutations" class="tsqd-radio-toggle">
613
+ <RadioGroup.ItemInput />
614
+ <RadioGroup.ItemControl>
615
+ <RadioGroup.ItemIndicator />
616
+ </RadioGroup.ItemControl>
617
+ <RadioGroup.ItemLabel title="Toggle Mutations View">
618
+ Mutations
619
+ </RadioGroup.ItemLabel>
620
+ </RadioGroup.Item>
621
+ </RadioGroup.Root>
622
+ </div>
623
+
624
+ <Show when={selectedView() === 'queries'}>
625
+ <QueryStatusCount />
626
+ </Show>
627
+ <Show when={selectedView() === 'mutations'}>
628
+ <MutationStatusCount />
629
+ </Show>
503
630
  </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
- >
631
+ <div class={cx(styles().row, 'tsqd-filters-actions-container')}>
513
632
  <div class={cx(styles().filtersContainer, 'tsqd-filters-container')}>
514
633
  <div
515
634
  class={cx(
@@ -522,11 +641,19 @@ export const DevtoolsPanel: Component<DevtoolsPanelProps> = (props) => {
522
641
  aria-label="Filter queries by query key"
523
642
  type="text"
524
643
  placeholder="Filter"
525
- onInput={(e) =>
526
- props.setLocalStore('filter', e.currentTarget.value)
527
- }
644
+ onInput={(e) => {
645
+ if (selectedView() === 'queries') {
646
+ props.setLocalStore('filter', e.currentTarget.value)
647
+ } else {
648
+ props.setLocalStore('mutationFilter', e.currentTarget.value)
649
+ }
650
+ }}
528
651
  class="tsqd-query-filter-textfield"
529
- value={props.localStore.filter || ''}
652
+ value={
653
+ selectedView() === 'queries'
654
+ ? props.localStore.filter || ''
655
+ : props.localStore.mutationFilter || ''
656
+ }
530
657
  />
531
658
  </div>
532
659
  <div
@@ -535,33 +662,74 @@ export const DevtoolsPanel: Component<DevtoolsPanelProps> = (props) => {
535
662
  'tsqd-query-filter-sort-container',
536
663
  )}
537
664
  >
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>
665
+ <Show when={selectedView() === 'queries'}>
666
+ <select
667
+ value={sort()}
668
+ onChange={(e) => {
669
+ props.setLocalStore('sort', e.currentTarget.value)
670
+ }}
671
+ >
672
+ {Object.keys(sortFns).map((key) => (
673
+ <option value={key}>Sort by {key}</option>
674
+ ))}
675
+ </select>
676
+ </Show>
677
+ <Show when={selectedView() === 'mutations'}>
678
+ <select
679
+ value={mutationSort()}
680
+ onChange={(e) => {
681
+ props.setLocalStore('mutationSort', e.currentTarget.value)
682
+ }}
683
+ >
684
+ {Object.keys(mutationSortFns).map((key) => (
685
+ <option value={key}>Sort by {key}</option>
686
+ ))}
687
+ </select>
688
+ </Show>
548
689
  <ChevronDown />
549
690
  </div>
550
691
  <button
551
692
  onClick={() => {
552
- props.setLocalStore('sortOrder', String(sortOrder() * -1))
693
+ if (selectedView() === 'queries') {
694
+ props.setLocalStore('sortOrder', String(sortOrder() * -1))
695
+ } else {
696
+ props.setLocalStore(
697
+ 'mutationSortOrder',
698
+ String(mutationSortOrder() * -1),
699
+ )
700
+ }
553
701
  }}
554
702
  aria-label={`Sort order ${
555
- sortOrder() === -1 ? 'descending' : 'ascending'
703
+ (selectedView() === 'queries'
704
+ ? sortOrder()
705
+ : mutationSortOrder()) === -1
706
+ ? 'descending'
707
+ : 'ascending'
556
708
  }`}
557
- aria-pressed={sortOrder() === -1}
709
+ aria-pressed={
710
+ (selectedView() === 'queries'
711
+ ? sortOrder()
712
+ : mutationSortOrder()) === -1
713
+ }
558
714
  class="tsqd-query-filter-sort-order-btn"
559
715
  >
560
- <Show when={sortOrder() === 1}>
716
+ <Show
717
+ when={
718
+ (selectedView() === 'queries'
719
+ ? sortOrder()
720
+ : mutationSortOrder()) === 1
721
+ }
722
+ >
561
723
  <span>Asc</span>
562
724
  <ArrowUp />
563
725
  </Show>
564
- <Show when={sortOrder() === -1}>
726
+ <Show
727
+ when={
728
+ (selectedView() === 'queries'
729
+ ? sortOrder()
730
+ : mutationSortOrder()) === -1
731
+ }
732
+ >
565
733
  <span>Desc</span>
566
734
  <ArrowDown />
567
735
  </Show>
@@ -571,7 +739,11 @@ export const DevtoolsPanel: Component<DevtoolsPanelProps> = (props) => {
571
739
  <div class={cx(styles().actionsContainer, 'tsqd-actions-container')}>
572
740
  <button
573
741
  onClick={() => {
574
- cache().clear()
742
+ if (selectedView() === 'queries') {
743
+ query_cache().clear()
744
+ } else {
745
+ mutation_cache().clear()
746
+ }
575
747
  }}
576
748
  class={cx(
577
749
  styles().actionsBtn,
@@ -579,7 +751,7 @@ export const DevtoolsPanel: Component<DevtoolsPanelProps> = (props) => {
579
751
  'tsqd-action-clear-cache',
580
752
  )}
581
753
  aria-label="Clear query cache"
582
- title="Clear query cache"
754
+ title={`Clear ${selectedView()} cache`}
583
755
  >
584
756
  <Trash />
585
757
  </button>
@@ -787,23 +959,43 @@ export const DevtoolsPanel: Component<DevtoolsPanelProps> = (props) => {
787
959
  </DropdownMenu.Root>
788
960
  </div>
789
961
  </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>
962
+ <Show when={selectedView() === 'queries'}>
963
+ <div
964
+ class={cx(
965
+ styles().overflowQueryContainer,
966
+ 'tsqd-queries-overflow-container',
967
+ )}
968
+ >
969
+ <div class="tsqd-queries-container">
970
+ <Key by={(q) => q.queryHash} each={queries()}>
971
+ {(query) => <QueryRow query={query()} />}
972
+ </Key>
973
+ </div>
800
974
  </div>
801
- </div>
975
+ </Show>
976
+ <Show when={selectedView() === 'mutations'}>
977
+ <div
978
+ class={cx(
979
+ styles().overflowQueryContainer,
980
+ 'tsqd-mutations-overflow-container',
981
+ )}
982
+ >
983
+ <div class="tsqd-mutations-container">
984
+ <Key by={(m) => m.mutationId} each={mutations()}>
985
+ {(mutation) => <MutationRow mutation={mutation()} />}
986
+ </Key>
987
+ </div>
988
+ </div>
989
+ </Show>
802
990
  </div>
803
- <Show when={selectedQueryHash()}>
991
+ <Show when={selectedView() === 'queries' && selectedQueryHash()}>
804
992
  <QueryDetails />
805
993
  </Show>
806
- </aside>
994
+
995
+ <Show when={selectedView() === 'mutations' && selectedMutationId()}>
996
+ <MutationDetails />
997
+ </Show>
998
+ </>
807
999
  )
808
1000
  }
809
1001
 
@@ -907,6 +1099,114 @@ export const QueryRow: Component<{ query: Query }> = (props) => {
907
1099
  )
908
1100
  }
909
1101
 
1102
+ export const MutationRow: Component<{ mutation: Mutation }> = (props) => {
1103
+ const theme = useTheme()
1104
+ const styles = createMemo(() => {
1105
+ return theme() === 'dark' ? darkStyles : lightStyles
1106
+ })
1107
+
1108
+ const { colors, alpha } = tokens
1109
+ const t = (light: string, dark: string) => (theme() === 'dark' ? dark : light)
1110
+
1111
+ const mutationState = createSubscribeToMutationCacheBatcher(
1112
+ (mutationCache) => {
1113
+ const mutations = mutationCache().getAll()
1114
+ const mutation = mutations.find(
1115
+ (m) => m.mutationId === props.mutation.mutationId,
1116
+ )
1117
+ return mutation?.state
1118
+ },
1119
+ )
1120
+
1121
+ const isPaused = createSubscribeToMutationCacheBatcher((mutationCache) => {
1122
+ const mutations = mutationCache().getAll()
1123
+ const mutation = mutations.find(
1124
+ (m) => m.mutationId === props.mutation.mutationId,
1125
+ )
1126
+ if (!mutation) return false
1127
+ return mutation.state.isPaused
1128
+ })
1129
+
1130
+ const status = createSubscribeToMutationCacheBatcher((mutationCache) => {
1131
+ const mutations = mutationCache().getAll()
1132
+ const mutation = mutations.find(
1133
+ (m) => m.mutationId === props.mutation.mutationId,
1134
+ )
1135
+ if (!mutation) return 'idle'
1136
+ return mutation.state.status
1137
+ })
1138
+
1139
+ const color = createMemo(() =>
1140
+ getMutationStatusColor({
1141
+ isPaused: isPaused(),
1142
+ status: status(),
1143
+ }),
1144
+ )
1145
+
1146
+ const getObserverCountColorStyles = () => {
1147
+ if (color() === 'gray') {
1148
+ return css`
1149
+ background-color: ${t(colors[color()][200], colors[color()][700])};
1150
+ color: ${t(colors[color()][700], colors[color()][300])};
1151
+ `
1152
+ }
1153
+
1154
+ return css`
1155
+ background-color: ${t(
1156
+ colors[color()][200] + alpha[80],
1157
+ colors[color()][900],
1158
+ )};
1159
+ color: ${t(colors[color()][800], colors[color()][300])};
1160
+ `
1161
+ }
1162
+
1163
+ return (
1164
+ <Show when={mutationState()}>
1165
+ <button
1166
+ onClick={() => {
1167
+ setSelectedMutationId(
1168
+ props.mutation.mutationId === selectedMutationId()
1169
+ ? null
1170
+ : props.mutation.mutationId,
1171
+ )
1172
+ }}
1173
+ class={cx(
1174
+ styles().queryRow,
1175
+ selectedMutationId() === props.mutation.mutationId &&
1176
+ styles().selectedQueryRow,
1177
+ 'tsqd-query-row',
1178
+ )}
1179
+ aria-label={`Mutation submitted at ${new Date(
1180
+ props.mutation.state.submittedAt,
1181
+ ).toLocaleString()}`}
1182
+ >
1183
+ <div
1184
+ class={cx(getObserverCountColorStyles(), 'tsqd-query-observer-count')}
1185
+ >
1186
+ <Show when={color() === 'purple'}>
1187
+ <PauseCircle />
1188
+ </Show>
1189
+ <Show when={color() === 'green'}>
1190
+ <CheckCircle />
1191
+ </Show>
1192
+ <Show when={color() === 'red'}>
1193
+ <XCircle />
1194
+ </Show>
1195
+ <Show when={color() === 'yellow'}>
1196
+ <LoadingCircle />
1197
+ </Show>
1198
+ </div>
1199
+ <code class="tsqd-query-hash">
1200
+ <Show when={props.mutation.options.mutationKey}>
1201
+ {JSON.stringify(props.mutation.options.mutationKey)} -{' '}
1202
+ </Show>
1203
+ {new Date(props.mutation.state.submittedAt).toLocaleString()}
1204
+ </code>
1205
+ </button>
1206
+ </Show>
1207
+ )
1208
+ }
1209
+
910
1210
  export const QueryStatusCount: Component = () => {
911
1211
  const stale = createSubscribeToQueryCacheBatcher(
912
1212
  (queryCache) =>
@@ -961,6 +1261,76 @@ export const QueryStatusCount: Component = () => {
961
1261
  )
962
1262
  }
963
1263
 
1264
+ export const MutationStatusCount: Component = () => {
1265
+ const success = createSubscribeToMutationCacheBatcher(
1266
+ (mutationCache) =>
1267
+ mutationCache()
1268
+ .getAll()
1269
+ .filter(
1270
+ (m) =>
1271
+ getMutationStatusColor({
1272
+ isPaused: m.state.isPaused,
1273
+ status: m.state.status,
1274
+ }) === 'green',
1275
+ ).length,
1276
+ )
1277
+
1278
+ const pending = createSubscribeToMutationCacheBatcher(
1279
+ (mutationCache) =>
1280
+ mutationCache()
1281
+ .getAll()
1282
+ .filter(
1283
+ (m) =>
1284
+ getMutationStatusColor({
1285
+ isPaused: m.state.isPaused,
1286
+ status: m.state.status,
1287
+ }) === 'yellow',
1288
+ ).length,
1289
+ )
1290
+
1291
+ const paused = createSubscribeToMutationCacheBatcher(
1292
+ (mutationCache) =>
1293
+ mutationCache()
1294
+ .getAll()
1295
+ .filter(
1296
+ (m) =>
1297
+ getMutationStatusColor({
1298
+ isPaused: m.state.isPaused,
1299
+ status: m.state.status,
1300
+ }) === 'purple',
1301
+ ).length,
1302
+ )
1303
+
1304
+ const error = createSubscribeToMutationCacheBatcher(
1305
+ (mutationCache) =>
1306
+ mutationCache()
1307
+ .getAll()
1308
+ .filter(
1309
+ (m) =>
1310
+ getMutationStatusColor({
1311
+ isPaused: m.state.isPaused,
1312
+ status: m.state.status,
1313
+ }) === 'red',
1314
+ ).length,
1315
+ )
1316
+
1317
+ const theme = useTheme()
1318
+ const styles = createMemo(() => {
1319
+ return theme() === 'dark' ? darkStyles : lightStyles
1320
+ })
1321
+
1322
+ return (
1323
+ <div
1324
+ class={cx(styles().queryStatusContainer, 'tsqd-query-status-container')}
1325
+ >
1326
+ <QueryStatus label="Paused" color="purple" count={paused()} />
1327
+ <QueryStatus label="Pending" color="yellow" count={pending()} />
1328
+ <QueryStatus label="Success" color="green" count={success()} />
1329
+ <QueryStatus label="Error" color="red" count={error()} />
1330
+ </div>
1331
+ )
1332
+ }
1333
+
964
1334
  export const QueryStatus: Component<QueryStatusProps> = (props) => {
965
1335
  const theme = useTheme()
966
1336
  const styles = createMemo(() => {
@@ -1462,7 +1832,170 @@ const QueryDetails = () => {
1462
1832
  )
1463
1833
  }
1464
1834
 
1465
- const signalsMap = new Map<(q: Accessor<QueryCache>) => any, Setter<any>>()
1835
+ const MutationDetails = () => {
1836
+ const theme = useTheme()
1837
+ const styles = createMemo(() => {
1838
+ return theme() === 'dark' ? darkStyles : lightStyles
1839
+ })
1840
+
1841
+ const { colors } = tokens
1842
+ const t = (light: string, dark: string) => (theme() === 'dark' ? dark : light)
1843
+
1844
+ const isPaused = createSubscribeToMutationCacheBatcher((mutationCache) => {
1845
+ const mutations = mutationCache().getAll()
1846
+ const mutation = mutations.find(
1847
+ (m) => m.mutationId === selectedMutationId(),
1848
+ )
1849
+ if (!mutation) return false
1850
+ return mutation.state.isPaused
1851
+ })
1852
+
1853
+ const status = createSubscribeToMutationCacheBatcher((mutationCache) => {
1854
+ const mutations = mutationCache().getAll()
1855
+ const mutation = mutations.find(
1856
+ (m) => m.mutationId === selectedMutationId(),
1857
+ )
1858
+ if (!mutation) return 'idle'
1859
+ return mutation.state.status
1860
+ })
1861
+
1862
+ const color = createMemo(() =>
1863
+ getMutationStatusColor({
1864
+ isPaused: isPaused(),
1865
+ status: status(),
1866
+ }),
1867
+ )
1868
+
1869
+ const activeMutation = createSubscribeToMutationCacheBatcher(
1870
+ (mutationCache) =>
1871
+ mutationCache()
1872
+ .getAll()
1873
+ .find((mutation) => mutation.mutationId === selectedMutationId()),
1874
+ false,
1875
+ )
1876
+
1877
+ const getQueryStatusColors = () => {
1878
+ if (color() === 'gray') {
1879
+ return css`
1880
+ background-color: ${t(colors[color()][200], colors[color()][700])};
1881
+ color: ${t(colors[color()][700], colors[color()][300])};
1882
+ border-color: ${t(colors[color()][400], colors[color()][600])};
1883
+ `
1884
+ }
1885
+ return css`
1886
+ background-color: ${t(colors[color()][100], colors[color()][900])};
1887
+ color: ${t(colors[color()][700], colors[color()][300])};
1888
+ border-color: ${t(colors[color()][400], colors[color()][600])};
1889
+ `
1890
+ }
1891
+
1892
+ return (
1893
+ <Show when={activeMutation()}>
1894
+ <div
1895
+ class={cx(styles().detailsContainer, 'tsqd-query-details-container')}
1896
+ >
1897
+ <div class={cx(styles().detailsHeader, 'tsqd-query-details-header')}>
1898
+ Mutation Details
1899
+ </div>
1900
+ <div
1901
+ class={cx(
1902
+ styles().detailsBody,
1903
+ 'tsqd-query-details-summary-container',
1904
+ )}
1905
+ >
1906
+ <div class="tsqd-query-details-summary">
1907
+ <pre>
1908
+ <code>
1909
+ <Show
1910
+ when={activeMutation()!.options.mutationKey}
1911
+ fallback={'No mutationKey found'}
1912
+ >
1913
+ {displayValue(activeMutation()!.options.mutationKey, true)}
1914
+ </Show>
1915
+ </code>
1916
+ </pre>
1917
+ <span
1918
+ class={cx(styles().queryDetailsStatus, getQueryStatusColors())}
1919
+ >
1920
+ <Show when={color() === 'purple'}>pending</Show>
1921
+ <Show when={color() !== 'purple'}>{status()}</Show>
1922
+ </span>
1923
+ </div>
1924
+ <div class="tsqd-query-details-last-updated">
1925
+ <span>Submitted At:</span>
1926
+ <span>
1927
+ {new Date(
1928
+ activeMutation()!.state.submittedAt,
1929
+ ).toLocaleTimeString()}
1930
+ </span>
1931
+ </div>
1932
+ </div>
1933
+ <div class={cx(styles().detailsHeader, 'tsqd-query-details-header')}>
1934
+ Variables Details
1935
+ </div>
1936
+ <div
1937
+ style={{
1938
+ padding: '0.5rem',
1939
+ }}
1940
+ class="tsqd-query-details-explorer-container tsqd-query-details-query-explorer"
1941
+ >
1942
+ <Explorer
1943
+ label="Variables"
1944
+ defaultExpanded={['Variables']}
1945
+ value={activeMutation()!.state.variables}
1946
+ />
1947
+ </div>
1948
+ <div class={cx(styles().detailsHeader, 'tsqd-query-details-header')}>
1949
+ Context Details
1950
+ </div>
1951
+ <div
1952
+ style={{
1953
+ padding: '0.5rem',
1954
+ }}
1955
+ class="tsqd-query-details-explorer-container tsqd-query-details-query-explorer"
1956
+ >
1957
+ <Explorer
1958
+ label="Context"
1959
+ defaultExpanded={['Context']}
1960
+ value={activeMutation()!.state.context}
1961
+ />
1962
+ </div>
1963
+ <div class={cx(styles().detailsHeader, 'tsqd-query-details-header')}>
1964
+ Data Explorer
1965
+ </div>
1966
+ <div
1967
+ style={{
1968
+ padding: '0.5rem',
1969
+ }}
1970
+ class="tsqd-query-details-explorer-container tsqd-query-details-query-explorer"
1971
+ >
1972
+ <Explorer
1973
+ label="Data"
1974
+ defaultExpanded={['Data']}
1975
+ value={activeMutation()!.state.data}
1976
+ />
1977
+ </div>
1978
+ <div class={cx(styles().detailsHeader, 'tsqd-query-details-header')}>
1979
+ Mutations Explorer
1980
+ </div>
1981
+ <div
1982
+ style={{
1983
+ padding: '0.5rem',
1984
+ }}
1985
+ class="tsqd-query-details-explorer-container tsqd-query-details-query-explorer"
1986
+ >
1987
+ <Explorer
1988
+ label="Mutation"
1989
+ defaultExpanded={['Mutation']}
1990
+ value={activeMutation()}
1991
+ />
1992
+ </div>
1993
+ </div>
1994
+ </Show>
1995
+ )
1996
+ }
1997
+
1998
+ const queryCacheMap = new Map<(q: Accessor<QueryCache>) => any, Setter<any>>()
1466
1999
 
1467
2000
  const setupQueryCacheSubscription = () => {
1468
2001
  const queryCache = createMemo(() => {
@@ -1471,7 +2004,7 @@ const setupQueryCacheSubscription = () => {
1471
2004
  })
1472
2005
 
1473
2006
  const unsub = queryCache().subscribe(() => {
1474
- for (const [callback, setter] of signalsMap.entries()) {
2007
+ for (const [callback, setter] of queryCacheMap.entries()) {
1475
2008
  queueMicrotask(() => {
1476
2009
  setter(callback(queryCache))
1477
2010
  })
@@ -1479,7 +2012,7 @@ const setupQueryCacheSubscription = () => {
1479
2012
  })
1480
2013
 
1481
2014
  onCleanup(() => {
1482
- signalsMap.clear()
2015
+ queryCacheMap.clear()
1483
2016
  unsub()
1484
2017
  })
1485
2018
 
@@ -1505,11 +2038,67 @@ const createSubscribeToQueryCacheBatcher = <T,>(
1505
2038
  })
1506
2039
 
1507
2040
  // @ts-ignore
1508
- signalsMap.set(callback, setValue)
2041
+ queryCacheMap.set(callback, setValue)
1509
2042
 
1510
2043
  onCleanup(() => {
1511
2044
  // @ts-ignore
1512
- signalsMap.delete(callback)
2045
+ queryCacheMap.delete(callback)
2046
+ })
2047
+
2048
+ return value
2049
+ }
2050
+
2051
+ const mutationCacheMap = new Map<
2052
+ (q: Accessor<MutationCache>) => any,
2053
+ Setter<any>
2054
+ >()
2055
+
2056
+ const setupMutationCacheSubscription = () => {
2057
+ const mutationCache = createMemo(() => {
2058
+ const client = useQueryDevtoolsContext().client
2059
+ return client.getMutationCache()
2060
+ })
2061
+
2062
+ const unsub = mutationCache().subscribe(() => {
2063
+ for (const [callback, setter] of mutationCacheMap.entries()) {
2064
+ queueMicrotask(() => {
2065
+ setter(callback(mutationCache))
2066
+ })
2067
+ }
2068
+ })
2069
+
2070
+ onCleanup(() => {
2071
+ mutationCacheMap.clear()
2072
+ unsub()
2073
+ })
2074
+
2075
+ return unsub
2076
+ }
2077
+
2078
+ const createSubscribeToMutationCacheBatcher = <T,>(
2079
+ callback: (queryCache: Accessor<MutationCache>) => Exclude<T, Function>,
2080
+ equalityCheck: boolean = true,
2081
+ ) => {
2082
+ const mutationCache = createMemo(() => {
2083
+ const client = useQueryDevtoolsContext().client
2084
+ return client.getMutationCache()
2085
+ })
2086
+
2087
+ const [value, setValue] = createSignal<T>(
2088
+ callback(mutationCache),
2089
+ !equalityCheck ? { equals: false } : undefined,
2090
+ )
2091
+
2092
+ createEffect(() => {
2093
+ setValue(callback(mutationCache))
2094
+ })
2095
+
2096
+ // @ts-ignore
2097
+ mutationCacheMap.set(callback, setValue)
2098
+
2099
+ onCleanup(() => {
2100
+ // @ts-ignore
2101
+ mutationCacheMap.delete(callback)
1513
2102
  })
1514
2103
 
1515
2104
  return value
@@ -1788,7 +2377,7 @@ const stylesFactory = (theme: 'light' | 'dark') => {
1788
2377
  justify-content: space-between;
1789
2378
  align-items: center;
1790
2379
  padding: ${tokens.size[2]} ${tokens.size[2.5]};
1791
- gap: ${tokens.size[3]};
2380
+ gap: ${tokens.size[2.5]};
1792
2381
  border-bottom: ${t(colors.gray[300], colors.darkGray[500])} 1px solid;
1793
2382
  align-items: center;
1794
2383
  & > button {
@@ -1800,8 +2389,19 @@ const stylesFactory = (theme: 'light' | 'dark') => {
1800
2389
  flex-direction: column;
1801
2390
  }
1802
2391
  `,
2392
+ logoAndToggleContainer: css`
2393
+ display: flex;
2394
+ gap: ${tokens.size[3]};
2395
+ align-items: center;
2396
+ `,
1803
2397
  logo: css`
1804
2398
  cursor: pointer;
2399
+ display: flex;
2400
+ flex-direction: column;
2401
+ background-color: transparent;
2402
+ border: none;
2403
+ gap: ${tokens.size[0.5]};
2404
+ padding: 0px;
1805
2405
  &:hover {
1806
2406
  opacity: 0.7;
1807
2407
  }
@@ -2173,6 +2773,8 @@ const stylesFactory = (theme: 'light' | 'dark') => {
2173
2773
 
2174
2774
  & pre {
2175
2775
  margin: 0;
2776
+ display: flex;
2777
+ align-items: center;
2176
2778
  }
2177
2779
  `,
2178
2780
  queryDetailsStatus: css`
@@ -2353,6 +2955,56 @@ const stylesFactory = (theme: 'light' | 'dark') => {
2353
2955
  background-color: ${t(colors.purple[100], colors.purple[900])};
2354
2956
  }
2355
2957
  `,
2958
+ viewToggle: css`
2959
+ border-radius: ${tokens.border.radius.sm};
2960
+ background-color: ${t(colors.gray[200], colors.darkGray[600])};
2961
+ border: 1px solid ${t(colors.gray[300], colors.darkGray[200])};
2962
+ display: flex;
2963
+ padding: 0;
2964
+ font-size: ${font.size.xs};
2965
+ color: ${t(colors.gray[700], colors.gray[300])};
2966
+ overflow: hidden;
2967
+
2968
+ &:has(:focus-visible) {
2969
+ outline: 2px solid ${colors.blue[800]};
2970
+ }
2971
+
2972
+ & .tsqd-radio-toggle {
2973
+ opacity: 0.5;
2974
+ display: flex;
2975
+ & label {
2976
+ display: flex;
2977
+ align-items: center;
2978
+ cursor: pointer;
2979
+ line-height: ${font.lineHeight.md};
2980
+ }
2981
+
2982
+ & label:hover {
2983
+ background-color: ${t(colors.gray[100], colors.darkGray[500])};
2984
+ }
2985
+ }
2986
+
2987
+ & > [data-checked] {
2988
+ opacity: 1;
2989
+ background-color: ${t(colors.gray[100], colors.darkGray[400])};
2990
+ & label:hover {
2991
+ background-color: ${t(colors.gray[100], colors.darkGray[400])};
2992
+ }
2993
+ }
2994
+
2995
+ & .tsqd-radio-toggle:first-child {
2996
+ & label {
2997
+ padding: 0 ${tokens.size[1.5]} 0 ${tokens.size[2]};
2998
+ }
2999
+ border-right: 1px solid ${t(colors.gray[300], colors.darkGray[200])};
3000
+ }
3001
+
3002
+ & .tsqd-radio-toggle:nth-child(2) {
3003
+ & label {
3004
+ padding: 0 ${tokens.size[2]} 0 ${tokens.size[1.5]};
3005
+ }
3006
+ }
3007
+ `,
2356
3008
  }
2357
3009
  }
2358
3010