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.
@@ -1,10 +1,13 @@
1
1
  <script setup lang="ts">
2
2
  import { ref, computed } from 'vue'
3
- import { useObservatoryData, type FetchEntry } from '../stores/observatory'
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" style="color: var(--teal)">{{ counts.ok }}</div>
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" style="color: var(--amber)">{{ counts.pending }}</div>
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" style="color: var(--red)">{{ counts.error }}</div>
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 v-model="search" type="search" placeholder="search key or url…" style="max-width: 240px; margin-left: auto" />
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 style="min-width: 80px">bar</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" style="font-size: 11px; color: var(--text2)">{{ entry.key }}</span>
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 style="height: 4px; background: var(--bg2); border-radius: 2px; overflow: hidden">
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" style="text-align: center; color: var(--text3); padding: 24px">
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="detail-panel">
234
- <div class="detail-header">
235
- <span class="mono bold" style="font-size: 12px">{{ selected.key }}</span>
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="meta-grid">
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" style="word-break: break-all">{{ value }}</span>
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="payload-box">{{ payloadStr }}</pre>
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" style="margin-top: 10px">source</div>
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="waterfall">
258
- <div class="waterfall-header">
259
- <div class="section-label" style="margin-top: 0; margin-bottom: 0">waterfall</div>
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="waterfall-body">
266
- <div v-for="entry in entries" :key="entry.id" class="wf-row">
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="wf-track">
272
+ <div class="fetch-dashboard__waterfall-track">
274
273
  <div
275
- class="wf-bar"
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" style="width: 44px; text-align: right; flex-shrink: 0">
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
- .view {
294
- display: flex;
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
- .split {
318
- display: flex;
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
- .table-wrap {
326
- flex: 1;
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
- .detail-empty {
346
- width: 280px;
347
- flex-shrink: 0;
304
+ .fetch-dashboard__detail-header {
348
305
  display: flex;
349
306
  align-items: center;
350
- justify-content: center;
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
- .detail-header {
358
- display: flex;
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
- .meta-grid {
314
+ .fetch-dashboard__meta-grid {
364
315
  display: grid;
365
316
  grid-template-columns: auto 1fr;
366
- gap: 4px 12px;
367
- font-size: 11px;
317
+ gap: var(--tracker-space-1) var(--tracker-space-3);
318
+ font-size: var(--tracker-font-size-sm);
368
319
  }
369
320
 
370
- .section-label {
371
- font-size: 10px;
372
- font-weight: 500;
373
- text-transform: uppercase;
374
- letter-spacing: 0.4px;
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
- .payload-box {
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: 11px;
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: 8px 10px;
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
- .waterfall {
346
+ .fetch-dashboard__waterfall {
393
347
  flex-shrink: 0;
394
348
  background: var(--bg3);
395
- border: 0.5px solid var(--border);
349
+ border: var(--tracker-border-width) solid var(--border);
396
350
  border-radius: var(--radius-lg);
397
- padding: 10px 12px;
351
+ padding: 10px var(--tracker-space-3);
398
352
  }
399
353
 
400
- .waterfall-header {
354
+ .fetch-dashboard__waterfall-header {
401
355
  display: flex;
402
356
  align-items: center;
403
357
  justify-content: space-between;
404
- gap: 8px;
358
+ gap: var(--tracker-space-2);
405
359
  }
406
360
 
407
- .waterfall-body {
408
- margin-top: 6px;
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
- .wf-row {
369
+ .fetch-dashboard__waterfall-row {
412
370
  display: flex;
413
371
  align-items: center;
414
- gap: 8px;
415
- margin-bottom: 4px;
372
+ gap: var(--tracker-space-2);
373
+ margin-bottom: var(--tracker-space-1);
416
374
  }
417
375
 
418
- .wf-track {
419
- flex: 1;
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
- .wf-bar {
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>