Skip to content

Commit

Permalink
feat(timeline): dialog to enable with one click
Browse files Browse the repository at this point in the history
  • Loading branch information
antfu committed Jul 18, 2023
1 parent 1050e05 commit 92dfd21
Show file tree
Hide file tree
Showing 8 changed files with 210 additions and 105 deletions.
7 changes: 7 additions & 0 deletions packages/devtools-kit/src/_types/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export interface ModuleOptions {
experimental?: {
/**
* Timline tab
* @deprecated Use `timeline.enable` instead
*/
timeline?: boolean
}
Expand All @@ -61,6 +62,12 @@ export interface ModuleOptions {
* Options for the timeline tab
*/
timeline?: {
/**
* Enable timeline tab
*
* @default false
*/
enabled?: boolean
/**
* Track on function calls
*/
Expand Down
1 change: 1 addition & 0 deletions packages/devtools-kit/src/_types/rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export interface ServerFunctions {
restartNuxt(hard?: boolean): Promise<void>
installNuxtModule(token: string, name: string, dry?: boolean): Promise<InstallModuleReturn>
uninstallNuxtModule(token: string, name: string, dry?: boolean): Promise<InstallModuleReturn>
enableTimeline(dry: boolean): Promise<[string, string]>
}

export interface ClientFunctions {
Expand Down
107 changes: 107 additions & 0 deletions packages/devtools/client/components/TimelineView.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
<script setup lang="ts">
import type { TimelineEvent } from '../../types'
const client = useClient()
const view = ref<'table' | 'list'>('table')
const selected = ref<TimelineEvent | undefined>()
const metrics = computed(() => client.value?.clientTimelineMetrics)
function clear() {
if (metrics.value)
metrics.value.events = []
}
function toggleView() {
view.value = view.value === 'table' ? 'list' : 'table'
}
</script>

<template>
<div v-if="metrics" h-screen of-hidden>
<div h-screen w-full flex flex-col>
<div h-10 flex="~ gap-2 items-center justify-end" p2 px3>
<VTooltip flex>
<div
text-lg
:class="metrics.options.enabled ? 'i-carbon-radio-button-checked text-primary animate-pulse' : 'i-carbon-pause-outline op30'"
/>
<template #popper>
<div text-sm>
{{ metrics.options.enabled ? 'Recording...' : 'Paused' }}
</div>
</template>
</VTooltip>

<NButton
v-if="!metrics.options.enabled"
size="small" ml1 text-sm
n="primary"
icon="i-carbon-play"
@click="metrics.options.enabled = true"
>
Start Tracking
</NButton>
<NButton
v-else
size="small" ml1 text-sm
n="orange"
icon="i-carbon-stop"
@click="metrics.options.enabled = false"
>
Stop Tracking
</NButton>
<!-- <template v-if="metrics.options.enabled">
<NCheckbox
v-model="metrics.options.stacktrace"
label="Enabled"
class="text-sm"
>
Record stacktrace
</NCheckbox>
<NCheckbox
v-model="metrics.options.arguments"
label="Enabled"
class="text-sm"
>
Record arguments
</NCheckbox>
</template> -->
<div flex-auto />
<NIconButton
:icon="view === 'table' ? 'i-carbon-roadmap' : 'i-carbon-list'"
class="ml-2"
title="Toggle View"
@click="toggleView"
/>
<NIconButton
icon="i-carbon-trash-can"
hover-text-red
class="ml-2"
@click="clear"
/>
</div>
<TimelineTable
v-if="view === 'table'"
:data="{ ...metrics }"
@select="s => selected = s.event"
/>
<TimelineList
v-else
:data="{ ...metrics }"
@select="s => selected = s"
/>
</div>
<DrawerBottom
:model-value="!!selected"
auto-close
@close="selected = undefined"
>
<div min-h-50 px3 py2>
<TimelineDetailsFunction v-if="selected?.type === 'function'" :record="selected" />
<TimelineDetailsRoute v-else-if="selected?.type === 'route'" :record="selected" />
</div>
</DrawerBottom>
</div>
</template>
4 changes: 4 additions & 0 deletions packages/devtools/client/composables/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ export function useServerConfig() {
return useAsyncState('getServerConfig', () => rpc.getServerConfig())
}

export function useModuleOptions() {
return useAsyncState('getModuleOptions', () => rpc.getModuleOptions())
}

export function useServerApp() {
return useAsyncState('getServerApp', () => rpc.getServerApp())
}
Expand Down
163 changes: 59 additions & 104 deletions packages/devtools/client/pages/modules/timeline.vue
Original file line number Diff line number Diff line change
@@ -1,123 +1,78 @@
<script setup lang="ts">
import type { TimelineEvent } from '../../../types'
definePageMeta({
icon: 'i-carbon-roadmap',
title: 'Timeline',
category: 'analyze',
show() {
const config = useServerConfig()
return () => {
if (typeof config.value?.devtools !== 'boolean')
return config.value?.devtools?.experimental.timeline
return false
}
},
})
const client = useClient()
const view = ref<'table' | 'list'>('table')
const selected = ref<TimelineEvent | undefined>()
const options = useModuleOptions()
const config = useServerConfig()
const metrics = computed(() => client.value?.clientTimelineMetrics)
const Dialog = createTemplatePromise<boolean, [string, string]>()
function clear() {
if (metrics.value)
metrics.value.events = []
}
const openInEditor = useOpenInEditor()
function toggleView() {
view.value = view.value === 'table' ? 'list' : 'table'
async function showPopup() {
const [source, modified] = await rpc.enableTimeline(true)
if (!await Dialog.start(source, modified))
return
await rpc.enableTimeline(false)
}
</script>

<template>
<div v-if="metrics" h-screen of-hidden>
<div h-screen w-full flex flex-col>
<div h-10 flex="~ gap-2 items-center justify-end" p2 px3>
<VTooltip flex>
<div
text-lg
:class="metrics.options.enabled ? 'i-carbon-radio-button-checked text-primary animate-pulse' : 'i-carbon-pause-outline op30'"
<TimelineView v-if="options?.timeline?.enabled" />
<template v-else>
<NPanelGrids>
<LaunchPage
icon="i-carbon-roadmap"
title="Timeline"
description="Timeline enables the inspection of when composable being executed and the route changes."
:actions="[
{
label: 'Enable',
},
]"
@action="showPopup"
/>
</NPanelGrids>
<Dialog v-slot="{ resolve, args }">
<NDialog :model-value="true" @close="resolve(false)">
<div flex="~ col gap-2" w-150 p4 border="t base">
<h2 text-xl>
<span capitalize>Enable Timeline?</span>
</h2>
<p op50>
Your <NLink
role="button" n="primary"
@click="openInEditor(config?._nuxtConfigFile)" v-text="'Nuxt config'"
/> will be updated as:
</p>
<CodeDiff
:from="args[0]"
:to="args[1]"
max-h-80 of-auto py2 border="~ base rounded"
lang="ts"
/>
<template #popper>
<div text-sm>
{{ metrics.options.enabled ? 'Recording...' : 'Paused' }}
</div>
</template>
</VTooltip>
<NButton
v-if="!metrics.options.enabled"
size="small" ml1 text-sm
n="primary"
icon="i-carbon-play"
@click="metrics.options.enabled = true"
>
Start Tracking
</NButton>
<NButton
v-else
size="small" ml1 text-sm
n="orange"
icon="i-carbon-stop"
@click="metrics.options.enabled = false"
>
Stop Tracking
</NButton>
<!-- <template v-if="metrics.options.enabled">
<NCheckbox
v-model="metrics.options.stacktrace"
label="Enabled"
class="text-sm"
>
Record stacktrace
</NCheckbox>
<NCheckbox
v-model="metrics.options.arguments"
label="Enabled"
class="text-sm"
>
Record arguments
</NCheckbox>
</template> -->
<div flex-auto />
<NIconButton
:icon="view === 'table' ? 'i-carbon-roadmap' : 'i-carbon-list'"
class="ml-2"
title="Toggle View"
@click="toggleView"
/>
<NIconButton
icon="i-carbon-trash-can"
hover-text-red
class="ml-2"
@click="clear"
/>
</div>
<TimelineTable
v-if="view === 'table'"
:data="{ ...metrics }"
@select="s => selected = s.event"
/>
<TimelineList
v-else
:data="{ ...metrics }"
@select="s => selected = s"
/>
</div>
<DrawerBottom
:model-value="!!selected"
auto-close
@close="selected = undefined"
>
<div min-h-50 px3 py2>
<TimelineDetailsFunction v-if="selected?.type === 'function'" :record="selected" />
<TimelineDetailsRoute v-else-if="selected?.type === 'route'" :record="selected" />
</div>
</DrawerBottom>
</div>
<p>
<span op50>Then Nuxt will </span><span text-orange>restart automatically</span>.
</p>
<div flex="~ gap-3" mt2 justify-end>
<NButton @click="resolve(false)">
Cancel
</NButton>
<NButton n="solid primary" capitalize @click="resolve(true)">
Enable
</NButton>
</div>
</div>
</NDialog>
</Dialog>
</template>
<HelpFab>
<DocsTimeline />
Expand Down
2 changes: 1 addition & 1 deletion packages/devtools/src/module-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ window.__NUXT_DEVTOOLS_TIME_METRIC__.appInit = Date.now()
options.vscode?.enabled
? import('./integrations/vscode').then(({ setup }) => setup(ctx))
: null,
options.experimental?.timeline
(options.experimental?.timeline || options.timeline?.enabled)
? import('./integrations/timeline').then(({ setup }) => setup(ctx))
: null,
]
Expand Down
2 changes: 2 additions & 0 deletions packages/devtools/src/server-rpc/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { setupTerminalRPC } from './terminals'
import { setupServerRoutesRPC } from './server-routes'
import { setupAnalyzeBuildRPC } from './analyze-build'
import { setupOptionsRPC } from './options'
import { setupTimelineRPC } from './timeline'

export function setupRPC(nuxt: Nuxt, options: ModuleOptions) {
const serverFunctions = {} as ServerFunctions
Expand Down Expand Up @@ -90,6 +91,7 @@ export function setupRPC(nuxt: Nuxt, options: ModuleOptions) {
...setupServerRoutesRPC(ctx),
...setupAnalyzeBuildRPC(ctx),
...setupOptionsRPC(ctx),
...setupTimelineRPC(ctx),
} satisfies ServerFunctions)

const wsClients = new Set<WebSocket>()
Expand Down
29 changes: 29 additions & 0 deletions packages/devtools/src/server-rpc/timeline.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import fs from 'node:fs/promises'
import { parseModule } from 'magicast'
import { getDefaultExportOptions } from 'magicast/helpers'
import type { NuxtDevtoolsServerContext, ServerFunctions } from '../types'

export function setupTimelineRPC({ nuxt }: NuxtDevtoolsServerContext) {
return {
async enableTimeline(dry: boolean) {
const filepath = nuxt.options._nuxtConfigFile
const source = await fs.readFile(filepath, 'utf-8')
const mod = await parseModule(source, { sourceFileName: filepath })

const options = getDefaultExportOptions(mod)

options.devtools = options.devtools || {}
options.devtools.timeline = options.devtools.timeline || {}
options.devtools.timeline.enabled = true

const generated = mod.generate().code

if (!dry) {
await fs.writeFile(filepath, generated, 'utf-8')
await nuxt.callHook('restart', { hard: true })
}

return [source, generated]
},
} satisfies Partial<ServerFunctions>
}

0 comments on commit 92dfd21

Please sign in to comment.