@tanstack/react-query-devtools 4.0.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.
Files changed (59) hide show
  1. package/build/cjs/Explorer.js +215 -0
  2. package/build/cjs/Explorer.js.map +1 -0
  3. package/build/cjs/Logo.js +66 -0
  4. package/build/cjs/Logo.js.map +1 -0
  5. package/build/cjs/_virtual/_rollupPluginBabelHelpers.js +33 -0
  6. package/build/cjs/_virtual/_rollupPluginBabelHelpers.js.map +1 -0
  7. package/build/cjs/devtools.js +822 -0
  8. package/build/cjs/devtools.js.map +1 -0
  9. package/build/cjs/index.js +21 -0
  10. package/build/cjs/index.js.map +1 -0
  11. package/build/cjs/packages/react-query-devtools/src/Explorer.js +215 -0
  12. package/build/cjs/packages/react-query-devtools/src/Explorer.js.map +1 -0
  13. package/build/cjs/packages/react-query-devtools/src/Logo.js +66 -0
  14. package/build/cjs/packages/react-query-devtools/src/Logo.js.map +1 -0
  15. package/build/cjs/packages/react-query-devtools/src/devtools.js +818 -0
  16. package/build/cjs/packages/react-query-devtools/src/devtools.js.map +1 -0
  17. package/build/cjs/packages/react-query-devtools/src/index.js +22 -0
  18. package/build/cjs/packages/react-query-devtools/src/index.js.map +1 -0
  19. package/build/cjs/packages/react-query-devtools/src/styledComponents.js +113 -0
  20. package/build/cjs/packages/react-query-devtools/src/styledComponents.js.map +1 -0
  21. package/build/cjs/packages/react-query-devtools/src/theme.js +68 -0
  22. package/build/cjs/packages/react-query-devtools/src/theme.js.map +1 -0
  23. package/build/cjs/packages/react-query-devtools/src/useLocalStorage.js +81 -0
  24. package/build/cjs/packages/react-query-devtools/src/useLocalStorage.js.map +1 -0
  25. package/build/cjs/packages/react-query-devtools/src/useMediaQuery.js +66 -0
  26. package/build/cjs/packages/react-query-devtools/src/useMediaQuery.js.map +1 -0
  27. package/build/cjs/packages/react-query-devtools/src/utils.js +100 -0
  28. package/build/cjs/packages/react-query-devtools/src/utils.js.map +1 -0
  29. package/build/cjs/styledComponents.js +113 -0
  30. package/build/cjs/styledComponents.js.map +1 -0
  31. package/build/cjs/theme.js +68 -0
  32. package/build/cjs/theme.js.map +1 -0
  33. package/build/cjs/useLocalStorage.js +81 -0
  34. package/build/cjs/useLocalStorage.js.map +1 -0
  35. package/build/cjs/useMediaQuery.js +66 -0
  36. package/build/cjs/useMediaQuery.js.map +1 -0
  37. package/build/cjs/utils.js +100 -0
  38. package/build/cjs/utils.js.map +1 -0
  39. package/build/esm/index.js +1973 -0
  40. package/build/esm/index.js.map +1 -0
  41. package/build/stats-html.html +2689 -0
  42. package/build/umd/index.development.js +2001 -0
  43. package/build/umd/index.development.js.map +1 -0
  44. package/build/umd/index.production.js +29 -0
  45. package/build/umd/index.production.js.map +1 -0
  46. package/package.json +30 -0
  47. package/src/Explorer.tsx +261 -0
  48. package/src/Logo.tsx +32 -0
  49. package/src/__tests__/Explorer.test.tsx +68 -0
  50. package/src/__tests__/devtools.test.tsx +766 -0
  51. package/src/__tests__/utils.tsx +81 -0
  52. package/src/devtools.tsx +1128 -0
  53. package/src/index.ts +1 -0
  54. package/src/noop.ts +7 -0
  55. package/src/styledComponents.ts +108 -0
  56. package/src/theme.tsx +32 -0
  57. package/src/useLocalStorage.ts +52 -0
  58. package/src/useMediaQuery.ts +32 -0
  59. package/src/utils.ts +126 -0
