Merge branch 'dev' into cz/fixTestsCI
This commit is contained in:
commit
dad0aa282f
64 changed files with 981 additions and 441 deletions
10
.github/workflows/lint-js.yml
vendored
10
.github/workflows/lint-js.yml
vendored
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
2
.github/workflows/python_test.yml
vendored
2
.github/workflows/python_test.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
53
Makefile
53
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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 ###
|
||||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
from .model import MessageTable, MessageCreate, MessageRead, MessageUpdate
|
||||
|
||||
__all__ = ["MessageTable", "MessageCreate", "MessageRead", "MessageUpdate"]
|
||||
|
|
@ -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
|
||||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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" : "",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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 => {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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 || []);
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 = () => {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)))
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
76
tests/test_messages_endpoints.py
Normal file
76
tests/test_messages_endpoints.py
Normal 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
|
||||
72
tests/unit/test_messages.py
Normal file
72
tests/unit/test_messages.py
Normal 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"
|
||||
Loading…
Add table
Add a link
Reference in a new issue