Profile Page implementation and fix on Admin Page (#896)
This commit is contained in:
commit
374ae22563
36 changed files with 1343 additions and 844 deletions
|
|
@ -0,0 +1,49 @@
|
||||||
|
"""Add profile-image column
|
||||||
|
|
||||||
|
Revision ID: 67cc006d50bf
|
||||||
|
Revises: 260dbcc8b680
|
||||||
|
Create Date: 2023-09-08 07:36:13.387318
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
import sqlmodel
|
||||||
|
from sqlalchemy.engine.reflection import Inspector
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = "67cc006d50bf"
|
||||||
|
down_revision: Union[str, None] = "260dbcc8b680"
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
conn = op.get_bind()
|
||||||
|
inspector = Inspector.from_engine(conn)
|
||||||
|
if "user" in inspector.get_table_names() and "profile_image" not in [
|
||||||
|
column["name"] for column in inspector.get_columns("user")
|
||||||
|
]:
|
||||||
|
with op.batch_alter_table("user", schema=None) as batch_op:
|
||||||
|
batch_op.add_column(
|
||||||
|
sa.Column(
|
||||||
|
"profile_image", sqlmodel.sql.sqltypes.AutoString(), nullable=True
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
conn = op.get_bind()
|
||||||
|
inspector = Inspector.from_engine(conn)
|
||||||
|
if "user" in inspector.get_table_names() and "profile_image" in [
|
||||||
|
column["name"] for column in inspector.get_columns("user")
|
||||||
|
]:
|
||||||
|
with op.batch_alter_table("user", schema=None) as batch_op:
|
||||||
|
batch_op.drop_column("profile_image")
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
@ -18,15 +18,17 @@ from langflow.services.auth.utils import (
|
||||||
get_current_active_superuser,
|
get_current_active_superuser,
|
||||||
get_current_active_user,
|
get_current_active_user,
|
||||||
get_password_hash,
|
get_password_hash,
|
||||||
|
verify_password,
|
||||||
)
|
)
|
||||||
from langflow.services.database.models.user.crud import (
|
from langflow.services.database.models.user.crud import (
|
||||||
|
get_user_by_id,
|
||||||
update_user,
|
update_user,
|
||||||
)
|
)
|
||||||
|
|
||||||
router = APIRouter(tags=["Users"])
|
router = APIRouter(tags=["Users"], prefix="/users")
|
||||||
|
|
||||||
|
|
||||||
@router.post("/user", response_model=UserRead, status_code=201)
|
@router.post("/", response_model=UserRead, status_code=201)
|
||||||
def add_user(
|
def add_user(
|
||||||
user: UserCreate,
|
user: UserCreate,
|
||||||
session: Session = Depends(get_session),
|
session: Session = Depends(get_session),
|
||||||
|
|
@ -50,7 +52,7 @@ def add_user(
|
||||||
return new_user
|
return new_user
|
||||||
|
|
||||||
|
|
||||||
@router.get("/user", response_model=UserRead)
|
@router.get("/whoami", response_model=UserRead)
|
||||||
def read_current_user(
|
def read_current_user(
|
||||||
current_user: User = Depends(get_current_active_user),
|
current_user: User = Depends(get_current_active_user),
|
||||||
) -> User:
|
) -> User:
|
||||||
|
|
@ -60,7 +62,7 @@ def read_current_user(
|
||||||
return current_user
|
return current_user
|
||||||
|
|
||||||
|
|
||||||
@router.get("/users", response_model=UsersResponse)
|
@router.get("/", response_model=UsersResponse)
|
||||||
def read_all_users(
|
def read_all_users(
|
||||||
skip: int = 0,
|
skip: int = 0,
|
||||||
limit: int = 10,
|
limit: int = 10,
|
||||||
|
|
@ -82,20 +84,61 @@ def read_all_users(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.patch("/user/{user_id}", response_model=UserRead)
|
@router.patch("/{user_id}", response_model=UserRead)
|
||||||
def patch_user(
|
def patch_user(
|
||||||
user_id: UUID,
|
user_id: UUID,
|
||||||
user: UserUpdate,
|
user_update: UserUpdate,
|
||||||
_: Session = Depends(get_current_active_user),
|
user: Session = Depends(get_current_active_user),
|
||||||
session: Session = Depends(get_session),
|
session: Session = Depends(get_session),
|
||||||
) -> User:
|
) -> User:
|
||||||
"""
|
"""
|
||||||
Update an existing user's data.
|
Update an existing user's data.
|
||||||
"""
|
"""
|
||||||
return update_user(user_id, user, session)
|
if not user.is_superuser and user.id != user_id:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=403, detail="You don't have the permission to update this user"
|
||||||
|
)
|
||||||
|
if user_update.password:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400, detail="You can't change your password here"
|
||||||
|
)
|
||||||
|
|
||||||
|
if user_db := get_user_by_id(session, user_id):
|
||||||
|
return update_user(user_db, user_update, session)
|
||||||
|
else:
|
||||||
|
raise HTTPException(status_code=404, detail="User not found")
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/user/{user_id}")
|
@router.patch("/{user_id}/reset-password", response_model=UserRead)
|
||||||
|
def reset_password(
|
||||||
|
user_id: UUID,
|
||||||
|
user_update: UserUpdate,
|
||||||
|
user: Session = Depends(get_current_active_user),
|
||||||
|
session: Session = Depends(get_session),
|
||||||
|
) -> User:
|
||||||
|
"""
|
||||||
|
Reset a user's password.
|
||||||
|
"""
|
||||||
|
if user_id != user.id:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400, detail="You can't change another user's password"
|
||||||
|
)
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
raise HTTPException(status_code=404, detail="User not found")
|
||||||
|
if verify_password(user_update.password, user.password):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400, detail="You can't use your current password"
|
||||||
|
)
|
||||||
|
new_password = get_password_hash(user_update.password)
|
||||||
|
user.password = new_password
|
||||||
|
session.commit()
|
||||||
|
session.refresh(user)
|
||||||
|
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/{user_id}", response_model=dict)
|
||||||
def delete_user(
|
def delete_user(
|
||||||
user_id: UUID,
|
user_id: UUID,
|
||||||
current_user: User = Depends(get_current_active_superuser),
|
current_user: User = Depends(get_current_active_superuser),
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,12 @@
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import Union
|
from typing import Union
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
from fastapi import Depends, HTTPException
|
from fastapi import Depends, HTTPException, status
|
||||||
from langflow.services.database.models.user.user import User, UserUpdate
|
from langflow.services.database.models.user.user import User, UserUpdate
|
||||||
from langflow.services.utils import get_session
|
from langflow.services.utils import get_session
|
||||||
from sqlalchemy.exc import IntegrityError
|
from sqlalchemy.exc import IntegrityError
|
||||||
from sqlmodel import Session
|
from sqlmodel import Session
|
||||||
|
|
||||||
|
|
||||||
from sqlalchemy.orm.attributes import flag_modified
|
from sqlalchemy.orm.attributes import flag_modified
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -20,20 +19,26 @@ def get_user_by_id(db: Session, id: UUID) -> Union[User, None]:
|
||||||
|
|
||||||
|
|
||||||
def update_user(
|
def update_user(
|
||||||
user_id: UUID, user: UserUpdate, db: Session = Depends(get_session)
|
user_db: User, user: UserUpdate, db: Session = Depends(get_session)
|
||||||
) -> User:
|
) -> User:
|
||||||
user_db = get_user_by_id(db, user_id)
|
|
||||||
if not user_db:
|
if not user_db:
|
||||||
raise HTTPException(status_code=404, detail="User not found")
|
raise HTTPException(status_code=404, detail="User not found")
|
||||||
|
|
||||||
user_db_by_username = get_user_by_username(db, user.username) # type: ignore
|
# user_db_by_username = get_user_by_username(db, user.username) # type: ignore
|
||||||
if user_db_by_username and user_db_by_username.id != user_id:
|
# if user_db_by_username and user_db_by_username.id != user_id:
|
||||||
raise HTTPException(status_code=409, detail="Username already exists")
|
# raise HTTPException(status_code=409, detail="Username already exists")
|
||||||
|
|
||||||
user_data = user.dict(exclude_unset=True)
|
user_data = user.dict(exclude_unset=True)
|
||||||
|
changed = False
|
||||||
for attr, value in user_data.items():
|
for attr, value in user_data.items():
|
||||||
if hasattr(user_db, attr) and value is not None:
|
if hasattr(user_db, attr) and value is not None:
|
||||||
setattr(user_db, attr, value)
|
setattr(user_db, attr, value)
|
||||||
|
changed = True
|
||||||
|
|
||||||
|
if not changed:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_304_NOT_MODIFIED, detail="Nothing to update"
|
||||||
|
)
|
||||||
|
|
||||||
user_db.updated_at = datetime.now(timezone.utc)
|
user_db.updated_at = datetime.now(timezone.utc)
|
||||||
flag_modified(user_db, "updated_at")
|
flag_modified(user_db, "updated_at")
|
||||||
|
|
@ -49,5 +54,5 @@ def update_user(
|
||||||
|
|
||||||
def update_user_last_login_at(user_id: UUID, db: Session = Depends(get_session)):
|
def update_user_last_login_at(user_id: UUID, db: Session = Depends(get_session)):
|
||||||
user_data = UserUpdate(last_login_at=datetime.now(timezone.utc)) # type: ignore
|
user_data = UserUpdate(last_login_at=datetime.now(timezone.utc)) # type: ignore
|
||||||
|
user = get_user_by_id(db, user_id)
|
||||||
return update_user(user_id, user_data, db)
|
return update_user(user, user_data, db)
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ class User(SQLModelSerializable, table=True):
|
||||||
id: UUID = Field(default_factory=uuid4, primary_key=True, unique=True)
|
id: UUID = Field(default_factory=uuid4, primary_key=True, unique=True)
|
||||||
username: str = Field(index=True, unique=True)
|
username: str = Field(index=True, unique=True)
|
||||||
password: str = Field()
|
password: str = Field()
|
||||||
|
profile_image: Optional[str] = Field(default=None)
|
||||||
is_active: bool = Field(default=False)
|
is_active: bool = Field(default=False)
|
||||||
is_superuser: bool = Field(default=False)
|
is_superuser: bool = Field(default=False)
|
||||||
create_at: datetime = Field(default_factory=datetime.utcnow)
|
create_at: datetime = Field(default_factory=datetime.utcnow)
|
||||||
|
|
@ -32,6 +33,7 @@ class UserCreate(SQLModel):
|
||||||
class UserRead(SQLModel):
|
class UserRead(SQLModel):
|
||||||
id: UUID = Field(default_factory=uuid4)
|
id: UUID = Field(default_factory=uuid4)
|
||||||
username: str = Field()
|
username: str = Field()
|
||||||
|
profile_image: Optional[str] = Field()
|
||||||
is_active: bool = Field()
|
is_active: bool = Field()
|
||||||
is_superuser: bool = Field()
|
is_superuser: bool = Field()
|
||||||
create_at: datetime = Field()
|
create_at: datetime = Field()
|
||||||
|
|
@ -40,7 +42,8 @@ class UserRead(SQLModel):
|
||||||
|
|
||||||
|
|
||||||
class UserUpdate(SQLModel):
|
class UserUpdate(SQLModel):
|
||||||
username: Optional[str] = Field()
|
profile_image: Optional[str] = Field()
|
||||||
|
password: Optional[str] = Field()
|
||||||
is_active: Optional[bool] = Field()
|
is_active: Optional[bool] = Field()
|
||||||
is_superuser: Optional[bool] = Field()
|
is_superuser: Optional[bool] = Field()
|
||||||
last_login_at: Optional[datetime] = Field()
|
last_login_at: Optional[datetime] = Field()
|
||||||
|
|
|
||||||
868
src/frontend/package-lock.json
generated
868
src/frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -12,17 +12,15 @@ import { Button } from "../ui/button";
|
||||||
|
|
||||||
export default function PaginatorComponent({
|
export default function PaginatorComponent({
|
||||||
pageSize = 10,
|
pageSize = 10,
|
||||||
pageIndex = 0,
|
pageIndex = 1,
|
||||||
rowsCount = [10, 20, 50, 100],
|
rowsCount = [10, 20, 50, 100],
|
||||||
totalRowsCount = 0,
|
totalRowsCount = 0,
|
||||||
paginate,
|
paginate,
|
||||||
}: PaginatorComponentType) {
|
}: PaginatorComponentType) {
|
||||||
const [size, setPageSize] = useState(pageSize);
|
const [size, setPageSize] = useState(pageSize);
|
||||||
const [index, setPageIndex] = useState(pageIndex);
|
|
||||||
const [maxIndex, setMaxPageIndex] = useState(
|
const [maxIndex, setMaxPageIndex] = useState(
|
||||||
Math.ceil(totalRowsCount / pageSize)
|
Math.ceil(totalRowsCount / pageSize)
|
||||||
);
|
);
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setMaxPageIndex(Math.ceil(totalRowsCount / size));
|
setMaxPageIndex(Math.ceil(totalRowsCount / size));
|
||||||
|
|
@ -39,8 +37,9 @@ export default function PaginatorComponent({
|
||||||
onValueChange={(pageSize: string) => {
|
onValueChange={(pageSize: string) => {
|
||||||
setPageSize(Number(pageSize));
|
setPageSize(Number(pageSize));
|
||||||
setMaxPageIndex(Math.ceil(totalRowsCount / Number(pageSize)));
|
setMaxPageIndex(Math.ceil(totalRowsCount / Number(pageSize)));
|
||||||
paginate(Number(pageSize), 0);
|
paginate(Number(pageSize), 1);
|
||||||
}}
|
}}
|
||||||
|
value={pageSize.toString()}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="w-[100px]">
|
<SelectTrigger className="w-[100px]">
|
||||||
<SelectValue placeholder="10" />
|
<SelectValue placeholder="10" />
|
||||||
|
|
@ -55,30 +54,25 @@ export default function PaginatorComponent({
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-[100px] items-center justify-center text-sm font-medium">
|
<div className="flex w-[100px] items-center justify-center text-sm font-medium">
|
||||||
Page {currentPage} of {maxIndex}
|
Page {pageIndex} of {maxIndex}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Button
|
<Button
|
||||||
disabled={index <= 0}
|
disabled={pageIndex <= 1}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="hidden h-8 w-8 p-0 lg:flex"
|
className="hidden h-8 w-8 p-0 lg:flex"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setPageIndex(0);
|
paginate(size, 1);
|
||||||
setCurrentPage(1);
|
|
||||||
paginate(size, 0);
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="sr-only">Go to first page</span>
|
<span className="sr-only">Go to first page</span>
|
||||||
<IconComponent name="ChevronsLeft" className="h-4 w-4" />
|
<IconComponent name="ChevronsLeft" className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
disabled={index <= 0}
|
disabled={pageIndex <= 1}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (index > 0) {
|
if (pageIndex > 0) {
|
||||||
const pgIndex = size - index;
|
paginate(size, pageIndex - 1);
|
||||||
setCurrentPage(currentPage - 1);
|
|
||||||
setPageIndex(pgIndex);
|
|
||||||
paginate(size, pgIndex);
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
|
@ -88,12 +82,9 @@ export default function PaginatorComponent({
|
||||||
<IconComponent name="ChevronLeft" className="h-4 w-4" />
|
<IconComponent name="ChevronLeft" className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
disabled={currentPage === maxIndex}
|
disabled={pageIndex === maxIndex}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const pgIndex = size + index;
|
paginate(size, pageIndex + 1);
|
||||||
setPageIndex(pgIndex);
|
|
||||||
setCurrentPage(currentPage + 1);
|
|
||||||
paginate(size, pgIndex);
|
|
||||||
}}
|
}}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="h-8 w-8 p-0"
|
className="h-8 w-8 p-0"
|
||||||
|
|
@ -102,13 +93,11 @@ export default function PaginatorComponent({
|
||||||
<IconComponent name="ChevronRight" className="h-4 w-4" />
|
<IconComponent name="ChevronRight" className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
disabled={currentPage === maxIndex}
|
disabled={pageIndex === maxIndex}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="hidden h-8 w-8 p-0 lg:flex"
|
className="hidden h-8 w-8 p-0 lg:flex"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setPageIndex(maxIndex - 1);
|
paginate(size, maxIndex);
|
||||||
setCurrentPage(maxIndex);
|
|
||||||
paginate(size, size);
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="sr-only">Go to last page</span>
|
<span className="sr-only">Go to last page</span>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { gradients } from "../../utils/styleUtils";
|
||||||
|
|
||||||
|
export default function GradientChooserComponent({ value, onChange }) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-wrap items-center justify-center gap-4">
|
||||||
|
{gradients.map((gradient, idx) => (
|
||||||
|
<div
|
||||||
|
onClick={() => {
|
||||||
|
onChange(gradient);
|
||||||
|
}}
|
||||||
|
className={
|
||||||
|
"duration-400 h-12 w-12 cursor-pointer rounded-full transition-all " +
|
||||||
|
gradient +
|
||||||
|
(value === gradient ? " shadow-lg ring-2 ring-primary" : "")
|
||||||
|
}
|
||||||
|
key={idx}
|
||||||
|
></div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -139,7 +139,7 @@ export default function Header(): JSX.Element {
|
||||||
<button
|
<button
|
||||||
className={
|
className={
|
||||||
"h-7 w-7 rounded-full focus-visible:outline-0 " +
|
"h-7 w-7 rounded-full focus-visible:outline-0 " +
|
||||||
gradients[gradientIndex]
|
(userData?.profile_image ?? gradients[gradientIndex])
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
|
|
@ -154,6 +154,12 @@ export default function Header(): JSX.Element {
|
||||||
Admin Page
|
Admin Page
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="cursor-pointer"
|
||||||
|
onClick={() => navigate("/account/settings")}
|
||||||
|
>
|
||||||
|
Profile Settings
|
||||||
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,10 @@ export default function LoadingComponent({
|
||||||
remSize,
|
remSize,
|
||||||
}: LoadingComponentProps): JSX.Element {
|
}: LoadingComponentProps): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<div role="status" className="m-auto w-min">
|
<div role="status" className="flex flex-col items-center justify-center">
|
||||||
<svg
|
<svg
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className={`w-${remSize} h-${remSize} mr-2 animate-spin fill-almost-medium-blue text-muted`}
|
className={`w-${remSize} h-${remSize} animate-spin fill-almost-medium-blue text-muted`}
|
||||||
viewBox="0 0 100 101"
|
viewBox="0 0 100 101"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
|
|
||||||
|
|
@ -499,6 +499,21 @@ export const NOUNS: string[] = [
|
||||||
*/
|
*/
|
||||||
export const USER_PROJECTS_HEADER = "My Collection";
|
export const USER_PROJECTS_HEADER = "My Collection";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Header text for admin page
|
||||||
|
* @constant
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export const ADMIN_HEADER_TITLE = "Admin Page";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Header description for admin page
|
||||||
|
* @constant
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export const ADMIN_HEADER_DESCRIPTION =
|
||||||
|
"Navigate through this section to efficiently oversee all application users. From here, you can seamlessly manage user accounts.";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* URLs excluded from error retries.
|
* URLs excluded from error retries.
|
||||||
* @constant
|
* @constant
|
||||||
|
|
@ -519,6 +534,12 @@ export const CONTROL_INPUT_STATE = {
|
||||||
username: "",
|
username: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const CONTROL_PATCH_USER_STATE = {
|
||||||
|
password: "",
|
||||||
|
cnfPassword: "",
|
||||||
|
gradient: "",
|
||||||
|
};
|
||||||
|
|
||||||
export const CONTROL_LOGIN_STATE = {
|
export const CONTROL_LOGIN_STATE = {
|
||||||
username: "",
|
username: "",
|
||||||
password: "",
|
password: "",
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,7 @@ export function AuthProvider({ children }): React.ReactElement {
|
||||||
.then((user) => {
|
.then((user) => {
|
||||||
setUserData(user);
|
setUserData(user);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
const isSuperUser = user.is_superuser;
|
const isSuperUser = user!.is_superuser;
|
||||||
setIsAdmin(isSuperUser);
|
setIsAdmin(isSuperUser);
|
||||||
})
|
})
|
||||||
.catch((error) => {});
|
.catch((error) => {});
|
||||||
|
|
|
||||||
|
|
@ -238,7 +238,6 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
||||||
function hardReset() {
|
function hardReset() {
|
||||||
newNodeId.current = uid();
|
newNodeId.current = uid();
|
||||||
setTabId("");
|
setTabId("");
|
||||||
|
|
||||||
setFlows([]);
|
setFlows([]);
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setId(uid());
|
setId(uid());
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ import {
|
||||||
APIObjectType,
|
APIObjectType,
|
||||||
LoginType,
|
LoginType,
|
||||||
Users,
|
Users,
|
||||||
|
changeUser,
|
||||||
|
resetPasswordType,
|
||||||
sendAllProps,
|
sendAllProps,
|
||||||
} from "../../types/api/index";
|
} from "../../types/api/index";
|
||||||
import { UserInputType } from "../../types/components";
|
import { UserInputType } from "../../types/components";
|
||||||
|
|
@ -402,9 +404,9 @@ export async function renewAccessToken(token: string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getLoggedUser(): Promise<Users> {
|
export async function getLoggedUser(): Promise<Users | null> {
|
||||||
try {
|
try {
|
||||||
const res = await api.get(`${BASE_URL_API}user`);
|
const res = await api.get(`${BASE_URL_API}users/whoami`);
|
||||||
|
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
return res.data;
|
return res.data;
|
||||||
|
|
@ -413,14 +415,16 @@ export async function getLoggedUser(): Promise<Users> {
|
||||||
console.log("Error:", error);
|
console.log("Error:", error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function addUser(user: UserInputType): Promise<Users> {
|
export async function addUser(user: UserInputType): Promise<Array<Users>> {
|
||||||
try {
|
try {
|
||||||
const res = await api.post(`${BASE_URL_API}user`, user);
|
const res = await api.post(`${BASE_URL_API}users/`, user);
|
||||||
if (res.status === 200) {
|
if (res.status !== 201) {
|
||||||
return res.data;
|
throw new Error(res.data.detail);
|
||||||
}
|
}
|
||||||
|
return res.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("Error:", error);
|
console.log("Error:", error);
|
||||||
throw error;
|
throw error;
|
||||||
|
|
@ -430,10 +434,10 @@ export async function addUser(user: UserInputType): Promise<Users> {
|
||||||
export async function getUsersPage(
|
export async function getUsersPage(
|
||||||
skip: number,
|
skip: number,
|
||||||
limit: number
|
limit: number
|
||||||
): Promise<[Users]> {
|
): Promise<Array<Users>> {
|
||||||
try {
|
try {
|
||||||
const res = await api.get(
|
const res = await api.get(
|
||||||
`${BASE_URL_API}users?skip=${skip}&limit=${limit}`
|
`${BASE_URL_API}users/?skip=${skip}&limit=${limit}`
|
||||||
);
|
);
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
return res.data;
|
return res.data;
|
||||||
|
|
@ -442,11 +446,12 @@ export async function getUsersPage(
|
||||||
console.log("Error:", error);
|
console.log("Error:", error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteUser(user_id: string) {
|
export async function deleteUser(user_id: string) {
|
||||||
try {
|
try {
|
||||||
const res = await api.delete(`${BASE_URL_API}user/${user_id}`);
|
const res = await api.delete(`${BASE_URL_API}users/${user_id}`);
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
@ -456,9 +461,24 @@ export async function deleteUser(user_id: string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateUser(user_id: string, user: Users) {
|
export async function updateUser(user_id: string, user: changeUser) {
|
||||||
try {
|
try {
|
||||||
const res = await api.patch(`${BASE_URL_API}user/${user_id}`, user);
|
const res = await api.patch(`${BASE_URL_API}users/${user_id}`, user);
|
||||||
|
if (res.status === 200) {
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function resetPassword(user_id: string, user: resetPasswordType) {
|
||||||
|
try {
|
||||||
|
const res = await api.patch(
|
||||||
|
`${BASE_URL_API}users/${user_id}/reset-password`,
|
||||||
|
user
|
||||||
|
);
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,11 +33,9 @@ const ApiModal = forwardRef(
|
||||||
{
|
{
|
||||||
flow,
|
flow,
|
||||||
children,
|
children,
|
||||||
disable,
|
|
||||||
}: {
|
}: {
|
||||||
flow: FlowType;
|
flow: FlowType;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
disable: boolean;
|
|
||||||
},
|
},
|
||||||
ref
|
ref
|
||||||
) => {
|
) => {
|
||||||
|
|
@ -201,8 +199,8 @@ const ApiModal = forwardRef(
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseModal open={open} setOpen={setOpen} disable={disable}>
|
<BaseModal open={open} setOpen={setOpen}>
|
||||||
<BaseModal.Trigger>{children}</BaseModal.Trigger>
|
<BaseModal.Trigger asChild>{children}</BaseModal.Trigger>
|
||||||
<BaseModal.Header description={EXPORT_CODE_DIALOG}>
|
<BaseModal.Header description={EXPORT_CODE_DIALOG}>
|
||||||
<span className="pr-2">Code</span>
|
<span className="pr-2">Code</span>
|
||||||
<IconComponent
|
<IconComponent
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import BaseModal from "../baseModal";
|
||||||
|
|
||||||
export default function ConfirmationModal({
|
export default function ConfirmationModal({
|
||||||
title,
|
title,
|
||||||
|
asChild,
|
||||||
titleHeader,
|
titleHeader,
|
||||||
modalContent,
|
modalContent,
|
||||||
modalContentTitle,
|
modalContentTitle,
|
||||||
|
|
@ -22,7 +23,7 @@ export default function ConfirmationModal({
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
return (
|
return (
|
||||||
<BaseModal size="x-small" open={open} setOpen={setOpen}>
|
<BaseModal size="x-small" open={open} setOpen={setOpen}>
|
||||||
<BaseModal.Trigger>{children}</BaseModal.Trigger>
|
<BaseModal.Trigger asChild={asChild}>{children}</BaseModal.Trigger>
|
||||||
<BaseModal.Header description={titleHeader}>
|
<BaseModal.Header description={titleHeader}>
|
||||||
<span className="pr-2">{title}</span>
|
<span className="pr-2">{title}</span>
|
||||||
<Icon
|
<Icon
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ export default function UserManagementModal({
|
||||||
data,
|
data,
|
||||||
index,
|
index,
|
||||||
onConfirm,
|
onConfirm,
|
||||||
|
asChild,
|
||||||
}: UserManagementType) {
|
}: UserManagementType) {
|
||||||
const Icon: any = nodeIconsLucide[icon];
|
const Icon: any = nodeIconsLucide[icon];
|
||||||
const [pwdVisible, setPwdVisible] = useState(false);
|
const [pwdVisible, setPwdVisible] = useState(false);
|
||||||
|
|
@ -60,7 +61,7 @@ export default function UserManagementModal({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseModal size="medium-h-full" open={open} setOpen={setOpen}>
|
<BaseModal size="medium-h-full" open={open} setOpen={setOpen}>
|
||||||
<BaseModal.Trigger>{children}</BaseModal.Trigger>
|
<BaseModal.Trigger asChild={asChild}>{children}</BaseModal.Trigger>
|
||||||
<BaseModal.Header description={titleHeader}>
|
<BaseModal.Header description={titleHeader}>
|
||||||
<span className="pr-2">{title}</span>
|
<span className="pr-2">{title}</span>
|
||||||
<Icon
|
<Icon
|
||||||
|
|
|
||||||
|
|
@ -14,13 +14,25 @@ import { modalHeaderType } from "../../types/components";
|
||||||
type ContentProps = { children: ReactNode };
|
type ContentProps = { children: ReactNode };
|
||||||
type HeaderProps = { children: ReactNode; description: string };
|
type HeaderProps = { children: ReactNode; description: string };
|
||||||
type FooterProps = { children: ReactNode };
|
type FooterProps = { children: ReactNode };
|
||||||
type TriggerProps = { children: ReactNode };
|
type TriggerProps = {
|
||||||
|
children: ReactNode;
|
||||||
|
asChild?: boolean;
|
||||||
|
disable?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
const Content: React.FC<ContentProps> = ({ children }) => {
|
const Content: React.FC<ContentProps> = ({ children }) => {
|
||||||
return <div className="h-full w-full">{children}</div>;
|
return <div className="h-full w-full">{children}</div>;
|
||||||
};
|
};
|
||||||
const Trigger: React.FC<ContentProps> = ({ children }) => {
|
const Trigger: React.FC<TriggerProps> = ({ children, asChild, disable }) => {
|
||||||
return <>{children}</>;
|
return (
|
||||||
|
<DialogTrigger
|
||||||
|
className={asChild ? "" : "w-full"}
|
||||||
|
hidden={children ? false : true}
|
||||||
|
asChild={asChild}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</DialogTrigger>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const Header: React.FC<{ children: ReactNode; description: string | null }> = ({
|
const Header: React.FC<{ children: ReactNode; description: string | null }> = ({
|
||||||
|
|
@ -47,7 +59,6 @@ interface BaseModalProps {
|
||||||
];
|
];
|
||||||
open?: boolean;
|
open?: boolean;
|
||||||
setOpen?: (open: boolean) => void;
|
setOpen?: (open: boolean) => void;
|
||||||
disable?: boolean;
|
|
||||||
size?:
|
size?:
|
||||||
| "x-small"
|
| "x-small"
|
||||||
| "smaller"
|
| "smaller"
|
||||||
|
|
@ -61,7 +72,6 @@ interface BaseModalProps {
|
||||||
function BaseModal({
|
function BaseModal({
|
||||||
open,
|
open,
|
||||||
setOpen,
|
setOpen,
|
||||||
disable = false,
|
|
||||||
children,
|
children,
|
||||||
size = "large",
|
size = "large",
|
||||||
}: BaseModalProps) {
|
}: BaseModalProps) {
|
||||||
|
|
@ -120,17 +130,12 @@ function BaseModal({
|
||||||
//UPDATE COLORS AND STYLE CLASSSES
|
//UPDATE COLORS AND STYLE CLASSSES
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
<DialogTrigger
|
{triggerChild}
|
||||||
className={"w-full " + (disable ? "button-disable" : "")}
|
|
||||||
hidden={triggerChild ? false : true}
|
|
||||||
>
|
|
||||||
{triggerChild}
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent className={minWidth}>
|
<DialogContent className={minWidth}>
|
||||||
<div className="truncate-doubleline word-break-break-word">
|
<div className="truncate-doubleline word-break-break-word">
|
||||||
{headerChild}
|
{headerChild}
|
||||||
</div>
|
</div>
|
||||||
<div className={`mt-2 flex flex-col ${height} w-full `}>
|
<div className={`mt-2 flex flex-col ${height!} w-full `}>
|
||||||
{ContentChild}
|
{ContentChild}
|
||||||
</div>
|
</div>
|
||||||
{ContentFooter && (
|
{ContentFooter && (
|
||||||
|
|
|
||||||
|
|
@ -28,8 +28,7 @@ export default function CodeAreaModal({
|
||||||
const { dark } = useContext(darkContext);
|
const { dark } = useContext(darkContext);
|
||||||
const { reactFlowInstance } = useContext(typesContext);
|
const { reactFlowInstance } = useContext(typesContext);
|
||||||
const [height, setHeight] = useState<string | null>(null);
|
const [height, setHeight] = useState<string | null>(null);
|
||||||
const { setErrorData, setSuccessData, isTweakPage } =
|
const { setErrorData, setSuccessData } = useContext(alertContext);
|
||||||
useContext(alertContext);
|
|
||||||
const [error, setError] = useState<{
|
const [error, setError] = useState<{
|
||||||
detail: { error: string | undefined; traceback: string | undefined };
|
detail: { error: string | undefined; traceback: string | undefined };
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
|
|
||||||
|
|
@ -10,15 +10,15 @@ import BaseModal from "../baseModal";
|
||||||
|
|
||||||
const ExportModal = forwardRef(
|
const ExportModal = forwardRef(
|
||||||
(props: { children: ReactNode }, ref): JSX.Element => {
|
(props: { children: ReactNode }, ref): JSX.Element => {
|
||||||
const { flows, tabId, updateFlow, downloadFlow } = useContext(TabsContext);
|
const { flows, tabId, downloadFlow } = useContext(TabsContext);
|
||||||
const [checked, setChecked] = useState(false);
|
const [checked, setChecked] = useState(false);
|
||||||
const flow = flows.find((f) => f.id === tabId);
|
const flow = flows.find((f) => f.id === tabId);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setName(flow.name);
|
setName(flow!.name);
|
||||||
setDescription(flow.description);
|
setDescription(flow!.description);
|
||||||
}, [flow.name, flow.description]);
|
}, [flow!.name, flow!.description]);
|
||||||
const [name, setName] = useState(flow.name);
|
const [name, setName] = useState(flow!.name);
|
||||||
const [description, setDescription] = useState(flow.description);
|
const [description, setDescription] = useState(flow!.description);
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -40,7 +40,6 @@ const ExportModal = forwardRef(
|
||||||
tabId={tabId}
|
tabId={tabId}
|
||||||
setName={setName}
|
setName={setName}
|
||||||
setDescription={setDescription}
|
setDescription={setDescription}
|
||||||
updateFlow={updateFlow}
|
|
||||||
/>
|
/>
|
||||||
<div className="mt-3 flex items-center space-x-2">
|
<div className="mt-3 flex items-center space-x-2">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
|
|
|
||||||
|
|
@ -194,7 +194,8 @@ export default function FormModal({
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleWsMessage(data: any) {
|
function handleWsMessage(data: any) {
|
||||||
if (Array.isArray(data)) {
|
console.log(data);
|
||||||
|
if (Array.isArray(data) && data.length > 0) {
|
||||||
//set chat history
|
//set chat history
|
||||||
setChatHistory((_) => {
|
setChatHistory((_) => {
|
||||||
let newChatHistory: ChatMessageType[] = [];
|
let newChatHistory: ChatMessageType[] = [];
|
||||||
|
|
@ -313,7 +314,7 @@ export default function FormModal({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// do not add connectWS on dependencies array
|
// do not add connectWS on dependencies array
|
||||||
}, []);
|
}, [open]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ export default function LoginAdminPage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getUser() {
|
function getUser() {
|
||||||
if (getAuthentication) {
|
if (getAuthentication()) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
getLoggedUser()
|
getLoggedUser()
|
||||||
.then((user) => {
|
.then((user) => {
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { cloneDeep } from "lodash";
|
import { cloneDeep } from "lodash";
|
||||||
import { X } from "lucide-react";
|
|
||||||
import { useContext, useEffect, useRef, useState } from "react";
|
import { useContext, useEffect, useRef, useState } from "react";
|
||||||
import PaginatorComponent from "../../components/PaginatorComponent";
|
import PaginatorComponent from "../../components/PaginatorComponent";
|
||||||
import ShadTooltip from "../../components/ShadTooltipComponent";
|
import ShadTooltip from "../../components/ShadTooltipComponent";
|
||||||
import IconComponent from "../../components/genericIconComponent";
|
import IconComponent from "../../components/genericIconComponent";
|
||||||
import Header from "../../components/headerComponent";
|
import Header from "../../components/headerComponent";
|
||||||
|
import LoadingComponent from "../../components/loadingComponent";
|
||||||
import { Button } from "../../components/ui/button";
|
import { Button } from "../../components/ui/button";
|
||||||
import { Checkbox } from "../../components/ui/checkbox";
|
import { Checkbox } from "../../components/ui/checkbox";
|
||||||
import { Input } from "../../components/ui/input";
|
import { Input } from "../../components/ui/input";
|
||||||
|
|
@ -16,8 +16,13 @@ import {
|
||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "../../components/ui/table";
|
} from "../../components/ui/table";
|
||||||
|
import {
|
||||||
|
ADMIN_HEADER_DESCRIPTION,
|
||||||
|
ADMIN_HEADER_TITLE,
|
||||||
|
} from "../../constants/constants";
|
||||||
import { alertContext } from "../../contexts/alertContext";
|
import { alertContext } from "../../contexts/alertContext";
|
||||||
import { AuthContext } from "../../contexts/authContext";
|
import { AuthContext } from "../../contexts/authContext";
|
||||||
|
import { TabsContext } from "../../contexts/tabsContext";
|
||||||
import {
|
import {
|
||||||
addUser,
|
addUser,
|
||||||
deleteUser,
|
deleteUser,
|
||||||
|
|
@ -33,12 +38,19 @@ export default function AdminPage() {
|
||||||
const [inputValue, setInputValue] = useState("");
|
const [inputValue, setInputValue] = useState("");
|
||||||
|
|
||||||
const [size, setPageSize] = useState(10);
|
const [size, setPageSize] = useState(10);
|
||||||
const [index, setPageIndex] = useState(0);
|
const [index, setPageIndex] = useState(1);
|
||||||
const [loadingUsers, setLoadingUsers] = useState(true);
|
const [loadingUsers, setLoadingUsers] = useState(true);
|
||||||
const { setErrorData, setSuccessData } = useContext(alertContext);
|
const { setErrorData, setSuccessData } = useContext(alertContext);
|
||||||
const { userData } = useContext(AuthContext);
|
const { userData } = useContext(AuthContext);
|
||||||
const [totalRowsCount, setTotalRowsCount] = useState(0);
|
const [totalRowsCount, setTotalRowsCount] = useState(0);
|
||||||
|
|
||||||
|
const { setTabId } = useContext(TabsContext);
|
||||||
|
|
||||||
|
// set null id
|
||||||
|
useEffect(() => {
|
||||||
|
setTabId("");
|
||||||
|
}, []);
|
||||||
|
|
||||||
const userList = useRef([]);
|
const userList = useRef([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -65,7 +77,9 @@ export default function AdminPage() {
|
||||||
|
|
||||||
function handleChangePagination(pageIndex: number, pageSize: number) {
|
function handleChangePagination(pageIndex: number, pageSize: number) {
|
||||||
setLoadingUsers(true);
|
setLoadingUsers(true);
|
||||||
getUsersPage(pageIndex, pageSize)
|
setPageSize(pageSize);
|
||||||
|
setPageIndex(pageIndex);
|
||||||
|
getUsersPage(pageSize * (pageIndex - 1), pageSize)
|
||||||
.then((users) => {
|
.then((users) => {
|
||||||
setTotalRowsCount(users["total_count"]);
|
setTotalRowsCount(users["total_count"]);
|
||||||
userList.current = users["users"];
|
userList.current = users["users"];
|
||||||
|
|
@ -78,7 +92,7 @@ export default function AdminPage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetFilter() {
|
function resetFilter() {
|
||||||
setPageIndex(0);
|
setPageIndex(1);
|
||||||
setPageSize(10);
|
setPageSize(10);
|
||||||
getUsers();
|
getUsers();
|
||||||
}
|
}
|
||||||
|
|
@ -168,270 +182,264 @@ export default function AdminPage() {
|
||||||
function handleNewUser(user: UserInputType) {
|
function handleNewUser(user: UserInputType) {
|
||||||
addUser(user)
|
addUser(user)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
resetFilter();
|
updateUser(res["id"], {
|
||||||
setSuccessData({
|
is_active: user.is_active,
|
||||||
title: "Success! New user added!",
|
is_superuser: user.is_superuser,
|
||||||
});
|
}).then((res) => {
|
||||||
|
resetFilter();
|
||||||
|
setSuccessData({
|
||||||
|
title: "Success! New user added!",
|
||||||
|
});
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
setErrorData({
|
setErrorData({
|
||||||
title: "Error on add new user",
|
title: "Error when adding new user",
|
||||||
list: [error["response"]["data"]["detail"]],
|
list: [error.response.data.detail],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col">
|
<Header />
|
||||||
<Header />
|
{userData && (
|
||||||
{userData && (
|
<div className="admin-page-panel flex h-full flex-col pb-8">
|
||||||
<div className="main-page-panel">
|
<div className="main-page-nav-arrangement">
|
||||||
<div className="m-auto flex h-full flex-row justify-center">
|
<span className="main-page-nav-title">
|
||||||
<div className="basis-5/6">
|
<IconComponent name="Shield" className="w-6" />
|
||||||
<div className="m-auto flex h-full flex-col space-y-8 p-8 ">
|
{ADMIN_HEADER_TITLE}
|
||||||
<div className="flex items-center justify-between space-y-2">
|
</span>
|
||||||
<div>
|
</div>
|
||||||
<h2 className="text-2xl font-bold tracking-tight">
|
<span className="admin-page-description-text">
|
||||||
Welcome back!
|
{ADMIN_HEADER_DESCRIPTION}
|
||||||
</h2>
|
</span>
|
||||||
<p className="text-muted-foreground">
|
<div className="flex w-full justify-between px-4">
|
||||||
Navigate through this section to efficiently oversee all
|
<div className="flex w-96 items-center gap-4">
|
||||||
application users. From here, you can seamlessly manage
|
<Input
|
||||||
user accounts.
|
placeholder="Search Username"
|
||||||
</p>
|
value={inputValue}
|
||||||
</div>
|
onChange={(e) => handleFilterUsers(e.target.value)}
|
||||||
<div className="flex items-center space-x-2"></div>
|
/>
|
||||||
</div>
|
{inputValue.length > 0 ? (
|
||||||
|
<div
|
||||||
{userList.current.length === 0 && !loadingUsers && (
|
className="cursor-pointer"
|
||||||
<>
|
onClick={() => {
|
||||||
<div className="flex items-center justify-between">
|
setInputValue("");
|
||||||
<h2>There's no users registered :)</h2>
|
setFilterUserList(userList.current);
|
||||||
</div>
|
}}
|
||||||
</>
|
>
|
||||||
)}
|
<IconComponent name="X" className="w-6 text-foreground" />
|
||||||
<>
|
|
||||||
<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={() => {
|
|
||||||
setInputValue("");
|
|
||||||
setFilterUserList(userList.current);
|
|
||||||
}}
|
|
||||||
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>
|
|
||||||
{loadingUsers && (
|
|
||||||
<div>
|
|
||||||
<strong>Loading...</strong>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div
|
|
||||||
className={
|
|
||||||
"max-h-[26rem] min-h-[26rem] overflow-scroll overflow-x-hidden rounded-md border-2 bg-muted custom-scroll" +
|
|
||||||
(loadingUsers ? " border-0" : "")
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Table className={"table-fixed bg-muted outline-1"}>
|
|
||||||
<TableHeader
|
|
||||||
className={
|
|
||||||
loadingUsers
|
|
||||||
? "hidden"
|
|
||||||
: "table-fixed bg-muted outline-1"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead className="h-10">Id</TableHead>
|
|
||||||
<TableHead className="h-10">Username</TableHead>
|
|
||||||
<TableHead className="h-10">Active</TableHead>
|
|
||||||
<TableHead className="h-10">Superuser</TableHead>
|
|
||||||
<TableHead className="h-10">Created At</TableHead>
|
|
||||||
<TableHead className="h-10">Updated At</TableHead>
|
|
||||||
<TableHead className="h-10 w-[100px] text-right"></TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
{!loadingUsers && (
|
|
||||||
<TableBody>
|
|
||||||
{filterUserList.map(
|
|
||||||
(user: UserInputType, index) => (
|
|
||||||
<TableRow key={index}>
|
|
||||||
<TableCell className="truncate py-2 font-medium">
|
|
||||||
<ShadTooltip content={user.id}>
|
|
||||||
<span className="cursor-default">
|
|
||||||
{user.id}
|
|
||||||
</span>
|
|
||||||
</ShadTooltip>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="truncate py-2">
|
|
||||||
<ShadTooltip content={user.username}>
|
|
||||||
<span className="cursor-default">
|
|
||||||
{user.username}
|
|
||||||
</span>
|
|
||||||
</ShadTooltip>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="relative left-5 truncate py-2 text-align-last-left">
|
|
||||||
<ConfirmationModal
|
|
||||||
title="Edit"
|
|
||||||
titleHeader={`${user.username}`}
|
|
||||||
modalContentTitle="Attention!"
|
|
||||||
modalContent="Are you completely confident about the changes you are making to this user?"
|
|
||||||
cancelText="Cancel"
|
|
||||||
confirmationText="Confirm"
|
|
||||||
icon={"UserCog2"}
|
|
||||||
data={user}
|
|
||||||
index={index}
|
|
||||||
onConfirm={(index, user) => {
|
|
||||||
handleDisableUser(
|
|
||||||
user.is_active,
|
|
||||||
user.id,
|
|
||||||
user
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Checkbox
|
|
||||||
id="is_active"
|
|
||||||
checked={user.is_active}
|
|
||||||
/>
|
|
||||||
</ConfirmationModal>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="relative left-5 truncate py-2 text-align-last-left">
|
|
||||||
<ConfirmationModal
|
|
||||||
title="Edit"
|
|
||||||
titleHeader={`${user.username}`}
|
|
||||||
modalContentTitle="Attention!"
|
|
||||||
modalContent="Are you completely confident about the changes you are making to this user?"
|
|
||||||
cancelText="Cancel"
|
|
||||||
confirmationText="Confirm"
|
|
||||||
icon={"UserCog2"}
|
|
||||||
data={user}
|
|
||||||
index={index}
|
|
||||||
onConfirm={(index, user) => {
|
|
||||||
handleSuperUserEdit(
|
|
||||||
user.is_superuser,
|
|
||||||
user.id,
|
|
||||||
user
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Checkbox
|
|
||||||
id="is_superuser"
|
|
||||||
checked={user.is_superuser}
|
|
||||||
/>
|
|
||||||
</ConfirmationModal>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="truncate py-2 ">
|
|
||||||
{
|
|
||||||
new Date(user.create_at!)
|
|
||||||
.toISOString()
|
|
||||||
.split("T")[0]
|
|
||||||
}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="truncate py-2">
|
|
||||||
{
|
|
||||||
new Date(user.updated_at!)
|
|
||||||
.toISOString()
|
|
||||||
.split("T")[0]
|
|
||||||
}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="flex w-[100px] py-2 text-right">
|
|
||||||
<div className="flex">
|
|
||||||
<UserManagementModal
|
|
||||||
title="Edit"
|
|
||||||
titleHeader={`${user.id}`}
|
|
||||||
cancelText="Cancel"
|
|
||||||
confirmationText="Save"
|
|
||||||
icon={"UserPlus2"}
|
|
||||||
data={user}
|
|
||||||
index={index}
|
|
||||||
onConfirm={(index, editUser) => {
|
|
||||||
handleEditUser(user.id, editUser);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<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(user);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<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={totalRowsCount}
|
|
||||||
paginate={(pageIndex, pageSize) => {
|
|
||||||
handleChangePagination(pageSize, pageIndex);
|
|
||||||
}}
|
|
||||||
></PaginatorComponent>
|
|
||||||
</>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
) : (
|
||||||
|
<div>
|
||||||
|
<IconComponent
|
||||||
|
name="Search"
|
||||||
|
className="w-6 text-foreground"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<UserManagementModal
|
||||||
|
title="New User"
|
||||||
|
titleHeader={"Add a new user"}
|
||||||
|
cancelText="Cancel"
|
||||||
|
confirmationText="Save"
|
||||||
|
icon={"UserPlus2"}
|
||||||
|
onConfirm={(index, user) => {
|
||||||
|
handleNewUser(user);
|
||||||
|
}}
|
||||||
|
asChild
|
||||||
|
>
|
||||||
|
<Button variant="primary">New User</Button>
|
||||||
|
</UserManagementModal>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
{loadingUsers ? (
|
||||||
</div>
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
|
<LoadingComponent remSize={12} />
|
||||||
|
</div>
|
||||||
|
) : userList.current.length === 0 ? (
|
||||||
|
<>
|
||||||
|
<div className="m-4 flex items-center justify-between text-sm">
|
||||||
|
No users registered.
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
"m-4 h-full overflow-x-hidden overflow-y-scroll rounded-md border-2 bg-background custom-scroll" +
|
||||||
|
(loadingUsers ? " border-0" : "")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Table className={"table-fixed outline-1 "}>
|
||||||
|
<TableHeader
|
||||||
|
className={
|
||||||
|
loadingUsers ? "hidden" : "table-fixed bg-muted outline-1"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead className="h-10">Id</TableHead>
|
||||||
|
<TableHead className="h-10">Username</TableHead>
|
||||||
|
<TableHead className="h-10">Active</TableHead>
|
||||||
|
<TableHead className="h-10">Superuser</TableHead>
|
||||||
|
<TableHead className="h-10">Created At</TableHead>
|
||||||
|
<TableHead className="h-10">Updated At</TableHead>
|
||||||
|
<TableHead className="h-10 w-[100px] text-right"></TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
{!loadingUsers && (
|
||||||
|
<TableBody>
|
||||||
|
{filterUserList.map((user: UserInputType, index) => (
|
||||||
|
<TableRow key={index}>
|
||||||
|
<TableCell className="truncate py-2 font-medium">
|
||||||
|
<ShadTooltip content={user.id}>
|
||||||
|
<span className="cursor-default">{user.id}</span>
|
||||||
|
</ShadTooltip>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="truncate py-2">
|
||||||
|
<ShadTooltip content={user.username}>
|
||||||
|
<span className="cursor-default">
|
||||||
|
{user.username}
|
||||||
|
</span>
|
||||||
|
</ShadTooltip>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="relative left-1 truncate py-2 text-align-last-left">
|
||||||
|
<ConfirmationModal
|
||||||
|
asChild
|
||||||
|
title="Edit"
|
||||||
|
titleHeader={`${user.username}`}
|
||||||
|
modalContentTitle="Attention!"
|
||||||
|
modalContent="Are you completely confident about the changes you are making to this user?"
|
||||||
|
cancelText="Cancel"
|
||||||
|
confirmationText="Confirm"
|
||||||
|
icon={"UserCog2"}
|
||||||
|
data={user}
|
||||||
|
index={index}
|
||||||
|
onConfirm={(index, user) => {
|
||||||
|
handleDisableUser(
|
||||||
|
user.is_active,
|
||||||
|
user.id,
|
||||||
|
user
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex w-fit">
|
||||||
|
<Checkbox
|
||||||
|
id="is_active"
|
||||||
|
checked={user.is_active}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ConfirmationModal>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="relative left-1 truncate py-2 text-align-last-left">
|
||||||
|
<ConfirmationModal
|
||||||
|
asChild
|
||||||
|
title="Edit"
|
||||||
|
titleHeader={`${user.username}`}
|
||||||
|
modalContentTitle="Attention!"
|
||||||
|
modalContent="Are you completely confident about the changes you are making to this user?"
|
||||||
|
cancelText="Cancel"
|
||||||
|
confirmationText="Confirm"
|
||||||
|
icon={"UserCog2"}
|
||||||
|
data={user}
|
||||||
|
index={index}
|
||||||
|
onConfirm={(index, user) => {
|
||||||
|
handleSuperUserEdit(
|
||||||
|
user.is_superuser,
|
||||||
|
user.id,
|
||||||
|
user
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex w-fit">
|
||||||
|
<Checkbox
|
||||||
|
id="is_superuser"
|
||||||
|
checked={user.is_superuser}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ConfirmationModal>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="truncate py-2 ">
|
||||||
|
{
|
||||||
|
new Date(user.create_at!)
|
||||||
|
.toISOString()
|
||||||
|
.split("T")[0]
|
||||||
|
}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="truncate py-2">
|
||||||
|
{
|
||||||
|
new Date(user.updated_at!)
|
||||||
|
.toISOString()
|
||||||
|
.split("T")[0]
|
||||||
|
}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="flex w-[100px] py-2 text-right">
|
||||||
|
<div className="flex">
|
||||||
|
<UserManagementModal
|
||||||
|
title="Edit"
|
||||||
|
titleHeader={`${user.id}`}
|
||||||
|
cancelText="Cancel"
|
||||||
|
confirmationText="Save"
|
||||||
|
icon={"UserPlus2"}
|
||||||
|
data={user}
|
||||||
|
index={index}
|
||||||
|
onConfirm={(index, editUser) => {
|
||||||
|
handleEditUser(user.id, editUser);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<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(user);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<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={totalRowsCount}
|
||||||
|
paginate={(pageSize, pageIndex) => {
|
||||||
|
handleChangePagination(pageIndex, pageSize);
|
||||||
|
}}
|
||||||
|
></PaginatorComponent>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import { useNavigate } from "react-router-dom";
|
||||||
import { CardComponent } from "../../components/cardComponent";
|
import { CardComponent } from "../../components/cardComponent";
|
||||||
import IconComponent from "../../components/genericIconComponent";
|
import IconComponent from "../../components/genericIconComponent";
|
||||||
import Header from "../../components/headerComponent";
|
import Header from "../../components/headerComponent";
|
||||||
|
import { SkeletonCardComponent } from "../../components/skeletonCardComponent";
|
||||||
import { getExamples } from "../../controllers/API";
|
import { getExamples } from "../../controllers/API";
|
||||||
import { FlowType } from "../../types/flow";
|
import { FlowType } from "../../types/flow";
|
||||||
export default function CommunityPage(): JSX.Element {
|
export default function CommunityPage(): JSX.Element {
|
||||||
|
|
@ -74,7 +75,14 @@ export default function CommunityPage(): JSX.Element {
|
||||||
new and powerful features.
|
new and powerful features.
|
||||||
</span>
|
</span>
|
||||||
<div className="community-pages-flows-panel">
|
<div className="community-pages-flows-panel">
|
||||||
{!loadingExamples &&
|
{loadingExamples ? (
|
||||||
|
<>
|
||||||
|
<SkeletonCardComponent />
|
||||||
|
<SkeletonCardComponent />
|
||||||
|
<SkeletonCardComponent />
|
||||||
|
<SkeletonCardComponent />
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
examples.map((flow, idx) => (
|
examples.map((flow, idx) => (
|
||||||
<CardComponent
|
<CardComponent
|
||||||
key={idx}
|
key={idx}
|
||||||
|
|
@ -99,7 +107,8 @@ export default function CommunityPage(): JSX.Element {
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
))}
|
))
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -298,7 +298,14 @@ export default function Page({
|
||||||
setNodes((nds) => nds.concat(newNode));
|
setNodes((nds) => nds.concat(newNode));
|
||||||
} else if (event.dataTransfer.types.some((types) => types === "Files")) {
|
} else if (event.dataTransfer.types.some((types) => types === "Files")) {
|
||||||
takeSnapshot();
|
takeSnapshot();
|
||||||
uploadFlow(false, event.dataTransfer.files.item(0)!);
|
if (event.dataTransfer.files.item(0)!.type === "application/json") {
|
||||||
|
uploadFlow(true, event.dataTransfer.files.item(0)!);
|
||||||
|
} else {
|
||||||
|
setErrorData({
|
||||||
|
title: "Invalid file type",
|
||||||
|
list: ["Please upload a JSON file"],
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// Specify dependencies for useCallback
|
// Specify dependencies for useCallback
|
||||||
|
|
|
||||||
|
|
@ -99,16 +99,20 @@ export default function ExtraSidebar(): JSX.Element {
|
||||||
<ShadTooltip content={"Code"} side="top">
|
<ShadTooltip content={"Code"} side="top">
|
||||||
<div className="side-bar-button">
|
<div className="side-bar-button">
|
||||||
{flow && flow.data && (
|
{flow && flow.data && (
|
||||||
<ApiModal flow={flow} disable={!isBuilt}>
|
<ApiModal flow={flow}>
|
||||||
<div className={classNames("extra-side-bar-buttons")}>
|
<button
|
||||||
<IconComponent
|
className={"w-full " + (!isBuilt ? "button-disable" : "")}
|
||||||
name="Code2"
|
>
|
||||||
className={
|
<div className={classNames("extra-side-bar-buttons")}>
|
||||||
"side-bar-button-size" +
|
<IconComponent
|
||||||
(isBuilt ? " " : " extra-side-bar-save-disable")
|
name="Code2"
|
||||||
}
|
className={
|
||||||
/>
|
"side-bar-button-size" +
|
||||||
</div>
|
(isBuilt ? " " : " extra-side-bar-save-disable")
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
</ApiModal>
|
</ApiModal>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useContext, useEffect } from "react";
|
import { useContext, useEffect, useState } from "react";
|
||||||
import { Link, useNavigate } from "react-router-dom";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
import DropdownButton from "../../components/DropdownButtonComponent";
|
import DropdownButton from "../../components/DropdownButtonComponent";
|
||||||
import { CardComponent } from "../../components/cardComponent";
|
import { CardComponent } from "../../components/cardComponent";
|
||||||
|
|
@ -7,6 +7,7 @@ import Header from "../../components/headerComponent";
|
||||||
import { SkeletonCardComponent } from "../../components/skeletonCardComponent";
|
import { SkeletonCardComponent } from "../../components/skeletonCardComponent";
|
||||||
import { Button } from "../../components/ui/button";
|
import { Button } from "../../components/ui/button";
|
||||||
import { USER_PROJECTS_HEADER } from "../../constants/constants";
|
import { USER_PROJECTS_HEADER } from "../../constants/constants";
|
||||||
|
import { alertContext } from "../../contexts/alertContext";
|
||||||
import { TabsContext } from "../../contexts/tabsContext";
|
import { TabsContext } from "../../contexts/tabsContext";
|
||||||
export default function HomePage(): JSX.Element {
|
export default function HomePage(): JSX.Element {
|
||||||
const {
|
const {
|
||||||
|
|
@ -19,6 +20,7 @@ export default function HomePage(): JSX.Element {
|
||||||
uploadFlow,
|
uploadFlow,
|
||||||
isLoading,
|
isLoading,
|
||||||
} = useContext(TabsContext);
|
} = useContext(TabsContext);
|
||||||
|
const { setErrorData } = useContext(alertContext);
|
||||||
const dropdownOptions = [
|
const dropdownOptions = [
|
||||||
{
|
{
|
||||||
name: "Import from JSON",
|
name: "Import from JSON",
|
||||||
|
|
@ -35,9 +37,36 @@ export default function HomePage(): JSX.Element {
|
||||||
}, []);
|
}, []);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
useEffect(() => {
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
console.log(isLoading);
|
|
||||||
}, [isLoading]);
|
const dragOver = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsDragging(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const dragEnter = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsDragging(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const dragLeave = () => {
|
||||||
|
setIsDragging(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const fileDrop = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsDragging(false);
|
||||||
|
if (e.dataTransfer.types.some((types) => types === "Files")) {
|
||||||
|
if (e.dataTransfer.files.item(0).type === "application/json") {
|
||||||
|
uploadFlow(true, e.dataTransfer.files.item(0)!);
|
||||||
|
} else {
|
||||||
|
setErrorData({
|
||||||
|
title: "Invalid file type",
|
||||||
|
list: ["Please upload a JSON file"],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Personal flows display
|
// Personal flows display
|
||||||
return (
|
return (
|
||||||
|
|
@ -82,40 +111,63 @@ export default function HomePage(): JSX.Element {
|
||||||
<span className="main-page-description-text">
|
<span className="main-page-description-text">
|
||||||
Manage your personal projects. Download or upload your collection.
|
Manage your personal projects. Download or upload your collection.
|
||||||
</span>
|
</span>
|
||||||
<div className="main-page-flows-display">
|
<div
|
||||||
{isLoading && flows.length == 0 ? (
|
onDragOver={dragOver}
|
||||||
|
onDragEnter={dragEnter}
|
||||||
|
onDragLeave={dragLeave}
|
||||||
|
onDrop={fileDrop}
|
||||||
|
className={
|
||||||
|
"h-full w-full " +
|
||||||
|
(isDragging
|
||||||
|
? "mb-24 flex flex-col items-center justify-center gap-4 text-2xl font-light"
|
||||||
|
: "")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{isDragging ? (
|
||||||
<>
|
<>
|
||||||
<SkeletonCardComponent />
|
<IconComponent
|
||||||
<SkeletonCardComponent />
|
name="ArrowUpToLine"
|
||||||
<SkeletonCardComponent />
|
className="h-12 w-12 stroke-1"
|
||||||
<SkeletonCardComponent />
|
/>
|
||||||
|
Drop your flow here
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
flows.map((flow, idx) => (
|
<div className="main-page-flows-display">
|
||||||
<CardComponent
|
{isLoading && flows.length == 0 ? (
|
||||||
key={idx}
|
<>
|
||||||
flow={flow}
|
<SkeletonCardComponent />
|
||||||
id={flow.id}
|
<SkeletonCardComponent />
|
||||||
button={
|
<SkeletonCardComponent />
|
||||||
<Link to={"/flow/" + flow.id}>
|
<SkeletonCardComponent />
|
||||||
<Button
|
</>
|
||||||
variant="outline"
|
) : (
|
||||||
size="sm"
|
flows.map((flow, idx) => (
|
||||||
className="whitespace-nowrap "
|
<CardComponent
|
||||||
>
|
key={idx}
|
||||||
<IconComponent
|
flow={flow}
|
||||||
name="ExternalLink"
|
id={flow.id}
|
||||||
className="main-page-nav-button"
|
button={
|
||||||
/>
|
<Link to={"/flow/" + flow.id}>
|
||||||
Edit Flow
|
<Button
|
||||||
</Button>
|
variant="outline"
|
||||||
</Link>
|
size="sm"
|
||||||
}
|
className="whitespace-nowrap "
|
||||||
onDelete={() => {
|
>
|
||||||
removeFlow(flow.id);
|
<IconComponent
|
||||||
}}
|
name="ExternalLink"
|
||||||
/>
|
className="main-page-nav-button"
|
||||||
))
|
/>
|
||||||
|
Edit Flow
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
}
|
||||||
|
onDelete={() => {
|
||||||
|
removeFlow(flow.id);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
173
src/frontend/src/pages/ProfileSettingsPage/index.tsx
Normal file
173
src/frontend/src/pages/ProfileSettingsPage/index.tsx
Normal file
|
|
@ -0,0 +1,173 @@
|
||||||
|
import * as Form from "@radix-ui/react-form";
|
||||||
|
import { cloneDeep } from "lodash";
|
||||||
|
import { useContext, useEffect, useState } from "react";
|
||||||
|
import IconComponent from "../../components/genericIconComponent";
|
||||||
|
import GradientChooserComponent from "../../components/gradientChooserComponent";
|
||||||
|
import Header from "../../components/headerComponent";
|
||||||
|
import InputComponent from "../../components/inputComponent";
|
||||||
|
import { Button } from "../../components/ui/button";
|
||||||
|
import { CONTROL_PATCH_USER_STATE } from "../../constants/constants";
|
||||||
|
import { alertContext } from "../../contexts/alertContext";
|
||||||
|
import { AuthContext } from "../../contexts/authContext";
|
||||||
|
import { TabsContext } from "../../contexts/tabsContext";
|
||||||
|
import { resetPassword, updateUser } from "../../controllers/API";
|
||||||
|
import {
|
||||||
|
inputHandlerEventType,
|
||||||
|
patchUserInputStateType,
|
||||||
|
} from "../../types/components";
|
||||||
|
import { gradients } from "../../utils/styleUtils";
|
||||||
|
export default function ProfileSettingsPage(): JSX.Element {
|
||||||
|
const { setTabId } = useContext(TabsContext);
|
||||||
|
|
||||||
|
const [inputState, setInputState] = useState<patchUserInputStateType>(
|
||||||
|
CONTROL_PATCH_USER_STATE
|
||||||
|
);
|
||||||
|
|
||||||
|
// set null id
|
||||||
|
useEffect(() => {
|
||||||
|
setTabId("");
|
||||||
|
}, []);
|
||||||
|
const { setErrorData, setSuccessData } = useContext(alertContext);
|
||||||
|
const { userData, setUserData } = useContext(AuthContext);
|
||||||
|
const { password, cnfPassword, gradient } = inputState;
|
||||||
|
|
||||||
|
async function handlePatchUser() {
|
||||||
|
if (password !== cnfPassword) {
|
||||||
|
setErrorData({
|
||||||
|
title: "Error changing password",
|
||||||
|
list: ["Passwords do not match"],
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (password !== "") await resetPassword(userData!.id, { password });
|
||||||
|
if (gradient !== "")
|
||||||
|
await updateUser(userData!.id, { profile_image: gradient });
|
||||||
|
if (gradient !== "") {
|
||||||
|
let newUserData = cloneDeep(userData);
|
||||||
|
newUserData!.profile_image = gradient;
|
||||||
|
|
||||||
|
setUserData(newUserData);
|
||||||
|
}
|
||||||
|
handleInput({ target: { name: "password", value: "" } });
|
||||||
|
handleInput({ target: { name: "cnfPassword", value: "" } });
|
||||||
|
setSuccessData({ title: "Changes saved successfully!" });
|
||||||
|
} catch (error) {
|
||||||
|
setErrorData({
|
||||||
|
title: "Error saving changes",
|
||||||
|
list: [(error as any).response.data.detail],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleInput({
|
||||||
|
target: { name, value },
|
||||||
|
}: inputHandlerEventType): void {
|
||||||
|
setInputState((prev) => ({ ...prev, [name]: value }));
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Header />
|
||||||
|
|
||||||
|
<div className="community-page-arrangement">
|
||||||
|
<div className="community-page-nav-arrangement">
|
||||||
|
<span className="community-page-nav-title">
|
||||||
|
<IconComponent name="User" className="w-6" />
|
||||||
|
Profile Settings
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span className="community-page-description-text">
|
||||||
|
Change your profile settings like your password and your profile
|
||||||
|
picture.
|
||||||
|
</span>
|
||||||
|
<Form.Root
|
||||||
|
onSubmit={(event) => {
|
||||||
|
handlePatchUser();
|
||||||
|
const data = Object.fromEntries(new FormData(event.currentTarget));
|
||||||
|
event.preventDefault();
|
||||||
|
}}
|
||||||
|
className="flex h-full flex-col px-6 pb-16"
|
||||||
|
>
|
||||||
|
<div className="flex h-full flex-col gap-4">
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<div className="mb-3 w-96">
|
||||||
|
<Form.Field name="password">
|
||||||
|
<Form.Label className="data-[invalid]:label-invalid">
|
||||||
|
Password{" "}
|
||||||
|
</Form.Label>
|
||||||
|
<InputComponent
|
||||||
|
onChange={(value) => {
|
||||||
|
handleInput({ target: { name: "password", value } });
|
||||||
|
}}
|
||||||
|
value={password}
|
||||||
|
isForm
|
||||||
|
password={true}
|
||||||
|
placeholder="Password"
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
<Form.Message match="valueMissing" className="field-invalid">
|
||||||
|
Please enter your password
|
||||||
|
</Form.Message>
|
||||||
|
</Form.Field>
|
||||||
|
</div>
|
||||||
|
<div className="mb-3 w-96">
|
||||||
|
<Form.Field name="cnfPassword">
|
||||||
|
<Form.Label className="data-[invalid]:label-invalid">
|
||||||
|
Confirm Password{" "}
|
||||||
|
</Form.Label>
|
||||||
|
|
||||||
|
<InputComponent
|
||||||
|
onChange={(value) => {
|
||||||
|
handleInput({ target: { name: "cnfPassword", value } });
|
||||||
|
}}
|
||||||
|
value={cnfPassword}
|
||||||
|
isForm
|
||||||
|
password={true}
|
||||||
|
placeholder="Confirm Password"
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Form.Message className="field-invalid" match="valueMissing">
|
||||||
|
Please confirm your password
|
||||||
|
</Form.Message>
|
||||||
|
</Form.Field>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Form.Field name="gradient">
|
||||||
|
<Form.Label className="data-[invalid]:label-invalid">
|
||||||
|
Profile Gradient{" "}
|
||||||
|
</Form.Label>
|
||||||
|
|
||||||
|
<div className="mt-4 w-[1010px]">
|
||||||
|
<GradientChooserComponent
|
||||||
|
value={
|
||||||
|
gradient == ""
|
||||||
|
? userData!.profile_image ??
|
||||||
|
gradients[
|
||||||
|
parseInt(userData!.id ?? "", 30) % gradients.length
|
||||||
|
]
|
||||||
|
: gradient
|
||||||
|
}
|
||||||
|
onChange={(value) => {
|
||||||
|
handleInput({ target: { name: "gradient", value } });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Form.Field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex w-full justify-end">
|
||||||
|
<div className="w-32">
|
||||||
|
<Form.Submit asChild>
|
||||||
|
<Button className="mr-3 mt-6 w-full" type="submit">
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</Form.Submit>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Form.Root>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -54,7 +54,7 @@ export default function LoginPage(): JSX.Element {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
getLoggedUser()
|
getLoggedUser()
|
||||||
.then((user) => {
|
.then((user) => {
|
||||||
const isSuperUser = user.is_superuser;
|
const isSuperUser = user!.is_superuser;
|
||||||
setIsAdmin(isSuperUser);
|
setIsAdmin(isSuperUser);
|
||||||
setUserData(user);
|
setUserData(user);
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import ApiKeysPage from "./pages/ApiKeysPage";
|
||||||
import CommunityPage from "./pages/CommunityPage";
|
import CommunityPage from "./pages/CommunityPage";
|
||||||
import FlowPage from "./pages/FlowPage";
|
import FlowPage from "./pages/FlowPage";
|
||||||
import HomePage from "./pages/MainPage";
|
import HomePage from "./pages/MainPage";
|
||||||
|
import ProfileSettingsPage from "./pages/ProfileSettingsPage";
|
||||||
import ViewPage from "./pages/ViewPage";
|
import ViewPage from "./pages/ViewPage";
|
||||||
import DeleteAccountPage from "./pages/deleteAccountPage";
|
import DeleteAccountPage from "./pages/deleteAccountPage";
|
||||||
import LoginPage from "./pages/loginPage";
|
import LoginPage from "./pages/loginPage";
|
||||||
|
|
@ -95,6 +96,14 @@ const Router = () => {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Route path="/account">
|
<Route path="/account">
|
||||||
|
<Route
|
||||||
|
path="settings"
|
||||||
|
element={
|
||||||
|
<ProtectedRoute>
|
||||||
|
<ProfileSettingsPage />
|
||||||
|
</ProtectedRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="delete"
|
path="delete"
|
||||||
element={
|
element={
|
||||||
|
|
|
||||||
|
|
@ -212,6 +212,10 @@
|
||||||
@apply flex-max-width h-full flex-col overflow-auto bg-muted px-16;
|
@apply flex-max-width h-full flex-col overflow-auto bg-muted px-16;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.admin-page-panel {
|
||||||
|
@apply flex-max-width h-full flex-col overflow-auto bg-muted px-16;
|
||||||
|
}
|
||||||
|
|
||||||
.main-page-nav-arrangement {
|
.main-page-nav-arrangement {
|
||||||
@apply flex-max-width justify-between px-6 py-12 pb-2;
|
@apply flex-max-width justify-between px-6 py-12 pb-2;
|
||||||
}
|
}
|
||||||
|
|
@ -228,6 +232,10 @@
|
||||||
@apply flex w-[60%] px-6 pb-14 text-muted-foreground;
|
@apply flex w-[60%] px-6 pb-14 text-muted-foreground;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.admin-page-description-text {
|
||||||
|
@apply flex w-[80%] px-6 pb-8 text-muted-foreground;
|
||||||
|
}
|
||||||
|
|
||||||
.main-page-flows-display {
|
.main-page-flows-display {
|
||||||
@apply grid w-full gap-4 p-4 md:grid-cols-2 lg:grid-cols-4;
|
@apply grid w-full gap-4 p-4 md:grid-cols-2 lg:grid-cols-4;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -78,11 +78,25 @@ export type LoginAuthType = {
|
||||||
token_type?: string;
|
token_type?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type changeUser = {
|
||||||
|
username?: string;
|
||||||
|
is_active?: boolean;
|
||||||
|
is_superuser?: boolean;
|
||||||
|
password?: string;
|
||||||
|
profile_image?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type resetPasswordType = {
|
||||||
|
password?: string;
|
||||||
|
profile_image?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type Users = {
|
export type Users = {
|
||||||
id: string;
|
id: string;
|
||||||
username: string;
|
username: string;
|
||||||
is_active: boolean;
|
is_active: boolean;
|
||||||
is_superuser: boolean;
|
is_superuser: boolean;
|
||||||
|
profile_image: string;
|
||||||
create_at: Date;
|
create_at: Date;
|
||||||
updated_at: Date;
|
updated_at: Date;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -233,6 +233,7 @@ export type PaginatorComponentType = {
|
||||||
export type ConfirmationModalType = {
|
export type ConfirmationModalType = {
|
||||||
title: string;
|
title: string;
|
||||||
titleHeader: string;
|
titleHeader: string;
|
||||||
|
asChild?: boolean;
|
||||||
modalContent: string;
|
modalContent: string;
|
||||||
modalContentTitle: string;
|
modalContentTitle: string;
|
||||||
cancelText: string;
|
cancelText: string;
|
||||||
|
|
@ -253,6 +254,7 @@ export type UserManagementType = {
|
||||||
icon: string;
|
icon: string;
|
||||||
data?: any;
|
data?: any;
|
||||||
index?: number;
|
index?: number;
|
||||||
|
asChild?: boolean;
|
||||||
onConfirm: (index, data) => void;
|
onConfirm: (index, data) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -261,6 +263,12 @@ export type loginInputStateType = {
|
||||||
password: string;
|
password: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type patchUserInputStateType = {
|
||||||
|
password: string;
|
||||||
|
cnfPassword: string;
|
||||||
|
gradient: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type UserInputType = {
|
export type UserInputType = {
|
||||||
username: string;
|
username: string;
|
||||||
password: string;
|
password: string;
|
||||||
|
|
|
||||||
|
|
@ -255,7 +255,9 @@ export function addVersionToDuplicates(flow: FlowType, flows: FlowType[]) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function handleKeyDown(
|
export function handleKeyDown(
|
||||||
e: React.KeyboardEvent<HTMLInputElement>,
|
e:
|
||||||
|
| React.KeyboardEvent<HTMLInputElement>
|
||||||
|
| React.KeyboardEvent<HTMLTextAreaElement>,
|
||||||
inputValue: string | string[] | null,
|
inputValue: string | string[] | null,
|
||||||
block: string
|
block: string
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import {
|
import {
|
||||||
|
ArrowUpToLine,
|
||||||
Bell,
|
Bell,
|
||||||
Check,
|
Check,
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
|
|
@ -58,6 +59,7 @@ import {
|
||||||
Scissors,
|
Scissors,
|
||||||
Search,
|
Search,
|
||||||
Settings2,
|
Settings2,
|
||||||
|
Shield,
|
||||||
Sparkles,
|
Sparkles,
|
||||||
SunIcon,
|
SunIcon,
|
||||||
TerminalSquare,
|
TerminalSquare,
|
||||||
|
|
@ -65,6 +67,7 @@ import {
|
||||||
Undo,
|
Undo,
|
||||||
Unplug,
|
Unplug,
|
||||||
Upload,
|
Upload,
|
||||||
|
User,
|
||||||
UserCog2,
|
UserCog2,
|
||||||
UserMinus2,
|
UserMinus2,
|
||||||
UserPlus2,
|
UserPlus2,
|
||||||
|
|
@ -86,7 +89,7 @@ import { EvernoteIcon } from "../icons/Evernote";
|
||||||
import { FBIcon } from "../icons/FacebookMessenger";
|
import { FBIcon } from "../icons/FacebookMessenger";
|
||||||
import { GitBookIcon } from "../icons/GitBook";
|
import { GitBookIcon } from "../icons/GitBook";
|
||||||
import { GoogleIcon } from "../icons/Google";
|
import { GoogleIcon } from "../icons/Google";
|
||||||
import GradientSparkles from "../icons/GradientSparkles";
|
import {GradientSparkles} from "../icons/GradientSparkles";
|
||||||
import { HuggingFaceIcon } from "../icons/HuggingFace";
|
import { HuggingFaceIcon } from "../icons/HuggingFace";
|
||||||
import { IFixIcon } from "../icons/IFixIt";
|
import { IFixIcon } from "../icons/IFixIt";
|
||||||
import { MetaIcon } from "../icons/Meta";
|
import { MetaIcon } from "../icons/Meta";
|
||||||
|
|
@ -186,6 +189,7 @@ export const nodeNames: { [char: string]: string } = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const nodeIconsLucide: iconsType = {
|
export const nodeIconsLucide: iconsType = {
|
||||||
|
ArrowUpToLine: ArrowUpToLine,
|
||||||
Chroma: ChromaIcon,
|
Chroma: ChromaIcon,
|
||||||
AirbyteJSONLoader: AirbyteIcon,
|
AirbyteJSONLoader: AirbyteIcon,
|
||||||
Anthropic: AnthropicIcon,
|
Anthropic: AnthropicIcon,
|
||||||
|
|
@ -220,6 +224,7 @@ export const nodeIconsLucide: iconsType = {
|
||||||
ChatVertexAI: VertexAIIcon,
|
ChatVertexAI: VertexAIIcon,
|
||||||
VertexAIEmbeddings: VertexAIIcon,
|
VertexAIEmbeddings: VertexAIIcon,
|
||||||
agents: Rocket,
|
agents: Rocket,
|
||||||
|
User,
|
||||||
WikipediaAPIWrapper: SvgWikipedia,
|
WikipediaAPIWrapper: SvgWikipedia,
|
||||||
chains: Link,
|
chains: Link,
|
||||||
memories: Cpu,
|
memories: Cpu,
|
||||||
|
|
@ -260,6 +265,7 @@ export const nodeIconsLucide: iconsType = {
|
||||||
Bell,
|
Bell,
|
||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
|
Shield,
|
||||||
Plus,
|
Plus,
|
||||||
Redo,
|
Redo,
|
||||||
Settings2,
|
Settings2,
|
||||||
|
|
|
||||||
|
|
@ -166,7 +166,8 @@ def test_user(client):
|
||||||
username="testuser",
|
username="testuser",
|
||||||
password="testpassword",
|
password="testpassword",
|
||||||
)
|
)
|
||||||
response = client.post("/api/v1/user", json=user_data.dict())
|
response = client.post("/api/v1/users", json=user_data.dict())
|
||||||
|
assert response.status_code == 201
|
||||||
return response.json()
|
return response.json()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -78,24 +78,27 @@ def test_deactivated_user_cannot_access(client, deactivated_user, logged_in_head
|
||||||
assert response.json()["detail"] == "The user doesn't have enough privileges"
|
assert response.json()["detail"] == "The user doesn't have enough privileges"
|
||||||
|
|
||||||
|
|
||||||
def test_data_consistency_after_update(client, active_user, logged_in_headers):
|
def test_data_consistency_after_update(
|
||||||
|
client, active_user, logged_in_headers, super_user_headers
|
||||||
|
):
|
||||||
user_id = active_user.id
|
user_id = active_user.id
|
||||||
update_data = UserUpdate(username="newname")
|
update_data = UserUpdate(is_active=False)
|
||||||
|
|
||||||
response = client.patch(
|
response = client.patch(
|
||||||
f"/api/v1/user/{user_id}", json=update_data.dict(), headers=logged_in_headers
|
f"/api/v1/users/{user_id}", json=update_data.dict(), headers=super_user_headers
|
||||||
)
|
)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200, response.json()
|
||||||
|
|
||||||
# Fetch the updated user from the database
|
# Fetch the updated user from the database
|
||||||
response = client.get("/api/v1/user", headers=logged_in_headers)
|
response = client.get("/api/v1/users/whoami", headers=logged_in_headers)
|
||||||
assert response.json()["username"] == "newname", response.json()
|
assert response.status_code == 401, response.json()
|
||||||
|
assert response.json()["detail"] == "Could not validate credentials"
|
||||||
|
|
||||||
|
|
||||||
def test_data_consistency_after_delete(client, test_user, super_user_headers):
|
def test_data_consistency_after_delete(client, test_user, super_user_headers):
|
||||||
user_id = test_user["id"]
|
user_id = test_user.get("id")
|
||||||
response = client.delete(f"/api/v1/user/{user_id}", headers=super_user_headers)
|
response = client.delete(f"/api/v1/users/{user_id}", headers=super_user_headers)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200, response.json()
|
||||||
|
|
||||||
# Attempt to fetch the deleted user from the database
|
# Attempt to fetch the deleted user from the database
|
||||||
response = client.get("/api/v1/users", headers=super_user_headers)
|
response = client.get("/api/v1/users", headers=super_user_headers)
|
||||||
|
|
@ -157,11 +160,37 @@ def test_patch_user(client, active_user, logged_in_headers):
|
||||||
)
|
)
|
||||||
|
|
||||||
response = client.patch(
|
response = client.patch(
|
||||||
f"/api/v1/user/{user_id}", json=update_data.dict(), headers=logged_in_headers
|
f"/api/v1/users/{user_id}", json=update_data.dict(), headers=logged_in_headers
|
||||||
|
)
|
||||||
|
assert response.status_code == 304, response.json()
|
||||||
|
update_data = UserUpdate(
|
||||||
|
profile_image="new_image",
|
||||||
|
)
|
||||||
|
|
||||||
|
response = client.patch(
|
||||||
|
f"/api/v1/users/{user_id}", json=update_data.dict(), headers=logged_in_headers
|
||||||
)
|
)
|
||||||
assert response.status_code == 200, response.json()
|
assert response.status_code == 200, response.json()
|
||||||
|
|
||||||
|
|
||||||
|
def test_patch_reset_password(client, active_user, logged_in_headers):
|
||||||
|
user_id = active_user.id
|
||||||
|
update_data = UserUpdate(
|
||||||
|
password="newpassword",
|
||||||
|
)
|
||||||
|
|
||||||
|
response = client.patch(
|
||||||
|
f"/api/v1/users/{user_id}/reset-password",
|
||||||
|
json=update_data.dict(),
|
||||||
|
headers=logged_in_headers,
|
||||||
|
)
|
||||||
|
assert response.status_code == 200, response.json()
|
||||||
|
# Now we need to test if the new password works
|
||||||
|
login_data = {"username": active_user.username, "password": "newpassword"}
|
||||||
|
response = client.post("/api/v1/login", data=login_data)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
def test_patch_user_wrong_id(client, active_user, logged_in_headers):
|
def test_patch_user_wrong_id(client, active_user, logged_in_headers):
|
||||||
user_id = "wrong_id"
|
user_id = "wrong_id"
|
||||||
update_data = UserUpdate(
|
update_data = UserUpdate(
|
||||||
|
|
@ -169,7 +198,7 @@ def test_patch_user_wrong_id(client, active_user, logged_in_headers):
|
||||||
)
|
)
|
||||||
|
|
||||||
response = client.patch(
|
response = client.patch(
|
||||||
f"/api/v1/user/{user_id}", json=update_data.dict(), headers=logged_in_headers
|
f"/api/v1/users/{user_id}", json=update_data.dict(), headers=logged_in_headers
|
||||||
)
|
)
|
||||||
assert response.status_code == 422, response.json()
|
assert response.status_code == 422, response.json()
|
||||||
assert response.json() == {
|
assert response.json() == {
|
||||||
|
|
@ -185,14 +214,14 @@ def test_patch_user_wrong_id(client, active_user, logged_in_headers):
|
||||||
|
|
||||||
def test_delete_user(client, test_user, super_user_headers):
|
def test_delete_user(client, test_user, super_user_headers):
|
||||||
user_id = test_user["id"]
|
user_id = test_user["id"]
|
||||||
response = client.delete(f"/api/v1/user/{user_id}", headers=super_user_headers)
|
response = client.delete(f"/api/v1/users/{user_id}", headers=super_user_headers)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert response.json() == {"detail": "User deleted"}
|
assert response.json() == {"detail": "User deleted"}
|
||||||
|
|
||||||
|
|
||||||
def test_delete_user_wrong_id(client, test_user, super_user_headers):
|
def test_delete_user_wrong_id(client, test_user, super_user_headers):
|
||||||
user_id = "wrong_id"
|
user_id = "wrong_id"
|
||||||
response = client.delete(f"/api/v1/user/{user_id}", headers=super_user_headers)
|
response = client.delete(f"/api/v1/users/{user_id}", headers=super_user_headers)
|
||||||
assert response.status_code == 422
|
assert response.status_code == 422
|
||||||
assert response.json() == {
|
assert response.json() == {
|
||||||
"detail": [
|
"detail": [
|
||||||
|
|
@ -207,13 +236,6 @@ def test_delete_user_wrong_id(client, test_user, super_user_headers):
|
||||||
|
|
||||||
def test_normal_user_cant_delete_user(client, test_user, logged_in_headers):
|
def test_normal_user_cant_delete_user(client, test_user, logged_in_headers):
|
||||||
user_id = test_user["id"]
|
user_id = test_user["id"]
|
||||||
response = client.delete(f"/api/v1/user/{user_id}", headers=logged_in_headers)
|
response = client.delete(f"/api/v1/users/{user_id}", headers=logged_in_headers)
|
||||||
assert response.status_code == 400
|
assert response.status_code == 400
|
||||||
assert response.json() == {"detail": "The user doesn't have enough privileges"}
|
assert response.json() == {"detail": "The user doesn't have enough privileges"}
|
||||||
|
|
||||||
|
|
||||||
# If you still want to test the superuser endpoint
|
|
||||||
def test_add_super_user_for_testing_purposes_delete_me_before_merge_into_dev(client):
|
|
||||||
response = client.post("/api/v1/super_user")
|
|
||||||
assert response.status_code == 200
|
|
||||||
assert response.json()["username"] == "superuser"
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue