@tanstack/query-devtools 5.0.0-alpha.34 → 5.0.0-alpha.42

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