@tanstack/query-devtools 5.52.3 → 5.54.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.
@@ -0,0 +1,47 @@
1
+ import { createLocalStorage } from '@solid-primitives/storage'
2
+ import { createMemo } from 'solid-js'
3
+ import { ContentView, ParentPanel } from './Devtools'
4
+ import { getPreferredColorScheme } from './utils'
5
+ import { THEME_PREFERENCE } from './constants'
6
+ import { PiPProvider, QueryDevtoolsContext, ThemeContext } from './contexts'
7
+ import type { DevtoolsComponentType } from './Devtools'
8
+
9
+ const DevtoolsPanelComponent: DevtoolsComponentType = (props) => {
10
+ const [localStore, setLocalStore] = createLocalStorage({
11
+ prefix: 'TanstackQueryDevtools',
12
+ })
13
+
14
+ const colorScheme = getPreferredColorScheme()
15
+
16
+ const theme = createMemo(() => {
17
+ const preference = (localStore.theme_preference || THEME_PREFERENCE) as
18
+ | 'system'
19
+ | 'dark'
20
+ | 'light'
21
+ if (preference !== 'system') return preference
22
+ return colorScheme()
23
+ })
24
+
25
+ return (
26
+ <QueryDevtoolsContext.Provider value={props}>
27
+ <PiPProvider
28
+ disabled
29
+ localStore={localStore}
30
+ setLocalStore={setLocalStore}
31
+ >
32
+ <ThemeContext.Provider value={theme}>
33
+ <ParentPanel>
34
+ <ContentView
35
+ localStore={localStore}
36
+ setLocalStore={setLocalStore}
37
+ onClose={props.onClose}
38
+ showPanelViewOnly
39
+ />
40
+ </ParentPanel>
41
+ </ThemeContext.Provider>
42
+ </PiPProvider>
43
+ </QueryDevtoolsContext.Provider>
44
+ )
45
+ }
46
+
47
+ export default DevtoolsPanelComponent
package/src/Explorer.tsx CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  updateNestedDataByPath,
11
11
  } from './utils'
12
12
  import { Check, CopiedCopier, Copier, ErrorCopier, List, Trash } from './icons'
13
- import { useQueryDevtoolsContext, useTheme } from './Context'
13
+ import { useQueryDevtoolsContext, useTheme } from './contexts'
14
14
  import type { Query } from '@tanstack/query-core'
15
15
 
