Add Admin Login Page with User Management (#737)
This pull request introduces a new admin login page to the application, enhancing the user management experience by implementing a user table with pagination. This will allow administrators to efficiently manage users, edit passwords, delete accounts, and apply filters through a user-friendly table interface. /login/admin - Just click login
This commit is contained in:
commit
5ae597497e
16 changed files with 4422 additions and 1907 deletions
5223
src/frontend/package-lock.json
generated
5223
src/frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -12,11 +12,13 @@
|
||||||
"@radix-ui/react-checkbox": "^1.0.4",
|
"@radix-ui/react-checkbox": "^1.0.4",
|
||||||
"@radix-ui/react-dialog": "^1.0.4",
|
"@radix-ui/react-dialog": "^1.0.4",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.0.5",
|
"@radix-ui/react-dropdown-menu": "^2.0.5",
|
||||||
|
"@radix-ui/react-form": "^0.0.3",
|
||||||
"@radix-ui/react-icons": "^1.3.0",
|
"@radix-ui/react-icons": "^1.3.0",
|
||||||
"@radix-ui/react-label": "^2.0.2",
|
"@radix-ui/react-label": "^2.0.2",
|
||||||
"@radix-ui/react-menubar": "^1.0.3",
|
"@radix-ui/react-menubar": "^1.0.3",
|
||||||
"@radix-ui/react-popover": "^1.0.6",
|
"@radix-ui/react-popover": "^1.0.6",
|
||||||
"@radix-ui/react-progress": "^1.0.3",
|
"@radix-ui/react-progress": "^1.0.3",
|
||||||
|
"@radix-ui/react-select": "^1.2.2",
|
||||||
"@radix-ui/react-separator": "^1.0.3",
|
"@radix-ui/react-separator": "^1.0.3",
|
||||||
"@radix-ui/react-slot": "^1.0.2",
|
"@radix-ui/react-slot": "^1.0.2",
|
||||||
"@radix-ui/react-switch": "^1.0.3",
|
"@radix-ui/react-switch": "^1.0.3",
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,9 @@ export default function App() {
|
||||||
}>
|
}>
|
||||||
>([]);
|
>([]);
|
||||||
|
|
||||||
|
const isLoginPage = location.pathname.includes('login');
|
||||||
|
const isAdminPage = location.pathname.includes('admin');
|
||||||
|
|
||||||
// Use effect hook to update alertsList when a new alert is added
|
// Use effect hook to update alertsList when a new alert is added
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// If there is an error alert open with data, add it to the alertsList
|
// If there is an error alert open with data, add it to the alertsList
|
||||||
|
|
@ -121,6 +124,7 @@ export default function App() {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
//need parent component with width and height
|
//need parent component with width and height
|
||||||
<div className="flex h-full flex-col">
|
<div className="flex h-full flex-col">
|
||||||
|
|
@ -133,7 +137,10 @@ export default function App() {
|
||||||
}}
|
}}
|
||||||
FallbackComponent={CrashErrorComponent}
|
FallbackComponent={CrashErrorComponent}
|
||||||
>
|
>
|
||||||
<Header />
|
{
|
||||||
|
!isLoginPage
|
||||||
|
&&
|
||||||
|
<Header />}
|
||||||
<Router />
|
<Router />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
<div></div>
|
<div></div>
|
||||||
|
|
|
||||||
118
src/frontend/src/components/PaginatorComponent/index.tsx
Normal file
118
src/frontend/src/components/PaginatorComponent/index.tsx
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
import { useState } from "react";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "../../components/ui/select";
|
||||||
|
import { PaginatorComponentType } from "../../types/components";
|
||||||
|
import IconComponent from "../genericIconComponent";
|
||||||
|
import { Button } from "../ui/button";
|
||||||
|
|
||||||
|
export default function PaginatorComponent({
|
||||||
|
pageSize = 10,
|
||||||
|
pageIndex = 1,
|
||||||
|
rowsCount = [10, 20, 30],
|
||||||
|
totalRowsCount = 0,
|
||||||
|
paginate,
|
||||||
|
}: PaginatorComponentType) {
|
||||||
|
const [size, setPageSize] = useState(pageSize);
|
||||||
|
const [index, setPageIndex] = useState(pageIndex);
|
||||||
|
|
||||||
|
const [maxIndex, setMaxPageIndex] = useState(
|
||||||
|
Math.ceil(totalRowsCount / pageSize)
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex items-center justify-between px-2">
|
||||||
|
<div className="flex-1 text-sm text-muted-foreground"></div>
|
||||||
|
<div className="flex items-center space-x-6 lg:space-x-8">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<p className="text-sm font-medium">Rows per page</p>
|
||||||
|
<Select
|
||||||
|
onValueChange={(pageSize: string) => {
|
||||||
|
setPageSize(Number(pageSize));
|
||||||
|
setMaxPageIndex(Math.ceil(totalRowsCount / Number(pageSize)));
|
||||||
|
paginate(Number(pageSize), index);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-[100px]">
|
||||||
|
<SelectValue placeholder="10" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{rowsCount.map((item, i) => (
|
||||||
|
<SelectItem key={i} value={item.toString()}>
|
||||||
|
{item}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-[100px] items-center justify-center text-sm font-medium">
|
||||||
|
Page {index} of {maxIndex}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="hidden h-8 w-8 p-0 lg:flex"
|
||||||
|
onClick={() => {
|
||||||
|
setPageIndex(1);
|
||||||
|
paginate(size, 1);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="sr-only">Go to first page</span>
|
||||||
|
<IconComponent name="ChevronsLeft" className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
if (index <= 1) {
|
||||||
|
setPageIndex(1);
|
||||||
|
paginate(size, 1);
|
||||||
|
} else {
|
||||||
|
{
|
||||||
|
setPageIndex(index - 1);
|
||||||
|
paginate(size, index - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
variant="outline"
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
>
|
||||||
|
<span className="sr-only">Go to previous page</span>
|
||||||
|
<IconComponent name="ChevronLeft" className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
if (index >= maxIndex) {
|
||||||
|
setPageIndex(maxIndex);
|
||||||
|
paginate(size, maxIndex);
|
||||||
|
} else {
|
||||||
|
setPageIndex(index + 1);
|
||||||
|
paginate(size, index + 1);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
variant="outline"
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
>
|
||||||
|
<span className="sr-only">Go to next page</span>
|
||||||
|
<IconComponent name="ChevronRight" className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="hidden h-8 w-8 p-0 lg:flex"
|
||||||
|
onClick={() => {
|
||||||
|
setPageIndex(maxIndex);
|
||||||
|
paginate(size, maxIndex);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="sr-only">Go to last page</span>
|
||||||
|
<IconComponent name="ChevronsRight" className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -7,5 +7,5 @@ export default function IconComponent({
|
||||||
iconColor,
|
iconColor,
|
||||||
}: IconComponentProps): JSX.Element {
|
}: IconComponentProps): JSX.Element {
|
||||||
const TargetIcon = nodeIconsLucide[name] ?? nodeIconsLucide["unknown"];
|
const TargetIcon = nodeIconsLucide[name] ?? nodeIconsLucide["unknown"];
|
||||||
return <TargetIcon className={className} style={{ color: iconColor }} />;
|
return <TargetIcon strokeWidth={1.5} className={className} style={{ color: iconColor }} />;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
120
src/frontend/src/components/ui/select.tsx
Normal file
120
src/frontend/src/components/ui/select.tsx
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as SelectPrimitive from "@radix-ui/react-select"
|
||||||
|
import { Check, ChevronDown } from "lucide-react"
|
||||||
|
import { cn } from "../../utils/utils"
|
||||||
|
|
||||||
|
const Select = SelectPrimitive.Root
|
||||||
|
|
||||||
|
const SelectGroup = SelectPrimitive.Group
|
||||||
|
|
||||||
|
const SelectValue = SelectPrimitive.Value
|
||||||
|
|
||||||
|
const SelectTrigger = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.Trigger
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<SelectPrimitive.Icon asChild>
|
||||||
|
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||||
|
</SelectPrimitive.Icon>
|
||||||
|
</SelectPrimitive.Trigger>
|
||||||
|
))
|
||||||
|
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
|
||||||
|
|
||||||
|
const SelectContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
||||||
|
>(({ className, children, position = "popper", ...props }, ref) => (
|
||||||
|
<SelectPrimitive.Portal>
|
||||||
|
<SelectPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||||
|
position === "popper" &&
|
||||||
|
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
position={position}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<SelectPrimitive.Viewport
|
||||||
|
className={cn(
|
||||||
|
"p-1",
|
||||||
|
position === "popper" &&
|
||||||
|
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</SelectPrimitive.Viewport>
|
||||||
|
</SelectPrimitive.Content>
|
||||||
|
</SelectPrimitive.Portal>
|
||||||
|
))
|
||||||
|
SelectContent.displayName = SelectPrimitive.Content.displayName
|
||||||
|
|
||||||
|
const SelectLabel = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.Label>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.Label
|
||||||
|
ref={ref}
|
||||||
|
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
SelectLabel.displayName = SelectPrimitive.Label.displayName
|
||||||
|
|
||||||
|
const SelectItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.Item>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.Item
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
|
<SelectPrimitive.ItemIndicator>
|
||||||
|
<Check className="h-4 w-4" />
|
||||||
|
</SelectPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||||
|
</SelectPrimitive.Item>
|
||||||
|
))
|
||||||
|
SelectItem.displayName = SelectPrimitive.Item.displayName
|
||||||
|
|
||||||
|
const SelectSeparator = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.Separator>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.Separator
|
||||||
|
ref={ref}
|
||||||
|
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
|
||||||
|
|
||||||
|
export {
|
||||||
|
Select,
|
||||||
|
SelectGroup,
|
||||||
|
SelectValue,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectContent,
|
||||||
|
SelectLabel,
|
||||||
|
SelectItem,
|
||||||
|
SelectSeparator,
|
||||||
|
}
|
||||||
|
|
@ -16,32 +16,32 @@ function ApiInterceptor() {
|
||||||
const interceptor = api.interceptors.response.use(
|
const interceptor = api.interceptors.response.use(
|
||||||
(response) => response,
|
(response) => response,
|
||||||
async (error: AxiosError) => {
|
async (error: AxiosError) => {
|
||||||
if (URL_EXCLUDED_FROM_ERROR_RETRIES.includes(error.config?.url)) {
|
// if (URL_EXCLUDED_FROM_ERROR_RETRIES.includes(error.config?.url)) {
|
||||||
return Promise.reject(error);
|
// return Promise.reject(error);
|
||||||
}
|
// }
|
||||||
let retryCount = 0;
|
// let retryCount = 0;
|
||||||
|
|
||||||
while (retryCount < 4) {
|
// while (retryCount < 4) {
|
||||||
await sleep(5000); // Sleep for 5 seconds
|
// await sleep(5000); // Sleep for 5 seconds
|
||||||
retryCount++;
|
// retryCount++;
|
||||||
try {
|
// try {
|
||||||
const response = await axios.request(error.config);
|
// const response = await axios.request(error.config);
|
||||||
return response;
|
// return response;
|
||||||
} catch (error) {
|
// } catch (error) {
|
||||||
if (retryCount === 3) {
|
// if (retryCount === 3) {
|
||||||
setErrorData({
|
// setErrorData({
|
||||||
title: "There was an error on web connection, please: ",
|
// title: "There was an error on web connection, please: ",
|
||||||
list: [
|
// list: [
|
||||||
"Refresh the page",
|
// "Refresh the page",
|
||||||
"Use a new flow tab",
|
// "Use a new flow tab",
|
||||||
"Check if the backend is up",
|
// "Check if the backend is up",
|
||||||
"Endpoint: " + error.config?.url,
|
// "Endpoint: " + error.config?.url,
|
||||||
],
|
// ],
|
||||||
});
|
// });
|
||||||
return Promise.reject(error);
|
// return Promise.reject(error);
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
66
src/frontend/src/modals/ConfirmationModal/index.tsx
Normal file
66
src/frontend/src/modals/ConfirmationModal/index.tsx
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Button } from "../../components/ui/button";
|
||||||
|
import { ConfirmationModalType } from "../../types/components";
|
||||||
|
import { nodeIconsLucide } from "../../utils/styleUtils";
|
||||||
|
import BaseModal from "../baseModal";
|
||||||
|
|
||||||
|
export default function ConfirmationModal({
|
||||||
|
title,
|
||||||
|
titleHeader,
|
||||||
|
modalContent,
|
||||||
|
modalContentTitle,
|
||||||
|
cancelText,
|
||||||
|
confirmationText,
|
||||||
|
children,
|
||||||
|
icon,
|
||||||
|
data,
|
||||||
|
index,
|
||||||
|
onConfirm,
|
||||||
|
}: ConfirmationModalType) {
|
||||||
|
const Icon: any = nodeIconsLucide[icon];
|
||||||
|
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
return (
|
||||||
|
<BaseModal size="x-small" open={open} setOpen={setOpen}>
|
||||||
|
<BaseModal.Trigger>{children}</BaseModal.Trigger>
|
||||||
|
<BaseModal.Header description={titleHeader}>
|
||||||
|
<span className="pr-2">{title}</span>
|
||||||
|
<Icon
|
||||||
|
name="icon"
|
||||||
|
className="h-6 w-6 pl-1 text-foreground"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</BaseModal.Header>
|
||||||
|
<BaseModal.Content>
|
||||||
|
{modalContentTitle != "" && (
|
||||||
|
<>
|
||||||
|
<strong>{modalContentTitle}</strong>
|
||||||
|
<br></br>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<span>{modalContent}</span>
|
||||||
|
</BaseModal.Content>
|
||||||
|
|
||||||
|
<BaseModal.Footer>
|
||||||
|
<Button
|
||||||
|
className="ml-3"
|
||||||
|
onClick={() => {
|
||||||
|
setOpen(false);
|
||||||
|
onConfirm(index, data);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{confirmationText}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{cancelText}
|
||||||
|
</Button>
|
||||||
|
</BaseModal.Footer>
|
||||||
|
</BaseModal>
|
||||||
|
);
|
||||||
|
}
|
||||||
237
src/frontend/src/modals/UserManagementModal/index.tsx
Normal file
237
src/frontend/src/modals/UserManagementModal/index.tsx
Normal file
|
|
@ -0,0 +1,237 @@
|
||||||
|
import * as Form from "@radix-ui/react-form";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Button } from "../../components/ui/button";
|
||||||
|
import { UserManagementType } from "../../types/components";
|
||||||
|
import { nodeIconsLucide } from "../../utils/styleUtils";
|
||||||
|
import BaseModal from "../baseModal";
|
||||||
|
|
||||||
|
export default function UserManagementModal({
|
||||||
|
title,
|
||||||
|
titleHeader,
|
||||||
|
cancelText,
|
||||||
|
confirmationText,
|
||||||
|
children,
|
||||||
|
icon,
|
||||||
|
data,
|
||||||
|
index,
|
||||||
|
onConfirm,
|
||||||
|
}: UserManagementType) {
|
||||||
|
const Icon: any = nodeIconsLucide[icon];
|
||||||
|
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
const [password, setPassword] = useState(data?.password ?? "");
|
||||||
|
const [username, setUserName] = useState(data?.user ?? "");
|
||||||
|
const [confirmPassword, setConfirmPassword] = useState(data?.password ?? "");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!data) {
|
||||||
|
resetForm();
|
||||||
|
}
|
||||||
|
}, [data, open]);
|
||||||
|
|
||||||
|
function resetForm() {
|
||||||
|
setPassword("");
|
||||||
|
setUserName("");
|
||||||
|
setConfirmPassword("");
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseModal size="medium-h-full" open={open} setOpen={setOpen}>
|
||||||
|
<BaseModal.Trigger>{children}</BaseModal.Trigger>
|
||||||
|
<BaseModal.Header description={titleHeader}>
|
||||||
|
<span className="pr-2">{title}</span>
|
||||||
|
<Icon
|
||||||
|
name="icon"
|
||||||
|
className="h-6 w-6 pl-1 text-foreground"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</BaseModal.Header>
|
||||||
|
<BaseModal.Content>
|
||||||
|
<Form.Root
|
||||||
|
onSubmit={(event) => {
|
||||||
|
if (password !== confirmPassword) {
|
||||||
|
event.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = Object.fromEntries(new FormData(event.currentTarget));
|
||||||
|
resetForm();
|
||||||
|
onConfirm(index ?? -1, data);
|
||||||
|
setOpen(false);
|
||||||
|
event.preventDefault();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="grid gap-5">
|
||||||
|
<Form.Field name="username">
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "baseline",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Form.Label className="data-[invalid]:label-invalid">
|
||||||
|
Username{" "}
|
||||||
|
<span className="font-medium text-destructive">*</span>
|
||||||
|
</Form.Label>
|
||||||
|
</div>
|
||||||
|
<Form.Control asChild>
|
||||||
|
<input
|
||||||
|
onChange={(input) => {
|
||||||
|
setUserName(input.target.value);
|
||||||
|
}}
|
||||||
|
value={username}
|
||||||
|
className="primary-input"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</Form.Control>
|
||||||
|
<Form.Message match="valueMissing" className="field-invalid">
|
||||||
|
Please enter your username
|
||||||
|
</Form.Message>
|
||||||
|
</Form.Field>
|
||||||
|
|
||||||
|
<div className="flex flex-row">
|
||||||
|
<div className="mr-3 basis-1/2">
|
||||||
|
<Form.Field
|
||||||
|
name="password"
|
||||||
|
serverInvalid={password != confirmPassword}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "baseline",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Form.Label className="data-[invalid]:label-invalid">
|
||||||
|
Password{" "}
|
||||||
|
<span className="font-medium text-destructive">*</span>
|
||||||
|
</Form.Label>
|
||||||
|
</div>
|
||||||
|
<Form.Control asChild>
|
||||||
|
<input
|
||||||
|
onChange={(input) => {
|
||||||
|
setPassword(input.target.value);
|
||||||
|
}}
|
||||||
|
value={password}
|
||||||
|
className="primary-input"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</Form.Control>
|
||||||
|
<Form.Message className="field-invalid" match="valueMissing">
|
||||||
|
Please enter a password
|
||||||
|
</Form.Message>
|
||||||
|
|
||||||
|
{password != confirmPassword && (
|
||||||
|
<Form.Message className="field-invalid">
|
||||||
|
Passwords do not match
|
||||||
|
</Form.Message>
|
||||||
|
)}
|
||||||
|
</Form.Field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="basis-1/2">
|
||||||
|
<Form.Field
|
||||||
|
name="confirmpassword"
|
||||||
|
serverInvalid={password != confirmPassword}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "baseline",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Form.Label className="data-[invalid]:label-invalid">
|
||||||
|
Confirm password{" "}
|
||||||
|
<span className="font-medium text-destructive">*</span>
|
||||||
|
</Form.Label>
|
||||||
|
</div>
|
||||||
|
<Form.Control asChild>
|
||||||
|
<input
|
||||||
|
onChange={(input) => {
|
||||||
|
setConfirmPassword(input.target.value);
|
||||||
|
}}
|
||||||
|
value={confirmPassword}
|
||||||
|
className="primary-input"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</Form.Control>
|
||||||
|
<Form.Message className="field-invalid" match="valueMissing">
|
||||||
|
Please confirm your password
|
||||||
|
</Form.Message>
|
||||||
|
</Form.Field>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/*
|
||||||
|
<Form.Field name="email">
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "baseline",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Form.Label className="data-[invalid]:label-invalid">
|
||||||
|
Email <span className="font-medium text-destructive">*</span>
|
||||||
|
</Form.Label>
|
||||||
|
<Form.Message className="field-invalid" match="valueMissing">
|
||||||
|
Please enter your email
|
||||||
|
</Form.Message>
|
||||||
|
<Form.Message className="field-invalid" match="typeMismatch">
|
||||||
|
Please provide a valid email
|
||||||
|
</Form.Message>
|
||||||
|
</div>
|
||||||
|
<Form.Control asChild>
|
||||||
|
<input className="primary-input" type="email" required />
|
||||||
|
</Form.Control>
|
||||||
|
</Form.Field> */}
|
||||||
|
|
||||||
|
{/*
|
||||||
|
<Form.Field name="birth">
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "baseline",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Form.Label className="data-[invalid]:label-invalid">
|
||||||
|
Date of birth{" "}
|
||||||
|
<span className="font-medium text-destructive">*</span>
|
||||||
|
</Form.Label>
|
||||||
|
<Form.Message className="field-invalid" match="valueMissing">
|
||||||
|
Please enter your date of birth
|
||||||
|
</Form.Message>
|
||||||
|
</div>
|
||||||
|
<Form.Control asChild>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
className="primary-input"
|
||||||
|
required
|
||||||
|
max={new Date().toISOString().split("T")[0]}
|
||||||
|
/>
|
||||||
|
</Form.Control>
|
||||||
|
</Form.Field> */}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="float-right">
|
||||||
|
<Form.Submit asChild>
|
||||||
|
<Button className="mr-3 mt-8">{confirmationText}</Button>
|
||||||
|
</Form.Submit>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{cancelText}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Form.Root>
|
||||||
|
</BaseModal.Content>
|
||||||
|
</BaseModal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -47,7 +47,7 @@ interface BaseModalProps {
|
||||||
open?: boolean;
|
open?: boolean;
|
||||||
setOpen?: (open: boolean) => void;
|
setOpen?: (open: boolean) => void;
|
||||||
disable?: boolean;
|
disable?: boolean;
|
||||||
size?: "x-small" | "smaller" | "small" | "medium" | "large" | "large-h-full";
|
size?: "x-small" | "smaller" | "small" | "medium" | "large" | "large-h-full" | "small-h-full" | "medium-h-full";
|
||||||
}
|
}
|
||||||
function BaseModal({
|
function BaseModal({
|
||||||
open,
|
open,
|
||||||
|
|
@ -85,10 +85,16 @@ function BaseModal({
|
||||||
minWidth = "min-w-[40vw]";
|
minWidth = "min-w-[40vw]";
|
||||||
height = "h-[40vh]";
|
height = "h-[40vh]";
|
||||||
break;
|
break;
|
||||||
|
case "small-h-full":
|
||||||
|
minWidth = "min-w-[40vw]";
|
||||||
|
break;
|
||||||
case "medium":
|
case "medium":
|
||||||
minWidth = "min-w-[60vw]";
|
minWidth = "min-w-[60vw]";
|
||||||
height = "h-[60vh]";
|
height = "h-[60vh]";
|
||||||
break;
|
break;
|
||||||
|
case "medium-h-full":
|
||||||
|
minWidth = "min-w-[60vw]";
|
||||||
|
break;
|
||||||
case "large":
|
case "large":
|
||||||
minWidth = "min-w-[80vw]";
|
minWidth = "min-w-[80vw]";
|
||||||
height = "h-[80vh]";
|
height = "h-[80vh]";
|
||||||
|
|
@ -112,7 +118,9 @@ function BaseModal({
|
||||||
{triggerChild}
|
{triggerChild}
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className={minWidth}>
|
<DialogContent className={minWidth}>
|
||||||
|
<div className="word-break-break-word truncate-doubleline">
|
||||||
{headerChild}
|
{headerChild}
|
||||||
|
</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>
|
||||||
|
|
|
||||||
32
src/frontend/src/pages/AdminPage/LoginPage/index.tsx
Normal file
32
src/frontend/src/pages/AdminPage/LoginPage/index.tsx
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { FaApple, FaGithub } from "react-icons/fa";
|
||||||
|
import { Button } from "../../../components/ui/button";
|
||||||
|
import { Input } from "../../../components/ui/input";
|
||||||
|
import { GoogleIcon } from "../../../icons/Google";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
export default function LoginAdminPage() {
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
function loginAdmin() {
|
||||||
|
navigate("/admin/");
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-full w-full flex-col items-center justify-center bg-muted">
|
||||||
|
<div className="flex w-72 flex-col items-center justify-center gap-2">
|
||||||
|
<span className="mb-4 text-5xl">⛓️</span>
|
||||||
|
<span className="mb-6 text-2xl font-semibold text-primary">
|
||||||
|
Admin
|
||||||
|
</span>
|
||||||
|
<Input className="bg-background" placeholder="Email address" />
|
||||||
|
<Input className="bg-background" placeholder="Password" />
|
||||||
|
<Button
|
||||||
|
onClick={() => {loginAdmin()}}
|
||||||
|
variant="default" className="w-full">
|
||||||
|
Login
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
397
src/frontend/src/pages/AdminPage/index.tsx
Normal file
397
src/frontend/src/pages/AdminPage/index.tsx
Normal file
|
|
@ -0,0 +1,397 @@
|
||||||
|
import _ from "lodash";
|
||||||
|
import { Pencil, Trash2, X } from "lucide-react";
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import PaginatorComponent from "../../components/PaginatorComponent";
|
||||||
|
import ShadTooltip from "../../components/ShadTooltipComponent";
|
||||||
|
import { Button } from "../../components/ui/button";
|
||||||
|
import { Input } from "../../components/ui/input";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "../../components/ui/table";
|
||||||
|
import ConfirmationModal from "../../modals/ConfirmationModal";
|
||||||
|
import UserManagementModal from "../../modals/UserManagementModal";
|
||||||
|
import IconComponent from "../../components/genericIconComponent";
|
||||||
|
|
||||||
|
export default function AdminPage() {
|
||||||
|
const [inputValue, setInputValue] = useState("");
|
||||||
|
|
||||||
|
const [size, setPageSize] = useState(10);
|
||||||
|
const [index, setPageIndex] = useState(1);
|
||||||
|
|
||||||
|
const userList = useRef([
|
||||||
|
{
|
||||||
|
user: generateRandomString(50),
|
||||||
|
email: generateRandomString(50) + "@example.com",
|
||||||
|
password: generateRandomString(50),
|
||||||
|
register_date: generateRandomDate(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
user: generateRandomString(8),
|
||||||
|
email: generateRandomString(10) + "@example.com",
|
||||||
|
password: generateRandomString(12),
|
||||||
|
register_date: generateRandomDate(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
user: generateRandomString(8),
|
||||||
|
email: generateRandomString(10) + "@example.com",
|
||||||
|
password: generateRandomString(12),
|
||||||
|
register_date: generateRandomDate(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
user: generateRandomString(8),
|
||||||
|
email: generateRandomString(10) + "@example.com",
|
||||||
|
password: generateRandomString(12),
|
||||||
|
register_date: generateRandomDate(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
user: generateRandomString(8),
|
||||||
|
email: generateRandomString(10) + "@example.com",
|
||||||
|
password: generateRandomString(12),
|
||||||
|
register_date: generateRandomDate(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
user: generateRandomString(8),
|
||||||
|
email: generateRandomString(10) + "@example.com",
|
||||||
|
password: generateRandomString(12),
|
||||||
|
register_date: generateRandomDate(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
user: generateRandomString(8),
|
||||||
|
email: generateRandomString(10) + "@example.com",
|
||||||
|
password: generateRandomString(12),
|
||||||
|
register_date: generateRandomDate(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
user: generateRandomString(8),
|
||||||
|
email: generateRandomString(10) + "@example.com",
|
||||||
|
password: generateRandomString(12),
|
||||||
|
register_date: generateRandomDate(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
user: generateRandomString(8),
|
||||||
|
email: generateRandomString(10) + "@example.com",
|
||||||
|
password: generateRandomString(12),
|
||||||
|
register_date: generateRandomDate(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
user: generateRandomString(8),
|
||||||
|
email: generateRandomString(10) + "@example.com",
|
||||||
|
password: generateRandomString(12),
|
||||||
|
register_date: generateRandomDate(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
user: generateRandomString(8),
|
||||||
|
email: generateRandomString(10) + "@example.com",
|
||||||
|
password: generateRandomString(12),
|
||||||
|
register_date: generateRandomDate(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
user: generateRandomString(8),
|
||||||
|
email: generateRandomString(10) + "@example.com",
|
||||||
|
password: generateRandomString(12),
|
||||||
|
register_date: generateRandomDate(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
user: generateRandomString(8),
|
||||||
|
email: generateRandomString(10) + "@example.com",
|
||||||
|
password: generateRandomString(12),
|
||||||
|
register_date: generateRandomDate(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
user: generateRandomString(8),
|
||||||
|
email: generateRandomString(10) + "@example.com",
|
||||||
|
password: generateRandomString(12),
|
||||||
|
register_date: generateRandomDate(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
user: generateRandomString(8),
|
||||||
|
email: generateRandomString(10) + "@example.com",
|
||||||
|
password: generateRandomString(12),
|
||||||
|
register_date: generateRandomDate(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
user: generateRandomString(8),
|
||||||
|
email: generateRandomString(10) + "@example.com",
|
||||||
|
password: generateRandomString(12),
|
||||||
|
register_date: generateRandomDate(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
user: generateRandomString(8),
|
||||||
|
email: generateRandomString(10) + "@example.com",
|
||||||
|
password: generateRandomString(12),
|
||||||
|
register_date: generateRandomDate(),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const [filterUserList, setFilterUserList] = useState(userList.current);
|
||||||
|
|
||||||
|
function generateRandomString(length) {
|
||||||
|
const characters =
|
||||||
|
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||||
|
let result = "";
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
const randomIndex = Math.floor(Math.random() * characters.length);
|
||||||
|
result += characters.charAt(randomIndex);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateRandomDate() {
|
||||||
|
const start = new Date(2010, 0, 1);
|
||||||
|
const end = new Date();
|
||||||
|
const randomTimestamp =
|
||||||
|
start.getTime() + Math.random() * (end.getTime() - start.getTime());
|
||||||
|
const randomDate = new Date(randomTimestamp);
|
||||||
|
|
||||||
|
const options = { year: "numeric", month: "short", day: "numeric" };
|
||||||
|
return randomDate.toLocaleDateString("en-US");
|
||||||
|
}
|
||||||
|
|
||||||
|
const [editUser, setEditUser] = useState(-1);
|
||||||
|
const [editedUser, setEditedUser] = useState("");
|
||||||
|
const [modalEditOpen, setModalEditOpen] = useState(false);
|
||||||
|
const [modalDeleteOpen, setModalDeleteOpen] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
resetFilter();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleInputChange = (event, index) => {
|
||||||
|
const user = _.cloneDeepWith(userList.current);
|
||||||
|
user[index].password = event.target.value;
|
||||||
|
userList.current = user;
|
||||||
|
|
||||||
|
const userFilter = _.cloneDeepWith(filterUserList);
|
||||||
|
userFilter[index].password = event.target.value;
|
||||||
|
setFilterUserList(userFilter);
|
||||||
|
|
||||||
|
setEditedUser(event.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
function handleChangePagination(pageIndex: number, pageSize: number) {
|
||||||
|
setPageIndex(pageIndex);
|
||||||
|
setPageSize(pageSize);
|
||||||
|
|
||||||
|
const startIndex = (pageIndex - 1) * pageSize;
|
||||||
|
const endIndex = startIndex + pageSize;
|
||||||
|
const newList = userList.current.slice(startIndex, endIndex);
|
||||||
|
|
||||||
|
setFilterUserList(newList);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetFilter() {
|
||||||
|
setFilterUserList(userList.current);
|
||||||
|
setPageIndex(1);
|
||||||
|
setPageSize(10);
|
||||||
|
|
||||||
|
const startIndex = (index - 1) * size;
|
||||||
|
const endIndex = index + size - 1;
|
||||||
|
const newList = userList.current.slice(startIndex, endIndex);
|
||||||
|
|
||||||
|
console.log(userList.current);
|
||||||
|
|
||||||
|
setFilterUserList(newList);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleFilterUsers(input: string) {
|
||||||
|
setInputValue(input);
|
||||||
|
|
||||||
|
if (input === "") {
|
||||||
|
resetFilter();
|
||||||
|
} else {
|
||||||
|
const filteredList = userList.current.filter(
|
||||||
|
(user) =>
|
||||||
|
user.user.toLowerCase().includes(input.toLowerCase()) ||
|
||||||
|
user.email.toLowerCase().includes(input.toLowerCase())
|
||||||
|
);
|
||||||
|
setFilterUserList(filteredList);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDeleteUser(index) {
|
||||||
|
const user = _.cloneDeepWith(userList.current);
|
||||||
|
user.splice(index, 1);
|
||||||
|
userList.current = user;
|
||||||
|
|
||||||
|
const userFilter = _.cloneDeepWith(filterUserList);
|
||||||
|
userFilter.splice(index, 1);
|
||||||
|
setFilterUserList(userFilter);
|
||||||
|
|
||||||
|
resetFilter();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleEditUser(index, user) {
|
||||||
|
const newUser = _.cloneDeepWith(userList.current);
|
||||||
|
newUser[index].password = user.password;
|
||||||
|
newUser[index].user = user.username;
|
||||||
|
userList.current = newUser;
|
||||||
|
resetFilter();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleNewUser(user) {
|
||||||
|
const newUser = {
|
||||||
|
user: user.username,
|
||||||
|
email: generateRandomString(50) + "@example.com",
|
||||||
|
password: user.password,
|
||||||
|
register_date: generateRandomDate(),
|
||||||
|
};
|
||||||
|
|
||||||
|
userList.current.unshift(newUser);
|
||||||
|
console.log(userList.current);
|
||||||
|
|
||||||
|
resetFilter();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="grid grid-cols-6 gap-4">
|
||||||
|
<div className="col-span-4 col-start-2">
|
||||||
|
<div className="m-auto h-full flex-1 flex-col space-y-8 p-8 md:flex">
|
||||||
|
<div className="flex items-center justify-between space-y-2">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-2xl font-bold tracking-tight">
|
||||||
|
Welcome back!
|
||||||
|
</h2>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
Here's a list of all users!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{userList.current.length === 0 && (
|
||||||
|
<>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h2>There's no users left :)</h2>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{userList.current.length > 0 && (
|
||||||
|
<>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex flex-1 items-center space-x-2">
|
||||||
|
<Input
|
||||||
|
value={inputValue}
|
||||||
|
placeholder="Filter users..."
|
||||||
|
className="h-8 w-[150px] lg:w-[250px]"
|
||||||
|
onChange={(e) => handleFilterUsers(e.target.value)}
|
||||||
|
/>
|
||||||
|
{inputValue.length > 0 && (
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
resetFilter();
|
||||||
|
setInputValue("");
|
||||||
|
}}
|
||||||
|
variant="ghost"
|
||||||
|
className="h-8 px-2 lg:px-3"
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
<X className="ml-2 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<UserManagementModal
|
||||||
|
title="New User"
|
||||||
|
titleHeader={"Add a new user"}
|
||||||
|
cancelText="Cancel"
|
||||||
|
confirmationText="Save"
|
||||||
|
icon={"UserPlus2"}
|
||||||
|
onConfirm={(index, user) => {
|
||||||
|
handleNewUser(user);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button>New User</Button>
|
||||||
|
</UserManagementModal>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="overflow-scroll overflow-x-hidden rounded-md border-2 bg-muted
|
||||||
|
custom-scroll min-[320px]:h-[20rem] md:h-[25rem] xl:h-[26rem] 2xl:h-[30rem]"
|
||||||
|
>
|
||||||
|
<Table className="table-fixed bg-muted outline-1 ">
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead className="h-10">User</TableHead>
|
||||||
|
<TableHead className="h-10">Password</TableHead>
|
||||||
|
<TableHead className="h-10 w-[100px] text-right"></TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{filterUserList.map((user, index) => (
|
||||||
|
<TableRow key={user.user}>
|
||||||
|
<TableCell className="truncate py-2 font-medium">
|
||||||
|
{user.user}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="truncate py-2">
|
||||||
|
{user.password}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="flex w-[100px] py-2 text-right">
|
||||||
|
<div className="flex">
|
||||||
|
<UserManagementModal
|
||||||
|
title="Edit"
|
||||||
|
titleHeader={`${user.user}`}
|
||||||
|
cancelText="Cancel"
|
||||||
|
confirmationText="Edit"
|
||||||
|
icon={"UserPlus2"}
|
||||||
|
data={user}
|
||||||
|
index={index}
|
||||||
|
onConfirm={(index, user) => {
|
||||||
|
handleEditUser(index, user);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ShadTooltip content="Edit" side="top">
|
||||||
|
<IconComponent name="Pencil"
|
||||||
|
className="h-4 w-4 cursor-pointer"
|
||||||
|
/>
|
||||||
|
</ShadTooltip>
|
||||||
|
</UserManagementModal>
|
||||||
|
|
||||||
|
<ConfirmationModal
|
||||||
|
title="Delete"
|
||||||
|
titleHeader="Delete User"
|
||||||
|
modalContentTitle="Attention!"
|
||||||
|
modalContent="Are you sure you want to delete this user? This action cannot be undone."
|
||||||
|
cancelText="Cancel"
|
||||||
|
confirmationText="Delete"
|
||||||
|
icon={"UserMinus2"}
|
||||||
|
data={user}
|
||||||
|
index={index}
|
||||||
|
onConfirm={(index, user) => {
|
||||||
|
handleDeleteUser(index);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ShadTooltip content="Delete" side="top">
|
||||||
|
<IconComponent name="Trash2"
|
||||||
|
className="ml-2 h-4 w-4 cursor-pointer"
|
||||||
|
/>
|
||||||
|
</ShadTooltip>
|
||||||
|
</ConfirmationModal>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
<PaginatorComponent
|
||||||
|
pageIndex={index}
|
||||||
|
pageSize={size}
|
||||||
|
totalRowsCount={filterUserList.length}
|
||||||
|
paginate={(pageIndex, pageSize) => {
|
||||||
|
handleChangePagination(pageSize, pageIndex);
|
||||||
|
}}
|
||||||
|
></PaginatorComponent>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,8 @@ import FlowPage from "./pages/FlowPage";
|
||||||
import HomePage from "./pages/MainPage";
|
import HomePage from "./pages/MainPage";
|
||||||
import DeleteAccountPage from "./pages/deleteAccountPage";
|
import DeleteAccountPage from "./pages/deleteAccountPage";
|
||||||
import LoginPage from "./pages/loginPage";
|
import LoginPage from "./pages/loginPage";
|
||||||
|
import LoginAdminPage from "./pages/AdminPage/LoginPage";
|
||||||
|
import AdminPage from "./pages/AdminPage";
|
||||||
|
|
||||||
const Router = () => {
|
const Router = () => {
|
||||||
return (
|
return (
|
||||||
|
|
@ -14,7 +16,12 @@ const Router = () => {
|
||||||
<Route path="" element={<FlowPage />} />
|
<Route path="" element={<FlowPage />} />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="*" element={<HomePage />} />
|
<Route path="*" element={<HomePage />} />
|
||||||
|
|
||||||
<Route path="/login" element={<LoginPage />} />
|
<Route path="/login" element={<LoginPage />} />
|
||||||
|
<Route path="/login/admin" element={<LoginAdminPage />} />
|
||||||
|
|
||||||
|
<Route path="/admin" element={<AdminPage />} />
|
||||||
|
|
||||||
<Route path="/account">
|
<Route path="/account">
|
||||||
<Route path="delete" element={<DeleteAccountPage />}></Route>
|
<Route path="delete" element={<DeleteAccountPage />}></Route>
|
||||||
</Route>
|
</Route>
|
||||||
|
|
|
||||||
|
|
@ -1019,4 +1019,12 @@
|
||||||
.chat-message-highlight {
|
.chat-message-highlight {
|
||||||
@apply rounded-md bg-indigo-100 px-0.5 dark:bg-indigo-900;
|
@apply rounded-md bg-indigo-100 px-0.5 dark:bg-indigo-900;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.field-invalid{
|
||||||
|
@apply font-medium text-[0.8rem] text-destructive absolute
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-invalid{
|
||||||
|
@apply text-destructive
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -171,3 +171,37 @@ export type IconComponentProps = {
|
||||||
export interface languageMap {
|
export interface languageMap {
|
||||||
[key: string]: string | undefined;
|
[key: string]: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type PaginatorComponentType = {
|
||||||
|
pageSize: number;
|
||||||
|
pageIndex: number;
|
||||||
|
rowsCount?: number[];
|
||||||
|
totalRowsCount: number;
|
||||||
|
paginate: (pageIndex: number, pageSize: number) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ConfirmationModalType = {
|
||||||
|
title: string;
|
||||||
|
titleHeader: string;
|
||||||
|
modalContent: string;
|
||||||
|
modalContentTitle: string;
|
||||||
|
cancelText: string;
|
||||||
|
confirmationText: string;
|
||||||
|
children: ReactElement;
|
||||||
|
icon: string;
|
||||||
|
data: any;
|
||||||
|
index: number;
|
||||||
|
onConfirm: (index, data) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserManagementType = {
|
||||||
|
title: string;
|
||||||
|
titleHeader: string;
|
||||||
|
cancelText: string;
|
||||||
|
confirmationText: string;
|
||||||
|
children: ReactElement;
|
||||||
|
icon: string;
|
||||||
|
data?: any;
|
||||||
|
index?: number;
|
||||||
|
onConfirm: (index, data) => void;
|
||||||
|
}
|
||||||
|
|
@ -57,6 +57,8 @@ import {
|
||||||
Trash2,
|
Trash2,
|
||||||
Undo,
|
Undo,
|
||||||
Upload,
|
Upload,
|
||||||
|
UserMinus2,
|
||||||
|
UserPlus2,
|
||||||
Users2,
|
Users2,
|
||||||
Variable,
|
Variable,
|
||||||
Wand2,
|
Wand2,
|
||||||
|
|
@ -64,6 +66,9 @@ import {
|
||||||
X,
|
X,
|
||||||
XCircle,
|
XCircle,
|
||||||
Zap,
|
Zap,
|
||||||
|
Pencil,
|
||||||
|
ChevronsRight,
|
||||||
|
ChevronsLeft
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { AirbyteIcon } from "../icons/Airbyte";
|
import { AirbyteIcon } from "../icons/Airbyte";
|
||||||
import { AnthropicIcon } from "../icons/Anthropic";
|
import { AnthropicIcon } from "../icons/Anthropic";
|
||||||
|
|
@ -275,4 +280,9 @@ export const nodeIconsLucide = {
|
||||||
Upload,
|
Upload,
|
||||||
MessageSquare,
|
MessageSquare,
|
||||||
MoreHorizontal,
|
MoreHorizontal,
|
||||||
|
UserMinus2,
|
||||||
|
UserPlus2,
|
||||||
|
Pencil,
|
||||||
|
ChevronsRight,
|
||||||
|
ChevronsLeft
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue