Merge branch 'login' into types_refactor
This commit is contained in:
commit
e33e236466
39 changed files with 5711 additions and 2253 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)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import contextlib
|
||||
import json
|
||||
import os
|
||||
import secrets
|
||||
from typing import Optional, List
|
||||
from pathlib import Path
|
||||
|
||||
|
|
@ -35,6 +36,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("DATABASE_URL", pre=True)
|
||||
def set_database_url(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",
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
import { createElement } from "react";
|
||||
import { IconComponentProps } from "../../types/components";
|
||||
import { nodeIconsLucide } from "../../utils/styleUtils";
|
||||
|
||||
|
|
@ -7,9 +6,12 @@ export default function IconComponent({
|
|||
className,
|
||||
iconColor,
|
||||
}: IconComponentProps): JSX.Element {
|
||||
const TargetIcon = createElement(nodeIconsLucide[name], {
|
||||
className,
|
||||
style: { color: iconColor },
|
||||
});
|
||||
return TargetIcon as JSX.Element;
|
||||
const TargetIcon = nodeIconsLucide[name] ?? nodeIconsLucide["unknown"];
|
||||
return (
|
||||
<TargetIcon
|
||||
strokeWidth={1.5}
|
||||
className={className}
|
||||
style={{ color: iconColor }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,24 +31,12 @@ export default function Header(): JSX.Element {
|
|||
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,8 +8,12 @@ export default function InputComponent({
|
|||
value,
|
||||
onChange,
|
||||
disabled,
|
||||
required = false,
|
||||
isForm = false,
|
||||
password,
|
||||
editNode = false,
|
||||
placeholder = "Type something...",
|
||||
className,
|
||||
}: InputComponentType): JSX.Element {
|
||||
const [pwdVisible, setPwdVisible] = useState(false);
|
||||
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -27,4 +27,4 @@ const PopoverContent = React.forwardRef<
|
|||
));
|
||||
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
|
||||
|
||||
export { Popover, PopoverTrigger, PopoverContent };
|
||||
export { Popover, PopoverContent, PopoverTrigger };
|
||||
|
|
|
|||
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],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
|
|||
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);
|
||||
|
||||
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,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
|
||||
|
|
@ -16,32 +15,31 @@ function ApiInterceptor(): null {
|
|||
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 as AxiosError).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);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
|||
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>
|
||||
);
|
||||
}
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
|
@ -48,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,
|
||||
|
|
@ -74,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]";
|
||||
|
|
@ -82,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]";
|
||||
|
|
@ -109,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>
|
||||
|
|
|
|||
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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,9 +10,13 @@ export type InputComponentType = {
|
|||
disabled?: boolean;
|
||||
onChange: (value: string) => void;
|
||||
password: boolean;
|
||||
required?: boolean;
|
||||
isForm?: boolean;
|
||||
editNode?: boolean;
|
||||
onChangePass?: (value: boolean | boolean) => void;
|
||||
showPass?: boolean;
|
||||
placeholder?: string;
|
||||
className?: string;
|
||||
};
|
||||
export type ToggleComponentType = {
|
||||
enabled: boolean;
|
||||
|
|
@ -167,7 +171,7 @@ export interface IVarHighlightType {
|
|||
|
||||
export type IconComponentProps = {
|
||||
name: string;
|
||||
className: string;
|
||||
className?: string;
|
||||
iconColor?: string;
|
||||
};
|
||||
|
||||
|
|
@ -206,6 +210,57 @@ export interface languageMap {
|
|||
[key: string]: string | undefined;
|
||||
}
|
||||
|
||||
export type signUpInputStateType = {
|
||||
password: string;
|
||||
cnfPassword: string;
|
||||
username: string;
|
||||
};
|
||||
|
||||
export type inputHandlerEventType = {
|
||||
target: {
|
||||
value: string;
|
||||
name: string;
|
||||
};
|
||||
};
|
||||
export type PaginatorComponentType = {
|
||||
pageSize: number;
|
||||
pageIndex: number;
|
||||
rowsCount?: number[];
|
||||
totalRowsCount: number;
|
||||
paginate: (pageIndex: number, pageSize: number) => void;
|
||||
};
|
||||
|
||||
export type ConfirmationModalType = {
|
||||
title: string;
|
||||
titleHeader: string;
|
||||
modalContent: string;
|
||||
modalContentTitle: string;
|
||||
cancelText: string;
|
||||
confirmationText: string;
|
||||
children: ReactElement;
|
||||
icon: string;
|
||||
data: any;
|
||||
index: number;
|
||||
onConfirm: (index, data) => void;
|
||||
};
|
||||
|
||||
export type UserManagementType = {
|
||||
title: string;
|
||||
titleHeader: string;
|
||||
cancelText: string;
|
||||
confirmationText: string;
|
||||
children: ReactElement;
|
||||
icon: string;
|
||||
data?: any;
|
||||
index?: number;
|
||||
onConfirm: (index, data) => void;
|
||||
};
|
||||
|
||||
export type loginInputStateType = {
|
||||
username: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
export type groupedObjType = {
|
||||
family: string;
|
||||
type: string;
|
||||
|
|
|
|||
16
src/frontend/src/types/contexts/auth.ts
Normal file
16
src/frontend/src/types/contexts/auth.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
export type AuthContextType = {
|
||||
isAuthenticated: boolean;
|
||||
accessToken: string | null;
|
||||
login: (accessToken: string, refreshToken: string) => void;
|
||||
logout: () => void;
|
||||
refreshAccessToken: (refreshToken: string) => Promise<void>;
|
||||
userData: userData | null;
|
||||
setUserData: (userData: userData | null) => void;
|
||||
};
|
||||
|
||||
export type userData = {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
role: string;
|
||||
};
|
||||
|
|
@ -5,6 +5,8 @@ import {
|
|||
ChevronDown,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
ChevronsLeft,
|
||||
ChevronsRight,
|
||||
ChevronsUpDown,
|
||||
Circle,
|
||||
Clipboard,
|
||||
|
|
@ -44,6 +46,7 @@ import {
|
|||
MoonIcon,
|
||||
MoreHorizontal,
|
||||
Paperclip,
|
||||
Pencil,
|
||||
Plus,
|
||||
Redo,
|
||||
Rocket,
|
||||
|
|
@ -57,6 +60,8 @@ import {
|
|||
Trash2,
|
||||
Undo,
|
||||
Upload,
|
||||
UserMinus2,
|
||||
UserPlus2,
|
||||
Users2,
|
||||
Variable,
|
||||
Wand2,
|
||||
|
|
@ -65,6 +70,7 @@ import {
|
|||
XCircle,
|
||||
Zap,
|
||||
} from "lucide-react";
|
||||
import { FaApple, FaGithub } from "react-icons/fa";
|
||||
import { AirbyteIcon } from "../icons/Airbyte";
|
||||
import { AnthropicIcon } from "../icons/Anthropic";
|
||||
import { BingIcon } from "../icons/Bing";
|
||||
|
|
@ -277,4 +283,11 @@ export const nodeIconsLucide: iconsType = {
|
|||
Upload,
|
||||
MessageSquare,
|
||||
MoreHorizontal,
|
||||
UserMinus2,
|
||||
UserPlus2,
|
||||
Pencil,
|
||||
ChevronsRight,
|
||||
ChevronsLeft,
|
||||
FaGithub,
|
||||
FaApple,
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue