nuxt-devtools-observatory 0.1.32 → 0.1.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -1
- package/client/.env.example +2 -0
- package/client/dist/assets/index-BO7neKEi.css +1 -0
- package/client/dist/assets/index-fFBuk6M6.js +20 -0
- package/client/dist/index.html +2 -2
- package/client/src/App.vue +8 -0
- package/client/src/components/Flamegraph.vue +4 -4
- package/client/src/components/SpanInspector.vue +1 -1
- package/client/src/composables/composable-search.ts +3 -0
- package/client/src/composables/trace-render-aggregation.ts +11 -2
- package/client/src/composables/useVirtualizationConfig.ts +40 -0
- package/client/src/composables/useVirtualizationFlags.ts +129 -0
- package/client/src/stores/observatory.ts +20 -0
- package/client/src/views/ComposableTracker.vue +212 -71
- package/client/src/views/FetchDashboard.vue +181 -16
- package/client/src/views/PiniaStoreTracker.vue +343 -0
- package/client/src/views/ProvideInjectGraph.vue +66 -18
- package/client/src/views/RenderHeatmap.vue +329 -75
- package/client/src/views/TraceViewer.vue +190 -20
- package/client/src/views/TransitionTimeline.vue +112 -19
- package/dist/module.d.mts +15 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +28 -24
- package/dist/runtime/composables/pinia-store-registry.d.ts +44 -0
- package/dist/runtime/composables/pinia-store-registry.js +447 -0
- package/dist/runtime/composables/provide-inject-registry.js +13 -8
- package/dist/runtime/composables/render-registry.js +6 -4
- package/dist/runtime/instrumentation/asyncData.d.ts +1 -1
- package/dist/runtime/instrumentation/fetch.d.ts +7 -1
- package/dist/runtime/instrumentation/fetch.js +22 -1
- package/dist/runtime/plugin.js +39 -2
- package/dist/runtime/test-bridge.d.ts +18 -0
- package/dist/runtime/test-bridge.js +100 -0
- package/package.json +14 -3
- package/client/dist/assets/index-5Wl1XYRH.js +0 -17
- package/client/dist/assets/index-DT_QUiIh.css +0 -1
|
@@ -1,18 +1,24 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { ref, computed } from 'vue'
|
|
2
|
+
import { ref, computed, watch } from 'vue'
|
|
3
|
+
import { useVirtualizer } from '@tanstack/vue-virtual'
|
|
4
|
+
import { useVirtualizationConfig } from '@observatory-client/composables/useVirtualizationConfig'
|
|
3
5
|
import { useResizablePane } from '@observatory-client/composables/useResizablePane'
|
|
4
6
|
import { useObservatoryData } from '@observatory-client/stores/observatory'
|
|
5
7
|
import type { FetchEntry } from '@observatory/types/snapshot'
|
|
6
8
|
|
|
7
9
|
type FetchViewEntry = FetchEntry & { startOffset: number }
|
|
8
10
|
|
|
9
|
-
const { fetch, connected } = useObservatoryData()
|
|
11
|
+
const { fetch, connected, features } = useObservatoryData()
|
|
10
12
|
const { paneWidth: detailWidth, onHandleMouseDown } = useResizablePane(280, 'observatory:fetch:detailWidth')
|
|
11
13
|
|
|
12
14
|
const filter = ref<string>('all')
|
|
13
15
|
const search = ref('')
|
|
14
16
|
const selectedId = ref<string | null>(null)
|
|
15
17
|
const waterfallOpen = ref(true)
|
|
18
|
+
const tableScrollRef = ref<HTMLElement | null>(null)
|
|
19
|
+
const currentPage = ref(1)
|
|
20
|
+
|
|
21
|
+
const { preset: virtualizationPreset } = useVirtualizationConfig({ rowHeight: 38, overscan: 6 })
|
|
16
22
|
|
|
17
23
|
const entries = computed<FetchViewEntry[]>(() => {
|
|
18
24
|
const sorted = [...fetch.value].sort((a, b) => a.startTime - b.startTime)
|
|
@@ -24,7 +30,15 @@ const entries = computed<FetchViewEntry[]>(() => {
|
|
|
24
30
|
}))
|
|
25
31
|
})
|
|
26
32
|
|
|
27
|
-
const
|
|
33
|
+
const entriesById = computed(() => new Map(entries.value.map((entry) => [entry.id, entry] as const)))
|
|
34
|
+
|
|
35
|
+
const selected = computed(() => {
|
|
36
|
+
if (!selectedId.value) {
|
|
37
|
+
return null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return entriesById.value.get(selectedId.value) ?? null
|
|
41
|
+
})
|
|
28
42
|
|
|
29
43
|
const counts = computed(() => ({
|
|
30
44
|
ok: entries.value.filter((entry) => entry.status === 'ok' || entry.status === 'cached').length,
|
|
@@ -48,6 +62,94 @@ const filtered = computed(() => {
|
|
|
48
62
|
})
|
|
49
63
|
})
|
|
50
64
|
|
|
65
|
+
const fetchPageSize = computed(() => {
|
|
66
|
+
const raw = features.value?.fetchPageSize
|
|
67
|
+
|
|
68
|
+
if (typeof raw !== 'number' || !Number.isFinite(raw)) {
|
|
69
|
+
return 20
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return Math.max(1, Math.floor(raw))
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
const pagedFiltered = computed(() => filtered.value.slice(0, currentPage.value * fetchPageSize.value))
|
|
76
|
+
const hasMoreRows = computed(() => pagedFiltered.value.length < filtered.value.length)
|
|
77
|
+
|
|
78
|
+
const virtualizedRowsEnabled = computed(() => true)
|
|
79
|
+
|
|
80
|
+
const rowVirtualizerOptions = computed(() => ({
|
|
81
|
+
count: pagedFiltered.value.length,
|
|
82
|
+
getScrollElement: () => tableScrollRef.value,
|
|
83
|
+
estimateSize: () => virtualizationPreset.value.rowHeight,
|
|
84
|
+
overscan: virtualizationPreset.value.overscan,
|
|
85
|
+
}))
|
|
86
|
+
|
|
87
|
+
const rowVirtualizer = useVirtualizer(rowVirtualizerOptions)
|
|
88
|
+
|
|
89
|
+
const virtualItems = computed(() => {
|
|
90
|
+
if (!virtualizedRowsEnabled.value) {
|
|
91
|
+
return []
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return rowVirtualizer.value.getVirtualItems()
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
const topVirtualPadding = computed(() => {
|
|
98
|
+
if (!virtualizedRowsEnabled.value || virtualItems.value.length === 0) {
|
|
99
|
+
return 0
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return virtualItems.value[0].start
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
const bottomVirtualPadding = computed(() => {
|
|
106
|
+
if (!virtualizedRowsEnabled.value || virtualItems.value.length === 0) {
|
|
107
|
+
return 0
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const total = rowVirtualizer.value.getTotalSize()
|
|
111
|
+
const last = virtualItems.value[virtualItems.value.length - 1]
|
|
112
|
+
|
|
113
|
+
return Math.max(0, total - last.end)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
const visibleRows = computed(() => {
|
|
117
|
+
if (!virtualizedRowsEnabled.value) {
|
|
118
|
+
return pagedFiltered.value
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return virtualItems.value.map((item) => pagedFiltered.value[item.index]).filter((entry): entry is FetchViewEntry => Boolean(entry))
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
watch([filter, search], () => {
|
|
125
|
+
currentPage.value = 1
|
|
126
|
+
if (tableScrollRef.value) {
|
|
127
|
+
tableScrollRef.value.scrollTop = 0
|
|
128
|
+
}
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
watch(filtered, (nextRows) => {
|
|
132
|
+
const maxPage = Math.max(1, Math.ceil(nextRows.length / fetchPageSize.value))
|
|
133
|
+
|
|
134
|
+
if (currentPage.value > maxPage) {
|
|
135
|
+
currentPage.value = maxPage
|
|
136
|
+
}
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
function onTableScroll() {
|
|
140
|
+
const element = tableScrollRef.value
|
|
141
|
+
|
|
142
|
+
if (!element || !hasMoreRows.value) {
|
|
143
|
+
return
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const nearBottom = element.scrollTop + element.clientHeight >= element.scrollHeight - 32
|
|
147
|
+
|
|
148
|
+
if (nearBottom) {
|
|
149
|
+
currentPage.value += 1
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
51
153
|
const metaRows = computed(() => {
|
|
52
154
|
if (!selected.value) {
|
|
53
155
|
return []
|
|
@@ -91,11 +193,22 @@ function barColor(status: string) {
|
|
|
91
193
|
return { ok: 'var(--teal)', error: 'var(--red)', pending: 'var(--amber)', cached: 'var(--border)' }[status] ?? 'var(--border)'
|
|
92
194
|
}
|
|
93
195
|
|
|
196
|
+
const maxCompletedMs = computed(() => {
|
|
197
|
+
let maxMs = 1
|
|
198
|
+
|
|
199
|
+
for (const entry of entries.value) {
|
|
200
|
+
if (entry.ms != null && entry.ms > maxMs) {
|
|
201
|
+
maxMs = entry.ms
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return maxMs
|
|
206
|
+
})
|
|
207
|
+
|
|
94
208
|
function barWidth(entry: FetchViewEntry) {
|
|
95
209
|
// Only consider completed entries for the max, so pending entries don't
|
|
96
210
|
// collapse all bars to a dot while waiting.
|
|
97
|
-
const
|
|
98
|
-
const maxMs = completedMs.length > 0 ? Math.max(...completedMs, 1) : 1
|
|
211
|
+
const maxMs = maxCompletedMs.value
|
|
99
212
|
|
|
100
213
|
return entry.ms != null ? Math.max(4, Math.round((entry.ms / maxMs) * 100)) : 4
|
|
101
214
|
}
|
|
@@ -103,15 +216,26 @@ function barWidth(entry: FetchViewEntry) {
|
|
|
103
216
|
// Waterfall uses absolute time offsets from the earliest startTime.
|
|
104
217
|
// maxEnd is computed only from completed entries so that a long-running
|
|
105
218
|
// pending request doesn't squash all completed bars to invisible.
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
219
|
+
const waterfallScale = computed(() => {
|
|
220
|
+
let maxEnd = 1
|
|
221
|
+
|
|
222
|
+
for (const entry of entries.value) {
|
|
223
|
+
if (entry.ms == null) {
|
|
224
|
+
continue
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const end = entry.startOffset + entry.ms
|
|
228
|
+
|
|
229
|
+
if (end > maxEnd) {
|
|
230
|
+
maxEnd = end
|
|
231
|
+
}
|
|
232
|
+
}
|
|
109
233
|
|
|
110
234
|
return maxEnd
|
|
111
|
-
}
|
|
235
|
+
})
|
|
112
236
|
|
|
113
237
|
function wfLeft(entry: FetchViewEntry) {
|
|
114
|
-
const scale = waterfallScale
|
|
238
|
+
const scale = waterfallScale.value
|
|
115
239
|
return Math.min(98, Math.round((entry.startOffset / scale) * 100))
|
|
116
240
|
}
|
|
117
241
|
|
|
@@ -122,7 +246,7 @@ function wfWidth(entry: FetchViewEntry) {
|
|
|
122
246
|
return 2
|
|
123
247
|
}
|
|
124
248
|
|
|
125
|
-
const scale = waterfallScale
|
|
249
|
+
const scale = waterfallScale.value
|
|
126
250
|
const left = wfLeft(entry)
|
|
127
251
|
|
|
128
252
|
// Clamp so bar + left never exceeds 100%
|
|
@@ -173,7 +297,7 @@ function formatSize(bytes: number) {
|
|
|
173
297
|
</div>
|
|
174
298
|
|
|
175
299
|
<div class="fetch-dashboard__split tracker-split">
|
|
176
|
-
<div class="fetch-dashboard__table tracker-table-wrap">
|
|
300
|
+
<div ref="tableScrollRef" class="fetch-dashboard__table tracker-table-wrap" @scroll="onTableScroll">
|
|
177
301
|
<table class="data-table">
|
|
178
302
|
<thead>
|
|
179
303
|
<tr>
|
|
@@ -188,7 +312,14 @@ function formatSize(bytes: number) {
|
|
|
188
312
|
</thead>
|
|
189
313
|
<tbody>
|
|
190
314
|
<tr
|
|
191
|
-
v-
|
|
315
|
+
v-if="virtualizedRowsEnabled && topVirtualPadding > 0"
|
|
316
|
+
class="fetch-dashboard__virtual-spacer-row"
|
|
317
|
+
aria-hidden="true"
|
|
318
|
+
>
|
|
319
|
+
<td colspan="7" :style="{ height: `${topVirtualPadding}px` }"></td>
|
|
320
|
+
</tr>
|
|
321
|
+
<tr
|
|
322
|
+
v-for="entry in visibleRows"
|
|
192
323
|
:key="entry.id"
|
|
193
324
|
:class="{ selected: selected?.id === entry.id }"
|
|
194
325
|
@click="selectedId = entry.id"
|
|
@@ -221,16 +352,29 @@ function formatSize(bytes: number) {
|
|
|
221
352
|
</div>
|
|
222
353
|
</td>
|
|
223
354
|
</tr>
|
|
355
|
+
<tr
|
|
356
|
+
v-if="virtualizedRowsEnabled && bottomVirtualPadding > 0"
|
|
357
|
+
class="fetch-dashboard__virtual-spacer-row"
|
|
358
|
+
aria-hidden="true"
|
|
359
|
+
>
|
|
360
|
+
<td colspan="7" :style="{ height: `${bottomVirtualPadding}px` }"></td>
|
|
361
|
+
</tr>
|
|
224
362
|
<tr v-if="!filtered.length">
|
|
225
363
|
<td colspan="7" class="tracker-empty-cell">
|
|
226
364
|
{{ connected ? 'No fetches recorded yet.' : 'Waiting for connection to the Nuxt app…' }}
|
|
227
365
|
</td>
|
|
228
366
|
</tr>
|
|
367
|
+
<tr v-else class="fetch-dashboard__pagination-row" aria-live="polite">
|
|
368
|
+
<td colspan="7" class="fetch-dashboard__pagination-cell">
|
|
369
|
+
<span class="mono muted text-sm">showing {{ pagedFiltered.length }} of {{ filtered.length }}</span>
|
|
370
|
+
<span v-if="hasMoreRows" class="mono muted text-sm">scroll to load more</span>
|
|
371
|
+
</td>
|
|
372
|
+
</tr>
|
|
229
373
|
</tbody>
|
|
230
374
|
</table>
|
|
231
375
|
</div>
|
|
232
376
|
|
|
233
|
-
<div
|
|
377
|
+
<div class="tracker-resize-handle" @mousedown="onHandleMouseDown" />
|
|
234
378
|
|
|
235
379
|
<div v-if="selected" class="fetch-dashboard__detail tracker-detail-panel" :style="{ width: detailWidth + 'px' }">
|
|
236
380
|
<div class="fetch-dashboard__detail-header">
|
|
@@ -253,7 +397,7 @@ function formatSize(bytes: number) {
|
|
|
253
397
|
<div class="tracker-section-label fetch-dashboard__section-label fetch-dashboard__section-label--source">source</div>
|
|
254
398
|
<div class="mono text-sm muted">{{ selected.file }}:{{ selected.line }}</div>
|
|
255
399
|
</div>
|
|
256
|
-
<div v-else class="tracker-detail-empty">select a call to inspect</div>
|
|
400
|
+
<div v-else class="tracker-detail-empty" :style="{ width: detailWidth + 'px' }">select a call to inspect</div>
|
|
257
401
|
</div>
|
|
258
402
|
|
|
259
403
|
<div class="fetch-dashboard__waterfall">
|
|
@@ -297,6 +441,25 @@ function formatSize(bytes: number) {
|
|
|
297
441
|
min-width: 80px;
|
|
298
442
|
}
|
|
299
443
|
|
|
444
|
+
.fetch-dashboard__virtual-spacer-row td {
|
|
445
|
+
padding: 0;
|
|
446
|
+
border-bottom: 0;
|
|
447
|
+
background: transparent;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
.fetch-dashboard__pagination-row td {
|
|
451
|
+
background: transparent;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
.fetch-dashboard__pagination-cell {
|
|
455
|
+
display: flex;
|
|
456
|
+
align-items: center;
|
|
457
|
+
justify-content: space-between;
|
|
458
|
+
gap: var(--tracker-space-2);
|
|
459
|
+
padding-top: var(--tracker-space-2);
|
|
460
|
+
border-bottom: 0;
|
|
461
|
+
}
|
|
462
|
+
|
|
300
463
|
.fetch-dashboard__url {
|
|
301
464
|
max-width: 200px;
|
|
302
465
|
}
|
|
@@ -340,7 +503,7 @@ function formatSize(bytes: number) {
|
|
|
340
503
|
padding: var(--tracker-space-2) 10px;
|
|
341
504
|
overflow: auto;
|
|
342
505
|
white-space: pre;
|
|
343
|
-
|
|
506
|
+
min-height: fit-content;
|
|
344
507
|
}
|
|
345
508
|
|
|
346
509
|
.fetch-dashboard__waterfall {
|
|
@@ -349,6 +512,8 @@ function formatSize(bytes: number) {
|
|
|
349
512
|
border: var(--tracker-border-width) solid var(--border);
|
|
350
513
|
border-radius: var(--radius-lg);
|
|
351
514
|
padding: 10px var(--tracker-space-3);
|
|
515
|
+
max-height: 35%;
|
|
516
|
+
overflow-x: auto;
|
|
352
517
|
}
|
|
353
518
|
|
|
354
519
|
.fetch-dashboard__waterfall-header {
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// No store is accessed or created here, so Pinia Tracker will show 'no store available' by default.
|
|
3
|
+
import { computed, ref } from 'vue'
|
|
4
|
+
import { useObservatoryData, clearPiniaStores /* editPiniaState */ } from '@observatory-client/stores/observatory'
|
|
5
|
+
import type { PiniaMutationEvent, PiniaStoreEntry } from '@observatory/types/snapshot'
|
|
6
|
+
|
|
7
|
+
const { piniaStores, connected } = useObservatoryData()
|
|
8
|
+
|
|
9
|
+
const selectedStoreId = ref<string | null>(null)
|
|
10
|
+
const selectedEventId = ref<string | null>(null)
|
|
11
|
+
// Disabled until we can get it working properly. See comments in the template and applyEdit function for details.
|
|
12
|
+
// const editPath = ref('')
|
|
13
|
+
// const editValue = ref('')
|
|
14
|
+
const editError = ref('')
|
|
15
|
+
|
|
16
|
+
const stores = computed(() => [...piniaStores.value].sort((a, b) => a.id.localeCompare(b.id)))
|
|
17
|
+
|
|
18
|
+
const selectedStore = computed<PiniaStoreEntry | null>(() => {
|
|
19
|
+
if (!selectedStoreId.value) {
|
|
20
|
+
return stores.value[0] ?? null
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return stores.value.find((item) => item.id === selectedStoreId.value) ?? null
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const timeline = computed<PiniaMutationEvent[]>(() => {
|
|
27
|
+
return selectedStore.value ? [...selectedStore.value.timeline].slice().reverse() : []
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
const selectedEvent = computed<PiniaMutationEvent | null>(() => {
|
|
31
|
+
if (!selectedEventId.value) {
|
|
32
|
+
return timeline.value[0] ?? null
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return timeline.value.find((event) => event.id === selectedEventId.value) ?? null
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
function selectStore(id: string) {
|
|
39
|
+
selectedStoreId.value = id
|
|
40
|
+
selectedEventId.value = null
|
|
41
|
+
editError.value = ''
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function pretty(value: unknown) {
|
|
45
|
+
try {
|
|
46
|
+
return JSON.stringify(value, null, 2)
|
|
47
|
+
} catch {
|
|
48
|
+
return String(value)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function renderDuration(event: PiniaMutationEvent) {
|
|
53
|
+
if (typeof event.durationMs !== 'number') {
|
|
54
|
+
return '-'
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return `${event.durationMs.toFixed(2)}ms`
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* Edit functionality is temporarily disabled due to issues with state updates not being reflected in the UI. This is likely related to how the editPiniaState function applies changes and how the store's reactivity system detects those changes. Further investigation is needed to determine the root cause and implement a proper fix.
|
|
61
|
+
function applyEdit() {
|
|
62
|
+
const store = selectedStore.value
|
|
63
|
+
|
|
64
|
+
if (!store) {
|
|
65
|
+
editError.value = 'Select a store first.'
|
|
66
|
+
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!editPath.value.trim()) {
|
|
71
|
+
editError.value = 'Path is required (for example: preferences.theme).'
|
|
72
|
+
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let nextValue: unknown = editValue.value
|
|
77
|
+
|
|
78
|
+
if (editValue.value.trim()) {
|
|
79
|
+
try {
|
|
80
|
+
nextValue = JSON.parse(editValue.value)
|
|
81
|
+
} catch {
|
|
82
|
+
nextValue = editValue.value
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
editError.value = ''
|
|
87
|
+
editPiniaState(store.id, editPath.value.trim(), nextValue)
|
|
88
|
+
} */
|
|
89
|
+
</script>
|
|
90
|
+
|
|
91
|
+
<template>
|
|
92
|
+
<section class="pinia-tracker">
|
|
93
|
+
<header class="toolbar">
|
|
94
|
+
<h2>Pinia State and Mutations</h2>
|
|
95
|
+
<div class="toolbar-actions">
|
|
96
|
+
<span v-if="connected" class="status connected">Connected</span>
|
|
97
|
+
<span v-else class="status">Waiting for app</span>
|
|
98
|
+
<button class="danger" @click="clearPiniaStores">Clear timeline</button>
|
|
99
|
+
</div>
|
|
100
|
+
</header>
|
|
101
|
+
|
|
102
|
+
<div v-if="stores.length === 0" class="empty">No Pinia stores detected yet. Trigger store usage in the app.</div>
|
|
103
|
+
|
|
104
|
+
<div v-else class="content-grid">
|
|
105
|
+
<aside class="panel stores">
|
|
106
|
+
<h3>Stores</h3>
|
|
107
|
+
<button
|
|
108
|
+
v-for="store in stores"
|
|
109
|
+
:key="store.id"
|
|
110
|
+
class="store-row"
|
|
111
|
+
:class="{ active: selectedStore?.id === store.id }"
|
|
112
|
+
@click="selectStore(store.id)"
|
|
113
|
+
>
|
|
114
|
+
<span>{{ store.name }}</span>
|
|
115
|
+
<small>{{ store.timeline.length }} events</small>
|
|
116
|
+
</button>
|
|
117
|
+
</aside>
|
|
118
|
+
|
|
119
|
+
<section class="panel timeline">
|
|
120
|
+
<h3>Timeline</h3>
|
|
121
|
+
<div class="list">
|
|
122
|
+
<button
|
|
123
|
+
v-for="event in timeline"
|
|
124
|
+
:key="event.id"
|
|
125
|
+
class="event-row"
|
|
126
|
+
:class="{ active: selectedEvent?.id === event.id }"
|
|
127
|
+
@click="selectedEventId = event.id"
|
|
128
|
+
>
|
|
129
|
+
<strong>{{ event.kind }}</strong>
|
|
130
|
+
<span>{{ event.name }}</span>
|
|
131
|
+
<small>{{ renderDuration(event) }}</small>
|
|
132
|
+
</button>
|
|
133
|
+
</div>
|
|
134
|
+
</section>
|
|
135
|
+
|
|
136
|
+
<section class="panel inspector">
|
|
137
|
+
<h3>Inspector</h3>
|
|
138
|
+
|
|
139
|
+
<div v-if="selectedStore" class="block">
|
|
140
|
+
<h4>Current state</h4>
|
|
141
|
+
<pre>{{ pretty(selectedStore.state) }}</pre>
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
<!-- Edit functionality is temporarily disabled due to issues with state updates not being reflected in the UI. This is likely related to how the editPiniaState function applies changes and how the store's reactivity system detects those changes. Further investigation is needed to determine the root cause and implement a proper fix.
|
|
145
|
+
<div v-if="selectedStore" class="block edit-box">
|
|
146
|
+
<h4>Edit state path</h4>
|
|
147
|
+
<input v-model="editPath" placeholder="preferences.theme" />
|
|
148
|
+
<textarea v-model="editValue" rows="4" placeholder='"dark" or {"enabled":true}' />
|
|
149
|
+
<div class="actions">
|
|
150
|
+
<button @click="applyEdit">Apply edit</button>
|
|
151
|
+
<small v-if="editError" class="error">{{ editError }}</small>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
-->
|
|
155
|
+
|
|
156
|
+
<div v-if="selectedEvent" class="block">
|
|
157
|
+
<h4>Selected event</h4>
|
|
158
|
+
<p class="meta">
|
|
159
|
+
{{ selectedEvent.kind }} · {{ selectedEvent.name }} · {{ selectedEvent.status }} ·
|
|
160
|
+
{{ renderDuration(selectedEvent) }}
|
|
161
|
+
</p>
|
|
162
|
+
<h5>Diff</h5>
|
|
163
|
+
<ul class="diff-list">
|
|
164
|
+
<li v-for="item in selectedEvent.diff" :key="item.path">
|
|
165
|
+
<code>{{ item.path }}</code>
|
|
166
|
+
</li>
|
|
167
|
+
</ul>
|
|
168
|
+
<h5>Before</h5>
|
|
169
|
+
<pre>{{ pretty(selectedEvent.beforeState) }}</pre>
|
|
170
|
+
<h5>After</h5>
|
|
171
|
+
<pre>{{ pretty(selectedEvent.afterState) }}</pre>
|
|
172
|
+
</div>
|
|
173
|
+
</section>
|
|
174
|
+
</div>
|
|
175
|
+
</section>
|
|
176
|
+
</template>
|
|
177
|
+
|
|
178
|
+
<style scoped>
|
|
179
|
+
.pinia-tracker {
|
|
180
|
+
display: flex;
|
|
181
|
+
flex-direction: column;
|
|
182
|
+
height: 100%;
|
|
183
|
+
gap: 12px;
|
|
184
|
+
padding: 12px;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.toolbar {
|
|
188
|
+
display: flex;
|
|
189
|
+
justify-content: space-between;
|
|
190
|
+
align-items: center;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.toolbar h2 {
|
|
194
|
+
margin: 0;
|
|
195
|
+
font-size: 14px;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.toolbar-actions {
|
|
199
|
+
display: flex;
|
|
200
|
+
align-items: center;
|
|
201
|
+
gap: 8px;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.status {
|
|
205
|
+
color: var(--text3);
|
|
206
|
+
font-size: 12px;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.status.connected {
|
|
210
|
+
color: var(--green);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.content-grid {
|
|
214
|
+
display: grid;
|
|
215
|
+
grid-template-columns: 220px 280px 1fr;
|
|
216
|
+
gap: 10px;
|
|
217
|
+
min-height: 0;
|
|
218
|
+
flex: 1;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.panel {
|
|
222
|
+
border: 1px solid var(--border);
|
|
223
|
+
border-radius: 8px;
|
|
224
|
+
background: var(--bg2);
|
|
225
|
+
padding: 10px;
|
|
226
|
+
min-height: 0;
|
|
227
|
+
display: flex;
|
|
228
|
+
flex-direction: column;
|
|
229
|
+
gap: 8px;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.panel h3 {
|
|
233
|
+
margin: 0;
|
|
234
|
+
font-size: 12px;
|
|
235
|
+
color: var(--text2);
|
|
236
|
+
letter-spacing: 0.02em;
|
|
237
|
+
text-transform: uppercase;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
.stores,
|
|
241
|
+
.timeline,
|
|
242
|
+
.inspector,
|
|
243
|
+
.list {
|
|
244
|
+
overflow: auto;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.store-row,
|
|
248
|
+
.event-row {
|
|
249
|
+
width: 100%;
|
|
250
|
+
border: 1px solid var(--border);
|
|
251
|
+
border-radius: 6px;
|
|
252
|
+
background: var(--bg3);
|
|
253
|
+
color: var(--text);
|
|
254
|
+
text-align: left;
|
|
255
|
+
padding: 8px;
|
|
256
|
+
display: flex;
|
|
257
|
+
flex-direction: column;
|
|
258
|
+
gap: 2px;
|
|
259
|
+
cursor: pointer;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.store-row.active,
|
|
263
|
+
.event-row.active {
|
|
264
|
+
border-color: var(--purple);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
.store-row small,
|
|
268
|
+
.event-row small {
|
|
269
|
+
color: var(--text3);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.block {
|
|
273
|
+
border-top: 1px solid var(--border);
|
|
274
|
+
padding-top: 8px;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.block h4,
|
|
278
|
+
.block h5 {
|
|
279
|
+
margin: 0 0 6px;
|
|
280
|
+
font-size: 12px;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.meta {
|
|
284
|
+
margin: 0 0 6px;
|
|
285
|
+
color: var(--text2);
|
|
286
|
+
font-size: 12px;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
pre {
|
|
290
|
+
margin: 0;
|
|
291
|
+
background: var(--bg3);
|
|
292
|
+
border: 1px solid var(--border);
|
|
293
|
+
border-radius: 6px;
|
|
294
|
+
padding: 8px;
|
|
295
|
+
white-space: pre-wrap;
|
|
296
|
+
word-break: break-word;
|
|
297
|
+
font-size: 11px;
|
|
298
|
+
max-height: 200px;
|
|
299
|
+
overflow: auto;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
.edit-box input,
|
|
303
|
+
.edit-box textarea {
|
|
304
|
+
width: 100%;
|
|
305
|
+
background: var(--bg3);
|
|
306
|
+
border: 1px solid var(--border);
|
|
307
|
+
border-radius: 6px;
|
|
308
|
+
color: var(--text);
|
|
309
|
+
padding: 6px;
|
|
310
|
+
font-size: 12px;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
.actions {
|
|
314
|
+
display: flex;
|
|
315
|
+
align-items: center;
|
|
316
|
+
gap: 8px;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
.error {
|
|
320
|
+
color: var(--red);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
.empty {
|
|
324
|
+
padding: 24px;
|
|
325
|
+
border: 1px dashed var(--border);
|
|
326
|
+
border-radius: 8px;
|
|
327
|
+
color: var(--text2);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
.diff-list {
|
|
331
|
+
margin: 0;
|
|
332
|
+
padding-left: 16px;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
.danger {
|
|
336
|
+
border: 1px solid var(--border);
|
|
337
|
+
border-radius: 6px;
|
|
338
|
+
background: var(--bg2);
|
|
339
|
+
color: var(--text);
|
|
340
|
+
padding: 6px 10px;
|
|
341
|
+
cursor: pointer;
|
|
342
|
+
}
|
|
343
|
+
</style>
|