[Manager] Preview the individual nodes for packs on the registry (#3408)

Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
Christian Byrne
2025-04-12 04:18:41 +08:00
committed by GitHub
parent 60c0ce228a
commit 67835edfca
14 changed files with 223 additions and 107 deletions

View File

@@ -4,7 +4,10 @@
<div class="top-0 z-10 px-6 pt-6 w-full">
<InfoPanelHeader :node-packs="[nodePack]" />
</div>
<div class="p-6 pt-2 overflow-y-auto flex-1 text-sm hidden-scrollbar">
<div
ref="scrollContainer"
class="p-6 pt-2 overflow-y-auto flex-1 text-sm hidden-scrollbar"
>
<div class="mb-6">
<MetadataRow
v-if="isPackInstalled(nodePack.id)"
@@ -46,7 +49,7 @@
</template>
<script setup lang="ts">
import { whenever } from '@vueuse/core'
import { useScroll, whenever } from '@vueuse/core'
import { computed, provide, ref } from 'vue'
import { useI18n } from 'vue-i18n'
@@ -70,6 +73,8 @@ const { nodePack } = defineProps<{
nodePack: components['schemas']['Node']
}>()
const scrollContainer = ref<HTMLElement | null>(null)
const managerStore = useComfyManagerStore()
const isInstalled = computed(() => managerStore.isPackInstalled(nodePack.id))
const isInstalling = ref(false)
@@ -103,6 +108,17 @@ const infoItems = computed<InfoItem[]>(() => [
: undefined
}
])
const { y } = useScroll(scrollContainer, {
eventListenerOptions: {
passive: true
}
})
const onNodePackChange = () => {
y.value = 0
}
whenever(() => nodePack, onNodePackChange, { immediate: true, deep: true })
</script>
<style scoped>
.hidden-scrollbar {

View File

@@ -31,28 +31,29 @@
<script setup lang="ts">
import { useAsyncState } from '@vueuse/core'
import { computed } from 'vue'
import { computed, onUnmounted } from 'vue'
import PackStatusMessage from '@/components/dialog/content/manager/PackStatusMessage.vue'
import PackInstallButton from '@/components/dialog/content/manager/button/PackInstallButton.vue'
import InfoPanelHeader from '@/components/dialog/content/manager/infoPanel/InfoPanelHeader.vue'
import MetadataRow from '@/components/dialog/content/manager/infoPanel/MetadataRow.vue'
import PackIconStacked from '@/components/dialog/content/manager/packIcon/PackIconStacked.vue'
import { useComfyRegistryService } from '@/services/comfyRegistryService'
import { useComfyRegistryStore } from '@/stores/comfyRegistryStore'
import { components } from '@/types/comfyRegistryTypes'
const { nodePacks } = defineProps<{
nodePacks: components['schemas']['Node'][]
}>()
const comfyRegistryService = useComfyRegistryService()
const { getNodeDefs } = useComfyRegistryStore()
const getPackNodes = async (pack: components['schemas']['Node']) => {
if (!comfyRegistryService.packNodesAvailable(pack)) return []
return comfyRegistryService.getNodeDefs({
if (!pack.latest_version?.version) return []
const nodeDefs = await getNodeDefs.call({
packId: pack.id,
versionId: pack.latest_version?.id
version: pack.latest_version?.version
})
return nodeDefs?.comfy_nodes ?? []
}
const { state: allNodeDefs } = useAsyncState(
@@ -69,4 +70,8 @@ const totalNodesCount = computed(() =>
0
)
)
onUnmounted(() => {
getNodeDefs.cancel()
})
</script>

View File

@@ -14,7 +14,7 @@
<DescriptionTabPanel :node-pack="nodePack" />
</TabPanel>
<TabPanel value="nodes">
<NodesTabPanel :node-pack="nodePack" />
<NodesTabPanel :node-pack="nodePack" :node-names="nodeNames" />
</TabPanel>
</TabPanels>
</Tabs>
@@ -27,15 +27,21 @@ import TabList from 'primevue/tablist'
import TabPanel from 'primevue/tabpanel'
import TabPanels from 'primevue/tabpanels'
import Tabs from 'primevue/tabs'
import { ref } from 'vue'
import { computed, ref } from 'vue'
import DescriptionTabPanel from '@/components/dialog/content/manager/infoPanel/tabs/DescriptionTabPanel.vue'
import NodesTabPanel from '@/components/dialog/content/manager/infoPanel/tabs/NodesTabPanel.vue'
import { components } from '@/types/comfyRegistryTypes'
defineProps<{
const { nodePack } = defineProps<{
nodePack: components['schemas']['Node']
}>()
const nodeNames = computed(() => {
// @ts-expect-error comfy_nodes is an Algolia-specific field
const { comfy_nodes } = nodePack
return comfy_nodes ?? []
})
const activeTab = ref('description')
</script>

View File

@@ -1,47 +1,92 @@
<template>
<div class="flex flex-col gap-4 mt-4 overflow-auto text-sm">
<div v-if="nodeDefs?.length">
<!-- TODO: when registry returns node defs, use them here -->
</div>
<div
v-for="i in 3"
v-else
:key="i"
class="border border-surface-border rounded-lg p-4"
>
<NodePreview
:node-def="placeholderNodeDef"
class="!text-[.625rem] !min-w-full"
<div class="flex flex-col gap-4 mt-4 text-sm">
<template v-if="mappedNodeDefs?.length">
<div
v-for="nodeDef in mappedNodeDefs"
:key="createNodeDefKey(nodeDef)"
class="border border-surface-border rounded-lg p-4"
>
<NodePreview :node-def="nodeDef" class="!text-[.625rem] !min-w-full" />
</div>
</template>
<template v-else-if="isLoading">
<ProgressSpinner />
</template>
<template v-else-if="nodeNames.length">
<div v-for="node in nodeNames" :key="node" class="text-muted truncate">
{{ node }}
</div>
</template>
<template v-else>
<NoResultsPlaceholder
:title="$t('manager.noNodesFound')"
:message="$t('manager.noNodesFoundDescription')"
/>
</div>
</template>
</div>
</template>
<script setup lang="ts">
import NodePreview from '@/components/node/NodePreview.vue'
import { ComfyNodeDef } from '@/schemas/nodeDef/nodeDefSchemaV2'
import { components } from '@/types/comfyRegistryTypes'
import { whenever } from '@vueuse/core'
import ProgressSpinner from 'primevue/progressspinner'
import { computed, ref, shallowRef, useId } from 'vue'
defineProps<{
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
import NodePreview from '@/components/node/NodePreview.vue'
import { useComfyRegistryStore } from '@/stores/comfyRegistryStore'
import { components, operations } from '@/types/comfyRegistryTypes'
import { registryToFrontendV2NodeDef } from '@/utils/mapperUtil'
type ListComfyNodesResponse =
operations['ListComfyNodes']['responses'][200]['content']['application/json']['comfy_nodes']
const { nodePack, nodeNames } = defineProps<{
nodePack: components['schemas']['Node']
nodeDefs?: components['schemas']['ComfyNode'][]
nodeNames: string[]
}>()
// TODO: when registry returns node defs, use them here
const placeholderNodeDef: ComfyNodeDef = {
name: 'Sample Node',
display_name: 'Sample Node',
description: 'This is a sample node for preview purposes',
inputs: {
input1: { name: 'Input 1', type: 'IMAGE' },
input2: { name: 'Input 2', type: 'CONDITIONING' }
},
outputs: [
{ name: 'Output 1', type: 'IMAGE', index: 0, is_list: false },
{ name: 'Output 2', type: 'MASK', index: 1, is_list: false }
],
category: 'Utility',
output_node: false,
python_module: 'nodes'
const { getNodeDefs } = useComfyRegistryStore()
const isLoading = ref(false)
const registryNodeDefs = shallowRef<ListComfyNodesResponse | null>(null)
const fetchNodeDefs = async () => {
isLoading.value = true
const { id: packId } = nodePack
const version = nodePack.latest_version?.version
if (!packId || !version) {
registryNodeDefs.value = null
} else {
const response = await getNodeDefs.call({
packId,
version,
page: 1,
limit: 256
})
registryNodeDefs.value = response?.comfy_nodes ?? null
}
isLoading.value = false
}
whenever(() => nodePack, fetchNodeDefs, { immediate: true, deep: true })
const toFrontendNodeDef = (nodeDef: components['schemas']['ComfyNode']) => {
try {
return registryToFrontendV2NodeDef(nodeDef, nodePack)
} catch (error) {
return null
}
}
const mappedNodeDefs = computed(() => {
if (!registryNodeDefs.value) return null
return registryNodeDefs.value
.map(toFrontendNodeDef)
.filter((nodeDef) => nodeDef !== null)
})
const createNodeDefKey = (nodeDef: components['schemas']['ComfyNode']) =>
`${nodeDef.category}${nodeDef.comfy_node_name ?? useId()}`
</script>

View File

@@ -111,6 +111,8 @@
"manager": {
"title": "Custom Nodes Manager",
"failed": "Failed ({count})",
"noNodesFound": "No nodes found",
"noNodesFoundDescription": "The pack's nodes either could not be parsed, or the pack is a frontend extension only and doesn't have any nodes.",
"installationQueue": "Installation Queue",
"changingVersion": "Changing version from {from} to {to}",
"dependencies": "Dependencies",

View File

@@ -428,6 +428,8 @@
"loadingVersions": "Cargando versiones...",
"nightlyVersion": "Nocturna",
"noDescription": "No hay descripción disponible",
"noNodesFound": "No se encontraron nodos",
"noNodesFoundDescription": "Los nodos del paquete no se pudieron analizar, o el paquete es solo una extensión de frontend y no tiene ningún nodo.",
"noResultsFound": "No se encontraron resultados que coincidan con tu búsqueda.",
"nodePack": "Paquete de Nodos",
"packsSelected": "Paquetes Seleccionados",

View File

@@ -428,6 +428,8 @@
"loadingVersions": "Chargement des versions...",
"nightlyVersion": "Nocturne",
"noDescription": "Aucune description disponible",
"noNodesFound": "Aucun nœud trouvé",
"noNodesFoundDescription": "Les nœuds du pack n'ont pas pu être analysés, ou le pack est une extension frontend uniquement et n'a pas de nœuds.",
"noResultsFound": "Aucun résultat trouvé correspondant à votre recherche.",
"nodePack": "Pack de Nœuds",
"packsSelected": "Packs sélectionnés",

View File

@@ -428,6 +428,8 @@
"loadingVersions": "バージョンを読み込んでいます...",
"nightlyVersion": "ナイトリー",
"noDescription": "説明はありません",
"noNodesFound": "ノードが見つかりません",
"noNodesFoundDescription": "パックのノードは解析できなかったか、パックがフロントエンドの拡張機能のみでノードがない可能性があります。",
"noResultsFound": "検索に一致する結果が見つかりませんでした。",
"nodePack": "ノードパック",
"packsSelected": "選択したパック",

View File

@@ -428,6 +428,8 @@
"loadingVersions": "버전 로딩 중...",
"nightlyVersion": "최신 테스트 버전(nightly)",
"noDescription": "설명이 없습니다",
"noNodesFound": "노드를 찾을 수 없습니다",
"noNodesFoundDescription": "팩의 노드를 파싱할 수 없거나, 팩이 프론트엔드 확장만을 가지고 있어서 노드가 없습니다.",
"noResultsFound": "검색과 일치하는 결과가 없습니다.",
"nodePack": "노드 팩",
"packsSelected": "선택한 노드 팩",

View File

@@ -428,6 +428,8 @@
"loadingVersions": "Загрузка версий...",
"nightlyVersion": "Ночная",
"noDescription": "Описание отсутствует",
"noNodesFound": "Узлы не найдены",
"noNodesFoundDescription": "Узлы пакета не могут быть проанализированы, или пакет является только расширением интерфейса и не имеет узлов.",
"noResultsFound": "По вашему запросу ничего не найдено.",
"nodePack": "Пакет Узлов",
"packsSelected": "Выбрано пакетов",

View File

@@ -428,6 +428,8 @@
"loadingVersions": "正在加载版本...",
"nightlyVersion": "每夜",
"noDescription": "无可用描述",
"noNodesFound": "未找到节点",
"noNodesFoundDescription": "无法解析包的节点,或者该包仅为前端扩展,没有任何节点。",
"noResultsFound": "未找到符合您搜索的结果。",
"nodePack": "节点包",
"packsSelected": "选定的包",

View File

@@ -1,7 +1,6 @@
import axios, { AxiosError, AxiosResponse } from 'axios'
import { ref } from 'vue'
import { useNodeDefStore } from '@/stores/nodeDefStore'
import type { components, operations } from '@/types/comfyRegistryTypes'
import { isAbortError } from '@/utils/typeGuardUtil'
@@ -25,29 +24,6 @@ export const useComfyRegistryService = () => {
const isLoading = ref(false)
const error = ref<string | null>(null)
const nodeDefStore = useNodeDefStore()
const isLocalNodePack = (nodePackId: string) =>
!!nodeDefStore.nodeDefsByName[nodePackId]
const isLocalNode = (nodeName: string, nodePackId: string) => {
if (!nodeDefStore.nodeDefsByName[nodeName]) return false
return (
nodeDefStore.nodeDefsByName[nodeName].python_module.toLowerCase() ===
nodePackId.toLowerCase()
)
}
/**
* Check if the node definitions for the pack are available
*/
const packNodesAvailable = (node: components['schemas']['Node']) => {
if (node.id && isLocalNodePack(node.id)) return true
if (node.latest_version?.comfy_node_extract_status !== 'success')
return false
return true
}
const handleApiError = (
err: unknown,
context: string,
@@ -125,11 +101,11 @@ export const useComfyRegistryService = () => {
const getNodeDefs = async (
params: {
packId: components['schemas']['Node']['id']
versionId: components['schemas']['NodeVersion']['id']
},
version: components['schemas']['NodeVersion']['version']
} & operations['ListComfyNodes']['parameters']['query'],
signal?: AbortSignal
) => {
const { packId, versionId } = params
const { packId, version: versionId, ...queryParams } = params
if (!packId || !versionId) return null
const endpoint = `/nodes/${packId}/versions/${versionId}/comfy-nodes`
@@ -141,7 +117,10 @@ export const useComfyRegistryService = () => {
return executeApiRequest(
() =>
registryApiClient.get<components['schemas']['ComfyNode'][]>(endpoint, {
registryApiClient.get<
operations['ListComfyNodes']['responses'][200]['content']['application/json']
>(endpoint, {
params: queryParams,
signal
}),
errorContext,
@@ -149,33 +128,6 @@ export const useComfyRegistryService = () => {
)
}
/**
* Get a Comfy Node definition for a specific node in a specific version of a node pack
* @param packId - The ID of the node pack
* @param versionId - The version of the node pack
* @param comfyNodeName - The name of the comfy node (corresponds to `ComfyNodeDef#name`)
* @returns The node definition or null if not found or an error occurred
*/
const getNodeDef = async (
params: {
packId: components['schemas']['Node']['id']
versionId: components['schemas']['NodeVersion']['id']
comfyNodeName: components['schemas']['ComfyNode']['comfy_node_name']
},
signal?: AbortSignal
) => {
const { packId, versionId, comfyNodeName } = params
if (!comfyNodeName || !packId || !versionId) return null
if (isLocalNode(comfyNodeName, packId))
return nodeDefStore.nodeDefsByName[comfyNodeName]
const nodeDefs = await getNodeDefs({ packId, versionId }, signal)
return (
nodeDefs?.find((nodeDef) => nodeDef.comfy_node_name === comfyNodeName) ||
null
)
}
/**
* Get a paginated list of packs matching specific criteria.
* Search packs using `search` param. Search individual nodes using `comfy_node_search` param.
@@ -377,9 +329,7 @@ export const useComfyRegistryService = () => {
getPackByVersion,
getPublisherById,
listPacksForPublisher,
getNodeDef,
getNodeDefs,
postPackReview,
packNodesAvailable
postPackReview
}
}

View File

@@ -13,7 +13,10 @@ type NodePack = components['schemas']['Node']
type ListPacksParams = operations['listAllNodes']['parameters']['query']
type ListPacksResult =
operations['listAllNodes']['responses'][200]['content']['application/json']
type ComfyNode = components['schemas']['ComfyNode']
type GetNodeDefsParams = operations['ListComfyNodes']['parameters']['query'] & {
packId: components['schemas']['Node']['id']
version: components['schemas']['NodeVersion']['version']
}
type GetPackByIdPath = operations['getNode']['parameters']['path']['nodeId']
const isNodePack = (pack: NodePack | undefined): pack is NodePack => {
@@ -89,8 +92,8 @@ export const useComfyRegistryStore = defineStore('comfyRegistry', () => {
* Get the node definitions for a pack
*/
const getNodeDefs = useCachedRequest<
{ packId: string; versionId: string },
ComfyNode[]
GetNodeDefsParams,
operations['ListComfyNodes']['responses'][200]['content']['application/json']
>(registryService.getNodeDefs, { maxSize: PACK_BY_ID_CACHE_SIZE })
/**

77
src/utils/mapperUtil.ts Normal file
View File

@@ -0,0 +1,77 @@
import { transformInputSpecV1ToV2 } from '@/schemas/nodeDef/migration'
import {
ComfyNodeDef as ComfyNodeDefV2,
InputSpec
} from '@/schemas/nodeDef/nodeDefSchemaV2'
import { ComfyNodeDef as ComfyNodeDefV1 } from '@/schemas/nodeDefSchema'
import { components } from '@/types/comfyRegistryTypes'
const registryToFrontendV2NodeOutputs = (
registryDef: components['schemas']['ComfyNode']
): ComfyNodeDefV2['outputs'] => {
const returnTypes = JSON.parse(registryDef.return_types ?? '{}')
if (!returnTypes.length) return []
const returnNames = JSON.parse(registryDef.return_names ?? '{}')
const outputsIsList = registryDef.output_is_list ?? []
const outputs = []
for (let i = 0; i < returnTypes.length; i++) {
outputs.push({
type: returnTypes[i],
name: returnNames[i] || returnTypes[i],
is_list: outputsIsList[i] ?? false,
index: i
})
}
return outputs
}
const registryToFrontendV2NodeInputs = (
registryDef: components['schemas']['ComfyNode']
): ComfyNodeDefV2['inputs'] => {
const inputTypes = JSON.parse(
registryDef.input_types ?? '{}'
) as ComfyNodeDefV1['input']
if (!inputTypes || !Object.keys(inputTypes).length) return {}
const inputsV2: Record<string, InputSpec> = {}
if (inputTypes.required) {
Object.entries(inputTypes.required).forEach(([name, inputSpecV1]) => {
inputsV2[name] = transformInputSpecV1ToV2(inputSpecV1, {
name,
isOptional: false
})
})
}
if (inputTypes.optional) {
Object.entries(inputTypes.optional).forEach(([name, inputSpecV1]) => {
inputsV2[name] = transformInputSpecV1ToV2(inputSpecV1, {
name,
isOptional: true
})
})
}
return inputsV2
}
export const registryToFrontendV2NodeDef = (
nodeDef: components['schemas']['ComfyNode'],
nodePack: components['schemas']['Node']
): ComfyNodeDefV2 => {
const name = nodeDef.comfy_node_name ?? 'Node Name'
return {
category: nodeDef.category ?? 'unknown',
description: nodeDef.description ?? '',
display_name: name,
name,
inputs: registryToFrontendV2NodeInputs(nodeDef),
outputs: registryToFrontendV2NodeOutputs(nodeDef),
output_node: false,
python_module: nodePack.name ?? nodePack.id ?? 'unknown'
}
}