Profile Page implementation and fix on Admin Page (#896)

This commit is contained in:
anovazzi1 2023-09-11 18:49:43 -03:00 • committed by GitHub
commit 374ae22563
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
36 changed files with 1343 additions and 844 deletions

View file

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

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -48,7 +48,7 @@ export default function LoginAdminPage() {
} }
function getUser() { function getUser() {
if (getAuthentication) { if (getAuthentication()) {
setTimeout(() => { setTimeout(() => {
getLoggedUser() getLoggedUser()
.then((user) => { .then((user) => {

View file

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

View file

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

View file

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

View file

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

View file

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

View 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>
</>
);
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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