Skip to content

Commit

Permalink
feat: filter tree/states for custom inspectors (#664)
Browse files Browse the repository at this point in the history
  • Loading branch information
alexzhang1030 authored Oct 31, 2024
1 parent b478639 commit dce7c1e
Show file tree
Hide file tree
Showing 10 changed files with 128 additions and 52 deletions.
2 changes: 2 additions & 0 deletions packages/applet/src/composables/custom-inspector-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ type CustomInspectorState = Partial<{
label: string
logo: string
timelineLayerIds: string[]
treeFilterPlaceholder: string
stateFilterPlaceholder: string
}>

const VueDevToolsStateSymbol: InjectionKey<Ref<CustomInspectorState>> = Symbol.for('VueDevToolsCustomInspectorStateSymbol')
Expand Down
33 changes: 10 additions & 23 deletions packages/applet/src/modules/components/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import RootStateViewer from '~/components/state/RootStateViewer.vue'
import ComponentTree from '~/components/tree/TreeViewer.vue'
import { createSelectedContext } from '~/composables/select'
import { createExpandedContext } from '~/composables/toggle-expanded'
import { searchDeepInObject } from '~/utils'
import { filterInspectorState } from '~/utils'
import ComponentRenderCode from './components/RenderCode.vue'
const emit = defineEmits(['openInEditor', 'onInspectComponentStart', 'onInspectComponentEnd'])
Expand Down Expand Up @@ -104,27 +104,14 @@ const activeTreeNode = computed(() => {
})
const activeTreeNodeFilePath = computed(() => activeTreeNode.value?.file ?? '')
const filteredState = computed(() => {
const result = {}
for (const groupKey in activeComponentState.value) {
const group = activeComponentState.value[groupKey]
const groupFields = group.filter((el) => {
try {
return searchDeepInObject({
[el.key]: el.value,
}, filterStateName.value)
}
catch (e) {
return {
[el.key]: e,
}
}
})
const normalized = flatten(Object.values(groupBy(sortByKey(groupFields), 'stateType')))
if (groupFields.length)
result[groupKey] = normalized
}
return result
const displayState = computed(() => {
return filterInspectorState({
state: activeComponentState.value,
filterKey: filterStateName.value,
processGroup(groupFields) {
return flatten(Object.values(groupBy(sortByKey(groupFields), 'stateType')))
},
})
})
const { expanded: expandedTreeNodes } = createExpandedContext()
Expand Down Expand Up @@ -357,7 +344,7 @@ function toggleApp(id: string) {
<i v-if="activeTreeNodeFilePath" v-tooltip.bottom="'Open in Editor'" class="i-carbon-launch h-4 w-4 cursor-pointer hover:(op-70)" @click="openInEditor" />
</div>
</div>
<RootStateViewer class="no-scrollbar flex-1 select-none overflow-scroll" :data="filteredState" :node-id="activeComponentId" :inspector-id="inspectorId" expanded-state-id="component-state" />
<RootStateViewer class="no-scrollbar flex-1 select-none overflow-scroll" :data="displayState" :node-id="activeComponentId" :inspector-id="inspectorId" expanded-state-id="component-state" />
</div>
<ComponentRenderCode v-if="componentRenderCodeVisible && componentRenderCode" :code="componentRenderCode" @close="closeComponentRenderCode" />
</Pane>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import type { CustomInspectorNode, CustomInspectorOptions, CustomInspectorState } from '@vue/devtools-kit'
import { DevToolsMessagingEvents, onRpcConnected, rpc } from '@vue/devtools-core'
import { parse } from '@vue/devtools-kit'
import { vTooltip, VueIcIcon } from '@vue/devtools-ui'
import { vTooltip, VueIcIcon, VueInput } from '@vue/devtools-ui'
import { until } from '@vueuse/core'
import { Pane, Splitpanes } from 'splitpanes'
import { computed, onUnmounted, ref, watch } from 'vue'
Expand All @@ -13,6 +13,7 @@ import RootStateViewer from '~/components/state/RootStateViewer.vue'
import ComponentTree from '~/components/tree/TreeViewer.vue'
import { useCustomInspectorState } from '~/composables/custom-inspector-state'
import { createExpandedContext } from '~/composables/toggle-expanded'
import { filterInspectorState } from '~/utils'
const { expanded: expandedTreeNodes } = createExpandedContext()
const { expanded: expandedStateNodes } = createExpandedContext('custom-inspector-state')
Expand All @@ -32,6 +33,24 @@ const selected = ref('')
const state = ref<Record<string, CustomInspectorState[]>>({})
const emptyState = computed(() => !Object.keys(state.value).length)
const inspectorState = useCustomInspectorState()
const filterTreeKey = ref('')
const filterStateKey = ref('')
watch(filterTreeKey, (value, oldValue) => {
if (!value.trim().length && !oldValue.trim().length)
return
getInspectorTree(value)
})
const displayState = computed(() => {
return filterInspectorState({
state: state.value,
filterKey: filterStateKey.value,
})
})
// tree
function dfs(node: { id: string, children?: { id: string }[] }, path: string[] = [], linkedList: string[][] = []) {
path.push(node.id)
Expand Down Expand Up @@ -120,8 +139,8 @@ watch(selected, () => {
getInspectorState(selected.value)
})
const getInspectorTree = () => {
rpc.value.getInspectorTree({ inspectorId: inspectorId.value, filter: '' }).then((_data) => {
function getInspectorTree(filter = '') {
rpc.value.getInspectorTree({ inspectorId: inspectorId.value, filter }).then((_data) => {
const data = parse(_data!)
tree.value = data
if (!selected.value && data.length) {
Expand All @@ -132,7 +151,7 @@ const getInspectorTree = () => {
})
}
until(inspectorId).toBeTruthy().then(getInspectorTree)
until(inspectorId).toBeTruthy().then(() => getInspectorTree())
function onInspectorTreeUpdated(_data: string) {
const data = parse(_data) as {
Expand Down Expand Up @@ -179,41 +198,46 @@ onUnmounted(() => {
<DevToolsHeader :doc-link="customInspectState.homepage!">
<Navbar />
</DevToolsHeader>
<template v-if="tree.length">
<Empty v-if="!tree.length && !filterTreeKey.trim().length">
No Data
</Empty>
<template v-else>
<Splitpanes class="flex-1 overflow-auto">
<Pane border="r base" size="40" h-full>
<div class="h-full flex flex-col p2">
<div v-if="actions?.length" class="mb-1 flex justify-end pb-1" border="b dashed base">
<div class="flex items-center gap-2 px-1">
<div class="grid grid-cols-[1fr_auto] mb1 items-center gap2 pb1" border="b dashed base">
<VueInput v-model="filterTreeKey" :placeholder="inspectorState.treeFilterPlaceholder" />
<div v-if="actions?.length" class="flex items-center gap-2 px-1">
<div v-for="(action, index) in actions" :key="index" v-tooltip.bottom-end="{ content: action.tooltip }" class="flex items-center gap1" @click="callAction(index)">
<VueIcIcon :name="`baseline-${action.icon.replace(/\_/g, '-')}`" cursor-pointer op70 text-base hover:op100 />
</div>
</div>
</div>
<div class="no-scrollbar flex-1 select-none overflow-scroll">
<div v-if="tree.length" class="no-scrollbar flex-1 select-none overflow-scroll">
<ComponentTree v-model="selected" :data="tree" />
</div>
<Empty v-else>
No Data
</Empty>
</div>
</Pane>
<Pane size="60">
<div class="h-full flex flex-col p2">
<div v-if="nodeActions?.length" class="mb-1 flex justify-end pb-1" border="b dashed base">
<div class="flex items-center gap-2 px-1">
<div class="grid grid-cols-[1fr_auto] mb1 items-center gap2 pb1" border="b dashed base">
<VueInput v-model="filterStateKey" :placeholder="inspectorState.stateFilterPlaceholder" />
<div v-if="nodeActions?.length" class="flex items-center gap-2 px-1">
<div v-for="(action, index) in nodeActions" :key="index" v-tooltip.bottom-end="{ content: action.tooltip }" class="flex items-center gap1" @click="callNodeAction(index)">
<VueIcIcon :name="`baseline-${action.icon.replace(/\_/g, '-')}`" cursor-pointer op70 text-base hover:op100 />
</div>
</div>
</div>
<RootStateViewer v-if="selected && !emptyState" :data="state" :node-id="selected" :inspector-id="inspectorId" expanded-state-id="custom-inspector-state" class="no-scrollbar flex-1 select-none overflow-scroll" />
<RootStateViewer v-if="selected && !emptyState" :data="displayState" :node-id="selected" :inspector-id="inspectorId" expanded-state-id="custom-inspector-state" class="no-scrollbar flex-1 select-none overflow-scroll" />
<Empty v-else>
No Data
</Empty>
</div>
</Pane>
</Splitpanes>
</template>
<Empty v-else>
No Data
</Empty>
</div>
</template>
2 changes: 2 additions & 0 deletions packages/applet/src/modules/custom-inspector/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ function getInspectorInfo() {
logo: payload?.logo,
timelineLayerIds: payload?.timelineLayers.map(item => item.id),
pluginId: props.pluginId,
treeFilterPlaceholder: payload.treeFilterPlaceholder,
stateFilterPlaceholder: payload.stateFilterPlaceholder,
}
inspectorState.value = state
restoreRouter()
Expand Down
39 changes: 30 additions & 9 deletions packages/applet/src/modules/pinia/components/store/Index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import type { CustomInspectorNode, CustomInspectorOptions, CustomInspectorState } from '@vue/devtools-kit'
import { DevToolsMessagingEvents, rpc } from '@vue/devtools-core'
import { parse } from '@vue/devtools-kit'
import { vTooltip } from '@vue/devtools-ui'
import { vTooltip, VueInput } from '@vue/devtools-ui'
import { Pane, Splitpanes } from 'splitpanes'
import { computed, onUnmounted, ref, watch } from 'vue'
import DevToolsHeader from '~/components/basic/DevToolsHeader.vue'
Expand All @@ -11,21 +11,40 @@ import Navbar from '~/components/basic/Navbar.vue'
import RootStateViewer from '~/components/state/RootStateViewer.vue'
import ComponentTree from '~/components/tree/TreeViewer.vue'
import { useCustomInspectorState } from '~/composables/custom-inspector-state'
import { createExpandedContext } from '~/composables/toggle-expanded'
import { filterInspectorState } from '~/utils'
import { PiniaPluginInspectorId } from '../../constants'
const { expanded: expandedTreeNodes } = createExpandedContext()
const { expanded: expandedStateNodes } = createExpandedContext('pinia-store-state')
const inspectorId = 'pinia'
const inspectorId = PiniaPluginInspectorId
const nodeActions = ref<CustomInspectorOptions['nodeActions']>([])
const actions = ref<CustomInspectorOptions['nodeActions']>([])
const inspectorState = useCustomInspectorState()
const selected = ref('')
const tree = ref<CustomInspectorNode[]>([])
const treeNodeLinkedList = computed(() => tree.value?.length ? dfs(tree.value?.[0]) : [])
const flattenedTreeNodes = computed(() => flattenTreeNodes(tree.value))
const flattenedTreeNodesIds = computed(() => flattenedTreeNodes.value.map(node => node.id))
const state = ref<Record<string, CustomInspectorState[]>>({})
const filterStoreKey = ref('')
const filterStateKey = ref('')
watch(filterStoreKey, (value, oldValue) => {
if (!value.trim().length && !oldValue.trim().length)
return
getPiniaInspectorTree(value)
})
const displayState = computed(() => {
return filterInspectorState({
state: state.value,
filterKey: filterStateKey.value,
})
})
const emptyState = computed(() => !state.value.state?.length && !state.value.getters?.length)
Expand Down Expand Up @@ -118,8 +137,8 @@ watch(selected, () => {
getPiniaState(selected.value)
})
const getPiniaInspectorTree = () => {
rpc.value.getInspectorTree({ inspectorId, filter: '' }).then((_data) => {
function getPiniaInspectorTree(filter: string = '') {
rpc.value.getInspectorTree({ inspectorId, filter }).then((_data) => {
const data = parse(_data!)
tree.value = data
if (!selected.value && data.length) {
Expand Down Expand Up @@ -184,8 +203,9 @@ onUnmounted(() => {
<Splitpanes class="flex-1 overflow-auto">
<Pane border="r base" size="40" h-full>
<div class="h-full flex flex-col p2">
<div v-if="actions?.length" class="mb-1 flex justify-end pb-1" border="b dashed base">
<div class="flex items-center gap-2 px-1">
<div class="grid grid-cols-[1fr_auto] mb1 items-center gap2 pb1" border="b dashed base">
<VueInput v-model="filterStoreKey" :placeholder="inspectorState.treeFilterPlaceholder" />
<div v-if="actions?.length" class="flex items-center gap-2 px-1">
<div v-for="(action, index) in actions" :key="index" v-tooltip.bottom-end="{ content: action.tooltip }" class="flex items-center gap1" @click="callAction(index)">
<i :class="`i-ic-baseline-${action.icon.replace(/\_/g, '-')}`" cursor-pointer op70 text-base hover:op100 />
</div>
Expand All @@ -198,14 +218,15 @@ onUnmounted(() => {
</Pane>
<Pane size="60">
<div class="h-full flex flex-col p2">
<div v-if="nodeActions?.length" class="mb-1 flex justify-end pb-1" border="b dashed base">
<div class="flex items-center gap-2 px-1">
<div class="grid grid-cols-[1fr_auto] mb1 items-center gap2 pb1" border="b dashed base">
<VueInput v-model="filterStateKey" :placeholder="inspectorState.stateFilterPlaceholder" />
<div v-if="nodeActions?.length" class="flex items-center gap-2 px-1">
<div v-for="(action, index) in nodeActions" :key="index" v-tooltip.bottom-end="{ content: action.tooltip }" class="flex items-center gap1" @click="callNodeAction(index)">
<i :class="`i-ic-baseline-${action.icon.replace(/\_/g, '-')}`" cursor-pointer op70 text-base hover:op100 />
</div>
</div>
</div>
<RootStateViewer v-if="selected && !emptyState" class="no-scrollbar flex-1 select-none overflow-scroll" :data="state" :node-id="selected" :inspector-id="inspectorId" expanded-state-id="pinia-store-state" />
<RootStateViewer v-if="selected && !emptyState" class="no-scrollbar flex-1 select-none overflow-scroll" :data="displayState" :node-id="selected" :inspector-id="inspectorId" expanded-state-id="pinia-store-state" />
<Empty v-else>
No Data
</Empty>
Expand Down
2 changes: 2 additions & 0 deletions packages/applet/src/modules/pinia/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const PiniaPluginDescriptorId = 'dev.esm.pinia'
export const PiniaPluginInspectorId = 'pinia'
15 changes: 13 additions & 2 deletions packages/applet/src/modules/pinia/index.vue
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
<script setup lang="ts">
import { onRpcConnected, rpc } from '@vue/devtools-core'
import { computed, provide, ref } from 'vue'
import { createCustomInspectorStateContext } from '~/composables/custom-inspector-state'
import { registerVirtualRouter, VirtualRoute } from '~/composables/virtual-router'
import About from './components/About.vue'
import Settings from './components/Settings.vue'
import Store from './components/store/Index.vue'
import Timeline from './components/timeline/Index.vue'
import { PiniaPluginDescriptorId, PiniaPluginInspectorId } from './constants'
const pluginSettings = ref(null)
provide('pluginSettings', pluginSettings)
Expand Down Expand Up @@ -43,9 +45,10 @@ const { VirtualRouterView, restoreRouter } = registerVirtualRouter(routes, {
defaultRoutePath: '/store',
})
const inspectorState = createCustomInspectorStateContext()
onRpcConnected(() => {
const pluginDescriptorId = 'dev.esm.pinia'
rpc.value.getPluginSettings(pluginDescriptorId).then((settings) => {
rpc.value.getPluginSettings(PiniaPluginDescriptorId).then((settings) => {
if (settings.options) {
// @ts-expect-error skip type check
pluginSettings.value = settings
Expand All @@ -54,6 +57,14 @@ onRpcConnected(() => {
pluginSettings.value = null
}
})
rpc.value.getInspectorInfo(PiniaPluginInspectorId).then((payload) => {
if (!payload)
return
inspectorState.value = {
stateFilterPlaceholder: payload.stateFilterPlaceholder,
treeFilterPlaceholder: payload.treeFilterPlaceholder,
}
})
})
</script>

Expand Down
25 changes: 24 additions & 1 deletion packages/applet/src/utils/search.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { INFINITY, isPlainObject, NAN, NEGATIVE_INFINITY, UNDEFINED } from '@vue/devtools-kit'
import { CustomInspectorState, INFINITY, isPlainObject, NAN, NEGATIVE_INFINITY, UNDEFINED } from '@vue/devtools-kit'

/**
* Searches a key or value in the object, with a maximum deepness
Expand Down Expand Up @@ -132,3 +132,26 @@ function internalSearchArray(array, searchTerm, seen, depth) {
}
return match
}

export function filterInspectorState<T extends CustomInspectorState>(params: {
state: Record<string, T[]>
filterKey?: string | null | undefined
// Each group is a flatten object
processGroup?: (item: T[]) => T[]
}) {
const { state, filterKey, processGroup } = params
if (!filterKey || !filterKey.trim().length)
return state
const result = {}
for (const groupKey in state) {
const group = state[groupKey]
const groupFields = group.filter(el => searchDeepInObject({
// @ts-expect-error typing weak
[el.key]: el.value,
}, filterKey))
if (groupFields.length) {
result[groupKey] = processGroup ? processGroup(groupFields) : groupFields
}
}
return result
}
2 changes: 1 addition & 1 deletion packages/devtools-kit/src/ctx/hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ export function createDevToolsCtxHooks() {
const _payload = {
app: plugin.descriptor.app,
inspectorId,
filter: inspector?.treeFilter || '',
filter: inspector?.treeFilterPlaceholder || '',
rootNodes: [],
}

Expand Down
8 changes: 6 additions & 2 deletions packages/devtools-kit/src/ctx/inspector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import { devtoolsTimelineLayers } from './timeline'
interface DevToolsKitInspector {
options: CustomInspectorOptions
descriptor: PluginDescriptor
treeFilter: string
treeFilterPlaceholder: string
stateFilterPlaceholder: string
selectedNodeId: string
appRecord: unknown
}
Expand All @@ -31,7 +32,8 @@ export function addInspector(inspector: CustomInspectorOptions, descriptor: Plug
devtoolsInspector.push({
options: inspector,
descriptor,
treeFilter: '',
treeFilterPlaceholder: inspector.treeFilterPlaceholder ?? 'Search tree...',
stateFilterPlaceholder: inspector.stateFilterPlaceholder ?? 'Search state...',
selectedNodeId: '',
appRecord: getAppRecord(descriptor.app),
})
Expand Down Expand Up @@ -76,6 +78,8 @@ export function getInspectorInfo(id: string) {
packageName: descriptor.packageName,
homepage: descriptor.homepage,
timelineLayers,
treeFilterPlaceholder: inspector.treeFilterPlaceholder,
stateFilterPlaceholder: inspector.stateFilterPlaceholder,
}
}

Expand Down

0 comments on commit dce7c1e

Please sign in to comment.