@@ -0,0 +1,1128 @@
1
+ import * as React from 'react'
2
+ import { useSyncExternalStore } from 'use-sync-external-store/shim'
3
+ import {
4
+ Query,
5
+ useQueryClient,
6
+ onlineManager,
7
+ notifyManager,
8
+ QueryCache,
9
+ QueryClient,
10
+ QueryKey as QueryKeyType,
11
+ ContextOptions,
12
+ } from '@tanstack/react-query'
13
+ import { rankItem, compareItems } from '@tanstack/match-sorter-utils'
14
+ import useLocalStorage from './useLocalStorage'
15
+ import { useIsMounted } from './utils'
16
+
17
+ import {
18
+ Panel,
19
+ QueryKeys,
20
+ QueryKey,
21
+ Button,
22
+ Code,
23
+ Input,
24
+ Select,
25
+ ActiveQueryPanel,
26
+ } from './styledComponents'
27
+ import { ThemeProvider, defaultTheme as theme } from './theme'
28
+ import { getQueryStatusLabel, getQueryStatusColor } from './utils'
29
+ import Explorer from './Explorer'
30
+ import Logo from './Logo'
31
+
32
+ interface DevtoolsOptions extends ContextOptions {
33
+ /**
34
+ * Set this true if you want the dev tools to default to being open
35
+ */
36
+ initialIsOpen?: boolean
37
+ /**
38
+ * Use this to add props to the panel. For example, you can add className, style (merge and override default style), etc.
39
+ */
40
+ panelProps?: React.DetailedHTMLProps<
41
+ React.HTMLAttributes<HTMLDivElement>,
42
+ HTMLDivElement
43
+ >
44
+ /**
45
+ * Use this to add props to the close button. For example, you can add className, style (merge and override default style), onClick (extend default handler), etc.
46
+ */
47
+ closeButtonProps?: React.DetailedHTMLProps<
48
+ React.ButtonHTMLAttributes<HTMLButtonElement>,
49
+ HTMLButtonElement
50
+ >
51
+ /**
52
+ * Use this to add props to the toggle button. For example, you can add className, style (merge and override default style), onClick (extend default handler), etc.
53
+ */
54
+ toggleButtonProps?: React.DetailedHTMLProps<
55
+ React.ButtonHTMLAttributes<HTMLButtonElement>,
56
+ HTMLButtonElement
57
+ >
58
+ /**
59
+ * The position of the React Query logo to open and close the devtools panel.
60
+ * Defaults to 'bottom-left'.
61
+ */
62
+ position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'
63
+ /**
64
+ * Use this to render the devtools inside a different type of container element for a11y purposes.
65
+ * Any string which corresponds to a valid intrinsic JSX element is allowed.
66
+ * Defaults to 'aside'.
67
+ */
68
+ containerElement?: string | any
69
+ /**
70
+ * nonce for style element for CSP
71
+ */
72
+ styleNonce?: string
73
+ }
74
+
75
+ interface DevtoolsPanelOptions extends ContextOptions {
76
+ /**
77
+ * The standard React style object used to style a component with inline styles
78
+ */
79
+ style?: React.CSSProperties
80
+ /**
81
+ * The standard React className property used to style a component with classes
82
+ */
83
+ className?: string
84
+ /**
85
+ * A boolean variable indicating whether the panel is open or closed
86
+ */
87
+ isOpen?: boolean
88
+ /**
89
+ * nonce for style element for CSP
90
+ */
91
+ styleNonce?: string
92
+ /**
93
+ * A function that toggles the open and close state of the panel
94
+ */
95
+ setIsOpen: (isOpen: boolean) => void
96
+ /**
97
+ * Handles the opening and closing the devtools panel
98
+ */
99
+ handleDragStart: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void
100
+ }
101
+
102
+ const isServer = typeof window === 'undefined'
103
+
104
+ export function ReactQueryDevtools({
105
+ initialIsOpen,
106
+ panelProps = {},
107
+ closeButtonProps = {},
108
+ toggleButtonProps = {},
109
+ position = 'bottom-left',
110
+ containerElement: Container = 'aside',
111
+ context,
112
+ styleNonce,
113
+ }: DevtoolsOptions): React.ReactElement | null {
114
+ const rootRef = React.useRef<HTMLDivElement>(null)
115
+ const panelRef = React.useRef<HTMLDivElement>(null)
116
+ const [isOpen, setIsOpen] = useLocalStorage(
117
+ 'reactQueryDevtoolsOpen',
118
+ initialIsOpen,
119
+ )
120
+ const [devtoolsHeight, setDevtoolsHeight] = useLocalStorage<number | null>(
121
+ 'reactQueryDevtoolsHeight',
122
+ null,
123
+ )
124
+ const [isResolvedOpen, setIsResolvedOpen] = React.useState(false)
125
+ const [isResizing, setIsResizing] = React.useState(false)
126
+ const isMounted = useIsMounted()
127
+
128
+ const handleDragStart = (
129
+ panelElement: HTMLDivElement | null,
130
+ startEvent: React.MouseEvent<HTMLDivElement, MouseEvent>,
131
+ ) => {
132
+ if (startEvent.button !== 0) return // Only allow left click for drag
133
+
134
+ setIsResizing(true)
135
+
136
+ const dragInfo = {
137
+ originalHeight: panelElement?.getBoundingClientRect().height ?? 0,
138
+ pageY: startEvent.pageY,
139
+ }
140
+
141
+ const run = (moveEvent: MouseEvent) => {
142
+ const delta = dragInfo.pageY - moveEvent.pageY
143
+ const newHeight = dragInfo.originalHeight + delta
144
+
145
+ setDevtoolsHeight(newHeight)
146
+
147
+ if (newHeight < 70) {
148
+ setIsOpen(false)
149
+ } else {
150
+ setIsOpen(true)
151
+ }
152
+ }
153
+
154
+ const unsub = () => {
155
+ setIsResizing(false)
156
+ document.removeEventListener('mousemove', run)
157
+ document.removeEventListener('mouseUp', unsub)
158
+ }
159
+
160
+ document.addEventListener('mousemove', run)
161
+ document.addEventListener('mouseup', unsub)
162
+ }
163
+
164
+ React.useEffect(() => {
165
+ setIsResolvedOpen(isOpen ?? false)
166
+ }, [isOpen, isResolvedOpen, setIsResolvedOpen])
167
+
168
+ // Toggle panel visibility before/after transition (depending on direction).
169
+ // Prevents focusing in a closed panel.
170
+ React.useEffect(() => {
171
+ const ref = panelRef.current
172
+ if (ref) {
173
+ const handlePanelTransitionStart = () => {
174
+ if (isResolvedOpen) {
175
+ ref.style.visibility = 'visible'
176
+ }
177
+ }
178
+
179
+ const handlePanelTransitionEnd = () => {
180
+ if (!isResolvedOpen) {
181
+ ref.style.visibility = 'hidden'
182
+ }
183
+ }
184
+
185
+ ref.addEventListener('transitionstart', handlePanelTransitionStart)
186
+ ref.addEventListener('transitionend', handlePanelTransitionEnd)
187
+
188
+ return () => {
189
+ ref.removeEventListener('transitionstart', handlePanelTransitionStart)
190
+ ref.removeEventListener('transitionend', handlePanelTransitionEnd)
191
+ }
192
+ }
193
+ }, [isResolvedOpen])
194
+
195
+ React[isServer ? 'useEffect' : 'useLayoutEffect'](() => {
196
+ if (isResolvedOpen) {
197
+ const previousValue = rootRef.current?.parentElement?.style.paddingBottom
198
+
199
+ const run = () => {
200
+ const containerHeight = panelRef.current?.getBoundingClientRect().height
201
+ if (rootRef.current?.parentElement) {
202
+ rootRef.current.parentElement.style.paddingBottom = `${containerHeight}px`
203
+ }
204
+ }
205
+
206
+ run()
207
+
208
+ if (typeof window !== 'undefined') {
209
+ window.addEventListener('resize', run)
210
+
211
+ return () => {
212
+ window.removeEventListener('resize', run)
213
+ if (
214
+ rootRef.current?.parentElement &&
215
+ typeof previousValue === 'string'
216
+ ) {
217
+ rootRef.current.parentElement.style.paddingBottom = previousValue
218
+ }
219
+ }
220
+ }
221
+ }
222
+ }, [isResolvedOpen])
223
+
224
+ const { style: panelStyle = {}, ...otherPanelProps } = panelProps
225
+
226
+ const {
227
+ style: closeButtonStyle = {},
228
+ onClick: onCloseClick,
229
+ ...otherCloseButtonProps
230
+ } = closeButtonProps
231
+
232
+ const {
233
+ style: toggleButtonStyle = {},
234
+ onClick: onToggleClick,
235
+ ...otherToggleButtonProps
236
+ } = toggleButtonProps
237
+
238
+ // Do not render on the server
239
+ if (!isMounted()) return null
240
+
241
+ return (
242
+ <Container
243
+ ref={rootRef}
244
+ className="ReactQueryDevtools"
245
+ aria-label="React Query Devtools"
246
+ >
247
+ <ThemeProvider theme={theme}>
248
+ <ReactQueryDevtoolsPanel
249
+ ref={panelRef as any}
250
+ context={context}
251
+ styleNonce={styleNonce}
252
+ {...otherPanelProps}
253
+ style={{
254
+ position: 'fixed',
255
+ bottom: '0',
256
+ right: '0',
257
+ zIndex: 99999,
258
+ width: '100%',
259
+ height: devtoolsHeight ?? 500,
260
+ maxHeight: '90%',
261
+ boxShadow: '0 0 20px rgba(0,0,0,.3)',
262
+ borderTop: `1px solid ${theme.gray}`,
263
+ transformOrigin: 'top',
264
+ // visibility will be toggled after transitions, but set initial state here
265
+ visibility: isOpen ? 'visible' : 'hidden',
266
+ ...panelStyle,
267
+ ...(isResizing
268
+ ? {
269
+ transition: `none`,
270
+ }
271
+ : { transition: `all .2s ease` }),
272
+ ...(isResolvedOpen
273
+ ? {
274
+ opacity: 1,
275
+ pointerEvents: 'all',
276
+ transform: `translateY(0) scale(1)`,
277
+ }
278
+ : {
279
+ opacity: 0,
280
+ pointerEvents: 'none',
281
+ transform: `translateY(15px) scale(1.02)`,
282
+ }),
283
+ }}
284
+ isOpen={isResolvedOpen}
285
+ setIsOpen={setIsOpen}
286
+ handleDragStart={(e) => handleDragStart(panelRef.current, e)}
287
+ />
288
+ {isResolvedOpen ? (
289
+ <Button
290
+ type="button"
291
+ aria-controls="ReactQueryDevtoolsPanel"
292
+ aria-haspopup="true"
293
+ aria-expanded="true"
294
+ {...(otherCloseButtonProps as Record<string, unknown>)}
295
+ onClick={(e) => {
296
+ setIsOpen(false)
297
+ onCloseClick?.(e)
298
+ }}
299
+ style={{
300
+ position: 'fixed',
301
+ zIndex: 99999,
302
+ margin: '.5em',
303
+ bottom: 0,
304
+ ...(position === 'top-right'
305
+ ? {
306
+ right: '0',
307
+ }
308
+ : position === 'top-left'
309
+ ? {
310
+ left: '0',
311
+ }
312
+ : position === 'bottom-right'
313
+ ? {
314
+ right: '0',
315
+ }
316
+ : {
317
+ left: '0',
318
+ }),
319
+ ...closeButtonStyle,
320
+ }}
321
+ >
322
+ Close
323
+ </Button>
324
+ ) : null}
325
+ </ThemeProvider>
326
+ {!isResolvedOpen ? (
327
+ <button
328
+ type="button"
329
+ {...otherToggleButtonProps}
330
+ aria-label="Open React Query Devtools"
331
+ aria-controls="ReactQueryDevtoolsPanel"
332
+ aria-haspopup="true"
333
+ aria-expanded="false"
334
+ onClick={(e) => {
335
+ setIsOpen(true)
336
+ onToggleClick?.(e)
337
+ }}
338
+ style={{
339
+ background: 'none',
340
+ border: 0,
341
+ padding: 0,
342
+ position: 'fixed',
343
+ zIndex: 99999,
344
+ display: 'inline-flex',
345
+ fontSize: '1.5em',
346
+ margin: '.5em',
347
+ cursor: 'pointer',
348
+ width: 'fit-content',
349
+ ...(position === 'top-right'
350
+ ? {
351
+ top: '0',
352
+ right: '0',
353
+ }
354
+ : position === 'top-left'
355
+ ? {
356
+ top: '0',
357
+ left: '0',
358
+ }
359
+ : position === 'bottom-right'
360
+ ? {
361
+ bottom: '0',
362
+ right: '0',
363
+ }
364
+ : {
365
+ bottom: '0',
366
+ left: '0',
367
+ }),
368
+ ...toggleButtonStyle,
369
+ }}
370
+ >
371
+ <Logo aria-hidden />
372
+ </button>
373
+ ) : null}
374
+ </Container>
375
+ )
376
+ }
377
+
378
+ const getStatusRank = (q: Query) =>
379
+ q.state.fetchStatus !== 'idle'
380
+ ? 0
381
+ : !q.getObserversCount()
382
+ ? 3
383
+ : q.isStale()
384
+ ? 2
385
+ : 1
386
+
387
+ export const sortFns: Record<string, (a: Query, b: Query) => number> = {
388
+ 'Status > Last Updated': (a, b) =>
389
+ getStatusRank(a) === getStatusRank(b)
390
+ ? (sortFns['Last Updated']?.(a, b) as number)
391
+ : getStatusRank(a) > getStatusRank(b)
392
+ ? 1
393
+ : -1,
394
+ 'Query Hash': (a, b) => (a.queryHash > b.queryHash ? 1 : -1),
395
+ 'Last Updated': (a, b) =>
396
+ a.state.dataUpdatedAt < b.state.dataUpdatedAt ? 1 : -1,
397
+ }
398
+
399
+ const useSubscribeToQueryCache = <T,>(
400
+ queryCache: QueryCache,
401
+ getSnapshot: () => T,
402
+ ): T => {
403
+ return useSyncExternalStore(
404
+ React.useCallback(
405
+ (onStoreChange) =>
406
+ queryCache.subscribe(notifyManager.batchCalls(onStoreChange)),
407
+ [queryCache],
408
+ ),
409
+ getSnapshot,
410
+ getSnapshot,
411
+ )
412
+ }
413
+
414
+ export const ReactQueryDevtoolsPanel = React.forwardRef<
415
+ HTMLDivElement,
416
+ DevtoolsPanelOptions
417
+ >(function ReactQueryDevtoolsPanel(props, ref): React.ReactElement {
418
+ const {
419
+ isOpen = true,
420
+ styleNonce,
421
+ setIsOpen,
422
+ handleDragStart,
423
+ context,
424
+ ...panelProps
425
+ } = props
426
+
427
+ const queryClient = useQueryClient({ context })
428
+ const queryCache = queryClient.getQueryCache()
429
+
430
+ const [sort, setSort] = useLocalStorage(
431
+ 'reactQueryDevtoolsSortFn',
432
+ Object.keys(sortFns)[0],
433
+ )
434
+
435
+ const [filter, setFilter] = useLocalStorage('reactQueryDevtoolsFilter', '')
436
+
437
+ const [sortDesc, setSortDesc] = useLocalStorage(
438
+ 'reactQueryDevtoolsSortDesc',
439
+ false,
440
+ )
441
+
442
+ const sortFn = React.useMemo(() => sortFns[sort as string], [sort])
443
+
444
+ const queriesCount = useSubscribeToQueryCache(
445
+ queryCache,
446
+ () => queryCache.getAll().length,
447
+ )
448
+
449
+ const [activeQueryHash, setActiveQueryHash] = useLocalStorage(
450
+ 'reactQueryDevtoolsActiveQueryHash',
451
+ '',
452
+ )
453
+
454
+ const queries = React.useMemo(() => {
455
+ const unsortedQueries = queryCache.getAll()
456
+ const sorted = queriesCount > 0 ? [...unsortedQueries].sort(sortFn) : []
457
+
458
+ if (sortDesc) {
459
+ sorted.reverse()
460
+ }
461
+
462
+ if (!filter) {
463
+ return sorted
464
+ }
465
+
466
+ let ranked = sorted.map(
467
+ (item) => [item, rankItem(item.queryHash, filter)] as const,
468
+ )
469
+
470
+ ranked = ranked.filter((d) => d[1].passed)
471
+
472
+ ranked = ranked.sort((a, b) => compareItems(a[1], b[1]))
473
+
474
+ return ranked.map((d) => d[0])
475
+ }, [sortDesc, sortFn, filter, queriesCount, queryCache])
476
+
477
+ const [isMockOffline, setMockOffline] = React.useState(false)
478
+
479
+ return (
480
+ <ThemeProvider theme={theme}>
481
+ <Panel
482
+ ref={ref}
483
+ className="ReactQueryDevtoolsPanel"
484
+ aria-label="React Query Devtools Panel"
485
+ id="ReactQueryDevtoolsPanel"
486
+ {...panelProps}
487
+ >
488
+ <style
489
+ nonce={styleNonce}
490
+ dangerouslySetInnerHTML={{
491
+ __html: `
492
+ .ReactQueryDevtoolsPanel * {
493
+ scrollbar-color: ${theme.backgroundAlt} ${theme.gray};
494
+ }
495
+
496
+ .ReactQueryDevtoolsPanel *::-webkit-scrollbar, .ReactQueryDevtoolsPanel scrollbar {
497
+ width: 1em;
498
+ height: 1em;
499
+ }
500
+
501
+ .ReactQueryDevtoolsPanel *::-webkit-scrollbar-track, .ReactQueryDevtoolsPanel scrollbar-track {
502
+ background: ${theme.backgroundAlt};
503
+ }
504
+
505
+ .ReactQueryDevtoolsPanel *::-webkit-scrollbar-thumb, .ReactQueryDevtoolsPanel scrollbar-thumb {
506
+ background: ${theme.gray};
507
+ border-radius: .5em;
508
+ border: 3px solid ${theme.backgroundAlt};
509
+ }
510
+ `,
511
+ }}
512
+ />
513
+ <div
514
+ style={{
515
+ position: 'absolute',
516
+ left: 0,
517
+ top: 0,
518
+ width: '100%',
519
+ height: '4px',
520
+ marginBottom: '-4px',
521
+ cursor: 'row-resize',
522
+ zIndex: 100000,
523
+ }}
524
+ onMouseDown={handleDragStart}
525
+ ></div>
526
+ <div
527
+ style={{
528
+ flex: '1 1 500px',
529
+ minHeight: '40%',
530
+ maxHeight: '100%',
531
+ overflow: 'auto',
532
+ borderRight: `1px solid ${theme.grayAlt}`,
533
+ display: isOpen ? 'flex' : 'none',
534
+ flexDirection: 'column',
535
+ }}
536
+ >
537
+ <div
538
+ style={{
539
+ padding: '.5em',
540
+ background: theme.backgroundAlt,
541
+ display: 'flex',
542
+ justifyContent: 'space-between',
543
+ alignItems: 'center',
544
+ }}
545
+ >
546
+ <button
547
+ type="button"
548
+ aria-label="Close React Query Devtools"
549
+ aria-controls="ReactQueryDevtoolsPanel"
550
+ aria-haspopup="true"
551
+ aria-expanded="true"
552
+ onClick={() => setIsOpen(false)}
553
+ style={{
554
+ display: 'inline-flex',
555
+ background: 'none',
556
+ border: 0,
557
+ padding: 0,
558
+ marginRight: '.5em',
559
+ cursor: 'pointer',
560
+ }}
561
+ >
562
+ <Logo aria-hidden />
563
+ </button>
564
+ <div
565
+ style={{
566
+ display: 'flex',
567
+ flexDirection: 'column',
568
+ }}
569
+ >
570
+ <QueryStatusCount queryCache={queryCache} />
571
+ <div
572
+ style={{
573
+ display: 'flex',
574
+ alignItems: 'center',
575
+ }}
576
+ >
577
+ <Input
578
+ placeholder="Filter"
579
+ aria-label="Filter by queryhash"
580
+ value={filter ?? ''}
581
+ onChange={(e) => setFilter(e.target.value)}
582
+ onKeyDown={(e) => {
583
+ if (e.key === 'Escape') setFilter('')
584
+ }}
585
+ style={{
586
+ flex: '1',
587
+ marginRight: '.5em',
588
+ width: '100%',
589
+ }}
590
+ />
591
+ {!filter ? (
592
+ <>
593
+ <Select
594
+ aria-label="Sort queries"
595
+ value={sort}
596
+ onChange={(e) => setSort(e.target.value)}
597
+ style={{
598
+ flex: '1',
599
+ minWidth: 75,
600
+ marginRight: '.5em',
601
+ }}
602
+ >
603
+ {Object.keys(sortFns).map((key) => (
604
+ <option key={key} value={key}>
605
+ Sort by {key}
606
+ </option>
607
+ ))}
608
+ </Select>
609
+ <Button
610
+ type="button"
611
+ onClick={() => setSortDesc((old) => !old)}
612
+ style={{
613
+ padding: '.3em .4em',
614
+ marginRight: '.5em',
615
+ }}
616
+ >
617
+ {sortDesc ? '⬇ Desc' : '⬆ Asc'}
618
+ </Button>
619
+ <Button
620
+ type="button"
621
+ onClick={() => {
622
+ if (isMockOffline) {
623
+ onlineManager.setOnline(undefined)
624
+ setMockOffline(false)
625
+ window.dispatchEvent(new Event('online'))
626
+ } else {
627
+ onlineManager.setOnline(false)
628
+ setMockOffline(true)
629
+ }
630
+ }}
631
+ aria-label={
632
+ isMockOffline
633
+ ? 'Restore offline mock'
634
+ : 'Mock offline behavior'
635
+ }
636
+ title={
637
+ isMockOffline
638
+ ? 'Restore offline mock'
639
+ : 'Mock offline behavior'
640
+ }
641
+ style={{
642
+ padding: '0',
643
+ height: '2em',
644
+ }}
645
+ >
646
+ <svg
647
+ xmlns="http://www.w3.org/2000/svg"
648
+ width="2em"
649
+ height="2em"
650
+ viewBox="0 0 24 24"
651
+ stroke={isMockOffline ? theme.danger : 'currentColor'}
652
+ fill="none"
653
+ >
654
+ {isMockOffline ? (
655
+ <>
656
+ <path stroke="none" d="M0 0h24v24H0z" fill="none" />
657
+ <line x1="12" y1="18" x2="12.01" y2="18" />
658
+ <path d="M9.172 15.172a4 4 0 0 1 5.656 0" />
659
+ <path d="M6.343 12.343a7.963 7.963 0 0 1 3.864 -2.14m4.163 .155a7.965 7.965 0 0 1 3.287 2" />
660
+ <path d="M3.515 9.515a12 12 0 0 1 3.544 -2.455m3.101 -.92a12 12 0 0 1 10.325 3.374" />
661
+ <line x1="3" y1="3" x2="21" y2="21" />
662
+ </>
663
+ ) : (
664
+ <>
665
+ <path stroke="none" d="M0 0h24v24H0z" fill="none" />
666
+ <line x1="12" y1="18" x2="12.01" y2="18" />
667
+ <path d="M9.172 15.172a4 4 0 0 1 5.656 0" />
668
+ <path d="M6.343 12.343a8 8 0 0 1 11.314 0" />
669
+ <path d="M3.515 9.515c4.686 -4.687 12.284 -4.687 17 0" />
670
+ </>
671
+ )}
672
+ </svg>
673
+ </Button>
674
+ </>
675
+ ) : null}
676
+ </div>
677
+ </div>
678
+ </div>
679
+ <div
680
+ style={{
681
+ overflowY: 'auto',
682
+ flex: '1',
683
+ }}
684
+ >
685
+ {queries.map((query) => {
686
+ return (
687
+ <QueryRow
688
+ queryKey={query.queryKey}
689
+ activeQueryHash={activeQueryHash}
690
+ setActiveQueryHash={setActiveQueryHash}
691
+ key={query.queryHash}
692
+ queryCache={queryCache}
693
+ />
694
+ )
695
+ })}
696
+ </div>
697
+ </div>
698
+
699
+ {activeQueryHash ? (
700
+ <ActiveQuery
701
+ activeQueryHash={activeQueryHash}
702
+ queryCache={queryCache}
703
+ queryClient={queryClient}
704
+ />
705
+ ) : null}
706
+ </Panel>
707
+ </ThemeProvider>
708
+ )
709
+ })
710
+
711
+ const ActiveQuery = ({
712
+ queryCache,
713
+ activeQueryHash,
714
+ queryClient,
715
+ }: {
716
+ queryCache: QueryCache
717
+ activeQueryHash: string
718
+ queryClient: QueryClient
719
+ }) => {
720
+ const activeQuery = useSubscribeToQueryCache(queryCache, () =>
721
+ queryCache.getAll().find((query) => query.queryHash === activeQueryHash),
722
+ )
723
+
724
+ const activeQueryState = useSubscribeToQueryCache(
725
+ queryCache,
726
+ () =>
727
+ queryCache.getAll().find((query) => query.queryHash === activeQueryHash)
728
+ ?.state,
729
+ )
730
+
731
+ const isStale =
732
+ useSubscribeToQueryCache(queryCache, () =>
733
+ queryCache
734
+ .getAll()
735
+ .find((query) => query.queryHash === activeQueryHash)
736
+ ?.isStale(),
737
+ ) ?? false
738
+
739
+ const observerCount =
740
+ useSubscribeToQueryCache(queryCache, () =>
741
+ queryCache
742
+ .getAll()
743
+ .find((query) => query.queryHash === activeQueryHash)
744
+ ?.getObserversCount(),
745
+ ) ?? 0
746
+
747
+ const handleRefetch = () => {
748
+ const promise = activeQuery?.fetch()
749
+ promise?.catch(noop)
750
+ }
751
+
752
+ if (!activeQuery || !activeQueryState) {
753
+ return null
754
+ }
755
+
756
+ return (
757
+ <ActiveQueryPanel>
758
+ <div
759
+ style={{
760
+ padding: '.5em',
761
+ background: theme.backgroundAlt,
762
+ position: 'sticky',
763
+ top: 0,
764
+ zIndex: 1,
765
+ }}
766
+ >
767
+ Query Details
768
+ </div>
769
+ <div
770
+ style={{
771
+ padding: '.5em',
772
+ }}
773
+ >
774
+ <div
775
+ style={{
776
+ marginBottom: '.5em',
777
+ display: 'flex',
778
+ alignItems: 'stretch',
779
+ justifyContent: 'space-between',
780
+ }}
781
+ >
782
+ <Code
783
+ style={{
784
+ lineHeight: '1.8em',
785
+ }}
786
+ >
787
+ <pre
788
+ style={{
789
+ margin: 0,
790
+ padding: 0,
791
+ overflow: 'auto',
792
+ }}
793
+ >
794
+ {JSON.stringify(activeQuery.queryKey, null, 2)}
795
+ </pre>
796
+ </Code>
797
+ <span
798
+ style={{
799
+ padding: '0.3em .6em',
800
+ borderRadius: '0.4em',
801
+ fontWeight: 'bold',
802
+ textShadow: '0 2px 10px black',
803
+ background: getQueryStatusColor({
804
+ queryState: activeQueryState,
805
+ isStale: isStale,
806
+ observerCount: observerCount,
807
+ theme,
808
+ }),
809
+ flexShrink: 0,
810
+ }}
811
+ >
812
+ {getQueryStatusLabel(activeQuery)}
813
+ </span>
814
+ </div>
815
+ <div
816
+ style={{
817
+ marginBottom: '.5em',
818
+ display: 'flex',
819
+ alignItems: 'center',
820
+ justifyContent: 'space-between',
821
+ }}
822
+ >
823
+ Observers: <Code>{observerCount}</Code>
824
+ </div>
825
+ <div
826
+ style={{
827
+ display: 'flex',
828
+ alignItems: 'center',
829
+ justifyContent: 'space-between',
830
+ }}
831
+ >
832
+ Last Updated:{' '}
833
+ <Code>
834
+ {new Date(activeQueryState.dataUpdatedAt).toLocaleTimeString()}
835
+ </Code>
836
+ </div>
837
+ </div>
838
+ <div
839
+ style={{
840
+ background: theme.backgroundAlt,
841
+ padding: '.5em',
842
+ position: 'sticky',
843
+ top: 0,
844
+ zIndex: 1,
845
+ }}
846
+ >
847
+ Actions
848
+ </div>
849
+ <div
850
+ style={{
851
+ padding: '0.5em',
852
+ }}
853
+ >
854
+ <Button
855
+ type="button"
856
+ onClick={handleRefetch}
857
+ disabled={activeQueryState.fetchStatus === 'fetching'}
858
+ style={{
859
+ background: theme.active,
860
+ }}
861
+ >
862
+ Refetch
863
+ </Button>{' '}
864
+ <Button
865
+ type="button"
866
+ onClick={() => queryClient.invalidateQueries(activeQuery)}
867
+ style={{
868
+ background: theme.warning,
869
+ color: theme.inputTextColor,
870
+ }}
871
+ >
872
+ Invalidate
873
+ </Button>{' '}
874
+ <Button
875
+ type="button"
876
+ onClick={() => queryClient.resetQueries(activeQuery)}
877
+ style={{
878
+ background: theme.gray,
879
+ }}
880
+ >
881
+ Reset
882
+ </Button>{' '}
883
+ <Button
884
+ type="button"
885
+ onClick={() => queryClient.removeQueries(activeQuery)}
886
+ style={{
887
+ background: theme.danger,
888
+ }}
889
+ >
890
+ Remove
891
+ </Button>
892
+ </div>
893
+ <div
894
+ style={{
895
+ background: theme.backgroundAlt,
896
+ padding: '.5em',
897
+ position: 'sticky',
898
+ top: 0,
899
+ zIndex: 1,
900
+ }}
901
+ >
902
+ Data Explorer
903
+ </div>
904
+ <div
905
+ style={{
906
+ padding: '.5em',
907
+ }}
908
+ >
909
+ <Explorer
910
+ label="Data"
911
+ value={activeQueryState.data}
912
+ defaultExpanded={{}}
913
+ />
914
+ </div>
915
+ <div
916
+ style={{
917
+ background: theme.backgroundAlt,
918
+ padding: '.5em',
919
+ position: 'sticky',
920
+ top: 0,
921
+ zIndex: 1,
922
+ }}
923
+ >
924
+ Query Explorer
925
+ </div>
926
+ <div
927
+ style={{
928
+ padding: '.5em',
929
+ }}
930
+ >
931
+ <Explorer
932
+ label="Query"
933
+ value={activeQuery}
934
+ defaultExpanded={{
935
+ queryKey: true,
936
+ }}
937
+ />
938
+ </div>
939
+ </ActiveQueryPanel>
940
+ )
941
+ }
942
+
943
+ const QueryStatusCount = ({ queryCache }: { queryCache: QueryCache }) => {
944
+ const hasFresh = useSubscribeToQueryCache(
945
+ queryCache,
946
+ () =>
947
+ queryCache.getAll().filter((q) => getQueryStatusLabel(q) === 'fresh')
948
+ .length,
949
+ )
950
+ const hasFetching = useSubscribeToQueryCache(
951
+ queryCache,
952
+ () =>
953
+ queryCache.getAll().filter((q) => getQueryStatusLabel(q) === 'fetching')
954
+ .length,
955
+ )
956
+ const hasPaused = useSubscribeToQueryCache(
957
+ queryCache,
958
+ () =>
959
+ queryCache.getAll().filter((q) => getQueryStatusLabel(q) === 'paused')
960
+ .length,
961
+ )
962
+ const hasStale = useSubscribeToQueryCache(
963
+ queryCache,
964
+ () =>
965
+ queryCache.getAll().filter((q) => getQueryStatusLabel(q) === 'stale')
966
+ .length,
967
+ )
968
+ const hasInactive = useSubscribeToQueryCache(
969
+ queryCache,
970
+ () =>
971
+ queryCache.getAll().filter((q) => getQueryStatusLabel(q) === 'inactive')
972
+ .length,
973
+ )
974
+ return (
975
+ <QueryKeys style={{ marginBottom: '.5em' }}>
976
+ <QueryKey
977
+ style={{
978
+ background: theme.success,
979
+ opacity: hasFresh ? 1 : 0.3,
980
+ }}
981
+ >
982
+ fresh <Code>({hasFresh})</Code>
983
+ </QueryKey>{' '}
984
+ <QueryKey
985
+ style={{
986
+ background: theme.active,
987
+ opacity: hasFetching ? 1 : 0.3,
988
+ }}
989
+ >
990
+ fetching <Code>({hasFetching})</Code>
991
+ </QueryKey>{' '}
992
+ <QueryKey
993
+ style={{
994
+ background: theme.paused,
995
+ opacity: hasPaused ? 1 : 0.3,
996
+ }}
997
+ >
998
+ paused <Code>({hasPaused})</Code>
999
+ </QueryKey>{' '}
1000
+ <QueryKey
1001
+ style={{
1002
+ background: theme.warning,
1003
+ color: 'black',
1004
+ textShadow: '0',
1005
+ opacity: hasStale ? 1 : 0.3,
1006
+ }}
1007
+ >
1008
+ stale <Code>({hasStale})</Code>
1009
+ </QueryKey>{' '}
1010
+ <QueryKey
1011
+ style={{
1012
+ background: theme.gray,
1013
+ opacity: hasInactive ? 1 : 0.3,
1014
+ }}
1015
+ >
1016
+ inactive <Code>({hasInactive})</Code>
1017
+ </QueryKey>
1018
+ </QueryKeys>
1019
+ )
1020
+ }
1021
+
1022
+ interface QueryRowProps {
1023
+ queryKey: QueryKeyType
1024
+ setActiveQueryHash: (hash: string) => void
1025
+ activeQueryHash?: string
1026
+ queryCache: QueryCache
1027
+ }
1028
+
1029
+ const QueryRow = ({
1030
+ queryKey,
1031
+ setActiveQueryHash,
1032
+ activeQueryHash,
1033
+ queryCache,
1034
+ }: QueryRowProps) => {
1035
+ const queryHash =
1036
+ useSubscribeToQueryCache(
1037
+ queryCache,
1038
+ () => queryCache.find(queryKey)?.queryHash,
1039
+ ) ?? ''
1040
+
1041
+ const queryState = useSubscribeToQueryCache(
1042
+ queryCache,
1043
+ () => queryCache.find(queryKey)?.state,
1044
+ )
1045
+
1046
+ const isStale =
1047
+ useSubscribeToQueryCache(queryCache, () =>
1048
+ queryCache.find(queryKey)?.isStale(),
1049
+ ) ?? false
1050
+
1051
+ const isDisabled =
1052
+ useSubscribeToQueryCache(queryCache, () =>
1053
+ queryCache.find(queryKey)?.isDisabled(),
1054
+ ) ?? false
1055
+
1056
+ const observerCount =
1057
+ useSubscribeToQueryCache(queryCache, () =>
1058
+ queryCache.find(queryKey)?.getObserversCount(),
1059
+ ) ?? 0
1060
+
1061
+ if (!queryState) {
1062
+ return null
1063
+ }
1064
+
1065
+ return (
1066
+ <div
1067
+ role="button"
1068
+ aria-label={`Open query details for ${queryHash}`}
1069
+ onClick={() =>
1070
+ setActiveQueryHash(activeQueryHash === queryHash ? '' : queryHash)
1071
+ }
1072
+ style={{
1073
+ display: 'flex',
1074
+ borderBottom: `solid 1px ${theme.grayAlt}`,
1075
+ cursor: 'pointer',
1076
+ background:
1077
+ queryHash === activeQueryHash ? 'rgba(255,255,255,.1)' : undefined,
1078
+ }}
1079
+ >
1080
+ <div
1081
+ style={{
1082
+ flex: '0 0 auto',
1083
+ width: '2em',
1084
+ height: '2em',
1085
+ background: getQueryStatusColor({
1086
+ queryState,
1087
+ isStale,
1088
+ observerCount,
1089
+ theme,
1090
+ }),
1091
+ display: 'flex',
1092
+ alignItems: 'center',
1093
+ justifyContent: 'center',
1094
+ fontWeight: 'bold',
1095
+ textShadow: isStale ? '0' : '0 0 10px black',
1096
+ color: isStale ? 'black' : 'white',
1097
+ }}
1098
+ >
1099
+ {observerCount}
1100
+ </div>
1101
+ {isDisabled ? (
1102
+ <div
1103
+ style={{
1104
+ flex: '0 0 auto',
1105
+ height: '2em',
1106
+ background: theme.gray,
1107
+ display: 'flex',
1108
+ alignItems: 'center',
1109
+ fontWeight: 'bold',
1110
+ padding: '0 0.5em',
1111
+ }}
1112
+ >
1113
+ disabled
1114
+ </div>
1115
+ ) : null}
1116
+ <Code
1117
+ style={{
1118
+ padding: '.5em',
1119
+ }}
1120
+ >
1121
+ {`${queryHash}`}
1122
+ </Code>
1123
+ </div>
1124
+ )
1125
+ }
1126
+
1127
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
1128
+ function noop() {}