Merge remote-tracking branch 'origin/dev' into fix_db_location
This commit is contained in:
commit
7539ba3166
114 changed files with 6750 additions and 3275 deletions
0
src/backend/langflow/auth/__init__.py
Normal file
0
src/backend/langflow/auth/__init__.py
Normal file
177
src/backend/langflow/auth/auth.py
Normal file
177
src/backend/langflow/auth/auth.py
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
from uuid import UUID
|
||||
from typing import Annotated
|
||||
from jose import JWTError, jwt
|
||||
from sqlalchemy.orm import Session
|
||||
from passlib.context import CryptContext
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from langflow.services.utils import get_settings_manager
|
||||
|
||||
from langflow.services.utils import get_session
|
||||
from langflow.database.models.user import (
|
||||
User,
|
||||
get_user_by_id,
|
||||
get_user_by_username,
|
||||
update_user_last_login_at,
|
||||
)
|
||||
|
||||
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login")
|
||||
|
||||
|
||||
async def get_current_user(
|
||||
token: Annotated[str, Depends(oauth2_scheme)], db: Session = Depends(get_session)
|
||||
) -> User:
|
||||
settings_manager = get_settings_manager()
|
||||
|
||||
credentials_exception = HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Could not validate credentials",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
try:
|
||||
payload = jwt.decode(
|
||||
token,
|
||||
settings_manager.settings.SECRET_KEY,
|
||||
algorithms=[settings_manager.settings.ALGORITHM],
|
||||
)
|
||||
user_id: UUID = payload.get("sub") # type: ignore
|
||||
token_type: str = payload.get("type") # type: ignore
|
||||
|
||||
if user_id is None or token_type:
|
||||
raise credentials_exception
|
||||
except JWTError as e:
|
||||
raise credentials_exception from e
|
||||
|
||||
user = get_user_by_id(db, user_id) # type: ignore
|
||||
if user is None:
|
||||
raise credentials_exception
|
||||
return user
|
||||
|
||||
|
||||
async def get_current_active_user(
|
||||
current_user: Annotated[User, Depends(get_current_user)]
|
||||
):
|
||||
if not current_user.is_active:
|
||||
raise HTTPException(status_code=400, detail="Inactive user")
|
||||
return current_user
|
||||
|
||||
|
||||
def verify_password(plain_password, hashed_password):
|
||||
return pwd_context.verify(plain_password, hashed_password)
|
||||
|
||||
|
||||
def get_password_hash(password):
|
||||
return pwd_context.hash(password)
|
||||
|
||||
|
||||
def create_token(data: dict, expires_delta: timedelta):
|
||||
settings_manager = get_settings_manager()
|
||||
|
||||
to_encode = data.copy()
|
||||
expire = datetime.now(timezone.utc) + expires_delta
|
||||
to_encode["exp"] = expire
|
||||
|
||||
return jwt.encode(
|
||||
to_encode,
|
||||
settings_manager.settings.SECRET_KEY,
|
||||
algorithm=settings_manager.settings.ALGORITHM,
|
||||
)
|
||||
|
||||
|
||||
def create_user_longterm_token(
|
||||
user_id: UUID, db: Session = Depends(get_session), update_last_login: bool = False
|
||||
) -> dict:
|
||||
access_token_expires_longterm = timedelta(days=365)
|
||||
access_token = create_token(
|
||||
data={"sub": str(user_id)},
|
||||
expires_delta=access_token_expires_longterm,
|
||||
)
|
||||
|
||||
# Update: last_login_at
|
||||
if update_last_login:
|
||||
update_user_last_login_at(user_id, db)
|
||||
|
||||
return {
|
||||
"access_token": access_token,
|
||||
"refresh_token": None,
|
||||
"token_type": "bearer",
|
||||
}
|
||||
|
||||
|
||||
def create_user_tokens(
|
||||
user_id: UUID, db: Session = Depends(get_session), update_last_login: bool = False
|
||||
) -> dict:
|
||||
settings_manager = get_settings_manager()
|
||||
|
||||
access_token_expires = timedelta(
|
||||
minutes=settings_manager.settings.ACCESS_TOKEN_EXPIRE_MINUTES
|
||||
)
|
||||
access_token = create_token(
|
||||
data={"sub": str(user_id)},
|
||||
expires_delta=access_token_expires,
|
||||
)
|
||||
|
||||
refresh_token_expires = timedelta(
|
||||
minutes=settings_manager.settings.REFRESH_TOKEN_EXPIRE_MINUTES
|
||||
)
|
||||
refresh_token = create_token(
|
||||
data={"sub": str(user_id), "type": "rf"},
|
||||
expires_delta=refresh_token_expires,
|
||||
)
|
||||
|
||||
# Update: last_login_at
|
||||
if update_last_login:
|
||||
update_user_last_login_at(user_id, db)
|
||||
|
||||
return {
|
||||
"access_token": access_token,
|
||||
"refresh_token": refresh_token,
|
||||
"token_type": "bearer",
|
||||
}
|
||||
|
||||
|
||||
def create_refresh_token(refresh_token: str, db: Session = Depends(get_session)):
|
||||
settings_manager = get_settings_manager()
|
||||
|
||||
try:
|
||||
payload = jwt.decode(
|
||||
refresh_token,
|
||||
settings_manager.settings.SECRET_KEY,
|
||||
algorithms=[settings_manager.settings.ALGORITHM],
|
||||
)
|
||||
user_id: UUID = payload.get("sub") # type: ignore
|
||||
token_type: str = payload.get("type") # type: ignore
|
||||
|
||||
if user_id is None or token_type is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid refresh token"
|
||||
)
|
||||
|
||||
return create_user_tokens(user_id, db)
|
||||
|
||||
except JWTError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid refresh token",
|
||||
) from e
|
||||
|
||||
|
||||
def authenticate_user(
|
||||
username: str, password: str, db: Session = Depends(get_session)
|
||||
) -> User | None:
|
||||
user = get_user_by_username(db, username)
|
||||
|
||||
if not user:
|
||||
return None
|
||||
|
||||
if not user.is_active:
|
||||
if not user.last_login_at:
|
||||
raise HTTPException(status_code=400, detail="Waiting for approval")
|
||||
raise HTTPException(status_code=400, detail="Inactive user")
|
||||
|
||||
return user if verify_password(password, user.password) else None
|
||||
7
src/backend/langflow/database/models/token.py
Normal file
7
src/backend/langflow/database/models/token.py
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class Token(BaseModel):
|
||||
access_token: str
|
||||
refresh_token: str
|
||||
token_type: str
|
||||
94
src/backend/langflow/database/models/user.py
Normal file
94
src/backend/langflow/database/models/user.py
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
from sqlmodel import Field
|
||||
from uuid import UUID, uuid4
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional, List
|
||||
from sqlalchemy.orm import Session
|
||||
from datetime import timezone, datetime
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from fastapi import HTTPException, Depends
|
||||
|
||||
from langflow.services.utils import get_session
|
||||
from langflow.services.database.models.base import SQLModelSerializable, SQLModel
|
||||
|
||||
|
||||
class User(SQLModelSerializable, table=True):
|
||||
id: UUID = Field(default_factory=uuid4, primary_key=True, unique=True)
|
||||
username: str = Field(index=True, unique=True)
|
||||
password: str = Field()
|
||||
is_active: bool = Field(default=False)
|
||||
is_superuser: bool = Field(default=False)
|
||||
create_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
updated_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
last_login_at: Optional[datetime] = Field()
|
||||
|
||||
|
||||
class UserAddModel(SQLModel):
|
||||
username: str = Field()
|
||||
password: str = Field()
|
||||
|
||||
|
||||
class UserListModel(SQLModel):
|
||||
id: UUID = Field(default_factory=uuid4)
|
||||
username: str = Field()
|
||||
is_active: bool = Field()
|
||||
is_superuser: bool = Field()
|
||||
create_at: datetime = Field()
|
||||
updated_at: datetime = Field()
|
||||
last_login_at: Optional[datetime] = Field()
|
||||
|
||||
|
||||
class UserPatchModel(SQLModel):
|
||||
username: Optional[str] = Field()
|
||||
is_active: Optional[bool] = Field()
|
||||
is_superuser: Optional[bool] = Field()
|
||||
last_login_at: Optional[datetime] = Field()
|
||||
|
||||
|
||||
class UsersResponse(BaseModel):
|
||||
total_count: int
|
||||
users: List[UserListModel]
|
||||
|
||||
|
||||
def get_user_by_username(db: Session, username: str) -> User:
|
||||
db_user = db.query(User).filter(User.username == username).first()
|
||||
return User.from_orm(db_user) if db_user else None # type: ignore
|
||||
|
||||
|
||||
def get_user_by_id(db: Session, id: UUID) -> User:
|
||||
db_user = db.query(User).filter(User.id == id).first()
|
||||
return User.from_orm(db_user) if db_user else None # type: ignore
|
||||
|
||||
|
||||
def update_user(
|
||||
user_id: UUID, user: UserPatchModel, db: Session = Depends(get_session)
|
||||
) -> User:
|
||||
user_db = get_user_by_username(db, user.username) # type: ignore
|
||||
if user_db and user_db.id != user_id:
|
||||
raise HTTPException(status_code=409, detail="Username already exists")
|
||||
|
||||
user_db = get_user_by_id(db, user_id)
|
||||
if not user_db:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
|
||||
try:
|
||||
user_data = user.dict(exclude_unset=True)
|
||||
for key, value in user_data.items():
|
||||
setattr(user_db, key, value)
|
||||
|
||||
user_db.updated_at = datetime.now(timezone.utc)
|
||||
user_db = db.merge(user_db)
|
||||
db.commit()
|
||||
if db.identity_key(instance=user_db) is not None:
|
||||
db.refresh(user_db)
|
||||
|
||||
except IntegrityError as e:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=400, detail=str(e)) from e
|
||||
|
||||
return user_db
|
||||
|
||||
|
||||
def update_user_last_login_at(user_id: UUID, db: Session = Depends(get_session)):
|
||||
user_data = UserPatchModel(last_login_at=datetime.now(timezone.utc)) # type: ignore
|
||||
|
||||
return update_user(user_id, user_data, db)
|
||||
|
|
@ -6,6 +6,8 @@ from fastapi.responses import FileResponse
|
|||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
from langflow.api import router
|
||||
from langflow.routers import login, users, health
|
||||
|
||||
from langflow.interface.utils import setup_llm_caching
|
||||
from langflow.services.database.utils import initialize_database
|
||||
from langflow.services.manager import initialize_services
|
||||
|
|
@ -19,13 +21,7 @@ def create_app():
|
|||
|
||||
app = FastAPI()
|
||||
|
||||
origins = [
|
||||
"*",
|
||||
]
|
||||
|
||||
@app.get("/health")
|
||||
def get_health():
|
||||
return {"status": "OK"}
|
||||
origins = ["*"]
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
|
|
@ -34,6 +30,11 @@ def create_app():
|
|||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
app.include_router(login.router)
|
||||
app.include_router(users.router)
|
||||
app.include_router(health.router)
|
||||
|
||||
app.include_router(router)
|
||||
|
||||
app.on_event("startup")(initialize_services)
|
||||
|
|
|
|||
0
src/backend/langflow/routers/__init__.py
Normal file
0
src/backend/langflow/routers/__init__.py
Normal file
8
src/backend/langflow/routers/health.py
Normal file
8
src/backend/langflow/routers/health.py
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
from fastapi import APIRouter
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/health")
|
||||
def get_health():
|
||||
return {"status": "OK"}
|
||||
62
src/backend/langflow/routers/login.py
Normal file
62
src/backend/langflow/routers/login.py
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
from uuid import UUID
|
||||
from sqlalchemy.orm import Session
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
|
||||
from langflow.services.utils import get_session
|
||||
from langflow.database.models.token import Token
|
||||
from langflow.auth.auth import (
|
||||
authenticate_user,
|
||||
create_user_tokens,
|
||||
create_refresh_token,
|
||||
create_user_longterm_token,
|
||||
)
|
||||
|
||||
from langflow.services.utils import get_settings_manager
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/login", response_model=Token)
|
||||
async def login_to_get_access_token(
|
||||
form_data: OAuth2PasswordRequestForm = Depends(),
|
||||
db: Session = Depends(get_session),
|
||||
# _: Session = Depends(get_current_active_user)
|
||||
):
|
||||
if user := authenticate_user(form_data.username, form_data.password, db):
|
||||
return create_user_tokens(user_id=user.id, db=db, update_last_login=True)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Incorrect username or password",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/auto_login")
|
||||
async def auto_login(db: Session = Depends(get_session)):
|
||||
settings_manager = get_settings_manager()
|
||||
|
||||
if settings_manager.settings.AUTO_LOGIN:
|
||||
user_id = UUID("3fa85f64-5717-4562-b3fc-2c963f66afa6")
|
||||
return create_user_longterm_token(user_id, db)
|
||||
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail={
|
||||
"message": "Auto login is disabled. Please enable it in the settings",
|
||||
"auto_login": False,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@router.post("/refresh")
|
||||
async def refresh_token(token: str):
|
||||
if token:
|
||||
return create_refresh_token(token)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid refresh token",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
133
src/backend/langflow/routers/users.py
Normal file
133
src/backend/langflow/routers/users.py
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
from uuid import UUID
|
||||
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
from sqlmodel import Session, select
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
|
||||
from langflow.services.utils import get_session
|
||||
from langflow.auth.auth import get_current_active_user, get_password_hash
|
||||
from langflow.database.models.user import (
|
||||
User,
|
||||
UserAddModel,
|
||||
UserListModel,
|
||||
UserPatchModel,
|
||||
UsersResponse,
|
||||
update_user,
|
||||
)
|
||||
|
||||
router = APIRouter(tags=["Login"])
|
||||
|
||||
|
||||
@router.post("/user", response_model=UserListModel)
|
||||
def add_user(
|
||||
user: UserAddModel,
|
||||
db: Session = Depends(get_session),
|
||||
) -> User:
|
||||
"""
|
||||
Add a new user to the database.
|
||||
"""
|
||||
new_user = User(**user.dict())
|
||||
try:
|
||||
new_user.password = get_password_hash(user.password)
|
||||
|
||||
db.add(new_user)
|
||||
db.commit()
|
||||
db.refresh(new_user)
|
||||
except IntegrityError as e:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=400, detail="User exists") from e
|
||||
|
||||
return new_user
|
||||
|
||||
|
||||
@router.get("/user", response_model=UserListModel)
|
||||
def read_current_user(current_user: User = Depends(get_current_active_user)) -> User:
|
||||
"""
|
||||
Retrieve the current user's data.
|
||||
"""
|
||||
return current_user
|
||||
|
||||
|
||||
@router.get("/users", response_model=UsersResponse)
|
||||
def read_all_users(
|
||||
skip: int = 0,
|
||||
limit: int = 10,
|
||||
_: Session = Depends(get_current_active_user),
|
||||
db: Session = Depends(get_session),
|
||||
) -> UsersResponse:
|
||||
"""
|
||||
Retrieve a list of users from the database with pagination.
|
||||
"""
|
||||
query = select(User).offset(skip).limit(limit)
|
||||
users = db.execute(query).fetchall()
|
||||
|
||||
count_query = select(func.count()).select_from(User) # type: ignore
|
||||
total_count = db.execute(count_query).scalar()
|
||||
|
||||
return UsersResponse(
|
||||
total_count=total_count, # type: ignore
|
||||
users=[UserListModel(**dict(user.User)) for user in users],
|
||||
)
|
||||
|
||||
|
||||
@router.patch("/user/{user_id}", response_model=UserListModel)
|
||||
def patch_user(
|
||||
user_id: UUID,
|
||||
user: UserPatchModel,
|
||||
_: Session = Depends(get_current_active_user),
|
||||
db: Session = Depends(get_session),
|
||||
) -> User:
|
||||
"""
|
||||
Update an existing user's data.
|
||||
"""
|
||||
return update_user(user_id, user, db)
|
||||
|
||||
|
||||
@router.delete("/user/{user_id}")
|
||||
def delete_user(
|
||||
user_id: UUID,
|
||||
_: Session = Depends(get_current_active_user),
|
||||
db: Session = Depends(get_session),
|
||||
) -> dict:
|
||||
"""
|
||||
Delete a user from the database.
|
||||
"""
|
||||
user_db = db.query(User).filter(User.id == user_id).first()
|
||||
if not user_db:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
|
||||
db.delete(user_db)
|
||||
db.commit()
|
||||
|
||||
return {"detail": "User deleted"}
|
||||
|
||||
|
||||
# TODO: REMOVE - Just for testing purposes
|
||||
@router.post("/super_user", response_model=User)
|
||||
def add_super_user_for_testing_purposes_delete_me_before_merge_into_dev(
|
||||
db: Session = Depends(get_session),
|
||||
) -> User:
|
||||
"""
|
||||
Add a superuser for testing purposes.
|
||||
(This should be removed in production)
|
||||
"""
|
||||
new_user = User(
|
||||
username="superuser",
|
||||
password="12345",
|
||||
is_active=True,
|
||||
is_superuser=True,
|
||||
last_login_at=None,
|
||||
)
|
||||
|
||||
try:
|
||||
new_user.password = get_password_hash(new_user.password)
|
||||
db.add(new_user)
|
||||
db.commit()
|
||||
db.refresh(new_user)
|
||||
except IntegrityError as e:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=400, detail="User exists") from e
|
||||
|
||||
return new_user
|
||||
|
|
@ -1,11 +1,16 @@
|
|||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
from langflow.services.base import Service
|
||||
from langflow.services.utils import get_settings_manager
|
||||
from sqlmodel import SQLModel, Session, create_engine
|
||||
from langflow.utils.logger import logger
|
||||
from alembic.config import Config
|
||||
from alembic import command
|
||||
from langflow.services.database import models # noqa
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sqlalchemy.engine import Engine
|
||||
|
||||
|
||||
class DatabaseManager(Service):
|
||||
name = "database_manager"
|
||||
|
|
@ -17,7 +22,19 @@ class DatabaseManager(Service):
|
|||
langflow_dir = Path(__file__).parent.parent.parent
|
||||
self.script_location = langflow_dir / "alembic"
|
||||
self.alembic_cfg_path = langflow_dir / "alembic.ini"
|
||||
self.engine = create_engine(database_url)
|
||||
self.engine = self._create_engine()
|
||||
|
||||
def _create_engine(self) -> "Engine":
|
||||
"""Create the engine for the database."""
|
||||
settings_manager = get_settings_manager()
|
||||
if (
|
||||
settings_manager.settings.DATABASE_URL
|
||||
and settings_manager.settings.DATABASE_URL.startswith("sqlite")
|
||||
):
|
||||
connect_args = {"check_same_thread": False}
|
||||
else:
|
||||
connect_args = {}
|
||||
return create_engine(self.database_url, connect_args=connect_args)
|
||||
|
||||
def __enter__(self):
|
||||
self._session = Session(self.engine)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import contextlib
|
|||
import json
|
||||
import os
|
||||
from shutil import copy2
|
||||
import secrets
|
||||
from typing import Optional, List
|
||||
from pathlib import Path
|
||||
|
||||
|
|
@ -39,6 +40,15 @@ class Settings(BaseSettings):
|
|||
REMOVE_API_KEYS: bool = False
|
||||
COMPONENTS_PATH: List[str] = []
|
||||
|
||||
# Login settings
|
||||
SECRET_KEY: str = secrets.token_hex(32)
|
||||
ALGORITHM: str = "HS256"
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES: int = 60
|
||||
REFRESH_TOKEN_EXPIRE_MINUTES: int = 70
|
||||
# If AUTO_LOGIN = True
|
||||
# > The application does not request login and logs in automatically as a super user.
|
||||
AUTO_LOGIN: bool = True
|
||||
|
||||
@validator("CONFIG_DIR", pre=True, allow_reuse=True)
|
||||
def set_langflow_dir(cls, value):
|
||||
if not value:
|
||||
|
|
|
|||
|
|
@ -1,171 +0,0 @@
|
|||
import contextlib
|
||||
import json
|
||||
import os
|
||||
from typing import Optional, List
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
from pydantic import BaseSettings, root_validator, validator
|
||||
from langflow.utils.logger import logger
|
||||
|
||||
BASE_COMPONENTS_PATH = str(Path(__file__).parent / "components")
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
CHAINS: dict = {}
|
||||
AGENTS: dict = {}
|
||||
PROMPTS: dict = {}
|
||||
LLMS: dict = {}
|
||||
TOOLS: dict = {}
|
||||
MEMORIES: dict = {}
|
||||
EMBEDDINGS: dict = {}
|
||||
VECTORSTORES: dict = {}
|
||||
DOCUMENTLOADERS: dict = {}
|
||||
WRAPPERS: dict = {}
|
||||
RETRIEVERS: dict = {}
|
||||
TOOLKITS: dict = {}
|
||||
TEXTSPLITTERS: dict = {}
|
||||
UTILITIES: dict = {}
|
||||
OUTPUT_PARSERS: dict = {}
|
||||
CUSTOM_COMPONENTS: dict = {}
|
||||
|
||||
DEV: bool = False
|
||||
DATABASE_URL: Optional[str] = None
|
||||
CACHE: str = "InMemoryCache"
|
||||
REMOVE_API_KEYS: bool = False
|
||||
COMPONENTS_PATH: List[str] = []
|
||||
|
||||
@validator("DATABASE_URL", pre=True)
|
||||
def set_database_url(cls, value):
|
||||
if not value:
|
||||
logger.debug(
|
||||
"No database_url provided, trying LANGFLOW_DATABASE_URL env variable"
|
||||
)
|
||||
if langflow_database_url := os.getenv("LANGFLOW_DATABASE_URL"):
|
||||
value = langflow_database_url
|
||||
logger.debug("Using LANGFLOW_DATABASE_URL env variable.")
|
||||
else:
|
||||
logger.debug("No DATABASE_URL env variable, using sqlite database")
|
||||
value = "sqlite:///./langflow.db"
|
||||
|
||||
return value
|
||||
|
||||
@validator("COMPONENTS_PATH", pre=True)
|
||||
def set_components_path(cls, value):
|
||||
if os.getenv("LANGFLOW_COMPONENTS_PATH"):
|
||||
logger.debug("Adding LANGFLOW_COMPONENTS_PATH to components_path")
|
||||
langflow_component_path = os.getenv("LANGFLOW_COMPONENTS_PATH")
|
||||
if (
|
||||
Path(langflow_component_path).exists()
|
||||
and langflow_component_path not in value
|
||||
):
|
||||
if isinstance(langflow_component_path, list):
|
||||
for path in langflow_component_path:
|
||||
if path not in value:
|
||||
value.append(path)
|
||||
logger.debug(
|
||||
f"Extending {langflow_component_path} to components_path"
|
||||
)
|
||||
elif langflow_component_path not in value:
|
||||
value.append(langflow_component_path)
|
||||
logger.debug(
|
||||
f"Appending {langflow_component_path} to components_path"
|
||||
)
|
||||
|
||||
if not value:
|
||||
value = [BASE_COMPONENTS_PATH]
|
||||
logger.debug("Setting default components path to components_path")
|
||||
elif BASE_COMPONENTS_PATH not in value:
|
||||
value.append(BASE_COMPONENTS_PATH)
|
||||
logger.debug("Adding default components path to components_path")
|
||||
|
||||
logger.debug(f"Components path: {value}")
|
||||
return value
|
||||
|
||||
class Config:
|
||||
validate_assignment = True
|
||||
extra = "ignore"
|
||||
env_prefix = "LANGFLOW_"
|
||||
|
||||
@root_validator(allow_reuse=True)
|
||||
def validate_lists(cls, values):
|
||||
for key, value in values.items():
|
||||
if key != "dev" and not value:
|
||||
values[key] = []
|
||||
return values
|
||||
|
||||
def update_from_yaml(self, file_path: str, dev: bool = False):
|
||||
new_settings = load_settings_from_yaml(file_path)
|
||||
self.CHAINS = new_settings.CHAINS or {}
|
||||
self.AGENTS = new_settings.AGENTS or {}
|
||||
self.PROMPTS = new_settings.PROMPTS or {}
|
||||
self.LLMS = new_settings.LLMS or {}
|
||||
self.TOOLS = new_settings.TOOLS or {}
|
||||
self.MEMORIES = new_settings.MEMORIES or {}
|
||||
self.WRAPPERS = new_settings.WRAPPERS or {}
|
||||
self.TOOLKITS = new_settings.TOOLKITS or {}
|
||||
self.TEXTSPLITTERS = new_settings.TEXTSPLITTERS or {}
|
||||
self.UTILITIES = new_settings.UTILITIES or {}
|
||||
self.EMBEDDINGS = new_settings.EMBEDDINGS or {}
|
||||
self.VECTORSTORES = new_settings.VECTORSTORES or {}
|
||||
self.DOCUMENTLOADERS = new_settings.DOCUMENTLOADERS or {}
|
||||
self.RETRIEVERS = new_settings.RETRIEVERS or {}
|
||||
self.OUTPUT_PARSERS = new_settings.OUTPUT_PARSERS or {}
|
||||
self.CUSTOM_COMPONENTS = new_settings.CUSTOM_COMPONENTS or {}
|
||||
self.COMPONENTS_PATH = new_settings.COMPONENTS_PATH or []
|
||||
self.DEV = dev
|
||||
|
||||
def update_settings(self, **kwargs):
|
||||
logger.debug("Updating settings")
|
||||
for key, value in kwargs.items():
|
||||
# value may contain sensitive information, so we don't want to log it
|
||||
if not hasattr(self, key):
|
||||
logger.debug(f"Key {key} not found in settings")
|
||||
continue
|
||||
logger.debug(f"Updating {key}")
|
||||
if isinstance(getattr(self, key), list):
|
||||
# value might be a '[something]' string
|
||||
with contextlib.suppress(json.decoder.JSONDecodeError):
|
||||
value = json.loads(str(value))
|
||||
if isinstance(value, list):
|
||||
for item in value:
|
||||
if item not in getattr(self, key):
|
||||
getattr(self, key).append(item)
|
||||
logger.debug(f"Extended {key}")
|
||||
else:
|
||||
getattr(self, key).append(value)
|
||||
logger.debug(f"Appended {key}")
|
||||
|
||||
else:
|
||||
setattr(self, key, value)
|
||||
logger.debug(f"Updated {key}")
|
||||
logger.debug(f"{key}: {getattr(self, key)}")
|
||||
|
||||
|
||||
def save_settings_to_yaml(settings: Settings, file_path: str):
|
||||
with open(file_path, "w") as f:
|
||||
settings_dict = settings.dict()
|
||||
yaml.dump(settings_dict, f)
|
||||
|
||||
|
||||
def load_settings_from_yaml(file_path: str) -> Settings:
|
||||
# Check if a string is a valid path or a file name
|
||||
if "/" not in file_path:
|
||||
# Get current path
|
||||
current_path = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
file_path = os.path.join(current_path, file_path)
|
||||
|
||||
with open(file_path, "r") as f:
|
||||
settings_dict = yaml.safe_load(f)
|
||||
settings_dict = {k.upper(): v for k, v in settings_dict.items()}
|
||||
|
||||
for key in settings_dict:
|
||||
if key not in Settings.__fields__.keys():
|
||||
raise KeyError(f"Key {key} not found in settings")
|
||||
logger.debug(f"Loading {len(settings_dict[key])} {key} from {file_path}")
|
||||
|
||||
return Settings(**settings_dict)
|
||||
|
||||
|
||||
settings = load_settings_from_yaml("config.yaml")
|
||||
5225
src/frontend/package-lock.json
generated
5225
src/frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -12,11 +12,13 @@
|
|||
"@radix-ui/react-checkbox": "^1.0.4",
|
||||
"@radix-ui/react-dialog": "^1.0.4",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.5",
|
||||
"@radix-ui/react-form": "^0.0.3",
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
"@radix-ui/react-label": "^2.0.2",
|
||||
"@radix-ui/react-menubar": "^1.0.3",
|
||||
"@radix-ui/react-popover": "^1.0.6",
|
||||
"@radix-ui/react-progress": "^1.0.3",
|
||||
"@radix-ui/react-select": "^1.2.2",
|
||||
"@radix-ui/react-separator": "^1.0.3",
|
||||
"@radix-ui/react-slot": "^1.0.2",
|
||||
"@radix-ui/react-switch": "^1.0.3",
|
||||
|
|
@ -69,7 +71,8 @@
|
|||
"start": "vite",
|
||||
"build": "vite build",
|
||||
"serve": "vite preview",
|
||||
"format": "npx prettier --write \"**/*.{js,jsx,ts,tsx,json,md}\""
|
||||
"format": "npx prettier --write \"**/*.{js,jsx,ts,tsx,json,md}\"",
|
||||
"type-check": "tsc --noEmit --pretty --project tsconfig.json && vite"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
|
|
|
|||
|
|
@ -46,6 +46,10 @@ export default function App() {
|
|||
}>
|
||||
>([]);
|
||||
|
||||
const isLoginPage = location.pathname.includes("login");
|
||||
const isAdminPage = location.pathname.includes("admin");
|
||||
const isSignUpPage = location.pathname.includes("signup");
|
||||
|
||||
// Use effect hook to update alertsList when a new alert is added
|
||||
useEffect(() => {
|
||||
// If there is an error alert open with data, add it to the alertsList
|
||||
|
|
@ -133,7 +137,7 @@ export default function App() {
|
|||
}}
|
||||
FallbackComponent={CrashErrorComponent}
|
||||
>
|
||||
<Header />
|
||||
{!isLoginPage && !isSignUpPage && <Header />}
|
||||
<Router />
|
||||
</ErrorBoundary>
|
||||
<div></div>
|
||||
|
|
|
|||
|
|
@ -111,6 +111,7 @@ export default function ParameterComponent({
|
|||
let groupedObj = groupByFamily(myData, tooltipTitle!, left, flow!);
|
||||
|
||||
if (groupedObj && groupedObj.length > 0) {
|
||||
//@ts-ignore
|
||||
//@ts-ignore
|
||||
refHtml.current = groupedObj.map((item, index) => {
|
||||
const Icon: any =
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { useSSE } from "../../contexts/SSEContext";
|
|||
import { TabsContext } from "../../contexts/tabsContext";
|
||||
import { typesContext } from "../../contexts/typesContext";
|
||||
import NodeToolbarComponent from "../../pages/FlowPage/components/nodeToolbarComponent";
|
||||
import { validationStatusType } from "../../types/components";
|
||||
import { NodeDataType } from "../../types/flow";
|
||||
import { cleanEdges } from "../../utils/reactflowUtils";
|
||||
import { nodeColors, nodeIconsLucide } from "../../utils/styleUtils";
|
||||
|
|
@ -20,13 +21,14 @@ export default function GenericNode({
|
|||
}: {
|
||||
data: NodeDataType;
|
||||
selected: boolean;
|
||||
}) {
|
||||
}): JSX.Element {
|
||||
const [data, setData] = useState(olddata);
|
||||
const { updateFlow, flows, tabId } = useContext(TabsContext);
|
||||
const updateNodeInternals = useUpdateNodeInternals();
|
||||
const { types, deleteNode, reactFlowInstance } = useContext(typesContext);
|
||||
const name = nodeIconsLucide[data.type] ? data.type : types[data.type];
|
||||
const [validationStatus, setValidationStatus] = useState(null);
|
||||
const [validationStatus, setValidationStatus] =
|
||||
useState<validationStatusType | null>(null);
|
||||
// State for outline color
|
||||
const { sseData, isBuilding } = useSSE();
|
||||
useEffect(() => {
|
||||
|
|
@ -41,7 +43,7 @@ export default function GenericNode({
|
|||
nodes: flow.data.nodes,
|
||||
},
|
||||
updateEdge: (edge) => {
|
||||
flow.data.edges = edge;
|
||||
flow.data!.edges = edge;
|
||||
reactFlowInstance.setEdges(edge);
|
||||
updateNodeInternals(data.id);
|
||||
},
|
||||
|
|
@ -77,7 +79,7 @@ export default function GenericNode({
|
|||
"generic-node-div"
|
||||
)}
|
||||
>
|
||||
{data.node.beta && (
|
||||
{data.node?.beta && (
|
||||
<div className="beta-badge-wrapper">
|
||||
<div className="beta-badge-content">BETA</div>
|
||||
</div>
|
||||
|
|
@ -90,9 +92,9 @@ export default function GenericNode({
|
|||
iconColor={`${nodeColors[types[data.type]]}`}
|
||||
/>
|
||||
<div className="generic-node-tooltip-div">
|
||||
<ShadTooltip content={data.node.display_name}>
|
||||
<ShadTooltip content={data.node?.display_name}>
|
||||
<div className="generic-node-tooltip-div text-primary">
|
||||
{data.node.display_name}
|
||||
{data.node?.display_name}
|
||||
</div>
|
||||
</ShadTooltip>
|
||||
</div>
|
||||
|
|
@ -117,7 +119,9 @@ export default function GenericNode({
|
|||
{typeof validationStatus.params === "string"
|
||||
? validationStatus.params
|
||||
.split("\n")
|
||||
.map((line, index) => <div key={index}>{line}</div>)
|
||||
.map((line: string, index: number) => (
|
||||
<div key={index}>{line}</div>
|
||||
))
|
||||
: ""}
|
||||
</div>
|
||||
)
|
||||
|
|
@ -155,20 +159,20 @@ export default function GenericNode({
|
|||
</div>
|
||||
|
||||
<div className="generic-node-desc">
|
||||
<div className="generic-node-desc-text">{data.node.description}</div>
|
||||
<div className="generic-node-desc-text">{data.node?.description}</div>
|
||||
|
||||
<>
|
||||
{Object.keys(data.node.template)
|
||||
{Object.keys(data.node!.template)
|
||||
.filter((templateField) => templateField.charAt(0) !== "_")
|
||||
.map((templateField: string, idx) => (
|
||||
<div key={idx}>
|
||||
{data.node.template[templateField].show &&
|
||||
!data.node.template[templateField].advanced ? (
|
||||
{data.node!.template[templateField].show &&
|
||||
!data.node!.template[templateField].advanced ? (
|
||||
<ParameterComponent
|
||||
key={
|
||||
(data.node.template[templateField].input_types?.join(
|
||||
(data.node!.template[templateField].input_types?.join(
|
||||
";"
|
||||
) ?? data.node.template[templateField].type) +
|
||||
) ?? data.node!.template[templateField].type) +
|
||||
"|" +
|
||||
templateField +
|
||||
"|" +
|
||||
|
|
@ -178,39 +182,39 @@ export default function GenericNode({
|
|||
setData={setData}
|
||||
color={
|
||||
nodeColors[
|
||||
types[data.node.template[templateField].type]
|
||||
types[data.node?.template[templateField].type!]
|
||||
] ??
|
||||
nodeColors[data.node.template[templateField].type] ??
|
||||
nodeColors[data.node?.template[templateField].type!] ??
|
||||
nodeColors.unknown
|
||||
}
|
||||
title={
|
||||
data.node.template[templateField].display_name
|
||||
data.node?.template[templateField].display_name
|
||||
? data.node.template[templateField].display_name
|
||||
: data.node.template[templateField].name
|
||||
: data.node?.template[templateField].name
|
||||
? toTitleCase(data.node.template[templateField].name)
|
||||
: toTitleCase(templateField)
|
||||
}
|
||||
info={data.node.template[templateField].info}
|
||||
info={data.node?.template[templateField].info}
|
||||
name={templateField}
|
||||
tooltipTitle={
|
||||
data.node.template[templateField].input_types?.join(
|
||||
data.node?.template[templateField].input_types?.join(
|
||||
"\n"
|
||||
) ?? data.node.template[templateField].type
|
||||
) ?? data.node?.template[templateField].type
|
||||
}
|
||||
required={data.node.template[templateField].required}
|
||||
required={data.node?.template[templateField].required}
|
||||
id={
|
||||
(data.node.template[templateField].input_types?.join(
|
||||
(data.node?.template[templateField].input_types?.join(
|
||||
";"
|
||||
) ?? data.node.template[templateField].type) +
|
||||
) ?? data.node?.template[templateField].type) +
|
||||
"|" +
|
||||
templateField +
|
||||
"|" +
|
||||
data.id
|
||||
}
|
||||
left={true}
|
||||
type={data.node.template[templateField].type}
|
||||
type={data.node?.template[templateField].type}
|
||||
optionalHandle={
|
||||
data.node.template[templateField].input_types
|
||||
data.node?.template[templateField].input_types
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
|
|
@ -220,25 +224,25 @@ export default function GenericNode({
|
|||
))}
|
||||
<div
|
||||
className={classNames(
|
||||
Object.keys(data.node.template).length < 1 ? "hidden" : "",
|
||||
Object.keys(data.node!.template).length < 1 ? "hidden" : "",
|
||||
"flex-max-width justify-center"
|
||||
)}
|
||||
>
|
||||
{" "}
|
||||
</div>
|
||||
<ParameterComponent
|
||||
key={[data.type, data.id, ...data.node.base_classes].join("|")}
|
||||
key={[data.type, data.id, ...data.node!.base_classes].join("|")}
|
||||
data={data}
|
||||
setData={setData}
|
||||
color={nodeColors[types[data.type]] ?? nodeColors.unknown}
|
||||
title={
|
||||
data.node.output_types && data.node.output_types.length > 0
|
||||
data.node?.output_types && data.node.output_types.length > 0
|
||||
? data.node.output_types.join("|")
|
||||
: data.type
|
||||
}
|
||||
tooltipTitle={data.node.base_classes.join("\n")}
|
||||
id={[data.type, data.id, ...data.node.base_classes].join("|")}
|
||||
type={data.node.base_classes.join("|")}
|
||||
tooltipTitle={data.node?.base_classes.join("\n")}
|
||||
id={[data.type, data.id, ...data.node!.base_classes].join("|")}
|
||||
type={data.node?.base_classes.join("|")}
|
||||
left={false}
|
||||
/>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { SingleAlertComponentType } from "../../../../types/alerts";
|
|||
export default function SingleAlert({
|
||||
dropItem,
|
||||
removeAlert,
|
||||
}: SingleAlertComponentType) {
|
||||
}: SingleAlertComponentType): JSX.Element {
|
||||
const [show, setShow] = useState(true);
|
||||
const type = dropItem.type;
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,9 @@ import { alertContext } from "../../contexts/alertContext";
|
|||
import { AlertDropdownType } from "../../types/alerts";
|
||||
import SingleAlert from "./components/singleAlertComponent";
|
||||
|
||||
export default function AlertDropdown({ children }: AlertDropdownType) {
|
||||
export default function AlertDropdown({
|
||||
children,
|
||||
}: AlertDropdownType): JSX.Element {
|
||||
const {
|
||||
notificationList,
|
||||
clearNotificationList,
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ export default function ErrorAlert({
|
|||
list = [],
|
||||
id,
|
||||
removeAlert,
|
||||
}: ErrorAlertType) {
|
||||
}: ErrorAlertType): JSX.Element {
|
||||
const [show, setShow] = useState(true);
|
||||
useEffect(() => {
|
||||
if (show) {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { useEffect } from "react";
|
|||
|
||||
export function useOnClickOutside(ref, handler) {
|
||||
useEffect(() => {
|
||||
const listener = (event) => {
|
||||
const listener = (event: Event) => {
|
||||
// Do nothing if clicking ref's element or its children
|
||||
if (!ref.current || ref.current.contains(event.target)) {
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ export default function NoticeAlert({
|
|||
link = "",
|
||||
id,
|
||||
removeAlert,
|
||||
}: NoticeAlertType) {
|
||||
}: NoticeAlertType): JSX.Element {
|
||||
const [show, setShow] = useState(true);
|
||||
useEffect(() => {
|
||||
if (show) {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ export default function SuccessAlert({
|
|||
title,
|
||||
id,
|
||||
removeAlert,
|
||||
}: SuccessAlertType) {
|
||||
}: SuccessAlertType): JSX.Element {
|
||||
const [show, setShow] = useState(true);
|
||||
useEffect(() => {
|
||||
if (show) {
|
||||
|
|
|
|||
|
|
@ -12,12 +12,12 @@ export default function AccordionComponent({
|
|||
children,
|
||||
open = [],
|
||||
keyValue,
|
||||
}: AccordionComponentType) {
|
||||
}: AccordionComponentType): JSX.Element {
|
||||
const [value, setValue] = useState(
|
||||
open.length === 0 ? "" : getOpenAccordion()
|
||||
);
|
||||
|
||||
function getOpenAccordion() {
|
||||
function getOpenAccordion(): string {
|
||||
let value = "";
|
||||
open.forEach((el) => {
|
||||
if (el == trigger) {
|
||||
|
|
@ -28,8 +28,8 @@ export default function AccordionComponent({
|
|||
return value;
|
||||
}
|
||||
|
||||
function handleClick() {
|
||||
value === "" ? setValue(keyValue) : setValue("");
|
||||
function handleClick(): void {
|
||||
value === "" ? setValue(keyValue!) : setValue("");
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -40,7 +40,7 @@ export default function AccordionComponent({
|
|||
value={value}
|
||||
onValueChange={setValue}
|
||||
>
|
||||
<AccordionItem value={keyValue} className="border-b">
|
||||
<AccordionItem value={keyValue!} className="border-b">
|
||||
<AccordionTrigger
|
||||
onClick={() => {
|
||||
handleClick();
|
||||
|
|
|
|||
|
|
@ -1,4 +1,9 @@
|
|||
export default function CrashErrorComponent({ error, resetErrorBoundary }) {
|
||||
import { crashComponentPropsType } from "../../types/components";
|
||||
|
||||
export default function CrashErrorComponent({
|
||||
error,
|
||||
resetErrorBoundary,
|
||||
}: crashComponentPropsType): JSX.Element {
|
||||
return (
|
||||
<div className="fixed left-0 top-0 z-50 flex h-full w-full items-center justify-center bg-foreground bg-opacity-50">
|
||||
<div className="flex h-1/3 min-h-fit max-w-4xl flex-col justify-evenly rounded-lg bg-background p-8 text-start shadow-lg">
|
||||
|
|
|
|||
|
|
@ -3,19 +3,8 @@ import { Input } from "../../components/ui/input";
|
|||
import { Label } from "../../components/ui/label";
|
||||
import { Textarea } from "../../components/ui/textarea";
|
||||
import { readFlowsFromDatabase } from "../../controllers/API";
|
||||
|
||||
type InputProps = {
|
||||
name: string | null;
|
||||
description: string | null;
|
||||
maxLength?: number;
|
||||
flows: Array<{ id: string; name: string; description: string }>;
|
||||
tabId: string;
|
||||
invalidName: boolean;
|
||||
setInvalidName: (invalidName: boolean) => void;
|
||||
setName: (name: string) => void;
|
||||
setDescription: (description: string) => void;
|
||||
updateFlow: (flow: { id: string; name: string }) => void;
|
||||
};
|
||||
import { InputProps } from "../../types/components";
|
||||
import { FlowType } from "../../types/flow";
|
||||
|
||||
export const EditFlowSettings: React.FC<InputProps> = ({
|
||||
name,
|
||||
|
|
@ -27,13 +16,12 @@ export const EditFlowSettings: React.FC<InputProps> = ({
|
|||
tabId,
|
||||
setName,
|
||||
setDescription,
|
||||
updateFlow,
|
||||
}) => {
|
||||
}: InputProps): JSX.Element => {
|
||||
const [isMaxLength, setIsMaxLength] = useState(false);
|
||||
const nameLists = useRef([]);
|
||||
const nameLists = useRef<string[]>([]);
|
||||
useEffect(() => {
|
||||
readFlowsFromDatabase().then((flows) => {
|
||||
flows.forEach((flow) => {
|
||||
flows.forEach((flow: FlowType) => {
|
||||
nameLists.current.push(flow.name);
|
||||
});
|
||||
});
|
||||
|
|
@ -47,20 +35,20 @@ export const EditFlowSettings: React.FC<InputProps> = ({
|
|||
setIsMaxLength(false);
|
||||
}
|
||||
if (!nameLists.current.includes(value)) {
|
||||
setInvalidName(false);
|
||||
setInvalidName!(false);
|
||||
} else {
|
||||
setInvalidName(true);
|
||||
setInvalidName!(true);
|
||||
}
|
||||
setName(value);
|
||||
};
|
||||
|
||||
const [desc, setDesc] = useState(
|
||||
flows.find((flow) => flow.id === tabId).description
|
||||
flows.find((flow) => flow.id === tabId)?.description
|
||||
);
|
||||
|
||||
const handleDescriptionChange = (event: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
flows.find((flow) => flow.id === tabId).description = event.target.value;
|
||||
setDesc(flows.find((flow) => flow.id === tabId).description);
|
||||
flows.find((flow) => flow.id === tabId)!.description = event.target.value;
|
||||
setDesc(flows.find((flow) => flow.id === tabId)?.description);
|
||||
setDescription(event.target.value);
|
||||
};
|
||||
|
||||
|
|
|
|||
118
src/frontend/src/components/PaginatorComponent/index.tsx
Normal file
118
src/frontend/src/components/PaginatorComponent/index.tsx
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
import { useState } from "react";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "../../components/ui/select";
|
||||
import { PaginatorComponentType } from "../../types/components";
|
||||
import IconComponent from "../genericIconComponent";
|
||||
import { Button } from "../ui/button";
|
||||
|
||||
export default function PaginatorComponent({
|
||||
pageSize = 10,
|
||||
pageIndex = 1,
|
||||
rowsCount = [10, 20, 30],
|
||||
totalRowsCount = 0,
|
||||
paginate,
|
||||
}: PaginatorComponentType) {
|
||||
const [size, setPageSize] = useState(pageSize);
|
||||
const [index, setPageIndex] = useState(pageIndex);
|
||||
|
||||
const [maxIndex, setMaxPageIndex] = useState(
|
||||
Math.ceil(totalRowsCount / pageSize)
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center justify-between px-2">
|
||||
<div className="flex-1 text-sm text-muted-foreground"></div>
|
||||
<div className="flex items-center space-x-6 lg:space-x-8">
|
||||
<div className="flex items-center space-x-2">
|
||||
<p className="text-sm font-medium">Rows per page</p>
|
||||
<Select
|
||||
onValueChange={(pageSize: string) => {
|
||||
setPageSize(Number(pageSize));
|
||||
setMaxPageIndex(Math.ceil(totalRowsCount / Number(pageSize)));
|
||||
paginate(Number(pageSize), index);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-[100px]">
|
||||
<SelectValue placeholder="10" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{rowsCount.map((item, i) => (
|
||||
<SelectItem key={i} value={item.toString()}>
|
||||
{item}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex w-[100px] items-center justify-center text-sm font-medium">
|
||||
Page {index} of {maxIndex}
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="hidden h-8 w-8 p-0 lg:flex"
|
||||
onClick={() => {
|
||||
setPageIndex(1);
|
||||
paginate(size, 1);
|
||||
}}
|
||||
>
|
||||
<span className="sr-only">Go to first page</span>
|
||||
<IconComponent name="ChevronsLeft" className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (index <= 1) {
|
||||
setPageIndex(1);
|
||||
paginate(size, 1);
|
||||
} else {
|
||||
{
|
||||
setPageIndex(index - 1);
|
||||
paginate(size, index - 1);
|
||||
}
|
||||
}
|
||||
}}
|
||||
variant="outline"
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<span className="sr-only">Go to previous page</span>
|
||||
<IconComponent name="ChevronLeft" className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (index >= maxIndex) {
|
||||
setPageIndex(maxIndex);
|
||||
paginate(size, maxIndex);
|
||||
} else {
|
||||
setPageIndex(index + 1);
|
||||
paginate(size, index + 1);
|
||||
}
|
||||
}}
|
||||
variant="outline"
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<span className="sr-only">Go to next page</span>
|
||||
<IconComponent name="ChevronRight" className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="hidden h-8 w-8 p-0 lg:flex"
|
||||
onClick={() => {
|
||||
setPageIndex(maxIndex);
|
||||
paginate(size, maxIndex);
|
||||
}}
|
||||
>
|
||||
<span className="sr-only">Go to last page</span>
|
||||
<IconComponent name="ChevronsRight" className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -3,16 +3,16 @@ import { RadialProgressType } from "../../types/components";
|
|||
export default function RadialProgressComponent({
|
||||
value,
|
||||
color,
|
||||
}: RadialProgressType) {
|
||||
}: RadialProgressType): JSX.Element {
|
||||
const style = {
|
||||
"--value": value * 100,
|
||||
"--value": value! * 100,
|
||||
"--size": "1.5rem",
|
||||
"--thickness": "2px",
|
||||
} as React.CSSProperties;
|
||||
|
||||
return (
|
||||
<div className={"radial-progress " + color} style={style}>
|
||||
<strong className="text-[8px]">{Math.trunc(value * 100)}%</strong>
|
||||
<strong className="text-[8px]">{Math.trunc(value! * 100)}%</strong>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,20 +3,9 @@ import type { FC } from "react";
|
|||
import React from "react";
|
||||
import { Tooltip as ReactTooltip } from "react-tooltip";
|
||||
import "react-tooltip/dist/react-tooltip.css";
|
||||
import { TooltipProps } from "../../types/components";
|
||||
import { classNames } from "../../utils/utils";
|
||||
|
||||
type TooltipProps = {
|
||||
selector: string;
|
||||
content?: string;
|
||||
disabled?: boolean;
|
||||
htmlContent?: React.ReactNode;
|
||||
className?: string; // This should use !impornant to override the default styles eg: '!bg-white'
|
||||
position?: "top" | "right" | "bottom" | "left";
|
||||
clickable?: boolean;
|
||||
children: React.ReactNode;
|
||||
delayShow?: number;
|
||||
};
|
||||
|
||||
const TooltipReact: FC<TooltipProps> = ({
|
||||
selector,
|
||||
content,
|
||||
|
|
@ -27,7 +16,7 @@ const TooltipReact: FC<TooltipProps> = ({
|
|||
className,
|
||||
clickable,
|
||||
delayShow,
|
||||
}) => {
|
||||
}: TooltipProps): JSX.Element => {
|
||||
return (
|
||||
<div className="tooltip-container">
|
||||
{React.cloneElement(children as React.ReactElement, {
|
||||
|
|
@ -38,7 +27,7 @@ const TooltipReact: FC<TooltipProps> = ({
|
|||
content={content}
|
||||
className={classNames(
|
||||
"z-[9999] !bg-white !text-xs !font-normal !text-foreground !opacity-100 !shadow-md",
|
||||
className
|
||||
className!
|
||||
)}
|
||||
place={position}
|
||||
clickable={clickable}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import DOMPurify from "dompurify";
|
||||
import { SanitizedHTMLWrapperType } from "../../types/components";
|
||||
|
||||
const SanitizedHTMLWrapper = ({
|
||||
className,
|
||||
content,
|
||||
onClick,
|
||||
suppressWarning = false,
|
||||
}) => {
|
||||
}: SanitizedHTMLWrapperType): JSX.Element => {
|
||||
const sanitizedHTML = DOMPurify.sanitize(content);
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ export default function ShadTooltip({
|
|||
children,
|
||||
styleClasses,
|
||||
delayDuration = 500,
|
||||
}: ShadToolTipType) {
|
||||
}: ShadToolTipType): JSX.Element {
|
||||
return (
|
||||
<Tooltip delayDuration={delayDuration}>
|
||||
<TooltipTrigger asChild={asChild}>{children}</TooltipTrigger>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ export default function Tooltip({
|
|||
children,
|
||||
title,
|
||||
placement,
|
||||
}: TooltipComponentType) {
|
||||
}: TooltipComponentType): JSX.Element {
|
||||
return (
|
||||
<LightTooltip placement={placement} title={title} arrow>
|
||||
{children}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useContext } from "react";
|
||||
import { TabsContext } from "../../contexts/tabsContext";
|
||||
import { FlowType } from "../../types/flow";
|
||||
import { cardComponentPropsType } from "../../types/components";
|
||||
import { gradients } from "../../utils/styleUtils";
|
||||
import IconComponent from "../genericIconComponent";
|
||||
import {
|
||||
|
|
@ -16,12 +16,7 @@ export const CardComponent = ({
|
|||
id,
|
||||
onDelete,
|
||||
button,
|
||||
}: {
|
||||
flow: FlowType;
|
||||
id: string;
|
||||
onDelete?: () => void;
|
||||
button?: JSX.Element;
|
||||
}) => {
|
||||
}: cardComponentPropsType): JSX.Element => {
|
||||
const { removeFlow } = useContext(TabsContext);
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ import { postBuildInit } from "../../../controllers/API";
|
|||
import { FlowType } from "../../../types/flow";
|
||||
|
||||
import { TabsContext } from "../../../contexts/tabsContext";
|
||||
import { parsedDataType } from "../../../types/components";
|
||||
import { TabsState } from "../../../types/tabs";
|
||||
import { validateNodes } from "../../../utils/reactflowUtils";
|
||||
import RadialProgressComponent from "../../RadialProgress";
|
||||
import IconComponent from "../../genericIconComponent";
|
||||
|
|
@ -21,7 +23,7 @@ export default function BuildTrigger({
|
|||
flow: FlowType;
|
||||
setIsBuilt: any;
|
||||
isBuilt: boolean;
|
||||
}) {
|
||||
}): JSX.Element {
|
||||
const { updateSSEData, isBuilding, setIsBuilding, sseData } = useSSE();
|
||||
const { reactFlowInstance } = useContext(typesContext);
|
||||
const { setTabsState } = useContext(TabsContext);
|
||||
|
|
@ -30,12 +32,12 @@ export default function BuildTrigger({
|
|||
const eventClick = isBuilding ? "pointer-events-none" : "";
|
||||
const [progress, setProgress] = useState(0);
|
||||
|
||||
async function handleBuild(flow: FlowType) {
|
||||
async function handleBuild(flow: FlowType): Promise<void> {
|
||||
try {
|
||||
if (isBuilding) {
|
||||
return;
|
||||
}
|
||||
const errors = validateNodes(reactFlowInstance);
|
||||
const errors = validateNodes(reactFlowInstance!);
|
||||
if (errors.length > 0) {
|
||||
setErrorData({
|
||||
title: "Oops! Looks like you missed something",
|
||||
|
|
@ -69,7 +71,7 @@ export default function BuildTrigger({
|
|||
const response = await postBuildInit(flow);
|
||||
const { flowId } = response.data;
|
||||
// Step 2: Use the session ID to establish an SSE connection using EventSource
|
||||
let validationResults = [];
|
||||
let validationResults: boolean[] = [];
|
||||
let finished = false;
|
||||
const apiUrl = `/api/v1/build/stream/${flowId}`;
|
||||
const eventSource = new EventSource(apiUrl);
|
||||
|
|
@ -91,7 +93,8 @@ export default function BuildTrigger({
|
|||
// If the event is a log, log it
|
||||
setSuccessData({ title: parsedData.log });
|
||||
} else if (parsedData.input_keys !== undefined) {
|
||||
setTabsState((old) => {
|
||||
//@ts-ignore
|
||||
setTabsState((old: TabsState) => {
|
||||
return {
|
||||
...old,
|
||||
[flowId]: {
|
||||
|
|
@ -120,13 +123,13 @@ export default function BuildTrigger({
|
|||
// Step 3: Wait for the stream to finish
|
||||
while (!finished) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
finished = validationResults.length === flow.data.nodes.length;
|
||||
finished = validationResults.length === flow.data!.nodes.length;
|
||||
}
|
||||
// Step 4: Return true if all nodes are valid, false otherwise
|
||||
return validationResults.every((result) => result);
|
||||
}
|
||||
|
||||
function processStreamResult(parsedData) {
|
||||
function processStreamResult(parsedData: parsedDataType) {
|
||||
// Process each chunk of data here
|
||||
// Parse the chunk and update the context
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -8,12 +8,18 @@ import {
|
|||
FLOW_NOT_BUILT_TITLE,
|
||||
} from "../../../constants/constants";
|
||||
import { alertContext } from "../../../contexts/alertContext";
|
||||
import { chatTriggerPropType } from "../../../types/components";
|
||||
import IconComponent from "../../genericIconComponent";
|
||||
|
||||
export default function ChatTrigger({ open, setOpen, isBuilt, canOpen }) {
|
||||
export default function ChatTrigger({
|
||||
open,
|
||||
setOpen,
|
||||
isBuilt,
|
||||
canOpen,
|
||||
}: chatTriggerPropType): JSX.Element {
|
||||
const { setErrorData } = useContext(alertContext);
|
||||
|
||||
function handleClick() {
|
||||
function handleClick(): void {
|
||||
if (isBuilt) {
|
||||
if (canOpen) {
|
||||
setOpen(true);
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import { getBuildStatus } from "../../controllers/API";
|
|||
import FormModal from "../../modals/formModal";
|
||||
import { NodeType } from "../../types/flow";
|
||||
|
||||
export default function Chat({ flow }: ChatType) {
|
||||
export default function Chat({ flow }: ChatType): JSX.Element {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [canOpen, setCanOpen] = useState(false);
|
||||
const { tabsState, isBuilt, setIsBuilt } = useContext(TabsContext);
|
||||
|
|
@ -44,11 +44,11 @@ export default function Chat({ flow }: ChatType) {
|
|||
}, [flow]);
|
||||
|
||||
const prevNodesRef = useRef<any[] | undefined>();
|
||||
const nodes = useNodes();
|
||||
const nodes: NodeType[] = useNodes();
|
||||
useEffect(() => {
|
||||
const prevNodes = prevNodesRef.current;
|
||||
const currentNodes = nodes.map((node: NodeType) =>
|
||||
_.cloneDeep(node.data.node.template)
|
||||
_.cloneDeep(node.data.node?.template)
|
||||
);
|
||||
if (
|
||||
tabsState &&
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ export default function CodeAreaComponent({
|
|||
dynamic={dynamic}
|
||||
value={myValue}
|
||||
nodeClass={nodeClass}
|
||||
setNodeClass={setNodeClass}
|
||||
setNodeClass={setNodeClass!}
|
||||
setValue={(value: string) => {
|
||||
setMyValue(value);
|
||||
onChange(value);
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ import {
|
|||
TabsTrigger,
|
||||
} from "../../components/ui/tabs";
|
||||
import { darkContext } from "../../contexts/darkContext";
|
||||
import { FlowType } from "../../types/flow/index";
|
||||
import { codeTabsPropsType } from "../../types/components";
|
||||
import { classNames } from "../../utils/utils";
|
||||
import IconComponent from "../genericIconComponent";
|
||||
|
||||
|
|
@ -40,28 +40,15 @@ export default function CodeTabsComponent({
|
|||
setActiveTab,
|
||||
isMessage,
|
||||
tweaks,
|
||||
}: {
|
||||
flow?: FlowType;
|
||||
tabs: any;
|
||||
activeTab: string;
|
||||
setActiveTab: any;
|
||||
isMessage?: boolean;
|
||||
tweaks?: {
|
||||
tweak?: any;
|
||||
tweaksList?: any;
|
||||
buildContent?: any;
|
||||
getValue?: any;
|
||||
buildTweakObject?: any;
|
||||
};
|
||||
}) {
|
||||
}: codeTabsPropsType) {
|
||||
const [isCopied, setIsCopied] = useState<Boolean>(false);
|
||||
const [data, setData] = useState(flow ? flow["data"]["nodes"] : null);
|
||||
const [openAccordion, setOpenAccordion] = useState([]);
|
||||
const [data, setData] = useState(flow ? flow["data"]!["nodes"] : null);
|
||||
const [openAccordion, setOpenAccordion] = useState<string[]>([]);
|
||||
const { dark } = useContext(darkContext);
|
||||
|
||||
useEffect(() => {
|
||||
if (flow && flow["data"]["nodes"]) {
|
||||
setData(flow["data"]["nodes"]);
|
||||
if (flow && flow["data"]!["nodes"]) {
|
||||
setData(flow["data"]!["nodes"]);
|
||||
}
|
||||
}, [flow]);
|
||||
|
||||
|
|
@ -102,8 +89,8 @@ export default function CodeTabsComponent({
|
|||
};
|
||||
|
||||
function openAccordions() {
|
||||
let accordionsToOpen = [];
|
||||
tweaks.tweak.current.forEach((el) => {
|
||||
let accordionsToOpen: string[] = [];
|
||||
tweaks?.tweak!.current.forEach((el) => {
|
||||
Object.keys(el).forEach((key) => {
|
||||
if (Object.keys(el[key]).length > 0) {
|
||||
accordionsToOpen.push(key);
|
||||
|
|
@ -205,9 +192,9 @@ export default function CodeTabsComponent({
|
|||
: "overflow-hidden"
|
||||
)}
|
||||
>
|
||||
{data.map((node: any, index) => (
|
||||
{data?.map((node: any, index) => (
|
||||
<div className="px-3" key={index}>
|
||||
{tweaks.tweaksList.current.includes(
|
||||
{tweaks?.tweaksList!.current.includes(
|
||||
node["data"]["id"]
|
||||
) && (
|
||||
<AccordionComponent
|
||||
|
|
@ -290,14 +277,14 @@ export default function CodeTabsComponent({
|
|||
setData((old) => {
|
||||
let newInputList =
|
||||
cloneDeep(old);
|
||||
newInputList[
|
||||
newInputList![
|
||||
index
|
||||
].data.node.template[
|
||||
templateField
|
||||
].value = target;
|
||||
return newInputList;
|
||||
});
|
||||
tweaks.buildTweakObject(
|
||||
tweaks.buildTweakObject!(
|
||||
node["data"]["id"],
|
||||
target,
|
||||
node.data.node.template[
|
||||
|
|
@ -310,7 +297,7 @@ export default function CodeTabsComponent({
|
|||
templateField
|
||||
].multiline ? (
|
||||
<ShadTooltip
|
||||
content={tweaks.buildContent(
|
||||
content={tweaks.buildContent!(
|
||||
node.data.node.template[
|
||||
templateField
|
||||
].value
|
||||
|
|
@ -339,14 +326,14 @@ export default function CodeTabsComponent({
|
|||
setData((old) => {
|
||||
let newInputList =
|
||||
cloneDeep(old);
|
||||
newInputList[
|
||||
newInputList![
|
||||
index
|
||||
].data.node.template[
|
||||
templateField
|
||||
].value = target;
|
||||
return newInputList;
|
||||
});
|
||||
tweaks.buildTweakObject(
|
||||
tweaks.buildTweakObject!(
|
||||
node["data"]["id"],
|
||||
target,
|
||||
node.data.node
|
||||
|
|
@ -384,14 +371,14 @@ export default function CodeTabsComponent({
|
|||
setData((old) => {
|
||||
let newInputList =
|
||||
cloneDeep(old);
|
||||
newInputList[
|
||||
newInputList![
|
||||
index
|
||||
].data.node.template[
|
||||
templateField
|
||||
].value = target;
|
||||
return newInputList;
|
||||
});
|
||||
tweaks.buildTweakObject(
|
||||
tweaks.buildTweakObject!(
|
||||
node["data"]["id"],
|
||||
target,
|
||||
node.data.node.template[
|
||||
|
|
@ -417,14 +404,14 @@ export default function CodeTabsComponent({
|
|||
setData((old) => {
|
||||
let newInputList =
|
||||
cloneDeep(old);
|
||||
newInputList[
|
||||
newInputList![
|
||||
index
|
||||
].data.node.template[
|
||||
templateField
|
||||
].value = e;
|
||||
return newInputList;
|
||||
});
|
||||
tweaks.buildTweakObject(
|
||||
tweaks.buildTweakObject!(
|
||||
node["data"]["id"],
|
||||
e,
|
||||
node.data.node.template[
|
||||
|
|
@ -440,7 +427,7 @@ export default function CodeTabsComponent({
|
|||
templateField
|
||||
].type === "file" ? (
|
||||
<ShadTooltip
|
||||
content={tweaks.buildContent(
|
||||
content={tweaks.buildContent!(
|
||||
!node.data.node.template[
|
||||
templateField
|
||||
].value ||
|
||||
|
|
@ -508,14 +495,14 @@ export default function CodeTabsComponent({
|
|||
setData((old) => {
|
||||
let newInputList =
|
||||
cloneDeep(old);
|
||||
newInputList[
|
||||
newInputList![
|
||||
index
|
||||
].data.node.template[
|
||||
templateField
|
||||
].value = target;
|
||||
return newInputList;
|
||||
});
|
||||
tweaks.buildTweakObject(
|
||||
tweaks.buildTweakObject!(
|
||||
node["data"]["id"],
|
||||
target,
|
||||
node.data.node.template[
|
||||
|
|
@ -544,14 +531,14 @@ export default function CodeTabsComponent({
|
|||
setData((old) => {
|
||||
let newInputList =
|
||||
cloneDeep(old);
|
||||
newInputList[
|
||||
newInputList![
|
||||
index
|
||||
].data.node.template[
|
||||
templateField
|
||||
].value = target;
|
||||
return newInputList;
|
||||
});
|
||||
tweaks.buildTweakObject(
|
||||
tweaks.buildTweakObject!(
|
||||
node["data"]["id"],
|
||||
target,
|
||||
node.data.node.template[
|
||||
|
|
@ -596,14 +583,14 @@ export default function CodeTabsComponent({
|
|||
setData((old) => {
|
||||
let newInputList =
|
||||
cloneDeep(old);
|
||||
newInputList[
|
||||
newInputList![
|
||||
index
|
||||
].data.node.template[
|
||||
templateField
|
||||
].value = target;
|
||||
return newInputList;
|
||||
});
|
||||
tweaks.buildTweakObject(
|
||||
tweaks.buildTweakObject!(
|
||||
node["data"]["id"],
|
||||
target,
|
||||
node.data.node.template[
|
||||
|
|
@ -617,7 +604,7 @@ export default function CodeTabsComponent({
|
|||
templateField
|
||||
].type === "prompt" ? (
|
||||
<ShadTooltip
|
||||
content={tweaks.buildContent(
|
||||
content={tweaks.buildContent!(
|
||||
!node.data.node.template[
|
||||
templateField
|
||||
].value ||
|
||||
|
|
@ -651,14 +638,14 @@ export default function CodeTabsComponent({
|
|||
setData((old) => {
|
||||
let newInputList =
|
||||
cloneDeep(old);
|
||||
newInputList[
|
||||
newInputList![
|
||||
index
|
||||
].data.node.template[
|
||||
templateField
|
||||
].value = target;
|
||||
return newInputList;
|
||||
});
|
||||
tweaks.buildTweakObject(
|
||||
tweaks.buildTweakObject!(
|
||||
node["data"]["id"],
|
||||
target,
|
||||
node.data.node.template[
|
||||
|
|
@ -673,8 +660,8 @@ export default function CodeTabsComponent({
|
|||
templateField
|
||||
].type === "code" ? (
|
||||
<ShadTooltip
|
||||
content={tweaks.buildContent(
|
||||
tweaks.getValue(
|
||||
content={tweaks.buildContent!(
|
||||
tweaks.getValue!(
|
||||
node.data.node.template[
|
||||
templateField
|
||||
].value,
|
||||
|
|
@ -706,14 +693,14 @@ export default function CodeTabsComponent({
|
|||
setData((old) => {
|
||||
let newInputList =
|
||||
cloneDeep(old);
|
||||
newInputList[
|
||||
newInputList![
|
||||
index
|
||||
].data.node.template[
|
||||
templateField
|
||||
].value = target;
|
||||
return newInputList;
|
||||
});
|
||||
tweaks.buildTweakObject(
|
||||
tweaks.buildTweakObject!(
|
||||
node["data"]["id"],
|
||||
target,
|
||||
node.data.node.template[
|
||||
|
|
@ -742,7 +729,7 @@ export default function CodeTabsComponent({
|
|||
</AccordionComponent>
|
||||
)}
|
||||
|
||||
{tweaks.tweaksList.current.length === 0 && (
|
||||
{tweaks?.tweaksList!.current.length === 0 && (
|
||||
<>
|
||||
<div className="pt-3">
|
||||
No tweaks are available for this flow.
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ export default function Dropdown({
|
|||
editNode = false,
|
||||
numberOfOptions = 0,
|
||||
apiModal = false,
|
||||
}: DropDownComponentType) {
|
||||
}: DropDownComponentType): JSX.Element {
|
||||
let [internalValue, setInternalValue] = useState(
|
||||
value === "" || !value ? "Choose an option" : value
|
||||
);
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ export default function FloatComponent({
|
|||
onChange,
|
||||
disabled,
|
||||
editNode = false,
|
||||
}: FloatComponentType) {
|
||||
}: FloatComponentType): JSX.Element {
|
||||
const step = 0.1;
|
||||
const min = 0;
|
||||
const max = 1;
|
||||
|
|
|
|||
|
|
@ -7,5 +7,11 @@ export default function IconComponent({
|
|||
iconColor,
|
||||
}: IconComponentProps): JSX.Element {
|
||||
const TargetIcon = nodeIconsLucide[name] ?? nodeIconsLucide["unknown"];
|
||||
return <TargetIcon className={className} style={{ color: iconColor }} />;
|
||||
return (
|
||||
<TargetIcon
|
||||
strokeWidth={1.5}
|
||||
className={className}
|
||||
style={{ color: iconColor }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,10 +12,11 @@ import { Link, useNavigate } from "react-router-dom";
|
|||
import { alertContext } from "../../../../contexts/alertContext";
|
||||
import { undoRedoContext } from "../../../../contexts/undoRedoContext";
|
||||
import FlowSettingsModal from "../../../../modals/flowSettingsModal";
|
||||
import { menuBarPropsType } from "../../../../types/components";
|
||||
import IconComponent from "../../../genericIconComponent";
|
||||
import { Button } from "../../../ui/button";
|
||||
|
||||
export const MenuBar = ({ flows, tabId }) => {
|
||||
export const MenuBar = ({ flows, tabId }: menuBarPropsType): JSX.Element => {
|
||||
const { addFlow } = useContext(TabsContext);
|
||||
const { setErrorData } = useContext(alertContext);
|
||||
const { undo, redo } = useContext(undoRedoContext);
|
||||
|
|
@ -25,12 +26,12 @@ export const MenuBar = ({ flows, tabId }) => {
|
|||
|
||||
function handleAddFlow() {
|
||||
try {
|
||||
addFlow(null, true).then((id) => {
|
||||
addFlow(undefined, true).then((id) => {
|
||||
navigate("/flow/" + id);
|
||||
});
|
||||
// saveFlowStyleInDataBase();
|
||||
} catch (err) {
|
||||
setErrorData(err);
|
||||
setErrorData(err as { title: string; list?: Array<string> });
|
||||
}
|
||||
}
|
||||
let current_flow = flows.find((flow) => flow.id === tabId);
|
||||
|
|
@ -45,7 +46,9 @@ export const MenuBar = ({ flows, tabId }) => {
|
|||
<DropdownMenuTrigger asChild>
|
||||
<Button asChild variant="primary" size="sm">
|
||||
<div className="header-menu-bar-display">
|
||||
<div className="header-menu-flow-name">{current_flow.name}</div>
|
||||
<div className="header-menu-flow-name">
|
||||
{current_flow!.name}
|
||||
</div>
|
||||
<IconComponent name="ChevronDown" className="h-4 w-4" />
|
||||
</div>
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import { Button } from "../ui/button";
|
|||
import { Separator } from "../ui/separator";
|
||||
import MenuBar from "./components/menuBar";
|
||||
|
||||
export default function Header() {
|
||||
export default function Header(): JSX.Element {
|
||||
const { flows, tabId } = useContext(TabsContext);
|
||||
const { dark, setDark } = useContext(darkContext);
|
||||
const { notificationCenter } = useContext(alertContext);
|
||||
|
|
@ -31,24 +31,12 @@ export default function Header() {
|
|||
return (
|
||||
<div className="header-arrangement">
|
||||
<div className="header-start-display">
|
||||
{tabId === "" || !tabId ? (
|
||||
<div className="ml-2">
|
||||
<a
|
||||
href="https://www.langflow.org/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="header-waitlist-link-box"
|
||||
>
|
||||
<span className="pr-1 text-2xl">⛓️</span>
|
||||
<span>Join The Waitlist</span>
|
||||
</a>
|
||||
</div>
|
||||
) : (
|
||||
<Link to="/">
|
||||
<span className="ml-4 text-2xl">⛓️</span>
|
||||
</Link>
|
||||
)}
|
||||
|
||||
<Link to="/">
|
||||
<span className="ml-4 text-2xl">⛓️</span>
|
||||
</Link>
|
||||
<Button variant="outline" className="">
|
||||
Sign out
|
||||
</Button>
|
||||
{flows.findIndex((f) => tabId === f.id) !== -1 && tabId !== "" && (
|
||||
<MenuBar flows={flows} tabId={tabId} />
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import * as Form from "@radix-ui/react-form";
|
||||
import { useEffect, useState } from "react";
|
||||
import { InputComponentType } from "../../types/components";
|
||||
import { classNames } from "../../utils/utils";
|
||||
|
|
@ -7,9 +8,13 @@ export default function InputComponent({
|
|||
value,
|
||||
onChange,
|
||||
disabled,
|
||||
required = false,
|
||||
isForm = false,
|
||||
password,
|
||||
editNode = false,
|
||||
}: InputComponentType) {
|
||||
placeholder = "Type something...",
|
||||
className,
|
||||
}: InputComponentType): JSX.Element {
|
||||
const [pwdVisible, setPwdVisible] = useState(false);
|
||||
|
||||
// Clear component state
|
||||
|
|
@ -21,20 +26,47 @@ export default function InputComponent({
|
|||
|
||||
return (
|
||||
<div className="relative w-full">
|
||||
<Input
|
||||
value={value}
|
||||
disabled={disabled}
|
||||
className={classNames(
|
||||
password && !pwdVisible && value !== "" ? " text-clip password " : "",
|
||||
editNode ? " input-edit-node " : "",
|
||||
password && editNode ? "pr-8" : "",
|
||||
password && !editNode ? "pr-10" : ""
|
||||
)}
|
||||
placeholder={password && editNode ? "Key" : "Type something..."}
|
||||
onChange={(event) => {
|
||||
onChange(event.target.value);
|
||||
}}
|
||||
/>
|
||||
{isForm ? (
|
||||
<Form.Control asChild>
|
||||
<Input
|
||||
value={value}
|
||||
disabled={disabled}
|
||||
required={required}
|
||||
className={classNames(
|
||||
password && !pwdVisible && value !== ""
|
||||
? " text-clip password "
|
||||
: "",
|
||||
editNode ? " input-edit-node " : "",
|
||||
password && editNode ? "pr-8" : "",
|
||||
password && !editNode ? "pr-10" : "",
|
||||
className!
|
||||
)}
|
||||
placeholder={password && editNode ? "Key" : placeholder}
|
||||
onChange={(e) => {
|
||||
onChange(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</Form.Control>
|
||||
) : (
|
||||
<Input
|
||||
value={value}
|
||||
disabled={disabled}
|
||||
required={required}
|
||||
className={classNames(
|
||||
password && !pwdVisible && value !== ""
|
||||
? " text-clip password "
|
||||
: "",
|
||||
editNode ? " input-edit-node " : "",
|
||||
password && editNode ? "pr-8" : "",
|
||||
password && !editNode ? "pr-10" : "",
|
||||
className!
|
||||
)}
|
||||
placeholder={password && editNode ? "Key" : placeholder}
|
||||
onChange={(e) => {
|
||||
onChange(e.target.value);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{password && (
|
||||
<button
|
||||
className={classNames(
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ export default function InputFileComponent({
|
|||
fileTypes,
|
||||
onFileChange,
|
||||
editNode = false,
|
||||
}: FileComponentType) {
|
||||
}: FileComponentType): JSX.Element {
|
||||
const [myValue, setMyValue] = useState(value);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { setErrorData } = useContext(alertContext);
|
||||
|
|
@ -41,7 +41,7 @@ export default function InputFileComponent({
|
|||
setMyValue(value);
|
||||
}, [value]);
|
||||
|
||||
const handleButtonClick = () => {
|
||||
const handleButtonClick = (): void => {
|
||||
// Create a file input element
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
|
|
@ -49,7 +49,7 @@ export default function InputFileComponent({
|
|||
input.style.display = "none"; // Hidden from view
|
||||
input.multiple = false; // Allow only one file selection
|
||||
|
||||
input.onchange = (event: Event) => {
|
||||
input.onchange = (event: Event): void => {
|
||||
setLoading(true);
|
||||
|
||||
// Get the selected file
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ export default function InputListComponent({
|
|||
onChange,
|
||||
disabled,
|
||||
editNode = false,
|
||||
}: InputListComponentType) {
|
||||
}: InputListComponentType): JSX.Element {
|
||||
useEffect(() => {
|
||||
if (disabled) {
|
||||
onChange([""]);
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ export default function IntComponent({
|
|||
onChange,
|
||||
disabled,
|
||||
editNode = false,
|
||||
}: FloatComponentType) {
|
||||
}: FloatComponentType): JSX.Element {
|
||||
const min = 0;
|
||||
|
||||
// Clear component state
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
type LoadingComponentProps = {
|
||||
remSize: number;
|
||||
};
|
||||
import { LoadingComponentProps } from "../../types/components";
|
||||
|
||||
export default function LoadingComponent({ remSize }: LoadingComponentProps) {
|
||||
export default function LoadingComponent({
|
||||
remSize,
|
||||
}: LoadingComponentProps): JSX.Element {
|
||||
return (
|
||||
<div role="status" className="m-auto w-min">
|
||||
<svg
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ export default function PromptAreaComponent({
|
|||
onChange,
|
||||
disabled,
|
||||
editNode = false,
|
||||
}: TextAreaComponentType) {
|
||||
}: TextAreaComponentType): JSX.Element {
|
||||
useEffect(() => {
|
||||
if (disabled) {
|
||||
onChange("");
|
||||
|
|
@ -23,9 +23,9 @@ export default function PromptAreaComponent({
|
|||
|
||||
useEffect(() => {
|
||||
if (value !== "" && !editNode) {
|
||||
postValidatePrompt(field_name, value, nodeClass).then((apiReturn) => {
|
||||
postValidatePrompt(field_name!, value, nodeClass!).then((apiReturn) => {
|
||||
if (apiReturn.data) {
|
||||
setNodeClass(apiReturn.data.frontend_node);
|
||||
setNodeClass!(apiReturn.data.frontend_node);
|
||||
// need to update reactFlowInstance to re-render the nodes.
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ export default function TextAreaComponent({
|
|||
onChange,
|
||||
disabled,
|
||||
editNode = false,
|
||||
}: TextAreaComponentType) {
|
||||
}: TextAreaComponentType): JSX.Element {
|
||||
// Clear text area
|
||||
useEffect(() => {
|
||||
if (disabled) {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ export default function ToggleComponent({
|
|||
enabled,
|
||||
setEnabled,
|
||||
disabled,
|
||||
}: ToggleComponentType) {
|
||||
}: ToggleComponentType): JSX.Element {
|
||||
// set component state as disabled
|
||||
useEffect(() => {
|
||||
if (disabled) {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ export default function ToggleShadComponent({
|
|||
setEnabled,
|
||||
disabled,
|
||||
size,
|
||||
}: ToggleComponentType) {
|
||||
}: ToggleComponentType): JSX.Element {
|
||||
let scaleX, scaleY;
|
||||
switch (size) {
|
||||
case "small":
|
||||
|
|
|
|||
|
|
@ -27,4 +27,4 @@ const PopoverContent = React.forwardRef<
|
|||
));
|
||||
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
|
||||
|
||||
export { Popover, PopoverTrigger, PopoverContent };
|
||||
export { Popover, PopoverContent, PopoverTrigger };
|
||||
|
|
|
|||
|
|
@ -23,14 +23,14 @@ export default function RenameLabel(props) {
|
|||
});
|
||||
if (inputRef.current) {
|
||||
setTimeout(() => {
|
||||
inputRef.current.focus();
|
||||
inputRef.current?.focus();
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
resizeInput();
|
||||
}, [isRename]);
|
||||
|
||||
const inputRef = useRef(null);
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
const resizeInput = () => {
|
||||
const input = inputRef.current;
|
||||
|
|
|
|||
120
src/frontend/src/components/ui/select.tsx
Normal file
120
src/frontend/src/components/ui/select.tsx
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
"use client";
|
||||
|
||||
import * as SelectPrimitive from "@radix-ui/react-select";
|
||||
import { Check, ChevronDown } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { cn } from "../../utils/utils";
|
||||
|
||||
const Select = SelectPrimitive.Root;
|
||||
|
||||
const SelectGroup = SelectPrimitive.Group;
|
||||
|
||||
const SelectValue = SelectPrimitive.Value;
|
||||
|
||||
const SelectTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
));
|
||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
|
||||
|
||||
const SelectContent = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
||||
>(({ className, children, position = "popper", ...props }, ref) => (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className
|
||||
)}
|
||||
position={position}
|
||||
{...props}
|
||||
>
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
"p-1",
|
||||
position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
));
|
||||
SelectContent.displayName = SelectPrimitive.Content.displayName;
|
||||
|
||||
const SelectLabel = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SelectLabel.displayName = SelectPrimitive.Label.displayName;
|
||||
|
||||
const SelectItem = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
));
|
||||
SelectItem.displayName = SelectPrimitive.Item.displayName;
|
||||
|
||||
const SelectSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
};
|
||||
|
|
@ -508,3 +508,93 @@ export const URL_EXCLUDED_FROM_ERROR_RETRIES = [
|
|||
"/api/v1/custom_component",
|
||||
"/api/v1/validate/prompt",
|
||||
];
|
||||
export const CONTROL_INPUT_STATE = {
|
||||
password: "",
|
||||
cnfPassword: "",
|
||||
username: "",
|
||||
};
|
||||
|
||||
export const CONTROL_LOGIN_STATE = {
|
||||
username: "",
|
||||
password: "",
|
||||
};
|
||||
export const tabsCode = [];
|
||||
|
||||
export function tabsArray(codes: string[], method: number) {
|
||||
if (!method) return;
|
||||
if (method === 0) {
|
||||
return [
|
||||
{
|
||||
name: "cURL",
|
||||
mode: "bash",
|
||||
image: "https://curl.se/logo/curl-symbol-transparent.png",
|
||||
language: "sh",
|
||||
code: codes[0],
|
||||
},
|
||||
{
|
||||
name: "Python API",
|
||||
mode: "python",
|
||||
image:
|
||||
"https://images.squarespace-cdn.com/content/v1/5df3d8c5d2be5962e4f87890/1628015119369-OY4TV3XJJ53ECO0W2OLQ/Python+API+Training+Logo.png?format=1000w",
|
||||
language: "py",
|
||||
code: codes[1],
|
||||
},
|
||||
{
|
||||
name: "Python Code",
|
||||
mode: "python",
|
||||
image: "https://cdn-icons-png.flaticon.com/512/5968/5968350.png",
|
||||
language: "py",
|
||||
code: codes[2],
|
||||
},
|
||||
{
|
||||
name: "Chat Widget HTML",
|
||||
description:
|
||||
"Insert this code anywhere in your <body> tag. To use with react and other libs, check our <a class='link-color' href='https://langflow.org/guidelines/widget'>documentation</a>.",
|
||||
mode: "html",
|
||||
image: "https://cdn-icons-png.flaticon.com/512/5968/5968350.png",
|
||||
language: "py",
|
||||
code: codes[3],
|
||||
},
|
||||
];
|
||||
}
|
||||
return [
|
||||
{
|
||||
name: "cURL",
|
||||
mode: "bash",
|
||||
image: "https://curl.se/logo/curl-symbol-transparent.png",
|
||||
language: "sh",
|
||||
code: codes[0],
|
||||
},
|
||||
{
|
||||
name: "Python API",
|
||||
mode: "python",
|
||||
image:
|
||||
"https://images.squarespace-cdn.com/content/v1/5df3d8c5d2be5962e4f87890/1628015119369-OY4TV3XJJ53ECO0W2OLQ/Python+API+Training+Logo.png?format=1000w",
|
||||
language: "py",
|
||||
code: codes[1],
|
||||
},
|
||||
{
|
||||
name: "Python Code",
|
||||
mode: "python",
|
||||
language: "py",
|
||||
image: "https://cdn-icons-png.flaticon.com/512/5968/5968350.png",
|
||||
code: codes[2],
|
||||
},
|
||||
{
|
||||
name: "Chat Widget HTML",
|
||||
description:
|
||||
"Insert this code anywhere in your <body> tag. To use with react and other libs, check our <a class='link-color' href='https://langflow.org/guidelines/widget'>documentation</a>.",
|
||||
mode: "html",
|
||||
image: "https://cdn-icons-png.flaticon.com/512/5968/5968350.png",
|
||||
language: "py",
|
||||
code: codes[3],
|
||||
},
|
||||
{
|
||||
name: "Tweaks",
|
||||
mode: "python",
|
||||
image: "https://cdn-icons-png.flaticon.com/512/5968/5968350.png",
|
||||
language: "py",
|
||||
code: codes[4],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,30 +1,9 @@
|
|||
import { createContext, ReactNode, useState } from "react";
|
||||
import { AlertItemType } from "../types/alerts";
|
||||
import { alertContextType } from "../types/typesContext";
|
||||
|
||||
import _ from "lodash";
|
||||
|
||||
//types for alertContextType
|
||||
type alertContextType = {
|
||||
errorData: { title: string; list?: Array<string> };
|
||||
setErrorData: (newState: { title: string; list?: Array<string> }) => void;
|
||||
errorOpen: boolean;
|
||||
setErrorOpen: (newState: boolean) => void;
|
||||
noticeData: { title: string; link?: string };
|
||||
setNoticeData: (newState: { title: string; link?: string }) => void;
|
||||
noticeOpen: boolean;
|
||||
setNoticeOpen: (newState: boolean) => void;
|
||||
successData: { title: string };
|
||||
setSuccessData: (newState: { title: string }) => void;
|
||||
successOpen: boolean;
|
||||
setSuccessOpen: (newState: boolean) => void;
|
||||
notificationCenter: boolean;
|
||||
setNotificationCenter: (newState: boolean) => void;
|
||||
notificationList: Array<AlertItemType>;
|
||||
pushNotificationList: (Object: AlertItemType) => void;
|
||||
clearNotificationList: () => void;
|
||||
removeFromNotificationList: (index: string) => void;
|
||||
};
|
||||
|
||||
//initial values to alertContextType
|
||||
const initialValue: alertContextType = {
|
||||
errorData: { title: "", list: [] },
|
||||
|
|
@ -65,7 +44,7 @@ export function AlertProvider({ children }: { children: ReactNode }) {
|
|||
});
|
||||
const [successOpen, setSuccessOpen] = useState(false);
|
||||
const [notificationCenter, setNotificationCenter] = useState(false);
|
||||
const [notificationList, setNotificationList] = useState([]);
|
||||
const [notificationList, setNotificationList] = useState<AlertItemType[]>([]);
|
||||
const pushNotificationList = (notification: AlertItemType) => {
|
||||
setNotificationList((old) => {
|
||||
let newNotificationList = _.cloneDeep(old);
|
||||
|
|
|
|||
77
src/frontend/src/contexts/authContext.tsx
Normal file
77
src/frontend/src/contexts/authContext.tsx
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
import { createContext, useEffect, useState } from "react";
|
||||
import { AuthContextType, userData } from "../types/contexts/auth";
|
||||
|
||||
const initialValue: AuthContextType = {
|
||||
isAuthenticated: false,
|
||||
accessToken: null,
|
||||
login: () => {},
|
||||
logout: () => {},
|
||||
refreshAccessToken: () => Promise.resolve(),
|
||||
userData: null,
|
||||
setUserData: () => {},
|
||||
};
|
||||
|
||||
const AuthContext = createContext<AuthContextType>(initialValue);
|
||||
|
||||
export function AuthProvider({ children }): React.ReactElement {
|
||||
const [accessToken, setAccessToken] = useState<string | null>(null);
|
||||
const [userData, setUserData] = useState<userData | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const storedAccessToken = localStorage.getItem("access_token");
|
||||
if (storedAccessToken) {
|
||||
setAccessToken(storedAccessToken);
|
||||
}
|
||||
}, []);
|
||||
|
||||
function login(newAccessToken: string, refreshToken: string) {
|
||||
localStorage.setItem("access_token", newAccessToken);
|
||||
setAccessToken(newAccessToken);
|
||||
// Store refreshToken if needed
|
||||
}
|
||||
|
||||
function logout() {
|
||||
localStorage.removeItem("access_token");
|
||||
// Clear refreshToken if used
|
||||
setAccessToken(null);
|
||||
}
|
||||
|
||||
async function refreshAccessToken(refreshToken: string) {
|
||||
try {
|
||||
// Call your API to refresh the access token using the refresh token
|
||||
const response = await fetch("/api/refresh-token", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ refreshToken }),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
login(data.accessToken, refreshToken);
|
||||
} else {
|
||||
logout();
|
||||
}
|
||||
} catch (error) {
|
||||
logout();
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
// !! to convert string to boolean
|
||||
<AuthContext.Provider
|
||||
value={{
|
||||
isAuthenticated: !!accessToken,
|
||||
accessToken,
|
||||
login,
|
||||
logout,
|
||||
refreshAccessToken,
|
||||
setUserData,
|
||||
userData,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,9 +1,5 @@
|
|||
import { createContext, useEffect, useState } from "react";
|
||||
|
||||
type darkContextType = {
|
||||
dark: {};
|
||||
setDark: (newState: {}) => void;
|
||||
};
|
||||
import { darkContextType } from "../types/typesContext";
|
||||
|
||||
const initialValue = {
|
||||
dark: {},
|
||||
|
|
@ -14,13 +10,13 @@ export const darkContext = createContext<darkContextType>(initialValue);
|
|||
|
||||
export function DarkProvider({ children }) {
|
||||
const [dark, setDark] = useState(
|
||||
JSON.parse(window.localStorage.getItem("isDark")) ?? false
|
||||
JSON.parse(window.localStorage.getItem("isDark")!) ?? false
|
||||
);
|
||||
useEffect(() => {
|
||||
if (dark) {
|
||||
document.getElementById("body").classList.add("dark");
|
||||
document.getElementById("body")!.classList.add("dark");
|
||||
} else {
|
||||
document.getElementById("body").classList.remove("dark");
|
||||
document.getElementById("body")!.classList.remove("dark");
|
||||
}
|
||||
window.localStorage.setItem("isDark", dark.toString());
|
||||
}, [dark]);
|
||||
|
|
|
|||
|
|
@ -1,34 +1,5 @@
|
|||
import { createContext, ReactNode, useState } from "react";
|
||||
|
||||
//types for location context
|
||||
type locationContextType = {
|
||||
current: Array<string>;
|
||||
setCurrent: (newState: Array<string>) => void;
|
||||
isStackedOpen: boolean;
|
||||
setIsStackedOpen: (newState: boolean) => void;
|
||||
showSideBar: boolean;
|
||||
setShowSideBar: (newState: boolean) => void;
|
||||
extraNavigation: {
|
||||
title: string;
|
||||
options?: Array<{
|
||||
name: string;
|
||||
href: string;
|
||||
icon: any;
|
||||
children?: Array<any>;
|
||||
}>;
|
||||
};
|
||||
setExtraNavigation: (newState: {
|
||||
title: string;
|
||||
options?: Array<{
|
||||
name: string;
|
||||
href: string;
|
||||
icon: any;
|
||||
children?: Array<any>;
|
||||
}>;
|
||||
}) => void;
|
||||
extraComponent: any;
|
||||
setExtraComponent: (newState: any) => void;
|
||||
};
|
||||
import { locationContextType } from "../types/typesContext";
|
||||
|
||||
//initial value for location context
|
||||
const initialValue = {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import {
|
|||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { addEdge } from "reactflow";
|
||||
import { Edge, Node, ReactFlowJsonObject, addEdge } from "reactflow";
|
||||
import ShortUniqueId from "short-unique-id";
|
||||
import {
|
||||
deleteFlowFromDatabase,
|
||||
|
|
@ -18,8 +18,9 @@ import {
|
|||
uploadFlowsToDatabase,
|
||||
} from "../controllers/API";
|
||||
import { APIClassType, APITemplateType } from "../types/api";
|
||||
import { FlowType, NodeType } from "../types/flow";
|
||||
import { TabsContextType, TabsState } from "../types/tabs";
|
||||
import { tweakType } from "../types/components";
|
||||
import { FlowType, NodeDataType, NodeType } from "../types/flow";
|
||||
import { TabsContextType, TabsState, errorsVarType } from "../types/tabs";
|
||||
import {
|
||||
addVersionToDuplicates,
|
||||
updateIds,
|
||||
|
|
@ -73,9 +74,12 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
const [flows, setFlows] = useState<Array<FlowType>>([]);
|
||||
const [id, setId] = useState(uid());
|
||||
const { templates, reactFlowInstance } = useContext(typesContext);
|
||||
const [lastCopiedSelection, setLastCopiedSelection] = useState(null);
|
||||
const [lastCopiedSelection, setLastCopiedSelection] = useState<{
|
||||
nodes: any;
|
||||
edges: any;
|
||||
} | null>(null);
|
||||
const [tabsState, setTabsState] = useState<TabsState>({});
|
||||
const [getTweak, setTweak] = useState([]);
|
||||
const [getTweak, setTweak] = useState<tweakType>([]);
|
||||
|
||||
const newNodeId = useRef(uid());
|
||||
function incrementNodeId() {
|
||||
|
|
@ -130,8 +134,8 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
//get tabs from db
|
||||
return readFlowsFromDatabase();
|
||||
}
|
||||
function processDBData(DbData) {
|
||||
DbData.forEach((flow) => {
|
||||
function processDBData(DbData: FlowType[]) {
|
||||
DbData.forEach((flow: FlowType) => {
|
||||
try {
|
||||
if (!flow.data) {
|
||||
return;
|
||||
|
|
@ -144,7 +148,7 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
});
|
||||
}
|
||||
|
||||
function processFlowEdges(flow) {
|
||||
function processFlowEdges(flow: FlowType) {
|
||||
if (!flow.data || !flow.data.edges) return;
|
||||
flow.data.edges.forEach((edge) => {
|
||||
edge.className = "";
|
||||
|
|
@ -153,14 +157,14 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
}
|
||||
|
||||
function updateDisplay_name(node: NodeType, template: APIClassType) {
|
||||
node.data.node.display_name = template["display_name"] || node.data.type;
|
||||
node.data.node!.display_name = template["display_name"] || node.data.type;
|
||||
}
|
||||
|
||||
function updateNodeDocumentation(node: NodeType, template: APIClassType) {
|
||||
node.data.node.documentation = template["documentation"];
|
||||
node.data.node!.documentation = template["documentation"];
|
||||
}
|
||||
|
||||
function processFlowNodes(flow) {
|
||||
function processFlowNodes(flow: FlowType) {
|
||||
if (!flow.data || !flow.data.nodes) return;
|
||||
flow.data.nodes.forEach((node: NodeType) => {
|
||||
const template = templates[node.data.type];
|
||||
|
|
@ -180,7 +184,7 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
}
|
||||
|
||||
function updateNodeBaseClasses(node: NodeType, template: APIClassType) {
|
||||
node.data.node.base_classes = template["base_classes"];
|
||||
node.data.node!.base_classes = template["base_classes"];
|
||||
}
|
||||
|
||||
function updateNodeEdges(
|
||||
|
|
@ -188,10 +192,10 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
node: NodeType,
|
||||
template: APIClassType
|
||||
) {
|
||||
flow.data.edges.forEach((edge) => {
|
||||
flow.data!.edges.forEach((edge) => {
|
||||
if (edge.source === node.id) {
|
||||
edge.sourceHandle = edge.sourceHandle
|
||||
.split("|")
|
||||
?.split("|")
|
||||
.slice(0, 2)
|
||||
.concat(template["base_classes"])
|
||||
.join("|");
|
||||
|
|
@ -200,17 +204,17 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
}
|
||||
|
||||
function updateNodeDescription(node: NodeType, template: APIClassType) {
|
||||
node.data.node.description = template["description"];
|
||||
node.data.node!.description = template["description"];
|
||||
}
|
||||
|
||||
function updateNodeTemplate(node: NodeType, template: APIClassType) {
|
||||
node.data.node.template = updateTemplate(
|
||||
node.data.node!.template = updateTemplate(
|
||||
template["template"] as unknown as APITemplateType,
|
||||
node.data.node.template as APITemplateType
|
||||
node.data.node!.template as APITemplateType
|
||||
);
|
||||
}
|
||||
|
||||
function updateStateWithDbData(tabsData) {
|
||||
function updateStateWithDbData(tabsData: FlowType[]) {
|
||||
setFlows(tabsData);
|
||||
}
|
||||
|
||||
|
|
@ -241,7 +245,7 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
link.download = `${
|
||||
flowName && flowName != ""
|
||||
? flowName
|
||||
: flows.find((f) => f.id === tabId).name
|
||||
: flows.find((f) => f.id === tabId)!.name
|
||||
}.json`;
|
||||
|
||||
// simulate a click on the link element to trigger the download
|
||||
|
|
@ -293,10 +297,10 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
input.onchange = (e: Event) => {
|
||||
// check if the file type is application/json
|
||||
if (
|
||||
(e.target as HTMLInputElement).files[0].type === "application/json"
|
||||
(e.target as HTMLInputElement).files![0].type === "application/json"
|
||||
) {
|
||||
// get the file from the file input
|
||||
const currentfile = (e.target as HTMLInputElement).files[0];
|
||||
const currentfile = (e.target as HTMLInputElement).files![0];
|
||||
// read the file as text
|
||||
currentfile.text().then((text) => {
|
||||
// parse the text into a JSON object
|
||||
|
|
@ -319,10 +323,10 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
input.onchange = (event: Event) => {
|
||||
// check if the file type is application/json
|
||||
if (
|
||||
(event.target as HTMLInputElement).files[0].type === "application/json"
|
||||
(event.target as HTMLInputElement).files![0].type === "application/json"
|
||||
) {
|
||||
// get the file from the file input
|
||||
const file = (event.target as HTMLInputElement).files[0];
|
||||
const file = (event.target as HTMLInputElement).files![0];
|
||||
// read the file as text
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
|
|
@ -353,15 +357,15 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
*/
|
||||
|
||||
function paste(
|
||||
selectionInstance,
|
||||
selectionInstance: { nodes: Node[]; edges: Edge[] },
|
||||
position: { x: number; y: number; paneX?: number; paneY?: number }
|
||||
) {
|
||||
let minimumX = Infinity;
|
||||
let minimumY = Infinity;
|
||||
let idsMap = {};
|
||||
let nodes = reactFlowInstance.getNodes();
|
||||
let edges = reactFlowInstance.getEdges();
|
||||
selectionInstance.nodes.forEach((node) => {
|
||||
let nodes: Node<NodeDataType>[] = reactFlowInstance!.getNodes();
|
||||
let edges = reactFlowInstance!.getEdges();
|
||||
selectionInstance.nodes.forEach((node: Node) => {
|
||||
if (node.position.y < minimumY) {
|
||||
minimumY = node.position.y;
|
||||
}
|
||||
|
|
@ -371,8 +375,8 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
});
|
||||
|
||||
const insidePosition = position.paneX
|
||||
? { x: position.paneX + position.x, y: position.paneY + position.y }
|
||||
: reactFlowInstance.project({ x: position.x, y: position.y });
|
||||
? { x: position.paneX + position.x, y: position.paneY! + position.y }
|
||||
: reactFlowInstance!.project({ x: position.x, y: position.y });
|
||||
|
||||
selectionInstance.nodes.forEach((node: NodeType) => {
|
||||
// Generate a unique node ID
|
||||
|
|
@ -384,8 +388,8 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
id: newId,
|
||||
type: "genericNode",
|
||||
position: {
|
||||
x: insidePosition.x + node.position.x - minimumX,
|
||||
y: insidePosition.y + node.position.y - minimumY,
|
||||
x: insidePosition.x + node.position!.x - minimumX,
|
||||
y: insidePosition.y + node.position!.y - minimumY,
|
||||
},
|
||||
data: {
|
||||
..._.cloneDeep(node.data),
|
||||
|
|
@ -398,19 +402,19 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
.map((node) => ({ ...node, selected: false }))
|
||||
.concat({ ...newNode, selected: false });
|
||||
});
|
||||
reactFlowInstance.setNodes(nodes);
|
||||
reactFlowInstance!.setNodes(nodes);
|
||||
|
||||
selectionInstance.edges.forEach((edge) => {
|
||||
let source = idsMap[edge.source];
|
||||
let target = idsMap[edge.target];
|
||||
let sourceHandleSplitted = edge.sourceHandle.split("|");
|
||||
let sourceHandleSplitted = edge.sourceHandle!.split("|");
|
||||
let sourceHandle =
|
||||
sourceHandleSplitted[0] +
|
||||
"|" +
|
||||
source +
|
||||
"|" +
|
||||
sourceHandleSplitted.slice(2).join("|");
|
||||
let targetHandleSplitted = edge.targetHandle.split("|");
|
||||
let targetHandleSplitted = edge.targetHandle!.split("|");
|
||||
let targetHandle =
|
||||
targetHandleSplitted.slice(0, -1).join("|") + "|" + target;
|
||||
let id =
|
||||
|
|
@ -438,21 +442,21 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
edges.map((edge) => ({ ...edge, selected: false }))
|
||||
);
|
||||
});
|
||||
reactFlowInstance.setEdges(edges);
|
||||
reactFlowInstance!.setEdges(edges);
|
||||
}
|
||||
|
||||
const addFlow = async (
|
||||
flow?: FlowType,
|
||||
newProject?: Boolean
|
||||
): Promise<String> => {
|
||||
): Promise<String | undefined> => {
|
||||
if (newProject) {
|
||||
let flowData = extractDataFromFlow(flow);
|
||||
let flowData = extractDataFromFlow(flow!);
|
||||
if (flowData.description == "") {
|
||||
flowData.description = getRandomDescription();
|
||||
}
|
||||
|
||||
// Create a new flow with a default name if no flow is provided.
|
||||
const newFlow = createNewFlow(flowData, flow);
|
||||
const newFlow = createNewFlow(flowData, flow!);
|
||||
processFlowEdges(newFlow);
|
||||
processFlowNodes(newFlow);
|
||||
|
||||
|
|
@ -477,13 +481,13 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
}
|
||||
} else {
|
||||
paste(
|
||||
{ nodes: flow.data.nodes, edges: flow.data.edges },
|
||||
{ nodes: flow!.data!.nodes, edges: flow!.data!.edges },
|
||||
{ x: 10, y: 10 }
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const extractDataFromFlow = (flow) => {
|
||||
const extractDataFromFlow = (flow: FlowType) => {
|
||||
let data = flow?.data ? flow.data : null;
|
||||
const description = flow?.description ? flow.description : "";
|
||||
|
||||
|
|
@ -496,17 +500,17 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
return { data, description };
|
||||
};
|
||||
|
||||
const updateEdges = (edges) => {
|
||||
const updateEdges = (edges: Edge[]) => {
|
||||
edges.forEach((edge) => {
|
||||
edge.className =
|
||||
(edge.targetHandle.split("|")[0] === "Text"
|
||||
(edge.targetHandle!.split("|")[0] === "Text"
|
||||
? "stroke-gray-800 "
|
||||
: "stroke-gray-900 ") + " stroke-connection";
|
||||
edge.animated = edge.targetHandle.split("|")[0] === "Text";
|
||||
edge.animated = edge.targetHandle!.split("|")[0] === "Text";
|
||||
});
|
||||
};
|
||||
|
||||
const updateNodes = (nodes, edges) => {
|
||||
const updateNodes = (nodes: Node[], edges: Edge[]) => {
|
||||
nodes.forEach((node) => {
|
||||
const template = templates[node.data.type];
|
||||
if (!template) {
|
||||
|
|
@ -517,8 +521,8 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
node.data.node.base_classes = template["base_classes"];
|
||||
edges.forEach((edge) => {
|
||||
if (edge.source === node.id) {
|
||||
edge.sourceHandle = edge.sourceHandle
|
||||
.split("|")
|
||||
edge.sourceHandle = edge
|
||||
.sourceHandle!.split("|")
|
||||
.slice(0, 2)
|
||||
.concat(template["base_classes"])
|
||||
.join("|");
|
||||
|
|
@ -533,14 +537,17 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
});
|
||||
};
|
||||
|
||||
const createNewFlow = (flowData, flow) => ({
|
||||
const createNewFlow = (
|
||||
flowData: { data: ReactFlowJsonObject | null; description: string },
|
||||
flow: FlowType
|
||||
) => ({
|
||||
description: flowData.description,
|
||||
name: flow?.name ?? getRandomName(),
|
||||
data: flowData.data,
|
||||
id: "",
|
||||
});
|
||||
|
||||
const addFlowToLocalState = (newFlow) => {
|
||||
const addFlowToLocalState = (newFlow: FlowType) => {
|
||||
setFlows((prevState) => {
|
||||
return [...prevState, newFlow];
|
||||
});
|
||||
|
|
@ -591,7 +598,7 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
});
|
||||
}
|
||||
} catch (err) {
|
||||
setErrorData(err);
|
||||
setErrorData(err as errorsVarType);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { createContext, ReactNode, useEffect, useState } from "react";
|
||||
import { Node } from "reactflow";
|
||||
import { Node, ReactFlowInstance } from "reactflow";
|
||||
import { getAll } from "../controllers/API";
|
||||
import { APIKindType } from "../types/api";
|
||||
import { typesContextType } from "../types/typesContext";
|
||||
|
|
@ -8,7 +8,7 @@ import { typesContextType } from "../types/typesContext";
|
|||
|
||||
const initialValue: typesContextType = {
|
||||
reactFlowInstance: null,
|
||||
setReactFlowInstance: () => {},
|
||||
setReactFlowInstance: (newState: ReactFlowInstance) => {},
|
||||
deleteNode: () => {},
|
||||
types: {},
|
||||
setTypes: () => {},
|
||||
|
|
@ -22,13 +22,14 @@ export const typesContext = createContext<typesContextType>(initialValue);
|
|||
|
||||
export function TypesProvider({ children }: { children: ReactNode }) {
|
||||
const [types, setTypes] = useState({});
|
||||
const [reactFlowInstance, setReactFlowInstance] = useState(null);
|
||||
const [reactFlowInstance, setReactFlowInstance] =
|
||||
useState<ReactFlowInstance | null>(null);
|
||||
const [templates, setTemplates] = useState({});
|
||||
const [data, setData] = useState({});
|
||||
|
||||
useEffect(() => {
|
||||
let delay = 1000; // Start delay of 1 second
|
||||
let intervalId = null;
|
||||
let intervalId: NodeJS.Timer;
|
||||
let retryCount = 0; // Count of retry attempts
|
||||
const maxRetryCount = 5; // Max retry attempts
|
||||
|
||||
|
|
@ -69,7 +70,7 @@ export function TypesProvider({ children }: { children: ReactNode }) {
|
|||
);
|
||||
}
|
||||
// Clear the interval if successful.
|
||||
clearInterval(intervalId);
|
||||
clearInterval(intervalId!);
|
||||
} catch (error) {
|
||||
console.error("An error has occurred while fetching types.");
|
||||
}
|
||||
|
|
@ -77,21 +78,20 @@ export function TypesProvider({ children }: { children: ReactNode }) {
|
|||
|
||||
// Start the initial interval.
|
||||
intervalId = setInterval(getTypes, delay);
|
||||
|
||||
return () => {
|
||||
// This will clear the interval when the component unmounts, or when the dependencies of the useEffect hook change.
|
||||
clearInterval(intervalId);
|
||||
clearInterval(intervalId!);
|
||||
// Indicate that the component has been unmounted.
|
||||
isMounted = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
function deleteNode(idx: string) {
|
||||
reactFlowInstance.setNodes(
|
||||
reactFlowInstance.getNodes().filter((node: Node) => node.id !== idx)
|
||||
reactFlowInstance!.setNodes(
|
||||
reactFlowInstance!.getNodes().filter((node: Node) => node.id !== idx)
|
||||
);
|
||||
reactFlowInstance.setEdges(
|
||||
reactFlowInstance
|
||||
reactFlowInstance!.setEdges(
|
||||
reactFlowInstance!
|
||||
.getEdges()
|
||||
.filter((edge) => edge.source !== idx && edge.target !== idx)
|
||||
);
|
||||
|
|
|
|||
|
|
@ -6,26 +6,15 @@ import {
|
|||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import { Edge, Node, useReactFlow } from "reactflow";
|
||||
import { useReactFlow } from "reactflow";
|
||||
import {
|
||||
HistoryItem,
|
||||
UseUndoRedoOptions,
|
||||
undoRedoContextType,
|
||||
} from "../types/typesContext";
|
||||
import { isWrappedWithClass } from "../utils/utils";
|
||||
import { TabsContext } from "./tabsContext";
|
||||
|
||||
type undoRedoContextType = {
|
||||
undo: () => void;
|
||||
redo: () => void;
|
||||
takeSnapshot: () => void;
|
||||
};
|
||||
|
||||
type UseUndoRedoOptions = {
|
||||
maxHistorySize: number;
|
||||
enableShortcuts: boolean;
|
||||
};
|
||||
|
||||
type HistoryItem = {
|
||||
nodes: Node[];
|
||||
edges: Edge[];
|
||||
};
|
||||
|
||||
const initialValue = {
|
||||
undo: () => {},
|
||||
redo: () => {},
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import axios, { AxiosError, AxiosInstance } from "axios";
|
||||
import { useContext, useEffect, useRef } from "react";
|
||||
import { URL_EXCLUDED_FROM_ERROR_RETRIES } from "../../constants/constants";
|
||||
import { alertContext } from "../../contexts/alertContext";
|
||||
|
||||
// Create a new Axios instance
|
||||
|
|
@ -8,7 +7,7 @@ const api: AxiosInstance = axios.create({
|
|||
baseURL: "",
|
||||
});
|
||||
|
||||
function ApiInterceptor() {
|
||||
function ApiInterceptor(): null {
|
||||
const retryCounts = useRef([]);
|
||||
const { setErrorData } = useContext(alertContext);
|
||||
|
||||
|
|
@ -16,32 +15,31 @@ function ApiInterceptor() {
|
|||
const interceptor = api.interceptors.response.use(
|
||||
(response) => response,
|
||||
async (error: AxiosError) => {
|
||||
if (URL_EXCLUDED_FROM_ERROR_RETRIES.includes(error.config?.url)) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
let retryCount = 0;
|
||||
|
||||
while (retryCount < 4) {
|
||||
await sleep(5000); // Sleep for 5 seconds
|
||||
retryCount++;
|
||||
try {
|
||||
const response = await axios.request(error.config);
|
||||
return response;
|
||||
} catch (error) {
|
||||
if (retryCount === 3) {
|
||||
setErrorData({
|
||||
title: "There was an error on web connection, please: ",
|
||||
list: [
|
||||
"Refresh the page",
|
||||
"Use a new flow tab",
|
||||
"Check if the backend is up",
|
||||
"Endpoint: " + error.config?.url,
|
||||
],
|
||||
});
|
||||
return Promise.reject(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
// if (URL_EXCLUDED_FROM_ERROR_RETRIES.includes(error.config?.url)) {
|
||||
// return Promise.reject(error);
|
||||
// }
|
||||
// let retryCount = 0;
|
||||
// while (retryCount < 4) {
|
||||
// await sleep(5000); // Sleep for 5 seconds
|
||||
// retryCount++;
|
||||
// try {
|
||||
// const response = await axios.request(error.config);
|
||||
// return response;
|
||||
// } catch (error) {
|
||||
// if (retryCount === 3) {
|
||||
// setErrorData({
|
||||
// title: "There was an error on web connection, please: ",
|
||||
// list: [
|
||||
// "Refresh the page",
|
||||
// "Use a new flow tab",
|
||||
// "Check if the backend is up",
|
||||
// "Endpoint: " + error.config?.url,
|
||||
// ],
|
||||
// });
|
||||
// return Promise.reject(error);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
);
|
||||
|
||||
|
|
@ -55,7 +53,7 @@ function ApiInterceptor() {
|
|||
}
|
||||
|
||||
// Function to sleep for a given duration in milliseconds
|
||||
function sleep(ms) {
|
||||
function sleep(ms: number) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ export async function getAll(): Promise<AxiosResponse<APIObjectType>> {
|
|||
|
||||
const GITHUB_API_URL = "https://api.github.com";
|
||||
|
||||
export async function getRepoStars(owner, repo) {
|
||||
export async function getRepoStars(owner: string, repo: string) {
|
||||
try {
|
||||
const response = await api.get(`${GITHUB_API_URL}/repos/${owner}/${repo}`);
|
||||
return response.data.stargazers_count;
|
||||
|
|
@ -100,7 +100,7 @@ export async function getExamples(): Promise<FlowType[]> {
|
|||
export async function saveFlowToDatabase(newFlow: {
|
||||
name: string;
|
||||
id: string;
|
||||
data: ReactFlowJsonObject;
|
||||
data: ReactFlowJsonObject | null;
|
||||
description: string;
|
||||
style?: FlowStyleType;
|
||||
}): Promise<FlowType> {
|
||||
|
|
@ -179,7 +179,7 @@ export async function downloadFlowsFromDatabase() {
|
|||
}
|
||||
}
|
||||
|
||||
export async function uploadFlowsToDatabase(flows) {
|
||||
export async function uploadFlowsToDatabase(flows: FormData) {
|
||||
try {
|
||||
const response = await api.post(`/api/v1/flows/upload/`, flows);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import React, { forwardRef } from "react";
|
||||
import { ReactComponent as PowerPointSVG } from "./PowerPoint.svg";
|
||||
import SvgPowerPoint from "./PowerPoint";
|
||||
|
||||
export const PowerPointIcon = forwardRef<
|
||||
SVGSVGElement,
|
||||
React.PropsWithChildren<{}>
|
||||
>((props, ref) => {
|
||||
return <PowerPointSVG ref={ref} {...props} />;
|
||||
return <SvgPowerPoint ref={ref} {...props} />;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -15,7 +15,9 @@ import CodeTabsComponent from "../../components/codeTabsComponent";
|
|||
import IconComponent from "../../components/genericIconComponent";
|
||||
import { EXPORT_CODE_DIALOG } from "../../constants/constants";
|
||||
import { TabsContext } from "../../contexts/tabsContext";
|
||||
import { FlowType } from "../../types/flow/index";
|
||||
import { TemplateVariableType } from "../../types/api";
|
||||
import { tweakType, uniqueTweakType } from "../../types/components";
|
||||
import { FlowType, NodeType } from "../../types/flow/index";
|
||||
import { buildTweaks } from "../../utils/reactflowUtils";
|
||||
import {
|
||||
getCurlCode,
|
||||
|
|
@ -41,8 +43,8 @@ const ApiModal = forwardRef(
|
|||
) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState("0");
|
||||
const tweak = useRef([]);
|
||||
const tweaksList = useRef([]);
|
||||
const tweak = useRef<tweakType>([]);
|
||||
const tweaksList = useRef<string[]>([]);
|
||||
const { setTweak, getTweak, tabsState } = useContext(TabsContext);
|
||||
const pythonApiCode = getPythonApiCode(flow, tweak.current, tabsState);
|
||||
const curl_code = getCurlCode(flow, tweak.current, tabsState);
|
||||
|
|
@ -65,7 +67,7 @@ const ApiModal = forwardRef(
|
|||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (flow["data"]["nodes"].length == 0) {
|
||||
if (flow["data"]!["nodes"].length == 0) {
|
||||
startState();
|
||||
} else {
|
||||
tweak.current = [];
|
||||
|
|
@ -81,12 +83,12 @@ const ApiModal = forwardRef(
|
|||
} else {
|
||||
setTabs(tabsArray(codesArray, 1));
|
||||
}
|
||||
}, [flow["data"]["nodes"], open]);
|
||||
}, [flow["data"]!["nodes"], open]);
|
||||
|
||||
function filterNodes() {
|
||||
let arrNodesWithValues = [];
|
||||
let arrNodesWithValues: string[] = [];
|
||||
|
||||
flow["data"]["nodes"].forEach((node) => {
|
||||
flow["data"]!["nodes"].forEach((node) => {
|
||||
Object.keys(node["data"]["node"]["template"])
|
||||
.filter(
|
||||
(templateField) =>
|
||||
|
|
@ -109,11 +111,15 @@ const ApiModal = forwardRef(
|
|||
return self.indexOf(value) === index;
|
||||
});
|
||||
}
|
||||
function buildTweakObject(tw, changes, template) {
|
||||
if (template.type === "float") {
|
||||
function buildTweakObject(
|
||||
tw: string,
|
||||
changes: string | string[] | boolean | number,
|
||||
template: TemplateVariableType
|
||||
) {
|
||||
if (typeof changes === "string" && template.type === "float") {
|
||||
changes = parseFloat(changes);
|
||||
}
|
||||
if (template.type === "int") {
|
||||
if (typeof changes === "string" && template.type === "int") {
|
||||
changes = parseInt(changes);
|
||||
}
|
||||
if (template.list === true && Array.isArray(changes)) {
|
||||
|
|
@ -125,7 +131,7 @@ const ApiModal = forwardRef(
|
|||
);
|
||||
|
||||
if (existingTweak) {
|
||||
existingTweak[tw][template["name"]] = changes;
|
||||
existingTweak[tw][template["name"]] = changes as string;
|
||||
|
||||
if (existingTweak[tw][template["name"]] == template.value) {
|
||||
tweak.current.forEach((element) => {
|
||||
|
|
@ -142,7 +148,7 @@ const ApiModal = forwardRef(
|
|||
[tw]: {
|
||||
[template["name"]]: changes,
|
||||
},
|
||||
};
|
||||
} as uniqueTweakType;
|
||||
tweak.current.push(newTweak);
|
||||
}
|
||||
|
||||
|
|
@ -151,15 +157,15 @@ const ApiModal = forwardRef(
|
|||
const pythonCode = getPythonCode(flow, tweak.current, tabsState);
|
||||
const widgetCode = getWidgetCode(flow, tabsState);
|
||||
|
||||
tabs[0].code = curl_code;
|
||||
tabs[1].code = pythonApiCode;
|
||||
tabs[2].code = pythonCode;
|
||||
tabs[3].code = widgetCode;
|
||||
tabs![0].code = curl_code;
|
||||
tabs![1].code = pythonApiCode;
|
||||
tabs![2].code = pythonCode;
|
||||
tabs![3].code = widgetCode;
|
||||
|
||||
setTweak(tweak.current);
|
||||
}
|
||||
|
||||
function buildContent(value) {
|
||||
function buildContent(value: string) {
|
||||
const htmlContent = (
|
||||
<div className="w-[200px]">
|
||||
<span>{value != null && value != "" ? value : "None"}</span>
|
||||
|
|
@ -168,7 +174,11 @@ const ApiModal = forwardRef(
|
|||
return htmlContent;
|
||||
}
|
||||
|
||||
function getValue(value, node, template) {
|
||||
function getValue(
|
||||
value: string,
|
||||
node: NodeType,
|
||||
template: TemplateVariableType
|
||||
) {
|
||||
let returnValue = value ?? "";
|
||||
|
||||
if (getTweak.length > 0) {
|
||||
|
|
@ -204,7 +214,7 @@ const ApiModal = forwardRef(
|
|||
<BaseModal.Content>
|
||||
<CodeTabsComponent
|
||||
flow={flow}
|
||||
tabs={tabs}
|
||||
tabs={tabs!}
|
||||
activeTab={activeTab}
|
||||
setActiveTab={setActiveTab}
|
||||
tweaks={{
|
||||
|
|
|
|||
66
src/frontend/src/modals/ConfirmationModal/index.tsx
Normal file
66
src/frontend/src/modals/ConfirmationModal/index.tsx
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import { useState } from "react";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import { ConfirmationModalType } from "../../types/components";
|
||||
import { nodeIconsLucide } from "../../utils/styleUtils";
|
||||
import BaseModal from "../baseModal";
|
||||
|
||||
export default function ConfirmationModal({
|
||||
title,
|
||||
titleHeader,
|
||||
modalContent,
|
||||
modalContentTitle,
|
||||
cancelText,
|
||||
confirmationText,
|
||||
children,
|
||||
icon,
|
||||
data,
|
||||
index,
|
||||
onConfirm,
|
||||
}: ConfirmationModalType) {
|
||||
const Icon: any = nodeIconsLucide[icon];
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
return (
|
||||
<BaseModal size="x-small" open={open} setOpen={setOpen}>
|
||||
<BaseModal.Trigger>{children}</BaseModal.Trigger>
|
||||
<BaseModal.Header description={titleHeader}>
|
||||
<span className="pr-2">{title}</span>
|
||||
<Icon
|
||||
name="icon"
|
||||
className="h-6 w-6 pl-1 text-foreground"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</BaseModal.Header>
|
||||
<BaseModal.Content>
|
||||
{modalContentTitle != "" && (
|
||||
<>
|
||||
<strong>{modalContentTitle}</strong>
|
||||
<br></br>
|
||||
</>
|
||||
)}
|
||||
<span>{modalContent}</span>
|
||||
</BaseModal.Content>
|
||||
|
||||
<BaseModal.Footer>
|
||||
<Button
|
||||
className="ml-3"
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
onConfirm(index, data);
|
||||
}}
|
||||
>
|
||||
{confirmationText}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
{cancelText}
|
||||
</Button>
|
||||
</BaseModal.Footer>
|
||||
</BaseModal>
|
||||
);
|
||||
}
|
||||
|
|
@ -25,6 +25,7 @@ import { limitScrollFieldsModal } from "../../constants/constants";
|
|||
import { TabsContext } from "../../contexts/tabsContext";
|
||||
import { typesContext } from "../../contexts/typesContext";
|
||||
import { NodeDataType } from "../../types/flow";
|
||||
import { TabsState } from "../../types/tabs";
|
||||
import { classNames } from "../../utils/utils";
|
||||
import BaseModal from "../baseModal";
|
||||
|
||||
|
|
@ -53,19 +54,22 @@ const EditNodeModal = forwardRef(
|
|||
?.getEdges()
|
||||
.some((edge) => edge.targetHandle === data.id) ?? false;
|
||||
|
||||
function changeAdvanced(templateParam) {
|
||||
function changeAdvanced(templateParam: string): void {
|
||||
setMyData((old) => {
|
||||
let newData = cloneDeep(old);
|
||||
newData.node.template[templateParam].advanced =
|
||||
!newData.node.template[templateParam].advanced;
|
||||
newData.node!.template[templateParam].advanced =
|
||||
!newData.node!.template[templateParam].advanced;
|
||||
return newData;
|
||||
});
|
||||
}
|
||||
|
||||
const handleOnNewValue = (newValue: any, name) => {
|
||||
const handleOnNewValue = (
|
||||
newValue: string | string[] | boolean,
|
||||
name: string
|
||||
) => {
|
||||
setMyData((old) => {
|
||||
let newData = cloneDeep(old);
|
||||
newData.node.template[name].value = newValue;
|
||||
newData.node!.template[name].value = newValue;
|
||||
return newData;
|
||||
});
|
||||
};
|
||||
|
|
@ -77,7 +81,7 @@ const EditNodeModal = forwardRef(
|
|||
return (
|
||||
<BaseModal size="large-h-full" open={modalOpen} setOpen={setModalOpen}>
|
||||
<BaseModal.Trigger>{children}</BaseModal.Trigger>
|
||||
<BaseModal.Header description={myData.node?.description}>
|
||||
<BaseModal.Header description={myData.node?.description!}>
|
||||
<span className="pr-2">{myData.type}</span>
|
||||
<Badge variant="secondary">ID: {myData.id}</Badge>
|
||||
</BaseModal.Header>
|
||||
|
|
@ -112,11 +116,11 @@ const EditNodeModal = forwardRef(
|
|||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody className="p-0">
|
||||
{Object.keys(myData.node.template)
|
||||
{Object.keys(myData.node!.template)
|
||||
.filter(
|
||||
(templateParam) =>
|
||||
templateParam.charAt(0) !== "_" &&
|
||||
myData.node.template[templateParam].show &&
|
||||
myData.node?.template[templateParam].show &&
|
||||
(myData.node.template[templateParam].type ===
|
||||
"str" ||
|
||||
myData.node.template[templateParam].type ===
|
||||
|
|
@ -135,13 +139,13 @@ const EditNodeModal = forwardRef(
|
|||
.map((templateParam, index) => (
|
||||
<TableRow key={index} className="h-10">
|
||||
<TableCell className="truncate p-0 text-center text-sm text-foreground sm:px-3">
|
||||
{myData.node.template[templateParam].name
|
||||
{myData.node?.template[templateParam].name
|
||||
? myData.node.template[templateParam].name
|
||||
: myData.node.template[templateParam]
|
||||
: myData.node?.template[templateParam]
|
||||
.display_name}
|
||||
</TableCell>
|
||||
<TableCell className="w-[300px] p-0 text-center text-xs text-foreground ">
|
||||
{myData.node.template[templateParam].type ===
|
||||
{myData.node?.template[templateParam].type ===
|
||||
"str" &&
|
||||
!myData.node.template[templateParam].options ? (
|
||||
<div className="mx-auto">
|
||||
|
|
@ -171,7 +175,7 @@ const EditNodeModal = forwardRef(
|
|||
myData.node.template[templateParam]
|
||||
.value ?? ""
|
||||
}
|
||||
onChange={(value: string) => {
|
||||
onChange={(value: string | string[]) => {
|
||||
handleOnNewValue(value, templateParam);
|
||||
}}
|
||||
/>
|
||||
|
|
@ -193,7 +197,7 @@ const EditNodeModal = forwardRef(
|
|||
/>
|
||||
)}
|
||||
</div>
|
||||
) : myData.node.template[templateParam].type ===
|
||||
) : myData.node?.template[templateParam].type ===
|
||||
"bool" ? (
|
||||
<div className="ml-auto">
|
||||
{" "}
|
||||
|
|
@ -211,7 +215,7 @@ const EditNodeModal = forwardRef(
|
|||
size="small"
|
||||
/>
|
||||
</div>
|
||||
) : myData.node.template[templateParam].type ===
|
||||
) : myData.node?.template[templateParam].type ===
|
||||
"float" ? (
|
||||
<div className="mx-auto">
|
||||
<FloatComponent
|
||||
|
|
@ -226,7 +230,7 @@ const EditNodeModal = forwardRef(
|
|||
}}
|
||||
/>
|
||||
</div>
|
||||
) : myData.node.template[templateParam].type ===
|
||||
) : myData.node?.template[templateParam].type ===
|
||||
"str" &&
|
||||
myData.node.template[templateParam].options ? (
|
||||
<div className="mx-auto">
|
||||
|
|
@ -246,7 +250,7 @@ const EditNodeModal = forwardRef(
|
|||
}
|
||||
></Dropdown>
|
||||
</div>
|
||||
) : myData.node.template[templateParam].type ===
|
||||
) : myData.node?.template[templateParam].type ===
|
||||
"int" ? (
|
||||
<div className="mx-auto">
|
||||
<IntComponent
|
||||
|
|
@ -261,7 +265,7 @@ const EditNodeModal = forwardRef(
|
|||
}}
|
||||
/>
|
||||
</div>
|
||||
) : myData.node.template[templateParam].type ===
|
||||
) : myData.node?.template[templateParam].type ===
|
||||
"file" ? (
|
||||
<div className="mx-auto">
|
||||
<InputFileComponent
|
||||
|
|
@ -271,7 +275,7 @@ const EditNodeModal = forwardRef(
|
|||
myData.node.template[templateParam]
|
||||
.value ?? ""
|
||||
}
|
||||
onChange={(value: string) => {
|
||||
onChange={(value: string | string[]) => {
|
||||
handleOnNewValue(value, templateParam);
|
||||
}}
|
||||
fileTypes={
|
||||
|
|
@ -283,13 +287,13 @@ const EditNodeModal = forwardRef(
|
|||
.suffixes
|
||||
}
|
||||
onFileChange={(filePath: string) => {
|
||||
data.node.template[
|
||||
data.node!.template[
|
||||
templateParam
|
||||
].file_path = filePath;
|
||||
}}
|
||||
></InputFileComponent>
|
||||
</div>
|
||||
) : myData.node.template[templateParam].type ===
|
||||
) : myData.node?.template[templateParam].type ===
|
||||
"prompt" ? (
|
||||
<div className="mx-auto">
|
||||
<PromptAreaComponent
|
||||
|
|
@ -304,17 +308,17 @@ const EditNodeModal = forwardRef(
|
|||
myData.node.template[templateParam]
|
||||
.value ?? ""
|
||||
}
|
||||
onChange={(value: string) => {
|
||||
onChange={(value: string | string[]) => {
|
||||
handleOnNewValue(value, templateParam);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : myData.node.template[templateParam].type ===
|
||||
) : myData.node?.template[templateParam].type ===
|
||||
"code" ? (
|
||||
<div className="mx-auto">
|
||||
<CodeAreaComponent
|
||||
dynamic={
|
||||
data.node.template[templateParam]
|
||||
data.node!.template[templateParam]
|
||||
.dynamic ?? false
|
||||
}
|
||||
setNodeClass={(nodeClass) => {
|
||||
|
|
@ -327,12 +331,12 @@ const EditNodeModal = forwardRef(
|
|||
myData.node.template[templateParam]
|
||||
.value ?? ""
|
||||
}
|
||||
onChange={(value: string) => {
|
||||
onChange={(value: string | string[]) => {
|
||||
handleOnNewValue(value, templateParam);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : myData.node.template[templateParam].type ===
|
||||
) : myData.node?.template[templateParam].type ===
|
||||
"Any" ? (
|
||||
"-"
|
||||
) : (
|
||||
|
|
@ -343,7 +347,7 @@ const EditNodeModal = forwardRef(
|
|||
<div className="items-center text-center">
|
||||
<ToggleShadComponent
|
||||
enabled={
|
||||
!myData.node.template[templateParam]
|
||||
!myData.node?.template[templateParam]
|
||||
.advanced
|
||||
}
|
||||
setEnabled={(e) =>
|
||||
|
|
@ -369,7 +373,8 @@ const EditNodeModal = forwardRef(
|
|||
className="mt-3"
|
||||
onClick={() => {
|
||||
setData(cloneDeep(myData)); //saves data with actual state of modal
|
||||
setTabsState((prev) => {
|
||||
//@ts-ignore
|
||||
setTabsState((prev: TabsState) => {
|
||||
return {
|
||||
...prev,
|
||||
[tabId]: {
|
||||
|
|
|
|||
|
|
@ -1,180 +0,0 @@
|
|||
import { useState } from "react";
|
||||
import CodeAreaComponent from "../../../../components/codeAreaComponent";
|
||||
import Dropdown from "../../../../components/dropdownComponent";
|
||||
import FloatComponent from "../../../../components/floatComponent";
|
||||
import InputComponent from "../../../../components/inputComponent";
|
||||
import InputFileComponent from "../../../../components/inputFileComponent";
|
||||
import InputListComponent from "../../../../components/inputListComponent";
|
||||
import IntComponent from "../../../../components/intComponent";
|
||||
import PromptAreaComponent from "../../../../components/promptComponent";
|
||||
import TextAreaComponent from "../../../../components/textAreaComponent";
|
||||
import ToggleComponent from "../../../../components/toggleComponent";
|
||||
import { classNames } from "../../../../utils/utils";
|
||||
|
||||
export default function ModalField({
|
||||
data,
|
||||
title,
|
||||
required,
|
||||
id,
|
||||
name,
|
||||
type,
|
||||
index,
|
||||
}) {
|
||||
const [enabled, setEnabled] = useState(
|
||||
data.node.template[name]?.value ?? false
|
||||
);
|
||||
const display =
|
||||
type === "str" ||
|
||||
type === "int" ||
|
||||
type === "prompt" ||
|
||||
type === "bool" ||
|
||||
type === "float" ||
|
||||
type === "file" ||
|
||||
type === "code";
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
"flex w-full flex-row items-center justify-between",
|
||||
display ? "" : "hidden",
|
||||
Object.keys(data.node.template).filter(
|
||||
(t) =>
|
||||
t.charAt(0) !== "_" &&
|
||||
data.node.template[t].advanced &&
|
||||
data.node.template[t].show
|
||||
).length -
|
||||
1 ===
|
||||
index
|
||||
? "pb-4"
|
||||
: ""
|
||||
)}
|
||||
>
|
||||
{display && (
|
||||
<div>
|
||||
<span className="mx-2">{title}</span>
|
||||
<span className="text-status-red">{required ? " *" : ""}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{type === "str" && !data.node.template[name].options ? (
|
||||
<div className="w-1/2">
|
||||
{data.node.template[name].list ? (
|
||||
<InputListComponent
|
||||
disabled={false}
|
||||
value={
|
||||
!data.node.template[name].value ||
|
||||
data.node.template[name].value === ""
|
||||
? [""]
|
||||
: data.node.template[name].value
|
||||
}
|
||||
onChange={(t: string[]) => {
|
||||
data.node.template[name].value = t;
|
||||
}}
|
||||
/>
|
||||
) : data.node.template[name].multiline ? (
|
||||
<TextAreaComponent
|
||||
disabled={false}
|
||||
value={data.node.template[name].value ?? ""}
|
||||
onChange={(t: string) => {
|
||||
data.node.template[name].value = t;
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<InputComponent
|
||||
disabled={false}
|
||||
password={data.node.template[name].password ?? false}
|
||||
value={data.node.template[name].value ?? ""}
|
||||
onChange={(t) => {
|
||||
data.node.template[name].value = t;
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : type === "bool" ? (
|
||||
<div className="ml-auto">
|
||||
{" "}
|
||||
<ToggleComponent
|
||||
disabled={false}
|
||||
enabled={enabled}
|
||||
setEnabled={(t) => {
|
||||
data.node.template[name].value = t;
|
||||
setEnabled(t);
|
||||
}}
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
) : type === "float" ? (
|
||||
<div className="w-1/2">
|
||||
<FloatComponent
|
||||
disabled={false}
|
||||
value={data.node.template[name].value ?? ""}
|
||||
onChange={(t) => {
|
||||
data.node.template[name].value = t;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : type === "str" && data.node.template[name].options ? (
|
||||
<div className="w-1/2">
|
||||
<Dropdown
|
||||
options={data.node.template[name].options}
|
||||
onSelect={(newValue) => (data.node.template[name].value = newValue)}
|
||||
value={data.node.template[name].value ?? "Choose an option"}
|
||||
></Dropdown>
|
||||
</div>
|
||||
) : type === "int" ? (
|
||||
<div className="w-1/2">
|
||||
<IntComponent
|
||||
disabled={false}
|
||||
value={data.node.template[name].value ?? ""}
|
||||
onChange={(t) => {
|
||||
data.node.template[name].value = t;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : type === "file" ? (
|
||||
<div className="w-1/2">
|
||||
<InputFileComponent
|
||||
disabled={false}
|
||||
value={data.node.template[name].value ?? ""}
|
||||
onChange={(t: string) => {
|
||||
data.node.template[name].value = t;
|
||||
}}
|
||||
fileTypes={data.node.template[name].fileTypes}
|
||||
suffixes={data.node.template[name].suffixes}
|
||||
onFileChange={(t: string) => {
|
||||
data.node.template[name].file_path = t;
|
||||
}}
|
||||
></InputFileComponent>
|
||||
</div>
|
||||
) : type === "prompt" ? (
|
||||
<div className="w-1/2">
|
||||
<PromptAreaComponent
|
||||
field_name={name}
|
||||
disabled={false}
|
||||
value={data.node.template[name].value ?? ""}
|
||||
onChange={(t: string) => {
|
||||
data.node.template[name].value = t;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : type === "code" ? (
|
||||
<div className="w-1/2">
|
||||
<CodeAreaComponent
|
||||
dynamic={data.node.template[name].dynamic ?? false}
|
||||
setNodeClass={(nodeClass) => {
|
||||
data.node = nodeClass;
|
||||
}}
|
||||
nodeClass={data.node}
|
||||
disabled={false}
|
||||
value={data.node.template[name].value ?? ""}
|
||||
onChange={(t: string) => {
|
||||
data.node.template[name].value = t;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="hidden"></div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,151 +0,0 @@
|
|||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { Fragment, useContext, useRef, useState } from "react";
|
||||
import IconComponent from "../../components/genericIconComponent";
|
||||
import { limitScrollFieldsModal } from "../../constants/constants";
|
||||
import { typesContext } from "../../contexts/typesContext";
|
||||
import { NodeDataType } from "../../types/flow";
|
||||
import { nodeColors, nodeIconsLucide } from "../../utils/styleUtils";
|
||||
import { classNames, toTitleCase } from "../../utils/utils";
|
||||
import ModalField from "./components/ModalField";
|
||||
|
||||
export default function NodeModal({ data }: { data: NodeDataType }) {
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const { types } = useContext(typesContext);
|
||||
const ref = useRef();
|
||||
// any to avoid type conflict
|
||||
const Icon: any = nodeIconsLucide[types[data.type]];
|
||||
return (
|
||||
<Dialog
|
||||
open={modalOpen}
|
||||
as="div"
|
||||
className="relative z-10"
|
||||
onClose={setModalOpen}
|
||||
initialFocus={ref}
|
||||
>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="node-modal-div" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="node-modal-dialog-arrangement">
|
||||
<div className="node-modal-dialog-div">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<Dialog.Panel className="node-modal-dialog-panel">
|
||||
<div className=" node-modal-dialog-panel-div">
|
||||
<button
|
||||
type="button"
|
||||
className="node-modal-dialog-button"
|
||||
onClick={() => {
|
||||
setModalOpen(false);
|
||||
}}
|
||||
>
|
||||
<span className="sr-only">Close</span>
|
||||
<IconComponent
|
||||
name="X"
|
||||
className="h-6 w-6"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div className="node-modal-dialog-icon-div">
|
||||
<div className="node-modal-icon-arrangement">
|
||||
<Icon
|
||||
strokeWidth={1.5}
|
||||
className="node-modal-icon"
|
||||
style={{
|
||||
color: nodeColors[types[data.type]] ?? nodeColors.unknown,
|
||||
}}
|
||||
/>
|
||||
<div className="node-modal-title-div">
|
||||
<Dialog.Title as="h3" className="node-modal-title">
|
||||
{data.type}
|
||||
</Dialog.Title>
|
||||
</div>
|
||||
</div>
|
||||
<div className="node-modal-template-div">
|
||||
<div className="flex-max-width h-[445px]">
|
||||
<div
|
||||
className={classNames(
|
||||
"node-modal-template",
|
||||
Object.keys(data.node.template).filter(
|
||||
(t) =>
|
||||
t.charAt(0) !== "_" &&
|
||||
data.node.template[t].advanced &&
|
||||
data.node.template[t].show
|
||||
).length > limitScrollFieldsModal
|
||||
? "overflow-scroll overflow-x-hidden custom-scroll"
|
||||
: "overflow-hidden"
|
||||
)}
|
||||
>
|
||||
<div className="node-modal-template-column">
|
||||
{Object.keys(data.node.template)
|
||||
.filter(
|
||||
(t) =>
|
||||
t.charAt(0) !== "_" &&
|
||||
data.node.template[t].advanced &&
|
||||
data.node.template[t].show
|
||||
)
|
||||
.map((t: string, idx) => {
|
||||
return (
|
||||
<ModalField
|
||||
key={idx}
|
||||
data={data}
|
||||
title={
|
||||
data.node.template[t].display_name
|
||||
? data.node.template[t].display_name
|
||||
: data.node.template[t].name
|
||||
? toTitleCase(data.node.template[t].name)
|
||||
: toTitleCase(t)
|
||||
}
|
||||
required={data.node.template[t].required}
|
||||
id={
|
||||
data.node.template[t].type +
|
||||
"|" +
|
||||
t +
|
||||
"|" +
|
||||
data.id
|
||||
}
|
||||
name={t}
|
||||
type={data.node.template[t].type}
|
||||
index={idx}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="node-modal-button-box">
|
||||
<button
|
||||
type="button"
|
||||
className="node-modal-button"
|
||||
onClick={() => {
|
||||
setModalOpen(false);
|
||||
}}
|
||||
>
|
||||
Done
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
241
src/frontend/src/modals/UserManagementModal/index.tsx
Normal file
241
src/frontend/src/modals/UserManagementModal/index.tsx
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
import * as Form from "@radix-ui/react-form";
|
||||
import { useEffect, useState } from "react";
|
||||
import InputComponent from "../../components/inputComponent";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import { UserManagementType } from "../../types/components";
|
||||
import { nodeIconsLucide } from "../../utils/styleUtils";
|
||||
import BaseModal from "../baseModal";
|
||||
|
||||
export default function UserManagementModal({
|
||||
title,
|
||||
titleHeader,
|
||||
cancelText,
|
||||
confirmationText,
|
||||
children,
|
||||
icon,
|
||||
data,
|
||||
index,
|
||||
onConfirm,
|
||||
}: UserManagementType) {
|
||||
const Icon: any = nodeIconsLucide[icon];
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const [password, setPassword] = useState(data?.password ?? "");
|
||||
const [username, setUserName] = useState(data?.user ?? "");
|
||||
const [confirmPassword, setConfirmPassword] = useState(data?.password ?? "");
|
||||
|
||||
useEffect(() => {
|
||||
if (!data) {
|
||||
resetForm();
|
||||
}
|
||||
}, [data, open]);
|
||||
|
||||
function resetForm() {
|
||||
setPassword("");
|
||||
setUserName("");
|
||||
setConfirmPassword("");
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseModal size="medium-h-full" open={open} setOpen={setOpen}>
|
||||
<BaseModal.Trigger>{children}</BaseModal.Trigger>
|
||||
<BaseModal.Header description={titleHeader}>
|
||||
<span className="pr-2">{title}</span>
|
||||
<Icon
|
||||
name="icon"
|
||||
className="h-6 w-6 pl-1 text-foreground"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</BaseModal.Header>
|
||||
<BaseModal.Content>
|
||||
<Form.Root
|
||||
onSubmit={(event) => {
|
||||
if (password !== confirmPassword) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
const data = Object.fromEntries(new FormData(event.currentTarget));
|
||||
resetForm();
|
||||
onConfirm(index ?? -1, data);
|
||||
setOpen(false);
|
||||
event.preventDefault();
|
||||
}}
|
||||
>
|
||||
<div className="grid gap-5">
|
||||
<Form.Field name="username">
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "baseline",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<Form.Label className="data-[invalid]:label-invalid">
|
||||
Username{" "}
|
||||
<span className="font-medium text-destructive">*</span>
|
||||
</Form.Label>
|
||||
</div>
|
||||
<Form.Control asChild>
|
||||
<input
|
||||
onChange={(input) => {
|
||||
setUserName(input.target.value);
|
||||
}}
|
||||
value={username}
|
||||
className="primary-input"
|
||||
required
|
||||
placeholder="Username"
|
||||
/>
|
||||
</Form.Control>
|
||||
<Form.Message match="valueMissing" className="field-invalid">
|
||||
Please enter your username
|
||||
</Form.Message>
|
||||
</Form.Field>
|
||||
|
||||
<div className="flex flex-row">
|
||||
<div className="mr-3 basis-1/2">
|
||||
<Form.Field
|
||||
name="password"
|
||||
serverInvalid={password != confirmPassword}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "baseline",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<Form.Label className="data-[invalid]:label-invalid">
|
||||
Password{" "}
|
||||
<span className="font-medium text-destructive">*</span>
|
||||
</Form.Label>
|
||||
</div>
|
||||
<InputComponent
|
||||
onChange={(input) => {
|
||||
setPassword(input);
|
||||
}}
|
||||
value={password}
|
||||
password={true}
|
||||
isForm
|
||||
className="primary-input"
|
||||
required
|
||||
placeholder="Password"
|
||||
/>
|
||||
<Form.Message className="field-invalid" match="valueMissing">
|
||||
Please enter a password
|
||||
</Form.Message>
|
||||
|
||||
{password != confirmPassword && (
|
||||
<Form.Message className="field-invalid">
|
||||
Passwords do not match
|
||||
</Form.Message>
|
||||
)}
|
||||
</Form.Field>
|
||||
</div>
|
||||
|
||||
<div className="basis-1/2">
|
||||
<Form.Field
|
||||
name="confirmpassword"
|
||||
serverInvalid={password != confirmPassword}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "baseline",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<Form.Label className="data-[invalid]:label-invalid">
|
||||
Confirm password{" "}
|
||||
<span className="font-medium text-destructive">*</span>
|
||||
</Form.Label>
|
||||
</div>
|
||||
<InputComponent
|
||||
onChange={(input) => {
|
||||
setConfirmPassword(input);
|
||||
}}
|
||||
value={confirmPassword}
|
||||
password={true}
|
||||
isForm
|
||||
className="primary-input"
|
||||
required
|
||||
placeholder="Confirm your password"
|
||||
/>
|
||||
<Form.Message className="field-invalid" match="valueMissing">
|
||||
Please confirm your password
|
||||
</Form.Message>
|
||||
</Form.Field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/*
|
||||
<Form.Field name="email">
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "baseline",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<Form.Label className="data-[invalid]:label-invalid">
|
||||
Email <span className="font-medium text-destructive">*</span>
|
||||
</Form.Label>
|
||||
<Form.Message className="field-invalid" match="valueMissing">
|
||||
Please enter your email
|
||||
</Form.Message>
|
||||
<Form.Message className="field-invalid" match="typeMismatch">
|
||||
Please provide a valid email
|
||||
</Form.Message>
|
||||
</div>
|
||||
<Form.Control asChild>
|
||||
<input className="primary-input" type="email" required />
|
||||
</Form.Control>
|
||||
</Form.Field> */}
|
||||
|
||||
{/*
|
||||
<Form.Field name="birth">
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "baseline",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<Form.Label className="data-[invalid]:label-invalid">
|
||||
Date of birth{" "}
|
||||
<span className="font-medium text-destructive">*</span>
|
||||
</Form.Label>
|
||||
<Form.Message className="field-invalid" match="valueMissing">
|
||||
Please enter your date of birth
|
||||
</Form.Message>
|
||||
</div>
|
||||
<Form.Control asChild>
|
||||
<input
|
||||
type="date"
|
||||
className="primary-input"
|
||||
required
|
||||
max={new Date().toISOString().split("T")[0]}
|
||||
/>
|
||||
</Form.Control>
|
||||
</Form.Field> */}
|
||||
</div>
|
||||
|
||||
<div className="float-right">
|
||||
<Form.Submit asChild>
|
||||
<Button className="mr-3 mt-8">{confirmationText}</Button>
|
||||
</Form.Submit>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
{cancelText}
|
||||
</Button>
|
||||
</div>
|
||||
</Form.Root>
|
||||
</BaseModal.Content>
|
||||
</BaseModal>
|
||||
);
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ import {
|
|||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "../../components/ui/dialog";
|
||||
import { modalHeaderType } from "../../types/components";
|
||||
|
||||
type ContentProps = { children: ReactNode };
|
||||
type HeaderProps = { children: ReactNode; description: string };
|
||||
|
|
@ -22,10 +23,10 @@ const Trigger: React.FC<ContentProps> = ({ children }) => {
|
|||
return <>{children}</>;
|
||||
};
|
||||
|
||||
const Header: React.FC<{ children: ReactNode; description: string }> = ({
|
||||
const Header: React.FC<{ children: ReactNode; description: string | null }> = ({
|
||||
children,
|
||||
description,
|
||||
}) => {
|
||||
}: modalHeaderType): JSX.Element => {
|
||||
return (
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center">{children}</DialogTitle>
|
||||
|
|
@ -47,7 +48,15 @@ interface BaseModalProps {
|
|||
open?: boolean;
|
||||
setOpen?: (open: boolean) => void;
|
||||
disable?: boolean;
|
||||
size?: "smaller" | "small" | "medium" | "large" | "large-h-full";
|
||||
size?:
|
||||
| "x-small"
|
||||
| "smaller"
|
||||
| "small"
|
||||
| "medium"
|
||||
| "large"
|
||||
| "large-h-full"
|
||||
| "small-h-full"
|
||||
| "medium-h-full";
|
||||
}
|
||||
function BaseModal({
|
||||
open,
|
||||
|
|
@ -73,6 +82,10 @@ function BaseModal({
|
|||
let height: string;
|
||||
|
||||
switch (size) {
|
||||
case "x-small":
|
||||
minWidth = "min-w-[20vw]";
|
||||
height = "h-[10vh]";
|
||||
break;
|
||||
case "smaller":
|
||||
minWidth = "min-w-[40vw]";
|
||||
height = "h-[27vh]";
|
||||
|
|
@ -81,10 +94,16 @@ function BaseModal({
|
|||
minWidth = "min-w-[40vw]";
|
||||
height = "h-[40vh]";
|
||||
break;
|
||||
case "small-h-full":
|
||||
minWidth = "min-w-[40vw]";
|
||||
break;
|
||||
case "medium":
|
||||
minWidth = "min-w-[60vw]";
|
||||
height = "h-[60vh]";
|
||||
break;
|
||||
case "medium-h-full":
|
||||
minWidth = "min-w-[60vw]";
|
||||
break;
|
||||
case "large":
|
||||
minWidth = "min-w-[80vw]";
|
||||
height = "h-[80vh]";
|
||||
|
|
@ -108,7 +127,9 @@ function BaseModal({
|
|||
{triggerChild}
|
||||
</DialogTrigger>
|
||||
<DialogContent className={minWidth}>
|
||||
{headerChild}
|
||||
<div className="truncate-doubleline word-break-break-word">
|
||||
{headerChild}
|
||||
</div>
|
||||
<div className={`mt-2 flex flex-col ${height} w-full `}>
|
||||
{ContentChild}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import "ace-builds/src-noconflict/mode-python";
|
|||
import "ace-builds/src-noconflict/theme-github";
|
||||
import "ace-builds/src-noconflict/theme-twilight";
|
||||
// import "ace-builds/webpack-resolver";
|
||||
import { ReactNode, useContext, useEffect, useState } from "react";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import AceEditor from "react-ace";
|
||||
import IconComponent from "../../components/genericIconComponent";
|
||||
import { Button } from "../../components/ui/button";
|
||||
|
|
@ -13,7 +13,7 @@ import { alertContext } from "../../contexts/alertContext";
|
|||
import { darkContext } from "../../contexts/darkContext";
|
||||
import { typesContext } from "../../contexts/typesContext";
|
||||
import { postCustomComponent, postValidateCode } from "../../controllers/API";
|
||||
import { APIClassType } from "../../types/api";
|
||||
import { codeAreaModalPropsType } from "../../types/components";
|
||||
import BaseModal from "../baseModal";
|
||||
|
||||
export default function CodeAreaModal({
|
||||
|
|
@ -23,27 +23,20 @@ export default function CodeAreaModal({
|
|||
setNodeClass,
|
||||
children,
|
||||
dynamic,
|
||||
}: {
|
||||
setValue: (value: string) => void;
|
||||
value: string;
|
||||
nodeClass?: APIClassType;
|
||||
children: ReactNode;
|
||||
setNodeClass?: (Class: APIClassType) => void;
|
||||
dynamic?: boolean;
|
||||
}) {
|
||||
}: codeAreaModalPropsType): JSX.Element {
|
||||
const [code, setCode] = useState(value);
|
||||
const { dark } = useContext(darkContext);
|
||||
const { reactFlowInstance } = useContext(typesContext);
|
||||
const [height, setHeight] = useState(null);
|
||||
const [height, setHeight] = useState<string | null>(null);
|
||||
const { setErrorData, setSuccessData } = useContext(alertContext);
|
||||
const [error, setError] = useState<{
|
||||
detail: { error: string; traceback: string };
|
||||
}>(null);
|
||||
detail: { error: string | undefined; traceback: string | undefined };
|
||||
} | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// if nodeClass.template has more fields other than code and dynamic is true
|
||||
// do not run handleClick
|
||||
if (dynamic && Object.keys(nodeClass.template).length > 2) {
|
||||
if (dynamic && Object.keys(nodeClass!.template).length > 2) {
|
||||
return;
|
||||
}
|
||||
processCode();
|
||||
|
|
@ -90,7 +83,7 @@ export default function CodeAreaModal({
|
|||
}
|
||||
|
||||
function processDynamicField() {
|
||||
postCustomComponent(code, nodeClass)
|
||||
postCustomComponent(code, nodeClass!)
|
||||
.then((apiReturn) => {
|
||||
const { data } = apiReturn;
|
||||
if (data) {
|
||||
|
|
|
|||
|
|
@ -8,77 +8,78 @@ import { TabsContext } from "../../contexts/tabsContext";
|
|||
import { removeApiKeys } from "../../utils/reactflowUtils";
|
||||
import BaseModal from "../baseModal";
|
||||
|
||||
const ExportModal = forwardRef((props: { children: ReactNode }, ref) => {
|
||||
const { flows, tabId, updateFlow, downloadFlow, saveFlow } =
|
||||
useContext(TabsContext);
|
||||
const [checked, setChecked] = useState(false);
|
||||
const [name, setName] = useState(
|
||||
flows.find((flow) => flow.id === tabId).name
|
||||
);
|
||||
const [invalidName, setInvalidName] = useState(false);
|
||||
const [description, setDescription] = useState(
|
||||
flows.find((flow) => flow.id === tabId).description
|
||||
);
|
||||
const [open, setOpen] = useState(false);
|
||||
return (
|
||||
<BaseModal size="smaller" open={open} setOpen={setOpen}>
|
||||
<BaseModal.Trigger>{props.children}</BaseModal.Trigger>
|
||||
<BaseModal.Header description={EXPORT_DIALOG_SUBTITLE}>
|
||||
<span className="pr-2">Export</span>
|
||||
<IconComponent
|
||||
name="Download"
|
||||
className="h-6 w-6 pl-1 text-foreground"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</BaseModal.Header>
|
||||
<BaseModal.Content>
|
||||
<EditFlowSettings
|
||||
invalidName={invalidName}
|
||||
setInvalidName={setInvalidName}
|
||||
name={name}
|
||||
description={description}
|
||||
flows={flows}
|
||||
tabId={tabId}
|
||||
setName={setName}
|
||||
setDescription={setDescription}
|
||||
updateFlow={updateFlow}
|
||||
/>
|
||||
<div className="mt-3 flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="terms"
|
||||
onCheckedChange={(event: boolean) => {
|
||||
setChecked(event);
|
||||
}}
|
||||
const ExportModal = forwardRef(
|
||||
(props: { children: ReactNode }, ref): JSX.Element => {
|
||||
const { flows, tabId, updateFlow, downloadFlow, saveFlow } =
|
||||
useContext(TabsContext);
|
||||
const [checked, setChecked] = useState(false);
|
||||
const [name, setName] = useState(
|
||||
flows.find((flow) => flow.id === tabId)?.name
|
||||
);
|
||||
const [invalidName, setInvalidName] = useState(false);
|
||||
const [description, setDescription] = useState(
|
||||
flows.find((flow) => flow.id === tabId)?.description
|
||||
);
|
||||
const [open, setOpen] = useState(false);
|
||||
return (
|
||||
<BaseModal size="smaller" open={open} setOpen={setOpen}>
|
||||
<BaseModal.Trigger>{props.children}</BaseModal.Trigger>
|
||||
<BaseModal.Header description={EXPORT_DIALOG_SUBTITLE}>
|
||||
<span className="pr-2">Export</span>
|
||||
<IconComponent
|
||||
name="Download"
|
||||
className="h-6 w-6 pl-1 text-foreground"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<label htmlFor="terms" className="export-modal-save-api text-sm ">
|
||||
Save with my API keys
|
||||
</label>
|
||||
</div>
|
||||
</BaseModal.Content>
|
||||
</BaseModal.Header>
|
||||
<BaseModal.Content>
|
||||
<EditFlowSettings
|
||||
invalidName={invalidName}
|
||||
setInvalidName={setInvalidName}
|
||||
name={name!}
|
||||
description={description!}
|
||||
flows={flows}
|
||||
tabId={tabId}
|
||||
setName={setName}
|
||||
setDescription={setDescription}
|
||||
/>
|
||||
<div className="mt-3 flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="terms"
|
||||
onCheckedChange={(event: boolean): void => {
|
||||
setChecked(event);
|
||||
}}
|
||||
/>
|
||||
<label htmlFor="terms" className="export-modal-save-api text-sm ">
|
||||
Save with my API keys
|
||||
</label>
|
||||
</div>
|
||||
</BaseModal.Content>
|
||||
|
||||
<BaseModal.Footer>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (checked)
|
||||
downloadFlow(
|
||||
flows.find((flow) => flow.id === tabId),
|
||||
name,
|
||||
description
|
||||
);
|
||||
else
|
||||
downloadFlow(
|
||||
removeApiKeys(flows.find((flow) => flow.id === tabId)),
|
||||
name,
|
||||
description
|
||||
);
|
||||
setOpen(false);
|
||||
}}
|
||||
type="submit"
|
||||
>
|
||||
Download Flow
|
||||
</Button>
|
||||
</BaseModal.Footer>
|
||||
</BaseModal>
|
||||
);
|
||||
});
|
||||
<BaseModal.Footer>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (checked)
|
||||
downloadFlow(
|
||||
flows.find((flow) => flow.id === tabId)!,
|
||||
name!,
|
||||
description
|
||||
);
|
||||
else
|
||||
downloadFlow(
|
||||
removeApiKeys(flows.find((flow) => flow.id === tabId)!),
|
||||
name!,
|
||||
description
|
||||
);
|
||||
setOpen(false);
|
||||
}}
|
||||
type="submit"
|
||||
>
|
||||
Download Flow
|
||||
</Button>
|
||||
</BaseModal.Footer>
|
||||
</BaseModal>
|
||||
);
|
||||
}
|
||||
);
|
||||
export default ExportModal;
|
||||
|
|
|
|||
|
|
@ -5,33 +5,30 @@ import { Button } from "../../components/ui/button";
|
|||
import { SETTINGS_DIALOG_SUBTITLE } from "../../constants/constants";
|
||||
import { alertContext } from "../../contexts/alertContext";
|
||||
import { TabsContext } from "../../contexts/tabsContext";
|
||||
import { FlowSettingsPropsType } from "../../types/components";
|
||||
import BaseModal from "../baseModal";
|
||||
|
||||
export default function FlowSettingsModal({
|
||||
open,
|
||||
setOpen,
|
||||
}: {
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
}) {
|
||||
}: FlowSettingsPropsType): JSX.Element {
|
||||
const { setErrorData, setSuccessData } = useContext(alertContext);
|
||||
const ref = useRef();
|
||||
const { flows, tabId, updateFlow, setTabsState, saveFlow } =
|
||||
useContext(TabsContext);
|
||||
const { flows, tabId, updateFlow, saveFlow } = useContext(TabsContext);
|
||||
const maxLength = 50;
|
||||
const [name, setName] = useState(
|
||||
flows.find((flow) => flow.id === tabId).name
|
||||
flows.find((flow) => flow.id === tabId)!.name
|
||||
);
|
||||
const [description, setDescription] = useState(
|
||||
flows.find((flow) => flow.id === tabId).description
|
||||
flows.find((flow) => flow.id === tabId)!.description
|
||||
);
|
||||
const [invalidName, setInvalidName] = useState(false);
|
||||
|
||||
function handleClick() {
|
||||
function handleClick(): void {
|
||||
let savedFlow = flows.find((flow) => flow.id === tabId);
|
||||
savedFlow.name = name;
|
||||
savedFlow.description = description;
|
||||
saveFlow(savedFlow);
|
||||
savedFlow!.name = name;
|
||||
savedFlow!.description = description;
|
||||
saveFlow(savedFlow!);
|
||||
setSuccessData({ title: "Changes saved successfully" });
|
||||
setOpen(false);
|
||||
}
|
||||
|
|
@ -51,7 +48,6 @@ export default function FlowSettingsModal({
|
|||
tabId={tabId}
|
||||
setName={setName}
|
||||
setDescription={setDescription}
|
||||
updateFlow={updateFlow}
|
||||
/>
|
||||
</BaseModal.Content>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { useEffect } from "react";
|
||||
import IconComponent from "../../../components/genericIconComponent";
|
||||
import { Textarea } from "../../../components/ui/textarea";
|
||||
import { chatInputType } from "../../../types/components";
|
||||
import { classNames } from "../../../utils/utils";
|
||||
|
||||
export default function ChatInput({
|
||||
|
|
@ -10,7 +11,7 @@ export default function ChatInput({
|
|||
setChatValue,
|
||||
inputRef,
|
||||
noInput,
|
||||
}) {
|
||||
}: chatInputType): JSX.Element {
|
||||
useEffect(() => {
|
||||
if (!lockChat && inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
|
|
@ -46,7 +47,7 @@ export default function ChatInput({
|
|||
}`,
|
||||
}}
|
||||
value={lockChat ? "Thinking..." : chatValue}
|
||||
onChange={(event) => {
|
||||
onChange={(event): void => {
|
||||
setChatValue(event.target.value);
|
||||
}}
|
||||
className={classNames(
|
||||
|
|
@ -75,7 +76,7 @@ export default function ChatInput({
|
|||
: "bg-chat-send text-background"
|
||||
)}
|
||||
disabled={lockChat}
|
||||
onClick={() => sendMessage()}
|
||||
onClick={(): void => sendMessage()}
|
||||
>
|
||||
{lockChat ? (
|
||||
<IconComponent
|
||||
|
|
|
|||
|
|
@ -3,16 +3,12 @@ import { useState } from "react";
|
|||
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
||||
import { oneDark } from "react-syntax-highlighter/dist/cjs/styles/prism";
|
||||
import { programmingLanguages } from "../../../../constants/constants";
|
||||
import { Props } from "../../../../types/components";
|
||||
|
||||
interface Props {
|
||||
language: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export function CodeBlock({ language, value }) {
|
||||
export function CodeBlock({ language, value }: Props): JSX.Element {
|
||||
const [isCopied, setIsCopied] = useState<Boolean>(false);
|
||||
|
||||
const copyToClipboard = () => {
|
||||
const copyToClipboard = (): void => {
|
||||
if (!navigator.clipboard || !navigator.clipboard.writeText) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -25,7 +21,7 @@ export function CodeBlock({ language, value }) {
|
|||
}, 2000);
|
||||
});
|
||||
};
|
||||
const downloadAsFile = () => {
|
||||
const downloadAsFile = (): void => {
|
||||
const fileExtension = programmingLanguages[language] || ".file";
|
||||
const suggestedFileName = `${"generated-code"}${fileExtension}`;
|
||||
const fileName = window.prompt("enter file name", suggestedFileName);
|
||||
|
|
|
|||
|
|
@ -9,18 +9,15 @@ import Robot from "../../../assets/robot.png";
|
|||
import SanitizedHTMLWrapper from "../../../components/SanitizedHTMLWrapper";
|
||||
import CodeTabsComponent from "../../../components/codeTabsComponent";
|
||||
import IconComponent from "../../../components/genericIconComponent";
|
||||
import { ChatMessageType } from "../../../types/chat";
|
||||
import { chatMessagePropsType } from "../../../types/components";
|
||||
import { classNames } from "../../../utils/utils";
|
||||
import FileCard from "../fileComponent";
|
||||
|
||||
export default function ChatMessage({
|
||||
chat,
|
||||
lockChat,
|
||||
lastMessage,
|
||||
}: {
|
||||
chat: ChatMessageType;
|
||||
lockChat: boolean;
|
||||
lastMessage: boolean;
|
||||
}) {
|
||||
}: chatMessagePropsType): JSX.Element {
|
||||
const convert = new Convert({ newline: true });
|
||||
const [hidden, setHidden] = useState(true);
|
||||
const template = chat.template;
|
||||
|
|
@ -57,7 +54,7 @@ export default function ChatMessage({
|
|||
<div className="form-modal-chat-text">
|
||||
{hidden && chat.thought && chat.thought !== "" && (
|
||||
<div
|
||||
onClick={() => setHidden((prev) => !prev)}
|
||||
onClick={(): void => setHidden((prev) => !prev)}
|
||||
className="form-modal-chat-icon-div"
|
||||
>
|
||||
<IconComponent
|
||||
|
|
@ -192,7 +189,7 @@ export default function ChatMessage({
|
|||
? template?.split("\n")?.map((line, index) => {
|
||||
const regex = /{([^}]+)}/g;
|
||||
let match;
|
||||
let parts = [];
|
||||
let parts: Array<JSX.Element | string> = [];
|
||||
let lastIndex = 0;
|
||||
while ((match = regex.exec(line)) !== null) {
|
||||
// Push text up to the match
|
||||
|
|
|
|||
|
|
@ -1,9 +1,14 @@
|
|||
import * as base64js from "base64-js";
|
||||
import { useState } from "react";
|
||||
import IconComponent from "../../../components/genericIconComponent";
|
||||
import { fileCardPropsType } from "../../../types/components";
|
||||
|
||||
export default function FileCard({ fileName, content, fileType }) {
|
||||
const handleDownload = () => {
|
||||
export default function FileCard({
|
||||
fileName,
|
||||
content,
|
||||
fileType,
|
||||
}: fileCardPropsType): JSX.Element {
|
||||
const handleDownload = (): void => {
|
||||
const byteArray = new Uint8Array(base64js.toByteArray(content));
|
||||
const blob = new Blob([byteArray], { type: "application/octet-stream" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
|
@ -16,10 +21,10 @@ export default function FileCard({ fileName, content, fileType }) {
|
|||
URL.revokeObjectURL(url);
|
||||
};
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
function handleMouseEnter() {
|
||||
function handleMouseEnter(): void {
|
||||
setIsHovered(true);
|
||||
}
|
||||
function handleMouseLeave() {
|
||||
function handleMouseLeave(): void {
|
||||
setIsHovered(false);
|
||||
}
|
||||
|
||||
|
|
@ -55,7 +60,7 @@ export default function FileCard({ fileName, content, fileType }) {
|
|||
return (
|
||||
<button onClick={handleDownload} className="file-card-modal-button">
|
||||
<div className="file-card-modal-div">
|
||||
{" "}
|
||||
ooooooooooooooo{" "}
|
||||
{fileType === "image" ? (
|
||||
<img
|
||||
src={`data:image/png;base64,${content}`}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import {
|
|||
import { Textarea } from "../../components/ui/textarea";
|
||||
import { CHAT_FORM_DIALOG_SUBTITLE } from "../../constants/constants";
|
||||
import { TabsContext } from "../../contexts/tabsContext";
|
||||
import { TabsState } from "../../types/tabs";
|
||||
import { validateNodes } from "../../utils/reactflowUtils";
|
||||
|
||||
export default function FormModal({
|
||||
|
|
@ -34,7 +35,7 @@ export default function FormModal({
|
|||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
flow: FlowType;
|
||||
}) {
|
||||
}): JSX.Element {
|
||||
const { tabsState, setTabsState } = useContext(TabsContext);
|
||||
const [chatValue, setChatValue] = useState(() => {
|
||||
try {
|
||||
|
|
@ -45,11 +46,11 @@ export default function FormModal({
|
|||
const inputKeys = formKeysData.input_keys;
|
||||
const handleKeys = formKeysData.handle_keys;
|
||||
|
||||
const keyToUse = Object.keys(inputKeys).find(
|
||||
(key) => !handleKeys.some((j) => j === key) && inputKeys[key] === ""
|
||||
const keyToUse = Object.keys(inputKeys!).find(
|
||||
(key) => !handleKeys?.some((j) => j === key) && inputKeys![key] === ""
|
||||
);
|
||||
|
||||
return inputKeys[keyToUse];
|
||||
return inputKeys![keyToUse!];
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
// return a sensible default or `undefined` if no default is possible
|
||||
|
|
@ -63,16 +64,17 @@ export default function FormModal({
|
|||
const ws = useRef<WebSocket | null>(null);
|
||||
const [lockChat, setLockChat] = useState(false);
|
||||
const isOpen = useRef(open);
|
||||
const messagesRef = useRef(null);
|
||||
const messagesRef = useRef<HTMLDivElement | null>(null);
|
||||
const id = useRef(flow.id);
|
||||
const tabsStateFlowId = tabsState[flow.id];
|
||||
const tabsStateFlowIdFormKeysData = tabsStateFlowId.formKeysData;
|
||||
const [chatKey, setChatKey] = useState(() => {
|
||||
if (tabsState[flow.id]?.formKeysData?.input_keys) {
|
||||
return Object.keys(tabsState[flow.id].formKeysData.input_keys).find(
|
||||
return Object.keys(tabsState[flow.id].formKeysData.input_keys!).find(
|
||||
(key) =>
|
||||
!tabsState[flow.id].formKeysData.handle_keys.some((j) => j === key) &&
|
||||
tabsState[flow.id].formKeysData.input_keys[key] === ""
|
||||
!tabsState[flow.id].formKeysData.handle_keys!.some(
|
||||
(j) => j === key
|
||||
) && tabsState[flow.id].formKeysData.input_keys![key] === ""
|
||||
);
|
||||
}
|
||||
// TODO: return a sensible default
|
||||
|
|
@ -149,7 +151,7 @@ export default function FormModal({
|
|||
});
|
||||
}
|
||||
|
||||
function handleOnClose(event: CloseEvent) {
|
||||
function handleOnClose(event: CloseEvent): void {
|
||||
if (isOpen.current) {
|
||||
setErrorData({ title: event.reason });
|
||||
setTimeout(() => {
|
||||
|
|
@ -159,7 +161,10 @@ export default function FormModal({
|
|||
}
|
||||
}
|
||||
|
||||
function getWebSocketUrl(chatId, isDevelopment = false) {
|
||||
function getWebSocketUrl(
|
||||
chatId: string,
|
||||
isDevelopment: boolean = false
|
||||
): string {
|
||||
const isSecureProtocol =
|
||||
window.location.protocol === "https:" || window.location.port === "443";
|
||||
const webSocketProtocol = isSecureProtocol ? "wss" : "ws";
|
||||
|
|
@ -212,7 +217,7 @@ export default function FormModal({
|
|||
});
|
||||
}
|
||||
if (data.type === "start") {
|
||||
addChatHistory("", false, chatKey);
|
||||
addChatHistory("", false, chatKey!);
|
||||
isStream = true;
|
||||
}
|
||||
if (data.type === "end") {
|
||||
|
|
@ -241,7 +246,7 @@ export default function FormModal({
|
|||
}
|
||||
}
|
||||
|
||||
function connectWS() {
|
||||
function connectWS(): void {
|
||||
try {
|
||||
const urlWs = getWebSocketUrl(
|
||||
id.current,
|
||||
|
|
@ -308,15 +313,15 @@ export default function FormModal({
|
|||
// do not add connectWS on dependencies array
|
||||
}, [lockChat]);
|
||||
|
||||
async function sendAll(data: sendAllProps) {
|
||||
async function sendAll(data: sendAllProps): Promise<void> {
|
||||
try {
|
||||
if (ws) {
|
||||
ws.current.send(JSON.stringify(data));
|
||||
ws.current?.send(JSON.stringify(data));
|
||||
}
|
||||
} catch (error) {
|
||||
setErrorData({
|
||||
title: "There was an error sending the message",
|
||||
list: [error.message],
|
||||
list: [(error as { message: string }).message],
|
||||
});
|
||||
setChatValue(data.inputs);
|
||||
connectWS();
|
||||
|
|
@ -327,7 +332,7 @@ export default function FormModal({
|
|||
if (ref.current) ref.current.scrollIntoView({ behavior: "smooth" });
|
||||
}, [chatHistory]);
|
||||
|
||||
const ref = useRef(null);
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (open && ref.current) {
|
||||
|
|
@ -335,30 +340,31 @@ export default function FormModal({
|
|||
}
|
||||
}, [open]);
|
||||
|
||||
function sendMessage() {
|
||||
let nodeValidationErrors = validateNodes(reactFlowInstance);
|
||||
function sendMessage(): void {
|
||||
let nodeValidationErrors = validateNodes(reactFlowInstance!);
|
||||
if (nodeValidationErrors.length === 0) {
|
||||
setLockChat(true);
|
||||
let inputs = tabsState[id.current].formKeysData.input_keys;
|
||||
setChatValue("");
|
||||
const message = inputs;
|
||||
addChatHistory(
|
||||
message,
|
||||
message!,
|
||||
true,
|
||||
chatKey,
|
||||
chatKey!,
|
||||
tabsState[flow.id].formKeysData.template
|
||||
);
|
||||
sendAll({
|
||||
...reactFlowInstance.toObject(),
|
||||
inputs: inputs,
|
||||
...reactFlowInstance?.toObject()!,
|
||||
inputs: inputs!,
|
||||
chatHistory,
|
||||
name: flow.name,
|
||||
description: flow.description,
|
||||
});
|
||||
setTabsState((old) => {
|
||||
//@ts-ignore
|
||||
setTabsState((old: TabsState) => {
|
||||
if (!chatKey) return old;
|
||||
let newTabsState = _.cloneDeep(old);
|
||||
newTabsState[id.current].formKeysData.input_keys[chatKey] = "";
|
||||
newTabsState[id.current].formKeysData.input_keys![chatKey] = "";
|
||||
return newTabsState;
|
||||
});
|
||||
} else {
|
||||
|
|
@ -368,18 +374,18 @@ export default function FormModal({
|
|||
});
|
||||
}
|
||||
}
|
||||
function clearChat() {
|
||||
function clearChat(): void {
|
||||
setChatHistory([]);
|
||||
ws.current.send(JSON.stringify({ clear_history: true }));
|
||||
ws.current?.send(JSON.stringify({ clear_history: true }));
|
||||
if (lockChat) setLockChat(false);
|
||||
}
|
||||
|
||||
function handleOnCheckedChange(checked: boolean, i: string) {
|
||||
if (checked === true) {
|
||||
setChatKey(i);
|
||||
setChatValue(tabsState[flow.id].formKeysData.input_keys[i]);
|
||||
setChatValue(tabsState[flow.id].formKeysData.input_keys![i]);
|
||||
} else {
|
||||
setChatKey(null);
|
||||
setChatKey(null!);
|
||||
setChatValue("");
|
||||
}
|
||||
}
|
||||
|
|
@ -425,7 +431,7 @@ export default function FormModal({
|
|||
|
||||
{tabsState[id.current]?.formKeysData?.input_keys
|
||||
? Object.keys(
|
||||
tabsState[id.current].formKeysData.input_keys
|
||||
tabsState[id.current].formKeysData.input_keys!
|
||||
).map((key, index) => (
|
||||
<div className="file-component-accordion-div" key={index}>
|
||||
<AccordionComponent
|
||||
|
|
@ -449,7 +455,7 @@ export default function FormModal({
|
|||
size="small"
|
||||
disabled={tabsState[
|
||||
id.current
|
||||
].formKeysData.handle_keys.some(
|
||||
].formKeysData.handle_keys!.some(
|
||||
(t) => t === key
|
||||
)}
|
||||
/>
|
||||
|
|
@ -460,7 +466,7 @@ export default function FormModal({
|
|||
keyValue={key}
|
||||
>
|
||||
<div className="file-component-tab-column">
|
||||
{tabsState[id.current].formKeysData.handle_keys.some(
|
||||
{tabsState[id.current].formKeysData.handle_keys!.some(
|
||||
(t) => t === key
|
||||
) && (
|
||||
<div className="font-normal text-muted-foreground ">
|
||||
|
|
@ -470,14 +476,18 @@ export default function FormModal({
|
|||
<Textarea
|
||||
className="custom-scroll"
|
||||
value={
|
||||
tabsState[id.current].formKeysData.input_keys[key]
|
||||
tabsState[id.current].formKeysData.input_keys![
|
||||
key
|
||||
]
|
||||
}
|
||||
onChange={(e) => {
|
||||
setTabsState((old) => {
|
||||
//@ts-ignore
|
||||
setTabsState((old: TabsState) => {
|
||||
let newTabsState = _.cloneDeep(old);
|
||||
newTabsState[
|
||||
id.current
|
||||
].formKeysData.input_keys[key] = e.target.value;
|
||||
].formKeysData.input_keys![key] =
|
||||
e.target.value;
|
||||
return newTabsState;
|
||||
});
|
||||
}}
|
||||
|
|
@ -489,7 +499,7 @@ export default function FormModal({
|
|||
</div>
|
||||
))
|
||||
: null}
|
||||
{tabsState[id.current].formKeysData.memory_keys.map(
|
||||
{tabsState[id.current].formKeysData.memory_keys!.map(
|
||||
(key, index) => (
|
||||
<div className="file-component-accordion-div" key={index}>
|
||||
<AccordionComponent
|
||||
|
|
@ -583,10 +593,11 @@ export default function FormModal({
|
|||
sendMessage={sendMessage}
|
||||
setChatValue={(value) => {
|
||||
setChatValue(value);
|
||||
setTabsState((old) => {
|
||||
//@ts-ignore
|
||||
setTabsState((old: TabsState) => {
|
||||
let newTabsState = _.cloneDeep(old);
|
||||
newTabsState[id.current].formKeysData.input_keys[
|
||||
chatKey
|
||||
newTabsState[id.current].formKeysData.input_keys![
|
||||
chatKey!
|
||||
] = value;
|
||||
return newTabsState;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { ReactNode, useContext, useEffect, useRef, useState } from "react";
|
||||
import { useContext, useEffect, useRef, useState } from "react";
|
||||
import SanitizedHTMLWrapper from "../../components/SanitizedHTMLWrapper";
|
||||
import ShadTooltip from "../../components/ShadTooltipComponent";
|
||||
import IconComponent from "../../components/genericIconComponent";
|
||||
|
|
@ -15,7 +15,7 @@ import {
|
|||
import { TypeModal } from "../../constants/enums";
|
||||
import { alertContext } from "../../contexts/alertContext";
|
||||
import { postValidatePrompt } from "../../controllers/API";
|
||||
import { APIClassType } from "../../types/api";
|
||||
import { genericModalPropsType } from "../../types/components";
|
||||
import {
|
||||
classNames,
|
||||
getRandomKeyByssmm,
|
||||
|
|
@ -33,39 +33,29 @@ export default function GenericModal({
|
|||
nodeClass,
|
||||
setNodeClass,
|
||||
children,
|
||||
}: {
|
||||
field_name?: string;
|
||||
setValue: (value: string) => void;
|
||||
value: string;
|
||||
buttonText: string;
|
||||
modalTitle: string;
|
||||
type: number;
|
||||
children: ReactNode;
|
||||
nodeClass?: APIClassType;
|
||||
setNodeClass?: (Class: APIClassType) => void;
|
||||
}) {
|
||||
}: genericModalPropsType): JSX.Element {
|
||||
const [myButtonText] = useState(buttonText);
|
||||
const [myModalTitle] = useState(modalTitle);
|
||||
const [myModalType] = useState(type);
|
||||
const [inputValue, setInputValue] = useState(value);
|
||||
const [isEdit, setIsEdit] = useState(true);
|
||||
const [wordsHighlight, setWordsHighlight] = useState([]);
|
||||
const [wordsHighlight, setWordsHighlight] = useState<string[]>([]);
|
||||
const { setErrorData, setSuccessData, setNoticeData } =
|
||||
useContext(alertContext);
|
||||
const ref = useRef();
|
||||
const divRef = useRef(null);
|
||||
const divRefPrompt = useRef(null);
|
||||
|
||||
function checkVariables(valueToCheck) {
|
||||
function checkVariables(valueToCheck: string): void {
|
||||
const regex = /\{([^{}]+)\}/g;
|
||||
const matches = [];
|
||||
const matches: string[] = [];
|
||||
let match;
|
||||
while ((match = regex.exec(valueToCheck))) {
|
||||
matches.push(`{${match[1]}}`);
|
||||
}
|
||||
|
||||
let invalid_chars = [];
|
||||
let fixed_variables = [];
|
||||
let invalid_chars: string[] = [];
|
||||
let fixed_variables: string[] = [];
|
||||
let input_variables = matches;
|
||||
for (let variable of input_variables) {
|
||||
let new_var = variable;
|
||||
|
|
@ -106,7 +96,7 @@ export default function GenericModal({
|
|||
.replace(regexHighlight, varHighlightHTML({ name: "$1" }))
|
||||
.replace(/\n/g, "<br />");
|
||||
|
||||
const TextAreaContentView = () => {
|
||||
const TextAreaContentView = (): JSX.Element => {
|
||||
return (
|
||||
<SanitizedHTMLWrapper
|
||||
className={getClassByNumberLength()}
|
||||
|
|
@ -119,7 +109,7 @@ export default function GenericModal({
|
|||
);
|
||||
};
|
||||
|
||||
function getClassByNumberLength() {
|
||||
function getClassByNumberLength(): string {
|
||||
let sumOfCaracteres: number = 0;
|
||||
wordsHighlight.forEach((element) => {
|
||||
sumOfCaracteres = sumOfCaracteres + element.replace(/[{}]/g, "").length;
|
||||
|
|
@ -129,8 +119,8 @@ export default function GenericModal({
|
|||
: "code-nohighlight";
|
||||
}
|
||||
|
||||
function validatePrompt(closeModal: boolean) {
|
||||
postValidatePrompt(field_name, inputValue, nodeClass)
|
||||
function validatePrompt(closeModal: boolean): void {
|
||||
postValidatePrompt(field_name, inputValue, nodeClass!)
|
||||
.then((apiReturn) => {
|
||||
if (apiReturn.data) {
|
||||
let inputVariables = apiReturn.data.input_variables ?? [];
|
||||
|
|
@ -144,7 +134,7 @@ export default function GenericModal({
|
|||
setSuccessData({
|
||||
title: "Prompt is ready",
|
||||
});
|
||||
setNodeClass(apiReturn.data?.frontend_node);
|
||||
setNodeClass!(apiReturn.data?.frontend_node);
|
||||
setModalOpen(closeModal);
|
||||
setValue(inputValue);
|
||||
}
|
||||
|
|
@ -218,6 +208,7 @@ export default function GenericModal({
|
|||
<TextAreaContentView />
|
||||
) : type !== TypeModal.PROMPT ? (
|
||||
<Textarea
|
||||
//@ts-ignore
|
||||
ref={ref}
|
||||
className="form-input h-full w-full rounded-lg focus-visible:ring-1"
|
||||
value={inputValue}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { ReactNode } from "react";
|
||||
import { buttonBoxPropsType } from "../../../types/components";
|
||||
import { classNames } from "../../../utils/utils";
|
||||
|
||||
export default function ButtonBox({
|
||||
|
|
@ -10,16 +10,7 @@ export default function ButtonBox({
|
|||
textColor,
|
||||
deactivate,
|
||||
size,
|
||||
}: {
|
||||
onClick: () => void;
|
||||
title: string;
|
||||
description: string;
|
||||
icon: ReactNode;
|
||||
bgColor: string;
|
||||
textColor: string;
|
||||
deactivate?: boolean;
|
||||
size: "small" | "medium" | "big";
|
||||
}) {
|
||||
}: buttonBoxPropsType): JSX.Element {
|
||||
let bigCircle: string;
|
||||
let smallCircle: string;
|
||||
let titleFontSize: string;
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ import { FlowType } from "../../types/flow";
|
|||
import { classNames } from "../../utils/utils";
|
||||
import ButtonBox from "./buttonBox";
|
||||
|
||||
export default function ImportModal() {
|
||||
export default function ImportModal(): JSX.Element {
|
||||
const [open, setOpen] = useState(true);
|
||||
const { setErrorData } = useContext(alertContext);
|
||||
const ref = useRef();
|
||||
|
|
@ -32,7 +32,7 @@ export default function ImportModal() {
|
|||
const [examples, setExamples] = useState<FlowType[]>([]);
|
||||
const { uploadFlow, addFlow } = useContext(TabsContext);
|
||||
|
||||
function handleExamples() {
|
||||
function handleExamples(): void {
|
||||
setLoadingExamples(true);
|
||||
getExamples()
|
||||
.then((result) => {
|
||||
|
|
|
|||
31
src/frontend/src/pages/AdminPage/LoginPage/index.tsx
Normal file
31
src/frontend/src/pages/AdminPage/LoginPage/index.tsx
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import { useNavigate } from "react-router-dom";
|
||||
import { Button } from "../../../components/ui/button";
|
||||
import { Input } from "../../../components/ui/input";
|
||||
|
||||
export default function LoginAdminPage() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
function loginAdmin() {
|
||||
navigate("/admin/");
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col items-center justify-center bg-muted">
|
||||
<div className="flex w-72 flex-col items-center justify-center gap-2">
|
||||
<span className="mb-4 text-5xl">⛓️</span>
|
||||
<span className="mb-6 text-2xl font-semibold text-primary">Admin</span>
|
||||
<Input className="bg-background" placeholder="Email address" />
|
||||
<Input className="bg-background" placeholder="Password" />
|
||||
<Button
|
||||
onClick={() => {
|
||||
loginAdmin();
|
||||
}}
|
||||
variant="default"
|
||||
className="w-full"
|
||||
>
|
||||
Login
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
401
src/frontend/src/pages/AdminPage/index.tsx
Normal file
401
src/frontend/src/pages/AdminPage/index.tsx
Normal file
|
|
@ -0,0 +1,401 @@
|
|||
import _ from "lodash";
|
||||
import { X } from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import PaginatorComponent from "../../components/PaginatorComponent";
|
||||
import ShadTooltip from "../../components/ShadTooltipComponent";
|
||||
import IconComponent from "../../components/genericIconComponent";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import { Input } from "../../components/ui/input";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "../../components/ui/table";
|
||||
import ConfirmationModal from "../../modals/ConfirmationModal";
|
||||
import UserManagementModal from "../../modals/UserManagementModal";
|
||||
|
||||
export default function AdminPage() {
|
||||
const [inputValue, setInputValue] = useState("");
|
||||
|
||||
const [size, setPageSize] = useState(10);
|
||||
const [index, setPageIndex] = useState(1);
|
||||
|
||||
const userList = useRef([
|
||||
{
|
||||
user: generateRandomString(50),
|
||||
email: generateRandomString(50) + "@example.com",
|
||||
password: generateRandomString(50),
|
||||
register_date: generateRandomDate(),
|
||||
},
|
||||
{
|
||||
user: generateRandomString(8),
|
||||
email: generateRandomString(10) + "@example.com",
|
||||
password: generateRandomString(12),
|
||||
register_date: generateRandomDate(),
|
||||
},
|
||||
{
|
||||
user: generateRandomString(8),
|
||||
email: generateRandomString(10) + "@example.com",
|
||||
password: generateRandomString(12),
|
||||
register_date: generateRandomDate(),
|
||||
},
|
||||
{
|
||||
user: generateRandomString(8),
|
||||
email: generateRandomString(10) + "@example.com",
|
||||
password: generateRandomString(12),
|
||||
register_date: generateRandomDate(),
|
||||
},
|
||||
{
|
||||
user: generateRandomString(8),
|
||||
email: generateRandomString(10) + "@example.com",
|
||||
password: generateRandomString(12),
|
||||
register_date: generateRandomDate(),
|
||||
},
|
||||
{
|
||||
user: generateRandomString(8),
|
||||
email: generateRandomString(10) + "@example.com",
|
||||
password: generateRandomString(12),
|
||||
register_date: generateRandomDate(),
|
||||
},
|
||||
{
|
||||
user: generateRandomString(8),
|
||||
email: generateRandomString(10) + "@example.com",
|
||||
password: generateRandomString(12),
|
||||
register_date: generateRandomDate(),
|
||||
},
|
||||
{
|
||||
user: generateRandomString(8),
|
||||
email: generateRandomString(10) + "@example.com",
|
||||
password: generateRandomString(12),
|
||||
register_date: generateRandomDate(),
|
||||
},
|
||||
{
|
||||
user: generateRandomString(8),
|
||||
email: generateRandomString(10) + "@example.com",
|
||||
password: generateRandomString(12),
|
||||
register_date: generateRandomDate(),
|
||||
},
|
||||
{
|
||||
user: generateRandomString(8),
|
||||
email: generateRandomString(10) + "@example.com",
|
||||
password: generateRandomString(12),
|
||||
register_date: generateRandomDate(),
|
||||
},
|
||||
{
|
||||
user: generateRandomString(8),
|
||||
email: generateRandomString(10) + "@example.com",
|
||||
password: generateRandomString(12),
|
||||
register_date: generateRandomDate(),
|
||||
},
|
||||
{
|
||||
user: generateRandomString(8),
|
||||
email: generateRandomString(10) + "@example.com",
|
||||
password: generateRandomString(12),
|
||||
register_date: generateRandomDate(),
|
||||
},
|
||||
{
|
||||
user: generateRandomString(8),
|
||||
email: generateRandomString(10) + "@example.com",
|
||||
password: generateRandomString(12),
|
||||
register_date: generateRandomDate(),
|
||||
},
|
||||
{
|
||||
user: generateRandomString(8),
|
||||
email: generateRandomString(10) + "@example.com",
|
||||
password: generateRandomString(12),
|
||||
register_date: generateRandomDate(),
|
||||
},
|
||||
{
|
||||
user: generateRandomString(8),
|
||||
email: generateRandomString(10) + "@example.com",
|
||||
password: generateRandomString(12),
|
||||
register_date: generateRandomDate(),
|
||||
},
|
||||
{
|
||||
user: generateRandomString(8),
|
||||
email: generateRandomString(10) + "@example.com",
|
||||
password: generateRandomString(12),
|
||||
register_date: generateRandomDate(),
|
||||
},
|
||||
{
|
||||
user: generateRandomString(8),
|
||||
email: generateRandomString(10) + "@example.com",
|
||||
password: generateRandomString(12),
|
||||
register_date: generateRandomDate(),
|
||||
},
|
||||
]);
|
||||
|
||||
const [filterUserList, setFilterUserList] = useState(userList.current);
|
||||
|
||||
function generateRandomString(length) {
|
||||
const characters =
|
||||
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
let result = "";
|
||||
for (let i = 0; i < length; i++) {
|
||||
const randomIndex = Math.floor(Math.random() * characters.length);
|
||||
result += characters.charAt(randomIndex);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function generateRandomDate() {
|
||||
const start = new Date(2010, 0, 1);
|
||||
const end = new Date();
|
||||
const randomTimestamp =
|
||||
start.getTime() + Math.random() * (end.getTime() - start.getTime());
|
||||
const randomDate = new Date(randomTimestamp);
|
||||
|
||||
const options = { year: "numeric", month: "short", day: "numeric" };
|
||||
return randomDate.toLocaleDateString("en-US");
|
||||
}
|
||||
|
||||
const [editUser, setEditUser] = useState(-1);
|
||||
const [editedUser, setEditedUser] = useState("");
|
||||
const [modalEditOpen, setModalEditOpen] = useState(false);
|
||||
const [modalDeleteOpen, setModalDeleteOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
resetFilter();
|
||||
}, []);
|
||||
|
||||
const handleInputChange = (event, index) => {
|
||||
const user = _.cloneDeepWith(userList.current);
|
||||
user[index].password = event.target.value;
|
||||
userList.current = user;
|
||||
|
||||
const userFilter = _.cloneDeepWith(filterUserList);
|
||||
userFilter[index].password = event.target.value;
|
||||
setFilterUserList(userFilter);
|
||||
|
||||
setEditedUser(event.target.value);
|
||||
};
|
||||
|
||||
function handleChangePagination(pageIndex: number, pageSize: number) {
|
||||
setPageIndex(pageIndex);
|
||||
setPageSize(pageSize);
|
||||
|
||||
const startIndex = (pageIndex - 1) * pageSize;
|
||||
const endIndex = startIndex + pageSize;
|
||||
const newList = userList.current.slice(startIndex, endIndex);
|
||||
|
||||
setFilterUserList(newList);
|
||||
}
|
||||
|
||||
function resetFilter() {
|
||||
setFilterUserList(userList.current);
|
||||
setPageIndex(1);
|
||||
setPageSize(10);
|
||||
|
||||
const startIndex = (index - 1) * size;
|
||||
const endIndex = index + size - 1;
|
||||
const newList = userList.current.slice(startIndex, endIndex);
|
||||
|
||||
console.log(userList.current);
|
||||
|
||||
setFilterUserList(newList);
|
||||
}
|
||||
|
||||
function handleFilterUsers(input: string) {
|
||||
setInputValue(input);
|
||||
|
||||
if (input === "") {
|
||||
resetFilter();
|
||||
} else {
|
||||
const filteredList = userList.current.filter(
|
||||
(user) =>
|
||||
user.user.toLowerCase().includes(input.toLowerCase()) ||
|
||||
user.email.toLowerCase().includes(input.toLowerCase())
|
||||
);
|
||||
setFilterUserList(filteredList);
|
||||
}
|
||||
}
|
||||
|
||||
function handleDeleteUser(index) {
|
||||
const user = _.cloneDeepWith(userList.current);
|
||||
user.splice(index, 1);
|
||||
userList.current = user;
|
||||
|
||||
const userFilter = _.cloneDeepWith(filterUserList);
|
||||
userFilter.splice(index, 1);
|
||||
setFilterUserList(userFilter);
|
||||
|
||||
resetFilter();
|
||||
}
|
||||
|
||||
function handleEditUser(index, user) {
|
||||
const newUser = _.cloneDeepWith(userList.current);
|
||||
newUser[index].password = user.password;
|
||||
newUser[index].user = user.username;
|
||||
userList.current = newUser;
|
||||
resetFilter();
|
||||
}
|
||||
|
||||
function handleNewUser(user) {
|
||||
const newUser = {
|
||||
user: user.username,
|
||||
email: generateRandomString(50) + "@example.com",
|
||||
password: user.password,
|
||||
register_date: generateRandomDate(),
|
||||
};
|
||||
|
||||
userList.current.unshift(newUser);
|
||||
console.log(userList.current);
|
||||
|
||||
resetFilter();
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="main-page-panel">
|
||||
<div className="m-auto flex h-full flex-row justify-center">
|
||||
<div className="basis-5/6">
|
||||
<div className="m-auto flex h-full flex-col space-y-8 p-8 ">
|
||||
<div className="flex items-center justify-between space-y-2">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold tracking-tight">
|
||||
Welcome back!
|
||||
</h2>
|
||||
<p className="text-muted-foreground">
|
||||
Here's a list of all users!
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2"></div>
|
||||
</div>
|
||||
|
||||
{userList.current.length === 0 && (
|
||||
<>
|
||||
<div className="flex items-center justify-between">
|
||||
<h2>There's no users left :)</h2>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{userList.current.length > 0 && (
|
||||
<>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex flex-1 items-center space-x-2">
|
||||
<Input
|
||||
value={inputValue}
|
||||
placeholder="Filter users..."
|
||||
className="h-8 w-[150px] lg:w-[250px]"
|
||||
onChange={(e) => handleFilterUsers(e.target.value)}
|
||||
/>
|
||||
{inputValue.length > 0 && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
resetFilter();
|
||||
setInputValue("");
|
||||
}}
|
||||
variant="ghost"
|
||||
className="h-8 px-2 lg:px-3"
|
||||
>
|
||||
Reset
|
||||
<X className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<UserManagementModal
|
||||
title="New User"
|
||||
titleHeader={"Add a new user"}
|
||||
cancelText="Cancel"
|
||||
confirmationText="Save"
|
||||
icon={"UserPlus2"}
|
||||
onConfirm={(index, user) => {
|
||||
handleNewUser(user);
|
||||
}}
|
||||
>
|
||||
<Button>New User</Button>
|
||||
</UserManagementModal>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="overflow-scroll overflow-x-hidden rounded-md border-2 bg-muted
|
||||
custom-scroll"
|
||||
>
|
||||
<Table className="table-fixed bg-muted outline-1 ">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="h-10">User</TableHead>
|
||||
<TableHead className="h-10">Password</TableHead>
|
||||
<TableHead className="h-10 w-[100px] text-right"></TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filterUserList.map((user, index) => (
|
||||
<TableRow key={user.user}>
|
||||
<TableCell className="truncate py-2 font-medium">
|
||||
{user.user}
|
||||
</TableCell>
|
||||
<TableCell className="truncate py-2">
|
||||
{user.password}
|
||||
</TableCell>
|
||||
<TableCell className="flex w-[100px] py-2 text-right">
|
||||
<div className="flex">
|
||||
<UserManagementModal
|
||||
title="Edit"
|
||||
titleHeader={`${user.user}`}
|
||||
cancelText="Cancel"
|
||||
confirmationText="Edit"
|
||||
icon={"UserPlus2"}
|
||||
data={user}
|
||||
index={index}
|
||||
onConfirm={(index, user) => {
|
||||
handleEditUser(index, user);
|
||||
}}
|
||||
>
|
||||
<ShadTooltip content="Edit" side="top">
|
||||
<IconComponent
|
||||
name="Pencil"
|
||||
className="h-4 w-4 cursor-pointer"
|
||||
/>
|
||||
</ShadTooltip>
|
||||
</UserManagementModal>
|
||||
|
||||
<ConfirmationModal
|
||||
title="Delete"
|
||||
titleHeader="Delete User"
|
||||
modalContentTitle="Attention!"
|
||||
modalContent="Are you sure you want to delete this user? This action cannot be undone."
|
||||
cancelText="Cancel"
|
||||
confirmationText="Delete"
|
||||
icon={"UserMinus2"}
|
||||
data={user}
|
||||
index={index}
|
||||
onConfirm={(index, user) => {
|
||||
handleDeleteUser(index);
|
||||
}}
|
||||
>
|
||||
<ShadTooltip content="Delete" side="top">
|
||||
<IconComponent
|
||||
name="Trash2"
|
||||
className="ml-2 h-4 w-4 cursor-pointer"
|
||||
/>
|
||||
</ShadTooltip>
|
||||
</ConfirmationModal>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<PaginatorComponent
|
||||
pageIndex={index}
|
||||
pageSize={size}
|
||||
totalRowsCount={filterUserList.length}
|
||||
paginate={(pageIndex, pageSize) => {
|
||||
handleChangePagination(pageSize, pageIndex);
|
||||
}}
|
||||
></PaginatorComponent>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@ import { CardComponent } from "../../components/cardComponent";
|
|||
import IconComponent from "../../components/genericIconComponent";
|
||||
import { getExamples } from "../../controllers/API";
|
||||
import { FlowType } from "../../types/flow";
|
||||
export default function CommunityPage() {
|
||||
export default function CommunityPage(): JSX.Element {
|
||||
const { flows, setTabId, downloadFlows, uploadFlows, addFlow } =
|
||||
useContext(TabsContext);
|
||||
|
||||
|
|
@ -21,7 +21,7 @@ export default function CommunityPage() {
|
|||
const [examples, setExamples] = useState<FlowType[]>([]);
|
||||
|
||||
// Show community examples on screen
|
||||
function handleExamples() {
|
||||
function handleExamples(): void {
|
||||
setLoadingExamples(true);
|
||||
getExamples()
|
||||
.then((result) => {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ const ConnectionLineComponent = ({
|
|||
toX,
|
||||
toY,
|
||||
connectionLineStyle = {}, // provide a default value for connectionLineStyle
|
||||
}: ConnectionLineComponentProps) => {
|
||||
}: ConnectionLineComponentProps): JSX.Element => {
|
||||
return (
|
||||
<g>
|
||||
<path
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ export default function DisclosureComponent({
|
|||
button: { title, Icon, buttons = [] },
|
||||
children,
|
||||
openDisc,
|
||||
}: DisclosureComponentType) {
|
||||
}: DisclosureComponentType): JSX.Element {
|
||||
return (
|
||||
<Disclosure as="div" key={title}>
|
||||
{({ open }) => (
|
||||
|
|
|
|||
|
|
@ -1,11 +1,19 @@
|
|||
import _ from "lodash";
|
||||
import { useCallback, useContext, useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
MouseEvent,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import ReactFlow, {
|
||||
Background,
|
||||
Connection,
|
||||
Controls,
|
||||
Edge,
|
||||
EdgeChange,
|
||||
Node,
|
||||
NodeChange,
|
||||
NodeDragHandler,
|
||||
OnEdgesDelete,
|
||||
|
|
@ -26,6 +34,7 @@ import { typesContext } from "../../../../contexts/typesContext";
|
|||
import { undoRedoContext } from "../../../../contexts/undoRedoContext";
|
||||
import { APIClassType } from "../../../../types/api";
|
||||
import { FlowType, NodeType } from "../../../../types/flow";
|
||||
import { TabsState } from "../../../../types/tabs";
|
||||
import { isValidConnection } from "../../../../utils/reactflowUtils";
|
||||
import { isWrappedWithClass } from "../../../../utils/utils";
|
||||
import ConnectionLineComponent from "../ConnectionLineComponent";
|
||||
|
|
@ -35,7 +44,7 @@ const nodeTypes = {
|
|||
genericNode: GenericNode,
|
||||
};
|
||||
|
||||
export default function Page({ flow }: { flow: FlowType }) {
|
||||
export default function Page({ flow }: { flow: FlowType }): JSX.Element {
|
||||
let {
|
||||
updateFlow,
|
||||
uploadFlow,
|
||||
|
|
@ -51,13 +60,13 @@ export default function Page({ flow }: { flow: FlowType }) {
|
|||
} = useContext(TabsContext);
|
||||
const { types, reactFlowInstance, setReactFlowInstance, templates } =
|
||||
useContext(typesContext);
|
||||
const reactFlowWrapper = useRef(null);
|
||||
const reactFlowWrapper = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { takeSnapshot } = useContext(undoRedoContext);
|
||||
|
||||
const [position, setPosition] = useState({ x: 0, y: 0 });
|
||||
const [lastSelection, setLastSelection] =
|
||||
useState<OnSelectionChangeParams>(null);
|
||||
useState<OnSelectionChangeParams | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// this effect is used to attach the global event handlers
|
||||
|
|
@ -78,10 +87,10 @@ export default function Page({ flow }: { flow: FlowType }) {
|
|||
lastCopiedSelection
|
||||
) {
|
||||
event.preventDefault();
|
||||
let bounds = reactFlowWrapper.current.getBoundingClientRect();
|
||||
let bounds = reactFlowWrapper.current?.getBoundingClientRect();
|
||||
paste(lastCopiedSelection, {
|
||||
x: position.x - bounds.left,
|
||||
y: position.y - bounds.top,
|
||||
x: position.x - bounds!.left,
|
||||
y: position.y - bounds!.top,
|
||||
});
|
||||
}
|
||||
if (
|
||||
|
|
@ -147,7 +156,8 @@ export default function Page({ flow }: { flow: FlowType }) {
|
|||
let newX = _.cloneDeep(node);
|
||||
return newX;
|
||||
});
|
||||
setTabsState((prev) => {
|
||||
//@ts-ignore
|
||||
setTabsState((prev: TabsState) => {
|
||||
return {
|
||||
...prev,
|
||||
[tabId]: {
|
||||
|
|
@ -163,7 +173,8 @@ export default function Page({ flow }: { flow: FlowType }) {
|
|||
const onNodesChangeMod = useCallback(
|
||||
(change: NodeChange[]) => {
|
||||
onNodesChange(change);
|
||||
setTabsState((prev) => {
|
||||
//@ts-ignore
|
||||
setTabsState((prev: TabsState) => {
|
||||
return {
|
||||
...prev,
|
||||
[tabId]: {
|
||||
|
|
@ -185,10 +196,10 @@ export default function Page({ flow }: { flow: FlowType }) {
|
|||
...params,
|
||||
style: { stroke: "#555" },
|
||||
className:
|
||||
(params.targetHandle.split("|")[0] === "Text"
|
||||
(params.targetHandle?.split("|")[0] === "Text"
|
||||
? "stroke-foreground "
|
||||
: "stroke-foreground ") + " stroke-connection",
|
||||
animated: params.targetHandle.split("|")[0] === "Text",
|
||||
animated: params.targetHandle?.split("|")[0] === "Text",
|
||||
},
|
||||
eds
|
||||
)
|
||||
|
|
@ -234,7 +245,7 @@ export default function Page({ flow }: { flow: FlowType }) {
|
|||
|
||||
// Get the current bounds of the ReactFlow wrapper element
|
||||
const reactflowBounds =
|
||||
reactFlowWrapper.current.getBoundingClientRect();
|
||||
reactFlowWrapper.current?.getBoundingClientRect();
|
||||
|
||||
// Extract the data from the drag event and parse it as a JSON object
|
||||
let data: { type: string; node?: APIClassType } = JSON.parse(
|
||||
|
|
@ -243,9 +254,9 @@ export default function Page({ flow }: { flow: FlowType }) {
|
|||
|
||||
// If data type is not "chatInput" or if there are no "chatInputNode" nodes present in the ReactFlow instance, create a new node
|
||||
// Calculate the position where the node should be created
|
||||
const position = reactFlowInstance.project({
|
||||
x: event.clientX - reactflowBounds.left,
|
||||
y: event.clientY - reactflowBounds.top,
|
||||
const position = reactFlowInstance!.project({
|
||||
x: event.clientX - reactflowBounds!.left,
|
||||
y: event.clientY - reactflowBounds!.top,
|
||||
});
|
||||
|
||||
// Generate a unique node ID
|
||||
|
|
@ -262,7 +273,6 @@ export default function Page({ flow }: { flow: FlowType }) {
|
|||
data: {
|
||||
...data,
|
||||
id: newId,
|
||||
value: null,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
|
|
@ -274,7 +284,6 @@ export default function Page({ flow }: { flow: FlowType }) {
|
|||
data: {
|
||||
...data,
|
||||
id: newId,
|
||||
value: null,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -283,7 +292,7 @@ export default function Page({ flow }: { flow: FlowType }) {
|
|||
setNodes((nds) => nds.concat(newNode));
|
||||
} else if (event.dataTransfer.types.some((types) => types === "Files")) {
|
||||
takeSnapshot();
|
||||
uploadFlow(false, event.dataTransfer.files.item(0));
|
||||
uploadFlow(false, event.dataTransfer.files.item(0)!);
|
||||
}
|
||||
},
|
||||
// Specify dependencies for useCallback
|
||||
|
|
@ -299,7 +308,7 @@ export default function Page({ flow }: { flow: FlowType }) {
|
|||
}, []);
|
||||
|
||||
const onDelete = useCallback(
|
||||
(mynodes) => {
|
||||
(mynodes: Node[]) => {
|
||||
takeSnapshot();
|
||||
setEdges(
|
||||
edges.filter(
|
||||
|
|
@ -319,7 +328,7 @@ export default function Page({ flow }: { flow: FlowType }) {
|
|||
|
||||
const onEdgeUpdate = useCallback(
|
||||
(oldEdge: Edge, newConnection: Connection) => {
|
||||
if (isValidConnection(newConnection, reactFlowInstance)) {
|
||||
if (isValidConnection(newConnection, reactFlowInstance!)) {
|
||||
edgeUpdateSuccessful.current = true;
|
||||
setEdges((els) => updateEdge(oldEdge, newConnection, els));
|
||||
}
|
||||
|
|
@ -327,11 +336,10 @@ export default function Page({ flow }: { flow: FlowType }) {
|
|||
[reactFlowInstance, setEdges]
|
||||
);
|
||||
|
||||
const onEdgeUpdateEnd = useCallback((_, edge) => {
|
||||
const onEdgeUpdateEnd = useCallback((_, edge: Edge): void => {
|
||||
if (!edgeUpdateSuccessful.current) {
|
||||
setEdges((eds) => eds.filter((edg) => edg.id !== edge.id));
|
||||
}
|
||||
|
||||
edgeUpdateSuccessful.current = true;
|
||||
}, []);
|
||||
|
||||
|
|
@ -340,7 +348,7 @@ export default function Page({ flow }: { flow: FlowType }) {
|
|||
const onSelectionEnd = useCallback(() => {
|
||||
setSelectionEnded(true);
|
||||
}, []);
|
||||
const onSelectionStart = useCallback((event) => {
|
||||
const onSelectionStart = useCallback((event: MouseEvent) => {
|
||||
event.preventDefault();
|
||||
setSelectionEnded(false);
|
||||
}, []);
|
||||
|
|
@ -354,9 +362,12 @@ export default function Page({ flow }: { flow: FlowType }) {
|
|||
}
|
||||
}, [selectionEnded, lastSelection]);
|
||||
|
||||
const onSelectionChange = useCallback((flow) => {
|
||||
setLastSelection(flow);
|
||||
}, []);
|
||||
const onSelectionChange = useCallback(
|
||||
(flow: OnSelectionChangeParams): void => {
|
||||
setLastSelection(flow);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex h-full overflow-hidden">
|
||||
|
|
@ -383,7 +394,6 @@ export default function Page({ flow }: { flow: FlowType }) {
|
|||
onEdgesChange={onEdgesChangeMod}
|
||||
onConnect={onConnect}
|
||||
disableKeyboardA11y={true}
|
||||
onLoad={setReactFlowInstance}
|
||||
onInit={setReactFlowInstance}
|
||||
nodeTypes={nodeTypes}
|
||||
onEdgeUpdate={onEdgeUpdate}
|
||||
|
|
@ -409,7 +419,7 @@ export default function Page({ flow }: { flow: FlowType }) {
|
|||
[&>button]:border-b-border hover:[&>button]:bg-border"
|
||||
></Controls>
|
||||
</ReactFlow>
|
||||
<Chat flow={flow} reactFlowInstance={reactFlowInstance} />
|
||||
<Chat flow={flow} reactFlowInstance={reactFlowInstance!} />
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import {
|
|||
import { classNames } from "../../../../utils/utils";
|
||||
import DisclosureComponent from "../DisclosureComponent";
|
||||
|
||||
export default function ExtraSidebar() {
|
||||
export default function ExtraSidebar(): JSX.Element {
|
||||
const { data, templates } = useContext(typesContext);
|
||||
const { flows, tabId, uploadFlow, tabsState, saveFlow, isBuilt } =
|
||||
useContext(TabsContext);
|
||||
|
|
@ -28,7 +28,7 @@ export default function ExtraSidebar() {
|
|||
function onDragStart(
|
||||
event: React.DragEvent<any>,
|
||||
data: { type: string; node?: APIClassType }
|
||||
) {
|
||||
): void {
|
||||
//start drag event
|
||||
var crt = event.currentTarget.cloneNode(true);
|
||||
crt.style.position = "absolute";
|
||||
|
|
@ -59,7 +59,7 @@ export default function ExtraSidebar() {
|
|||
const flow = flows.find((flow) => flow.id === tabId);
|
||||
useEffect(() => {
|
||||
// show components with error on load
|
||||
let errors = [];
|
||||
let errors: string[] = [];
|
||||
Object.keys(templates).forEach((component) => {
|
||||
if (templates[component].error) {
|
||||
errors.push(component);
|
||||
|
|
@ -120,7 +120,7 @@ export default function ExtraSidebar() {
|
|||
"extra-side-bar-buttons " + (isPending ? "" : "button-disable")
|
||||
}
|
||||
onClick={(event) => {
|
||||
saveFlow(flow);
|
||||
saveFlow(flow!);
|
||||
setSuccessData({ title: "Changes saved successfully" });
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -4,14 +4,19 @@ import ShadTooltip from "../../../../components/ShadTooltipComponent";
|
|||
import IconComponent from "../../../../components/genericIconComponent";
|
||||
import { TabsContext } from "../../../../contexts/tabsContext";
|
||||
import EditNodeModal from "../../../../modals/EditNodeModal";
|
||||
import { nodeToolbarPropsType } from "../../../../types/components";
|
||||
import { classNames } from "../../../../utils/utils";
|
||||
|
||||
export default function NodeToolbarComponent({ data, setData, deleteNode }) {
|
||||
export default function NodeToolbarComponent({
|
||||
data,
|
||||
setData,
|
||||
deleteNode,
|
||||
}: nodeToolbarPropsType): JSX.Element {
|
||||
const [nodeLength, setNodeLength] = useState(
|
||||
Object.keys(data.node.template).filter(
|
||||
Object.keys(data.node!.template).filter(
|
||||
(templateField) =>
|
||||
templateField.charAt(0) !== "_" &&
|
||||
data.node.template[templateField].show &&
|
||||
data.node?.template[templateField].show &&
|
||||
(data.node.template[templateField].type === "str" ||
|
||||
data.node.template[templateField].type === "bool" ||
|
||||
data.node.template[templateField].type === "float" ||
|
||||
|
|
@ -55,8 +60,8 @@ export default function NodeToolbarComponent({ data, setData, deleteNode }) {
|
|||
{
|
||||
x: 50,
|
||||
y: 10,
|
||||
paneX: reactFlowInstance.getNode(data.id).position.x,
|
||||
paneY: reactFlowInstance.getNode(data.id).position.y,
|
||||
paneX: reactFlowInstance.getNode(data.id)?.position.x,
|
||||
paneY: reactFlowInstance.getNode(data.id)?.position.y,
|
||||
}
|
||||
);
|
||||
}}
|
||||
|
|
@ -67,23 +72,23 @@ export default function NodeToolbarComponent({ data, setData, deleteNode }) {
|
|||
|
||||
<ShadTooltip
|
||||
content={
|
||||
data.node.documentation === "" ? "Coming Soon" : "Documentation"
|
||||
data.node?.documentation === "" ? "Coming Soon" : "Documentation"
|
||||
}
|
||||
side="top"
|
||||
>
|
||||
<a
|
||||
className={classNames(
|
||||
"relative -ml-px inline-flex items-center bg-background px-2 py-2 text-foreground shadow-md ring-1 ring-inset ring-ring transition-all duration-500 ease-in-out hover:bg-muted focus:z-10" +
|
||||
(data.node.documentation === ""
|
||||
(data.node?.documentation === ""
|
||||
? " text-muted-foreground"
|
||||
: " text-foreground")
|
||||
)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={data.node.documentation}
|
||||
href={data.node?.documentation}
|
||||
// deactivate link if no documentation is provided
|
||||
onClick={(event) => {
|
||||
if (data.node.documentation === "") {
|
||||
if (data.node?.documentation === "") {
|
||||
event.preventDefault();
|
||||
}
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -4,13 +4,13 @@ import { TabsContext } from "../../contexts/tabsContext";
|
|||
import { getVersion } from "../../controllers/API";
|
||||
import Page from "./components/PageComponent";
|
||||
|
||||
export default function FlowPage() {
|
||||
export default function FlowPage(): JSX.Element {
|
||||
const { flows, tabId, setTabId } = useContext(TabsContext);
|
||||
const { id } = useParams();
|
||||
|
||||
// Set flow tab id
|
||||
useEffect(() => {
|
||||
setTabId(id);
|
||||
setTabId(id!);
|
||||
}, [id]);
|
||||
|
||||
// Initialize state variable for the version
|
||||
|
|
@ -26,7 +26,7 @@ export default function FlowPage() {
|
|||
{flows.length > 0 &&
|
||||
tabId !== "" &&
|
||||
flows.findIndex((flow) => flow.id === tabId) !== -1 && (
|
||||
<Page flow={flows.find((flow) => flow.id === tabId)} />
|
||||
<Page flow={flows.find((flow) => flow.id === tabId)!} />
|
||||
)}
|
||||
<a
|
||||
target={"_blank"}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import IconComponent from "../../components/genericIconComponent";
|
|||
import { Button } from "../../components/ui/button";
|
||||
import { USER_PROJECTS_HEADER } from "../../constants/constants";
|
||||
import { TabsContext } from "../../contexts/tabsContext";
|
||||
export default function HomePage() {
|
||||
export default function HomePage(): JSX.Element {
|
||||
const { flows, setTabId, downloadFlows, uploadFlows, addFlow, removeFlow } =
|
||||
useContext(TabsContext);
|
||||
|
||||
|
|
@ -45,7 +45,7 @@ export default function HomePage() {
|
|||
<Button
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
addFlow(null, true).then((id) => {
|
||||
addFlow(null!, true).then((id) => {
|
||||
navigate("/flow/" + id);
|
||||
});
|
||||
}}
|
||||
|
|
|
|||
59
src/frontend/src/pages/deleteAccountPage/index.tsx
Normal file
59
src/frontend/src/pages/deleteAccountPage/index.tsx
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import { useState } from "react";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import { Input } from "../../components/ui/input";
|
||||
import BaseModal from "../../modals/baseModal";
|
||||
|
||||
export default function DeleteAccountPage() {
|
||||
const [showConfirmation, setShowConfirmation] = useState(false);
|
||||
|
||||
const handleDeleteAccount = () => {
|
||||
// Implement your account deletion logic here
|
||||
// For example, make an API call to delete the account
|
||||
// Upon successful deletion, you can redirect the user to another page
|
||||
console.log("Account deleted!");
|
||||
// Implement the logic to redirect the user after account deletion.
|
||||
// For example, use react-router-dom's useHistory hook.
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col items-center justify-center bg-muted">
|
||||
<div className="flex w-72 flex-col items-center justify-center gap-2">
|
||||
<span className="mb-4 text-5xl">⛓️</span>
|
||||
<span className="mb-4 text-center text-2xl font-semibold text-primary">
|
||||
Delete your account
|
||||
</span>
|
||||
<Input className="bg-background" placeholder="Confirm password" />
|
||||
|
||||
<BaseModal
|
||||
open={showConfirmation}
|
||||
setOpen={setShowConfirmation}
|
||||
size="x-small"
|
||||
>
|
||||
<BaseModal.Header description="This action is irreversible and will permanently erase all your data and information associated with the account. ">
|
||||
<h3>Are you sure ?</h3>
|
||||
</BaseModal.Header>
|
||||
<BaseModal.Trigger>
|
||||
<Button
|
||||
variant="default"
|
||||
className="w-full hover:bg-status-red"
|
||||
onClick={() => setShowConfirmation(true)}
|
||||
>
|
||||
Delete account
|
||||
</Button>
|
||||
</BaseModal.Trigger>
|
||||
<BaseModal.Content>
|
||||
<div className="flex h-full w-full flex-col justify-end">
|
||||
<Button
|
||||
variant="default"
|
||||
className="w-full hover:bg-status-red"
|
||||
onClick={() => handleDeleteAccount()}
|
||||
>
|
||||
Delete account
|
||||
</Button>
|
||||
</div>
|
||||
</BaseModal.Content>
|
||||
</BaseModal>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
105
src/frontend/src/pages/loginPage/index.tsx
Normal file
105
src/frontend/src/pages/loginPage/index.tsx
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
import * as Form from "@radix-ui/react-form";
|
||||
import { useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import InputComponent from "../../components/inputComponent";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import { Input } from "../../components/ui/input";
|
||||
import { CONTROL_LOGIN_STATE } from "../../constants/constants";
|
||||
import {
|
||||
inputHandlerEventType,
|
||||
loginInputStateType,
|
||||
} from "../../types/components";
|
||||
|
||||
export default function LoginPage(): JSX.Element {
|
||||
const [inputState, setInputState] =
|
||||
useState<loginInputStateType>(CONTROL_LOGIN_STATE);
|
||||
|
||||
const { password, username } = inputState;
|
||||
|
||||
function handleInput({
|
||||
target: { name, value },
|
||||
}: inputHandlerEventType): void {
|
||||
setInputState((prev) => ({ ...prev, [name]: value }));
|
||||
}
|
||||
return (
|
||||
<Form.Root
|
||||
onSubmit={(event) => {
|
||||
if (password === "") {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
const data = Object.fromEntries(new FormData(event.currentTarget));
|
||||
event.preventDefault();
|
||||
}}
|
||||
className="h-full w-full"
|
||||
>
|
||||
<div className="flex h-full w-full flex-col items-center justify-center bg-muted">
|
||||
<div className="flex w-72 flex-col items-center justify-center gap-2">
|
||||
<span className="mb-4 text-5xl">⛓️</span>
|
||||
<span className="mb-6 text-2xl font-semibold text-primary">
|
||||
Sign in to Langflow
|
||||
</span>
|
||||
<div className="mb-3 w-full">
|
||||
<Form.Field name="username">
|
||||
<Form.Label className="data-[invalid]:label-invalid">
|
||||
Username <span className="font-medium text-destructive">*</span>
|
||||
</Form.Label>
|
||||
|
||||
<Form.Control asChild>
|
||||
<Input
|
||||
onChange={({ target: { value } }) => {
|
||||
handleInput({ target: { name: "username", value } });
|
||||
}}
|
||||
value={username}
|
||||
className="w-full"
|
||||
required
|
||||
placeholder="Username"
|
||||
/>
|
||||
</Form.Control>
|
||||
|
||||
<Form.Message match="valueMissing" className="field-invalid">
|
||||
Please enter your username
|
||||
</Form.Message>
|
||||
</Form.Field>
|
||||
</div>
|
||||
<div className="mb-3 w-full">
|
||||
<Form.Field name="password">
|
||||
<Form.Label className="data-[invalid]:label-invalid">
|
||||
Password <span className="font-medium text-destructive">*</span>
|
||||
</Form.Label>
|
||||
|
||||
<InputComponent
|
||||
onChange={(value) => {
|
||||
handleInput({ target: { name: "password", value } });
|
||||
}}
|
||||
value={password}
|
||||
isForm
|
||||
password={true}
|
||||
required
|
||||
placeholder="Password"
|
||||
className="w-full"
|
||||
/>
|
||||
|
||||
<Form.Message className="field-invalid" match="valueMissing">
|
||||
Please enter your password
|
||||
</Form.Message>
|
||||
</Form.Field>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<Form.Submit asChild>
|
||||
<Button className="mr-3 mt-6 w-full">Sign in</Button>
|
||||
</Form.Submit>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<Link to="">
|
||||
<Button className="w-full" variant="outline">
|
||||
Don't have an account? <b>Sign Up</b>
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Form.Root>
|
||||
);
|
||||
}
|
||||
137
src/frontend/src/pages/signUpPage/index.tsx
Normal file
137
src/frontend/src/pages/signUpPage/index.tsx
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
import * as Form from "@radix-ui/react-form";
|
||||
import { FormEvent, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import InputComponent from "../../components/inputComponent";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import { Input } from "../../components/ui/input";
|
||||
import { CONTROL_INPUT_STATE } from "../../constants/constants";
|
||||
import {
|
||||
inputHandlerEventType,
|
||||
signUpInputStateType,
|
||||
} from "../../types/components";
|
||||
|
||||
export default function SignUp(): JSX.Element {
|
||||
const [inputState, setInputState] =
|
||||
useState<signUpInputStateType>(CONTROL_INPUT_STATE);
|
||||
|
||||
const { password, cnfPassword, username } = inputState;
|
||||
|
||||
function handleInput({
|
||||
target: { name, value },
|
||||
}: inputHandlerEventType): void {
|
||||
setInputState((prev) => ({ ...prev, [name]: value }));
|
||||
}
|
||||
return (
|
||||
<Form.Root
|
||||
onSubmit={(event: FormEvent<HTMLFormElement>) => {
|
||||
if (password === "") {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
const data = Object.fromEntries(new FormData(event.currentTarget));
|
||||
event.preventDefault();
|
||||
}}
|
||||
className="h-full w-full"
|
||||
>
|
||||
<div className="flex h-full w-full flex-col items-center justify-center bg-muted">
|
||||
<div className="flex w-72 flex-col items-center justify-center gap-2">
|
||||
<span className="mb-4 text-5xl">⛓️</span>
|
||||
<span className="mb-6 text-2xl font-semibold text-primary">
|
||||
Sign up to Langflow
|
||||
</span>
|
||||
<div className="mb-3 w-full">
|
||||
<Form.Field name="username">
|
||||
<Form.Label className="data-[invalid]:label-invalid">
|
||||
Username <span className="font-medium text-destructive">*</span>
|
||||
</Form.Label>
|
||||
|
||||
<Form.Control asChild>
|
||||
<Input
|
||||
onChange={({ target: { value } }) => {
|
||||
handleInput({ target: { name: "username", value } });
|
||||
}}
|
||||
value={username}
|
||||
className="w-full"
|
||||
required
|
||||
placeholder="Username"
|
||||
/>
|
||||
</Form.Control>
|
||||
|
||||
<Form.Message match="valueMissing" className="field-invalid">
|
||||
Please enter your username
|
||||
</Form.Message>
|
||||
</Form.Field>
|
||||
</div>
|
||||
<div className="mb-3 w-full">
|
||||
<Form.Field name="password" serverInvalid={password != cnfPassword}>
|
||||
<Form.Label className="data-[invalid]:label-invalid">
|
||||
Password <span className="font-medium text-destructive">*</span>
|
||||
</Form.Label>
|
||||
<InputComponent
|
||||
onChange={(value) => {
|
||||
handleInput({ target: { name: "password", value } });
|
||||
}}
|
||||
value={password}
|
||||
isForm
|
||||
password={true}
|
||||
required
|
||||
placeholder="Password"
|
||||
className="w-full"
|
||||
/>
|
||||
|
||||
<Form.Message className="field-invalid" match="valueMissing">
|
||||
Please enter a password
|
||||
</Form.Message>
|
||||
|
||||
{password != cnfPassword && (
|
||||
<Form.Message className="field-invalid">
|
||||
Passwords do not match
|
||||
</Form.Message>
|
||||
)}
|
||||
</Form.Field>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<Form.Field
|
||||
name="confirmpassword"
|
||||
serverInvalid={password != cnfPassword}
|
||||
>
|
||||
<Form.Label className="data-[invalid]:label-invalid">
|
||||
Confirm your password{" "}
|
||||
<span className="font-medium text-destructive">*</span>
|
||||
</Form.Label>
|
||||
|
||||
<InputComponent
|
||||
onChange={(value) => {
|
||||
handleInput({ target: { name: "cnfPassword", value } });
|
||||
}}
|
||||
value={cnfPassword}
|
||||
isForm
|
||||
password={true}
|
||||
required
|
||||
placeholder="Confirm your password"
|
||||
className="w-full"
|
||||
/>
|
||||
|
||||
<Form.Message className="field-invalid" match="valueMissing">
|
||||
Please confirm your password
|
||||
</Form.Message>
|
||||
</Form.Field>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<Form.Submit asChild>
|
||||
<Button className="mr-3 mt-6 w-full">Sign up</Button>
|
||||
</Form.Submit>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<Link to="/login">
|
||||
<Button className="w-full" variant="outline">
|
||||
Already have an account? <b>Sign in</b>
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Form.Root>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,7 +1,11 @@
|
|||
import { Route, Routes } from "react-router-dom";
|
||||
import AdminPage from "./pages/AdminPage";
|
||||
import LoginAdminPage from "./pages/AdminPage/LoginPage";
|
||||
import CommunityPage from "./pages/CommunityPage";
|
||||
import FlowPage from "./pages/FlowPage";
|
||||
import HomePage from "./pages/MainPage";
|
||||
import DeleteAccountPage from "./pages/deleteAccountPage";
|
||||
import LoginPage from "./pages/loginPage";
|
||||
|
||||
const Router = () => {
|
||||
return (
|
||||
|
|
@ -12,6 +16,16 @@ const Router = () => {
|
|||
<Route path="" element={<FlowPage />} />
|
||||
</Route>
|
||||
<Route path="*" element={<HomePage />} />
|
||||
|
||||
<Route path="/login" element={<LoginPage />} />
|
||||
{/* <Route path="/signup" element={<SignUp />} /> */}
|
||||
<Route path="/login/admin" element={<LoginAdminPage />} />
|
||||
|
||||
<Route path="/admin" element={<AdminPage />} />
|
||||
|
||||
<Route path="/account">
|
||||
<Route path="delete" element={<DeleteAccountPage />}></Route>
|
||||
</Route>
|
||||
</Routes>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1025,4 +1025,12 @@
|
|||
.chat-message-highlight {
|
||||
@apply rounded-md bg-indigo-100 px-0.5 dark:bg-indigo-900;
|
||||
}
|
||||
|
||||
.field-invalid{
|
||||
@apply font-medium text-[0.8rem] text-destructive absolute
|
||||
}
|
||||
|
||||
.label-invalid{
|
||||
@apply text-destructive
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
export type ErrorAlertType = {
|
||||
title: string;
|
||||
list: Array<string>;
|
||||
list: Array<string> | undefined;
|
||||
id: string;
|
||||
removeAlert: (id: string) => void;
|
||||
};
|
||||
export type NoticeAlertType = {
|
||||
title: string;
|
||||
link: string;
|
||||
link: string | undefined;
|
||||
id: string;
|
||||
removeAlert: (id: string) => void;
|
||||
};
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue