From d6fb2daeff92b421ee86838d36e7acbeae838f42 Mon Sep 17 00:00:00 2001 From: KarrixLee Date: Mon, 16 Jun 2025 18:47:18 +0800 Subject: [PATCH] Add workflow list management and search functionality - Introduced a new workflow-list.js file to manage workflows, including fetching, displaying, and searching workflows. - Enhanced the custom_routes.py file with a new endpoint for retrieving workflows, ensuring proper authorization and query parameter handling. - Updated index.js to initialize the workflows list and integrate search functionality within the UI. These changes improve the user experience by allowing efficient management and retrieval of workflows in the application. --- custom_routes.py | 42 +++++ web-plugin/index.js | 10 +- web-plugin/workflow-list.js | 305 ++++++++++++++++++++++++++++++++++++ 3 files changed, 355 insertions(+), 2 deletions(-) create mode 100644 web-plugin/workflow-list.js diff --git a/custom_routes.py b/custom_routes.py index 70d19e0..849fc92 100644 --- a/custom_routes.py +++ b/custom_routes.py @@ -31,6 +31,7 @@ import torch import psutil from collections import OrderedDict import io +from urllib.parse import urlencode # Global session client_session = None @@ -2993,3 +2994,44 @@ async def create_workflow_version_proxy(request): return web.json_response(json_data, status=response.status) except Exception as e: return web.json_response({"error": str(e)}, status=500) + + +@server.PromptServer.instance.routes.get("/comfyui-deploy/workflows") +async def get_workflows_proxy(request): + api_url = request.rel_url.query.get("api_url", "https://api.comfydeploy.com") + search = request.rel_url.query.get("search", "") + limit = request.rel_url.query.get("limit", 10) + offset = request.rel_url.query.get("offset", 0) + auth_header = request.headers.get("Authorization") + + if not auth_header: + return web.json_response( + {"error": "Authorization header is required"}, status=401 + ) + + # Build query parameters properly + params = {} + if search: + params["search"] = search + if limit: + params["limit"] = limit + if offset: + params["offset"] = offset + + target_url = f"{api_url}/api/workflows" + if params: + target_url += f"?{urlencode(params)}" + + try: + await ensure_client_session() + async with client_session.get( + target_url, + headers={ + "Content-Type": "application/json", + "Authorization": auth_header, + }, + ) as response: + json_data = await response.json() + return web.json_response(json_data, status=response.status) + except Exception as e: + return web.json_response({"error": str(e)}, status=500) diff --git a/web-plugin/index.js b/web-plugin/index.js index cb17bde..7351552 100644 --- a/web-plugin/index.js +++ b/web-plugin/index.js @@ -3,6 +3,7 @@ import { api } from "../../scripts/api.js"; // import { LGraphNode } from "../../scripts/widgets.js"; LGraphNode = LiteGraph.LGraphNode; import { ComfyDialog, $el } from "../../scripts/ui.js"; +import { initializeWorkflowsList, addWorkflowSearch } from "./workflow-list.js"; import { generateDependencyGraph } from "https://esm.sh/comfyui-json@0.1.25"; // import { ComfyDeploy } from "https://esm.sh/comfydeploy@2.0.0-beta.69"; @@ -1973,7 +1974,7 @@ if (isLocalhost) { title: "Deploy", tooltip: "Deploy and Configure", type: "custom", - render: (el) => { + render: async (el) => { el.innerHTML = `

Comfy Deploy

