Skip to content

Commit bddb38d

Browse files
authored
feat: plugin details (#76)
1 parent b8294db commit bddb38d

18 files changed

Lines changed: 801 additions & 14 deletions

File tree

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"@iconify-json/carbon": "catalog:icons",
2222
"@iconify-json/catppuccin": "catalog:icons",
2323
"@iconify-json/codicon": "catalog:icons",
24+
"@iconify-json/fluent": "catalog:icons",
2425
"@iconify-json/logos": "catalog:icons",
2526
"@iconify-json/ph": "catalog:icons",
2627
"@iconify-json/ri": "catalog:icons",
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
<script setup lang="ts">
2+
import type { SessionContext } from '~~/shared/types'
3+
import { useRoute } from '#app/composables/router'
4+
import { useRpc } from '#imports'
5+
import { useAsyncState } from '@vueuse/core'
6+
import { computed } from 'vue'
7+
import { settings } from '~~/app/state/settings'
8+
9+
const props = defineProps<{
10+
session: SessionContext
11+
}>()
12+
const emit = defineEmits<{
13+
(e: 'close'): void
14+
}>()
15+
16+
const route = useRoute()
17+
const rpc = useRpc()
18+
const { state, isLoading } = useAsyncState(
19+
async () => {
20+
const res = await rpc.value!['vite:rolldown:get-plugin-details']?.({
21+
session: props.session.id,
22+
id: route.query.plugin as string,
23+
})
24+
return res
25+
},
26+
null,
27+
)
28+
29+
const processedModules = computed(() => {
30+
const seen = new Set()
31+
return state.value?.calls?.filter((call) => {
32+
if (seen.has(call.module)) {
33+
return false
34+
}
35+
seen.add(call.module)
36+
return true
37+
}) ?? []
38+
})
39+
40+
const hookLoadDuration = computed(() => {
41+
const loadMetrics = state.value?.loadMetrics
42+
if (!loadMetrics?.length) {
43+
return
44+
}
45+
return loadMetrics[loadMetrics.length - 1]!.timestamp_end - loadMetrics[0]!.timestamp_start
46+
})
47+
48+
const hookTransformDuration = computed(() => {
49+
const transformMetrics = state.value?.transformMetrics
50+
if (!transformMetrics?.length) {
51+
return
52+
}
53+
return transformMetrics[transformMetrics.length - 1]!.timestamp_end - transformMetrics[0]!.timestamp_start
54+
})
55+
56+
const hookResolveIdDuration = computed(() => {
57+
const resolveIdMetrics = state.value?.resolveIdMetrics
58+
if (!resolveIdMetrics?.length) {
59+
return
60+
}
61+
return resolveIdMetrics[resolveIdMetrics.length - 1]!.timestamp_end - resolveIdMetrics[0]!.timestamp_start
62+
})
63+
64+
const totalDuration = computed(() => {
65+
const calls = state.value?.calls
66+
if (!calls?.length) {
67+
return
68+
}
69+
return calls[calls.length - 1]!.timestamp_end - calls[0]!.timestamp_start
70+
})
71+
</script>
72+
73+
<template>
74+
<VisualLoading v-if="isLoading" />
75+
<div v-else-if="state?.calls?.length" relative h-full w-full>
76+
<DisplayCloseButton
77+
absolute right-2 top-1.5
78+
@click="emit('close')"
79+
/>
80+
<div
81+
bg-glass absolute left-2 top-2 z-panel-content p2
82+
border="~ base rounded-lg"
83+
flex="~ col gap-2"
84+
>
85+
<DisplayPluginName :name="state?.plugin_name!" />
86+
<div text-xs font-mono flex="~ items-center gap-3" ml2>
87+
<DisplayDuration
88+
:duration="hookResolveIdDuration" flex="~ gap-1 items-center"
89+
:title="`Resolve Id hooks cost: ${hookResolveIdDuration}ms`"
90+
>
91+
<span i-ph-magnifying-glass-duotone inline-block />
92+
</DisplayDuration>
93+
<DisplayDuration
94+
:duration="hookLoadDuration" flex="~ gap-1 items-center"
95+
:title="`Load hooks cost: ${hookLoadDuration}ms`"
96+
>
97+
<span i-ph-upload-simple-duotone inline-block />
98+
</DisplayDuration>
99+
<DisplayDuration
100+
:duration="hookTransformDuration" flex="~ gap-1 items-center"
101+
:title="`Transform hooks cost: ${hookTransformDuration}ms`"
102+
>
103+
<span i-ph-magic-wand-duotone inline-block />
104+
</DisplayDuration>
105+
<span op40>|</span>
106+
<DisplayDuration
107+
:duration="totalDuration" flex="~ gap-1 items-center"
108+
:title="`Total build cost: ${totalDuration}ms`"
109+
>
110+
<span i-ph-clock-duotone inline-block />
111+
</DisplayDuration>
112+
<span op40>|</span>
113+
<DisplayNumberBadge
114+
:number="processedModules.length" icon="i-catppuccin-java-class-abstract"
115+
color="transparent color-scale-neutral"
116+
:title="`Module processed: ${processedModules.length}`"
117+
/>
118+
<span op40>|</span>
119+
<DisplayNumberBadge
120+
:number="state?.calls?.length ?? 0" icon="i-ph:arrow-counter-clockwise"
121+
color="transparent color-scale-neutral"
122+
:title="`Total calls: ${state?.calls?.length ?? 0}`"
123+
/>
124+
</div>
125+
<div flex="~ gap-2">
126+
<button
127+
:class="settings.pluginDetailsViewType === 'flow' ? 'text-primary' : ''"
128+
flex="~ gap-2 items-center justify-center"
129+
px2 py1 w-40
130+
border="~ base rounded-lg"
131+
hover="bg-active"
132+
@click="settings.pluginDetailsViewType = 'flow'"
133+
>
134+
<div i-ph-git-branch-duotone rotate-180 />
135+
Build Flow
136+
</button>
137+
<button
138+
:class="settings.pluginDetailsViewType === 'charts' ? 'text-primary' : ''"
139+
flex="~ gap-2 items-center justify-center"
140+
px2 py1 w-40
141+
border="~ base rounded-lg"
142+
hover="bg-active"
143+
@click="settings.pluginDetailsViewType = 'charts'"
144+
>
145+
<div i-ph-chart-donut-duotone />
146+
Charts
147+
</button>
148+
</div>
149+
</div>
150+
<div of-auto h-full pt-30>
151+
<FlowmapPluginFlow
152+
v-if="settings.pluginDetailsViewType === 'flow'"
153+
:session="session"
154+
:build-metrics="state"
155+
/>
156+
</div>
157+
</div>
158+
<div v-else flex="~ items-center justify-center" w-full h-full>
159+
<span italic op50>
160+
No data
161+
</span>
162+
</div>
163+
</template>
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
<script setup lang="ts">
2+
import type { RolldownPluginBuildMetrics, SessionContext } from '~~/shared/types/data'
3+
import type { FilterMatchRule } from '~/utils/icon'
4+
import { useCycleList } from '@vueuse/core'
5+
import { Menu as VMenu } from 'floating-vue'
6+
import { computed, ref } from 'vue'
7+
import { settings } from '~~/app/state/settings'
8+
import { parseReadablePath } from '~/utils/filepath'
9+
import { getFileTypeFromModuleId, ModuleTypeRules } from '~/utils/icon'
10+
11+
const props = defineProps<{
12+
session: SessionContext
13+
buildMetrics: RolldownPluginBuildMetrics
14+
selectedFields: string[]
15+
}>()
16+
17+
const HOOK_NAME_MAP = {
18+
resolve: 'Resolve Id',
19+
load: 'Load',
20+
transform: 'Transform',
21+
}
22+
23+
const parsedPaths = computed(() => props.session.modulesList.map((mod) => {
24+
const path = parseReadablePath(mod.id, props.session.meta.cwd)
25+
const type = getFileTypeFromModuleId(mod.id)
26+
return {
27+
mod,
28+
path,
29+
type,
30+
}
31+
}))
32+
33+
const searchFilterTypes = computed(() => ModuleTypeRules.filter(rule => parsedPaths.value.some(mod => rule.match.test(mod.mod.id))))
34+
35+
const filterModuleTypes = ref<string[]>(settings.value.pluginDetailsModuleTypes ?? searchFilterTypes.value.map(i => i.name))
36+
const { state: durationSortType, next } = useCycleList(['', 'desc', 'asc'], {
37+
initialValue: settings.value.pluginDetailsDurationSortType,
38+
})
39+
const filtered = computed(() => {
40+
const sorted = durationSortType.value
41+
? [...props.buildMetrics.calls].sort((a, b) => {
42+
if (durationSortType.value === 'asc') {
43+
return a.duration - b.duration
44+
}
45+
return b.duration - a.duration
46+
})
47+
: props.buildMetrics.calls
48+
return sorted.filter((i) => {
49+
const matched = getFileTypeFromModuleId(i.module)
50+
return filterModuleTypes.value.includes(matched.name)
51+
}).filter(settings.value.pluginDetailSelectedHook ? i => i.type === settings.value.pluginDetailSelectedHook : Boolean)
52+
})
53+
54+
function toggleModuleType(rule: FilterMatchRule) {
55+
if (filterModuleTypes.value?.includes(rule.name)) {
56+
filterModuleTypes.value = filterModuleTypes.value?.filter(t => t !== rule.name)
57+
}
58+
else {
59+
filterModuleTypes.value?.push(rule.name)
60+
}
61+
settings.value.pluginDetailsModuleTypes = filterModuleTypes.value
62+
}
63+
64+
function normalizeTimestamp(timestamp: number) {
65+
return new Date(timestamp).toLocaleString(undefined, {
66+
hour12: false,
67+
year: 'numeric',
68+
month: '2-digit',
69+
day: '2-digit',
70+
hour: '2-digit',
71+
minute: '2-digit',
72+
second: '2-digit',
73+
fractionalSecondDigits: 3,
74+
})
75+
}
76+
77+
function toggleDurationSortType() {
78+
next()
79+
settings.value.pluginDetailsDurationSortType = durationSortType.value
80+
}
81+
</script>
82+
83+
<template>
84+
<table w-full border-separate border-spacing-0>
85+
<thead border="b base">
86+
<tr px2 class="[&_th]:(sticky top-0 z10 border-b border-base)">
87+
<th v-if="selectedFields.includes('hookName')" bg-base w32 ws-nowrap p1 text-center font-600>
88+
Hook name
89+
</th>
90+
<th v-if="selectedFields.includes('module')" bg-base min-w100 ws-nowrap p1 text-left font-600>
91+
<button flex="~ row gap1 items-center" w-full>
92+
Module
93+
<VMenu>
94+
<span w-6 h-6 rounded-full cursor-pointer hover="bg-active" flex="~ items-center justify-center">
95+
<i text-xs class="i-carbon-filter" :class="filterModuleTypes.length !== searchFilterTypes.length ? 'text-primary op100' : 'op50'" />
96+
</span>
97+
<template #popper>
98+
<div class="p2" flex="~ col gap2">
99+
<label
100+
v-for="rule of searchFilterTypes"
101+
:key="rule.name"
102+
border="~ base rounded-md" px2 py1
103+
flex="~ items-center gap-1"
104+
select-none
105+
:title="rule.description"
106+
class="cursor-pointer module-type-filter"
107+
>
108+
<input
109+
type="checkbox"
110+
mr1
111+
:checked="filterModuleTypes?.includes(rule.name)"
112+
@change="toggleModuleType(rule)"
113+
>
114+
<div :class="rule.icon" icon-catppuccin />
115+
<div text-sm>{{ rule.description || rule.name }}</div>
116+
</label>
117+
</div>
118+
</template>
119+
</VMenu>
120+
</button>
121+
</th>
122+
<th v-if="selectedFields.includes('startTime')" rounded-tr-2 bg-base ws-nowrap p1 text-center font-600>
123+
Start Time
124+
</th>
125+
<th v-if="selectedFields.includes('endTime')" rounded-tr-2 bg-base ws-nowrap p1 text-center font-600>
126+
End Time
127+
</th>
128+
<th v-if="selectedFields.includes('duration')" rounded-tr-2 bg-base ws-nowrap p1 text-center font-600>
129+
<button flex="~ row gap1 items-center justify-center" w-full @click="toggleDurationSortType">
130+
Duration
131+
<span w-6 h-6 rounded-full cursor-pointer hover="bg-active" flex="~ items-center justify-center">
132+
<i text-xs :class="[durationSortType !== 'asc' ? 'i-carbon-arrow-down' : 'i-carbon-arrow-up', durationSortType ? 'op100 text-primary' : 'op50']" />
133+
</span>
134+
</button>
135+
</th>
136+
</tr>
137+
</thead>
138+
<tbody v-if="filtered.length">
139+
<tr v-for="(item, index) in filtered" :key="item.id" class="[&_td]:(border-base border-b-1 border-dashed)" :class="[index === filtered.length - 1 ? '[&_td]:(border-b-0)' : '']">
140+
<td v-if="selectedFields.includes('hookName')" w32 ws-nowrap text-center text-sm op80>
141+
{{ HOOK_NAME_MAP[item.type] }}
142+
</td>
143+
<td v-if="selectedFields.includes('module')" min-w100 text-left text-ellipsis line-clamp-2>
144+
<DisplayModuleId
145+
:id="item.module"
146+
w-full border-none
147+
:session="session"
148+
:link="`/session/${session.id}/graph?module=${item.module}`"
149+
hover="bg-active"
150+
border="~ base rounded" block px2 py1
151+
/>
152+
</td>
153+
<td v-if="selectedFields.includes('startTime')" text-center font-mono text-sm min-w52 op80>
154+
<time v-if="item.timestamp_start" :datetime="new Date(item.timestamp_start).toISOString()">{{ normalizeTimestamp(item.timestamp_start) }}</time>
155+
</td>
156+
<td v-if="selectedFields.includes('endTime')" text-center font-mono text-sm min-w52 op80>
157+
<time v-if="item.timestamp_end" :datetime="new Date(item.timestamp_end).toISOString()">{{ normalizeTimestamp(item.timestamp_end) }}</time>
158+
</td>
159+
<td v-if="selectedFields.includes('duration')" text-center text-sm>
160+
<DisplayDuration :duration="item.duration" />
161+
</td>
162+
</tr>
163+
</tbody>
164+
<tbody v-else>
165+
<tr>
166+
<td :colspan="selectedFields.length" p4>
167+
<div w-full h-48 flex="~ items-center justify-center" op50 italic>
168+
No data
169+
</div>
170+
</td>
171+
</tr>
172+
</tbody>
173+
</table>
174+
</template>

packages/devtools-vite/src/app/components/data/SearchPanel.vue

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,20 @@ import type { FilterMatchRule } from '~/utils/icon'
33
import { useVModel } from '@vueuse/core'
44
import { withDefaults } from 'vue'
55
6-
interface ModelValue { search: string, selected?: string[] | null }
6+
interface ModelValue { search?: string | false, selected?: string[] | null }
77
88
const props = withDefaults(
99
defineProps<{
1010
rules: FilterMatchRule[]
1111
modelValue?: ModelValue
12+
selectedContainerClass?: string
1213
}>(),
1314
{
1415
modelValue: () => ({
1516
search: '',
1617
selected: null,
1718
}),
19+
selectedContainerClass: '',
1820
},
1921
)
2022
@@ -71,7 +73,7 @@ function unselectToggle() {
7173

7274
<template>
7375
<div flex="col gap-2" max-w-90vw min-w-30vw border="~ base rounded-xl" bg-glass>
74-
<div>
76+
<div v-if="modelValue.search !== false">
7577
<input
7678
v-model="model.search"
7779
p2 px4
@@ -80,7 +82,7 @@ function unselectToggle() {
8082
placeholder="Search"
8183
>
8284
</div>
83-
<div v-if="rules.length" flex="~ gap-2 wrap" p2 border="t base">
85+
<div v-if="rules.length" :class="selectedContainerClass" flex="~ gap-2 wrap" p2 border="t base">
8486
<label
8587
v-for="rule of rules"
8688
:key="rule.name"

0 commit comments

Comments
 (0)