nuxt-devtools-observatory 0.1.4 → 0.1.6
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/client/dist/assets/{index-yIuOV1_N.css → index-CjQU78-e.css} +1 -1
- package/client/dist/assets/index-lG5ffIvt.js +17 -0
- package/client/dist/index.html +2 -2
- package/client/src/stores/observatory.ts +103 -5
- package/client/src/views/ComponentBlock.vue +162 -0
- package/client/src/views/ComposableTracker.vue +34 -110
- package/client/src/views/FetchDashboard.vue +13 -123
- package/client/src/views/ProvideInjectGraph.vue +28 -78
- package/client/src/views/RenderHeatmap.vue +13 -205
- package/client/src/views/TransitionTimeline.vue +9 -3
- package/dist/module.d.mts +2 -2
- package/dist/module.json +1 -1
- package/dist/module.mjs +72 -28
- package/dist/runtime/composables/composable-registry.js +42 -1
- package/dist/runtime/composables/fetch-registry.d.ts +2 -2
- package/dist/runtime/composables/fetch-registry.js +117 -43
- package/dist/runtime/composables/provide-inject-registry.d.ts +2 -19
- package/dist/runtime/composables/provide-inject-registry.js +43 -1
- package/dist/runtime/composables/render-registry.js +22 -1
- package/dist/runtime/composables/transition-registry.js +10 -1
- package/dist/runtime/plugin.js +25 -10
- package/package.json +2 -2
- package/client/dist/assets/index-C76d764s.js +0 -17
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
interface ComponentNode {
|
|
3
|
+
id: string
|
|
4
|
+
label: string
|
|
5
|
+
file: string
|
|
6
|
+
renders: number
|
|
7
|
+
avgMs: number
|
|
8
|
+
triggers: string[]
|
|
9
|
+
children: ComponentNode[]
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const props = defineProps<{
|
|
13
|
+
node: ComponentNode
|
|
14
|
+
mode: string
|
|
15
|
+
threshold: number
|
|
16
|
+
hotOnly: boolean
|
|
17
|
+
selected?: string
|
|
18
|
+
}>()
|
|
19
|
+
|
|
20
|
+
const emit = defineEmits(['select'])
|
|
21
|
+
|
|
22
|
+
function getVal(n: ComponentNode) {
|
|
23
|
+
return props.mode === 'count' ? n.renders : n.avgMs
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function getMax(n: ComponentNode): number {
|
|
27
|
+
let max = 1
|
|
28
|
+
|
|
29
|
+
function walk(ns: ComponentNode[]) {
|
|
30
|
+
ns.forEach((n) => {
|
|
31
|
+
const v = getVal(n)
|
|
32
|
+
|
|
33
|
+
if (v > max) {
|
|
34
|
+
max = v
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
walk(n.children)
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
walk([n])
|
|
42
|
+
|
|
43
|
+
return Math.max(max, props.mode === 'count' ? 40 : 20)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function heatColor(val: number, max: number) {
|
|
47
|
+
const r = Math.min(val / max, 1)
|
|
48
|
+
|
|
49
|
+
if (r < 0.25) {
|
|
50
|
+
return { bg: '#EAF3DE', text: '#27500A', border: '#97C459' }
|
|
51
|
+
} else {
|
|
52
|
+
if (r < 0.55) {
|
|
53
|
+
return { bg: '#FAEEDA', text: '#633806', border: '#EF9F27' }
|
|
54
|
+
} else {
|
|
55
|
+
if (r < 0.8) {
|
|
56
|
+
return { bg: '#FAECE7', text: '#712B13', border: '#D85A30' }
|
|
57
|
+
} else {
|
|
58
|
+
return { bg: '#FCEBEB', text: '#791F1F', border: '#E24B4A' }
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function isHotNode(n: ComponentNode) {
|
|
65
|
+
let value: number
|
|
66
|
+
|
|
67
|
+
if (props.mode === 'count') {
|
|
68
|
+
value = n.renders
|
|
69
|
+
} else {
|
|
70
|
+
value = n.avgMs
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (value >= props.threshold) {
|
|
74
|
+
return true
|
|
75
|
+
} else {
|
|
76
|
+
return false
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function shouldShow(n: ComponentNode): boolean {
|
|
81
|
+
if (!props.hotOnly) {
|
|
82
|
+
return true
|
|
83
|
+
} else {
|
|
84
|
+
if (isHotNode(n)) {
|
|
85
|
+
return true
|
|
86
|
+
} else {
|
|
87
|
+
if (n.children.some(isHotNode)) {
|
|
88
|
+
return true
|
|
89
|
+
} else {
|
|
90
|
+
return false
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function handleSelect(n: ComponentNode) {
|
|
97
|
+
emit('select', n)
|
|
98
|
+
}
|
|
99
|
+
</script>
|
|
100
|
+
|
|
101
|
+
<template>
|
|
102
|
+
<div
|
|
103
|
+
v-if="shouldShow(props.node)"
|
|
104
|
+
:style="{
|
|
105
|
+
background: heatColor(getVal(props.node), getMax(props.node)).bg,
|
|
106
|
+
border:
|
|
107
|
+
props.selected === props.node.id
|
|
108
|
+
? `2px solid ${heatColor(getVal(props.node), getMax(props.node)).border}`
|
|
109
|
+
: `1px solid ${heatColor(getVal(props.node), getMax(props.node)).border}`,
|
|
110
|
+
borderRadius: '6px',
|
|
111
|
+
padding: '6px 9px',
|
|
112
|
+
marginBottom: '5px',
|
|
113
|
+
cursor: 'pointer',
|
|
114
|
+
}"
|
|
115
|
+
@click="handleSelect(props.node)"
|
|
116
|
+
>
|
|
117
|
+
<div style="display: flex; align-items: center; gap: 6px">
|
|
118
|
+
<span
|
|
119
|
+
:style="{
|
|
120
|
+
fontFamily: 'var(--mono)',
|
|
121
|
+
fontSize: '11px',
|
|
122
|
+
fontWeight: 500,
|
|
123
|
+
color: heatColor(getVal(props.node), getMax(props.node)).text,
|
|
124
|
+
}"
|
|
125
|
+
>
|
|
126
|
+
{{ props.node.label }}
|
|
127
|
+
</span>
|
|
128
|
+
<span
|
|
129
|
+
:style="{
|
|
130
|
+
fontFamily: 'var(--mono)',
|
|
131
|
+
fontSize: '10px',
|
|
132
|
+
color: heatColor(getVal(props.node), getMax(props.node)).text,
|
|
133
|
+
opacity: 0.7,
|
|
134
|
+
marginLeft: 'auto',
|
|
135
|
+
}"
|
|
136
|
+
>
|
|
137
|
+
{{ props.mode === 'count' ? getVal(props.node) : getVal(props.node).toFixed(1) + 'ms' }}
|
|
138
|
+
{{ props.mode === 'count' ? 'renders' : 'ms avg' }}
|
|
139
|
+
</span>
|
|
140
|
+
</div>
|
|
141
|
+
<div
|
|
142
|
+
v-if="props.node.children && props.node.children.length"
|
|
143
|
+
:style="{
|
|
144
|
+
marginLeft: '10px',
|
|
145
|
+
borderLeft: `1.5px solid ${heatColor(getVal(props.node), getMax(props.node)).border}40`,
|
|
146
|
+
paddingLeft: '8px',
|
|
147
|
+
marginTop: '5px',
|
|
148
|
+
}"
|
|
149
|
+
>
|
|
150
|
+
<ComponentBlock
|
|
151
|
+
v-for="child in props.node.children"
|
|
152
|
+
:key="child.id"
|
|
153
|
+
:node="child"
|
|
154
|
+
:mode="props.mode"
|
|
155
|
+
:threshold="props.threshold"
|
|
156
|
+
:hot-only="props.hotOnly"
|
|
157
|
+
:selected="props.selected"
|
|
158
|
+
@select="handleSelect"
|
|
159
|
+
/>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
</template>
|
|
@@ -1,133 +1,57 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { ref, computed } from 'vue'
|
|
2
|
+
import { ref, computed, type Ref } from 'vue'
|
|
3
|
+
import { useObservatoryData } from '../stores/observatory'
|
|
3
4
|
|
|
4
|
-
interface RefEntry {
|
|
5
|
-
key: string
|
|
6
|
-
type: string
|
|
7
|
-
val: string
|
|
8
|
-
}
|
|
9
5
|
interface ComposableEntry {
|
|
10
6
|
id: string
|
|
11
7
|
name: string
|
|
12
8
|
component: string
|
|
13
9
|
instances: number
|
|
14
|
-
status:
|
|
10
|
+
status: string
|
|
15
11
|
leak: boolean
|
|
16
12
|
leakReason?: string
|
|
17
|
-
refs:
|
|
13
|
+
refs: Array<{ key: string; type: string; val: string }>
|
|
18
14
|
watchers: number
|
|
19
15
|
intervals: number
|
|
20
|
-
lifecycle: {
|
|
16
|
+
lifecycle: {
|
|
17
|
+
onMounted: boolean
|
|
18
|
+
onUnmounted: boolean
|
|
19
|
+
watchersCleaned: boolean
|
|
20
|
+
intervalsCleaned: boolean
|
|
21
|
+
}
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
id: '1',
|
|
26
|
-
name: 'useAuth',
|
|
27
|
-
component: 'App.vue',
|
|
28
|
-
instances: 1,
|
|
29
|
-
status: 'mounted',
|
|
30
|
-
leak: false,
|
|
31
|
-
refs: [
|
|
32
|
-
{ key: 'user', type: 'ref', val: '{ id: "u_9x3k", role: "admin" }' },
|
|
33
|
-
{ key: 'isLoggedIn', type: 'computed', val: 'true' },
|
|
34
|
-
],
|
|
35
|
-
watchers: 1,
|
|
36
|
-
intervals: 0,
|
|
37
|
-
lifecycle: { onMounted: true, onUnmounted: true, watchersCleaned: true, intervalsCleaned: true },
|
|
38
|
-
},
|
|
39
|
-
{
|
|
40
|
-
id: '2',
|
|
41
|
-
name: 'useWebSocket',
|
|
42
|
-
component: 'Dashboard.vue',
|
|
43
|
-
instances: 1,
|
|
44
|
-
status: 'unmounted',
|
|
45
|
-
leak: true,
|
|
46
|
-
leakReason: 'socket.close() never called — 2 watchers still running after unmount',
|
|
47
|
-
refs: [
|
|
48
|
-
{ key: 'socket', type: 'ref', val: 'WebSocket { readyState: 1 }' },
|
|
49
|
-
{ key: 'messages', type: 'ref', val: 'Array(47)' },
|
|
50
|
-
],
|
|
51
|
-
watchers: 2,
|
|
52
|
-
intervals: 0,
|
|
53
|
-
lifecycle: { onMounted: true, onUnmounted: false, watchersCleaned: false, intervalsCleaned: true },
|
|
54
|
-
},
|
|
55
|
-
{
|
|
56
|
-
id: '3',
|
|
57
|
-
name: 'usePoller',
|
|
58
|
-
component: 'StockTicker.vue',
|
|
59
|
-
instances: 2,
|
|
60
|
-
status: 'unmounted',
|
|
61
|
-
leak: true,
|
|
62
|
-
leakReason: 'setInterval #37 never cleared — still firing every 2000ms',
|
|
63
|
-
refs: [
|
|
64
|
-
{ key: 'data', type: 'ref', val: '{ price: 142.5 }' },
|
|
65
|
-
{ key: 'intervalId', type: 'ref', val: '37' },
|
|
66
|
-
],
|
|
67
|
-
watchers: 0,
|
|
68
|
-
intervals: 1,
|
|
69
|
-
lifecycle: { onMounted: true, onUnmounted: false, watchersCleaned: true, intervalsCleaned: false },
|
|
70
|
-
},
|
|
71
|
-
{
|
|
72
|
-
id: '4',
|
|
73
|
-
name: 'useCart',
|
|
74
|
-
component: 'CartDrawer.vue',
|
|
75
|
-
instances: 1,
|
|
76
|
-
status: 'mounted',
|
|
77
|
-
leak: false,
|
|
78
|
-
refs: [
|
|
79
|
-
{ key: 'items', type: 'ref', val: 'Array(3)' },
|
|
80
|
-
{ key: 'total', type: 'computed', val: '248.50' },
|
|
81
|
-
],
|
|
82
|
-
watchers: 0,
|
|
83
|
-
intervals: 0,
|
|
84
|
-
lifecycle: { onMounted: false, onUnmounted: false, watchersCleaned: true, intervalsCleaned: true },
|
|
85
|
-
},
|
|
86
|
-
{
|
|
87
|
-
id: '5',
|
|
88
|
-
name: 'useBreakpoint',
|
|
89
|
-
component: 'Layout.vue',
|
|
90
|
-
instances: 1,
|
|
91
|
-
status: 'mounted',
|
|
92
|
-
leak: false,
|
|
93
|
-
refs: [
|
|
94
|
-
{ key: 'isMobile', type: 'computed', val: 'false' },
|
|
95
|
-
{ key: 'width', type: 'ref', val: '1280' },
|
|
96
|
-
],
|
|
97
|
-
watchers: 0,
|
|
98
|
-
intervals: 0,
|
|
99
|
-
lifecycle: { onMounted: true, onUnmounted: true, watchersCleaned: true, intervalsCleaned: true },
|
|
100
|
-
},
|
|
101
|
-
{
|
|
102
|
-
id: '6',
|
|
103
|
-
name: 'useIntersectionObserver',
|
|
104
|
-
component: 'LazyImage.vue',
|
|
105
|
-
instances: 4,
|
|
106
|
-
status: 'mounted',
|
|
107
|
-
leak: false,
|
|
108
|
-
refs: [{ key: 'isVisible', type: 'ref', val: 'true' }],
|
|
109
|
-
watchers: 0,
|
|
110
|
-
intervals: 0,
|
|
111
|
-
lifecycle: { onMounted: true, onUnmounted: true, watchersCleaned: true, intervalsCleaned: true },
|
|
112
|
-
},
|
|
113
|
-
])
|
|
24
|
+
const { composables } = useObservatoryData()
|
|
25
|
+
const entries = composables as Ref<ComposableEntry[]>
|
|
114
26
|
|
|
115
27
|
const filter = ref('all')
|
|
116
28
|
const search = ref('')
|
|
117
29
|
const expanded = ref<string | null>(null)
|
|
118
30
|
|
|
119
|
-
const counts = computed(() =>
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
31
|
+
const counts = computed<{ mounted: number; leaks: number }>(() => {
|
|
32
|
+
return {
|
|
33
|
+
mounted: entries.value.filter((e) => e.status === 'mounted').length,
|
|
34
|
+
leaks: entries.value.filter((e) => e.leak).length,
|
|
35
|
+
}
|
|
36
|
+
})
|
|
123
37
|
|
|
124
|
-
const filtered = computed(() => {
|
|
38
|
+
const filtered = computed<ComposableEntry[]>(() => {
|
|
125
39
|
return entries.value.filter((e) => {
|
|
126
|
-
if (filter.value === 'leak' && !e.leak)
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
40
|
+
if (filter.value === 'leak' && !e.leak) {
|
|
41
|
+
return false
|
|
42
|
+
} else {
|
|
43
|
+
if (filter.value === 'unmounted' && e.status !== 'unmounted') {
|
|
44
|
+
return false
|
|
45
|
+
} else {
|
|
46
|
+
const q = search.value.toLowerCase()
|
|
47
|
+
|
|
48
|
+
if (q && !e.name.toLowerCase().includes(q) && !e.component.toLowerCase().includes(q)) {
|
|
49
|
+
return false
|
|
50
|
+
} else {
|
|
51
|
+
return true
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
131
55
|
})
|
|
132
56
|
})
|
|
133
57
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { ref, computed } from 'vue'
|
|
3
|
+
import { useObservatoryData } from '../stores/observatory'
|
|
3
4
|
|
|
4
5
|
interface FetchEntry {
|
|
5
6
|
id: string
|
|
@@ -16,126 +17,21 @@ interface FetchEntry {
|
|
|
16
17
|
startOffset?: number
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
//
|
|
20
|
-
const entries =
|
|
21
|
-
{
|
|
22
|
-
id: '1',
|
|
23
|
-
key: 'product-detail',
|
|
24
|
-
url: '/api/products/42',
|
|
25
|
-
status: 'ok',
|
|
26
|
-
origin: 'ssr',
|
|
27
|
-
ms: 48,
|
|
28
|
-
size: 3276,
|
|
29
|
-
cached: false,
|
|
30
|
-
startOffset: 0,
|
|
31
|
-
file: 'pages/products/[id].vue',
|
|
32
|
-
line: 8,
|
|
33
|
-
},
|
|
34
|
-
{
|
|
35
|
-
id: '2',
|
|
36
|
-
key: 'related-products',
|
|
37
|
-
url: '/api/products?related=42',
|
|
38
|
-
status: 'ok',
|
|
39
|
-
origin: 'ssr',
|
|
40
|
-
ms: 112,
|
|
41
|
-
size: 19148,
|
|
42
|
-
cached: false,
|
|
43
|
-
startOffset: 10,
|
|
44
|
-
file: 'pages/products/[id].vue',
|
|
45
|
-
line: 14,
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
id: '3',
|
|
49
|
-
key: 'user-session',
|
|
50
|
-
url: '/api/auth/session',
|
|
51
|
-
status: 'cached',
|
|
52
|
-
origin: 'csr',
|
|
53
|
-
ms: 0,
|
|
54
|
-
size: 819,
|
|
55
|
-
cached: true,
|
|
56
|
-
startOffset: 0,
|
|
57
|
-
file: 'layouts/default.vue',
|
|
58
|
-
line: 5,
|
|
59
|
-
},
|
|
60
|
-
{
|
|
61
|
-
id: '4',
|
|
62
|
-
key: 'cart-summary',
|
|
63
|
-
url: '/api/cart',
|
|
64
|
-
status: 'ok',
|
|
65
|
-
origin: 'csr',
|
|
66
|
-
ms: 67,
|
|
67
|
-
size: 2150,
|
|
68
|
-
cached: false,
|
|
69
|
-
startOffset: 5,
|
|
70
|
-
file: 'components/CartDrawer.vue',
|
|
71
|
-
line: 3,
|
|
72
|
-
},
|
|
73
|
-
{
|
|
74
|
-
id: '5',
|
|
75
|
-
key: 'product-reviews',
|
|
76
|
-
url: '/api/products/42/reviews',
|
|
77
|
-
status: 'pending',
|
|
78
|
-
origin: 'csr',
|
|
79
|
-
ms: undefined,
|
|
80
|
-
size: undefined,
|
|
81
|
-
cached: false,
|
|
82
|
-
startOffset: 120,
|
|
83
|
-
file: 'components/ReviewList.vue',
|
|
84
|
-
line: 6,
|
|
85
|
-
},
|
|
86
|
-
{
|
|
87
|
-
id: '6',
|
|
88
|
-
key: 'recommendations',
|
|
89
|
-
url: '/api/recommend?u=u_9x3k',
|
|
90
|
-
status: 'ok',
|
|
91
|
-
origin: 'csr',
|
|
92
|
-
ms: 201,
|
|
93
|
-
size: 9626,
|
|
94
|
-
cached: false,
|
|
95
|
-
startOffset: 30,
|
|
96
|
-
file: 'components/Recommendations.vue',
|
|
97
|
-
line: 4,
|
|
98
|
-
},
|
|
99
|
-
{
|
|
100
|
-
id: '7',
|
|
101
|
-
key: 'inventory-check',
|
|
102
|
-
url: '/api/inventory/42',
|
|
103
|
-
status: 'error',
|
|
104
|
-
origin: 'csr',
|
|
105
|
-
ms: 503,
|
|
106
|
-
size: undefined,
|
|
107
|
-
cached: false,
|
|
108
|
-
startOffset: 55,
|
|
109
|
-
file: 'components/StockBadge.vue',
|
|
110
|
-
line: 7,
|
|
111
|
-
},
|
|
112
|
-
{
|
|
113
|
-
id: '8',
|
|
114
|
-
key: 'nav-links',
|
|
115
|
-
url: '/api/nav',
|
|
116
|
-
status: 'cached',
|
|
117
|
-
origin: 'ssr',
|
|
118
|
-
ms: 0,
|
|
119
|
-
size: 1126,
|
|
120
|
-
cached: true,
|
|
121
|
-
startOffset: 0,
|
|
122
|
-
file: 'layouts/default.vue',
|
|
123
|
-
line: 9,
|
|
124
|
-
},
|
|
125
|
-
])
|
|
20
|
+
// Use live fetch data from the Nuxt registry bridge
|
|
21
|
+
const { fetches: entries } = useObservatoryData()
|
|
126
22
|
|
|
127
23
|
const filter = ref<string>('all')
|
|
128
24
|
const search = ref('')
|
|
129
25
|
const selected = ref<FetchEntry | null>(null)
|
|
130
26
|
|
|
131
27
|
const counts = computed(() => ({
|
|
132
|
-
ok: entries.value
|
|
133
|
-
pending: entries.value
|
|
134
|
-
error: entries.value
|
|
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,
|
|
135
31
|
}))
|
|
136
32
|
|
|
137
33
|
const filtered = computed(() => {
|
|
138
|
-
return entries.value.filter((e) => {
|
|
34
|
+
return (entries.value ?? []).filter((e) => {
|
|
139
35
|
if (filter.value !== 'all' && e.status !== filter.value) return false
|
|
140
36
|
const q = search.value.toLowerCase()
|
|
141
37
|
if (q && !e.key.includes(q) && !e.url.includes(q)) return false
|
|
@@ -176,16 +72,16 @@ function barColor(s: string) {
|
|
|
176
72
|
}
|
|
177
73
|
|
|
178
74
|
function barWidth(e: FetchEntry) {
|
|
179
|
-
const maxMs = Math.max(...entries.value.filter((x) => x.ms).map((x) => x.ms!), 1)
|
|
75
|
+
const maxMs = Math.max(...(entries.value ?? []).filter((x) => x.ms).map((x) => x.ms!), 1)
|
|
180
76
|
return e.ms != null ? Math.max(4, Math.round((e.ms / maxMs) * 100)) : 4
|
|
181
77
|
}
|
|
182
78
|
|
|
183
79
|
function wfLeft(e: FetchEntry) {
|
|
184
|
-
const maxEnd = Math.max(...entries.value.map((x) => (x.startOffset ?? 0) + (x.ms ?? 0)), 1)
|
|
80
|
+
const maxEnd = Math.max(...(entries.value ?? []).map((x) => (x.startOffset ?? 0) + (x.ms ?? 0)), 1)
|
|
185
81
|
return Math.round(((e.startOffset ?? 0) / maxEnd) * 100)
|
|
186
82
|
}
|
|
187
83
|
function wfWidth(e: FetchEntry) {
|
|
188
|
-
const maxEnd = Math.max(...entries.value.map((x) => (x.startOffset ?? 0) + (x.ms ?? 0)), 1)
|
|
84
|
+
const maxEnd = Math.max(...(entries.value ?? []).map((x) => (x.startOffset ?? 0) + (x.ms ?? 0)), 1)
|
|
189
85
|
return e.ms != null ? Math.round((e.ms / maxEnd) * 100) : 2
|
|
190
86
|
}
|
|
191
87
|
|
|
@@ -195,18 +91,12 @@ function formatSize(bytes: number) {
|
|
|
195
91
|
}
|
|
196
92
|
|
|
197
93
|
function replayFetch() {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
e.status = 'pending'
|
|
201
|
-
e.ms = undefined
|
|
202
|
-
setTimeout(() => {
|
|
203
|
-
e.status = 'ok'
|
|
204
|
-
e.ms = Math.floor(Math.random() * 150 + 20)
|
|
205
|
-
}, 700)
|
|
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
|
|
206
96
|
}
|
|
207
97
|
|
|
208
98
|
function clearAll() {
|
|
209
|
-
|
|
99
|
+
// No-op: cannot clear live registry from client
|
|
210
100
|
selected.value = null
|
|
211
101
|
}
|
|
212
102
|
</script>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { ref, computed } from 'vue'
|
|
3
|
+
import { useObservatoryData } from '../stores/observatory'
|
|
3
4
|
|
|
4
5
|
interface TreeNodeData {
|
|
5
6
|
id: string
|
|
@@ -31,15 +32,30 @@ const V_GAP = 72
|
|
|
31
32
|
const H_GAP = 18
|
|
32
33
|
|
|
33
34
|
function nodeColor(n: TreeNodeData): string {
|
|
34
|
-
if (n.injects.some((i) => !i.ok))
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
if (n.injects.some((i) => !i.ok)) {
|
|
36
|
+
return 'var(--red)'
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (n.type === 'both') {
|
|
40
|
+
return 'var(--blue)'
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (n.type === 'provider') {
|
|
44
|
+
return 'var(--teal)'
|
|
45
|
+
}
|
|
46
|
+
|
|
37
47
|
return 'var(--text3)'
|
|
38
48
|
}
|
|
39
49
|
|
|
40
50
|
function matchesFilter(n: TreeNodeData, filter: string): boolean {
|
|
41
|
-
if (filter === 'all')
|
|
42
|
-
|
|
51
|
+
if (filter === 'all') {
|
|
52
|
+
return true
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (filter === 'warn') {
|
|
56
|
+
return n.injects.some((i) => !i.ok)
|
|
57
|
+
}
|
|
58
|
+
|
|
43
59
|
return n.provides.some((p) => p.key === filter) || n.injects.some((i) => i.key === filter)
|
|
44
60
|
}
|
|
45
61
|
|
|
@@ -47,79 +63,8 @@ function countLeaves(n: TreeNodeData): number {
|
|
|
47
63
|
return n.children.length === 0 ? 1 : n.children.reduce((s, c) => s + countLeaves(c), 0)
|
|
48
64
|
}
|
|
49
65
|
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
id: 'App',
|
|
53
|
-
label: 'App.vue',
|
|
54
|
-
type: 'provider',
|
|
55
|
-
provides: [
|
|
56
|
-
{ key: 'authContext', val: '{ user, logout }', reactive: true },
|
|
57
|
-
{ key: 'theme', val: '"dark"', reactive: false },
|
|
58
|
-
],
|
|
59
|
-
injects: [],
|
|
60
|
-
children: [
|
|
61
|
-
{
|
|
62
|
-
id: 'Layout',
|
|
63
|
-
label: 'Layout.vue',
|
|
64
|
-
type: 'both',
|
|
65
|
-
provides: [{ key: 'routerState', val: 'useRoute()', reactive: true }],
|
|
66
|
-
injects: [{ key: 'theme', from: 'App.vue', ok: true }],
|
|
67
|
-
children: [
|
|
68
|
-
{
|
|
69
|
-
id: 'Sidebar',
|
|
70
|
-
label: 'Sidebar.vue',
|
|
71
|
-
type: 'consumer',
|
|
72
|
-
provides: [],
|
|
73
|
-
injects: [
|
|
74
|
-
{ key: 'authContext', from: 'App.vue', ok: true },
|
|
75
|
-
{ key: 'theme', from: 'App.vue', ok: true },
|
|
76
|
-
],
|
|
77
|
-
children: [],
|
|
78
|
-
},
|
|
79
|
-
{
|
|
80
|
-
id: 'NavBar',
|
|
81
|
-
label: 'NavBar.vue',
|
|
82
|
-
type: 'consumer',
|
|
83
|
-
provides: [],
|
|
84
|
-
injects: [
|
|
85
|
-
{ key: 'authContext', from: 'App.vue', ok: true },
|
|
86
|
-
{ key: 'routerState', from: 'Layout.vue', ok: true },
|
|
87
|
-
],
|
|
88
|
-
children: [],
|
|
89
|
-
},
|
|
90
|
-
{
|
|
91
|
-
id: 'ProductList',
|
|
92
|
-
label: 'ProductList.vue',
|
|
93
|
-
type: 'error',
|
|
94
|
-
provides: [],
|
|
95
|
-
injects: [
|
|
96
|
-
{ key: 'cartContext', from: null, ok: false },
|
|
97
|
-
{ key: 'theme', from: 'App.vue', ok: true },
|
|
98
|
-
],
|
|
99
|
-
children: [
|
|
100
|
-
{
|
|
101
|
-
id: 'ProductCard',
|
|
102
|
-
label: 'ProductCard.vue',
|
|
103
|
-
type: 'error',
|
|
104
|
-
provides: [],
|
|
105
|
-
injects: [{ key: 'cartContext', from: null, ok: false }],
|
|
106
|
-
children: [],
|
|
107
|
-
},
|
|
108
|
-
],
|
|
109
|
-
},
|
|
110
|
-
{
|
|
111
|
-
id: 'UserMenu',
|
|
112
|
-
label: 'UserMenu.vue',
|
|
113
|
-
type: 'consumer',
|
|
114
|
-
provides: [],
|
|
115
|
-
injects: [{ key: 'authContext', from: 'App.vue', ok: true }],
|
|
116
|
-
children: [],
|
|
117
|
-
},
|
|
118
|
-
],
|
|
119
|
-
},
|
|
120
|
-
],
|
|
121
|
-
},
|
|
122
|
-
])
|
|
66
|
+
const { provideInject } = useObservatoryData()
|
|
67
|
+
const nodes = provideInject
|
|
123
68
|
|
|
124
69
|
const activeFilter = ref('all')
|
|
125
70
|
const selectedNode = ref<TreeNodeData | null>(null)
|
|
@@ -147,13 +92,16 @@ const layout = computed<LayoutNode[]>(() => {
|
|
|
147
92
|
function place(node: TreeNodeData, depth: number, slotLeft: number, parentId: string | null) {
|
|
148
93
|
const leaves = countLeaves(node)
|
|
149
94
|
const slotW = leaves * (NODE_W + H_GAP) - H_GAP
|
|
95
|
+
|
|
150
96
|
flat.push({
|
|
151
97
|
data: node,
|
|
152
98
|
parentId,
|
|
153
99
|
x: Math.round(slotLeft + slotW / 2),
|
|
154
100
|
y: Math.round(pad + depth * (NODE_H + V_GAP) + NODE_H / 2),
|
|
155
101
|
})
|
|
102
|
+
|
|
156
103
|
let childLeft = slotLeft
|
|
104
|
+
|
|
157
105
|
for (const child of node.children) {
|
|
158
106
|
const cl = countLeaves(child)
|
|
159
107
|
place(child, depth + 1, childLeft, node.id)
|
|
@@ -162,6 +110,7 @@ const layout = computed<LayoutNode[]>(() => {
|
|
|
162
110
|
}
|
|
163
111
|
|
|
164
112
|
let left = pad
|
|
113
|
+
|
|
165
114
|
for (const root of nodes.value) {
|
|
166
115
|
const leaves = countLeaves(root)
|
|
167
116
|
place(root, 0, left, null)
|
|
@@ -177,6 +126,7 @@ const canvasH = computed(() => layout.value.reduce((m, n) => Math.max(m, n.y + N
|
|
|
177
126
|
|
|
178
127
|
const edges = computed<Edge[]>(() => {
|
|
179
128
|
const byId = new Map(layout.value.map((n) => [n.data.id, n]))
|
|
129
|
+
|
|
180
130
|
return layout.value
|
|
181
131
|
.filter((n) => n.parentId !== null)
|
|
182
132
|
.map((n) => {
|