diff --git a/.github/workflows/lint-js.yml b/.github/workflows/lint-js.yml index 11156c8b8..b528ee8ed 100644 --- a/.github/workflows/lint-js.yml +++ b/.github/workflows/lint-js.yml @@ -5,7 +5,7 @@ on: paths: - "src/frontend/**" merge_group: - branches: [dev] + types: [checks_requested] env: NODE_VERSION: "21" @@ -45,10 +45,6 @@ jobs: - name: Run Prettier run: | cd src/frontend - npm run format - - name: Commit changes - uses: stefanzweifel/git-auto-commit-action@v5 - with: - commit_message: Apply Prettier formatting - branch: ${{ github.head_ref }} + npm run check-format + diff --git a/.github/workflows/python_test.yml b/.github/workflows/python_test.yml index 9e065c58f..8e37f40f7 100644 --- a/.github/workflows/python_test.yml +++ b/.github/workflows/python_test.yml @@ -45,7 +45,7 @@ jobs: poetry run python -m langflow run --host 127.0.0.1 --port 7860 --backend-only & SERVER_PID=$! # Wait for the server to start - timeout 120 bash -c 'until curl -f http://127.0.0.1:7860/auto_login; do sleep 2; done' || (echo "Server did not start in time" && kill $SERVER_PID && exit 1) + timeout 120 bash -c 'until curl -f http://127.0.0.1:7860/api/v1/auto_login; do sleep 5; done' || (echo "Server did not start in time" && kill $SERVER_PID && exit 1) # Terminate the server kill $SERVER_PID || (echo "Failed to terminate the server" && exit 1) sleep 10 # give the server some time to terminate diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9f84be063..f16dc35d3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -62,7 +62,7 @@ jobs: python -m langflow run --host 127.0.0.1 --port 7860 & SERVER_PID=$! # Wait for the server to start - timeout 120 bash -c 'until curl -f http://127.0.0.1:7860/auto_login; do sleep 2; done' || (echo "Server did not start in time" && kill $SERVER_PID && exit 1) + timeout 120 bash -c 'until curl -f http://127.0.0.1:7860/api/v1/auto_login; do sleep 2; done' || (echo "Server did not start in time" && kill $SERVER_PID && exit 1) # Terminate the server kill $SERVER_PID || (echo "Failed to terminate the server" && exit 1) sleep 10 # give the server some time to terminate @@ -124,7 +124,7 @@ jobs: python -m langflow run --host 127.0.0.1 --port 7860 & SERVER_PID=$! # Wait for the server to start - timeout 120 bash -c 'until curl -f http://127.0.0.1:7860/auto_login; do sleep 2; done' || (echo "Server did not start in time" && kill $SERVER_PID && exit 1) + timeout 120 bash -c 'until curl -f http://127.0.0.1:7860/api/v1/auto_login; do sleep 2; done' || (echo "Server did not start in time" && kill $SERVER_PID && exit 1) # Terminate the server kill $SERVER_PID || (echo "Failed to terminate the server" && exit 1) sleep 10 # give the server some time to terminate diff --git a/Makefile b/Makefile index 853fe6196..0b03489ab 100644 --- a/Makefile +++ b/Makefile @@ -47,20 +47,22 @@ init: coverage: ## run the tests and generate a coverage report - poetry run pytest --cov \ - --cov-config=.coveragerc \ - --cov-report xml \ - --cov-report term-missing:skip-covered \ - --cov-report lcov:coverage/lcov-pytest.info + @poetry run coverage run + @poetry run coverage erase # allow passing arguments to pytest unit_tests: - poetry run pytest --ignore=tests/integration --instafail -ra -n auto -m "not api_key_required" $(args) + poetry run pytest \ + --ignore=tests/integration \ + --instafail -ra -n auto -m "not api_key_required" \ + $(args) integration_tests: - poetry run pytest tests/integration --instafail -ra -n auto $(args) + poetry run pytest tests/integration \ + --instafail -ra -n auto \ + $(args) format: ## run code formatters poetry run ruff check . --fix @@ -129,9 +131,20 @@ start: @echo 'Running the CLI' ifeq ($(open_browser),false) - @make install_backend && poetry run langflow run --path $(path) --log-level $(log_level) --host $(host) --port $(port) --env-file $(env) --no-open-browser + @make install_backend && poetry run langflow run \ + --path $(path) \ + --log-level $(log_level) \ + --host $(host) \ + --port $(port) \ + --env-file $(env) \ + --no-open-browser else - @make install_backend && poetry run langflow run --path $(path) --log-level $(log_level) --host $(host) --port $(port) --env-file $(env) + @make install_backend && poetry run langflow run \ + --path $(path) \ + --log-level $(log_level) \ + --host $(host) \ + --port $(port) \ + --env-file $(env) endif @@ -166,13 +179,27 @@ backend: ## run the backend in development mode @echo 'Setting up the environment' @make setup_env make install_backend - @-kill -9 $(lsof -t -i:7860) + @-kill -9 $$(lsof -t -i:7860) ifdef login @echo "Running backend autologin is $(login)"; - LANGFLOW_AUTO_LOGIN=$(login) poetry run uvicorn --factory langflow.main:create_app --host 0.0.0.0 --port 7860 --reload --env-file .env --loop asyncio --workers $(workers) + LANGFLOW_AUTO_LOGIN=$(login) poetry run uvicorn \ + --factory langflow.main:create_app \ + --host 0.0.0.0 \ + --port $(port) \ + --reload \ + --env-file $(env) \ + --loop asyncio \ + --workers $(workers) else - @echo "Running backend respecting the .env file"; - poetry run uvicorn --factory langflow.main:create_app --host 0.0.0.0 --port 7860 --reload --env-file .env --loop asyncio --workers $(workers) + @echo "Running backend respecting the $(env) file"; + poetry run uvicorn \ + --factory langflow.main:create_app \ + --host 0.0.0.0 \ + --port $(port) \ + --reload \ + --env-file $(env) \ + --loop asyncio \ + --workers $(workers) endif diff --git a/docs/docs/deployment/kubernetes.mdx b/docs/docs/deployment/kubernetes.md similarity index 98% rename from docs/docs/deployment/kubernetes.mdx rename to docs/docs/deployment/kubernetes.md index 8896ab875..7e93731d0 100644 --- a/docs/docs/deployment/kubernetes.mdx +++ b/docs/docs/deployment/kubernetes.md @@ -1,10 +1,6 @@ -import Admonition from "@theme/Admonition"; # Kubernetes - -This page may contain outdated information. It will be updated as soon as possible. - This guide will help you get LangFlow up and running in Kubernetes cluster, including the following steps: diff --git a/pyproject.toml b/pyproject.toml index 1bd744a36..52d1ecd01 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -151,6 +151,28 @@ log_cli = true markers = ["async_test", "api_key_required"] +[tool.coverage.run] +command_line = """ + -m pytest + --cov --cov-report=term --cov-report=html + --instafail -ra -n auto -m "not api_key_required" + tests/unit +""" +source = ["src/backend/base/langflow/"] +omit = ["*/alembic/*", "tests/*", "*/__init__.py"] + + +[tool.coverage.report] +sort = "Stmts" +skip_empty = true +show_missing = false +ignore_errors = true + + +[tool.coverage.html] +directory = "coverage" + + [tool.ruff] exclude = ["src/backend/langflow/alembic/*"] line-length = 120 diff --git a/src/backend/base/langflow/alembic/versions/d066bfd22890_add_message_table.py b/src/backend/base/langflow/alembic/versions/d066bfd22890_add_message_table.py new file mode 100644 index 000000000..dd63398b7 --- /dev/null +++ b/src/backend/base/langflow/alembic/versions/d066bfd22890_add_message_table.py @@ -0,0 +1,52 @@ +"""Add message table + +Revision ID: 325180f0c4e1 +Revises: 631faacf5da2 +Create Date: 2024-06-23 21:29:28.220100 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa +import sqlmodel +from alembic import op + +from langflow.utils import migration + +# revision identifiers, used by Alembic. +revision: str = "325180f0c4e1" +down_revision: Union[str, None] = "631faacf5da2" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + conn = op.get_bind() + # ### commands auto generated by Alembic - please adjust! ### + if not migration.table_exists("message", conn): + op.create_table( + "message", + sa.Column("timestamp", sa.DateTime(), nullable=False), + sa.Column("sender", sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column("sender_name", sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column("session_id", sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column("text", sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column("id", sqlmodel.sql.sqltypes.GUID(), nullable=False), + sa.Column("flow_id", sqlmodel.sql.sqltypes.GUID(), nullable=True), + sa.Column("files", sa.JSON(), nullable=True), + sa.ForeignKeyConstraint( + ["flow_id"], + ["flow.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + conn = op.get_bind() + # ### commands auto generated by Alembic - please adjust! ### + if migration.table_exists("message", conn): + op.drop_table("message") + # ### end Alembic commands ### diff --git a/src/backend/base/langflow/api/v1/monitor.py b/src/backend/base/langflow/api/v1/monitor.py index a99c86bf8..f6c1fc4ac 100644 --- a/src/backend/base/langflow/api/v1/monitor.py +++ b/src/backend/base/langflow/api/v1/monitor.py @@ -1,15 +1,15 @@ from typing import List, Optional - +from uuid import UUID from fastapi import APIRouter, Depends, HTTPException, Query +from sqlalchemy import delete +from sqlmodel import Session, col, select -from langflow.services.deps import get_monitor_service -from langflow.services.monitor.schema import ( - MessageModelRequest, - MessageModelResponse, - TransactionModelResponse, - VertexBuildMapModel, -) +from langflow.services.auth.utils import get_current_active_user +from langflow.services.database.models.message.model import MessageRead, MessageTable, MessageUpdate +from langflow.services.database.models.user.model import User +from langflow.services.deps import get_monitor_service, get_session +from langflow.services.monitor.schema import MessageModelResponse, TransactionModelResponse, VertexBuildMapModel from langflow.services.monitor.service import MonitorService router = APIRouter(prefix="/monitor", tags=["Monitor"]) @@ -52,45 +52,58 @@ async def get_messages( sender: Optional[str] = Query(None), sender_name: Optional[str] = Query(None), order_by: Optional[str] = Query("timestamp"), - monitor_service: MonitorService = Depends(get_monitor_service), + session: Session = Depends(get_session), ): try: - df = monitor_service.get_messages( - flow_id=flow_id, - sender=sender, - sender_name=sender_name, - session_id=session_id, - order_by=order_by, - ) - dicts = df.to_dict(orient="records") - return [MessageModelResponse(**d) for d in dicts] + stmt = select(MessageTable) + if flow_id: + stmt = stmt.where(MessageTable.flow_id == flow_id) + if session_id: + stmt = stmt.where(MessageTable.session_id == session_id) + if sender: + stmt = stmt.where(MessageTable.sender == sender) + if sender_name: + stmt = stmt.where(MessageTable.sender_name == sender_name) + if order_by: + col = getattr(MessageTable, order_by).asc() + stmt = stmt.order_by(col) + messages = session.exec(stmt) + return [MessageModelResponse.model_validate(d, from_attributes=True) for d in messages] except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @router.delete("/messages", status_code=204) async def delete_messages( - message_ids: List[int], - monitor_service: MonitorService = Depends(get_monitor_service), + message_ids: List[UUID], + session: Session = Depends(get_session), + current_user: User = Depends(get_current_active_user), ): try: - monitor_service.delete_messages(message_ids=message_ids) + session.exec(select(MessageTable).where(MessageTable.id.in_(message_ids))) # type: ignore except Exception as e: raise HTTPException(status_code=500, detail=str(e)) -@router.post("/messages/{message_id}", response_model=MessageModelResponse) +@router.put("/messages/{message_id}", response_model=MessageRead) async def update_message( - message_id: int, - message: MessageModelRequest, - monitor_service: MonitorService = Depends(get_monitor_service), + message_id: UUID, + message: MessageUpdate, + session: Session = Depends(get_session), + user: User = Depends(get_current_active_user), ): try: - message_dict = message.model_dump(exclude_none=True) - message_dict.pop("index", None) - monitor_service.update_message(message_id=message_id, **message_dict) # type: ignore - return MessageModelResponse(index=message_id, **message_dict) - + db_message = session.get(MessageTable, message_id) + if not db_message: + raise HTTPException(status_code=404, detail="Message not found") + message_dict = message.model_dump(exclude_unset=True, exclude_none=True) + db_message.sqlmodel_update(message_dict) + session.add(db_message) + session.commit() + session.refresh(db_message) + return db_message + except HTTPException as e: + raise e except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @@ -98,10 +111,16 @@ async def update_message( @router.delete("/messages/session/{session_id}", status_code=204) async def delete_messages_session( session_id: str, - monitor_service: MonitorService = Depends(get_monitor_service), + session: Session = Depends(get_session), ): try: - monitor_service.delete_messages_session(session_id=session_id) + session.exec( # type: ignore + delete(MessageTable) + .where(col(MessageTable.session_id) == session_id) + .execution_options(synchronize_session="fetch") + ) + session.commit() + return {"message": "Messages deleted successfully"} except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @@ -137,4 +156,3 @@ async def get_transactions( return result except Exception as e: raise HTTPException(status_code=500, detail=str(e)) - raise HTTPException(status_code=500, detail=str(e)) diff --git a/src/backend/base/langflow/base/models/model.py b/src/backend/base/langflow/base/models/model.py index 81f124fd7..bea9c88d8 100644 --- a/src/backend/base/langflow/base/models/model.py +++ b/src/backend/base/langflow/base/models/model.py @@ -119,7 +119,11 @@ class LCModelComponent(Component): return status_message def get_chat_result( - self, runnable: LanguageModel, stream: bool, input_value: str | Message, system_message: Optional[str] = None + self, + runnable: LanguageModel, + stream: bool, + input_value: str | Message, + system_message: Optional[str] = None, ): messages: list[Union[BaseMessage]] = [] if not input_value and not system_message: diff --git a/src/backend/base/langflow/custom/custom_component/custom_component.py b/src/backend/base/langflow/custom/custom_component/custom_component.py index 5505ae132..24df42894 100644 --- a/src/backend/base/langflow/custom/custom_component/custom_component.py +++ b/src/backend/base/langflow/custom/custom_component/custom_component.py @@ -96,7 +96,7 @@ class CustomComponent(BaseComponent): def stop(self, output_name: str | None = None): if not output_name and self.vertex and len(self.vertex.outputs) == 1: output_name = self.vertex.outputs[0]["name"] - else: + elif not output_name: raise ValueError("You must specify an output name to call stop") if not self.vertex: raise ValueError("Vertex is not set") diff --git a/src/backend/base/langflow/graph/graph/base.py b/src/backend/base/langflow/graph/graph/base.py index 46f562c95..19511f271 100644 --- a/src/backend/base/langflow/graph/graph/base.py +++ b/src/backend/base/langflow/graph/graph/base.py @@ -1208,6 +1208,7 @@ class Graph: except ValueError: stop_or_start_vertex = self.get_root_of_group_node(vertex_id) stack = [stop_or_start_vertex.id] + vertex_id = stop_or_start_vertex.id stop_predecessors = [pre.id for pre in stop_or_start_vertex.predecessors] # DFS to collect all vertices that can reach the specified vertex while stack: diff --git a/src/backend/base/langflow/memory.py b/src/backend/base/langflow/memory.py index e89682969..d1793740d 100644 --- a/src/backend/base/langflow/memory.py +++ b/src/backend/base/langflow/memory.py @@ -1,11 +1,14 @@ import warnings from typing import List, Optional +from uuid import UUID from loguru import logger +from sqlalchemy import delete +from sqlmodel import Session, col, select from langflow.schema.message import Message -from langflow.services.deps import get_monitor_service -from langflow.services.monitor.schema import MessageModel +from langflow.services.database.models.message.model import MessageRead, MessageTable +from langflow.services.deps import session_scope def get_messages( @@ -14,6 +17,7 @@ def get_messages( session_id: Optional[str] = None, order_by: Optional[str] = "timestamp", order: Optional[str] = "DESC", + flow_id: Optional[UUID] = None, limit: Optional[int] = None, ): """ @@ -29,34 +33,29 @@ def get_messages( Returns: List[Data]: A list of Data objects representing the retrieved messages. """ - monitor_service = get_monitor_service() - messages_df = monitor_service.get_messages( - sender=sender, - sender_name=sender_name, - session_id=session_id, - order_by=order_by, - limit=limit, - order=order, - ) + messages_read: list[Message] = [] + with session_scope() as session: + stmt = select(MessageTable) + if sender: + stmt = stmt.where(MessageTable.sender == sender) + if sender_name: + stmt = stmt.where(MessageTable.sender_name == sender_name) + if session_id: + stmt = stmt.where(MessageTable.session_id == session_id) + if flow_id: + stmt = stmt.where(MessageTable.flow_id == flow_id) + if order_by: + if order == "DESC": + col = getattr(MessageTable, order_by).desc() + else: + col = getattr(MessageTable, order_by).asc() + stmt = stmt.order_by(col) + if limit: + stmt = stmt.limit(limit) + messages = session.exec(stmt) + messages_read = [Message(**d.model_dump()) for d in messages] - messages: list[Message] = [] - # messages_df has a timestamp - # it gets the last 5 messages, for example - # but now they are ordered from most recent to least recent - # so we need to reverse the order - messages_df = messages_df[::-1] if order == "DESC" else messages_df - for row in messages_df.itertuples(): - msg = Message( - text=row.text, - sender=row.sender, - session_id=row.session_id, - sender_name=row.sender_name, - timestamp=row.timestamp, - ) - - messages.append(msg) - - return messages + return messages_read def add_messages(messages: Message | list[Message], flow_id: Optional[str] = None): @@ -64,7 +63,6 @@ def add_messages(messages: Message | list[Message], flow_id: Optional[str] = Non Add a message to the monitor service. """ try: - monitor_service = get_monitor_service() if not isinstance(messages, list): messages = [messages] @@ -72,25 +70,29 @@ def add_messages(messages: Message | list[Message], flow_id: Optional[str] = Non types = ", ".join([str(type(message)) for message in messages]) raise ValueError(f"The messages must be instances of Message. Found: {types}") - messages_models: list[MessageModel] = [] + messages_models: list[MessageTable] = [] for msg in messages: - if not msg.timestamp: - msg.timestamp = monitor_service.get_timestamp() - messages_models.append(MessageModel.from_message(msg, flow_id=flow_id)) - - for message_model in messages_models: - try: - monitor_service.add_message(message_model) - except Exception as e: - logger.error(f"Error adding message to monitor service: {e}") - logger.exception(e) - raise e - return messages_models + messages_models.append(MessageTable.from_message(msg, flow_id=flow_id)) + with session_scope() as session: + messages_models = add_messagetables(messages_models, session) + return [Message(**message.model_dump()) for message in messages_models] except Exception as e: logger.exception(e) raise e +def add_messagetables(messages: list[MessageTable], session: Session): + for message in messages: + try: + session.add(message) + session.commit() + session.refresh(message) + except Exception as e: + logger.exception(e) + raise e + return [MessageRead.model_validate(message, from_attributes=True) for message in messages] + + def delete_messages(session_id: str): """ Delete messages from the monitor service based on the provided session ID. @@ -98,8 +100,13 @@ def delete_messages(session_id: str): Args: session_id (str): The session ID associated with the messages to delete. """ - monitor_service = get_monitor_service() - monitor_service.delete_messages_session(session_id) + with session_scope() as session: + session.exec( + delete(MessageTable) + .where(col(MessageTable.session_id) == session_id) + .execution_options(synchronize_session="fetch") + ) + session.commit() def store_message( diff --git a/src/backend/base/langflow/schema/message.py b/src/backend/base/langflow/schema/message.py index c50dab880..c03c0cb71 100644 --- a/src/backend/base/langflow/schema/message.py +++ b/src/backend/base/langflow/schema/message.py @@ -1,5 +1,6 @@ from datetime import datetime, timezone from typing import Annotated, Any, AsyncIterator, Iterator, List, Optional +from uuid import UUID from fastapi.encoders import jsonable_encoder from langchain_core.load import load @@ -31,7 +32,14 @@ class Message(Data): timestamp: Annotated[str, BeforeValidator(_timestamp_to_str)] = Field( default=datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S") ) - flow_id: Optional[str] = None + flow_id: Optional[str | UUID] = None + + @field_validator("flow_id", mode="before") + @classmethod + def validate_flow_id(cls, value): + if isinstance(value, UUID): + value = str(value) + return value @field_validator("files", mode="before") @classmethod diff --git a/src/backend/base/langflow/services/database/models/__init__.py b/src/backend/base/langflow/services/database/models/__init__.py index ce12a6fce..6e1f09fe3 100644 --- a/src/backend/base/langflow/services/database/models/__init__.py +++ b/src/backend/base/langflow/services/database/models/__init__.py @@ -1,7 +1,8 @@ from .api_key import ApiKey from .flow import Flow from .folder import Folder +from .message import MessageTable from .user import User from .variable import Variable -__all__ = ["Flow", "User", "ApiKey", "Variable", "Folder"] +__all__ = ["Flow", "User", "ApiKey", "Variable", "Folder", "MessageTable"] diff --git a/src/backend/base/langflow/services/database/models/flow/model.py b/src/backend/base/langflow/services/database/models/flow/model.py index 624ea0543..6d3e4aea8 100644 --- a/src/backend/base/langflow/services/database/models/flow/model.py +++ b/src/backend/base/langflow/services/database/models/flow/model.py @@ -3,7 +3,7 @@ import re import warnings from datetime import datetime, timezone -from typing import TYPE_CHECKING, Dict, Optional +from typing import TYPE_CHECKING, Dict, List, Optional from uuid import UUID, uuid4 import emoji @@ -17,6 +17,7 @@ from langflow.schema import Data if TYPE_CHECKING: from langflow.services.database.models.folder import Folder + from langflow.services.database.models.message import MessageTable from langflow.services.database.models.user import User @@ -141,6 +142,7 @@ class Flow(FlowBase, table=True): user: "User" = Relationship(back_populates="flows") folder_id: Optional[UUID] = Field(default=None, foreign_key="folder.id", nullable=True, index=True) folder: Optional["Folder"] = Relationship(back_populates="flows") + messages: List["MessageTable"] = Relationship(back_populates="flow") def to_data(self): serialized = self.model_dump() diff --git a/src/backend/base/langflow/services/database/models/message/__init__.py b/src/backend/base/langflow/services/database/models/message/__init__.py new file mode 100644 index 000000000..8cfb2ff4f --- /dev/null +++ b/src/backend/base/langflow/services/database/models/message/__init__.py @@ -0,0 +1,3 @@ +from .model import MessageTable, MessageCreate, MessageRead, MessageUpdate + +__all__ = ["MessageTable", "MessageCreate", "MessageRead", "MessageUpdate"] diff --git a/src/backend/base/langflow/services/database/models/message/model.py b/src/backend/base/langflow/services/database/models/message/model.py new file mode 100644 index 000000000..5a775d3d9 --- /dev/null +++ b/src/backend/base/langflow/services/database/models/message/model.py @@ -0,0 +1,74 @@ +from datetime import datetime, timezone +from typing import TYPE_CHECKING, List, Optional +from uuid import UUID, uuid4 + +from pydantic import field_validator +from sqlmodel import JSON, Column, Field, Relationship, SQLModel + +if TYPE_CHECKING: + from langflow.schema.message import Message + from langflow.services.database.models.flow.model import Flow + + +class MessageBase(SQLModel): + timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) + sender: str + sender_name: str + session_id: str + text: str + files: list[str] = Field(default_factory=list) + + @field_validator("files", mode="before") + @classmethod + def validate_files(cls, value): + if not value: + value = [] + return value + + @classmethod + def from_message(cls, message: "Message", flow_id: str | None = None): + # first check if the record has all the required fields + if message.text is None or not message.sender or not message.sender_name: + raise ValueError("The message does not have the required fields (text, sender, sender_name).") + if isinstance(message.timestamp, str): + timestamp = datetime.fromisoformat(message.timestamp) + else: + timestamp = message.timestamp + return cls( + sender=message.sender, + sender_name=message.sender_name, + text=message.text, + session_id=message.session_id, + files=message.files or [], + timestamp=timestamp, + flow_id=flow_id, + ) + + +class MessageTable(MessageBase, table=True): + __tablename__ = "message" + id: UUID = Field(default_factory=uuid4, primary_key=True) + flow_id: Optional[UUID] = Field(default=None, foreign_key="flow.id") + flow: "Flow" = Relationship(back_populates="messages") + files: List[str] = Field(sa_column=Column(JSON)) + + # Needed for Column(JSON) + class Config: + arbitrary_types_allowed = True + + +class MessageRead(MessageBase): + id: UUID + flow_id: Optional[UUID] = Field() + + +class MessageCreate(MessageBase): + pass + + +class MessageUpdate(SQLModel): + text: Optional[str] = None + sender: Optional[str] = None + sender_name: Optional[str] = None + session_id: Optional[str] = None + files: Optional[list[str]] = None diff --git a/src/backend/base/langflow/services/monitor/schema.py b/src/backend/base/langflow/services/monitor/schema.py index 2294678fe..eeea846a1 100644 --- a/src/backend/base/langflow/services/monitor/schema.py +++ b/src/backend/base/langflow/services/monitor/schema.py @@ -1,6 +1,7 @@ import json from datetime import datetime, timezone from typing import Any, Optional +from uuid import UUID from pydantic import BaseModel, Field, field_serializer, field_validator @@ -81,8 +82,8 @@ class TransactionModelResponse(DefaultModel): class MessageModel(DefaultModel): - index: Optional[int] = Field(default=None) - flow_id: Optional[str] = Field(default=None, alias="flow_id") + id: Optional[str | UUID] = Field(default=None) + flow_id: Optional[UUID] = Field(default=None) timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) sender: str sender_name: str @@ -127,16 +128,7 @@ class MessageModel(DefaultModel): class MessageModelResponse(MessageModel): - index: Optional[int] = Field(default=None) - - @field_validator("index", mode="before") - def validate_id(cls, v): - if isinstance(v, float): - try: - return int(v) - except ValueError: - return None - return v + pass class MessageModelRequest(MessageModel): diff --git a/src/backend/base/langflow/services/monitor/service.py b/src/backend/base/langflow/services/monitor/service.py index 6b99b9760..d15d31329 100644 --- a/src/backend/base/langflow/services/monitor/service.py +++ b/src/backend/base/langflow/services/monitor/service.py @@ -3,14 +3,15 @@ from pathlib import Path from typing import TYPE_CHECKING, List, Optional, Union import duckdb -from langflow.services.base import Service -from langflow.services.monitor.utils import add_row_to_table, drop_and_create_table_if_schema_mismatch from loguru import logger from platformdirs import user_cache_dir +from langflow.services.base import Service +from langflow.services.monitor.utils import add_row_to_table, drop_and_create_table_if_schema_mismatch + if TYPE_CHECKING: - from langflow.services.settings.service import SettingsService from langflow.services.monitor.schema import MessageModel, TransactionModel, VertexBuildModel + from langflow.services.settings.service import SettingsService class MonitorService(Service): @@ -129,45 +130,6 @@ class MonitorService(Service): return self.exec_query(query, read_only=False) - def add_message(self, message: "MessageModel"): - self.add_row("messages", message) - - def get_messages( - self, - flow_id: Optional[str] = None, - sender: Optional[str] = None, - sender_name: Optional[str] = None, - session_id: Optional[str] = None, - order_by: Optional[str] = "timestamp", - order: Optional[str] = "DESC", - limit: Optional[int] = None, - ): - query = "SELECT index, flow_id, sender_name, sender, session_id, text, files, timestamp FROM messages" - conditions = [] - if sender: - conditions.append(f"sender = '{sender}'") - if sender_name: - conditions.append(f"sender_name = '{sender_name}'") - if session_id: - conditions.append(f"session_id = '{session_id}'") - if flow_id: - conditions.append(f"flow_id = '{flow_id}'") - - if conditions: - query += " WHERE " + " AND ".join(conditions) - - if order_by and order: - # Make sure the order is from newest to oldest - query += f" ORDER BY {order_by} {order.upper()}" - - if limit is not None: - query += f" LIMIT {limit}" - - with duckdb.connect(str(self.db_path), read_only=True) as conn: - df = conn.execute(query).df() - - return df - def get_transactions( self, source: Optional[str] = None, diff --git a/src/frontend/package.json b/src/frontend/package.json index 219b5c7d4..42752d9c4 100644 --- a/src/frontend/package.json +++ b/src/frontend/package.json @@ -83,6 +83,7 @@ "build": "vite build", "serve": "vite preview", "format": "npx prettier --write \"{tests,src}/**/*.{js,jsx,ts,tsx,json,md}\" --ignore-path .prettierignore", + "check-format": "npx prettier --check \"{tests,src}/**/*.{js,jsx,ts,tsx,json,md}\" --ignore-path .prettierignore", "type-check": "tsc --noEmit --pretty --project tsconfig.json && vite" }, "eslintConfig": { @@ -132,4 +133,4 @@ "ua-parser-js": "^1.0.38", "vite": "^5.3.1" } -} +} \ No newline at end of file diff --git a/src/frontend/src/CustomNodes/hooks/use-fetch-data-on-mount.tsx b/src/frontend/src/CustomNodes/hooks/use-fetch-data-on-mount.tsx index 5802863b0..497c560c2 100644 --- a/src/frontend/src/CustomNodes/hooks/use-fetch-data-on-mount.tsx +++ b/src/frontend/src/CustomNodes/hooks/use-fetch-data-on-mount.tsx @@ -6,13 +6,14 @@ import { } from "../../constants/constants"; import useAlertStore from "../../stores/alertStore"; import { ResponseErrorDetailAPI } from "../../types/api"; +import { NodeDataType } from "../../types/flow"; const useFetchDataOnMount = ( - data, - name, - handleUpdateValues, - setNode, - setIsLoading, + data: NodeDataType, + name: string, + handleUpdateValues: (name: string, data: NodeDataType) => Promise, + setNode: (id: string, callback: (oldNode: any) => any) => void, + setIsLoading: (value: boolean) => void, ) => { const setErrorData = useAlertStore((state) => state.setErrorData); diff --git a/src/frontend/src/CustomNodes/hooks/use-handle-new-value.tsx b/src/frontend/src/CustomNodes/hooks/use-handle-new-value.tsx index b9690e270..a32c4d2dc 100644 --- a/src/frontend/src/CustomNodes/hooks/use-handle-new-value.tsx +++ b/src/frontend/src/CustomNodes/hooks/use-handle-new-value.tsx @@ -5,15 +5,16 @@ import { } from "../../constants/constants"; import useAlertStore from "../../stores/alertStore"; import { ResponseErrorTypeAPI } from "../../types/api"; +import { NodeDataType } from "../../types/flow"; const useHandleOnNewValue = ( - data, - name, - takeSnapshot, - handleUpdateValues, - debouncedHandleUpdateValues, - setNode, - setIsLoading, + data: NodeDataType, + name: string, + takeSnapshot: () => void, + handleUpdateValues: (name: string, data: NodeDataType) => Promise, + debouncedHandleUpdateValues: any, + setNode: (id: string, callback: (oldNode: any) => any) => void, + setIsLoading: (value: boolean) => void, ) => { const setErrorData = useAlertStore((state) => state.setErrorData); diff --git a/src/frontend/src/CustomNodes/hooks/use-handle-node-class.tsx b/src/frontend/src/CustomNodes/hooks/use-handle-node-class.tsx index 933f836a4..e82c06e72 100644 --- a/src/frontend/src/CustomNodes/hooks/use-handle-node-class.tsx +++ b/src/frontend/src/CustomNodes/hooks/use-handle-node-class.tsx @@ -1,11 +1,12 @@ import { cloneDeep } from "lodash"; +import { NodeDataType } from "../../types/flow"; const useHandleNodeClass = ( - data, - name, - takeSnapshot, - setNode, - updateNodeInternals, + data: NodeDataType, + name: string, + takeSnapshot: () => void, + setNode: (id: string, callback: (oldNode: any) => any) => void, + updateNodeInternals: (id: string) => void, ) => { const handleNodeClass = (newNodeClass, code, type?: string) => { if (!data.node) return; diff --git a/src/frontend/src/CustomNodes/hooks/use-handle-refresh-buttons.tsx b/src/frontend/src/CustomNodes/hooks/use-handle-refresh-buttons.tsx index e2ecb3f46..65345fcea 100644 --- a/src/frontend/src/CustomNodes/hooks/use-handle-refresh-buttons.tsx +++ b/src/frontend/src/CustomNodes/hooks/use-handle-refresh-buttons.tsx @@ -7,7 +7,10 @@ import useAlertStore from "../../stores/alertStore"; import { ResponseErrorDetailAPI } from "../../types/api"; import { handleUpdateValues } from "../../utils/parameterUtils"; -const useHandleRefreshButtonPress = (setIsLoading, setNode) => { +const useHandleRefreshButtonPress = ( + setIsLoading: (value: boolean) => void, + setNode: (id: string, callback: (oldNode: any) => any) => void, +) => { const setErrorData = useAlertStore((state) => state.setErrorData); const handleRefreshButtonPress = async (name, data) => { diff --git a/src/frontend/src/CustomNodes/hooks/use-update-validation-status.tsx b/src/frontend/src/CustomNodes/hooks/use-update-validation-status.tsx index 2a7153dfb..e93691b5f 100644 --- a/src/frontend/src/CustomNodes/hooks/use-update-validation-status.tsx +++ b/src/frontend/src/CustomNodes/hooks/use-update-validation-status.tsx @@ -1,6 +1,11 @@ import { useEffect } from "react"; +import { FlowPoolType } from "../../types/zustand/flow"; -const useUpdateValidationStatus = (dataId, flowPool, setValidationStatus) => { +const useUpdateValidationStatus = ( + dataId: string, + flowPool: FlowPoolType, + setValidationStatus: (value: any) => void, +) => { useEffect(() => { const relevantData = flowPool[dataId] && flowPool[dataId]?.length > 0 diff --git a/src/frontend/src/CustomNodes/hooks/use-validation-status-string.tsx b/src/frontend/src/CustomNodes/hooks/use-validation-status-string.tsx index 31929eb99..3ad905dc8 100644 --- a/src/frontend/src/CustomNodes/hooks/use-validation-status-string.tsx +++ b/src/frontend/src/CustomNodes/hooks/use-validation-status-string.tsx @@ -4,7 +4,7 @@ import { isErrorLog } from "../../types/utils/typeCheckingUtils"; const useValidationStatusString = ( validationStatus: VertexBuildTypeAPI | null, - setValidationString, + setValidationString: (value: any) => void, ) => { useEffect(() => { if (validationStatus && validationStatus.data?.outputs) { diff --git a/src/frontend/src/components/cardComponent/hooks/use-data-effect.tsx b/src/frontend/src/components/cardComponent/hooks/use-data-effect.tsx new file mode 100644 index 000000000..21d29049a --- /dev/null +++ b/src/frontend/src/components/cardComponent/hooks/use-data-effect.tsx @@ -0,0 +1,19 @@ +import { useEffect } from "react"; +import { storeComponent } from "../../../types/store"; + +const useDataEffect = ( + data: storeComponent, + setLikedByUser: (value: any) => void, + setLikesCount: (value: any) => void, + setDownloadsCount: (value: any) => void, +) => { + useEffect(() => { + if (data) { + setLikedByUser(data?.liked_by_user ?? false); + setLikesCount(data?.liked_by_count ?? 0); + setDownloadsCount(data?.downloads_count ?? 0); + } + }, [data, data?.liked_by_count, data?.liked_by_user, data?.downloads_count]); +}; + +export default useDataEffect; diff --git a/src/frontend/src/components/cardComponent/hooks/use-handle-install.tsx b/src/frontend/src/components/cardComponent/hooks/use-handle-install.tsx new file mode 100644 index 000000000..4c407349d --- /dev/null +++ b/src/frontend/src/components/cardComponent/hooks/use-handle-install.tsx @@ -0,0 +1,55 @@ +import { useState } from "react"; +import { getComponent } from "../../../controllers/API"; +import useFlowsManagerStore from "../../../stores/flowsManagerStore"; +import { storeComponent } from "../../../types/store"; +import cloneFlowWithParent from "../../../utils/storeUtils"; + +const useInstallComponent = ( + data: storeComponent, + name: string, + isStore: boolean, + downloadsCount: number, + setDownloadsCount: (value: any) => void, + setLoading: (value: boolean) => void, + setSuccessData: (value: { title: string }) => void, + setErrorData: (value: { title: string; list: string[] }) => void, +) => { + const addFlow = useFlowsManagerStore((state) => state.addFlow); + + const handleInstall = () => { + const temp = downloadsCount; + setDownloadsCount((old) => Number(old) + 1); + setLoading(true); + + getComponent(data.id) + .then((res) => { + const newFlow = cloneFlowWithParent(res, res.id, data.is_component); + addFlow(true, newFlow) + .then((id) => { + setSuccessData({ + title: `${name} ${isStore ? "Downloaded" : "Installed"} Successfully.`, + }); + setLoading(false); + }) + .catch((error) => { + setLoading(false); + setErrorData({ + title: `Error ${isStore ? "downloading" : "installing"} the ${name}`, + list: [error.response.data.detail], + }); + }); + }) + .catch((err) => { + setLoading(false); + setErrorData({ + title: `Error ${isStore ? "downloading" : "installing"} the ${name}`, + list: [err.response.data.detail], + }); + setDownloadsCount(temp); + }); + }; + + return { handleInstall }; +}; + +export default useInstallComponent; diff --git a/src/frontend/src/components/cardComponent/hooks/use-handle-like.tsx b/src/frontend/src/components/cardComponent/hooks/use-handle-like.tsx new file mode 100644 index 000000000..bfda076be --- /dev/null +++ b/src/frontend/src/components/cardComponent/hooks/use-handle-like.tsx @@ -0,0 +1,51 @@ +import { postLikeComponent } from "../../../controllers/API"; +import { storeComponent } from "../../../types/store"; + +const useLikeComponent = ( + data: storeComponent, + name: string, + setLoadingLike: (value: boolean) => void, + likedByUser: boolean | null | undefined, + likesCount: number, + setLikedByUser: (value: any) => void, + setLikesCount: (value: any) => void, + setValidApiKey: (value: boolean) => void, + setErrorData: (value: { title: string; list: string[] }) => void, +) => { + const handleLike = () => { + setLoadingLike(true); + if (likedByUser !== undefined || likedByUser !== null) { + const temp = likedByUser; + const tempNum = likesCount; + setLikedByUser((prev) => !prev); + setLikesCount((prev) => (temp ? prev - 1 : prev + 1)); + + postLikeComponent(data.id) + .then((response) => { + setLoadingLike(false); + setLikesCount(response.data.likes_count); + setLikedByUser(response.data.liked_by_user); + }) + .catch((error) => { + setLoadingLike(false); + setLikesCount(tempNum); + setLikedByUser(temp); + if (error.response.status === 403) { + setValidApiKey(false); + } else { + console.error(error); + setErrorData({ + title: `Error liking ${name}.`, + list: [error.response.data.detail], + }); + } + }); + } + }; + + return { + handleLike, + }; +}; + +export default useLikeComponent; diff --git a/src/frontend/src/components/cardComponent/hooks/use-on-drag-start.tsx b/src/frontend/src/components/cardComponent/hooks/use-on-drag-start.tsx new file mode 100644 index 000000000..251c668e2 --- /dev/null +++ b/src/frontend/src/components/cardComponent/hooks/use-on-drag-start.tsx @@ -0,0 +1,34 @@ +import { useCallback } from "react"; +import { createRoot } from "react-dom/client"; +import useFlowsManagerStore from "../../../stores/flowsManagerStore"; +import { storeComponent } from "../../../types/store"; +import DragCardComponent from "../components/dragCardComponent"; + +const useDragStart = (data: storeComponent) => { + const getFlowById = useFlowsManagerStore((state) => state.getFlowById); + + const onDragStart = useCallback( + (event) => { + let image = ; // Replace with whatever you want here + + const ghost = document.createElement("div"); + ghost.style.transform = "translate(-10000px, -10000px)"; + ghost.style.position = "absolute"; + document.body.appendChild(ghost); + event.dataTransfer.setDragImage(ghost, 0, 0); + + const root = createRoot(ghost); + root.render(image); + + const flow = getFlowById(data.id); + if (flow) { + event.dataTransfer.setData("flow", JSON.stringify(data)); + } + }, + [data], + ); + + return { onDragStart }; +}; + +export default useDragStart; diff --git a/src/frontend/src/components/cardComponent/hooks/use-playground-effect.tsx b/src/frontend/src/components/cardComponent/hooks/use-playground-effect.tsx new file mode 100644 index 000000000..e9236e98b --- /dev/null +++ b/src/frontend/src/components/cardComponent/hooks/use-playground-effect.tsx @@ -0,0 +1,27 @@ +import { useEffect } from "react"; +import { FlowType } from "../../../types/flow"; + +const usePlaygroundEffect = ( + currentFlowId: string, + playground: boolean, + openPlayground: boolean, + currentFlow: FlowType | undefined, + setNodes: (value: any, value2: boolean) => void, + setEdges: (value: any, value2: boolean) => void, + cleanFlowPool: () => void, +) => { + useEffect(() => { + if (currentFlowId && playground) { + if (openPlayground) { + setNodes(currentFlow?.data?.nodes ?? [], true); + setEdges(currentFlow?.data?.edges ?? [], true); + } else { + setNodes([], true); + setEdges([], true); + } + cleanFlowPool(); + } + }, [openPlayground]); +}; + +export default usePlaygroundEffect; diff --git a/src/frontend/src/components/cardComponent/index.tsx b/src/frontend/src/components/cardComponent/index.tsx index ba00e6958..9e5825155 100644 --- a/src/frontend/src/components/cardComponent/index.tsx +++ b/src/frontend/src/components/cardComponent/index.tsx @@ -28,6 +28,11 @@ import { Checkbox } from "../ui/checkbox"; import { FormControl, FormField } from "../ui/form"; import Loading from "../ui/loading"; import DragCardComponent from "./components/dragCardComponent"; +import useDataEffect from "./hooks/use-data-effect"; +import useInstallComponent from "./hooks/use-handle-install"; +import useLikeComponent from "./hooks/use-handle-like"; +import useDragStart from "./hooks/use-on-drag-start"; +import usePlaygroundEffect from "./hooks/use-playground-effect"; import { convertTestName } from "./utils/convert-test-name"; export default function CollectionCardComponent({ @@ -59,11 +64,9 @@ export default function CollectionCardComponent({ const isStore = false; const [loading, setLoading] = useState(false); const [loadingLike, setLoadingLike] = useState(false); - const [liked_by_user, setLiked_by_user] = useState( - data?.liked_by_user ?? false, - ); - const [likes_count, setLikes_count] = useState(data?.liked_by_count ?? 0); - const [downloads_count, setDownloads_count] = useState( + const [likedByUser, setLikedByUser] = useState(data?.liked_by_user ?? false); + const [likesCount, setLikesCount] = useState(data?.liked_by_count ?? 0); + const [downloadsCount, setDownloadsCount] = useState( data?.downloads_count ?? 0, ); const currentFlow = useFlowsManagerStore((state) => state.currentFlow); @@ -99,115 +102,45 @@ export default function CollectionCardComponent({ return inputs.length > 0 || outputs.length > 0; } - useEffect(() => { - if (currentFlowId && playground) { - if (openPlayground) { - setNodes(currentFlow?.data?.nodes ?? [], true); - setEdges(currentFlow?.data?.edges ?? [], true); - } else { - setNodes([], true); - setEdges([], true); - } - cleanFlowPool(); - } - }, [openPlayground]); + usePlaygroundEffect( + currentFlowId, + playground!, + openPlayground, + currentFlow, + setNodes, + setEdges, + cleanFlowPool, + ); - useEffect(() => { - if (data) { - setLiked_by_user(data?.liked_by_user ?? false); - setLikes_count(data?.liked_by_count ?? 0); - setDownloads_count(data?.downloads_count ?? 0); - } - }, [data, data.liked_by_count, data.liked_by_user, data.downloads_count]); + useDataEffect(data, setLikedByUser, setLikesCount, setDownloadsCount); - function handleInstall() { - const temp = downloads_count; - setDownloads_count((old) => Number(old) + 1); - setLoading(true); - getComponent(data.id) - .then((res) => { - const newFlow = cloneFLowWithParent(res, res.id, data.is_component); - addFlow(true, newFlow) - .then((id) => { - setSuccessData({ - title: `${name} ${ - isStore ? "Downloaded" : "Installed" - } Successfully.`, - }); - setLoading(false); - }) - .catch((error) => { - setLoading(false); - setErrorData({ - title: `Error ${ - isStore ? "downloading" : "installing" - } the ${name}`, - list: [error["response"]["data"]["detail"]], - }); - }); - }) - .catch((err) => { - setLoading(false); - setErrorData({ - title: `Error ${isStore ? "downloading" : "installing"} the ${name}`, - list: [err["response"]["data"]["detail"]], - }); - setDownloads_count(temp); - }); - } + const { handleInstall } = useInstallComponent( + data, + name, + isStore, + downloadsCount, + setDownloadsCount, + setLoading, + setSuccessData, + setErrorData, + ); - function handleLike() { - setLoadingLike(true); - if (liked_by_user !== undefined || liked_by_user !== null) { - const temp = liked_by_user; - const tempNum = likes_count; - setLiked_by_user((prev) => !prev); - if (!temp) { - setLikes_count((prev) => Number(prev) + 1); - } else { - setLikes_count((prev) => Number(prev) - 1); - } - postLikeComponent(data.id) - .then((response) => { - setLoadingLike(false); - setLikes_count(response.data.likes_count); - setLiked_by_user(response.data.liked_by_user); - }) - .catch((error) => { - setLoadingLike(false); - setLikes_count(tempNum); - setLiked_by_user(temp); - if (error.response.status === 403) { - setValidApiKey(false); - } else { - console.error(error); - setErrorData({ - title: `Error liking ${name}.`, - list: [error["response"]["data"]["detail"]], - }); - } - }); - } - } + const { handleLike } = useLikeComponent( + data, + name, + setLoadingLike, + likedByUser, + likesCount, + setLikedByUser, + setLikesCount, + setValidApiKey, + setErrorData, + ); const isSelectedCard = selectedFlowsComponentsCards?.includes(data?.id) ?? false; - function onDragStart(event: React.DragEvent) { - let image: JSX.Element = ; // <== whatever you want here - - var ghost = document.createElement("div"); - ghost.style.transform = "translate(-10000px, -10000px)"; - ghost.style.position = "absolute"; - document.body.appendChild(ghost); - event.dataTransfer.setDragImage(ghost, 0, 0); - const root = createRoot(ghost); - root.render(image); - const flow = getFlowById(data.id); - if (flow) { - event.dataTransfer.setData("flow", JSON.stringify(data)); - } - } + const { onDragStart } = useDragStart(data); return ( <> @@ -264,7 +197,7 @@ export default function CollectionCardComponent({ - {likes_count ?? 0} + {likesCount ?? 0} @@ -275,7 +208,7 @@ export default function CollectionCardComponent({ className="h-4 w-4" /> - {downloads_count ?? 0} + {downloadsCount ?? 0} @@ -324,20 +257,7 @@ export default function CollectionCardComponent({ )} )} -
- {/* {data.tags && - data.tags.length > 0 && - data.tags.map((tag, index) => ( - - {tag.name} - - ))} */} -
+
@@ -457,7 +377,7 @@ export default function CollectionCardComponent({ name="Heart" className={cn( "h-5 w-5", - liked_by_user + likedByUser ? "fill-destructive stroke-destructive" : "", !authorized ? "text-ring" : "", diff --git a/src/frontend/src/components/sidebarComponent/hooks/use-on-file-drop.tsx b/src/frontend/src/components/sidebarComponent/hooks/use-on-file-drop.tsx index c75bf4bec..4dd87fc50 100644 --- a/src/frontend/src/components/sidebarComponent/hooks/use-on-file-drop.tsx +++ b/src/frontend/src/components/sidebarComponent/hooks/use-on-file-drop.tsx @@ -9,7 +9,10 @@ import useFlowsManagerStore from "../../../stores/flowsManagerStore"; import { useFolderStore } from "../../../stores/foldersStore"; import { addVersionToDuplicates } from "../../../utils/reactflowUtils"; -const useFileDrop = (folderId, folderChangeCallback) => { +const useFileDrop = ( + folderId: string, + folderChangeCallback: (folderId: string) => void, +) => { const setFolderDragging = useFolderStore((state) => state.setFolderDragging); const setFolderIdDragging = useFolderStore( (state) => state.setFolderIdDragging, diff --git a/src/frontend/src/modals/IOModal/components/chatView/chatInput/hooks/use-auto-resize-text-area.tsx b/src/frontend/src/modals/IOModal/components/chatView/chatInput/hooks/use-auto-resize-text-area.tsx index d4102fe9d..41428e4a4 100644 --- a/src/frontend/src/modals/IOModal/components/chatView/chatInput/hooks/use-auto-resize-text-area.tsx +++ b/src/frontend/src/modals/IOModal/components/chatView/chatInput/hooks/use-auto-resize-text-area.tsx @@ -1,6 +1,9 @@ import { useEffect } from "react"; -const useAutoResizeTextArea = (value, inputRef) => { +const useAutoResizeTextArea = ( + value: string, + inputRef: React.RefObject, +) => { useEffect(() => { if (inputRef.current && inputRef.current.scrollHeight! !== 0) { inputRef.current.style!.height = "inherit"; // Reset the height diff --git a/src/frontend/src/modals/IOModal/components/chatView/chatInput/hooks/use-drag-and-drop.tsx b/src/frontend/src/modals/IOModal/components/chatView/chatInput/hooks/use-drag-and-drop.tsx index b1ff9d143..9b30fba41 100644 --- a/src/frontend/src/modals/IOModal/components/chatView/chatInput/hooks/use-drag-and-drop.tsx +++ b/src/frontend/src/modals/IOModal/components/chatView/chatInput/hooks/use-drag-and-drop.tsx @@ -7,10 +7,10 @@ import { import useFileUpload from "./use-file-upload"; const useDragAndDrop = ( - setIsDragging, - setFiles, - currentFlowId, - setErrorData, + setIsDragging: (value: boolean) => void, + setFiles: (value: any) => void, + currentFlowId: string, + setErrorData: (value: any) => void, ) => { const dragOver = (e) => { e.preventDefault(); diff --git a/src/frontend/src/modals/IOModal/components/chatView/chatInput/hooks/use-focus-unlock.tsx b/src/frontend/src/modals/IOModal/components/chatView/chatInput/hooks/use-focus-unlock.tsx index 15dfe70ae..6e951d80f 100644 --- a/src/frontend/src/modals/IOModal/components/chatView/chatInput/hooks/use-focus-unlock.tsx +++ b/src/frontend/src/modals/IOModal/components/chatView/chatInput/hooks/use-focus-unlock.tsx @@ -1,6 +1,9 @@ import { useEffect } from "react"; -const useFocusOnUnlock = (lockChat, inputRef) => { +const useFocusOnUnlock = ( + lockChat: boolean, + inputRef: React.RefObject, +) => { useEffect(() => { if (!lockChat && inputRef.current) { inputRef.current.focus(); diff --git a/src/frontend/src/modals/IOModal/components/chatView/chatInput/hooks/use-upload.tsx b/src/frontend/src/modals/IOModal/components/chatView/chatInput/hooks/use-upload.tsx index 5a9e85195..29bc86e6a 100644 --- a/src/frontend/src/modals/IOModal/components/chatView/chatInput/hooks/use-upload.tsx +++ b/src/frontend/src/modals/IOModal/components/chatView/chatInput/hooks/use-upload.tsx @@ -1,3 +1,4 @@ +import { AxiosResponse } from "axios"; import { useEffect } from "react"; import ShortUniqueId from "short-unique-id"; import { @@ -6,9 +7,18 @@ import { SN_ERROR_TEXT, } from "../../../../../../constants/constants"; import useAlertStore from "../../../../../../stores/alertStore"; +import { UploadFileTypeAPI } from "../../../../../../types/api"; import useFileUpload from "./use-file-upload"; -const useUpload = (uploadFile, currentFlowId, setFiles, lockChat) => { +const useUpload = ( + uploadFile: ( + file: File, + id: string, + ) => Promise>, + currentFlowId: string, + setFiles: any, + lockChat: boolean, +) => { const setErrorData = useAlertStore((state) => state.setErrorData); useEffect(() => { const handlePaste = (event: ClipboardEvent): void => { diff --git a/src/frontend/src/modals/editNodeModal/hooks/use-column-defs.tsx b/src/frontend/src/modals/editNodeModal/hooks/use-column-defs.tsx index 14263a9c6..71f8566de 100644 --- a/src/frontend/src/modals/editNodeModal/hooks/use-column-defs.tsx +++ b/src/frontend/src/modals/editNodeModal/hooks/use-column-defs.tsx @@ -2,9 +2,10 @@ import { ColDef, ValueGetterParams } from "ag-grid-community"; import { useMemo } from "react"; import TableNodeCellRender from "../../../components/tableComponent/components/tableNodeCellRender"; import TableToggleCellRender from "../../../components/tableComponent/components/tableToggleCellRender"; +import { NodeDataType } from "../../../types/flow"; const useColumnDefs = ( - myData: any, + myData: NodeDataType, handleOnNewValue: (newValue: any, name: string) => void, handleOnChangeDb: (value: boolean, key: string) => void, changeAdvanced: (n: string) => void, diff --git a/src/frontend/src/modals/editNodeModal/hooks/use-row-data.tsx b/src/frontend/src/modals/editNodeModal/hooks/use-row-data.tsx index cafd25c6d..e2cd5a772 100644 --- a/src/frontend/src/modals/editNodeModal/hooks/use-row-data.tsx +++ b/src/frontend/src/modals/editNodeModal/hooks/use-row-data.tsx @@ -1,14 +1,12 @@ import { useMemo } from "react"; import { LANGFLOW_SUPPORTED_TYPES } from "../../../constants/constants"; -import { TemplateVariableType } from "../../../types/api"; +import { NodeDataType } from "../../../types/flow"; -const useRowData = (myData, open) => { +const useRowData = (myData: NodeDataType, open: boolean) => { const rowData = useMemo(() => { return Object.keys(myData.node!.template) .filter((key: string) => { - const templateParam = myData.node!.template[ - key - ] as TemplateVariableType; + const templateParam = myData.node!.template[key] as any; return ( key.charAt(0) !== "_" && templateParam.show && @@ -20,9 +18,7 @@ const useRowData = (myData, open) => { ); }) .map((key: string) => { - const templateParam = myData.node!.template[ - key - ] as TemplateVariableType; + const templateParam = myData.node!.template[key] as any; return { ...templateParam, key: key, diff --git a/src/frontend/src/modals/editNodeModal/index.tsx b/src/frontend/src/modals/editNodeModal/index.tsx index f4a368fa6..2a01e6c0b 100644 --- a/src/frontend/src/modals/editNodeModal/index.tsx +++ b/src/frontend/src/modals/editNodeModal/index.tsx @@ -16,13 +16,11 @@ const EditNodeModal = forwardRef( nodeLength, open, setOpen, - // setOpenWDoubleClick, data, }: { nodeLength: number; open: boolean; setOpen: (open: boolean) => void; - // setOpenWDoubleClick: (open: boolean) => void; data: NodeDataType; }, ref, diff --git a/src/frontend/src/pages/MainPage/components/componentsComponent/components/collectionCard/index.tsx b/src/frontend/src/pages/MainPage/components/componentsComponent/components/collectionCard/index.tsx new file mode 100644 index 000000000..1c9c0799e --- /dev/null +++ b/src/frontend/src/pages/MainPage/components/componentsComponent/components/collectionCard/index.tsx @@ -0,0 +1,58 @@ +import React from "react"; +import { Link, useNavigate } from "react-router-dom"; +import CollectionCardComponent from "../../../../../../components/cardComponent"; +import IconComponent from "../../../../../../components/genericIconComponent"; +import { Button } from "../../../../../../components/ui/button"; +const CollectionCard = ({ item, type, isLoading, control }) => { + const navigate = useNavigate(); + const isComponent = item.is_component ?? false; + const editFlowLink = `/flow/${item.id}`; + const editFlowButtonTestId = `edit-flow-button-${item.id}`; + + const handleClick = () => { + if (!isComponent) { + navigate(editFlowLink); + } + }; + + const renderButton = () => { + if (!isComponent) { + return ( + + + + ); + } + return null; + }; + + return ( + + ); +}; + +export default CollectionCard; diff --git a/src/frontend/src/pages/MainPage/components/componentsComponent/hooks/use-delete-multiple.tsx b/src/frontend/src/pages/MainPage/components/componentsComponent/hooks/use-delete-multiple.tsx index 395088193..46caecbf1 100644 --- a/src/frontend/src/pages/MainPage/components/componentsComponent/hooks/use-delete-multiple.tsx +++ b/src/frontend/src/pages/MainPage/components/componentsComponent/hooks/use-delete-multiple.tsx @@ -1,15 +1,15 @@ import { useCallback } from "react"; const useDeleteMultipleFlows = ( - selectedFlowsComponentsCards, - removeFlow, - resetFilter, - getFoldersApi, - folderId, - myCollectionId, - getFolderById, - setSuccessData, - setErrorData, + selectedFlowsComponentsCards: string[], + removeFlow: (selectedFlowsComponentsCards: string[]) => Promise, + resetFilter: () => void, + getFoldersApi: (refetch?: boolean) => Promise, + folderId: string | undefined, + myCollectionId: string, + getFolderById: (id: string) => void, + setSuccessData: (data: { title: string }) => void, + setErrorData: (data: { title: string; list: string[] }) => void, ) => { const handleDeleteMultiple = useCallback(() => { removeFlow(selectedFlowsComponentsCards) diff --git a/src/frontend/src/pages/MainPage/components/componentsComponent/hooks/use-description-modal.tsx b/src/frontend/src/pages/MainPage/components/componentsComponent/hooks/use-description-modal.tsx index 6de2ebb6d..2af71e91a 100644 --- a/src/frontend/src/pages/MainPage/components/componentsComponent/hooks/use-description-modal.tsx +++ b/src/frontend/src/pages/MainPage/components/componentsComponent/hooks/use-description-modal.tsx @@ -1,6 +1,9 @@ import { useMemo } from "react"; -const useDescriptionModal = (selectedFlowsComponentsCards, type) => { +const useDescriptionModal = ( + selectedFlowsComponentsCards: string[] | undefined, + type: string | undefined, +) => { const getDescriptionModal = useMemo(() => { const getTypeLabel = (type) => { const labels = { diff --git a/src/frontend/src/pages/MainPage/components/componentsComponent/hooks/use-filtered-flows.tsx b/src/frontend/src/pages/MainPage/components/componentsComponent/hooks/use-filtered-flows.tsx index 96b1757ff..db8be8c72 100644 --- a/src/frontend/src/pages/MainPage/components/componentsComponent/hooks/use-filtered-flows.tsx +++ b/src/frontend/src/pages/MainPage/components/componentsComponent/hooks/use-filtered-flows.tsx @@ -1,10 +1,11 @@ import cloneDeep from "lodash/cloneDeep"; import { useEffect } from "react"; +import { FlowType } from "../../../../../types/flow"; const useFilteredFlows = ( - flowsFromFolder, - searchFlowsComponents, - setAllFlows, + flowsFromFolder: FlowType[], + searchFlowsComponents: string, + setAllFlows: (value: any[]) => void, ) => { useEffect(() => { const newFlows = cloneDeep(flowsFromFolder || []); diff --git a/src/frontend/src/pages/MainPage/components/componentsComponent/hooks/use-handle-duplicate.tsx b/src/frontend/src/pages/MainPage/components/componentsComponent/hooks/use-handle-duplicate.tsx index fc49fc0d1..d0ae38e4f 100644 --- a/src/frontend/src/pages/MainPage/components/componentsComponent/hooks/use-handle-duplicate.tsx +++ b/src/frontend/src/pages/MainPage/components/componentsComponent/hooks/use-handle-duplicate.tsx @@ -1,18 +1,31 @@ import { useCallback } from "react"; +import { XYPosition } from "reactflow"; +import { FlowType } from "../../../../../types/flow"; const useDuplicateFlows = ( - selectedFlowsComponentsCards, - addFlow, - allFlows, - resetFilter, - getFoldersApi, - folderId, - myCollectionId, - getFolderById, - setSuccessData, - setSelectedFlowsComponentsCards, - handleSelectAll, - cardTypes, + selectedFlowsComponentsCards: string[], + addFlow: ( + newProject: boolean, + flow?: FlowType, + override?: boolean, + position?: XYPosition, + fromDragAndDrop?: boolean, + ) => Promise, + allFlows: any[], + resetFilter: () => void, + getFoldersApi: ( + refetch?: boolean, + startupApplication?: boolean, + ) => Promise, + folderId: string, + myCollectionId: string, + getFolderById: (id: string) => void, + setSuccessData: (data: { title: string }) => void, + setSelectedFlowsComponentsCards: ( + selectedFlowsComponentsCards: string[], + ) => void, + handleSelectAll: (select: boolean) => void, + cardTypes: string, ) => { const handleDuplicate = useCallback(() => { Promise.all( diff --git a/src/frontend/src/pages/MainPage/components/componentsComponent/hooks/use-handle-export.tsx b/src/frontend/src/pages/MainPage/components/componentsComponent/hooks/use-handle-export.tsx index 8edabc146..4b5e2f925 100644 --- a/src/frontend/src/pages/MainPage/components/componentsComponent/hooks/use-handle-export.tsx +++ b/src/frontend/src/pages/MainPage/components/componentsComponent/hooks/use-handle-export.tsx @@ -1,15 +1,18 @@ import { useCallback } from "react"; +import { FlowType } from "../../../../../types/flow"; const useExportFlows = ( - selectedFlowsComponentsCards, - allFlows, - downloadFlow, - removeApiKeys, - version, - setSuccessData, - setSelectedFlowsComponentsCards, - handleSelectAll, - cardTypes, + selectedFlowsComponentsCards: string[], + allFlows: Array, + downloadFlow: (flow: any, name: string, description: string) => void, + removeApiKeys: (flow: any) => any, + version: string, + setSuccessData: (data: { title: string }) => void, + setSelectedFlowsComponentsCards: ( + selectedFlowsComponentsCards: string[], + ) => void, + handleSelectAll: (select: boolean) => void, + cardTypes: string, ) => { const handleExport = useCallback(() => { selectedFlowsComponentsCards.forEach((selectedFlowId) => { diff --git a/src/frontend/src/pages/MainPage/components/componentsComponent/hooks/use-handle-select-all.tsx b/src/frontend/src/pages/MainPage/components/componentsComponent/hooks/use-handle-select-all.tsx index a81515e0a..4961c73e9 100644 --- a/src/frontend/src/pages/MainPage/components/componentsComponent/hooks/use-handle-select-all.tsx +++ b/src/frontend/src/pages/MainPage/components/componentsComponent/hooks/use-handle-select-all.tsx @@ -1,6 +1,11 @@ import { useCallback } from "react"; +import { FlowType } from "../../../../../types/flow"; -const useSelectAll = (flowsFromFolder, getValues, setValue) => { +const useSelectAll = ( + flowsFromFolder: FlowType[], + getValues: () => Record, + setValue: (key: string, value: boolean) => void, +) => { const handleSelectAll = useCallback( (select) => { const flowsFromFolderIds = flowsFromFolder?.map((f) => f.id); diff --git a/src/frontend/src/pages/MainPage/components/componentsComponent/hooks/use-select-options-change.tsx b/src/frontend/src/pages/MainPage/components/componentsComponent/hooks/use-select-options-change.tsx index 56dc204c7..dc7053600 100644 --- a/src/frontend/src/pages/MainPage/components/componentsComponent/hooks/use-select-options-change.tsx +++ b/src/frontend/src/pages/MainPage/components/componentsComponent/hooks/use-select-options-change.tsx @@ -1,15 +1,15 @@ import { useCallback } from "react"; const useSelectOptionsChange = ( - selectedFlowsComponentsCards, - setErrorData, - setOpenDelete, - handleDuplicate, - handleExport, + selectedFlowsComponentsCards: string[] | undefined, + setErrorData: (data: { title: string; list: string[] }) => void, + setOpenDelete: (value: boolean) => void, + handleDuplicate: () => void, + handleExport: () => void, ) => { const handleSelectOptionsChange = useCallback( (action) => { - const hasSelected = selectedFlowsComponentsCards?.length > 0; + const hasSelected = selectedFlowsComponentsCards?.length! > 0; if (!hasSelected) { setErrorData({ title: "No items selected", diff --git a/src/frontend/src/pages/MainPage/components/componentsComponent/hooks/use-selected-flows.tsx b/src/frontend/src/pages/MainPage/components/componentsComponent/hooks/use-selected-flows.tsx index b6f00934e..e5ac9a90d 100644 --- a/src/frontend/src/pages/MainPage/components/componentsComponent/hooks/use-selected-flows.tsx +++ b/src/frontend/src/pages/MainPage/components/componentsComponent/hooks/use-selected-flows.tsx @@ -1,8 +1,10 @@ import { useEffect } from "react"; const useSelectedFlows = ( - entireFormValues, - setSelectedFlowsComponentsCards, + entireFormValues: Record | undefined, + setSelectedFlowsComponentsCards: ( + selectedFlowsComponentsCards: string[], + ) => void, ) => { useEffect(() => { if (!entireFormValues || Object.keys(entireFormValues).length === 0) return; diff --git a/src/frontend/src/pages/MainPage/components/componentsComponent/index.tsx b/src/frontend/src/pages/MainPage/components/componentsComponent/index.tsx index af13e2bb4..7ccd88048 100644 --- a/src/frontend/src/pages/MainPage/components/componentsComponent/index.tsx +++ b/src/frontend/src/pages/MainPage/components/componentsComponent/index.tsx @@ -19,6 +19,7 @@ import { getNameByType } from "../../utils/get-name-by-type"; import { sortFlows } from "../../utils/sort-flows"; import EmptyComponent from "../emptyComponent"; import HeaderComponent from "../headerComponent"; +import CollectionCard from "./components/collectionCard"; import useDeleteMultipleFlows from "./hooks/use-delete-multiple"; import useDescriptionModal from "./hooks/use-description-modal"; import useFilteredFlows from "./hooks/use-filtered-flows"; @@ -61,7 +62,6 @@ export default function ComponentsComponent({ const [handleFileDrop] = useFileDrop(uploadFlow, type)!; const [pageSize, setPageSize] = useState(20); const [pageIndex, setPageIndex] = useState(1); - const navigate = useNavigate(); const location = useLocation(); const all: FlowType[] = sortFlows(allFlows, type); const start = (pageIndex - 1) * pageSize; @@ -94,7 +94,7 @@ export default function ComponentsComponent({ getFolderById(folderId ? folderId : myCollectionId); }, [location]); - useFilteredFlows(flowsFromFolder, searchFlowsComponents, setAllFlows); + useFilteredFlows(flowsFromFolder!, searchFlowsComponents, setAllFlows); const resetFilter = () => { setPageIndex(1); @@ -107,7 +107,7 @@ export default function ComponentsComponent({ const methods = useForm(); const { handleSelectAll } = useSelectAll( - flowsFromFolder, + flowsFromFolder!, getValues, setValue, ); @@ -119,7 +119,7 @@ export default function ComponentsComponent({ resetFilter, getFoldersApi, folderId, - myCollectionId, + myCollectionId!, getFolderById, setSuccessData, setSelectedFlowsComponentsCards, @@ -155,7 +155,7 @@ export default function ComponentsComponent({ resetFilter, getFoldersApi, folderId, - myCollectionId, + myCollectionId!, getFolderById, setSuccessData, setErrorData, @@ -205,43 +205,10 @@ export default function ComponentsComponent({ {data?.map((item) => (
- - - - ) : ( - <> - ) - } - onClick={ - !item.is_component - ? () => { - navigate("/flow/" + item.id); - } - : undefined - } - playground={!item.is_component} + diff --git a/src/frontend/src/pages/MainPage/hooks/use-delete-folder.tsx b/src/frontend/src/pages/MainPage/hooks/use-delete-folder.tsx index 0d093bc32..27ccfdc19 100644 --- a/src/frontend/src/pages/MainPage/hooks/use-delete-folder.tsx +++ b/src/frontend/src/pages/MainPage/hooks/use-delete-folder.tsx @@ -2,7 +2,7 @@ import useAlertStore from "../../../stores/alertStore"; import { useFolderStore } from "../../../stores/foldersStore"; import { deleteFolder, getFolderById } from "../services"; -const useDeleteFolder = ({ navigate }) => { +const useDeleteFolder = ({ navigate }: { navigate: (url: string) => void }) => { const setSuccessData = useAlertStore((state) => state.setSuccessData); const setErrorData = useAlertStore((state) => state.setErrorData); const folderToEdit = useFolderStore((state) => state.folderToEdit); diff --git a/src/frontend/src/pages/MainPage/hooks/use-dropdown-options.tsx b/src/frontend/src/pages/MainPage/hooks/use-dropdown-options.tsx index 69e2c2df3..92957d295 100644 --- a/src/frontend/src/pages/MainPage/hooks/use-dropdown-options.tsx +++ b/src/frontend/src/pages/MainPage/hooks/use-dropdown-options.tsx @@ -1,7 +1,26 @@ +import { XYPosition } from "reactflow"; import { CONSOLE_ERROR_MSG } from "../../../constants/alerts_constants"; import useAlertStore from "../../../stores/alertStore"; -const useDropdownOptions = ({ uploadFlow, navigate, is_component }) => { +const useDropdownOptions = ({ + uploadFlow, + navigate, + is_component, +}: { + uploadFlow: ({ + newProject, + file, + isComponent, + position, + }: { + newProject: boolean; + file?: File; + isComponent: boolean | null; + position?: XYPosition; + }) => Promise; + navigate: (url: string) => void; + is_component: boolean; +}) => { const setSuccessData = useAlertStore((state) => state.setSuccessData); const setErrorData = useAlertStore((state) => state.setErrorData); const handleImportFromJSON = () => { diff --git a/src/frontend/src/pages/SettingsPage/pages/ApiKeysPage/hooks/use-api-keys.tsx b/src/frontend/src/pages/SettingsPage/pages/ApiKeysPage/hooks/use-api-keys.tsx index 8af9b4a0b..2bdbe33f9 100644 --- a/src/frontend/src/pages/SettingsPage/pages/ApiKeysPage/hooks/use-api-keys.tsx +++ b/src/frontend/src/pages/SettingsPage/pages/ApiKeysPage/hooks/use-api-keys.tsx @@ -1,6 +1,12 @@ import { getApiKey } from "../../../../../controllers/API"; +import { Users } from "../../../../../types/api"; -const useApiKeys = (userData, setLoadingKeys, keysList, setUserId) => { +const useApiKeys = ( + userData: Users | null, + setLoadingKeys: (load: boolean) => void, + keysList: React.MutableRefObject, + setUserId: (userId: string) => void, +) => { const fetchApiKeys = () => { setLoadingKeys(true); getApiKey() diff --git a/src/frontend/src/pages/SettingsPage/pages/ApiKeysPage/hooks/use-handle-delete-key.tsx b/src/frontend/src/pages/SettingsPage/pages/ApiKeysPage/hooks/use-handle-delete-key.tsx index 74d5dae99..0d642dfbf 100644 --- a/src/frontend/src/pages/SettingsPage/pages/ApiKeysPage/hooks/use-handle-delete-key.tsx +++ b/src/frontend/src/pages/SettingsPage/pages/ApiKeysPage/hooks/use-handle-delete-key.tsx @@ -7,10 +7,10 @@ import { import { deleteApiKey } from "../../../../../controllers/API"; const useDeleteApiKeys = ( - selectedRows, - resetFilter, - setSuccessData, - setErrorData, + selectedRows: string[], + resetFilter: () => void, + setSuccessData: (data: { title: string }) => void, + setErrorData: (data: { title: string; list: string[] }) => void, ) => { const handleDeleteKey = () => { Promise.all(selectedRows.map((selectedRow) => deleteApiKey(selectedRow))) diff --git a/src/frontend/src/pages/SettingsPage/pages/GeneralPage/components/ProfilePictureForm/components/profilePictureChooserComponent/hooks/use-preload-images.tsx b/src/frontend/src/pages/SettingsPage/pages/GeneralPage/components/ProfilePictureForm/components/profilePictureChooserComponent/hooks/use-preload-images.tsx index 988af6ea9..abc8587d2 100644 --- a/src/frontend/src/pages/SettingsPage/pages/GeneralPage/components/ProfilePictureForm/components/profilePictureChooserComponent/hooks/use-preload-images.tsx +++ b/src/frontend/src/pages/SettingsPage/pages/GeneralPage/components/ProfilePictureForm/components/profilePictureChooserComponent/hooks/use-preload-images.tsx @@ -4,7 +4,10 @@ import { BASE_URL_API, } from "../../../../../../../../../constants/constants"; -const usePreloadImages = (profilePictures, setImagesLoaded) => { +const usePreloadImages = ( + profilePictures: { [key: string]: string[] }, + setImagesLoaded: (value: boolean) => void, +) => { const preloadImages = async (imageUrls) => { return Promise.all( imageUrls.map( diff --git a/src/frontend/src/pages/SettingsPage/pages/hooks/use-patch-password.tsx b/src/frontend/src/pages/SettingsPage/pages/hooks/use-patch-password.tsx index c4b452e6b..0003fe4a1 100644 --- a/src/frontend/src/pages/SettingsPage/pages/hooks/use-patch-password.tsx +++ b/src/frontend/src/pages/SettingsPage/pages/hooks/use-patch-password.tsx @@ -5,8 +5,13 @@ import { SAVE_SUCCESS_ALERT, } from "../../../../constants/alerts_constants"; import { resetPassword } from "../../../../controllers/API"; +import { Users } from "../../../../types/api"; -const usePatchPassword = (userData, setSuccessData, setErrorData) => { +const usePatchPassword = ( + userData: Users | null, + setSuccessData: (data: { title: string; list?: string[] }) => void, + setErrorData: (data: { title: string; list: string[] }) => void, +) => { const handlePatchPassword = async (password, cnfPassword, handleInput) => { if (password !== cnfPassword) { setErrorData({ @@ -16,7 +21,7 @@ const usePatchPassword = (userData, setSuccessData, setErrorData) => { return; } try { - if (password !== "") await resetPassword(userData.id, { password }); + if (password !== "") await resetPassword(userData!.id, { password }); handleInput({ target: { name: "password", value: "" } }); handleInput({ target: { name: "cnfPassword", value: "" } }); setSuccessData({ title: SAVE_SUCCESS_ALERT }); diff --git a/src/frontend/src/pages/SettingsPage/pages/hooks/use-patch-profile-picture.tsx b/src/frontend/src/pages/SettingsPage/pages/hooks/use-patch-profile-picture.tsx index 584300fdf..6ad76f4ff 100644 --- a/src/frontend/src/pages/SettingsPage/pages/hooks/use-patch-profile-picture.tsx +++ b/src/frontend/src/pages/SettingsPage/pages/hooks/use-patch-profile-picture.tsx @@ -4,21 +4,22 @@ import { SAVE_SUCCESS_ALERT, } from "../../../../constants/alerts_constants"; import { updateUser } from "../../../../controllers/API"; +import { Users } from "../../../../types/api"; const usePatchProfilePicture = ( - setSuccessData, - setErrorData, - currentUserData, - setUserData, + setSuccessData: (data: { title: string; list?: string[] }) => void, + setErrorData: (data: { title: string; list: string[] }) => void, + currentUserData: Users | null, + setUserData: (data: any) => void, ) => { const handlePatchProfilePicture = async (profile_picture) => { try { if (profile_picture !== "") { - await updateUser(currentUserData.id, { + await updateUser(currentUserData!.id, { profile_image: profile_picture, }); let newUserData = cloneDeep(currentUserData); - newUserData.profile_image = profile_picture; + newUserData!.profile_image = profile_picture; setUserData(newUserData); } setSuccessData({ title: SAVE_SUCCESS_ALERT }); diff --git a/src/frontend/src/pages/SettingsPage/pages/hooks/use-save-key.tsx b/src/frontend/src/pages/SettingsPage/pages/hooks/use-save-key.tsx index cf5871a26..4f87405c3 100644 --- a/src/frontend/src/pages/SettingsPage/pages/hooks/use-save-key.tsx +++ b/src/frontend/src/pages/SettingsPage/pages/hooks/use-save-key.tsx @@ -7,11 +7,11 @@ import { AuthContext } from "../../../../contexts/authContext"; import { addApiKeyStore } from "../../../../controllers/API"; const useSaveKey = ( - setSuccessData, - setErrorData, - setHasApiKey, - setValidApiKey, - setLoadingApiKey, + setSuccessData: (data: { title: string }) => void, + setErrorData: (data: { title: string; list: string[] }) => void, + setHasApiKey: (hasApiKey: boolean) => void, + setValidApiKey: (validApiKey: boolean) => void, + setLoadingApiKey: (loadingApiKey: boolean) => void, ) => { const { storeApiKey } = useContext(AuthContext); diff --git a/src/frontend/src/pages/SettingsPage/pages/hooks/use-scroll-to-element.tsx b/src/frontend/src/pages/SettingsPage/pages/hooks/use-scroll-to-element.tsx index a56c2d5d6..faa7408a3 100644 --- a/src/frontend/src/pages/SettingsPage/pages/hooks/use-scroll-to-element.tsx +++ b/src/frontend/src/pages/SettingsPage/pages/hooks/use-scroll-to-element.tsx @@ -1,6 +1,9 @@ import { useEffect } from "react"; -const useScrollToElement = (scrollId, setCurrentFlowId) => { +const useScrollToElement = ( + scrollId: string | null | undefined, + setCurrentFlowId: (currentFlowId: string) => void, +) => { useEffect(() => { const element = document.getElementById(scrollId ?? "null"); if (element) { diff --git a/src/frontend/src/pages/SettingsPage/pages/messagesPage/hooks/use-messages-table.tsx b/src/frontend/src/pages/SettingsPage/pages/messagesPage/hooks/use-messages-table.tsx index 7f3a74352..fe344e478 100644 --- a/src/frontend/src/pages/SettingsPage/pages/messagesPage/hooks/use-messages-table.tsx +++ b/src/frontend/src/pages/SettingsPage/pages/messagesPage/hooks/use-messages-table.tsx @@ -1,8 +1,11 @@ +import { ColDef, ColGroupDef } from "ag-grid-community"; import { useEffect } from "react"; import { getMessagesTable } from "../../../../../controllers/API"; import { useMessagesStore } from "../../../../../stores/messagesStore"; -const useMessagesTable = (setColumns) => { +const useMessagesTable = ( + setColumns: (data: Array) => void, +) => { const setMessages = useMessagesStore((state) => state.setMessages); useEffect(() => { const fetchData = async () => { diff --git a/src/frontend/src/pages/SettingsPage/pages/messagesPage/hooks/use-remove-messages.tsx b/src/frontend/src/pages/SettingsPage/pages/messagesPage/hooks/use-remove-messages.tsx index d7f4d5202..f2c8a2a4d 100644 --- a/src/frontend/src/pages/SettingsPage/pages/messagesPage/hooks/use-remove-messages.tsx +++ b/src/frontend/src/pages/SettingsPage/pages/messagesPage/hooks/use-remove-messages.tsx @@ -2,10 +2,10 @@ import { deleteMessagesFn } from "../../../../../controllers/API"; import { useMessagesStore } from "../../../../../stores/messagesStore"; const useRemoveMessages = ( - setSelectedRows, - setSuccessData, - setErrorData, - selectedRows, + setSelectedRows: (data: number[]) => void, + setSuccessData: (data: { title: string }) => void, + setErrorData: (data: { title: string }) => void, + selectedRows: number[], ) => { const deleteMessages = useMessagesStore((state) => state.removeMessages); diff --git a/src/frontend/src/pages/SettingsPage/pages/messagesPage/hooks/use-updateMessage.tsx b/src/frontend/src/pages/SettingsPage/pages/messagesPage/hooks/use-updateMessage.tsx index f199ac489..dc2840b79 100644 --- a/src/frontend/src/pages/SettingsPage/pages/messagesPage/hooks/use-updateMessage.tsx +++ b/src/frontend/src/pages/SettingsPage/pages/messagesPage/hooks/use-updateMessage.tsx @@ -2,7 +2,10 @@ import { updateMessageApi } from "../../../../../controllers/API"; import { useMessagesStore } from "../../../../../stores/messagesStore"; import { Message } from "../../../../../types/messages"; -const useUpdateMessage = (setSuccessData, setErrorData) => { +const useUpdateMessage = ( + setSuccessData: (data: { title: string; list?: string[] }) => void, + setErrorData: (data: { title: string; list?: string[] }) => void, +) => { const updateMessage = useMessagesStore((state) => state.updateMessage); const handleUpdate = async (data: Message) => { diff --git a/tests/test_messages_endpoints.py b/tests/test_messages_endpoints.py new file mode 100644 index 000000000..ee4021784 --- /dev/null +++ b/tests/test_messages_endpoints.py @@ -0,0 +1,76 @@ +from uuid import UUID + +import pytest +from fastapi.testclient import TestClient + +from langflow.memory import add_messagetables + +# Assuming you have these imports available +from langflow.services.database.models.message import MessageCreate, MessageRead, MessageUpdate +from langflow.services.database.models.message.model import MessageTable +from langflow.services.deps import session_scope + + +@pytest.fixture() +def created_message(): + with session_scope() as session: + message = MessageCreate(text="Test message", sender="User", sender_name="User", session_id="session_id") + messagetable = MessageTable.model_validate(message, from_attributes=True) + messagetables = add_messagetables([messagetable], session) + message_read = MessageRead.model_validate(messagetables[0], from_attributes=True) + return message_read + + +@pytest.fixture() +def created_messages(session): + with session_scope() as session: + messages = [ + MessageCreate(text="Test message 1", sender="User", sender_name="User", session_id="session_id2"), + MessageCreate(text="Test message 2", sender="User", sender_name="User", session_id="session_id2"), + MessageCreate(text="Test message 3", sender="User", sender_name="User", session_id="session_id2"), + ] + messagetables = [MessageTable.model_validate(message, from_attributes=True) for message in messages] + message_list = add_messagetables(messagetables, session) + + return message_list + + +def test_delete_messages(client: TestClient, created_messages, logged_in_headers): + response = client.request( + "DELETE", "api/v1/monitor/messages", json=[str(msg.id) for msg in created_messages], headers=logged_in_headers + ) + assert response.status_code == 204, response.text + assert response.reason_phrase == "No Content" + + +def test_update_message(client: TestClient, logged_in_headers, created_message): + message_id = created_message.id + message_update = MessageUpdate(text="Updated content") + response = client.put( + f"api/v1/monitor/messages/{message_id}", json=message_update.model_dump(), headers=logged_in_headers + ) + assert response.status_code == 200, response.text + updated_message = MessageRead(**response.json()) + assert updated_message.text == "Updated content" + + +def test_update_message_not_found(client: TestClient, logged_in_headers): + non_existent_id = UUID("00000000-0000-0000-0000-000000000000") + message_update = MessageUpdate(text="Updated content") + response = client.put( + f"api/v1/monitor/messages/{non_existent_id}", json=message_update.model_dump(), headers=logged_in_headers + ) + assert response.status_code == 404, response.text + assert response.json()["detail"] == "Message not found" + + +def test_delete_messages_session(client: TestClient, created_messages, logged_in_headers): + session_id = "session_id2" + response = client.delete(f"api/v1/monitor/messages/session/{session_id}", headers=logged_in_headers) + assert response.status_code == 204 + assert response.reason_phrase == "No Content" + + assert len(created_messages) == 3 + response = client.get("api/v1/monitor/messages", headers=logged_in_headers) + assert response.status_code == 200 + assert len(response.json()) == 0 diff --git a/tests/unit/test_messages.py b/tests/unit/test_messages.py new file mode 100644 index 000000000..198387db8 --- /dev/null +++ b/tests/unit/test_messages.py @@ -0,0 +1,72 @@ +import pytest + +from langflow.memory import add_messages, add_messagetables, delete_messages, get_messages, store_message +from langflow.schema.message import Message + +# Assuming you have these imports available +from langflow.services.database.models.message import MessageCreate, MessageRead +from langflow.services.database.models.message.model import MessageTable +from langflow.services.deps import session_scope + + +@pytest.fixture() +def created_message(): + with session_scope() as session: + message = MessageCreate(text="Test message", sender="User", sender_name="User", session_id="session_id") + messagetable = MessageTable.model_validate(message, from_attributes=True) + messagetables = add_messagetables([messagetable], session) + message_read = MessageRead.model_validate(messagetables[0], from_attributes=True) + return message_read + + +@pytest.fixture() +def created_messages(session): + with session_scope() as session: + messages = [ + MessageCreate(text="Test message 1", sender="User", sender_name="User", session_id="session_id2"), + MessageCreate(text="Test message 2", sender="User", sender_name="User", session_id="session_id2"), + MessageCreate(text="Test message 3", sender="User", sender_name="User", session_id="session_id2"), + ] + messagetables = [MessageTable.model_validate(message, from_attributes=True) for message in messages] + messagetables = add_messagetables(messagetables, session) + messages_read = [ + MessageRead.model_validate(messagetable, from_attributes=True) for messagetable in messagetables + ] + return messages_read + + +def test_get_messages(session): + add_messages(Message(text="Test message 1", sender="User", sender_name="User", session_id="session_id2")) + add_messages(Message(text="Test message 2", sender="User", sender_name="User", session_id="session_id2")) + messages = get_messages(sender="User", session_id="session_id2", limit=2) + assert len(messages) == 2 + assert messages[0].text == "Test message 1" + assert messages[1].text == "Test message 2" + + +def test_add_messages(session): + message = Message(text="New Test message", sender="User", sender_name="User", session_id="new_session_id") + messages = add_messages(message) + assert len(messages) == 1 + assert messages[0].text == "New Test message" + + +def test_add_messagetables(session): + messages = [MessageTable(text="New Test message", sender="User", sender_name="User", session_id="new_session_id")] + added_messages = add_messagetables(messages, session) + assert len(added_messages) == 1 + assert added_messages[0].text == "New Test message" + + +def test_delete_messages(session): + session_id = "session_id2" + delete_messages(session_id) + messages = session.query(MessageTable).filter(MessageTable.session_id == session_id).all() + assert len(messages) == 0 + + +def test_store_message(session): + message = Message(text="Stored message", sender="User", sender_name="User", session_id="stored_session_id") + stored_messages = store_message(message) + assert len(stored_messages) == 1 + assert stored_messages[0].text == "Stored message"