Skip to content

Commit

Permalink
feat: timeline panel (#627)
Browse files Browse the repository at this point in the history
  • Loading branch information
webfansplz authored Oct 16, 2024
1 parent c3ec0bc commit d1e3a00
Show file tree
Hide file tree
Showing 23 changed files with 581 additions and 16 deletions.
6 changes: 6 additions & 0 deletions docs/getting-started/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ Components tab shows your components information, including the node tree, state

![components](/features/components.png)

## Timeline

Timeline tab shows the performance of your app, including the time spent on rendering, updating, and so on.

![timeline](/features/timeline.png)

## Assets(Vite only)

Assets tab shows your files from the project directory, you can see the information of selected file with some helpful actions.
Expand Down
13 changes: 2 additions & 11 deletions docs/guide/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,11 @@ The v7 version of devtools only supports Vue3. If your application is still usin

In v7, we've made some feature-level adjustments compared to v6. You can view the v7 feature overview in the [Features](/getting-started/features). Here, we mainly mention some of the main feature changes.

### Deprecated Features

Due to high performance costs and potential memory leak risks, we have removed some features in v7. These features are:

- `Performance` Timeline
- `Component Events` Timeline

💡 By the way, we are looking for a balanced approach to re-enable it with better performance. You can follow the latest progress [here](https://github.com/vuejs/devtools-next/issues/609).

### Feature Adjustments

- Timeline Tab
- Plugin Timeline Tab

In v7, we moved the timeline tab to be managed within each plugin's menu. Here is a screenshot of the pinia devtools plugin:
In v7, we moved the plugin timeline tab to be managed within each plugin's menu. Here is a screenshot of the pinia devtools plugin:

![pinia-timeline](/features/pinia-timeline.png)

Expand Down
Binary file added docs/public/features/timeline.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 13 additions & 3 deletions packages/applet/src/components/timeline/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,14 @@ import RootStateViewer from '~/components/state/RootStateViewer.vue'
import { createExpandedContext } from '~/composables/toggle-expanded'
import EventList from './EventList.vue'
const props = defineProps<{
const props = withDefaults(defineProps<{
layerIds: string[]
docLink: string
githubRepoLink?: string
}>()
headerVisible?: boolean
}>(), {
headerVisible: true,
})
const { expanded: expandedStateNodes } = createExpandedContext('timeline-state')
Expand Down Expand Up @@ -92,11 +95,18 @@ rpc.functions.on(DevToolsMessagingEvents.TIMELINE_EVENT_UPDATED, onTimelineEvent
onUnmounted(() => {
rpc.functions.off(DevToolsMessagingEvents.TIMELINE_EVENT_UPDATED, onTimelineEventUpdated)
})
defineExpose({
clear() {
eventList.value = []
groupList.value.clear()
},
})
</script>

<template>
<div class="h-full flex flex-col">
<DevToolsHeader :doc-link="docLink" :github-repo-link="githubRepoLink">
<DevToolsHeader v-if="headerVisible" :doc-link="docLink" :github-repo-link="githubRepoLink">
<Navbar />
</DevToolsHeader>
<template v-if="eventList.length">
Expand Down
7 changes: 7 additions & 0 deletions packages/applet/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import SelectiveList from './components/basic/SelectiveList.vue'
import Timeline from './components/timeline/index.vue'
import 'uno.css'
import '@unocss/reset/tailwind.css'
import './styles/base.css'
Expand All @@ -9,3 +11,8 @@ export * from './modules/components'
export * from './modules/custom-inspector'
export * from './modules/pinia'
export * from './modules/router'

export {
SelectiveList,
Timeline,
}
107 changes: 107 additions & 0 deletions packages/client/src/components/timeline/TimelineLayers.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
<script setup lang="ts">
import { rpc, useDevToolsState } from '@vue/devtools-core'
import { useDevToolsColorMode, vTooltip, VueIcIcon } from '@vue/devtools-ui'
import { defineModel } from 'vue'
defineProps<{ data: {
id: string
label: string
}[] }>()
const emit = defineEmits(['select', 'clear'])
const devtoolsState = useDevToolsState()
const recordingState = computed(() => devtoolsState.timelineLayersState.value.recordingState)
const timelineLayersState = computed(() => devtoolsState.timelineLayersState.value)
const recordingTooltip = computed(() => recordingState.value ? 'Stop recording' : 'Start recording')
const { colorMode } = useDevToolsColorMode()
const isDark = computed(() => colorMode.value === 'dark')
const selected = defineModel()
function select(id: string) {
selected.value = id
emit('select', id)
rpc.value.updateTimelineLayersState({
selected: id,
})
}
watch(() => timelineLayersState.value.selected, (state: string) => {
selected.value = state
}, {
immediate: true,
})
function getTimelineLayerEnabled(id: string) {
return {
'mouse': timelineLayersState.value.mouseEventEnabled,
'keyboard': timelineLayersState.value.keyboardEventEnabled,
'component-event': timelineLayersState.value.componentEventEnabled,
'performance': timelineLayersState.value.performanceEventEnabled,
}[id]
}
function toggleRecordingState() {
rpc.value.updateTimelineLayersState({
recordingState: !recordingState.value,
})
}
function toggleTimelineLayerEnabled(id: string) {
const normalizedId = {
'mouse': 'mouseEventEnabled',
'keyboard': 'keyboardEventEnabled',
'component-event': 'componentEventEnabled',
'performance': 'performanceEventEnabled',
}[id]
rpc.value.updateTimelineLayersState({
[normalizedId]: !getTimelineLayerEnabled(id),
})
}
</script>

<template>
<div h-full flex flex-col p2>
<div class="mb-1 flex justify-end pb-1" border="b dashed base">
<div class="flex items-center gap-2 px-1">
<div v-tooltip.bottom-end="{ content: recordingTooltip }" class="flex items-center gap1" @click="toggleRecordingState">
<span v-if="recordingState" class="recording recording-btn bg-[#ef4444]" />
<span v-else class="recording-btn bg-black op70 dark:(bg-white) hover:op100" />
</div>
<div v-tooltip.bottom-end="{ content: 'Clear all timelines' }" class="flex items-center gap1" @click="emit('clear')">
<VueIcIcon name="baseline-delete" cursor-pointer text-xl op70 hover:op100 />
</div>
<div v-tooltip.bottom-end="{ content: '<p style=\'width: 285px\'>Timeline events can cause significant performance overhead in large applications, so we recommend enabling it only when needed and on-demand. </p>', html: true }" class="flex items-center gap1">
<VueIcIcon name="baseline-tips-and-updates" cursor-pointer text-xl op70 hover:op100 />
</div>
</div>
</div>
<ul class="p2">
<li
v-for="item in data" :key="item.id"
class="group relative selectable-item"
:class="{ active: item.id === selected }"
@click="select(item.id)"
>
{{ item.label }}
<span class="absolute right-2 rounded-1 bg-primary-500 px1 text-3 text-white op0 [.active_&]:(bg-primary-400 dark:bg-gray-600) group-hover:op80 hover:op100!" @click.stop="toggleTimelineLayerEnabled(item.id)">
{{ getTimelineLayerEnabled(item.id) ? 'Disabled' : 'Enabled' }}
</span>
</li>
</ul>
</div>
</template>

<style scoped>
@keyframes pulse {
50% {
opacity: 0.5;
}
}
.recording-btn {
--at-apply: w-3.5 h-3.5 inline-flex cursor-pointer rounded-50%;
}
.recording {
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
transition-duration: 1s;
box-shadow: #ef4444 0 0 8px;
}
</style>
7 changes: 7 additions & 0 deletions packages/client/src/constants/tab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ export const builtinTab: [string, ModuleBuiltinTab[]][] = [
path: 'pages',
title: 'Pages',
},
{
icon: 'i-carbon-roadmap',
name: 'Timeline',
order: -100,
path: 'timeline',
title: 'Timeline',
},
{
icon: 'i-carbon-image-copy',
name: 'assets',
Expand Down
2 changes: 2 additions & 0 deletions packages/client/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import Pages from '~/pages/pages.vue'
import PiniaPage from '~/pages/pinia.vue'
import RouterPage from '~/pages/router.vue'
import Settings from '~/pages/settings.vue'
import Timeline from '~/pages/timeline.vue'
import App from './App.vue'
import '@unocss/reset/tailwind.css'
import 'uno.css'
Expand All @@ -32,6 +33,7 @@ const routes = [
{ path: '/pinia', component: PiniaPage },
{ path: '/router', component: RouterPage },
{ path: '/pages', component: Pages },
{ path: '/timeline', component: Timeline },
{ path: '/assets', component: Assets },
{ path: '/graph', component: Graph },
{ path: '/settings', component: Settings },
Expand Down
93 changes: 93 additions & 0 deletions packages/client/src/pages/timeline.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<script setup lang="ts">
import { SelectiveList, Timeline } from '@vue/devtools-applet'
import {
rpc,
useDevToolsState,
} from '@vue/devtools-core'
import { Pane, Splitpanes } from 'splitpanes'
const timelineRef = ref()
// responsive layout
const splitpanesRef = ref<HTMLDivElement>()
const splitpanesReady = ref(false)
const { width: splitpanesWidth } = useElementSize(splitpanesRef)
// prevent `Splitpanes` layout from being changed before it ready
const horizontal = computed(() => splitpanesReady.value ? splitpanesWidth.value < 700 : false)
// #region toggle app
const devtoolsState = useDevToolsState()
const appRecords = computed(() => devtoolsState.appRecords.value.map(app => ({
label: app.name + (app.version ? ` (${app.version})` : ''),
value: app.id,
})))
const normalizedAppRecords = computed(() => appRecords.value.map(app => ({
label: app.label,
id: app.value,
})))
const activeAppRecordId = ref(devtoolsState.activeAppRecordId.value)
watchEffect(() => {
activeAppRecordId.value = devtoolsState.activeAppRecordId.value
})
function toggleApp(id: string) {
rpc.value.toggleApp(id).then(() => {
clearTimelineEvents()
})
}
// #endregion
const activeTimelineLayer = ref('')
const timelineLayers = [
{
label: 'Mouse',
id: 'mouse',
},
{
label: 'Keyboard',
id: 'keyboard',
},
{
label: 'Component events',
id: 'component-event',
},
{
label: 'Performance',
id: 'performance',
},
]
function clearTimelineEvents() {
timelineRef.value?.clear()
}
function toggleTimelineLayer() {
clearTimelineEvents()
}
</script>

<template>
<div class="h-full w-full">
<Splitpanes ref="splitpanesRef" class="flex-1 overflow-auto" :horizontal="horizontal" @ready="splitpanesReady = true">
<Pane v-if="appRecords.length > 1" border="base h-full" size="20">
<div class="no-scrollbar h-full flex select-none gap-2 overflow-scroll">
<SelectiveList v-model="activeAppRecordId" :data="normalizedAppRecords" class="w-full" @select="toggleApp" />
</div>
</Pane>
<Pane border="base" h-full>
<div class="h-full flex flex-col">
<div class="no-scrollbar h-full flex select-none gap-2 overflow-scroll">
<TimelineLayers v-model="activeTimelineLayer" :data="timelineLayers" class="w-full" @select="toggleTimelineLayer" @clear="clearTimelineEvents" />
</div>
</div>
</Pane>
<Pane relative h-full size="65">
<div class="h-full flex flex-col p2">
<Timeline ref="timelineRef" :layer-ids="[activeTimelineLayer]" :header-visible="false" doc-link="" />
</div>
</Pane>
</Splitpanes>
</div>
</template>
6 changes: 5 additions & 1 deletion packages/core/src/rpc/global.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { DevToolsV6PluginAPIHookKeys, DevToolsV6PluginAPIHookPayloads, OpenInEditorOptions } from '@vue/devtools-kit'
import { devtools, DevToolsContextHookKeys, DevToolsMessagingHookKeys, devtoolsRouter, devtoolsRouterInfo, getActiveInspectors, getInspector, getInspectorActions, getInspectorInfo, getInspectorNodeActions, getRpcClient, getRpcServer, stringify, toggleClientConnected, updateDevToolsClientDetected } from '@vue/devtools-kit'
import { devtools, DevToolsContextHookKeys, DevToolsMessagingHookKeys, devtoolsRouter, devtoolsRouterInfo, getActiveInspectors, getInspector, getInspectorActions, getInspectorInfo, getInspectorNodeActions, getRpcClient, getRpcServer, stringify, toggleClientConnected, updateDevToolsClientDetected, updateTimelineLayersState } from '@vue/devtools-kit'
import { createHooks } from 'hookable'

const hooks = createHooks()
Expand Down Expand Up @@ -32,6 +32,7 @@ function getDevToolsState() {
routerId: item.routerId,
})),
activeAppRecordId: state.activeAppRecordId,
timelineLayersState: state.timelineLayersState,
}
}

Expand Down Expand Up @@ -93,6 +94,9 @@ export const functions = {
getInspectorActions(id: string) {
return getInspectorActions(id)
},
updateTimelineLayersState(state: Record<string, boolean>) {
return updateTimelineLayersState(state)
},
callInspectorNodeAction(inspectorId: string, actionIndex: number, nodeId: string) {
const nodeActions = getInspectorNodeActions(inspectorId)
if (nodeActions?.length) {
Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/vue-plugin/devtools-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ interface DevToolsState {
vitePluginDetected: boolean
appRecords: AppRecord[]
activeAppRecordId: string
timelineLayersState: Record<string, boolean>
}

type DevToolsRefState = {
Expand Down Expand Up @@ -44,6 +45,7 @@ export function createDevToolsStateContext() {
const vitePluginDetected = ref(false)
const appRecords = ref<Array<AppRecord>>([])
const activeAppRecordId = ref('')
const timelineLayersState = ref<Record<string, boolean>>({})

function updateState(data: DevToolsState) {
connected.value = data.connected
Expand All @@ -54,6 +56,7 @@ export function createDevToolsStateContext() {
vitePluginDetected.value = data.vitePluginDetected
appRecords.value = data.appRecords
activeAppRecordId.value = data.activeAppRecordId!
timelineLayersState.value = data.timelineLayersState!
}

function getDevToolsState() {
Expand All @@ -76,6 +79,7 @@ export function createDevToolsStateContext() {
vitePluginDetected,
appRecords,
activeAppRecordId,
timelineLayersState,
}
}

Expand Down
1 change: 1 addition & 0 deletions packages/devtools-kit/src/core/app/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export function createAppRecord(app: VueAppInstance['appContext']['app']): AppRe
id,
name,
instanceMap: new Map(),
perfGroupIds: new Map(),
rootInstance,
}

Expand Down
Loading

0 comments on commit d1e3a00

Please sign in to comment.