Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: timeline panel #627

Merged
merged 8 commits into from
Oct 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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