mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-01-31 13:29:55 +00:00
[Manager] Add 'Missing' and 'In Workflow' tabs (#3133)
Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
@@ -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(
|
||||
|
||||
34
src/composables/nodePack/useInstalledPacks.ts
Normal file
34
src/composables/nodePack/useInstalledPacks.ts
Normal 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
|
||||
}
|
||||
}
|
||||
78
src/composables/nodePack/useNodePacks.ts
Normal file
78
src/composables/nodePack/useNodePacks.ts
Normal 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
|
||||
}
|
||||
}
|
||||
87
src/composables/nodePack/useWorkflowPacks.ts
Normal file
87
src/composables/nodePack/useWorkflowPacks.ts
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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...",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -166,6 +166,7 @@
|
||||
"loadWorkflow": "ワークフローを読み込む",
|
||||
"loading": "読み込み中",
|
||||
"logs": "ログ",
|
||||
"missing": "不足している",
|
||||
"name": "名前",
|
||||
"newFolder": "新しいフォルダー",
|
||||
"next": "次へ",
|
||||
@@ -390,6 +391,7 @@
|
||||
"enabled": "有効",
|
||||
"nodePack": "ノードパック"
|
||||
},
|
||||
"inWorkflow": "ワークフロー内",
|
||||
"infoPanelEmpty": "アイテムをクリックして情報を表示します",
|
||||
"installSelected": "選択したものをインストール",
|
||||
"lastUpdated": "最終更新日",
|
||||
|
||||
@@ -166,6 +166,7 @@
|
||||
"loadWorkflow": "워크플로 로드",
|
||||
"loading": "로딩 중",
|
||||
"logs": "로그",
|
||||
"missing": "누락됨",
|
||||
"name": "이름",
|
||||
"newFolder": "새 폴더",
|
||||
"next": "다음",
|
||||
@@ -390,6 +391,7 @@
|
||||
"enabled": "활성화",
|
||||
"nodePack": "노드 팩"
|
||||
},
|
||||
"inWorkflow": "워크플로우 내",
|
||||
"infoPanelEmpty": "정보를 보려면 항목을 클릭하세요",
|
||||
"installSelected": "선택한 항목 설치",
|
||||
"lastUpdated": "마지막 업데이트",
|
||||
|
||||
@@ -166,6 +166,7 @@
|
||||
"loadWorkflow": "Загрузить рабочий процесс",
|
||||
"loading": "Загрузка",
|
||||
"logs": "Логи",
|
||||
"missing": "Отсутствует",
|
||||
"name": "Имя",
|
||||
"newFolder": "Новая папка",
|
||||
"next": "Далее",
|
||||
@@ -390,6 +391,7 @@
|
||||
"enabled": "Включено",
|
||||
"nodePack": "Пакет Узлов"
|
||||
},
|
||||
"inWorkflow": "В рабочем процессе",
|
||||
"infoPanelEmpty": "Нажмите на элемент, чтобы увидеть информацию",
|
||||
"installSelected": "Установить выбранное",
|
||||
"lastUpdated": "Последнее обновление",
|
||||
|
||||
@@ -166,6 +166,7 @@
|
||||
"loadWorkflow": "加载工作流",
|
||||
"loading": "加载中",
|
||||
"logs": "日志",
|
||||
"missing": "缺失",
|
||||
"name": "名称",
|
||||
"newFolder": "新文件夹",
|
||||
"next": "下一个",
|
||||
@@ -390,6 +391,7 @@
|
||||
"enabled": "已启用",
|
||||
"nodePack": "节点包"
|
||||
},
|
||||
"inWorkflow": "在工作流中",
|
||||
"infoPanelEmpty": "点击一个项目查看信息",
|
||||
"installSelected": "安装选定",
|
||||
"lastUpdated": "最后更新",
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user