From 25dd6c55e0021a24663c8fc0881ae89b4ea2d71a Mon Sep 17 00:00:00 2001 From: Arslan Ablikim Date: Wed, 24 Jan 2024 16:28:07 +0800 Subject: [PATCH 01/10] feat: missing modal drawer --- .../install-models/InatallModelsModal.tsx | 18 +-- .../install-models/InstallModelSearchBar.tsx | 4 +- .../install-models/InstallModelsButton.tsx | 2 +- .../MissingModelsListDrawer.tsx | 111 ++++++++++++++++++ .../topbar/InstallMissingModelsButton.tsx | 47 ++++---- 5 files changed, 149 insertions(+), 33 deletions(-) create mode 100644 ui/src/model-manager/missing-models-list-drawer/MissingModelsListDrawer.tsx diff --git a/ui/src/model-manager/install-models/InatallModelsModal.tsx b/ui/src/model-manager/install-models/InatallModelsModal.tsx index cbbaa44..3540612 100644 --- a/ui/src/model-manager/install-models/InatallModelsModal.tsx +++ b/ui/src/model-manager/install-models/InatallModelsModal.tsx @@ -59,19 +59,19 @@ const MODEL_TYPE_TO_FOLDER_MAPPING: Record = { Upscaler: "upscale_models", VAE: "vae", }; -export default function InatallModelsModal({ - onclose, -}: { + +interface Props { onclose: () => void; -}) { + searchQuery?: string; + modelType?: MODEL_TYPE; +} +export default function InatallModelsModal({ onclose, searchQuery: searchQueryProp = '', modelType: modelTypeProp }: Props) { const [models, setModels] = useState([]); const [loading, setLoading] = useState(false); - const [modelType, setModelType] = useState( - "Checkpoint", - ); + const [modelType, setModelType] = useState(modelTypeProp); const toast = useToast(); const [installing, setInstalling] = useState([]); - const [searchQuery, setSearchQuery] = useState(""); + const [searchQuery, setSearchQuery] = useState(searchQueryProp); const { isOpen, onOpen, onClose } = useDisclosure(); const file = useRef(); const loadData = useCallback(async () => { @@ -186,7 +186,7 @@ export default function InatallModelsModal({ Models - + diff --git a/ui/src/model-manager/install-models/InstallModelSearchBar.tsx b/ui/src/model-manager/install-models/InstallModelSearchBar.tsx index bedd898..7fed171 100644 --- a/ui/src/model-manager/install-models/InstallModelSearchBar.tsx +++ b/ui/src/model-manager/install-models/InstallModelSearchBar.tsx @@ -2,11 +2,13 @@ import { useEffect, useState } from "react"; import { Button, Flex, Input, Text } from "@chakra-ui/react"; export default function InstallModelSearchBar({ + searchQuery, setSearchQuery, }: { + searchQuery: string; setSearchQuery: (query: string) => void; }) { - const [text, setText] = useState(""); + const [text, setText] = useState(searchQuery); // const debounceSearchValue = useDebounce(text, 1000); // useEffect(() => { // if (debounceSearchValue !== "") { diff --git a/ui/src/model-manager/install-models/InstallModelsButton.tsx b/ui/src/model-manager/install-models/InstallModelsButton.tsx index cd24942..daf6ca4 100644 --- a/ui/src/model-manager/install-models/InstallModelsButton.tsx +++ b/ui/src/model-manager/install-models/InstallModelsButton.tsx @@ -17,7 +17,7 @@ export default function InstallModelsButton() { {showInstallModels && ( - setShowInstallModels(false)} /> + setShowInstallModels(false)} /> )} ); diff --git a/ui/src/model-manager/missing-models-list-drawer/MissingModelsListDrawer.tsx b/ui/src/model-manager/missing-models-list-drawer/MissingModelsListDrawer.tsx new file mode 100644 index 0000000..a2667f0 --- /dev/null +++ b/ui/src/model-manager/missing-models-list-drawer/MissingModelsListDrawer.tsx @@ -0,0 +1,111 @@ +import { + Box, + Button, + Card, + CardHeader, + Flex, + Grid, + GridItem, + Heading, + Portal, + Text, + VStack, +} from "@chakra-ui/react"; +import { useEffect, useState } from "react"; +// @ts-ignore +import { app } from "/scripts/app.js"; +import InstallModelsButton from "../install-models/InstallModelsButton"; +import InatallModelsModal from "../install-models/InatallModelsModal"; + +export interface MissingModel { + class_type: string; + input_name: string; + received_value: string; +} + +interface Props { + onClose: () => void; + missingModels: MissingModel[]; +} + +export default function MissingModelsListDrawer({ onClose, missingModels }: Props) { + const [showInstallModels, setShowInstallModels] = useState(false); + const [searchQuery, setSearchQuery] = useState(""); + + useEffect(() => { + app.canvasEl.addEventListener("click", onClose); + return () => { + app.canvasEl.removeEventListener("click", onClose); + }; + }, []); + + const DRAWER_WIDTH = 440; + + return ( + <> + + + + + + + Models + + + + + + {missingModels.map((model) => { + return ( + + + Class Type: + {model.class_type} + Input Name: + {model.input_name} + Received Value: + {model.received_value} + + + + ); + })} + + + + + {showInstallModels && ( + setShowInstallModels(false)} /> + )} + + ); +} + +function formatSearchQuery(query: string): string { + // Remove file extension + let formattedQuery = query.replace(/\.[^/.]+$/, "") + // Remove everything after underscore + .split('_')[0] + // Replace special characters with space + .replace(/[^a-zA-Z0-9]/g, " ") + // Add space before capital letters + .replace(/([A-Z])/g, " $1").trim(); + + return formattedQuery; +} diff --git a/ui/src/model-manager/topbar/InstallMissingModelsButton.tsx b/ui/src/model-manager/topbar/InstallMissingModelsButton.tsx index 0c04b0c..47a1c04 100644 --- a/ui/src/model-manager/topbar/InstallMissingModelsButton.tsx +++ b/ui/src/model-manager/topbar/InstallMissingModelsButton.tsx @@ -1,10 +1,11 @@ -import { Box, Button, HStack } from "@chakra-ui/react"; +import { Button } from "@chakra-ui/react"; // @ts-ignore import { app } from "/scripts/app.js"; // @ts-ignore import { api } from "/scripts/api.js"; import { useEffect, useState } from "react"; -interface Props {} +import MissingModelsListDrawer, { MissingModel } from "../missing-models-list-drawer/MissingModelsListDrawer"; +interface Props { } interface NodeError { errors: Array<{ @@ -21,10 +22,11 @@ interface NodeError { class_type: string; } -export default function InstallMissingModelsButton({}: Props) { - const [missngCount, setMissingCount] = useState(0); +export default function InstallMissingModelsButton({ }: Props) { + const [showMyModels, setShowMyModels] = useState(false); + const [missingModels, setMissingModels] = useState([]); useEffect(() => { - const validateInput = async (input: string) => {}; + const validateInput = async (input: string) => { }; // monkey patch queue prompt api to catch errors const queuePrompt = app.queuePrompt as Function; app.queuePrompt = async function () { @@ -36,20 +38,16 @@ export default function InstallMissingModelsButton({}: Props) { string, NodeError >; - setMissingCount(Object.keys(nodeErrors).length); - Object.values(nodeErrors).forEach((nodeError) => { - nodeError.errors.forEach((error) => { - if (error.type === "value_not_in_list") { - const { input_name, received_value } = error.extra_info; - console.log( - "invalid_input", - nodeError.class_type, - input_name, - received_value - ); - } - }); - }); + setMissingModels(Object.values(nodeErrors).flatMap((nodeError) => + nodeError.errors.filter((error) => error.type === "value_not_in_list").map((error) => { + const { input_name, received_value } = error.extra_info; + return { + class_type: nodeError.class_type, + input_name, + received_value + }; + }) + )); } }; const graphJson = app.graph.serialize(); @@ -66,8 +64,13 @@ export default function InstallMissingModelsButton({}: Props) { }); }, []); return ( - + <> + + {showMyModels && ( + setShowMyModels(false)} missingModels={missingModels} /> + )} + ); } From 461789d80337a4bde2e3d3c0ca35e653219b3c66 Mon Sep 17 00:00:00 2001 From: Weixuan Fu Date: Wed, 24 Jan 2024 18:37:15 +0800 Subject: [PATCH 02/10] ui fix --- .../MissingModelsListDrawer.tsx | 47 ++++++++---- .../topbar/InstallMissingModelsButton.tsx | 74 +++++++++++-------- .../topbar/ModelManagerTopbar.tsx | 2 +- 3 files changed, 78 insertions(+), 45 deletions(-) rename ui/src/model-manager/{missing-models-list-drawer => missing-models-drawer}/MissingModelsListDrawer.tsx (64%) diff --git a/ui/src/model-manager/missing-models-list-drawer/MissingModelsListDrawer.tsx b/ui/src/model-manager/missing-models-drawer/MissingModelsListDrawer.tsx similarity index 64% rename from ui/src/model-manager/missing-models-list-drawer/MissingModelsListDrawer.tsx rename to ui/src/model-manager/missing-models-drawer/MissingModelsListDrawer.tsx index a2667f0..1efc074 100644 --- a/ui/src/model-manager/missing-models-list-drawer/MissingModelsListDrawer.tsx +++ b/ui/src/model-manager/missing-models-drawer/MissingModelsListDrawer.tsx @@ -16,6 +16,7 @@ import { useEffect, useState } from "react"; import { app } from "/scripts/app.js"; import InstallModelsButton from "../install-models/InstallModelsButton"; import InatallModelsModal from "../install-models/InatallModelsModal"; +import { IconExternalLink } from "@tabler/icons-react"; export interface MissingModel { class_type: string; @@ -28,7 +29,10 @@ interface Props { missingModels: MissingModel[]; } -export default function MissingModelsListDrawer({ onClose, missingModels }: Props) { +export default function MissingModelsListDrawer({ + onClose, + missingModels, +}: Props) { const [showInstallModels, setShowInstallModels] = useState(false); const [searchQuery, setSearchQuery] = useState(""); @@ -68,19 +72,29 @@ export default function MissingModelsListDrawer({ onClose, missingModels }: Prop {missingModels.map((model) => { return ( - + - Class Type: {model.class_type} - Input Name: + Input: {model.input_name} Received Value: - {model.received_value} + {model.received_value} - @@ -91,7 +105,10 @@ export default function MissingModelsListDrawer({ onClose, missingModels }: Prop {showInstallModels && ( - setShowInstallModels(false)} /> + setShowInstallModels(false)} + /> )} ); @@ -99,13 +116,13 @@ export default function MissingModelsListDrawer({ onClose, missingModels }: Prop function formatSearchQuery(query: string): string { // Remove file extension - let formattedQuery = query.replace(/\.[^/.]+$/, "") - // Remove everything after underscore - .split('_')[0] + let formattedQuery = query + .replace(/\.[^/.]+$/, "") // Replace special characters with space - .replace(/[^a-zA-Z0-9]/g, " ") + // .replace(/[^a-zA-Z0-9]/g, " ") // Add space before capital letters - .replace(/([A-Z])/g, " $1").trim(); + // .replace(/([A-Z])/g, " $1") + .trim(); return formattedQuery; } diff --git a/ui/src/model-manager/topbar/InstallMissingModelsButton.tsx b/ui/src/model-manager/topbar/InstallMissingModelsButton.tsx index 47a1c04..134ef01 100644 --- a/ui/src/model-manager/topbar/InstallMissingModelsButton.tsx +++ b/ui/src/model-manager/topbar/InstallMissingModelsButton.tsx @@ -4,8 +4,10 @@ import { app } from "/scripts/app.js"; // @ts-ignore import { api } from "/scripts/api.js"; import { useEffect, useState } from "react"; -import MissingModelsListDrawer, { MissingModel } from "../missing-models-list-drawer/MissingModelsListDrawer"; -interface Props { } +import MissingModelsListDrawer, { + MissingModel, +} from "../missing-models-drawer/MissingModelsListDrawer"; +interface Props {} interface NodeError { errors: Array<{ @@ -22,11 +24,10 @@ interface NodeError { class_type: string; } -export default function InstallMissingModelsButton({ }: Props) { +export default function InstallMissingModelsButton({}: Props) { const [showMyModels, setShowMyModels] = useState(false); const [missingModels, setMissingModels] = useState([]); useEffect(() => { - const validateInput = async (input: string) => { }; // monkey patch queue prompt api to catch errors const queuePrompt = app.queuePrompt as Function; app.queuePrompt = async function () { @@ -38,38 +39,53 @@ export default function InstallMissingModelsButton({ }: Props) { string, NodeError >; - setMissingModels(Object.values(nodeErrors).flatMap((nodeError) => - nodeError.errors.filter((error) => error.type === "value_not_in_list").map((error) => { - const { input_name, received_value } = error.extra_info; - return { - class_type: nodeError.class_type, - input_name, - received_value - }; - }) - )); + setMissingModels( + Object.values(nodeErrors).flatMap((nodeError) => + nodeError.errors + .filter((error) => error.type === "value_not_in_list") + .map((error) => { + const { input_name, received_value } = error.extra_info; + return { + class_type: nodeError.class_type, + input_name, + received_value, + }; + }), + ), + ); } }; - const graphJson = app.graph.serialize(); - console.log(graphJson); - fetch("/model_manager/find_missing_models", { - method: "POST", - body: JSON.stringify({ - workflow: graphJson, - }), - }) - .then((res) => res.json()) - .then((res) => { - console.log(res); - }); + // const graphJson = app.graph.serialize(); + // console.log(graphJson); + // fetch("/model_manager/find_missing_models", { + // method: "POST", + // body: JSON.stringify({ + // workflow: graphJson, + // }), + // }) + // .then((res) => res.json()) + // .then((res) => { + // console.log(res); + // }); }, []); + if (missingModels.length === 0) { + return null; + } return ( <> - {showMyModels && ( - setShowMyModels(false)} missingModels={missingModels} /> + setShowMyModels(false)} + missingModels={missingModels} + /> )} ); diff --git a/ui/src/model-manager/topbar/ModelManagerTopbar.tsx b/ui/src/model-manager/topbar/ModelManagerTopbar.tsx index f42ff08..0dc41d8 100644 --- a/ui/src/model-manager/topbar/ModelManagerTopbar.tsx +++ b/ui/src/model-manager/topbar/ModelManagerTopbar.tsx @@ -35,7 +35,7 @@ export default function ModelManagerTopbar({}: Props) { }, []); return ( - {/* */} + - - - - - - ); -} \ No newline at end of file + + + + + + + + ); +} diff --git a/ui/src/model-manager/install-models/InatallModelsModal.tsx b/ui/src/model-manager/install-models/InatallModelsModal.tsx index 3540612..ece9828 100644 --- a/ui/src/model-manager/install-models/InatallModelsModal.tsx +++ b/ui/src/model-manager/install-models/InatallModelsModal.tsx @@ -1,22 +1,18 @@ import { Button, HStack, - Text, Modal, ModalOverlay, ModalContent, ModalHeader, ModalBody, ModalCloseButton, - IconButton, Heading, - Checkbox, Spinner, useToast, useDisclosure, } from "@chakra-ui/react"; import { useCallback, useEffect, useRef, useState } from "react"; -import { IconX } from "@tabler/icons-react"; import { CivitiModel, CivitiModelFileVersion } from "../types"; import { installModelsApi } from "../api/modelsApi"; import ModelCard from "./ModelCard"; @@ -65,7 +61,11 @@ interface Props { searchQuery?: string; modelType?: MODEL_TYPE; } -export default function InatallModelsModal({ onclose, searchQuery: searchQueryProp = '', modelType: modelTypeProp }: Props) { +export default function InatallModelsModal({ + onclose, + searchQuery: searchQueryProp = "", + modelType: modelTypeProp, +}: Props) { const [models, setModels] = useState([]); const [loading, setLoading] = useState(false); const [modelType, setModelType] = useState(modelTypeProp); @@ -175,7 +175,7 @@ export default function InatallModelsModal({ onclose, searchQuery: searchQueryPr useEffect(() => { loadData(); - }, [searchQuery, modelType]); + }, [modelType]); return ( <> @@ -186,7 +186,11 @@ export default function InatallModelsModal({ onclose, searchQuery: searchQueryPr Models - + diff --git a/ui/src/model-manager/install-models/InstallModelSearchBar.tsx b/ui/src/model-manager/install-models/InstallModelSearchBar.tsx index 7fed171..c9d611c 100644 --- a/ui/src/model-manager/install-models/InstallModelSearchBar.tsx +++ b/ui/src/model-manager/install-models/InstallModelSearchBar.tsx @@ -1,36 +1,26 @@ -import { useEffect, useState } from "react"; -import { Button, Flex, Input, Text } from "@chakra-ui/react"; +import { Button, Flex, Input } from "@chakra-ui/react"; export default function InstallModelSearchBar({ searchQuery, setSearchQuery, + onSearch, }: { searchQuery: string; setSearchQuery: (query: string) => void; + onSearch: () => void; }) { - const [text, setText] = useState(searchQuery); - // const debounceSearchValue = useDebounce(text, 1000); - // useEffect(() => { - // if (debounceSearchValue !== "") { - // setSearchQuery(debounceSearchValue); - // } - // }, [debounceSearchValue]); return ( setText(e.target.value)} + value={searchQuery} + onChange={(e) => setSearchQuery(e.target.value)} onKeyUp={(e) => { - e.code === "Enter" && setSearchQuery(text); + e.code === "Enter" && onSearch(); }} /> - diff --git a/ui/src/model-manager/install-models/InstallProgress.tsx b/ui/src/model-manager/install-models/InstallProgress.tsx index d686b84..160d968 100644 --- a/ui/src/model-manager/install-models/InstallProgress.tsx +++ b/ui/src/model-manager/install-models/InstallProgress.tsx @@ -1,9 +1,16 @@ // @ts-ignore import { api } from "/scripts/api.js"; -import { HStack, Progress, Stack, Text, useColorMode, useToast } from "@chakra-ui/react"; +import { + HStack, + Progress, + Stack, + Text, + useColorMode, + useToast, +} from "@chakra-ui/react"; import { useState, useEffect } from "react"; -type Queue = { save_path: string, progress: number }; +type Queue = { save_path: string; progress: number }; export default function InstallProgress() { const { colorMode } = useColorMode(); @@ -16,8 +23,7 @@ export default function InstallProgress() { }); api.addEventListener("download_error", (e: { detail: string }) => { toast({ - title: - "Download Error", + title: "Download Error", description: e.detail, status: "error", duration: 4000, @@ -27,13 +33,32 @@ export default function InstallProgress() { }, []); return ( - + {queue.map(({ save_path, progress }) => ( - {save_path.replace(/^.*[\\/]/, '')} - + + {save_path.replace(/^.*[\\/]/, "")} + + + + {progress.toFixed(1)}% + ))} - ) -} \ No newline at end of file + ); +} diff --git a/ui/src/model-manager/missing-models-drawer/MissingModelsListDrawer.tsx b/ui/src/model-manager/missing-models-drawer/MissingModelsListDrawer.tsx index 1efc074..8c5ad0a 100644 --- a/ui/src/model-manager/missing-models-drawer/MissingModelsListDrawer.tsx +++ b/ui/src/model-manager/missing-models-drawer/MissingModelsListDrawer.tsx @@ -91,8 +91,6 @@ export default function MissingModelsListDrawer({ `https://civitai.com/search/models?sortBy=models_v5&query=${formatSearchQuery(model.received_value)}`, "_blank", ); - // setSearchQuery(formatSearchQuery(model.received_value)); - // setShowInstallModels(true); }} > Search in CivitAI From 72e560759e2a71fd3f938982d0394a6799bfd228 Mon Sep 17 00:00:00 2001 From: Weixuan Fu Date: Sat, 13 Jan 2024 23:50:43 +0800 Subject: [PATCH 05/10] offload py server file io to another thread to reduce impact on main thread perf --- __init__.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/__init__.py b/__init__.py index 1161cf3..cb677ef 100644 --- a/__init__.py +++ b/__init__.py @@ -1,3 +1,4 @@ +import asyncio import server from aiohttp import web import aiohttp @@ -43,6 +44,11 @@ async def save_db(request): json_data = data['json'] file_name = f'{db_dir_path}/{table}.json' + # Offload file writing to a separate thread + await asyncio.to_thread(write_json_string_to_db, file_name, json_data) + return web.Response(text=f"JSON saved to {file_name}") + +def write_json_string_to_db(file_name, json_data): if not os.path.exists(db_dir_path): os.makedirs(db_dir_path) @@ -50,9 +56,6 @@ async def save_db(request): with open(file_name, 'w') as file: file.write(json.dumps(json_data, indent=4)) - return web.Response(text=f"JSON saved to {file_name}") - - def read_table(table): if not table: return None @@ -172,13 +175,18 @@ async def update_file(request): json_str = data['json_str'] my_workflows_dir = get_my_workflows_dir() full_path = os.path.join(my_workflows_dir, file_path) + + # Offload the file update to a separate thread + await asyncio.to_thread(write_json_to_file, full_path, json_str) + return web.Response(text="File updated successfully") + +def write_json_to_file(full_path, json_str): # Create the directory if it doesn't exist os.makedirs(os.path.dirname(full_path), exist_ok=True) + # Perform the file writing with open(full_path, 'w', encoding='utf-8') as file: file.write(json_str) - return web.Response(text="File updated successfully") - @server.PromptServer.instance.routes.post("/workspace/delete_file") async def delete_file(request): From 9d5c04b0e43a3b3296e2f0b7984030807b5c4a69 Mon Sep 17 00:00:00 2001 From: Weixuan Fu Date: Tue, 16 Jan 2024 15:06:48 +0800 Subject: [PATCH 06/10] make async func inside endpoint --- __init__.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/__init__.py b/__init__.py index cb677ef..3d23020 100644 --- a/__init__.py +++ b/__init__.py @@ -45,17 +45,15 @@ async def save_db(request): file_name = f'{db_dir_path}/{table}.json' # Offload file writing to a separate thread + def write_json_string_to_db(file_name, json_data): + if not os.path.exists(db_dir_path): + os.makedirs(db_dir_path) + # Write the JSON data to the specified file + with open(file_name, 'w') as file: + file.write(json.dumps(json_data, indent=4)) await asyncio.to_thread(write_json_string_to_db, file_name, json_data) return web.Response(text=f"JSON saved to {file_name}") -def write_json_string_to_db(file_name, json_data): - if not os.path.exists(db_dir_path): - os.makedirs(db_dir_path) - - # Write the JSON data to the specified file - with open(file_name, 'w') as file: - file.write(json.dumps(json_data, indent=4)) - def read_table(table): if not table: return None @@ -273,7 +271,7 @@ def file_handle(name, file, existFlowIds, fileList): json_data = json.load(file) fileInfo = { 'json': json.dumps(json_data), - 'name': '.'.join(name.split('.')[:-1]) + 'name': name.split(".")[0], } if 'extra' in json_data and 'workspace_info' in json_data['extra'] and 'id' in json_data['extra']['workspace_info']: if json_data['extra']['workspace_info']['id'] not in existFlowIds: From 615549fc77c92ee0a1d49a603a82349ab6c18f8b Mon Sep 17 00:00:00 2001 From: Weixuan Fu Date: Tue, 16 Jan 2024 15:08:50 +0800 Subject: [PATCH 07/10] cleanup --- __init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/__init__.py b/__init__.py index 3d23020..5f1b421 100644 --- a/__init__.py +++ b/__init__.py @@ -271,7 +271,7 @@ def file_handle(name, file, existFlowIds, fileList): json_data = json.load(file) fileInfo = { 'json': json.dumps(json_data), - 'name': name.split(".")[0], + 'name': '.'.join(name.split('.')[:-1]) } if 'extra' in json_data and 'workspace_info' in json_data['extra'] and 'id' in json_data['extra']['workspace_info']: if json_data['extra']['workspace_info']['id'] not in existFlowIds: From ab9a711b3e4e123390876381ef67bd0f1b9e3fa7 Mon Sep 17 00:00:00 2001 From: Weixuan Fu Date: Thu, 25 Jan 2024 01:14:41 +0800 Subject: [PATCH 08/10] clean up async thread code, remove save_backup unused functions --- __init__.py | 47 +++++++---------------------------------------- ui/src/Api.ts | 21 --------------------- 2 files changed, 7 insertions(+), 61 deletions(-) diff --git a/__init__.py b/__init__.py index 5f1b421..790ec0f 100644 --- a/__init__.py +++ b/__init__.py @@ -70,44 +70,12 @@ def read_table(table): async def get_workspace(request): # Extract the table parameter from the query string table = request.query.get('table') - data = read_table(table) + data = await asyncio.to_thread(read_table, table) return web.json_response(data) BACKUP_DIR = os.path.join(workspace_path, "backup") MAX_BACKUP_FILES = 20 - -@server.PromptServer.instance.routes.post("/workspace/save_backup") -async def save_backup(request): - try: - data = await request.json() - file_path = data.get('file_path') - json_str = data.get('json_str') - - file_path = os.path.join(BACKUP_DIR, file_path) - if not file_path or not json_str: - return web.Response(text=json.dumps({"error": "file_path and json_str are required"}), status=400) - directory = os.path.dirname(file_path) - # Create the directory if it does not exist - os.makedirs(directory, exist_ok=True) - - with open(file_path, 'w') as file: - file.write(json_str) - - # Check the number of files in the directory after writing the new file - files = [f for f in os.listdir(directory) if os.path.isfile( - os.path.join(directory, f))] - if len(files) > MAX_BACKUP_FILES: - # Find the oldest file (smallest filename) - oldest_file = min(files, key=lambda x: x) - # Delete the oldest file - os.remove(os.path.join(directory, oldest_file)) - - return web.Response(text=json.dumps({"message": "File saved successfully"}), status=200) - except Exception as e: - return web.Response(text=json.dumps({"error": str(e)}), status=500) - - @server.PromptServer.instance.routes.post("/workspace/list_backup") async def list_backup(request): try: @@ -174,17 +142,16 @@ async def update_file(request): my_workflows_dir = get_my_workflows_dir() full_path = os.path.join(my_workflows_dir, file_path) + def write_json_to_file(full_path, json_str): + # Create the directory if it doesn't exist + os.makedirs(os.path.dirname(full_path), exist_ok=True) + with open(full_path, 'w', encoding='utf-8') as file: + file.write(json_str) + # Offload the file update to a separate thread await asyncio.to_thread(write_json_to_file, full_path, json_str) return web.Response(text="File updated successfully") -def write_json_to_file(full_path, json_str): - # Create the directory if it doesn't exist - os.makedirs(os.path.dirname(full_path), exist_ok=True) - - # Perform the file writing - with open(full_path, 'w', encoding='utf-8') as file: - file.write(json_str) @server.PromptServer.instance.routes.post("/workspace/delete_file") async def delete_file(request): diff --git a/ui/src/Api.ts b/ui/src/Api.ts index 274b8e5..6582a6f 100644 --- a/ui/src/Api.ts +++ b/ui/src/Api.ts @@ -17,8 +17,6 @@ export async function getDB(table: Table): Promise { } export async function saveDB(table: Table, jsonData: string) { - // const tableBackupFile = table + "/" + Date.now() + ".json"; - // saveBackup(tableBackupFile, jsonData); try { const response = await fetch("/workspace/save_db", { method: "POST", @@ -73,25 +71,6 @@ export async function deleteFile(file_path: string, deleteEmptyFolder = false) { } } -export async function saveBackup(file_path: string, jsonData: string) { - try { - const response = await fetch("/workspace/save_backup", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - file_path: file_path, - json_str: jsonData, - }), - }); - const result = await response.text(); - return result; - } catch (error) { - console.error("Error saving workspace backup:", error); - } -} - export async function listBackup(dir: string) { try { const response = await fetch("/workspace/list_backup", { From 42784a1fcade2811e1c37778836c6b4509301712 Mon Sep 17 00:00:00 2001 From: Weixuan Fu Date: Thu, 25 Jan 2024 02:02:19 +0800 Subject: [PATCH 09/10] offload delete files to async, handle delete current flow and set curFlowID to null so it will not stuck in loop of all alert to save or discard --- __init__.py | 40 +++++++++++-------- ui/src/App.tsx | 31 +++++++++----- .../RecentFilesDrawer/RecentFilesDrawer.tsx | 9 +++-- ui/src/WorkspaceContext.ts | 2 +- ui/src/components/DropdownTitle.tsx | 2 +- ui/src/db-tables/WorkflowsTable.ts | 8 +++- 6 files changed, 59 insertions(+), 33 deletions(-) diff --git a/__init__.py b/__init__.py index 790ec0f..2c310dc 100644 --- a/__init__.py +++ b/__init__.py @@ -139,17 +139,17 @@ async def update_file(request): data = await request.json() file_path = data['file_path'] json_str = data['json_str'] - my_workflows_dir = get_my_workflows_dir() - full_path = os.path.join(my_workflows_dir, file_path) - def write_json_to_file(full_path, json_str): + def write_json_to_file(json_str): + my_workflows_dir = get_my_workflows_dir() + full_path = os.path.join(my_workflows_dir, file_path) # Create the directory if it doesn't exist os.makedirs(os.path.dirname(full_path), exist_ok=True) with open(full_path, 'w', encoding='utf-8') as file: file.write(json_str) # Offload the file update to a separate thread - await asyncio.to_thread(write_json_to_file, full_path, json_str) + await asyncio.to_thread(write_json_to_file, json_str) return web.Response(text="File updated successfully") @@ -158,22 +158,30 @@ async def delete_file(request): data = await request.json() file_path = data['file_path'] delete_empty_folder = data['deleteEmptyFolder'] - my_workflows_dir = get_my_workflows_dir() - full_path = os.path.join(my_workflows_dir, file_path) - if os.path.exists(full_path): - os.remove(full_path) + def sync_delete_file(file_path, delete_empty_folder): + my_workflows_dir = get_my_workflows_dir() + full_path = os.path.join(my_workflows_dir, file_path) - # Check if the directory is empty after deleting the file - directory = os.path.dirname(full_path) - if delete_empty_folder and not os.listdir(directory): - # If the directory is empty, remove the directory - os.rmdir(directory) - return web.Response(text="File and empty directory deleted successfully") + if os.path.exists(full_path): + os.remove(full_path) + directory = os.path.dirname(full_path) + if delete_empty_folder and not os.listdir(directory): + # If the directory is empty, remove the directory + os.rmdir(directory) + return "File and empty directory deleted successfully" + else: + return "File deleted successfully" else: - return web.Response(text="File deleted successfully") + return "File not found" + + # Run the synchronous file operation in a separate thread + response_text = await asyncio.to_thread(sync_delete_file, file_path, delete_empty_folder) + + if response_text == "File not found": + return web.Response(text=response_text, status=404) else: - return web.Response(text="File not found", status=404) + return web.Response(text=response_text) @server.PromptServer.instance.routes.post("/workspace/rename_file") diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 1687a40..be6214d 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -91,11 +91,16 @@ export default function App() { } }; - const setCurFlowIDAndName = (id: string, name: string) => { + const setCurFlowIDAndName = (id: string | null, name: string) => { + // curID null is when you deleted current workflow curFlowID.current = id; setFlowID(id); setCurFlowName(name); workflowsTable?.updateCurWorkflowID(id); + if (id == null) { + document.title = "ComfyUI"; + return; + } if (getWorkflowIdInUrlHash()) { const newUrlHash = generateUrlHashWithFlowId(id); window.location.hash = newUrlHash; @@ -198,7 +203,13 @@ export default function App() { setRoute("root"); }; - const loadWorkflowID = (id: string) => { + const loadWorkflowID = (id: string | null) => { + // curID null is when you deleted current workflow + if (id === null) { + setCurFlowIDAndName(null, ""); + app.graph.clear(); + return; + } const autoSaveEnabled = userSettingsTable?.getSetting("autoSave") ?? true; if (autoSaveEnabled || !isDirty) { loadWorkflowIDImpl(id); @@ -373,6 +384,14 @@ export default function App() { fileInput?.addEventListener("change", fileInputListener); const handleBeforeUnload = async (e: BeforeUnloadEvent) => { + const autoSaveEnabled = userSettingsTable?.getSetting("autoSave") ?? true; + const isDirty = + !!workflowsTable?.curWorkflow && + checkIsDirtyImpl(workflowsTable?.curWorkflow); + if (!autoSaveEnabled && isDirty) { + e.preventDefault(); // For modern browsers + e.returnValue = "You have unsaved changes!"; // For older browsers + } showDialog( `Please save or discard your changes before leaving, or your changes will be lost.`, [ @@ -392,14 +411,6 @@ export default function App() { }, ], ); - const autoSaveEnabled = userSettingsTable?.getSetting("autoSave") ?? true; - const isDirty = - !!workflowsTable?.curWorkflow && - checkIsDirtyImpl(workflowsTable?.curWorkflow); - if (!autoSaveEnabled && isDirty) { - e.preventDefault(); // For modern browsers - e.returnValue = "You have unsaved changes"; // For older browsers - } }; window.addEventListener("beforeunload", handleBeforeUnload); diff --git a/ui/src/RecentFilesDrawer/RecentFilesDrawer.tsx b/ui/src/RecentFilesDrawer/RecentFilesDrawer.tsx index b80a4f6..b1ff900 100644 --- a/ui/src/RecentFilesDrawer/RecentFilesDrawer.tsx +++ b/ui/src/RecentFilesDrawer/RecentFilesDrawer.tsx @@ -13,7 +13,7 @@ import { Flex, Tooltip, } from "@chakra-ui/react"; -import { useEffect, useState, useRef, useCallback } from "react"; +import { useEffect, useState, useRef, useCallback, useContext } from "react"; import { workflowsTable, isFolder, @@ -25,7 +25,7 @@ import { IconPlus, IconFolder, } from "@tabler/icons-react"; -import { RecentFilesContext } from "../WorkspaceContext"; +import { RecentFilesContext, WorkspaceContext } from "../WorkspaceContext"; import RecentFilesDrawerMenu from "./RecentFilesDrawerMenu"; import { sortFileItem } from "../utils"; import WorkflowListItem from "./WorkflowListItem"; @@ -53,7 +53,7 @@ export default function RecentFilesDrawer({ onClose, onClickNewFlow }: Props) { >([]); const aloneFlowsAndFoldersRef = useRef>([]); const allFlowsRef = useRef>([]); - + const { loadWorkflowID } = useContext(WorkspaceContext); const [selectedTag, setSelectedTag] = useState(); const [multipleState, setMultipleState] = useState(false); const [selectedKeys, setSelectedKeys] = useState([]); @@ -119,6 +119,9 @@ export default function RecentFilesDrawer({ onClose, onClickNewFlow }: Props) { const onDelete = useCallback( async (id: string) => { await workflowsTable?.deleteFlow(id); + if (workflowsTable?.curWorkflow?.id === id) { + loadWorkflowID?.(null); + } await loadLatestWorkflows(); }, [selectedTag, debounceSearchValue], diff --git a/ui/src/WorkspaceContext.ts b/ui/src/WorkspaceContext.ts index 847a296..52f36c3 100644 --- a/ui/src/WorkspaceContext.ts +++ b/ui/src/WorkspaceContext.ts @@ -5,7 +5,7 @@ import { Folder, Workflow } from "./types/dbTypes"; export const WorkspaceContext = createContext<{ curFlowID: string | null; onDuplicateWorkflow?: (flowID: string, newFlowName?: string) => void; - loadWorkflowID: (id: string) => void; + loadWorkflowID: (id: string | null) => void; saveCurWorkflow: () => void; discardUnsavedChanges: () => void; isDirty: boolean; diff --git a/ui/src/components/DropdownTitle.tsx b/ui/src/components/DropdownTitle.tsx index 07e0c94..45362e4 100644 --- a/ui/src/components/DropdownTitle.tsx +++ b/ui/src/components/DropdownTitle.tsx @@ -142,7 +142,7 @@ export default function DropdownTitle({ onClick }: { onClick?: () => void }) { > Save - + } diff --git a/ui/src/db-tables/WorkflowsTable.ts b/ui/src/db-tables/WorkflowsTable.ts index a3be344..92bc19b 100644 --- a/ui/src/db-tables/WorkflowsTable.ts +++ b/ui/src/db-tables/WorkflowsTable.ts @@ -26,7 +26,11 @@ export class WorkflowsTable extends TableBase { const instance = new WorkflowsTable(); return instance; } - public updateCurWorkflowID(id: string) { + public updateCurWorkflowID(id: string | null) { + if (id == null) { + this._curWorkflow = null; + return; + } this.get(id).then((w) => { this._curWorkflow = w ?? null; }); @@ -88,7 +92,7 @@ export class WorkflowsTable extends TableBase { } //update indexdb await indexdb.workflows.update(id, newWorkflow); - //update curWorkflow ram + //update curWorkflow RAM if (this._curWorkflow && this._curWorkflow.id === id) { this._curWorkflow = newWorkflow; } From c160276fde0320b683f0cd147d9bd1a6d7d2ea24 Mon Sep 17 00:00:00 2001 From: Weixuan Fu Date: Thu, 25 Jan 2024 02:13:06 +0800 Subject: [PATCH 10/10] fix bug when manual save workflow and it delete file on disk too cuz mistakenly use !==null instead of !=null --- ui/src/db-tables/WorkflowsTable.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/db-tables/WorkflowsTable.ts b/ui/src/db-tables/WorkflowsTable.ts index 92bc19b..7d19e0b 100644 --- a/ui/src/db-tables/WorkflowsTable.ts +++ b/ui/src/db-tables/WorkflowsTable.ts @@ -98,7 +98,7 @@ export class WorkflowsTable extends TableBase { } await this.saveDiskDB(); // save to my_workflows/ - if (input.name !== null || input.parentFolderID !== null) { + if (input.name != null || input.parentFolderID != null) { // renamed file or moved file folder await deleteJsonFileMyWorkflows(before); await saveJsonFileMyWorkflows(after);