Merge branch 'dev' into cz/fixTestsCI

This commit is contained in:
cristhianzl 2024-06-26 12:06:00 -03:00
commit dad0aa282f
64 changed files with 981 additions and 441 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -1,10 +1,6 @@
import Admonition from "@theme/Admonition";
# Kubernetes
<Admonition type="warning" title="warning">
This page may contain outdated information. It will be updated as soon as possible.
</Admonition>
This guide will help you get LangFlow up and running in Kubernetes cluster, including the following steps:

View file

@ -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

View file

@ -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 ###

View file

@ -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))

View file

@ -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:

View file

@ -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")

View file

@ -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:

View file

@ -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(

View file

@ -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

View file

@ -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"]

View file

@ -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()

View file

@ -0,0 +1,3 @@
from .model import MessageTable, MessageCreate, MessageRead, MessageUpdate
__all__ = ["MessageTable", "MessageCreate", "MessageRead", "MessageUpdate"]

View file

@ -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

View file

@ -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):

View file

@ -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,

View file

@ -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"
}
}
}

View file

@ -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<any>,
setNode: (id: string, callback: (oldNode: any) => any) => void,
setIsLoading: (value: boolean) => void,
) => {
const setErrorData = useAlertStore((state) => state.setErrorData);

View file

@ -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<any>,
debouncedHandleUpdateValues: any,
setNode: (id: string, callback: (oldNode: any) => any) => void,
setIsLoading: (value: boolean) => void,
) => {
const setErrorData = useAlertStore((state) => state.setErrorData);

View file

@ -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;

View file

@ -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) => {

View file

@ -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

View file

@ -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) {

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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 = <DragCardComponent data={data} />; // 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;

View file

@ -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;

View file

@ -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<any>) {
let image: JSX.Element = <DragCardComponent data={data} />; // <== 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({
<span className="flex items-center gap-1.5 text-xs text-muted-foreground">
<IconComponent name="Heart" className={cn("h-4 w-4")} />
<span data-testid={`likes-${data.name}`}>
{likes_count ?? 0}
{likesCount ?? 0}
</span>
</span>
</ShadTooltip>
@ -275,7 +208,7 @@ export default function CollectionCardComponent({
className="h-4 w-4"
/>
<span data-testid={`downloads-${data.name}`}>
{downloads_count ?? 0}
{downloadsCount ?? 0}
</span>
</span>
</ShadTooltip>
@ -324,20 +257,7 @@ export default function CollectionCardComponent({
)}
</span>
)}
<div className="flex w-full flex-1 flex-wrap gap-2">
{/* {data.tags &&
data.tags.length > 0 &&
data.tags.map((tag, index) => (
<Badge
key={index}
variant="outline"
size="xq"
className="text-muted-foreground"
>
{tag.name}
</Badge>
))} */}
</div>
<div className="flex w-full flex-1 flex-wrap gap-2"></div>
</div>
<CardDescription className="pb-2 pt-2">
@ -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" : "",

View file

@ -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,

View file

@ -1,6 +1,9 @@
import { useEffect } from "react";
const useAutoResizeTextArea = (value, inputRef) => {
const useAutoResizeTextArea = (
value: string,
inputRef: React.RefObject<HTMLInputElement>,
) => {
useEffect(() => {
if (inputRef.current && inputRef.current.scrollHeight! !== 0) {
inputRef.current.style!.height = "inherit"; // Reset the height

View file

@ -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();

View file

@ -1,6 +1,9 @@
import { useEffect } from "react";
const useFocusOnUnlock = (lockChat, inputRef) => {
const useFocusOnUnlock = (
lockChat: boolean,
inputRef: React.RefObject<HTMLInputElement>,
) => {
useEffect(() => {
if (!lockChat && inputRef.current) {
inputRef.current.focus();

View file

@ -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<AxiosResponse<UploadFileTypeAPI>>,
currentFlowId: string,
setFiles: any,
lockChat: boolean,
) => {
const setErrorData = useAlertStore((state) => state.setErrorData);
useEffect(() => {
const handlePaste = (event: ClipboardEvent): void => {

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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 (
<Link to={editFlowLink}>
<Button
tabIndex={-1}
variant="outline"
size="sm"
className="whitespace-nowrap"
data-testid={editFlowButtonTestId}
>
<IconComponent
name="ExternalLink"
className="main-page-nav-button select-none"
/>
Edit Flow
</Button>
</Link>
);
}
return null;
};
return (
<CollectionCardComponent
is_component={type === "component"}
data={{
is_component: isComponent,
...item,
}}
disabled={isLoading}
data-testid={editFlowButtonTestId}
button={renderButton()!}
onClick={!isComponent ? handleClick : undefined}
playground={!isComponent}
control={control}
/>
);
};
export default CollectionCard;

View file

@ -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<void>,
resetFilter: () => void,
getFoldersApi: (refetch?: boolean) => Promise<void>,
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)

View file

@ -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 = {

View file

@ -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 || []);

View file

@ -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<string | undefined>,
allFlows: any[],
resetFilter: () => void,
getFoldersApi: (
refetch?: boolean,
startupApplication?: boolean,
) => Promise<void>,
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(

View file

@ -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<FlowType>,
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) => {

View file

@ -1,6 +1,11 @@
import { useCallback } from "react";
import { FlowType } from "../../../../../types/flow";
const useSelectAll = (flowsFromFolder, getValues, setValue) => {
const useSelectAll = (
flowsFromFolder: FlowType[],
getValues: () => Record<string, boolean>,
setValue: (key: string, value: boolean) => void,
) => {
const handleSelectAll = useCallback(
(select) => {
const flowsFromFolderIds = flowsFromFolder?.map((f) => f.id);

View file

@ -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",

View file

@ -1,8 +1,10 @@
import { useEffect } from "react";
const useSelectedFlows = (
entireFormValues,
setSelectedFlowsComponentsCards,
entireFormValues: Record<string, boolean> | undefined,
setSelectedFlowsComponentsCards: (
selectedFlowsComponentsCards: string[],
) => void,
) => {
useEffect(() => {
if (!entireFormValues || Object.keys(entireFormValues).length === 0) return;

View file

@ -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) => (
<FormProvider {...methods} key={item.id}>
<form>
<CollectionCardComponent
is_component={type === "component"}
data={{
is_component: item.is_component ?? false,
...item,
}}
disabled={isLoading}
data-testid={"edit-flow-button-" + item.id}
button={
!item.is_component ? (
<Link to={"/flow/" + item.id}>
<Button
tabIndex={-1}
variant="outline"
size="sm"
className="whitespace-nowrap"
data-testid={"edit-flow-button-" + item.id}
>
<IconComponent
name="ExternalLink"
className="main-page-nav-button select-none"
/>
Edit Flow
</Button>
</Link>
) : (
<></>
)
}
onClick={
!item.is_component
? () => {
navigate("/flow/" + item.id);
}
: undefined
}
playground={!item.is_component}
<CollectionCard
item={item}
type={type}
isLoading={isLoading}
control={control}
/>
</form>

View file

@ -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);

View file

@ -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<string | never>;
navigate: (url: string) => void;
is_component: boolean;
}) => {
const setSuccessData = useAlertStore((state) => state.setSuccessData);
const setErrorData = useAlertStore((state) => state.setErrorData);
const handleImportFromJSON = () => {

View file

@ -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<never[]>,
setUserId: (userId: string) => void,
) => {
const fetchApiKeys = () => {
setLoadingKeys(true);
getApiKey()

View file

@ -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)))

View file

@ -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(

View file

@ -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 });

View file

@ -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 });

View file

@ -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);

View file

@ -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) {

View file

@ -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<ColDef | ColGroupDef>) => void,
) => {
const setMessages = useMessagesStore((state) => state.setMessages);
useEffect(() => {
const fetchData = async () => {

View file

@ -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);

View file

@ -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) => {

View file

@ -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

View file

@ -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"