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