[Manager] Add 'Missing' and 'In Workflow' tabs (#3133)

Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
Christian Byrne
2025-03-18 17:21:03 -07:00
committed by GitHub
parent 91a8591249
commit 8997ff4b2a
12 changed files with 324 additions and 57 deletions

View File

@@ -34,7 +34,7 @@
/>
<div class="flex-1 overflow-auto">
<div
v-if="(searchResults.length === 0 && isLoading) || isInitialLoad"
v-if="isLoading"
class="flex justify-center items-center h-full"
>
<ProgressSpinner />
@@ -95,9 +95,10 @@
</template>
<script setup lang="ts">
import { whenever } from '@vueuse/core'
import Button from 'primevue/button'
import ProgressSpinner from 'primevue/progressspinner'
import { computed, ref, watchEffect } from 'vue'
import { computed, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import ContentDivider from '@/components/common/ContentDivider.vue'
@@ -109,12 +110,20 @@ import InfoPanelMultiItem from '@/components/dialog/content/manager/infoPanel/In
import PackCard from '@/components/dialog/content/manager/packCard/PackCard.vue'
import RegistrySearchBar from '@/components/dialog/content/manager/registrySearchBar/RegistrySearchBar.vue'
import { useResponsiveCollapse } from '@/composables/element/useResponsiveCollapse'
import { useInstalledPacks } from '@/composables/useInstalledPacks'
import { useInstalledPacks } from '@/composables/nodePack/useInstalledPacks'
import { useWorkflowPacks } from '@/composables/nodePack/useWorkflowPacks'
import { useRegistrySearch } from '@/composables/useRegistrySearch'
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
import type { TabItem } from '@/types/comfyManagerTypes'
import { components } from '@/types/comfyRegistryTypes'
enum ManagerTab {
All = 'all',
Installed = 'installed',
Workflow = 'workflow',
Missing = 'missing'
}
const { t } = useI18n()
const comfyManagerStore = useComfyManagerStore()
@@ -125,15 +134,25 @@ const {
} = useResponsiveCollapse()
const tabs = ref<TabItem[]>([
{ id: 'all', label: t('g.all'), icon: 'pi-list' },
{ id: 'installed', label: t('g.installed'), icon: 'pi-box' }
{ id: ManagerTab.All, label: t('g.all'), icon: 'pi-list' },
{ id: ManagerTab.Installed, label: t('g.installed'), icon: 'pi-box' },
{
id: ManagerTab.Workflow,
label: t('manager.inWorkflow'),
icon: 'pi-folder'
},
{
id: ManagerTab.Missing,
label: t('g.missing'),
icon: 'pi-exclamation-circle'
}
])
const selectedTab = ref<TabItem>(tabs.value[0])
const {
searchQuery,
pageNumber,
isLoading,
isLoading: isSearchLoading,
searchResults,
searchMode,
suggestions
@@ -147,23 +166,94 @@ const isInitialLoad = computed(
() => searchResults.value.length === 0 && searchQuery.value === ''
)
const { getInstalledPacks } = useInstalledPacks()
const displayPacks = ref<components['schemas']['Node'][]>([])
const isEmptySearch = computed(() => searchQuery.value === '')
const displayPacks = ref<components['schemas']['Node'][]>([])
const getInstalledSearchResults = async () => {
if (isEmptySearch.value) return getInstalledPacks()
return searchResults.value.filter(
(pack) => pack.name && comfyManagerStore.installedPacksIds.has(pack.name)
)
const {
startFetchInstalled,
filterInstalledPack,
installedPacks,
isLoading: isLoadingInstalled
} = useInstalledPacks()
const {
startFetchWorkflowPacks,
filterWorkflowPack,
workflowPacks,
isLoading: isLoadingWorkflow
} = useWorkflowPacks()
const getInstalledResults = () => {
if (isEmptySearch.value) {
startFetchInstalled()
return installedPacks.value
} else {
return filterInstalledPack(searchResults.value)
}
}
watchEffect(async () => {
if (selectedTab.value.id === 'installed') {
displayPacks.value = await getInstalledSearchResults()
const getInWorkflowResults = () => {
if (isEmptySearch.value) {
startFetchWorkflowPacks()
return workflowPacks.value
} else {
displayPacks.value = searchResults.value
return filterWorkflowPack(searchResults.value)
}
}
const filterMissingPacks = (packs: components['schemas']['Node'][]) =>
packs.filter((pack) => !comfyManagerStore.isPackInstalled(pack.id))
const getMissingPacks = () => filterMissingPacks(getInWorkflowResults())
const onTabChange = () => {
switch (selectedTab.value?.id) {
case ManagerTab.Installed:
displayPacks.value = getInstalledResults()
break
case ManagerTab.Workflow:
displayPacks.value = getInWorkflowResults()
break
case ManagerTab.Missing:
displayPacks.value = getMissingPacks()
break
default:
displayPacks.value = searchResults.value
}
}
const onResultsChange = () => {
switch (selectedTab.value?.id) {
case ManagerTab.Installed:
displayPacks.value = filterInstalledPack(searchResults.value)
break
case ManagerTab.Workflow:
displayPacks.value = filterWorkflowPack(searchResults.value)
break
case ManagerTab.Missing:
displayPacks.value = filterMissingPacks(searchResults.value)
break
default:
displayPacks.value = searchResults.value
}
}
whenever(selectedTab, onTabChange)
watch(searchResults, onResultsChange, { flush: 'pre' })
const isLoading = computed(() => {
if (isSearchLoading.value)
return searchResults.value.length === 0 || isInitialLoad.value
if (selectedTab.value?.id === ManagerTab.Installed) {
return isLoadingInstalled.value
}
if (
selectedTab.value?.id === ManagerTab.Workflow ||
selectedTab.value?.id === ManagerTab.Missing
) {
return isLoadingWorkflow.value
}
return false
})
const resultsWithKeys = computed(

View File

@@ -0,0 +1,34 @@
import { computed, onUnmounted } from 'vue'
import { useNodePacks } from '@/composables/nodePack/useNodePacks'
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
import { UseNodePacksOptions } from '@/types/comfyManagerTypes'
import type { components } from '@/types/comfyRegistryTypes'
export const useInstalledPacks = (options: UseNodePacksOptions = {}) => {
const comfyManagerStore = useComfyManagerStore()
const installedPackIds = computed(() =>
Array.from(comfyManagerStore.installedPacksIds)
)
const { startFetch, cleanup, error, isLoading, nodePacks } = useNodePacks(
installedPackIds,
options
)
const filterInstalledPack = (packs: components['schemas']['Node'][]) =>
packs.filter((pack) => comfyManagerStore.isPackInstalled(pack.id))
onUnmounted(() => {
cleanup()
})
return {
error,
isLoading,
installedPacks: nodePacks,
startFetchInstalled: startFetch,
filterInstalledPack
}
}

View File

@@ -0,0 +1,78 @@
import { useAsyncState } from '@vueuse/core'
import { chunk } from 'lodash'
import { Ref, computed, isRef, ref } from 'vue'
import { useComfyRegistryStore } from '@/stores/comfyRegistryStore'
import { UseNodePacksOptions } from '@/types/comfyManagerTypes'
import { components } from '@/types/comfyRegistryTypes'
const DEFAULT_MAX_CONCURRENT = 6
type NodePack = components['schemas']['Node']
/**
* Handles fetching node packs from the registry given a list of node pack IDs
*/
export const useNodePacks = (
packsIds: string[] | Ref<string[]>,
options: UseNodePacksOptions = {}
) => {
const { immediate = false, maxConcurrent = DEFAULT_MAX_CONCURRENT } = options
const { getPackById, cancelRequests } = useComfyRegistryStore()
const nodePacks = ref<NodePack[]>([])
const processedIds = ref<Set<string>>(new Set())
const queuedPackIds = isRef(packsIds) ? packsIds : ref<string[]>(packsIds)
const remainingIds = computed(() =>
queuedPackIds.value?.filter((id) => !processedIds.value.has(id))
)
const chunks = computed(() =>
remainingIds.value?.length ? chunk(remainingIds.value, maxConcurrent) : []
)
const fetchPack = (ids: Parameters<typeof getPackById>[0]) =>
ids ? getPackById(ids) : null
const toRequestBatch = async (ids: string[]) =>
Promise.all(ids.map(fetchPack))
const isValidResponse = (response: NodePack | null) => response !== null
const fetchPacks = async () => {
for (const chunk of chunks.value) {
const resolvedChunk = await toRequestBatch(chunk)
chunk.forEach((id) => processedIds.value.add(id))
if (!resolvedChunk) continue
nodePacks.value.push(...resolvedChunk.filter(isValidResponse))
}
}
const { isReady, isLoading, error, execute } = useAsyncState(
fetchPacks,
null,
{
immediate
}
)
const clear = () => {
queuedPackIds.value = []
isReady.value = false
isLoading.value = false
}
const cleanup = () => {
cancelRequests()
clear()
}
return {
error,
isLoading,
isReady,
nodePacks,
startFetch: execute,
cleanup
}
}

View File

@@ -0,0 +1,87 @@
import { LGraphNode } from '@comfyorg/litegraph'
import { computed, onUnmounted } from 'vue'
import { useNodePacks } from '@/composables/nodePack/useNodePacks'
import { ComfyWorkflowJSON } from '@/schemas/comfyWorkflowSchema'
import { app } from '@/scripts/app'
import { UseNodePacksOptions } from '@/types/comfyManagerTypes'
import type { components } from '@/types/comfyRegistryTypes'
type WorkflowPack = {
id:
| ComfyWorkflowJSON['nodes'][number]['properties']['cnr_id']
| ComfyWorkflowJSON['nodes'][number]['properties']['aux_id']
version: ComfyWorkflowJSON['nodes'][number]['properties']['ver']
}
const CORE_NODES_PACK_NAME = 'comfy-core'
/**
* Handles parsing node pack metadata from nodes on the graph and fetching the
* associated node packs from the registry
*/
export const useWorkflowPacks = (options: UseNodePacksOptions = {}) => {
const getWorkflowNodeId = (node: LGraphNode): string | undefined => {
if (typeof node.properties?.cnr_id === 'string') {
return node.properties.cnr_id
}
if (typeof node.properties?.aux_id === 'string') {
return node.properties.aux_id
}
return undefined
}
const workflowNodeToPack = (node: LGraphNode): WorkflowPack | undefined => {
const id = getWorkflowNodeId(node)
if (!id) return undefined
if (id === CORE_NODES_PACK_NAME) return undefined
const version =
typeof node.properties.ver === 'string' ? node.properties.ver : undefined
return {
id,
version
}
}
const workflowPacks = computed<WorkflowPack[]>(() => {
if (!app.graph?.nodes?.length) return []
return app.graph.nodes
.map(workflowNodeToPack)
.filter((pack) => pack !== undefined)
})
const packsToUniqueIds = (packs: WorkflowPack[]) =>
packs.reduce((acc, pack) => {
if (pack?.id) acc.add(pack.id)
return acc
}, new Set<string>())
const workflowPacksIds = computed(() =>
Array.from(packsToUniqueIds(workflowPacks.value))
)
const { startFetch, cleanup, error, isLoading, nodePacks } = useNodePacks(
workflowPacksIds,
options
)
const isIdInWorkflow = (packId: string) =>
workflowPacksIds.value.includes(packId)
const filterWorkflowPack = (packs: components['schemas']['Node'][]) =>
packs.filter((pack) => !!pack.id && isIdInWorkflow(pack.id))
onUnmounted(() => {
cleanup()
})
return {
error,
isLoading,
workflowPacks: nodePacks,
startFetchWorkflowPacks: startFetch,
filterWorkflowPack
}
}

View File

@@ -1,39 +0,0 @@
import { chunk } from 'lodash'
import { onUnmounted } from 'vue'
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
import { useComfyRegistryStore } from '@/stores/comfyRegistryStore'
import { components } from '@/types/comfyRegistryTypes'
const MAX_SIMULTANEOUS_REQUESTS = 8
export const useInstalledPacks = () => {
const comfyManagerStore = useComfyManagerStore()
const { getPackById, cancelRequests } = useComfyRegistryStore()
const getInstalledIdsChunks = () =>
chunk(
Array.from(comfyManagerStore.installedPacksIds),
MAX_SIMULTANEOUS_REQUESTS
)
const getInstalledPacks = async () => {
const packs: components['schemas']['Node'][] = []
for (const packIdsChunk of getInstalledIdsChunks()) {
const requests = packIdsChunk.map((id) => getPackById(id))
const responses = await Promise.all(requests)
responses.forEach((pack) => {
if (pack) packs.push(pack)
})
}
return packs
}
onUnmounted(() => {
cancelRequests()
})
return {
getInstalledPacks
}
}

View File

@@ -95,11 +95,13 @@
"apply": "Apply",
"enabled": "Enabled",
"installed": "Installed",
"restart": "Restart"
"restart": "Restart",
"missing": "Missing"
},
"manager": {
"title": "Custom Nodes Manager",
"dependencies": "Dependencies",
"inWorkflow": "In Workflow",
"infoPanelEmpty": "Click an item to see the info",
"restartToApplyChanges": "To apply changes, please restart ComfyUI",
"loadingVersions": "Loading versions...",

View File

@@ -166,6 +166,7 @@
"loadWorkflow": "Charger le flux de travail",
"loading": "Chargement",
"logs": "Journaux",
"missing": "Manquant",
"name": "Nom",
"newFolder": "Nouveau dossier",
"next": "Suivant",
@@ -390,6 +391,7 @@
"enabled": "Activé",
"nodePack": "Pack de Nœuds"
},
"inWorkflow": "Dans le flux de travail",
"infoPanelEmpty": "Cliquez sur un élément pour voir les informations",
"installSelected": "Installer sélectionné",
"lastUpdated": "Dernière mise à jour",

View File

@@ -166,6 +166,7 @@
"loadWorkflow": "ワークフローを読み込む",
"loading": "読み込み中",
"logs": "ログ",
"missing": "不足している",
"name": "名前",
"newFolder": "新しいフォルダー",
"next": "次へ",
@@ -390,6 +391,7 @@
"enabled": "有効",
"nodePack": "ノードパック"
},
"inWorkflow": "ワークフロー内",
"infoPanelEmpty": "アイテムをクリックして情報を表示します",
"installSelected": "選択したものをインストール",
"lastUpdated": "最終更新日",

View File

@@ -166,6 +166,7 @@
"loadWorkflow": "워크플로 로드",
"loading": "로딩 중",
"logs": "로그",
"missing": "누락됨",
"name": "이름",
"newFolder": "새 폴더",
"next": "다음",
@@ -390,6 +391,7 @@
"enabled": "활성화",
"nodePack": "노드 팩"
},
"inWorkflow": "워크플로우 내",
"infoPanelEmpty": "정보를 보려면 항목을 클릭하세요",
"installSelected": "선택한 항목 설치",
"lastUpdated": "마지막 업데이트",

View File

@@ -166,6 +166,7 @@
"loadWorkflow": "Загрузить рабочий процесс",
"loading": "Загрузка",
"logs": "Логи",
"missing": "Отсутствует",
"name": "Имя",
"newFolder": "Новая папка",
"next": "Далее",
@@ -390,6 +391,7 @@
"enabled": "Включено",
"nodePack": "Пакет Узлов"
},
"inWorkflow": "В рабочем процессе",
"infoPanelEmpty": "Нажмите на элемент, чтобы увидеть информацию",
"installSelected": "Установить выбранное",
"lastUpdated": "Последнее обновление",

View File

@@ -166,6 +166,7 @@
"loadWorkflow": "加载工作流",
"loading": "加载中",
"logs": "日志",
"missing": "缺失",
"name": "名称",
"newFolder": "新文件夹",
"next": "下一个",
@@ -390,6 +391,7 @@
"enabled": "已启用",
"nodePack": "节点包"
},
"inWorkflow": "在工作流中",
"infoPanelEmpty": "点击一个项目查看信息",
"installSelected": "安装选定",
"lastUpdated": "最后更新",

View File

@@ -26,6 +26,11 @@ export type TaskLog = {
logs: string[]
}
export interface UseNodePacksOptions {
immediate?: boolean
maxConcurrent?: number
}
enum ManagerPackState {
/** Pack is installed and enabled */
INSTALLED = 'installed',