diff --git a/.githooks/pre-commit b/.githooks/pre-commit old mode 100644 new mode 100755 diff --git a/poetry.lock b/poetry.lock index 675118488..cdb1ffd1a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -7041,6 +7041,17 @@ files = [ {file = "types_pytz-2023.3.0.1-py3-none-any.whl", hash = "sha256:65152e872137926bb67a8fe6cc9cfd794365df86650c5d5fdc7b167b0f38892e"}, ] +[[package]] +name = "types-pywin32" +version = "306.0.0.4" +description = "Typing stubs for pywin32" +optional = false +python-versions = "*" +files = [ + {file = "types-pywin32-306.0.0.4.tar.gz", hash = "sha256:ae4bbec80d535053236d4bebedf55f58dee89cf5883d277f0fa89e857f3ff337"}, + {file = "types_pywin32-306.0.0.4-py3-none-any.whl", hash = "sha256:f76a343ed6933008af85e158063963f923e54f2f461e697b2929b4178c7b77a1"}, +] + [[package]] name = "types-pyyaml" version = "6.0.12.11" @@ -7773,4 +7784,4 @@ local = ["ctransformers", "llama-cpp-python", "sentence-transformers"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<3.11" -content-hash = "c877b4d713eef71815d858d30976ab21c42e5eadcc2df8159e940e03323681ee" +content-hash = "a3a506d483c2db7169a9790090095d1764aa5be223d135c6fc3fc2768dfef36c" diff --git a/pyproject.toml b/pyproject.toml index f852c2a7c..796145886 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -103,6 +103,7 @@ types-python-jose = "^3.3.4.8" types-passlib = "^1.7.7.13" pytest-mock = "^3.11.1" pytest-xdist = "^3.3.1" +types-pywin32 = "^306.0.0.4" [tool.poetry.extras] diff --git a/src/backend/langflow/__main__.py b/src/backend/langflow/__main__.py index b5ec034de..a08ae9fb0 100644 --- a/src/backend/langflow/__main__.py +++ b/src/backend/langflow/__main__.py @@ -356,7 +356,7 @@ def superuser( with session_getter(db_manager) as session: from langflow.services.auth.utils import create_super_user - if create_super_user(session, username, password): + if create_super_user(db=session, username=username, password=password): # Verify that the superuser was created from langflow.services.database.models.user.user import User diff --git a/src/backend/langflow/api/v1/base.py b/src/backend/langflow/api/v1/base.py index 39c8b0b9f..acffc1bc3 100644 --- a/src/backend/langflow/api/v1/base.py +++ b/src/backend/langflow/api/v1/base.py @@ -21,7 +21,7 @@ class FrontendNodeRequest(FrontendNode): class ValidatePromptRequest(BaseModel): name: str template: str - #optional for tweak call + # optional for tweak call frontend_node: Optional[FrontendNodeRequest] @@ -41,7 +41,7 @@ class CodeValidationResponse(BaseModel): class PromptValidationResponse(BaseModel): input_variables: list - #object return for tweak call + # object return for tweak call frontend_node: FrontendNodeRequest | object diff --git a/src/backend/langflow/api/v1/chat.py b/src/backend/langflow/api/v1/chat.py index e4fc71343..9d322b03c 100644 --- a/src/backend/langflow/api/v1/chat.py +++ b/src/backend/langflow/api/v1/chat.py @@ -61,14 +61,13 @@ async def chat( await websocket.close(code=status.WS_1011_INTERNAL_ERROR, reason=str(exc)) except Exception as exc: logger.error(f"Error in chat websocket: {exc}") - if isinstance(exc, HTTPException): - exc = exc.detail + messsage = exc.detail if isinstance(exc, HTTPException) else str(exc) if "Could not validate credentials" in str(exc): await websocket.close( code=status.WS_1008_POLICY_VIOLATION, reason="Unauthorized" ) else: - await websocket.close(code=status.WS_1011_INTERNAL_ERROR, reason=str(exc)) + await websocket.close(code=status.WS_1011_INTERNAL_ERROR, reason=messsage) @router.post("/build/init/{flow_id}", response_model=InitResponse, status_code=201) diff --git a/src/backend/langflow/api/v1/users.py b/src/backend/langflow/api/v1/users.py index 7365e7cc1..5094409cb 100644 --- a/src/backend/langflow/api/v1/users.py +++ b/src/backend/langflow/api/v1/users.py @@ -43,7 +43,9 @@ def add_user( db.refresh(new_user) except IntegrityError as e: db.rollback() - raise HTTPException(status_code=400, detail="This username is unavailable.") from e + raise HTTPException( + status_code=400, detail="This username is unavailable." + ) from e return new_user diff --git a/src/backend/langflow/main.py b/src/backend/langflow/main.py index 57f3e34dc..f567e65bb 100644 --- a/src/backend/langflow/main.py +++ b/src/backend/langflow/main.py @@ -10,7 +10,7 @@ from langflow.api import router from langflow.interface.utils import setup_llm_caching from langflow.services.database.utils import initialize_database -from langflow.services.manager import initialize_services +from langflow.services.manager import initialize_services, teardown_services from langflow.utils.logger import configure @@ -40,6 +40,7 @@ def create_app(): app.on_event("startup")(initialize_services) app.on_event("startup")(initialize_database) app.on_event("startup")(setup_llm_caching) + app.on_event("shutdown")(teardown_services) return app diff --git a/src/backend/langflow/services/auth/utils.py b/src/backend/langflow/services/auth/utils.py index 1431ee615..485968a38 100644 --- a/src/backend/langflow/services/auth/utils.py +++ b/src/backend/langflow/services/auth/utils.py @@ -37,7 +37,12 @@ async def api_key_security( result: Optional[Union[ApiKey, User]] = None if settings_manager.auth_settings.AUTO_LOGIN: # Get the first user - settings_manager.auth_settings.FIRST_SUPERUSER + if not settings_manager.auth_settings.FIRST_SUPERUSER: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Missing first superuser credentials", + ) + result = get_user_by_username( db, settings_manager.auth_settings.FIRST_SUPERUSER ) @@ -80,6 +85,9 @@ async def get_current_user( if isinstance(token, Coroutine): token = await token + if settings_manager.auth_settings.SECRET_KEY is None: + raise credentials_exception + try: payload = jwt.decode( token, @@ -150,22 +158,16 @@ def create_token(data: dict, expires_delta: timedelta): def create_super_user( + username: str, + password: str, db: Session = Depends(get_session), - username: Optional[str] = None, - password: Optional[str] = None, ) -> User: - settings_manager = get_settings_manager() - - super_user = get_user_by_username( - db, username or settings_manager.auth_settings.FIRST_SUPERUSER - ) + super_user = get_user_by_username(db, username) if not super_user: super_user = User( - username=username or settings_manager.auth_settings.FIRST_SUPERUSER, - password=get_password_hash( - password or settings_manager.auth_settings.FIRST_SUPERUSER_PASSWORD - ), + username=username, + password=get_password_hash(password), is_superuser=True, is_active=True, last_login_at=None, @@ -179,7 +181,15 @@ def create_super_user( def create_user_longterm_token(db: Session = Depends(get_session)) -> dict: - super_user = create_super_user(db) + settings_manager = get_settings_manager() + username = settings_manager.auth_settings.FIRST_SUPERUSER + password = settings_manager.auth_settings.FIRST_SUPERUSER_PASSWORD + if not username or not password: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Missing first superuser credentials", + ) + super_user = create_super_user(db=db, username=username, password=password) access_token_expires_longterm = timedelta(days=365) access_token = create_token( diff --git a/src/backend/langflow/services/base.py b/src/backend/langflow/services/base.py index 6bca6c4e2..aaa966047 100644 --- a/src/backend/langflow/services/base.py +++ b/src/backend/langflow/services/base.py @@ -1,2 +1,8 @@ -class Service: +from abc import ABC + + +class Service(ABC): name: str + + def teardown(self): + pass diff --git a/src/backend/langflow/services/database/manager.py b/src/backend/langflow/services/database/manager.py index 2b599a0ba..1159cbf7a 100644 --- a/src/backend/langflow/services/database/manager.py +++ b/src/backend/langflow/services/database/manager.py @@ -1,6 +1,7 @@ from pathlib import Path from typing import TYPE_CHECKING from langflow.services.base import Service +from langflow.services.database.models.user.crud import get_user_by_username from langflow.services.database.utils import Result, TableResults from langflow.services.utils import get_settings_manager from sqlalchemy import inspect @@ -159,3 +160,23 @@ class DatabaseManager(Service): ) logger.debug("Database and tables created successfully") + + def teardown(self): + logger.debug("Tearing down database") + try: + settings_manager = get_settings_manager() + # remove the default superuser if auto_login is enabled + # using the FIRST_SUPERUSER to get the user + if settings_manager.auth_settings.AUTO_LOGIN: + logger.debug("Removing default superuser") + username = settings_manager.auth_settings.FIRST_SUPERUSER + with Session(self.engine) as session: + user = get_user_by_username(session, username) + session.delete(user) + session.commit() + logger.debug("Default superuser removed") + + except Exception as exc: + logger.error(f"Error tearing down database: {exc}") + + self.engine.dispose() diff --git a/src/backend/langflow/services/manager.py b/src/backend/langflow/services/manager.py index e9895adab..ca9eb4e70 100644 --- a/src/backend/langflow/services/manager.py +++ b/src/backend/langflow/services/manager.py @@ -1,5 +1,6 @@ from langflow.services.schema import ServiceType from typing import TYPE_CHECKING, List, Optional +from langflow.utils.logger import logger if TYPE_CHECKING: from langflow.services.factory import ServiceFactory @@ -42,6 +43,7 @@ class ServiceManager: """ Create a new service given its name, handling dependencies. """ + logger.debug(f"Create service {service_name}") self._validate_service_creation(service_name) # Create dependencies first @@ -74,9 +76,21 @@ class ServiceManager: Update a service by its name. """ if service_name in self.services: + logger.debug(f"Update service {service_name}") self.services.pop(service_name, None) self.get(service_name) + def teardown(self): + """ + Teardown all the services. + """ + for service in self.services.values(): + logger.debug(f"Teardown service {service.name}") + service.teardown() + self.services = {} + self.factories = {} + self.dependencies = {} + service_manager = ServiceManager() @@ -121,7 +135,7 @@ def initialize_session_manager(): """ Initialize the session manager. """ - from langflow.services.session import factory as session_manager_factory + from langflow.services.session import factory as session_manager_factory # type: ignore from langflow.services.cache import factory as cache_factory initialize_settings_manager() @@ -134,3 +148,10 @@ def initialize_session_manager(): session_manager_factory.SessionManagerFactory(), dependencies=[ServiceType.CACHE_MANAGER], ) + + +def teardown_services(): + """ + Teardown all the services. + """ + service_manager.teardown() diff --git a/src/backend/langflow/services/settings/auth.py b/src/backend/langflow/services/settings/auth.py index 7550d3ddd..582aecb90 100644 --- a/src/backend/langflow/services/settings/auth.py +++ b/src/backend/langflow/services/settings/auth.py @@ -11,10 +11,11 @@ from langflow.utils.logger import logger class AuthSettings(BaseSettings): # Login settings CONFIG_DIR: str - SECRET_KEY: Optional[str] = Field( - None, + SECRET_KEY: str = Field( + default="", description="Secret key for JWT. If not provided, a random one will be generated.", env="LANGFLOW_SECRET_KEY", + allow_mutation=False, ) ALGORITHM: str = "HS256" ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 diff --git a/src/backend/langflow/services/settings/manager.py b/src/backend/langflow/services/settings/manager.py index 67e06108e..b0af8b7f1 100644 --- a/src/backend/langflow/services/settings/manager.py +++ b/src/backend/langflow/services/settings/manager.py @@ -35,5 +35,10 @@ class SettingsManager(Service): ) settings = Settings(**settings_dict) - auth_settings = AuthSettings(CONFIG_DIR=settings.CONFIG_DIR) + if not settings.CONFIG_DIR: + raise ValueError("CONFIG_DIR must be set in settings") + + auth_settings = AuthSettings( + CONFIG_DIR=settings.CONFIG_DIR, + ) return cls(settings, auth_settings) diff --git a/src/backend/langflow/services/settings/utils.py b/src/backend/langflow/services/settings/utils.py index 5eb4cd787..bb411a299 100644 --- a/src/backend/langflow/services/settings/utils.py +++ b/src/backend/langflow/services/settings/utils.py @@ -43,5 +43,5 @@ def write_secret_to_file(path: Path, value: str) -> None: def read_secret_from_file(path: Path) -> str: - with path.open("rb") as f: + with path.open("r") as f: return f.read() diff --git a/src/frontend/src/components/DropdownButtonComponent/index.tsx b/src/frontend/src/components/DropdownButtonComponent/index.tsx new file mode 100644 index 000000000..1ddddb3c1 --- /dev/null +++ b/src/frontend/src/components/DropdownButtonComponent/index.tsx @@ -0,0 +1,67 @@ +import { useState } from "react"; +import { dropdownButtonPropsType } from "../../types/components"; +import IconComponent from "../genericIconComponent"; +import { Button } from "../ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "../ui/dropdown-menu"; + +export default function DropdownButton({ + firstButtonName, + onFirstBtnClick, + options, +}: dropdownButtonPropsType): JSX.Element { + const [showOptions, setShowOptions] = useState(false); + + return ( +
+ + + + + { + event.stopPropagation(); + event.preventDefault(); + setShowOptions(!showOptions); + }} + > + {options.map(({ name, onBtnClick }, index) => ( + + {name} + + ))} + + +
+ ); +} diff --git a/src/frontend/src/components/codeTabsComponent/index.tsx b/src/frontend/src/components/codeTabsComponent/index.tsx index 2fa6563a6..d807326f0 100644 --- a/src/frontend/src/components/codeTabsComponent/index.tsx +++ b/src/frontend/src/components/codeTabsComponent/index.tsx @@ -28,7 +28,6 @@ import { TabsList, TabsTrigger, } from "../../components/ui/tabs"; -import { alertContext } from "../../contexts/alertContext"; import { darkContext } from "../../contexts/darkContext"; import { typesContext } from "../../contexts/typesContext"; import { codeTabsPropsType } from "../../types/components"; @@ -57,7 +56,7 @@ export default function CodeTabsComponent({ }, [flow]); useEffect(() => { - if(tweaks){ + if (tweaks) { unselectAllNodes({ data, updateNodes: (nodes) => { diff --git a/src/frontend/src/components/ui/skeleton.tsx b/src/frontend/src/components/ui/skeleton.tsx index 6556585a4..f7a1573f2 100644 --- a/src/frontend/src/components/ui/skeleton.tsx +++ b/src/frontend/src/components/ui/skeleton.tsx @@ -1,4 +1,4 @@ -import { cn } from "../../utils/utils" +import { cn } from "../../utils/utils"; function Skeleton({ className, @@ -9,7 +9,7 @@ function Skeleton({ className={cn("animate-pulse rounded-md bg-border", className)} {...props} /> - ) + ); } -export { Skeleton } +export { Skeleton }; diff --git a/src/frontend/src/contexts/alertContext.tsx b/src/frontend/src/contexts/alertContext.tsx index ba46dddac..0b183a297 100644 --- a/src/frontend/src/contexts/alertContext.tsx +++ b/src/frontend/src/contexts/alertContext.tsx @@ -25,7 +25,7 @@ const initialValue: alertContextType = { notificationList: [], pushNotificationList: () => {}, clearNotificationList: () => {}, - removeFromNotificationList: () => {} + removeFromNotificationList: () => {}, }; export const alertContext = createContext(initialValue); diff --git a/src/frontend/src/contexts/tabsContext.tsx b/src/frontend/src/contexts/tabsContext.tsx index 97ad50329..339ad67b5 100644 --- a/src/frontend/src/contexts/tabsContext.tsx +++ b/src/frontend/src/contexts/tabsContext.tsx @@ -48,7 +48,7 @@ const TabsContextInitialValue: TabsContextType = { downloadFlow: (flow: FlowType) => {}, downloadFlows: () => {}, uploadFlows: () => {}, - uploadFlow: () => {}, + uploadFlow: async () => "", isBuilt: false, setIsBuilt: (state: boolean) => {}, hardReset: () => {}, @@ -298,39 +298,38 @@ export function TabsProvider({ children }: { children: ReactNode }) { * If the file type is application/json, the file is read and parsed into a JSON object. * The resulting JSON object is passed to the addFlow function. */ - function uploadFlow(newProject?: boolean, file?: File) { + async function uploadFlow( + newProject?: boolean, + file?: File + ): Promise { + let id; if (file) { - file.text().then((text) => { - // parse the text into a JSON object - let flow: FlowType = JSON.parse(text); + let text = await file.text(); + // parse the text into a JSON object + let flow: FlowType = JSON.parse(text); - addFlow(flow, newProject); - }); + id = await addFlow(flow, newProject); } else { // create a file input const input = document.createElement("input"); input.type = "file"; input.accept = ".json"; // add a change event listener to the file input - input.onchange = (e: Event) => { - // check if the file type is application/json - if ( - (e.target as HTMLInputElement).files![0].type === "application/json" - ) { - // get the file from the file input - const currentfile = (e.target as HTMLInputElement).files![0]; - // read the file as text - currentfile.text().then((text) => { - // parse the text into a JSON object + id = await new Promise(resolve => { + input.onchange = async (e: Event) => { + if ((e.target as HTMLInputElement).files![0].type === "application/json") { + const currentfile = (e.target as HTMLInputElement).files![0]; + let text = await currentfile.text(); let flow: FlowType = JSON.parse(text); - - addFlow(flow, newProject); - }); - } - }; - // trigger the file input click event to open the file dialog - input.click(); + const flowId = await addFlow(flow, newProject); + resolve(flowId); + } + }; + // trigger the file input click event to open the file dialog + input.click(); + }); } + return id; } function uploadFlows() { diff --git a/src/frontend/src/modals/formModal/index.tsx b/src/frontend/src/modals/formModal/index.tsx index 0a9bf7483..2982b8738 100644 --- a/src/frontend/src/modals/formModal/index.tsx +++ b/src/frontend/src/modals/formModal/index.tsx @@ -8,7 +8,7 @@ import { classNames } from "../../utils/utils"; import ChatInput from "./chatInput"; import ChatMessage from "./chatMessage"; -import _, { set } from "lodash"; +import _ from "lodash"; import AccordionComponent from "../../components/AccordionComponent"; import IconComponent from "../../components/genericIconComponent"; import ToggleShadComponent from "../../components/toggleShadComponent"; @@ -25,9 +25,9 @@ import { Textarea } from "../../components/ui/textarea"; import { CHAT_FORM_DIALOG_SUBTITLE } from "../../constants/constants"; import { AuthContext } from "../../contexts/authContext"; import { TabsContext } from "../../contexts/tabsContext"; +import { getBuildStatus } from "../../controllers/API"; import { TabsState } from "../../types/tabs"; import { validateNodes } from "../../utils/reactflowUtils"; -import { getBuildStatus } from "../../controllers/API"; export default function FormModal({ flow, @@ -156,19 +156,21 @@ export default function FormModal({ function handleOnClose(event: CloseEvent): void { if (isOpen.current) { - getBuildStatus(flow.id).then((response) => { - if (response.data.built) { - connectWS(); - } - else { + getBuildStatus(flow.id) + .then((response) => { + if (response.data.built) { + connectWS(); + } else { + setErrorData({ + title: "Please build the flow again before using the chat.", + }); + } + }) + .catch((error) => { setErrorData({ - title: "Please build the flow again before using the chat." - }) - } - }).catch((error) => { - setErrorData({title:error.data?.detail?error.data.detail:error.message}) - - }); + title: error.data?.detail ? error.data.detail : error.message, + }); + }); setErrorData({ title: event.reason }); setTimeout(() => { setLockChat(false); @@ -186,8 +188,9 @@ export default function FormModal({ const host = isDevelopment ? "localhost:7860" : window.location.host; const chatEndpoint = `/api/v1/chat/${chatId}`; - return `${isDevelopment ? "ws" : webSocketProtocol - }://${host}${chatEndpoint}?token=${encodeURIComponent(accessToken!)}`; + return `${ + isDevelopment ? "ws" : webSocketProtocol + }://${host}${chatEndpoint}?token=${encodeURIComponent(accessToken!)}`; } function handleWsMessage(data: any) { @@ -209,20 +212,20 @@ export default function FormModal({ newChatHistory.push( chatItem.files ? { - isSend: !chatItem.is_bot, - message: chatItem.message, - template: chatItem.template, - thought: chatItem.intermediate_steps, - files: chatItem.files, - chatKey: chatItem.chatKey, - } + isSend: !chatItem.is_bot, + message: chatItem.message, + template: chatItem.template, + thought: chatItem.intermediate_steps, + files: chatItem.files, + chatKey: chatItem.chatKey, + } : { - isSend: !chatItem.is_bot, - message: chatItem.message, - template: chatItem.template, - thought: chatItem.intermediate_steps, - chatKey: chatItem.chatKey, - } + isSend: !chatItem.is_bot, + message: chatItem.message, + template: chatItem.template, + thought: chatItem.intermediate_steps, + chatKey: chatItem.chatKey, + } ); } } @@ -442,73 +445,73 @@ export default function FormModal({ {tabsState[id.current]?.formKeysData?.input_keys ? Object.keys( - tabsState[id.current].formKeysData.input_keys! - ).map((key, index) => ( -
- - - {key} - + tabsState[id.current].formKeysData.input_keys! + ).map((key, index) => ( +
+ + + {key} + -
{ - event.stopPropagation(); - }} - > - - handleOnCheckedChange(value, key) - } - size="small" - disabled={tabsState[ - id.current - ].formKeysData.handle_keys!.some( - (t) => t === key - )} - /> +
{ + event.stopPropagation(); + }} + > + + handleOnCheckedChange(value, key) + } + size="small" + disabled={tabsState[ + id.current + ].formKeysData.handle_keys!.some( + (t) => t === key + )} + /> +
-
- } - key={index} - keyValue={key} - > -
- {tabsState[id.current].formKeysData.handle_keys!.some( - (t) => t === key - ) && ( + } + key={index} + keyValue={key} + > +
+ {tabsState[id.current].formKeysData.handle_keys!.some( + (t) => t === key + ) && (
Source: Component
)} - -
- -
- )) + +
+ + + )) : null} {tabsState[id.current].formKeysData.memory_keys!.map( (key, index) => ( @@ -522,7 +525,7 @@ export default function FormModal({
{ }} + setEnabled={() => {}} size="small" disabled={true} /> diff --git a/src/frontend/src/modals/genericModal/index.tsx b/src/frontend/src/modals/genericModal/index.tsx index e0875eba3..b29c5acf8 100644 --- a/src/frontend/src/modals/genericModal/index.tsx +++ b/src/frontend/src/modals/genericModal/index.tsx @@ -136,7 +136,11 @@ export default function GenericModal({ setSuccessData({ title: "Prompt is ready", }); - if(JSON.stringify(apiReturn.data?.frontend_node)!==JSON.stringify({})) setNodeClass!(apiReturn.data?.frontend_node); + if ( + JSON.stringify(apiReturn.data?.frontend_node) !== + JSON.stringify({}) + ) + setNodeClass!(apiReturn.data?.frontend_node); setModalOpen(closeModal); setValue(inputValue); } diff --git a/src/frontend/src/pages/AdminPage/index.tsx b/src/frontend/src/pages/AdminPage/index.tsx index fc963fee8..08e83f700 100644 --- a/src/frontend/src/pages/AdminPage/index.tsx +++ b/src/frontend/src/pages/AdminPage/index.tsx @@ -4,6 +4,7 @@ import { useContext, useEffect, useRef, useState } from "react"; import PaginatorComponent from "../../components/PaginatorComponent"; import ShadTooltip from "../../components/ShadTooltipComponent"; import IconComponent from "../../components/genericIconComponent"; +import Header from "../../components/headerComponent"; import { Button } from "../../components/ui/button"; import { Checkbox } from "../../components/ui/checkbox"; import { Input } from "../../components/ui/input"; @@ -25,9 +26,8 @@ import { } from "../../controllers/API"; import ConfirmationModal from "../../modals/ConfirmationModal"; import UserManagementModal from "../../modals/UserManagementModal"; -import { UserInputType } from "../../types/components"; -import Header from "../../components/headerComponent"; import { Users } from "../../types/api"; +import { UserInputType } from "../../types/components"; export default function AdminPage() { const [inputValue, setInputValue] = useState(""); @@ -89,7 +89,7 @@ export default function AdminPage() { if (input === "") { setFilterUserList(userList.current); } else { - const filteredList = userList.current.filter((user:Users) => + const filteredList = userList.current.filter((user: Users) => user.username.toLowerCase().includes(input.toLowerCase()) ); setFilterUserList(filteredList); @@ -183,249 +183,254 @@ export default function AdminPage() { return ( <> -
-
- {userData && ( -
-
-
-
-
-
-

- Welcome back! -

-

- Navigate through this section to efficiently oversee all - application users. From here, you can seamlessly manage - user accounts. -

+
+
+ {userData && ( +
+
+
+
+
+
+

+ Welcome back! +

+

+ Navigate through this section to efficiently oversee all + application users. From here, you can seamlessly manage + user accounts. +

+
+
-
-
- {userList.current.length === 0 && !loadingUsers && ( + {userList.current.length === 0 && !loadingUsers && ( + <> +
+

There's no users registered :)

+
+ + )} <>
-

There's no users registered :)

-
- - )} - <> -
-
- handleFilterUsers(e.target.value)} - /> - {inputValue.length > 0 && ( - + )} +
+
+ { + handleNewUser(user); }} - variant="ghost" - className="h-8 px-2 lg:px-3" > - Reset - - - )} + + +
-
- { - handleNewUser(user); - }} - > - - -
-
- {loadingUsers && ( -
- Loading... -
- )} -
- - - - Id - Username - Active - Superuser - Created At - Updated At - - - - {!loadingUsers && ( - - {filterUserList.map((user:UserInputType, index) => ( - - - - - {user.id} - - - - - - - {user.username} - - - - - { - handleDisableUser( - user.is_active, - user.id, - user - ); - }} - > - - - - - { - handleSuperUserEdit( - user.is_superuser, - user.id, - user - ); - }} - > - - - - - { - new Date(user.create_at!) - .toISOString() - .split("T")[0] - } - - - { - new Date(user.updated_at!) - .toISOString() - .split("T")[0] - } - - -
- { - handleEditUser(user.id, editUser); - }} - > - - + {loadingUsers && ( +
+ Loading... +
+ )} +
+
+ + + Id + Username + Active + Superuser + Created At + Updated At + + + + {!loadingUsers && ( + + {filterUserList.map( + (user: UserInputType, index) => ( + + + + + {user.id} + - - - { - handleDeleteUser(user); - }} - > - - + + + + + {user.username} + - - - - - ))} - - )} -
-
+ + + { + handleDisableUser( + user.is_active, + user.id, + user + ); + }} + > + + + + + { + handleSuperUserEdit( + user.is_superuser, + user.id, + user + ); + }} + > + + + + + { + new Date(user.create_at!) + .toISOString() + .split("T")[0] + } + + + { + new Date(user.updated_at!) + .toISOString() + .split("T")[0] + } + + +
+ { + handleEditUser(user.id, editUser); + }} + > + + + + - { - handleChangePagination(pageSize, pageIndex); - }} - > - + { + handleDeleteUser(user); + }} + > + + + + +
+
+ + ) + )} + + )} + +
+ + { + handleChangePagination(pageSize, pageIndex); + }} + > + +
-
- )} + )}
); diff --git a/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx b/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx index 058be6e5c..620170fbb 100644 --- a/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx +++ b/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx @@ -421,7 +421,7 @@ export default function Page({ zoomOnScroll={!view} zoomOnPinch={!view} panOnDrag={!view} - proOptions={{hideAttribution: true}} + proOptions={{ hideAttribution: true }} > {!view && ( diff --git a/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx b/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx index c8dd34628..8ea74390a 100644 --- a/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx +++ b/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx @@ -21,8 +21,7 @@ export default function ExtraSidebar(): JSX.Element { const { data, templates } = useContext(typesContext); const { flows, tabId, uploadFlow, tabsState, saveFlow, isBuilt } = useContext(TabsContext); - const { setSuccessData, setErrorData } = - useContext(alertContext); + const { setSuccessData, setErrorData } = useContext(alertContext); const [dataFilter, setFilterData] = useState(data); const [search, setSearch] = useState(""); const isPending = tabsState[tabId]?.isPending; @@ -101,9 +100,7 @@ export default function ExtraSidebar(): JSX.Element {
{flow && flow.data && ( -
+
uploadFlow(true).then((id) => { + navigate("/flow/" + id); + })}] // Set a null id useEffect(() => { @@ -57,21 +61,19 @@ export default function HomePage(): JSX.Element { Upload Collection - + options={dropdownOptions} + />
- Manage your personal projects. Download or upload your collection. + Manage your personal projects. Download or upload your collection.
{isLoading && flows.length == 0 ? ( diff --git a/src/frontend/src/pages/loginPage/index.tsx b/src/frontend/src/pages/loginPage/index.tsx index fdd586b55..3c8879ed7 100644 --- a/src/frontend/src/pages/loginPage/index.tsx +++ b/src/frontend/src/pages/loginPage/index.tsx @@ -19,7 +19,8 @@ export default function LoginPage(): JSX.Element { useState(CONTROL_LOGIN_STATE); const { password, username } = inputState; - const { login, getAuthentication, setUserData, setIsAdmin } = useContext(AuthContext); + const { login, getAuthentication, setUserData, setIsAdmin } = + useContext(AuthContext); const navigate = useNavigate(); const { setErrorData } = useContext(alertContext); @@ -130,7 +131,9 @@ export default function LoginPage(): JSX.Element {
- +
diff --git a/src/frontend/src/types/components/index.ts b/src/frontend/src/types/components/index.ts index f7b974a93..b61ddf07d 100644 --- a/src/frontend/src/types/components/index.ts +++ b/src/frontend/src/types/components/index.ts @@ -268,7 +268,7 @@ export type UserInputType = { is_superuser?: boolean; id?: string; create_at?: string; - updated_at?:string; + updated_at?: string; }; export type ApiKeyType = { @@ -544,3 +544,9 @@ export type fetchErrorComponentType = { message: string; description: string; }; + +export type dropdownButtonPropsType = { + firstButtonName: string; + onFirstBtnClick: () => void; + options: Array<{ name: string; onBtnClick: () => void; }>; +}; diff --git a/src/frontend/src/types/tabs/index.ts b/src/frontend/src/types/tabs/index.ts index 036f82717..4c5b99dd7 100644 --- a/src/frontend/src/types/tabs/index.ts +++ b/src/frontend/src/types/tabs/index.ts @@ -24,7 +24,7 @@ export type TabsContextType = { uploadFlows: () => void; isBuilt: boolean; setIsBuilt: (state: boolean) => void; - uploadFlow: (newFlow?: boolean, file?: File) => void; + uploadFlow: (newFlow?: boolean, file?: File) => Promise; hardReset: () => void; getNodeId: (nodeType: string) => string; tabsState: TabsState; diff --git a/src/frontend/src/utils/styleUtils.ts b/src/frontend/src/utils/styleUtils.ts index 31a7adf7e..e74bb0e34 100644 --- a/src/frontend/src/utils/styleUtils.ts +++ b/src/frontend/src/utils/styleUtils.ts @@ -5,6 +5,7 @@ import { ChevronDown, ChevronLeft, ChevronRight, + ChevronUp, ChevronsLeft, ChevronsRight, ChevronsUpDown, @@ -300,4 +301,5 @@ export const nodeIconsLucide: iconsType = { UserCog2, Key, Unplug, + ChevronUp, }; diff --git a/tests/test_user.py b/tests/test_user.py index d734e4d61..bc617e127 100644 --- a/tests/test_user.py +++ b/tests/test_user.py @@ -9,7 +9,13 @@ from langflow.services.database.models.user import UserUpdate @pytest.fixture def super_user(client, session): - return create_super_user(session) + settings_manager = get_settings_manager() + auth_settings = settings_manager.auth_settings + return create_super_user( + db=session, + username=auth_settings.FIRST_SUPERUSER, + password=auth_settings.FIRST_SUPERUSER_PASSWORD, + ) @pytest.fixture