nuxt-devtools-observatory 0.1.26 → 0.1.30
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 +50 -11
- package/client/dist/assets/index-BCaKoHBH.js +17 -0
- package/client/dist/assets/index-BmGW_M3W.css +1 -0
- package/client/dist/index.html +2 -2
- package/client/src/App.vue +2 -6
- package/client/src/composables/useResizablePane.ts +65 -0
- package/client/src/stores/observatory.ts +162 -218
- package/client/src/style.css +203 -28
- package/client/src/views/ComposableTracker.vue +327 -294
- package/client/src/views/FetchDashboard.vue +104 -132
- package/client/src/views/ProvideInjectGraph.vue +101 -118
- package/client/src/views/RenderHeatmap.vue +192 -157
- package/client/src/views/TransitionTimeline.vue +166 -130
- package/client/tsconfig.json +3 -1
- package/client/vite.config.ts +12 -1
- package/dist/module.d.mts +6 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +238 -186
- package/dist/runtime/composables/fetch-registry.js +3 -0
- package/dist/runtime/plugin.js +89 -66
- package/package.json +12 -3
- package/client/dist/assets/index-1-H6UMCK.css +0 -1
- package/client/dist/assets/index-eSUuhYQ0.js +0 -17
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { ref, computed } from 'vue'
|
|
3
|
-
import {
|
|
3
|
+
import { useResizablePane } from '@observatory-client/composables/useResizablePane'
|
|
4
|
+
import { useObservatoryData } from '@observatory-client/stores/observatory'
|
|
5
|
+
import type { FetchEntry } from '@observatory/types/snapshot'
|
|
4
6
|
|
|
5
7
|
type FetchViewEntry = FetchEntry & { startOffset: number }
|
|
6
8
|
|
|
7
9
|
const { fetch, connected } = useObservatoryData()
|
|
10
|
+
const { paneWidth: detailWidth, onHandleMouseDown } = useResizablePane(280, 'observatory:fetch:detailWidth')
|
|
8
11
|
|
|
9
12
|
const filter = ref<string>('all')
|
|
10
13
|
const search = ref('')
|
|
@@ -93,6 +96,7 @@ function barWidth(entry: FetchViewEntry) {
|
|
|
93
96
|
// collapse all bars to a dot while waiting.
|
|
94
97
|
const completedMs = entries.value.filter((e) => e.ms != null).map((e) => e.ms!)
|
|
95
98
|
const maxMs = completedMs.length > 0 ? Math.max(...completedMs, 1) : 1
|
|
99
|
+
|
|
96
100
|
return entry.ms != null ? Math.max(4, Math.round((entry.ms / maxMs) * 100)) : 4
|
|
97
101
|
}
|
|
98
102
|
|
|
@@ -102,6 +106,7 @@ function barWidth(entry: FetchViewEntry) {
|
|
|
102
106
|
function waterfallScale() {
|
|
103
107
|
const completed = entries.value.filter((e) => e.ms != null)
|
|
104
108
|
const maxEnd = completed.length > 0 ? Math.max(...completed.map((e) => e.startOffset + e.ms!), 1) : 1
|
|
109
|
+
|
|
105
110
|
return maxEnd
|
|
106
111
|
}
|
|
107
112
|
|
|
@@ -116,8 +121,10 @@ function wfWidth(entry: FetchViewEntry) {
|
|
|
116
121
|
// a zero-width invisible bar.
|
|
117
122
|
return 2
|
|
118
123
|
}
|
|
124
|
+
|
|
119
125
|
const scale = waterfallScale()
|
|
120
126
|
const left = wfLeft(entry)
|
|
127
|
+
|
|
121
128
|
// Clamp so bar + left never exceeds 100%
|
|
122
129
|
return Math.min(100 - left, Math.max(2, Math.round((entry.ms / scale) * 100)))
|
|
123
130
|
}
|
|
@@ -132,36 +139,41 @@ function formatSize(bytes: number) {
|
|
|
132
139
|
</script>
|
|
133
140
|
|
|
134
141
|
<template>
|
|
135
|
-
<div class="view">
|
|
136
|
-
<div class="stats-row">
|
|
142
|
+
<div class="fetch-dashboard tracker-view">
|
|
143
|
+
<div class="fetch-dashboard__stats tracker-stats-row">
|
|
137
144
|
<div class="stat-card">
|
|
138
145
|
<div class="stat-label">total</div>
|
|
139
146
|
<div class="stat-val">{{ entries.length }}</div>
|
|
140
147
|
</div>
|
|
141
148
|
<div class="stat-card">
|
|
142
149
|
<div class="stat-label">success</div>
|
|
143
|
-
<div class="stat-val
|
|
150
|
+
<div class="stat-val stat-val--ok">{{ counts.ok }}</div>
|
|
144
151
|
</div>
|
|
145
152
|
<div class="stat-card">
|
|
146
153
|
<div class="stat-label">pending</div>
|
|
147
|
-
<div class="stat-val
|
|
154
|
+
<div class="stat-val stat-val--pending">{{ counts.pending }}</div>
|
|
148
155
|
</div>
|
|
149
156
|
<div class="stat-card">
|
|
150
157
|
<div class="stat-label">error</div>
|
|
151
|
-
<div class="stat-val
|
|
158
|
+
<div class="stat-val stat-val--error">{{ counts.error }}</div>
|
|
152
159
|
</div>
|
|
153
160
|
</div>
|
|
154
161
|
|
|
155
|
-
<div class="toolbar">
|
|
162
|
+
<div class="fetch-dashboard__toolbar tracker-toolbar">
|
|
156
163
|
<button :class="{ active: filter === 'all' }" @click="filter = 'all'">all</button>
|
|
157
164
|
<button :class="{ 'danger-active': filter === 'error' }" @click="filter = 'error'">errors</button>
|
|
158
165
|
<button :class="{ active: filter === 'pending' }" @click="filter = 'pending'">pending</button>
|
|
159
166
|
<button :class="{ active: filter === 'cached' }" @click="filter = 'cached'">cached</button>
|
|
160
|
-
<input
|
|
167
|
+
<input
|
|
168
|
+
v-model="search"
|
|
169
|
+
type="search"
|
|
170
|
+
class="fetch-dashboard__search tracker-toolbar__spacer"
|
|
171
|
+
placeholder="search key or url…"
|
|
172
|
+
/>
|
|
161
173
|
</div>
|
|
162
174
|
|
|
163
|
-
<div class="split">
|
|
164
|
-
<div class="table-wrap">
|
|
175
|
+
<div class="fetch-dashboard__split tracker-split">
|
|
176
|
+
<div class="fetch-dashboard__table tracker-table-wrap">
|
|
165
177
|
<table class="data-table">
|
|
166
178
|
<thead>
|
|
167
179
|
<tr>
|
|
@@ -171,7 +183,7 @@ function formatSize(bytes: number) {
|
|
|
171
183
|
<th>origin</th>
|
|
172
184
|
<th>size</th>
|
|
173
185
|
<th>time</th>
|
|
174
|
-
<th
|
|
186
|
+
<th class="fetch-dashboard__bar-column">bar</th>
|
|
175
187
|
</tr>
|
|
176
188
|
</thead>
|
|
177
189
|
<tbody>
|
|
@@ -182,21 +194,10 @@ function formatSize(bytes: number) {
|
|
|
182
194
|
@click="selectedId = entry.id"
|
|
183
195
|
>
|
|
184
196
|
<td>
|
|
185
|
-
<span class="mono
|
|
197
|
+
<span class="fetch-dashboard__key mono tracker-mono-secondary">{{ entry.key }}</span>
|
|
186
198
|
</td>
|
|
187
199
|
<td>
|
|
188
|
-
<span
|
|
189
|
-
class="mono"
|
|
190
|
-
style="
|
|
191
|
-
font-size: 11px;
|
|
192
|
-
max-width: 200px;
|
|
193
|
-
display: block;
|
|
194
|
-
overflow: hidden;
|
|
195
|
-
text-overflow: ellipsis;
|
|
196
|
-
white-space: nowrap;
|
|
197
|
-
"
|
|
198
|
-
:title="entry.url"
|
|
199
|
-
>
|
|
200
|
+
<span class="fetch-dashboard__url mono tracker-mono-secondary tracker-truncate" :title="entry.url">
|
|
200
201
|
{{ entry.url }}
|
|
201
202
|
</span>
|
|
202
203
|
</td>
|
|
@@ -209,20 +210,19 @@ function formatSize(bytes: number) {
|
|
|
209
210
|
<td class="muted text-sm">{{ entry.size ? formatSize(entry.size) : '—' }}</td>
|
|
210
211
|
<td class="mono text-sm">{{ entry.ms != null ? `${entry.ms}ms` : '—' }}</td>
|
|
211
212
|
<td>
|
|
212
|
-
<div
|
|
213
|
+
<div class="fetch-dashboard__bar-track tracker-progress-bar">
|
|
213
214
|
<div
|
|
215
|
+
class="fetch-dashboard__bar-fill tracker-progress-bar__fill"
|
|
214
216
|
:style="{
|
|
215
217
|
width: `${barWidth(entry)}%`,
|
|
216
218
|
background: barColor(entry.status),
|
|
217
|
-
height: '100%',
|
|
218
|
-
borderRadius: '2px',
|
|
219
219
|
}"
|
|
220
220
|
></div>
|
|
221
221
|
</div>
|
|
222
222
|
</td>
|
|
223
223
|
</tr>
|
|
224
224
|
<tr v-if="!filtered.length">
|
|
225
|
-
<td colspan="7"
|
|
225
|
+
<td colspan="7" class="tracker-empty-cell">
|
|
226
226
|
{{ connected ? 'No fetches recorded yet.' : 'Waiting for connection to the Nuxt app…' }}
|
|
227
227
|
</td>
|
|
228
228
|
</tr>
|
|
@@ -230,49 +230,48 @@ function formatSize(bytes: number) {
|
|
|
230
230
|
</table>
|
|
231
231
|
</div>
|
|
232
232
|
|
|
233
|
-
<div v-if="selected" class="
|
|
234
|
-
|
|
235
|
-
|
|
233
|
+
<div v-if="selected" class="tracker-resize-handle" @mousedown="onHandleMouseDown" />
|
|
234
|
+
|
|
235
|
+
<div v-if="selected" class="fetch-dashboard__detail tracker-detail-panel" :style="{ width: detailWidth + 'px' }">
|
|
236
|
+
<div class="fetch-dashboard__detail-header">
|
|
237
|
+
<span class="fetch-dashboard__detail-title mono bold">{{ selected.key }}</span>
|
|
236
238
|
<div class="flex gap-2">
|
|
237
239
|
<button @click="selectedId = null">×</button>
|
|
238
240
|
</div>
|
|
239
241
|
</div>
|
|
240
242
|
|
|
241
|
-
<div class="
|
|
243
|
+
<div class="fetch-dashboard__meta-grid">
|
|
242
244
|
<template v-for="[key, value] in metaRows" :key="key">
|
|
243
245
|
<span class="muted text-sm">{{ key }}</span>
|
|
244
|
-
<span class="mono text-sm"
|
|
246
|
+
<span class="fetch-dashboard__meta-value mono text-sm">{{ value }}</span>
|
|
245
247
|
</template>
|
|
246
248
|
</div>
|
|
247
249
|
|
|
248
|
-
<div class="section-label">payload</div>
|
|
249
|
-
<pre class="
|
|
250
|
+
<div class="tracker-section-label fetch-dashboard__section-label">payload</div>
|
|
251
|
+
<pre class="fetch-dashboard__payload-box">{{ payloadStr }}</pre>
|
|
250
252
|
|
|
251
|
-
<div class="section-label
|
|
253
|
+
<div class="tracker-section-label fetch-dashboard__section-label fetch-dashboard__section-label--source">source</div>
|
|
252
254
|
<div class="mono text-sm muted">{{ selected.file }}:{{ selected.line }}</div>
|
|
253
255
|
</div>
|
|
254
|
-
<div v-else class="detail-empty">select a call to inspect</div>
|
|
256
|
+
<div v-else class="tracker-detail-empty">select a call to inspect</div>
|
|
255
257
|
</div>
|
|
256
258
|
|
|
257
|
-
<div class="
|
|
258
|
-
<div class="
|
|
259
|
-
<div class="section-label
|
|
259
|
+
<div class="fetch-dashboard__waterfall">
|
|
260
|
+
<div class="fetch-dashboard__waterfall-header">
|
|
261
|
+
<div class="tracker-section-label fetch-dashboard__waterfall-label">waterfall</div>
|
|
260
262
|
<button :class="{ active: waterfallOpen }" @click="waterfallOpen = !waterfallOpen">
|
|
261
263
|
{{ waterfallOpen ? 'hide' : 'show' }}
|
|
262
264
|
</button>
|
|
263
265
|
</div>
|
|
264
266
|
|
|
265
|
-
<div v-if="waterfallOpen" class="
|
|
266
|
-
<div v-for="entry in entries" :key="entry.id" class="
|
|
267
|
-
<span
|
|
268
|
-
class="mono muted text-sm"
|
|
269
|
-
style="width: 140px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 0"
|
|
270
|
-
>
|
|
267
|
+
<div v-if="waterfallOpen" class="fetch-dashboard__waterfall-body">
|
|
268
|
+
<div v-for="entry in entries" :key="entry.id" class="fetch-dashboard__waterfall-row">
|
|
269
|
+
<span class="fetch-dashboard__waterfall-key mono muted text-sm">
|
|
271
270
|
{{ entry.key }}
|
|
272
271
|
</span>
|
|
273
|
-
<div class="
|
|
272
|
+
<div class="fetch-dashboard__waterfall-track">
|
|
274
273
|
<div
|
|
275
|
-
class="
|
|
274
|
+
class="fetch-dashboard__waterfall-bar"
|
|
276
275
|
:style="{
|
|
277
276
|
left: `${wfLeft(entry)}%`,
|
|
278
277
|
width: `${Math.max(2, wfWidth(entry))}%`,
|
|
@@ -280,7 +279,7 @@ function formatSize(bytes: number) {
|
|
|
280
279
|
}"
|
|
281
280
|
></div>
|
|
282
281
|
</div>
|
|
283
|
-
<span class="mono muted text-sm"
|
|
282
|
+
<span class="fetch-dashboard__waterfall-time mono muted text-sm">
|
|
284
283
|
{{ entry.ms != null ? `${entry.ms}ms` : '—' }}
|
|
285
284
|
</span>
|
|
286
285
|
</div>
|
|
@@ -290,145 +289,118 @@ function formatSize(bytes: number) {
|
|
|
290
289
|
</template>
|
|
291
290
|
|
|
292
291
|
<style scoped>
|
|
293
|
-
.
|
|
294
|
-
|
|
295
|
-
flex-direction: column;
|
|
296
|
-
height: 100%;
|
|
297
|
-
overflow: hidden;
|
|
298
|
-
padding: 12px;
|
|
299
|
-
gap: 10px;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
.stats-row {
|
|
303
|
-
display: grid;
|
|
304
|
-
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
305
|
-
gap: 8px;
|
|
306
|
-
flex-shrink: 0;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
.toolbar {
|
|
310
|
-
display: flex;
|
|
311
|
-
align-items: center;
|
|
312
|
-
gap: 6px;
|
|
313
|
-
flex-shrink: 0;
|
|
314
|
-
flex-wrap: wrap;
|
|
292
|
+
.fetch-dashboard__search {
|
|
293
|
+
max-width: 240px;
|
|
315
294
|
}
|
|
316
295
|
|
|
317
|
-
.
|
|
318
|
-
|
|
319
|
-
gap: 12px;
|
|
320
|
-
flex: 1;
|
|
321
|
-
overflow: hidden;
|
|
322
|
-
min-height: 0;
|
|
296
|
+
.fetch-dashboard__bar-column {
|
|
297
|
+
min-width: 80px;
|
|
323
298
|
}
|
|
324
299
|
|
|
325
|
-
.
|
|
326
|
-
|
|
327
|
-
overflow: auto;
|
|
328
|
-
border: 0.5px solid var(--border);
|
|
329
|
-
border-radius: var(--radius-lg);
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
.detail-panel {
|
|
333
|
-
width: 280px;
|
|
334
|
-
flex-shrink: 0;
|
|
335
|
-
display: flex;
|
|
336
|
-
flex-direction: column;
|
|
337
|
-
gap: 8px;
|
|
338
|
-
overflow: auto;
|
|
339
|
-
border: 0.5px solid var(--border);
|
|
340
|
-
border-radius: var(--radius-lg);
|
|
341
|
-
padding: 12px;
|
|
342
|
-
background: var(--bg3);
|
|
300
|
+
.fetch-dashboard__url {
|
|
301
|
+
max-width: 200px;
|
|
343
302
|
}
|
|
344
303
|
|
|
345
|
-
.
|
|
346
|
-
width: 280px;
|
|
347
|
-
flex-shrink: 0;
|
|
304
|
+
.fetch-dashboard__detail-header {
|
|
348
305
|
display: flex;
|
|
349
306
|
align-items: center;
|
|
350
|
-
justify-content:
|
|
351
|
-
color: var(--text3);
|
|
352
|
-
font-size: 12px;
|
|
353
|
-
border: 0.5px dashed var(--border);
|
|
354
|
-
border-radius: var(--radius-lg);
|
|
307
|
+
justify-content: space-between;
|
|
355
308
|
}
|
|
356
309
|
|
|
357
|
-
.
|
|
358
|
-
|
|
359
|
-
align-items: center;
|
|
360
|
-
justify-content: space-between;
|
|
310
|
+
.fetch-dashboard__detail-title {
|
|
311
|
+
font-size: var(--tracker-font-size-md);
|
|
361
312
|
}
|
|
362
313
|
|
|
363
|
-
.
|
|
314
|
+
.fetch-dashboard__meta-grid {
|
|
364
315
|
display: grid;
|
|
365
316
|
grid-template-columns: auto 1fr;
|
|
366
|
-
gap:
|
|
367
|
-
font-size:
|
|
317
|
+
gap: var(--tracker-space-1) var(--tracker-space-3);
|
|
318
|
+
font-size: var(--tracker-font-size-sm);
|
|
368
319
|
}
|
|
369
320
|
|
|
370
|
-
.
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
color: var(--text3);
|
|
321
|
+
.fetch-dashboard__meta-value {
|
|
322
|
+
word-break: break-all;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
.fetch-dashboard__section-label {
|
|
376
326
|
margin-top: 6px;
|
|
377
327
|
min-height: fit-content;
|
|
378
328
|
}
|
|
379
329
|
|
|
380
|
-
.
|
|
330
|
+
.fetch-dashboard__section-label--source {
|
|
331
|
+
margin-top: 10px;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
.fetch-dashboard__payload-box {
|
|
381
335
|
font-family: var(--mono);
|
|
382
|
-
font-size:
|
|
336
|
+
font-size: var(--tracker-font-size-sm);
|
|
383
337
|
color: var(--text2);
|
|
384
338
|
background: var(--bg2);
|
|
385
339
|
border-radius: var(--radius);
|
|
386
|
-
padding:
|
|
340
|
+
padding: var(--tracker-space-2) 10px;
|
|
387
341
|
overflow: auto;
|
|
388
342
|
white-space: pre;
|
|
389
343
|
max-height: 160px;
|
|
390
344
|
}
|
|
391
345
|
|
|
392
|
-
.
|
|
346
|
+
.fetch-dashboard__waterfall {
|
|
393
347
|
flex-shrink: 0;
|
|
394
348
|
background: var(--bg3);
|
|
395
|
-
border:
|
|
349
|
+
border: var(--tracker-border-width) solid var(--border);
|
|
396
350
|
border-radius: var(--radius-lg);
|
|
397
|
-
padding: 10px
|
|
351
|
+
padding: 10px var(--tracker-space-3);
|
|
398
352
|
}
|
|
399
353
|
|
|
400
|
-
.
|
|
354
|
+
.fetch-dashboard__waterfall-header {
|
|
401
355
|
display: flex;
|
|
402
356
|
align-items: center;
|
|
403
357
|
justify-content: space-between;
|
|
404
|
-
gap:
|
|
358
|
+
gap: var(--tracker-space-2);
|
|
405
359
|
}
|
|
406
360
|
|
|
407
|
-
.
|
|
408
|
-
margin
|
|
361
|
+
.fetch-dashboard__waterfall-label {
|
|
362
|
+
margin: 0;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
.fetch-dashboard__waterfall-body {
|
|
366
|
+
margin-top: var(--tracker-gap-toolbar);
|
|
409
367
|
}
|
|
410
368
|
|
|
411
|
-
.
|
|
369
|
+
.fetch-dashboard__waterfall-row {
|
|
412
370
|
display: flex;
|
|
413
371
|
align-items: center;
|
|
414
|
-
gap:
|
|
415
|
-
margin-bottom:
|
|
372
|
+
gap: var(--tracker-space-2);
|
|
373
|
+
margin-bottom: var(--tracker-space-1);
|
|
416
374
|
}
|
|
417
375
|
|
|
418
|
-
.
|
|
419
|
-
|
|
376
|
+
.fetch-dashboard__waterfall-key {
|
|
377
|
+
width: 140px;
|
|
378
|
+
overflow: hidden;
|
|
379
|
+
text-overflow: ellipsis;
|
|
380
|
+
white-space: nowrap;
|
|
381
|
+
flex-shrink: 0;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
.fetch-dashboard__waterfall-track {
|
|
420
385
|
position: relative;
|
|
386
|
+
flex: 1;
|
|
421
387
|
height: 8px;
|
|
422
388
|
background: var(--bg2);
|
|
423
389
|
border-radius: 2px;
|
|
424
390
|
overflow: hidden;
|
|
425
391
|
}
|
|
426
392
|
|
|
427
|
-
.
|
|
393
|
+
.fetch-dashboard__waterfall-bar {
|
|
428
394
|
position: absolute;
|
|
429
395
|
top: 0;
|
|
430
396
|
height: 100%;
|
|
431
397
|
border-radius: 2px;
|
|
432
398
|
opacity: 0.8;
|
|
433
399
|
}
|
|
400
|
+
|
|
401
|
+
.fetch-dashboard__waterfall-time {
|
|
402
|
+
width: 44px;
|
|
403
|
+
text-align: right;
|
|
404
|
+
flex-shrink: 0;
|
|
405
|
+
}
|
|
434
406
|
</style>
|