nuxt-devtools-observatory 0.1.6 → 0.1.7

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,109 +1,118 @@
1
1
  <script setup lang="ts">
2
2
  import { ref, computed } from 'vue'
3
- import { useObservatoryData } from '../stores/observatory'
4
-
5
- interface FetchEntry {
6
- id: string
7
- key: string
8
- url: string
9
- status: 'pending' | 'ok' | 'error' | 'cached'
10
- origin: 'ssr' | 'csr'
11
- ms?: number
12
- size?: number
13
- cached: boolean
14
- payload?: unknown
15
- file?: string
16
- line?: number
17
- startOffset?: number
18
- }
3
+ import { useObservatoryData, type FetchEntry } from '../stores/observatory'
4
+
5
+ type FetchViewEntry = FetchEntry & { startOffset: number }
19
6
 
20
- // Use live fetch data from the Nuxt registry bridge
21
- const { fetches: entries } = useObservatoryData()
7
+ const { fetch, connected } = useObservatoryData()
22
8
 
23
9
  const filter = ref<string>('all')
24
10
  const search = ref('')
25
- const selected = ref<FetchEntry | null>(null)
11
+ const selectedId = ref<string | null>(null)
12
+
13
+ const entries = computed<FetchViewEntry[]>(() => {
14
+ const sorted = [...fetch.value].sort((a, b) => a.startTime - b.startTime)
15
+ const minStart = Math.min(...sorted.map((entry) => entry.startTime), 0)
16
+
17
+ return sorted.map((entry) => ({
18
+ ...entry,
19
+ startOffset: Math.max(0, Math.round(entry.startTime - minStart)),
20
+ }))
21
+ })
22
+
23
+ const selected = computed(() => entries.value.find((entry) => entry.id === selectedId.value) ?? null)
26
24
 
27
25
  const counts = computed(() => ({
28
- ok: entries.value?.filter((e) => e.status === 'ok').length ?? 0,
29
- pending: entries.value?.filter((e) => e.status === 'pending').length ?? 0,
30
- error: entries.value?.filter((e) => e.status === 'error').length ?? 0,
26
+ ok: entries.value.filter((entry) => entry.status === 'ok').length,
27
+ pending: entries.value.filter((entry) => entry.status === 'pending').length,
28
+ error: entries.value.filter((entry) => entry.status === 'error').length,
31
29
  }))
32
30
 
