@tanstack/pacer-devtools 1.3.1 → 1.5.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.
@@ -1,170 +0,0 @@
1
- import { emitChange } from '@tanstack/pacer'
2
- import { shallow, useSelector } from '@tanstack/solid-store'
3
- import { useStyles } from '../styles/use-styles'
4
- import {
5
- getPacerUtilStoreState,
6
- isPacerUtilTanStackStore,
7
- } from '../utils/read-pacer-store-state'
8
- import type { PacerEventName } from '@tanstack/pacer'
9
-
10
- type ActionButtonsProps = {
11
- instance: any
12
- utilName: string
13
- }
14
-
15
- export function ActionButtons(props: ActionButtonsProps) {
16
- const styles = useStyles()
17
- const utilInstance = props.instance
18
- const store = utilInstance?.store
19
-
20
- const stateAccessor = isPacerUtilTanStackStore(store)
21
- ? useSelector(
22
- store as never,
23
- (s: unknown) =>
24
- s !== null && s !== undefined && typeof s === 'object'
25
- ? (s as Record<string, unknown>)
26
- : {},
27
- { compare: shallow },
28
- )
29
- : () => getPacerUtilStoreState(utilInstance) as Record<string, unknown>
30
-
31
- const getState = () => stateAccessor()
32
-
33
- const hasPending = () => {
34
- const u = getState()
35
- return 'isPending' in u
36
- }
37
- const isPending = () => {
38
- const u = getState()
39
- return 'isPending' in u ? !!u.isPending : false
40
- }
41
- const isEmptyFlag = () => {
42
- const u = getState()
43
- return 'isEmpty' in u ? !!u.isEmpty : false
44
- }
45
-
46
- const hasFlush = typeof utilInstance.flush === 'function'
47
- const hasCancel = typeof utilInstance.cancel === 'function'
48
- const hasReset = typeof utilInstance.reset === 'function'
49
- const hasClear = typeof utilInstance.clear === 'function'
50
- const hasStart = typeof utilInstance.start === 'function'
51
- const hasStop = typeof utilInstance.stop === 'function'
52
- const hasStartStop = hasStart && hasStop
53
-
54
- const isRunning = () => {
55
- const u = getState()
56
- return 'isRunning' in u ? !!u.isRunning : true
57
- }
58
-
59
- if (!hasPending() && !hasFlush && !hasCancel && !hasReset && !hasClear) {
60
- return (
61
- <div class={styles().sectionEmpty}>
62
- No actions available for this util
63
- </div>
64
- )
65
- }
66
-
67
- const emitName = `d-${props.utilName}` as PacerEventName
68
- const utilNameLower = props.utilName.toLowerCase()
69
-
70
- return (
71
- <div class={styles().actionsRow}>
72
- {hasPending() && (
73
- <button
74
- class={styles().actionButton}
75
- onMouseDown={() => {
76
- const next = !isPending()
77
- utilInstance.store.setState((prev: any) => ({
78
- ...prev,
79
- isPending: next,
80
- }))
81
- emitChange(emitName, utilInstance)
82
- }}
83
- >
84
- <span class={styles().actionDotBlue} />
85
- {isPending() ? 'Restore Pending' : 'Trigger Pending'}
86
- </button>
87
- )}
88
- {hasFlush && (
89
- <button
90
- class={styles().actionButton}
91
- onMouseDown={() => {
92
- utilInstance.flush()
93
- emitChange(emitName, utilInstance)
94
- }}
95
- disabled={
96
- (utilNameLower.includes('debouncer') && !isPending()) ||
97
- (utilNameLower.includes('throttler') && !isPending()) ||
98
- (utilNameLower.includes('batcher') && isEmptyFlag()) ||
99
- (utilNameLower.includes('queuer') && isEmptyFlag())
100
- }
101
- >
102
- <span class={styles().actionDotGreen} />
103
- Flush
104
- </button>
105
- )}
106
- {hasCancel && (
107
- <button
108
- class={styles().actionButton}
109
- onMouseDown={() => {
110
- utilInstance.cancel()
111
- emitChange(emitName, utilInstance)
112
- }}
113
- disabled={
114
- (utilNameLower.includes('debouncer') && !isPending()) ||
115
- (utilNameLower.includes('throttler') && !isPending()) ||
116
- (utilNameLower.includes('batcher') && isEmptyFlag()) ||
117
- (utilNameLower.includes('queuer') && isEmptyFlag())
118
- }
119
- >
120
- <span class={styles().actionDotRed} />
121
- Cancel
122
- </button>
123
- )}
124
- {hasReset && (
125
- <button
126
- class={styles().actionButton}
127
- onMouseDown={() => {
128
- utilInstance.reset()
129
- emitChange(emitName, utilInstance)
130
- }}
131
- >
132
- <span class={styles().actionDotYellow} />
133
- Reset
134
- </button>
135
- )}
136
- {hasClear && (
137
- <button
138
- class={styles().actionButton}
139
- onMouseDown={() => {
140
- utilInstance.clear()
141
- emitChange(emitName, utilInstance)
142
- }}
143
- disabled={
144
- (utilNameLower.includes('batcher') && isEmptyFlag()) ||
145
- (utilNameLower.includes('queuer') && isEmptyFlag())
146
- }
147
- >
148
- <span class={styles().actionDotOrange} />
149
- Clear
150
- </button>
151
- )}
152
- {hasStartStop && (
153
- <button
154
- class={styles().actionButton}
155
- onMouseDown={() => {
156
- if (isRunning()) {
157
- utilInstance.stop()
158
- } else {
159
- utilInstance.start()
160
- }
161
- emitChange(emitName, utilInstance)
162
- }}
163
- >
164
- <span class={styles().actionDotPurple} />
165
- {isRunning() ? 'Stop' : 'Start'}
166
- </button>
167
- )}
168
- </div>
169
- )
170
- }
@@ -1,62 +0,0 @@
1
- import { JsonTree } from '@tanstack/devtools-ui'
2
- import { useStyles } from '../styles/use-styles'
3
- import { ActionButtons } from './ActionButtons'
4
- import { StateHeader } from './StateHeader'
5
- import { UtilStateJsonTree } from './UtilStateJsonTree'
6
-
7
- type DetailsPanelProps = {
8
- selectedInstance: () => { instance: any; type: string } | null
9
- utilState: () => { lastUpdatedByKey: Record<string, number> }
10
- }
11
-
12
- export function DetailsPanel(props: DetailsPanelProps) {
13
- const styles = useStyles()
14
-
15
- return (
16
- <div class={styles().stateDetails}>
17
- {(() => {
18
- const entry = props.selectedInstance()
19
- if (!entry) {
20
- return (
21
- <div class={styles().noSelection}>
22
- Select a util from the left panel to view its state
23
- </div>
24
- )
25
- }
26
-
27
- return (
28
- <>
29
- <StateHeader
30
- selectedInstance={props.selectedInstance}
31
- utilState={props.utilState}
32
- />
33
-
34
- <div class={styles().detailsGrid}>
35
- <div class={styles().detailSection}>
36
- <div class={styles().detailSectionHeader}>Actions</div>
37
- <ActionButtons
38
- instance={entry.instance}
39
- utilName={entry.type}
40
- />
41
- </div>
42
-
43
- <div class={styles().detailSection}>
44
- <div class={styles().detailSectionHeader}>State</div>
45
- <div class={styles().stateContent}>
46
- <UtilStateJsonTree instance={entry.instance} />
47
- </div>
48
- </div>
49
-
50
- <div class={styles().detailSection}>
51
- <div class={styles().detailSectionHeader}>Options</div>
52
- <div class={styles().stateContent}>
53
- <JsonTree value={entry.instance.options as unknown} />
54
- </div>
55
- </div>
56
- </div>
57
- </>
58
- )
59
- })()}
60
- </div>
61
- )
62
- }
@@ -1,176 +0,0 @@
1
- import {
2
- createEffect,
3
- createMemo,
4
- createSignal,
5
- onCleanup,
6
- onMount,
7
- } from 'solid-js'
8
- import { Header, HeaderLogo, MainPanel } from '@tanstack/devtools-ui'
9
- import { useStyles } from '../styles/use-styles'
10
- import { usePacerDevtoolsState } from '../PacerContextProvider'
11
- import { UTIL_GROUPS } from './util-groups'
12
- import { UtilList } from './UtilList'
13
- import { DetailsPanel } from './DetailsPanel'
14
- import type { StateKey } from './util-groups'
15
-
16
- function readResizeObserverBlockSize(entry: ResizeObserverEntry): number {
17
- const [box] = entry.borderBoxSize
18
- if (box !== undefined) return box.blockSize
19
- return entry.contentRect.height
20
- }
21
-
22
- /** Outer plugin slot from TanStack Devtools (not the inner React mount div, which can report a tiny height). */
23
- function resolvePluginHost(el: HTMLElement): HTMLElement | null {
24
- const byId = el.closest<HTMLElement>('[id^="plugin-container-"]')
25
- if (byId) return byId
26
- return (
27
- el.parentElement?.parentElement?.parentElement ??
28
- el.parentElement?.parentElement ??
29
- el.parentElement
30
- )
31
- }
32
-
33
- export function Shell() {
34
- const styles = useStyles()
35
- const state = usePacerDevtoolsState()
36
- const utilState = () => state
37
- const [selectedKey, setSelectedKey] = createSignal<string | null>(null)
38
- const [leftPanelWidth, setLeftPanelWidth] = createSignal(300)
39
- const [isDragging, setIsDragging] = createSignal(false)
40
- const [shellRootEl, setShellRootEl] = createSignal<HTMLDivElement | null>(
41
- null,
42
- )
43
- const [slotHeightPx, setSlotHeightPx] = createSignal<number | undefined>(
44
- undefined,
45
- )
46
-
47
- const selectedInstance = createMemo(() => {
48
- const key = selectedKey()
49
- if (!key) return null
50
- for (const group of UTIL_GROUPS) {
51
- const instance = (utilState() as unknown as Record<StateKey, Array<any>>)[
52
- group.key
53
- ].find((inst) => inst.key === key)
54
- if (instance) return { instance, type: group.displayName }
55
- }
56
- return null
57
- })
58
-
59
- let dragStartX = 0
60
- let dragStartWidth = 0
61
-
62
- const handleMouseDown = (e: MouseEvent) => {
63
- e.preventDefault()
64
- e.stopPropagation()
65
- setIsDragging(true)
66
- document.body.style.cursor = 'col-resize'
67
- document.body.style.userSelect = 'none'
68
- dragStartX = e.clientX
69
- dragStartWidth = leftPanelWidth()
70
- }
71
-
72
- const handleMouseMove = (e: MouseEvent) => {
73
- if (!isDragging()) return
74
-
75
- e.preventDefault()
76
- const deltaX = e.clientX - dragStartX
77
- const newWidth = Math.max(150, Math.min(800, dragStartWidth + deltaX))
78
- setLeftPanelWidth(newWidth)
79
- }
80
-
81
- const handleMouseUp = () => {
82
- setIsDragging(false)
83
- document.body.style.cursor = ''
84
- document.body.style.userSelect = ''
85
- }
86
-
87
- onMount(() => {
88
- document.addEventListener('mousemove', handleMouseMove)
89
- document.addEventListener('mouseup', handleMouseUp)
90
- })
91
-
92
- onCleanup(() => {
93
- document.removeEventListener('mousemove', handleMouseMove)
94
- document.removeEventListener('mouseup', handleMouseUp)
95
- })
96
-
97
- /**
98
- * TanStack Devtools plugin slots use height: 100% without min-height: 0 on flex
99
- * ancestors, so percentage height often never constrains our tree. Observing the
100
- * outer `plugin-container-*` host (not the immediate React mount wrapper) and
101
- * setting an explicit pixel height makes inner overflow-y regions scroll without
102
- * capping to the mount div’s collapsed height.
103
- */
104
- createEffect(() => {
105
- const el = shellRootEl()
106
- if (!el || typeof ResizeObserver === 'undefined') return
107
- const pluginSlot = resolvePluginHost(el)
108
- if (!pluginSlot) return
109
-
110
- const ro = new ResizeObserver((entries) => {
111
- const entry = entries[0]
112
- if (!entry) return
113
- const target = entry.target as HTMLElement
114
- const h =
115
- target.clientHeight > 0
116
- ? target.clientHeight
117
- : readResizeObserverBlockSize(entry)
118
- if (h > 0) setSlotHeightPx(Math.round(h))
119
- })
120
- ro.observe(pluginSlot)
121
- onCleanup(() => ro.disconnect())
122
- })
123
-
124
- return (
125
- <div
126
- ref={setShellRootEl}
127
- class={styles().shellRoot}
128
- style={{
129
- ...(slotHeightPx() !== undefined
130
- ? {
131
- height: `${slotHeightPx()}px`,
132
- 'max-height': `${slotHeightPx()}px`,
133
- }
134
- : {}),
135
- }}
136
- >
137
- <MainPanel class={styles().mainPanelShell}>
138
- <Header>
139
- <HeaderLogo flavor={{ light: '#84cc16', dark: '#84cc16' }}>
140
- TanStack Pacer
141
- </HeaderLogo>
142
- </Header>
143
-
144
- <div class={styles().mainContainer}>
145
- <div
146
- class={styles().leftPanel}
147
- style={{
148
- width: `${leftPanelWidth()}px`,
149
- 'min-width': '150px',
150
- 'max-width': '800px',
151
- }}
152
- >
153
- <UtilList
154
- selectedKey={selectedKey}
155
- setSelectedKey={setSelectedKey}
156
- utilState={utilState}
157
- />
158
- </div>
159
-
160
- <div
161
- class={`${styles().dragHandle} ${isDragging() ? 'dragging' : ''}`}
162
- onMouseDown={handleMouseDown}
163
- />
164
-
165
- <div class={styles().rightPanel} style={{ flex: 1 }}>
166
- <div class={styles().panelHeader}>Details</div>
167
- <DetailsPanel
168
- selectedInstance={selectedInstance}
169
- utilState={utilState}
170
- />
171
- </div>
172
- </div>
173
- </MainPanel>
174
- </div>
175
- )
176
- }
@@ -1,139 +0,0 @@
1
- import dayjs from 'dayjs'
2
- import relativeTime from 'dayjs/plugin/relativeTime.js'
3
- import { Show, createSignal, onCleanup, onMount } from 'solid-js'
4
- import { shallow, useSelector } from '@tanstack/solid-store'
5
- import { useStyles } from '../styles/use-styles'
6
- import {
7
- getPacerUtilStoreState,
8
- isPacerUtilTanStackStore,
9
- } from '../utils/read-pacer-store-state'
10
-
11
- dayjs.extend(relativeTime)
12
-
13
- type StateHeaderProps = {
14
- selectedInstance: () => { instance: any; type: string } | null
15
- utilState: () => { lastUpdatedByKey: Record<string, number> }
16
- }
17
-
18
- function reductionFromState(
19
- entry: { type: string },
20
- state: Record<string, unknown> | null | undefined,
21
- ): number {
22
- if (!state) return 0
23
-
24
- const isAsync = entry.type.toLowerCase().includes('async')
25
- const completedExecutions = isAsync
26
- ? Number(state.settleCount) || 0
27
- : Number(state.executionCount) || 0
28
-
29
- if (entry.type.toLowerCase().includes('batcher')) {
30
- const totalItemsProcessed = Number(state.totalItemsProcessed) || 0
31
- if (totalItemsProcessed === 0) return 0
32
- return Math.round(
33
- ((totalItemsProcessed - completedExecutions) / totalItemsProcessed) * 100,
34
- )
35
- }
36
-
37
- let requestCount = 0
38
-
39
- if (state.maybeExecuteCount !== undefined) {
40
- requestCount = Number(state.maybeExecuteCount) || 0
41
- } else if (state.addItemCount !== undefined) {
42
- requestCount = Number(state.addItemCount) || 0
43
- } else {
44
- return 0
45
- }
46
-
47
- if (requestCount === 0) return 0
48
-
49
- const reduction = requestCount - completedExecutions
50
- return Math.round((reduction / requestCount) * 100)
51
- }
52
-
53
- function StateHeaderInner(props: {
54
- entry: { instance: any; type: string }
55
- styles: ReturnType<ReturnType<typeof useStyles>>
56
- now: () => number
57
- utilState: () => { lastUpdatedByKey: Record<string, number> }
58
- }) {
59
- const key = props.entry.instance.key as string
60
- const updatedAt = () => props.utilState().lastUpdatedByKey[key] ?? Date.now()
61
-
62
- const getRelativeTime = () => {
63
- const at = updatedAt()
64
- const diffMs = props.now() - at
65
- const diffSeconds = Math.floor(diffMs / 1000)
66
-
67
- if (diffSeconds < 60) {
68
- return `${diffSeconds} second${diffSeconds !== 1 ? 's' : ''} ago`
69
- }
70
-
71
- return dayjs(at).fromNow()
72
- }
73
-
74
- const store = props.entry.instance?.store
75
- const stateAccessor = isPacerUtilTanStackStore(store)
76
- ? useSelector(
77
- store as never,
78
- (s: unknown) =>
79
- s !== null && s !== undefined && typeof s === 'object'
80
- ? (s as Record<string, unknown>)
81
- : {},
82
- { compare: shallow },
83
- )
84
- : () =>
85
- getPacerUtilStoreState(props.entry.instance) as Record<string, unknown>
86
-
87
- const reductionPercentage = () =>
88
- reductionFromState(props.entry, stateAccessor())
89
-
90
- return (
91
- <div class={props.styles.stateHeader}>
92
- <div class={props.styles.stateTitle}>{props.entry.type}</div>
93
- <div style={{ display: 'flex', 'align-items': 'center', gap: '16px' }}>
94
- <div class={props.styles.infoGrid}>
95
- <div class={props.styles.infoLabel}>Key</div>
96
- <div class={props.styles.infoValueMono}>{key}</div>
97
- <div class={props.styles.infoLabel}>Last Updated</div>
98
- <div class={props.styles.infoValueMono}>
99
- {new Date(updatedAt()).toLocaleTimeString()} ({getRelativeTime()})
100
- </div>
101
- </div>
102
- <div
103
- class={props.styles.infoValueMono}
104
- style={{ 'margin-left': 'auto', 'font-weight': 'bold' }}
105
- >
106
- {reductionPercentage()}% reduction
107
- </div>
108
- </div>
109
- </div>
110
- )
111
- }
112
-
113
- export function StateHeader(props: StateHeaderProps) {
114
- const styles = useStyles()
115
- const [now, setNow] = createSignal(Date.now())
116
-
117
- onMount(() => {
118
- const interval = setInterval(() => {
119
- setNow(Date.now())
120
- }, 1000)
121
-
122
- onCleanup(() => {
123
- clearInterval(interval)
124
- })
125
- })
126
-
127
- return (
128
- <Show when={props.selectedInstance()}>
129
- {(entry) => (
130
- <StateHeaderInner
131
- entry={entry()}
132
- styles={styles()}
133
- now={now}
134
- utilState={props.utilState}
135
- />
136
- )}
137
- </Show>
138
- )
139
- }
@@ -1,62 +0,0 @@
1
- import { For } from 'solid-js'
2
- import { useSelector } from '@tanstack/solid-store'
3
- import clsx from 'clsx'
4
- import { useStyles } from '../styles/use-styles'
5
- import { UTIL_GROUPS } from './util-groups'
6
- import type { StateKey } from './util-groups'
7
-
8
- type UtilListProps = {
9
- selectedKey: () => string | null
10
- setSelectedKey: (key: string | null) => void
11
- utilState: () => Record<StateKey, Array<any>>
12
- }
13
-
14
- export function UtilList(props: UtilListProps) {
15
- const styles = useStyles()
16
-
17
- return (
18
- <div class={styles().utilList}>
19
- <For each={UTIL_GROUPS}>
20
- {(group) => (
21
- <>
22
- {props.utilState()[group.key].length > 0 && (
23
- <div class={styles().utilGroup}>
24
- <div class={styles().utilGroupHeader}>{group.label}</div>
25
- <For each={props.utilState()[group.key]}>
26
- {(instance) => {
27
- const status = (() => {
28
- try {
29
- const statusAccessor = useSelector(
30
- instance.store,
31
- (s: any) => s.status,
32
- )
33
- return () => statusAccessor() ?? 'unknown'
34
- } catch {
35
- return () => 'unknown'
36
- }
37
- })()
38
- return (
39
- <div
40
- class={clsx(
41
- styles().utilRow,
42
- props.selectedKey() === instance.key &&
43
- styles().utilRowSelected,
44
- )}
45
- onClick={() =>
46
- props.setSelectedKey(instance.key ?? null)
47
- }
48
- >
49
- <div class={styles().utilKey}>{instance.key}</div>
50
- <div class={styles().utilStatus}>{status()}</div>
51
- </div>
52
- )
53
- }}
54
- </For>
55
- </div>
56
- )}
57
- </>
58
- )}
59
- </For>
60
- </div>
61
- )
62
- }
@@ -1,25 +0,0 @@
1
- import { JsonTree } from '@tanstack/devtools-ui'
2
- import { useSelector } from '@tanstack/solid-store'
3
- import {
4
- getPacerUtilStoreState,
5
- isPacerUtilTanStackStore,
6
- } from '../utils/read-pacer-store-state'
7
- import type { JSX } from 'solid-js'
8
-
9
- type UtilStateJsonTreeProps = {
10
- instance: any
11
- }
12
-
13
- /**
14
- * Subscribes to the util's TanStack Store so the tree updates live (same
15
- * idea as {@link UtilList} status). A plain snapshot would only refresh when the
16
- * devtools shell re-renders, which does not happen on nested store updates.
17
- */
18
- export function UtilStateJsonTree(props: UtilStateJsonTreeProps): JSX.Element {
19
- const store = props.instance?.store
20
- if (isPacerUtilTanStackStore(store)) {
21
- const snapshot = useSelector(store as never, (s: unknown) => s)
22
- return <JsonTree value={snapshot()} />
23
- }
24
- return <JsonTree value={getPacerUtilStoreState(props.instance)} />
25
- }
@@ -1,19 +0,0 @@
1
- import { ThemeContextProvider } from '@tanstack/devtools-ui'
2
- import { PacerContextProvider } from '../PacerContextProvider'
3
- import { Shell } from './Shell'
4
-
5
- import type { TanStackDevtoolsTheme } from '@tanstack/devtools-ui'
6
-
7
- interface DevtoolsProps {
8
- theme: TanStackDevtoolsTheme
9
- }
10
-
11
- export default function PacerDevtools(props: DevtoolsProps) {
12
- return (
13
- <ThemeContextProvider theme={props.theme}>
14
- <PacerContextProvider>
15
- <Shell />
16
- </PacerContextProvider>
17
- </ThemeContextProvider>
18
- )
19
- }