@@ -2038,7 +2039,12 @@ if (isLocalhost) { const workflowsList = el.querySelector("#workflows-list"); const workflowsLoading = el.querySelector("#workflows-loading"); - // refreshWorkflowsList(el); + // Initialize workflows list and search + const data = getData(); + if (data.apiKey) { + addWorkflowSearch(el, getData, getTimeAgo); + await initializeWorkflowsList(el, getData, getTimeAgo); + } }, }); } diff --git a/web-plugin/workflow-list.js b/web-plugin/workflow-list.js new file mode 100644 index 0000000..46ce5a2 --- /dev/null +++ b/web-plugin/workflow-list.js @@ -0,0 +1,305 @@ +// Workflow list management +let workflowsState = { + workflows: [], + offset: 0, + limit: 20, + loading: false, + hasMore: true, + initialized: false, + currentSearch: "", +}; + +async function fetchWorkflows(getData, offset = 0, limit = 20, search = "") { + try { + const data = getData(); + if (!data.apiKey) { + throw new Error("API key not configured"); + } + + const params = new URLSearchParams({ + offset: offset.toString(), + limit: limit.toString(), + api_url: data.apiUrl || "https://api.comfydeploy.com", + ...(search && { search }), + }); + + const response = await fetch(`/comfyui-deploy/workflows?${params}`, { + method: "GET", + headers: { + Authorization: `Bearer ${data.apiKey}`, + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + throw new Error(`Failed to fetch workflows: ${response.status}`); + } + + const result = await response.json(); + console.log("result", result); + return Array.isArray(result) ? result : []; + } catch (error) { + console.error("Error fetching workflows:", error); + return []; + } +} + +function createWorkflowItem(workflow, getTimeAgo) { + const li = document.createElement("li"); + li.style.cssText = ` + border-bottom: 1px solid #444; + background: transparent; + transition: all 0.2s ease; + cursor: pointer; + `; + + li.addEventListener("mouseenter", () => { + li.style.background = "#333"; + }); + + li.addEventListener("mouseleave", () => { + li.style.background = "transparent"; + }); + + const updatedDate = new Date(workflow.updated_at); + const timeAgo = getTimeAgo(updatedDate); + + li.innerHTML = ` +
+
+ ${ + workflow.cover_image + ? ` +
+ Workflow cover +
+ 📄 +
+
+ ` + : ` +
+ 📄 +
+ ` + } + +
+
+
+ ${workflow.name} +
+ ${ + workflow.pinned + ? '📌' + : "" + } +
+ +
+ + Updated ${timeAgo} + +
+
+
+
+ `; + + return li; +} + +async function loadMoreWorkflows(element, getData, getTimeAgo) { + if (workflowsState.loading || !workflowsState.hasMore) return; + + workflowsState.loading = true; + + const workflowsList = element.querySelector("#workflows-list"); + const workflowsLoading = element.querySelector("#workflows-loading"); + + // Show loading indicator + workflowsLoading.style.display = "flex"; + + try { + const newWorkflows = await fetchWorkflows( + getData, + workflowsState.offset, + workflowsState.limit, + workflowsState.currentSearch + ); + + if (newWorkflows.length === 0) { + workflowsState.hasMore = false; + } else { + workflowsState.workflows.push(...newWorkflows); + workflowsState.offset += newWorkflows.length; + + // Render new workflow items + newWorkflows.forEach((workflow) => { + const workflowItem = createWorkflowItem(workflow, getTimeAgo); + workflowsList.appendChild(workflowItem); + }); + } + } catch (error) { + console.error("Error loading more workflows:", error); + } finally { + workflowsState.loading = false; + workflowsLoading.style.display = "none"; + } +} + +function setupInfiniteScroll(container, element, getData, getTimeAgo) { + let isScrolling = false; + + container.addEventListener("scroll", () => { + if (isScrolling) return; + + const { scrollTop, scrollHeight, clientHeight } = container; + + // Load more when scrolled to bottom (with 100px threshold) + if (scrollTop + clientHeight >= scrollHeight - 100) { + isScrolling = true; + loadMoreWorkflows(element, getData, getTimeAgo).finally(() => { + isScrolling = false; + }); + } + }); +} + +async function initializeWorkflowsList(element, getData, getTimeAgo) { + const workflowsContainer = element.querySelector("#workflows-container"); + const workflowsList = element.querySelector("#workflows-list"); + const workflowsLoading = element.querySelector("#workflows-loading"); + + if (workflowsState.initialized) return; + + try { + // Reset state + workflowsState = { + workflows: [], + offset: 0, + limit: 20, + loading: false, + hasMore: true, + initialized: true, + currentSearch: "", + }; + + // Show container and loading + workflowsContainer.style.display = "block"; + workflowsLoading.style.display = "flex"; + + // Style the workflows list for full height scrolling + workflowsList.style.cssText = ` + list-style-type: none; + padding: 0; + margin: 0; + height: calc(100vh - 400px); + overflow-y: auto; + scrollbar-width: thin; + scrollbar-color: #666 transparent; + border-top: 1px solid #444; + `; + + // Add webkit scrollbar styles + const style = document.createElement("style"); + style.textContent = ` + #workflows-list::-webkit-scrollbar { + width: 6px; + } + #workflows-list::-webkit-scrollbar-track { + background: transparent; + } + #workflows-list::-webkit-scrollbar-thumb { + background: #666; + border-radius: 3px; + } + #workflows-list::-webkit-scrollbar-thumb:hover { + background: #777; + } + `; + document.head.appendChild(style); + + // Setup infinite scroll + setupInfiniteScroll(workflowsList, element, getData, getTimeAgo); + + // Load initial workflows + await loadMoreWorkflows(element, getData, getTimeAgo); + + // Show the list + workflowsList.style.display = "block"; + } catch (error) { + console.error("Error initializing workflows list:", error); + workflowsLoading.innerHTML = ` +
+
Failed to load workflows
+ +
+ `; + } +} + +// Search functionality +function addWorkflowSearch(element, getData, getTimeAgo) { + const workflowsContainer = element.querySelector("#workflows-container"); + const h4 = workflowsContainer.querySelector("h4"); + + const searchContainer = document.createElement("div"); + searchContainer.style.cssText = "margin-bottom: 12px;"; + + const searchInput = document.createElement("input"); + searchInput.type = "text"; + searchInput.placeholder = "Search workflows..."; + searchInput.style.cssText = ` + width: 100%; + padding: 8px 12px; + border: 1px solid #555; + border-radius: 6px; + font-size: 12px; + box-sizing: border-box; + background: #333; + color: #fff; + `; + + let searchTimeout; + searchInput.addEventListener("input", (e) => { + clearTimeout(searchTimeout); + searchTimeout = setTimeout(async () => { + const searchTerm = e.target.value.trim(); + + // Update the tracked search term + workflowsState.currentSearch = searchTerm; + + // Reset state for new search + workflowsState.workflows = []; + workflowsState.offset = 0; + workflowsState.hasMore = true; + + // Clear current list + const workflowsList = element.querySelector("#workflows-list"); + workflowsList.innerHTML = ""; + + // Load with search term + workflowsState.loading = false; + await loadMoreWorkflows(element, getData, getTimeAgo); + }, 300); + }); + + searchContainer.appendChild(searchInput); + h4.after(searchContainer); +} + +// Export the functions +export { + initializeWorkflowsList, + addWorkflowSearch, + workflowsState, + fetchWorkflows, + loadMoreWorkflows, +};