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_user,
get_password_hash,
verify_password,
)
from langflow.services.database.models.user.crud import (
get_user_by_id,
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(
user: UserCreate,
session: Session = Depends(get_session),
@ -50,7 +52,7 @@ def add_user(
return new_user
@router.get("/user", response_model=UserRead)
@router.get("/whoami", response_model=UserRead)
def read_current_user(
current_user: User = Depends(get_current_active_user),
) -> User:
@ -60,7 +62,7 @@ def read_current_user(
return current_user
@router.get("/users", response_model=UsersResponse)
@router.get("/", response_model=UsersResponse)
def read_all_users(
skip: int = 0,
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(
user_id: UUID,
user: UserUpdate,
_: Session = Depends(get_current_active_user),
user_update: UserUpdate,
user: Session = Depends(get_current_active_user),
session: Session = Depends(get_session),
) -> User:
"""
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(
user_id: UUID,
current_user: User = Depends(get_current_active_superuser),

View file

@ -1,13 +1,12 @@
from datetime import datetime, timezone
from typing import Union
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.utils import get_session
from sqlalchemy.exc import IntegrityError
from sqlmodel import Session
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(
user_id: UUID, user: UserUpdate, db: Session = Depends(get_session)
user_db: User, user: UserUpdate, db: Session = Depends(get_session)
) -> User:
user_db = get_user_by_id(db, user_id)
if not user_db:
raise HTTPException(status_code=404, detail="User not found")
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:
raise HTTPException(status_code=409, detail="Username already exists")
# 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:
# raise HTTPException(status_code=409, detail="Username already exists")
user_data = user.dict(exclude_unset=True)
changed = False
for attr, value in user_data.items():
if hasattr(user_db, attr) and value is not None:
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)
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)):
user_data = UserUpdate(last_login_at=datetime.now(timezone.utc)) # type: ignore
return update_user(user_id, user_data, db)
user = get_user_by_id(db, user_id)
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)
username: str = Field(index=True, unique=True)
password: str = Field()
profile_image: Optional[str] = Field(default=None)
is_active: bool = Field(default=False)
is_superuser: bool = Field(default=False)
create_at: datetime = Field(default_factory=datetime.utcnow)
@ -32,6 +33,7 @@ class UserCreate(SQLModel):
class UserRead(SQLModel):
id: UUID = Field(default_factory=uuid4)
username: str = Field()
profile_image: Optional[str] = Field()
is_active: bool = Field()
is_superuser: bool = Field()
create_at: datetime = Field()
@ -40,7 +42,8 @@ class UserRead(SQLModel):
class UserUpdate(SQLModel):
username: Optional[str] = Field()
profile_image: Optional[str] = Field()
password: Optional[str] = Field()
is_active: Optional[bool] = Field()
is_superuser: Optional[bool] = 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({
pageSize = 10,
pageIndex = 0,
pageIndex = 1,
rowsCount = [10, 20, 50, 100],
totalRowsCount = 0,
paginate,
}: PaginatorComponentType) {
const [size, setPageSize] = useState(pageSize);
const [index, setPageIndex] = useState(pageIndex);
const [maxIndex, setMaxPageIndex] = useState(
Math.ceil(totalRowsCount / pageSize)
);
const [currentPage, setCurrentPage] = useState(1);
useEffect(() => {
setMaxPageIndex(Math.ceil(totalRowsCount / size));
@ -39,8 +37,9 @@ export default function PaginatorComponent({
onValueChange={(pageSize: string) => {
setPageSize(Number(pageSize));
setMaxPageIndex(Math.ceil(totalRowsCount / Number(pageSize)));
paginate(Number(pageSize), 0);
paginate(Number(pageSize), 1);
}}
value={pageSize.toString()}
>
<SelectTrigger className="w-[100px]">
<SelectValue placeholder="10" />
@ -55,30 +54,25 @@ export default function PaginatorComponent({
</Select>
</div>
<div className="flex w-[100px] items-center justify-center text-sm font-medium">
Page {currentPage} of {maxIndex}
Page {pageIndex} of {maxIndex}
</div>
<div className="flex items-center space-x-2">
<Button
disabled={index <= 0}
disabled={pageIndex <= 1}
variant="outline"
className="hidden h-8 w-8 p-0 lg:flex"
onClick={() => {
setPageIndex(0);
setCurrentPage(1);
paginate(size, 0);
paginate(size, 1);
}}
>
<span className="sr-only">Go to first page</span>
<IconComponent name="ChevronsLeft" className="h-4 w-4" />
</Button>
<Button
disabled={index <= 0}
disabled={pageIndex <= 1}
onClick={() => {
if (index > 0) {
const pgIndex = size - index;
setCurrentPage(currentPage - 1);
setPageIndex(pgIndex);
paginate(size, pgIndex);
if (pageIndex > 0) {
paginate(size, pageIndex - 1);
}
}}
variant="outline"
@ -88,12 +82,9 @@ export default function PaginatorComponent({
<IconComponent name="ChevronLeft" className="h-4 w-4" />
</Button>
<Button
disabled={currentPage === maxIndex}
disabled={pageIndex === maxIndex}
onClick={() => {
const pgIndex = size + index;
setPageIndex(pgIndex);
setCurrentPage(currentPage + 1);
paginate(size, pgIndex);
paginate(size, pageIndex + 1);
}}
variant="outline"
className="h-8 w-8 p-0"
@ -102,13 +93,11 @@ export default function PaginatorComponent({
<IconComponent name="ChevronRight" className="h-4 w-4" />
</Button>
<Button
disabled={currentPage === maxIndex}
disabled={pageIndex === maxIndex}
variant="outline"
className="hidden h-8 w-8 p-0 lg:flex"
onClick={() => {
setPageIndex(maxIndex - 1);
setCurrentPage(maxIndex);
paginate(size, size);
paginate(size, maxIndex);
}}
>
<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
className={
"h-7 w-7 rounded-full focus-visible:outline-0 " +
gradients[gradientIndex]
(userData?.profile_image ?? gradients[gradientIndex])
}
/>
</DropdownMenuTrigger>
@ -154,6 +154,12 @@ export default function Header(): JSX.Element {
Admin Page
</DropdownMenuItem>
)}
<DropdownMenuItem
className="cursor-pointer"
onClick={() => navigate("/account/settings")}
>
Profile Settings
</DropdownMenuItem>
<DropdownMenuItem
className="cursor-pointer"
onClick={() => {

View file

@ -4,10 +4,10 @@ export default function LoadingComponent({
remSize,
}: LoadingComponentProps): JSX.Element {
return (
<div role="status" className="m-auto w-min">
<div role="status" className="flex flex-col items-center justify-center">
<svg
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"
fill="none"
xmlns="http://www.w3.org/2000/svg"

View file

@ -499,6 +499,21 @@ export const NOUNS: string[] = [
*/
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.
* @constant
@ -519,6 +534,12 @@ export const CONTROL_INPUT_STATE = {
username: "",
};
export const CONTROL_PATCH_USER_STATE = {
password: "",
cnfPassword: "",
gradient: "",
};
export const CONTROL_LOGIN_STATE = {
username: "",
password: "",

View file

@ -63,7 +63,7 @@ export function AuthProvider({ children }): React.ReactElement {
.then((user) => {
setUserData(user);
setLoading(false);
const isSuperUser = user.is_superuser;
const isSuperUser = user!.is_superuser;
setIsAdmin(isSuperUser);
})
.catch((error) => {});

View file

@ -238,7 +238,6 @@ export function TabsProvider({ children }: { children: ReactNode }) {
function hardReset() {
newNodeId.current = uid();
setTabId("");
setFlows([]);
setIsLoading(true);
setId(uid());

View file

@ -6,6 +6,8 @@ import {
APIObjectType,
LoginType,
Users,
changeUser,
resetPasswordType,
sendAllProps,
} from "../../types/api/index";
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 {
const res = await api.get(`${BASE_URL_API}user`);
const res = await api.get(`${BASE_URL_API}users/whoami`);
if (res.status === 200) {
return res.data;
@ -413,14 +415,16 @@ export async function getLoggedUser(): Promise<Users> {
console.log("Error:", error);
throw error;
}
return null;
}
export async function addUser(user: UserInputType): Promise<Users> {
export async function addUser(user: UserInputType): Promise<Array<Users>> {
try {
const res = await api.post(`${BASE_URL_API}user`, user);
if (res.status === 200) {
return res.data;
const res = await api.post(`${BASE_URL_API}users/`, user);
if (res.status !== 201) {
throw new Error(res.data.detail);
}
return res.data;
} catch (error) {
console.log("Error:", error);
throw error;
@ -430,10 +434,10 @@ export async function addUser(user: UserInputType): Promise<Users> {
export async function getUsersPage(
skip: number,
limit: number
): Promise<[Users]> {
): Promise<Array<Users>> {
try {
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) {
return res.data;
@ -442,11 +446,12 @@ export async function getUsersPage(
console.log("Error:", error);
throw error;
}
return [];
}
export async function deleteUser(user_id: string) {
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) {
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 {
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) {
return res.data;
}

View file

@ -33,11 +33,9 @@ const ApiModal = forwardRef(
{
flow,
children,
disable,
}: {
flow: FlowType;
children: ReactNode;
disable: boolean;
},
ref
) => {
@ -201,8 +199,8 @@ const ApiModal = forwardRef(
}
return (
<BaseModal open={open} setOpen={setOpen} disable={disable}>
<BaseModal.Trigger>{children}</BaseModal.Trigger>
<BaseModal open={open} setOpen={setOpen}>
<BaseModal.Trigger asChild>{children}</BaseModal.Trigger>
<BaseModal.Header description={EXPORT_CODE_DIALOG}>
<span className="pr-2">Code</span>
<IconComponent

View file

@ -6,6 +6,7 @@ import BaseModal from "../baseModal";
export default function ConfirmationModal({
title,
asChild,
titleHeader,
modalContent,
modalContentTitle,
@ -22,7 +23,7 @@ export default function ConfirmationModal({
const [open, setOpen] = useState(false);
return (
<BaseModal size="x-small" open={open} setOpen={setOpen}>
<BaseModal.Trigger>{children}</BaseModal.Trigger>
<BaseModal.Trigger asChild={asChild}>{children}</BaseModal.Trigger>
<BaseModal.Header description={titleHeader}>
<span className="pr-2">{title}</span>
<Icon

View file

@ -23,6 +23,7 @@ export default function UserManagementModal({
data,
index,
onConfirm,
asChild,
}: UserManagementType) {
const Icon: any = nodeIconsLucide[icon];
const [pwdVisible, setPwdVisible] = useState(false);
@ -60,7 +61,7 @@ export default function UserManagementModal({
return (
<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}>
<span className="pr-2">{title}</span>
<Icon

View file

@ -14,13 +14,25 @@ import { modalHeaderType } from "../../types/components";
type ContentProps = { children: ReactNode };
type HeaderProps = { children: ReactNode; description: string };
type FooterProps = { children: ReactNode };
type TriggerProps = { children: ReactNode };
type TriggerProps = {
children: ReactNode;
asChild?: boolean;
disable?: boolean;
};
const Content: React.FC<ContentProps> = ({ children }) => {
return <div className="h-full w-full">{children}</div>;
};
const Trigger: React.FC<ContentProps> = ({ children }) => {
return <>{children}</>;
const Trigger: React.FC<TriggerProps> = ({ children, asChild, disable }) => {
return (
<DialogTrigger
className={asChild ? "" : "w-full"}
hidden={children ? false : true}
asChild={asChild}
>
{children}
</DialogTrigger>
);
};
const Header: React.FC<{ children: ReactNode; description: string | null }> = ({
@ -47,7 +59,6 @@ interface BaseModalProps {
];
open?: boolean;
setOpen?: (open: boolean) => void;
disable?: boolean;
size?:
| "x-small"
| "smaller"
@ -61,7 +72,6 @@ interface BaseModalProps {
function BaseModal({
open,
setOpen,
disable = false,
children,
size = "large",
}: BaseModalProps) {
@ -120,17 +130,12 @@ function BaseModal({
//UPDATE COLORS AND STYLE CLASSSES
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger
className={"w-full " + (disable ? "button-disable" : "")}
hidden={triggerChild ? false : true}
>
{triggerChild}
</DialogTrigger>
{triggerChild}
<DialogContent className={minWidth}>
<div className="truncate-doubleline word-break-break-word">
{headerChild}
</div>
<div className={`mt-2 flex flex-col ${height} w-full `}>
<div className={`mt-2 flex flex-col ${height!} w-full `}>
{ContentChild}
</div>
{ContentFooter && (

View file

@ -28,8 +28,7 @@ export default function CodeAreaModal({
const { dark } = useContext(darkContext);
const { reactFlowInstance } = useContext(typesContext);
const [height, setHeight] = useState<string | null>(null);
const { setErrorData, setSuccessData, isTweakPage } =
useContext(alertContext);
const { setErrorData, setSuccessData } = useContext(alertContext);
const [error, setError] = useState<{
detail: { error: string | undefined; traceback: string | undefined };
} | null>(null);

View file

@ -10,15 +10,15 @@ import BaseModal from "../baseModal";
const ExportModal = forwardRef(
(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 flow = flows.find((f) => f.id === tabId);
useEffect(() => {
setName(flow.name);
setDescription(flow.description);
}, [flow.name, flow.description]);
const [name, setName] = useState(flow.name);
const [description, setDescription] = useState(flow.description);
setName(flow!.name);
setDescription(flow!.description);
}, [flow!.name, flow!.description]);
const [name, setName] = useState(flow!.name);
const [description, setDescription] = useState(flow!.description);
const [open, setOpen] = useState(false);
return (
@ -40,7 +40,6 @@ const ExportModal = forwardRef(
tabId={tabId}
setName={setName}
setDescription={setDescription}
updateFlow={updateFlow}
/>
<div className="mt-3 flex items-center space-x-2">
<Checkbox

View file

@ -194,7 +194,8 @@ export default function FormModal({
}
function handleWsMessage(data: any) {
if (Array.isArray(data)) {
console.log(data);
if (Array.isArray(data) && data.length > 0) {
//set chat history
setChatHistory((_) => {
let newChatHistory: ChatMessageType[] = [];
@ -313,7 +314,7 @@ export default function FormModal({
}
};
// do not add connectWS on dependencies array
}, []);
}, [open]);
useEffect(() => {
if (

View file

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

View file

@ -1,10 +1,10 @@
import { cloneDeep } from "lodash";
import { X } from "lucide-react";
import { useContext, useEffect, useRef, useState } from "react";
import PaginatorComponent from "../../components/PaginatorComponent";
import ShadTooltip from "../../components/ShadTooltipComponent";
import IconComponent from "../../components/genericIconComponent";
import Header from "../../components/headerComponent";
import LoadingComponent from "../../components/loadingComponent";
import { Button } from "../../components/ui/button";
import { Checkbox } from "../../components/ui/checkbox";
import { Input } from "../../components/ui/input";
@ -16,8 +16,13 @@ import {
TableHeader,
TableRow,
} from "../../components/ui/table";
import {
ADMIN_HEADER_DESCRIPTION,
ADMIN_HEADER_TITLE,
} from "../../constants/constants";
import { alertContext } from "../../contexts/alertContext";
import { AuthContext } from "../../contexts/authContext";
import { TabsContext } from "../../contexts/tabsContext";
import {
addUser,
deleteUser,
@ -33,12 +38,19 @@ export default function AdminPage() {
const [inputValue, setInputValue] = useState("");
const [size, setPageSize] = useState(10);
const [index, setPageIndex] = useState(0);
const [index, setPageIndex] = useState(1);
const [loadingUsers, setLoadingUsers] = useState(true);
const { setErrorData, setSuccessData } = useContext(alertContext);
const { userData } = useContext(AuthContext);
const [totalRowsCount, setTotalRowsCount] = useState(0);
const { setTabId } = useContext(TabsContext);
// set null id
useEffect(() => {
setTabId("");
}, []);
const userList = useRef([]);
useEffect(() => {
@ -65,7 +77,9 @@ export default function AdminPage() {
function handleChangePagination(pageIndex: number, pageSize: number) {
setLoadingUsers(true);
getUsersPage(pageIndex, pageSize)
setPageSize(pageSize);
setPageIndex(pageIndex);
getUsersPage(pageSize * (pageIndex - 1), pageSize)
.then((users) => {
setTotalRowsCount(users["total_count"]);
userList.current = users["users"];
@ -78,7 +92,7 @@ export default function AdminPage() {
}
function resetFilter() {
setPageIndex(0);
setPageIndex(1);
setPageSize(10);
getUsers();
}
@ -168,270 +182,264 @@ export default function AdminPage() {
function handleNewUser(user: UserInputType) {
addUser(user)
.then((res) => {
resetFilter();
setSuccessData({
title: "Success! New user added!",
});
updateUser(res["id"], {
is_active: user.is_active,
is_superuser: user.is_superuser,
}).then((res) => {
resetFilter();
setSuccessData({
title: "Success! New user added!",
});
})
})
.catch((error) => {
setErrorData({
title: "Error on add new user",
list: [error["response"]["data"]["detail"]],
title: "Error when adding new user",
list: [error.response.data.detail],
});
});
}
return (
<>
<div className="flex flex-col">
<Header />
{userData && (
<div className="main-page-panel">
<div className="m-auto flex h-full flex-row justify-center">
<div className="basis-5/6">
<div className="m-auto flex h-full flex-col space-y-8 p-8 ">
<div className="flex items-center justify-between space-y-2">
<div>
<h2 className="text-2xl font-bold tracking-tight">
Welcome back!
</h2>
<p className="text-muted-foreground">
Navigate through this section to efficiently oversee all
application users. From here, you can seamlessly manage
user accounts.
</p>
</div>
<div className="flex items-center space-x-2"></div>
</div>
{userList.current.length === 0 && !loadingUsers && (
<>
<div className="flex items-center justify-between">
<h2>There's no users registered :)</h2>
</div>
</>
)}
<>
<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>
</>
<Header />
{userData && (
<div className="admin-page-panel flex h-full flex-col pb-8">
<div className="main-page-nav-arrangement">
<span className="main-page-nav-title">
<IconComponent name="Shield" className="w-6" />
{ADMIN_HEADER_TITLE}
</span>
</div>
<span className="admin-page-description-text">
{ADMIN_HEADER_DESCRIPTION}
</span>
<div className="flex w-full justify-between px-4">
<div className="flex w-96 items-center gap-4">
<Input
placeholder="Search Username"
value={inputValue}
onChange={(e) => handleFilterUsers(e.target.value)}
/>
{inputValue.length > 0 ? (
<div
className="cursor-pointer"
onClick={() => {
setInputValue("");
setFilterUserList(userList.current);
}}
>
<IconComponent name="X" className="w-6 text-foreground" />
</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>
{loadingUsers ? (
<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 IconComponent from "../../components/genericIconComponent";
import Header from "../../components/headerComponent";
import { SkeletonCardComponent } from "../../components/skeletonCardComponent";
import { getExamples } from "../../controllers/API";
import { FlowType } from "../../types/flow";
export default function CommunityPage(): JSX.Element {
@ -74,7 +75,14 @@ export default function CommunityPage(): JSX.Element {
new and powerful features.
</span>
<div className="community-pages-flows-panel">
{!loadingExamples &&
{loadingExamples ? (
<>
<SkeletonCardComponent />
<SkeletonCardComponent />
<SkeletonCardComponent />
<SkeletonCardComponent />
</>
) : (
examples.map((flow, idx) => (
<CardComponent
key={idx}
@ -99,7 +107,8 @@ export default function CommunityPage(): JSX.Element {
</Button>
}
/>
))}
))
)}
</div>
</div>
</>

View file

@ -298,7 +298,14 @@ export default function Page({
setNodes((nds) => nds.concat(newNode));
} else if (event.dataTransfer.types.some((types) => types === "Files")) {
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

View file

@ -99,16 +99,20 @@ export default function ExtraSidebar(): JSX.Element {
<ShadTooltip content={"Code"} side="top">
<div className="side-bar-button">
{flow && flow.data && (
<ApiModal flow={flow} disable={!isBuilt}>
<div className={classNames("extra-side-bar-buttons")}>
<IconComponent
name="Code2"
className={
"side-bar-button-size" +
(isBuilt ? " " : " extra-side-bar-save-disable")
}
/>
</div>
<ApiModal flow={flow}>
<button
className={"w-full " + (!isBuilt ? "button-disable" : "")}
>
<div className={classNames("extra-side-bar-buttons")}>
<IconComponent
name="Code2"
className={
"side-bar-button-size" +
(isBuilt ? " " : " extra-side-bar-save-disable")
}
/>
</div>
</button>
</ApiModal>
)}
</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 DropdownButton from "../../components/DropdownButtonComponent";
import { CardComponent } from "../../components/cardComponent";
@ -7,6 +7,7 @@ import Header from "../../components/headerComponent";
import { SkeletonCardComponent } from "../../components/skeletonCardComponent";
import { Button } from "../../components/ui/button";
import { USER_PROJECTS_HEADER } from "../../constants/constants";
import { alertContext } from "../../contexts/alertContext";
import { TabsContext } from "../../contexts/tabsContext";
export default function HomePage(): JSX.Element {
const {
@ -19,6 +20,7 @@ export default function HomePage(): JSX.Element {
uploadFlow,
isLoading,
} = useContext(TabsContext);
const { setErrorData } = useContext(alertContext);
const dropdownOptions = [
{
name: "Import from JSON",
@ -35,9 +37,36 @@ export default function HomePage(): JSX.Element {
}, []);
const navigate = useNavigate();
useEffect(() => {
console.log(isLoading);
}, [isLoading]);
const [isDragging, setIsDragging] = useState(false);
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
return (
@ -82,40 +111,63 @@ export default function HomePage(): JSX.Element {
<span className="main-page-description-text">
Manage your personal projects. Download or upload your collection.
</span>
<div className="main-page-flows-display">
{isLoading && flows.length == 0 ? (
<div
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 />
<SkeletonCardComponent />
<SkeletonCardComponent />
<SkeletonCardComponent />
<IconComponent
name="ArrowUpToLine"
className="h-12 w-12 stroke-1"
/>
Drop your flow here
</>
) : (
flows.map((flow, idx) => (
<CardComponent
key={idx}
flow={flow}
id={flow.id}
button={
<Link to={"/flow/" + flow.id}>
<Button
variant="outline"
size="sm"
className="whitespace-nowrap "
>
<IconComponent
name="ExternalLink"
className="main-page-nav-button"
/>
Edit Flow
</Button>
</Link>
}
onDelete={() => {
removeFlow(flow.id);
}}
/>
))
<div className="main-page-flows-display">
{isLoading && flows.length == 0 ? (
<>
<SkeletonCardComponent />
<SkeletonCardComponent />
<SkeletonCardComponent />
<SkeletonCardComponent />
</>
) : (
flows.map((flow, idx) => (
<CardComponent
key={idx}
flow={flow}
id={flow.id}
button={
<Link to={"/flow/" + flow.id}>
<Button
variant="outline"
size="sm"
className="whitespace-nowrap "
>
<IconComponent
name="ExternalLink"
className="main-page-nav-button"
/>
Edit Flow
</Button>
</Link>
}
onDelete={() => {
removeFlow(flow.id);
}}
/>
))
)}
</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(() => {
getLoggedUser()
.then((user) => {
const isSuperUser = user.is_superuser;
const isSuperUser = user!.is_superuser;
setIsAdmin(isSuperUser);
setUserData(user);
})

View file

@ -9,6 +9,7 @@ import ApiKeysPage from "./pages/ApiKeysPage";
import CommunityPage from "./pages/CommunityPage";
import FlowPage from "./pages/FlowPage";
import HomePage from "./pages/MainPage";
import ProfileSettingsPage from "./pages/ProfileSettingsPage";
import ViewPage from "./pages/ViewPage";
import DeleteAccountPage from "./pages/deleteAccountPage";
import LoginPage from "./pages/loginPage";
@ -95,6 +96,14 @@ const Router = () => {
/>
<Route path="/account">
<Route
path="settings"
element={
<ProtectedRoute>
<ProfileSettingsPage />
</ProtectedRoute>
}
/>
<Route
path="delete"
element={

View file

@ -212,6 +212,10 @@
@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 {
@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;
}
.admin-page-description-text {
@apply flex w-[80%] px-6 pb-8 text-muted-foreground;
}
.main-page-flows-display {
@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;
};
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 = {
id: string;
username: string;
is_active: boolean;
is_superuser: boolean;
profile_image: string;
create_at: Date;
updated_at: Date;
};

View file

@ -233,6 +233,7 @@ export type PaginatorComponentType = {
export type ConfirmationModalType = {
title: string;
titleHeader: string;
asChild?: boolean;
modalContent: string;
modalContentTitle: string;
cancelText: string;
@ -253,6 +254,7 @@ export type UserManagementType = {
icon: string;
data?: any;
index?: number;
asChild?: boolean;
onConfirm: (index, data) => void;
};
@ -261,6 +263,12 @@ export type loginInputStateType = {
password: string;
};
export type patchUserInputStateType = {
password: string;
cnfPassword: string;
gradient: string;
};
export type UserInputType = {
username: string;
password: string;

View file

@ -255,7 +255,9 @@ export function addVersionToDuplicates(flow: FlowType, flows: FlowType[]) {
}
export function handleKeyDown(
e: React.KeyboardEvent<HTMLInputElement>,
e:
| React.KeyboardEvent<HTMLInputElement>
| React.KeyboardEvent<HTMLTextAreaElement>,
inputValue: string | string[] | null,
block: string
) {

View file

@ -1,4 +1,5 @@
import {
ArrowUpToLine,
Bell,
Check,
CheckCircle2,
@ -58,6 +59,7 @@ import {
Scissors,
Search,
Settings2,
Shield,
Sparkles,
SunIcon,
TerminalSquare,
@ -65,6 +67,7 @@ import {
Undo,
Unplug,
Upload,
User,
UserCog2,
UserMinus2,
UserPlus2,
@ -86,7 +89,7 @@ import { EvernoteIcon } from "../icons/Evernote";
import { FBIcon } from "../icons/FacebookMessenger";
import { GitBookIcon } from "../icons/GitBook";
import { GoogleIcon } from "../icons/Google";
import GradientSparkles from "../icons/GradientSparkles";
import {GradientSparkles} from "../icons/GradientSparkles";
import { HuggingFaceIcon } from "../icons/HuggingFace";
import { IFixIcon } from "../icons/IFixIt";
import { MetaIcon } from "../icons/Meta";
@ -186,6 +189,7 @@ export const nodeNames: { [char: string]: string } = {
};
export const nodeIconsLucide: iconsType = {
ArrowUpToLine: ArrowUpToLine,
Chroma: ChromaIcon,
AirbyteJSONLoader: AirbyteIcon,
Anthropic: AnthropicIcon,
@ -220,6 +224,7 @@ export const nodeIconsLucide: iconsType = {
ChatVertexAI: VertexAIIcon,
VertexAIEmbeddings: VertexAIIcon,
agents: Rocket,
User,
WikipediaAPIWrapper: SvgWikipedia,
chains: Link,
memories: Cpu,
@ -260,6 +265,7 @@ export const nodeIconsLucide: iconsType = {
Bell,
ChevronLeft,
ChevronDown,
Shield,
Plus,
Redo,
Settings2,

View file

@ -166,7 +166,8 @@ def test_user(client):
username="testuser",
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()

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"
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
update_data = UserUpdate(username="newname")
update_data = UserUpdate(is_active=False)
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
response = client.get("/api/v1/user", headers=logged_in_headers)
assert response.json()["username"] == "newname", response.json()
response = client.get("/api/v1/users/whoami", headers=logged_in_headers)
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):
user_id = test_user["id"]
response = client.delete(f"/api/v1/user/{user_id}", headers=super_user_headers)
assert response.status_code == 200
user_id = test_user.get("id")
response = client.delete(f"/api/v1/users/{user_id}", headers=super_user_headers)
assert response.status_code == 200, response.json()
# Attempt to fetch the deleted user from the database
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(
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()
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):
user_id = "wrong_id"
update_data = UserUpdate(
@ -169,7 +198,7 @@ def test_patch_user_wrong_id(client, active_user, logged_in_headers):
)
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.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):
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.json() == {"detail": "User deleted"}
def test_delete_user_wrong_id(client, test_user, super_user_headers):
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.json() == {
"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):
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.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"