33
31
  const filtered = computed(() => {
34
- return (entries.value ?? []).filter((e) => {
35
- if (filter.value !== 'all' && e.status !== filter.value) return false
32
+ return entries.value.filter((entry) => {
33
+ if (filter.value !== 'all' && entry.status !== filter.value) {
34
+ return false
35
+ }
36
+
36
37
  const q = search.value.toLowerCase()
37
- if (q && !e.key.includes(q) && !e.url.includes(q)) return false
38
+
39
+ if (q && !entry.key.toLowerCase().includes(q) && !entry.url.toLowerCase().includes(q)) {
40
+ return false
41
+ }
42
+
38
43
  return true
39
44
  })
40
45
  })
41
46
 
42
47
  const metaRows = computed(() => {
43
- if (!selected.value) return []
44
- const e = selected.value
48
+ if (!selected.value) {
49
+ return []
50
+ }
51
+
52
+ const entry = selected.value
53
+
45
54
  return [
46
- ['url', e.url],
47
- ['status', e.status],
48
- ['origin', e.origin],
49
- ['duration', e.ms != null ? e.ms + 'ms' : '—'],
50
- ['size', e.size ? formatSize(e.size) : '—'],
51
- ['cached', e.cached ? 'yes' : 'no'],
55
+ ['url', entry.url],
56
+ ['status', entry.status],
57
+ ['origin', entry.origin],
58
+ ['duration', entry.ms != null ? `${entry.ms}ms` : '—'],
59
+ ['size', entry.size ? formatSize(entry.size) : '—'],
60
+ ['cached', entry.cached ? 'yes' : 'no'],
52
61
  ]
53
62
  })
54
63
 
55
64
  const payloadStr = computed(() => {
56
- if (!selected.value) return ''
57
- const p = selected.value.payload
58
- if (!p) return '(no payload captured yet)'
65
+ if (!selected.value) {
66
+ return ''
67
+ }
68
+
69
+ const payload = selected.value.payload
70
+
71
+ if (payload === undefined) {
72
+ return '(no payload captured yet)'
73
+ }
74
+
59
75
  try {
60
- return JSON.stringify(p, null, 2)
76
+ return JSON.stringify(payload, null, 2)
61
77
  } catch {
62
- return String(p)
78
+ return String(payload)
63
79
  }
64
80
  })
65
81
 
66
- function statusClass(s: string) {
67
- return { ok: 'badge-ok', error: 'badge-err', pending: 'badge-warn', cached: 'badge-gray' }[s] ?? 'badge-gray'
82
+ function statusClass(status: string) {
83
+ return { ok: 'badge-ok', error: 'badge-err', pending: 'badge-warn', cached: 'badge-gray' }[status] ?? 'badge-gray'
68
84
  }
69
85
 
70
- function barColor(s: string) {
71
- return { ok: 'var(--teal)', error: 'var(--red)', pending: 'var(--amber)', cached: 'var(--border)' }[s] ?? 'var(--border)'
86
+ function barColor(status: string) {
87
+ return { ok: 'var(--teal)', error: 'var(--red)', pending: 'var(--amber)', cached: 'var(--border)' }[status] ?? 'var(--border)'
72
88
  }
73
89
 
74
- function barWidth(e: FetchEntry) {
75
- const maxMs = Math.max(...(entries.value ?? []).filter((x) => x.ms).map((x) => x.ms!), 1)
76
- return e.ms != null ? Math.max(4, Math.round((e.ms / maxMs) * 100)) : 4
90
+ function barWidth(entry: FetchViewEntry) {
91
+ const maxMs = Math.max(...entries.value.filter((item) => item.ms).map((item) => item.ms!), 1)
92
+ return entry.ms != null ? Math.max(4, Math.round((entry.ms / maxMs) * 100)) : 4
77
93
  }
78
94
 
79
- function wfLeft(e: FetchEntry) {
80
- const maxEnd = Math.max(...(entries.value ?? []).map((x) => (x.startOffset ?? 0) + (x.ms ?? 0)), 1)
81
- return Math.round(((e.startOffset ?? 0) / maxEnd) * 100)
82
- }
83
- function wfWidth(e: FetchEntry) {
84
- const maxEnd = Math.max(...(entries.value ?? []).map((x) => (x.startOffset ?? 0) + (x.ms ?? 0)), 1)
85
- return e.ms != null ? Math.round((e.ms / maxEnd) * 100) : 2
95
+ function wfLeft(entry: FetchViewEntry) {
96
+ const maxEnd = Math.max(...entries.value.map((item) => item.startOffset + (item.ms ?? 0)), 1)
97
+ return Math.round((entry.startOffset / maxEnd) * 100)
86
98
  }
87
99
 
88
- function formatSize(bytes: number) {
89
- if (bytes < 1024) return bytes + 'B'
90
- return (bytes / 1024).toFixed(1) + 'KB'
100
+ function wfWidth(entry: FetchViewEntry) {
101
+ const maxEnd = Math.max(...entries.value.map((item) => item.startOffset + (item.ms ?? 0)), 1)
102
+ return entry.ms != null ? Math.round((entry.ms / maxEnd) * 100) : 2
91
103
  }
92
104
 
93
- function replayFetch() {
94
- // No-op: cannot replay fetch from devtools UI (see Nuxt docs)
95
- // You should call the original refresh() from useFetch in-app, not here
96
- }
105
+ function formatSize(bytes: number) {
106
+ if (bytes < 1024) {
107
+ return `${bytes}B`
108
+ }
97
109
 
98
- function clearAll() {
99
- // No-op: cannot clear live registry from client
100
- selected.value = null
110
+ return `${(bytes / 1024).toFixed(1)}KB`
101
111
  }
102
112
  </script>
103
113
 
104
114
  <template>
105
115
  <div class="view">
106
- <!-- Stats row -->
107
116
  <div class="stats-row">
108
117
  <div class="stat-card">
109
118
  <div class="stat-label">total</div>
@@ -123,19 +132,15 @@ function clearAll() {
123
132
  </div>
124
133
  </div>
125
134
 
126
- <!-- Toolbar -->
127
135
  <div class="toolbar">
128
136
  <button :class="{ active: filter === 'all' }" @click="filter = 'all'">all</button>
129
137
  <button :class="{ 'danger-active': filter === 'error' }" @click="filter = 'error'">errors</button>
130
138
  <button :class="{ active: filter === 'pending' }" @click="filter = 'pending'">pending</button>
131
139
  <button :class="{ active: filter === 'cached' }" @click="filter = 'cached'">cached</button>
132
140
  <input v-model="search" type="search" placeholder="search key or url…" style="max-width: 240px; margin-left: auto" />
133
- <button @click="clearAll">clear</button>
134
141
  </div>
135
142
 
136
- <!-- Split view -->
137
143
  <div class="split">
138
- <!-- Table -->
139
144
  <div class="table-wrap">
140
145
  <table class="data-table">
141
146
  <thead>
@@ -150,9 +155,14 @@ function clearAll() {
150
155
  </tr>
151
156
  </thead>
152
157
  <tbody>
153
- <tr v-for="e in filtered" :key="e.id" :class="{ selected: selected?.id === e.id }" @click="selected = e">
158
+ <tr
159
+ v-for="entry in filtered"
160
+ :key="entry.id"
161
+ :class="{ selected: selected?.id === entry.id }"
162
+ @click="selectedId = entry.id"
163
+ >
154
164
  <td>
155
- <span class="mono" style="font-size: 11px; color: var(--text2)">{{ e.key }}</span>
165
+ <span class="mono" style="font-size: 11px; color: var(--text2)">{{ entry.key }}</span>
156
166
  </td>
157
167
  <td>
158
168
  <span
@@ -165,25 +175,25 @@ function clearAll() {
165
175
  text-overflow: ellipsis;
166
176
  white-space: nowrap;
167
177
  "
168
- :title="e.url"
178
+ :title="entry.url"
169
179
  >
170
- {{ e.url }}
180
+ {{ entry.url }}
171
181
  </span>
172
182
  </td>
173
183
  <td>
174
- <span class="badge" :class="statusClass(e.status)">{{ e.status }}</span>
184
+ <span class="badge" :class="statusClass(entry.status)">{{ entry.status }}</span>
175
185
  </td>
176
186
  <td>
177
- <span class="badge" :class="e.origin === 'ssr' ? 'badge-info' : 'badge-gray'">{{ e.origin }}</span>
187
+ <span class="badge" :class="entry.origin === 'ssr' ? 'badge-info' : 'badge-gray'">{{ entry.origin }}</span>
178
188
  </td>
179
- <td class="muted text-sm">{{ e.size ? formatSize(e.size) : '—' }}</td>
180
- <td class="mono text-sm">{{ e.ms != null ? e.ms + 'ms' : '—' }}</td>
189
+ <td class="muted text-sm">{{ entry.size ? formatSize(entry.size) : '—' }}</td>
190
+ <td class="mono text-sm">{{ entry.ms != null ? `${entry.ms}ms` : '—' }}</td>
181
191
  <td>
182
192
  <div style="height: 4px; background: var(--bg2); border-radius: 2px; overflow: hidden">
183
193
  <div
184
194
  :style="{
185
- width: barWidth(e) + '%',
186
- background: barColor(e.status),
195
+ width: `${barWidth(entry)}%`,
196
+ background: barColor(entry.status),
187
197
  height: '100%',
188
198
  borderRadius: '2px',
189
199
  }"
@@ -191,24 +201,27 @@ function clearAll() {
191
201
  </div>
192
202
  </td>
193
203
  </tr>
204
+ <tr v-if="!filtered.length">
205
+ <td colspan="7" style="text-align: center; color: var(--text3); padding: 24px">
206
+ {{ connected ? 'No fetches recorded yet.' : 'Waiting for connection to the Nuxt app…' }}
207
+ </td>
208
+ </tr>
194
209
  </tbody>
195
210
  </table>
196
211
  </div>
197
212
 
198
- <!-- Detail panel -->
199
213
  <div v-if="selected" class="detail-panel">
200
214
  <div class="detail-header">
201
215
  <span class="mono bold" style="font-size: 12px">{{ selected.key }}</span>
202
216
  <div class="flex gap-2">
203
- <button @click="replayFetch">↺ replay</button>
204
- <button @click="selected = null">×</button>
217
+ <button @click="selectedId = null">×</button>
205
218
  </div>
206
219
  </div>
207
220
 
208
221
  <div class="meta-grid">
209
- <template v-for="[k, v] in metaRows" :key="k">
210
- <span class="muted text-sm">{{ k }}</span>
211
- <span class="mono text-sm" style="word-break: break-all">{{ v }}</span>
222
+ <template v-for="[key, value] in metaRows" :key="key">
223
+ <span class="muted text-sm">{{ key }}</span>
224
+ <span class="mono text-sm" style="word-break: break-all">{{ value }}</span>
212
225
  </template>
213
226
  </div>
214
227
 
@@ -221,28 +234,27 @@ function clearAll() {
221
234
  <div v-else class="detail-empty">select a call to inspect</div>
222
235
  </div>
223
236
 
224
- <!-- Waterfall -->
225
237
  <div class="waterfall">
226
238
  <div class="section-label" style="margin-bottom: 6px">waterfall</div>
227
- <div v-for="e in entries" :key="e.id" class="wf-row">
239
+ <div v-for="entry in entries" :key="entry.id" class="wf-row">
228
240
  <span
229
241
  class="mono muted text-sm"
230
242
  style="width: 140px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 0"
231
243
  >
232
- {{ e.key }}
244
+ {{ entry.key }}
233
245
  </span>
234
246
  <div class="wf-track">
235
247
  <div
236
248
  class="wf-bar"
237
249
  :style="{
238
- left: wfLeft(e) + '%',
239
- width: Math.max(2, wfWidth(e)) + '%',
240
- background: barColor(e.status),
250
+ left: `${wfLeft(entry)}%`,
251
+ width: `${Math.max(2, wfWidth(entry))}%`,
252
+ background: barColor(entry.status),
241
253
  }"
242
254
  ></div>
243
255
  </div>
244
256
  <span class="mono muted text-sm" style="width: 44px; text-align: right; flex-shrink: 0">
245
- {{ e.ms != null ? e.ms + 'ms' : '—' }}
257
+ {{ entry.ms != null ? `${entry.ms}ms` : '—' }}
246
258
  </span>
247
259
  </div>
248
260
  </div>