16
16
  /**
@@ -11,10 +11,9 @@ import type {
11
11
  DevtoolsErrorType,
12
12
  DevtoolsPosition,
13
13
  QueryDevtoolsProps,
14
- } from './Context'
14
+ } from './contexts'
15
15
  import type { Signal } from 'solid-js'
16
16
 
17
- export type { DevtoolsButtonPosition, DevtoolsPosition, DevtoolsErrorType }
18
17
  export interface TanstackQueryDevtoolsConfig extends QueryDevtoolsProps {
19
18
  styleNonce?: string
20
19
  shadowDOMTarget?: ShadowRoot
@@ -95,7 +94,7 @@ class TanstackQueryDevtools {
95
94
  if (this.#Component) {
96
95
  Devtools = this.#Component
97
96
  } else {
98
- Devtools = lazy(() => import('./Devtools'))
97
+ Devtools = lazy(() => import('./DevtoolsComponent'))
99
98
  this.#Component = Devtools
100
99
  }
101
100
 
@@ -0,0 +1,153 @@
1
+ import { render } from 'solid-js/web'
2
+ import { createSignal, lazy } from 'solid-js'
3
+ import { setupStyleSheet } from './utils'
4
+ import type {
5
+ QueryClient,
6
+ onlineManager as TOnlineManager,
7
+ } from '@tanstack/query-core'
8
+ import type { DevtoolsComponentType } from './Devtools'
9
+ import type {
10
+ DevtoolsButtonPosition,
11
+ DevtoolsErrorType,
12
+ DevtoolsPosition,
13
+ QueryDevtoolsProps,
14
+ } from './contexts'
15
+ import type { Signal } from 'solid-js'
16
+
17
+ export interface TanstackQueryDevtoolsPanelConfig extends QueryDevtoolsProps {
18
+ styleNonce?: string
19
+ shadowDOMTarget?: ShadowRoot
20
+ onClose?: () => unknown
21
+ }
22
+
23
+ class TanstackQueryDevtoolsPanel {
24
+ #client: Signal<QueryClient>
25
+ #onlineManager: typeof TOnlineManager
26
+ #queryFlavor: string
27
+ #version: string
28
+ #isMounted = false
29
+ #styleNonce?: string
30
+ #shadowDOMTarget?: ShadowRoot
31
+ #buttonPosition: Signal<DevtoolsButtonPosition | undefined>
32
+ #position: Signal<DevtoolsPosition | undefined>
33
+ #initialIsOpen: Signal<boolean | undefined>
34
+ #errorTypes: Signal<Array<DevtoolsErrorType> | undefined>
35
+ #onClose: Signal<(() => unknown) | undefined>
36
+ #Component: DevtoolsComponentType | undefined
37
+ #dispose?: () => void
38
+
39
+ constructor(config: TanstackQueryDevtoolsPanelConfig) {
40
+ const {
41
+ client,
42
+ queryFlavor,
43
+ version,
44
+ onlineManager,
45
+ buttonPosition,
46
+ position,
47
+ initialIsOpen,
48
+ errorTypes,
49
+ styleNonce,
50
+ shadowDOMTarget,
51
+ onClose,
52
+ } = config
53
+ this.#client = createSignal(client)
54
+ this.#queryFlavor = queryFlavor
55
+ this.#version = version
56
+ this.#onlineManager = onlineManager
57
+ this.#styleNonce = styleNonce
58
+ this.#shadowDOMTarget = shadowDOMTarget
59
+ this.#buttonPosition = createSignal(buttonPosition)
60
+ this.#position = createSignal(position)
61
+ this.#initialIsOpen = createSignal(initialIsOpen)
62
+ this.#errorTypes = createSignal(errorTypes)
63
+ this.#onClose = createSignal(onClose)
64
+ }
65
+
66
+ setButtonPosition(position: DevtoolsButtonPosition) {
67
+ this.#buttonPosition[1](position)
68
+ }
69
+
70
+ setPosition(position: DevtoolsPosition) {
71
+ this.#position[1](position)
72
+ }
73
+
74
+ setInitialIsOpen(isOpen: boolean) {
75
+ this.#initialIsOpen[1](isOpen)
76
+ }
77
+
78
+ setErrorTypes(errorTypes: Array<DevtoolsErrorType>) {
79
+ this.#errorTypes[1](errorTypes)
80
+ }
81
+
82
+ setClient(client: QueryClient) {
83
+ this.#client[1](client)
84
+ }
85
+
86
+ setOnClose(onClose: () => unknown) {
87
+ this.#onClose[1](() => onClose)
88
+ }
89
+
90
+ mount<T extends HTMLElement>(el: T) {
91
+ if (this.#isMounted) {
92
+ throw new Error('Devtools is already mounted')
93
+ }
94
+ const dispose = render(() => {
95
+ const [btnPosition] = this.#buttonPosition
96
+ const [pos] = this.#position
97
+ const [isOpen] = this.#initialIsOpen
98
+ const [errors] = this.#errorTypes
99
+ const [queryClient] = this.#client
100
+ const [onClose] = this.#onClose
101
+ let Devtools: DevtoolsComponentType
102
+
103
+ if (this.#Component) {
104
+ Devtools = this.#Component
105
+ } else {
106
+ Devtools = lazy(() => import('./DevtoolsPanelComponent'))
107
+ this.#Component = Devtools
108
+ }
109
+
110
+ setupStyleSheet(this.#styleNonce, this.#shadowDOMTarget)
111
+ return (
112
+ <Devtools
113
+ queryFlavor={this.#queryFlavor}
114
+ version={this.#version}
115
+ onlineManager={this.#onlineManager}
116
+ shadowDOMTarget={this.#shadowDOMTarget}
117
+ {...{
118
+ get client() {
119
+ return queryClient()
120
+ },
121
+ get buttonPosition() {
122
+ return btnPosition()
123
+ },
124
+ get position() {
125
+ return pos()
126
+ },
127
+ get initialIsOpen() {
128
+ return isOpen()
129
+ },
130
+ get errorTypes() {
131
+ return errors()
132
+ },
133
+ get onClose() {
134
+ return onClose()
135
+ },
136
+ }}
137
+ />
138
+ )
139
+ }, el)
140
+ this.#isMounted = true
141
+ this.#dispose = dispose
142
+ }
143
+
144
+ unmount() {
145
+ if (!this.#isMounted) {
146
+ throw new Error('Devtools is not mounted')
147
+ }
148
+ this.#dispose?.()
149
+ this.#isMounted = false
150
+ }
151
+ }
152
+
153
+ export { TanstackQueryDevtoolsPanel }
@@ -0,0 +1,17 @@
1
+ import { mutationSortFns, sortFns } from './utils'
2
+ import type { DevtoolsButtonPosition, DevtoolsPosition } from './contexts'
3
+
4
+ export const firstBreakpoint = 1024
5
+ export const secondBreakpoint = 796
6
+ export const thirdBreakpoint = 700
7
+
8
+ export const BUTTON_POSITION: DevtoolsButtonPosition = 'bottom-right'
9
+ export const POSITION: DevtoolsPosition = 'bottom'
10
+ export const THEME_PREFERENCE = 'system'
11
+ export const INITIAL_IS_OPEN = false
12
+ export const DEFAULT_HEIGHT = 500
13
+ export const PIP_DEFAULT_HEIGHT = 500
14
+ export const DEFAULT_WIDTH = 500
15
+ export const DEFAULT_SORT_FN_NAME = Object.keys(sortFns)[0]
16
+ export const DEFAULT_SORT_ORDER = 1
17
+ export const DEFAULT_MUTATION_SORT_FN_NAME = Object.keys(mutationSortFns)[0]
@@ -0,0 +1,191 @@
1
+ import {
2
+ createContext,
3
+ createEffect,
4
+ createMemo,
5
+ createSignal,
6
+ onCleanup,
7
+ useContext,
8
+ } from 'solid-js'
9
+ import { clearDelegatedEvents, delegateEvents } from 'solid-js/web'
10
+ import { PIP_DEFAULT_HEIGHT } from '../constants'
11
+ import { useQueryDevtoolsContext } from './QueryDevtoolsContext'
12
+ import type { Accessor, JSX } from 'solid-js'
13
+ import type { StorageObject, StorageSetter } from '@solid-primitives/storage'
14
+
15
+ interface PiPProviderProps {
16
+ children: JSX.Element
17
+ localStore: StorageObject<string>
18
+ setLocalStore: StorageSetter<string, unknown>
19
+ disabled?: boolean
20
+ }
21
+
22
+ type PiPContextType = {
23
+ pipWindow: Window | null
24
+ requestPipWindow: (width: number, height: number) => void
25
+ closePipWindow: () => void
26
+ disabled: boolean
27
+ }
28
+
29
+ const PiPContext = createContext<Accessor<PiPContextType> | undefined>(
30
+ undefined,
31
+ )
32
+
33
+ export const PiPProvider = (props: PiPProviderProps) => {
34
+ // Expose pipWindow that is currently active
35
+ const [pipWindow, setPipWindow] = createSignal<Window | null>(null)
36
+
37
+ // Close pipWindow programmatically
38
+ const closePipWindow = () => {
39
+ const w = pipWindow()
40
+ if (w != null) {
41
+ w.close()
42
+ setPipWindow(null)
43
+ }
44
+ }
45
+
46
+ // Open new pipWindow
47
+ const requestPipWindow = (width: number, height: number) => {
48
+ // We don't want to allow multiple requests.
49
+ if (pipWindow() != null) {
50
+ return
51
+ }
52
+
53
+ const pip = window.open(
54
+ '',
55
+ 'TSQD-Devtools-Panel',
56
+ `width=${width},height=${height},popup`,
57
+ )
58
+
59
+ if (!pip) {
60
+ throw new Error(
61
+ 'Failed to open popup. Please allow popups for this site to view the devtools in picture-in-picture mode.',
62
+ )
63
+ }
64
+
65
+ // Remove existing styles
66
+ pip.document.head.innerHTML = ''
67
+ // Remove existing body
68
+ pip.document.body.innerHTML = ''
69
+ // Clear Delegated Events
70
+ clearDelegatedEvents(pip.document)
71
+
72
+ pip.document.title = 'TanStack Query Devtools'
73
+ pip.document.body.style.margin = '0'
74
+
75
+ // Detect when window is closed by user
76
+ pip.addEventListener('pagehide', () => {
77
+ props.setLocalStore('pip_open', 'false')
78
+ setPipWindow(null)
79
+ })
80
+
81
+ // It is important to copy all parent window styles. Otherwise, there would be no CSS available at all
82
+ // https://developer.chrome.com/docs/web-platform/document-picture-in-picture/#copy-style-sheets-to-the-picture-in-picture-window
83
+ ;[
84
+ ...(useQueryDevtoolsContext().shadowDOMTarget || document).styleSheets,
85
+ ].forEach((styleSheet) => {
86
+ try {
87
+ const cssRules = [...styleSheet.cssRules]
88
+ .map((rule) => rule.cssText)
89
+ .join('')
90
+ const style = document.createElement('style')
91
+ const style_node = styleSheet.ownerNode
92
+ let style_id = ''
93
+
94
+ if (style_node && 'id' in style_node) {
95
+ style_id = style_node.id
96
+ }
97
+
98
+ if (style_id) {
99
+ style.setAttribute('id', style_id)
100
+ }
101
+ style.textContent = cssRules
102
+ pip.document.head.appendChild(style)
103
+ } catch (e) {
104
+ const link = document.createElement('link')
105
+ if (styleSheet.href == null) {
106
+ return
107
+ }
108
+
109
+ link.rel = 'stylesheet'
110
+ link.type = styleSheet.type
111
+ link.media = styleSheet.media.toString()
112
+ link.href = styleSheet.href
113
+ pip.document.head.appendChild(link)
114
+ }
115
+ })
116
+ delegateEvents(
117
+ [
118
+ 'focusin',
119
+ 'focusout',
120
+ 'pointermove',
121
+ 'keydown',
122
+ 'pointerdown',
123
+ 'pointerup',
124
+ 'click',
125
+ 'mousedown',
126
+ 'input',
127
+ ],
128
+ pip.document,
129
+ )
130
+ props.setLocalStore('pip_open', 'true')
131
+ setPipWindow(pip)
132
+ }
133
+
134
+ createEffect(() => {
135
+ const pip_open = (props.localStore.pip_open ?? 'false') as 'true' | 'false'
136
+ if (pip_open === 'true' && !props.disabled) {
137
+ requestPipWindow(
138
+ Number(window.innerWidth),
139
+ Number(props.localStore.height || PIP_DEFAULT_HEIGHT),
140
+ )
141
+ }
142
+ })
143
+
144
+ createEffect(() => {
145
+ // Setup mutation observer for goober styles with id `_goober
146
+ const gooberStyles = (
147
+ useQueryDevtoolsContext().shadowDOMTarget || document
148
+ ).querySelector('#_goober')
149
+ const w = pipWindow()
150
+ if (gooberStyles && w) {
151
+ const observer = new MutationObserver(() => {
152
+ const pip_style = (
153
+ useQueryDevtoolsContext().shadowDOMTarget || w.document
154
+ ).querySelector('#_goober')
155
+ if (pip_style) {
156
+ pip_style.textContent = gooberStyles.textContent
157
+ }
158
+ })
159
+ observer.observe(gooberStyles, {
160
+ childList: true, // observe direct children
161
+ subtree: true, // and lower descendants too
162
+ characterDataOldValue: true, // pass old data to callback
163
+ })
164
+ onCleanup(() => {
165
+ observer.disconnect()
166
+ })
167
+ }
168
+ })
169
+
170
+ const value = createMemo(() => ({
171
+ pipWindow: pipWindow(),
172
+ requestPipWindow,
173
+ closePipWindow,
174
+ disabled: props.disabled ?? false,
175
+ }))
176
+
177
+ return (
178
+ <PiPContext.Provider value={value}>{props.children}</PiPContext.Provider>
179
+ )
180
+ }
181
+
182
+ export const usePiPWindow = () => {
183
+ const context = createMemo(() => {
184
+ const ctx = useContext(PiPContext)
185
+ if (!ctx) {
186
+ throw new Error('usePiPWindow must be used within a PiPProvider')
187
+ }
188
+ return ctx()
189
+ })
190
+ return context
191
+ }
@@ -1,5 +1,4 @@
1
1
  import { createContext, useContext } from 'solid-js'
2
- import type { Accessor } from 'solid-js'
3
2
  import type { Query, QueryClient, onlineManager } from '@tanstack/query-core'
4
3
 
5
4
  type XPosition = 'left' | 'right'
@@ -29,6 +28,7 @@ export interface QueryDevtoolsProps {
29
28
  initialIsOpen?: boolean
30
29
  errorTypes?: Array<DevtoolsErrorType>
31
30
  shadowDOMTarget?: ShadowRoot
31
+ onClose?: () => unknown
32
32
  }
33
33
 
34
34
  export const QueryDevtoolsContext = createContext<QueryDevtoolsProps>({
@@ -42,11 +42,3 @@ export const QueryDevtoolsContext = createContext<QueryDevtoolsProps>({
42
42
  export function useQueryDevtoolsContext() {
43
43
  return useContext(QueryDevtoolsContext)
44
44
  }
45
-
46
- export const ThemeContext = createContext<Accessor<'light' | 'dark'>>(
47
- () => 'dark' as const,
48
- )
49
-
50
- export function useTheme() {
51
- return useContext(ThemeContext)
52
- }
@@ -0,0 +1,10 @@
1
+ import { createContext, useContext } from 'solid-js'
2
+ import type { Accessor } from 'solid-js'
3
+
4
+ export const ThemeContext = createContext<Accessor<'light' | 'dark'>>(
5
+ () => 'dark' as const,
6
+ )
7
+
8
+ export function useTheme() {
9
+ return useContext(ThemeContext)
10
+ }
@@ -0,0 +1,3 @@
1
+ export * from './PiPContext'
2
+ export * from './QueryDevtoolsContext'
3
+ export * from './ThemeContext'
package/src/index.ts ADDED
@@ -0,0 +1,13 @@
1
+ export type {
2
+ DevtoolsButtonPosition,
3
+ DevtoolsErrorType,
4
+ DevtoolsPosition,
5
+ } from './contexts'
6
+ export {
7
+ TanstackQueryDevtools,
8
+ type TanstackQueryDevtoolsConfig,
9
+ } from './TanstackQueryDevtools'
10
+ export {
11
+ TanstackQueryDevtoolsPanel,
12
+ type TanstackQueryDevtoolsPanelConfig,
13
+ } from './TanstackQueryDevtoolsPanel'
package/src/utils.tsx CHANGED
@@ -1,7 +1,7 @@
1
1
  import { serialize } from 'superjson'
2
2
  import { createSignal, onCleanup, onMount } from 'solid-js'
3
3
  import type { Mutation, Query } from '@tanstack/query-core'
4
- import type { DevtoolsPosition } from './Context'
4
+ import type { DevtoolsPosition } from './contexts'
5
5
 
6
6
  export function getQueryStatusLabel(query: Query) {
7
7
  return query.state.fetchStatus === 'fetching'