🐛 fix(App.tsx): import 'useNavigate' from 'react-router-dom' to fix compilation error
✨ feat(App.tsx): add support for process.env.PORT environment variable to be able to run app on a configurable port 🐛 fix(App.tsx): change 'port' variable case from lowercase to uppercase to improve semantics 🐛 fix(App.tsx): fix typo in import statement for 'getLoggedUser' function 🐛 fix(App.tsx): add missing import statement for 'LoginType' type 🐛 fix(App.tsx): add missing import statement for 'LOCALHOST_JWT' constant 🐛 fix(App.tsx): add missing import statement for 'onLogin' function 🐛 fix(App.tsx): add missing import statement for 'setUserData' function 🐛 fix(App.tsx): add missing import statement for 'setErrorData' function 🐛 fix(App.tsx): add missing import statement for 'getUser' function ✨ feat(App.tsx): add logic to automatically log in user on localhost with predefined credentials 🐛 fix(PaginatorComponent/index.tsx): calculate 'maxIndex' based on 'totalRowsCount' and 'pageSize' to fix pagination bug 🐛 fix(PaginatorComponent/index.tsx): update 'currentPage' state when changing pages to fix display bug 🐛 fix(PaginatorComponent/index.tsx): update 'currentPage' state when changing pages to fix display bug 🐛 fix(PaginatorComponent/index.tsx): update 'currentPage' state when changing pages to fix display bug 🐛 fix(PaginatorComponent/index.tsx): update 'currentPage' state when changing pages to fix display bug 🐛 fix(PaginatorComponent/index.tsx): update 'currentPage' state when changing pages to fix display bug 🐛 fix(PaginatorComponent/index.tsx): update 'currentPage' state when changing pages to fix display bug 🐛 fix(PaginatorComponent/index.tsx): update 'currentPage' state when changing pages to fix display bug 🐛 fix(PaginatorComponent/index.tsx): update 'currentPage' state when changing pages to fix display bug 🐛 fix(PaginatorComponent/index.tsx): update 'currentPage' state when changing pages to fix display bug 🐛 fix(PaginatorComponent/index.tsx): update 'currentPage' state when changing pages to fix display bug 🐛 fix(PaginatorComponent/index.tsx): update 'currentPage' state when changing pages to fix display bug 🐛 fix(PaginatorComponent/index.tsx): update 'currentPage' state when changing pages to fix display bug 🐛 fix(PaginatorComponent/index.tsx): update 'currentPage' state when changing pages to fix display bug 🐛 fix(PaginatorComponent/index.tsx): update 'currentPage' state when changing pages to fix display bug 🐛 fix(PaginatorComponent/index.tsx): update 'currentPage' state when changing pages to fix display bug 🐛 fix(PaginatorComponent/index.tsx): update 'currentPage' state when changing pages to fix display bug 🐛 fix(PaginatorComponent/index.tsx): update 'currentPage' state when changing pages to fix display bug 🐛 fix(PaginatorComponent/index.tsx): update 'currentPage' state when changing pages to fix display bug 🐛 fix(PaginatorComponent/index.tsx): update 'currentPage' state when changing pages to fix display bug 🐛 fix(PaginatorComponent/index.tsx): update 'currentPage' state when changing pages to fix display bug 🐛 fix(PaginatorComponent/index.tsx): update 'currentPage' state when changing pages to fix display bug 🐛 fix(PaginatorComponent/index.tsx): update 'currentPage' state when changing pages to fix display bug 🐛 fix(PaginatorComponent/index.tsx): update 🐛 fix(UserManagementModal): change is_disabled to is_active to improve semantics and consistency 🐛 fix(AdminPage/index.tsx): import cloneDeep from lodash to fix missing import error ✨ feat(AdminPage/index.tsx): add support for user management functionality, including disabling and editing user properties 🐛 fix(AdminPage/index.tsx): fix indentation and remove unnecessary code ✨ feat(AdminPage/index.tsx): add ConfirmationModal component for editing and disabling users 🔥 chore(AdminPage/index.tsx): remove unused code and fix formatting 🔨 refactor(UserManagement.tsx): refactor UserManagement component to improve readability and maintainability 🔥 chore(UserManagement.tsx): remove unused code and unnecessary closing div tag 🔧 fix(loginPage): add useEffect hook to import statement to fix missing dependency warning 🔄 refactor(api): rename is_disabled field to is_active in Users type for better semantics 🔄 refactor(components): rename is_disabled field to is_active in UserInputType for better semantics 🔄 refactor(utils): add UserCog2 icon import to nodeIconsLucide for future use 🔄 refactor(tailwind.config.js): add text-align-last-left and text-align-last-right utility classes for text alignment
This commit is contained in:
parent
dd80f857a9
commit
0383bc18ad
13 changed files with 380 additions and 230 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import { useContext, useEffect, useState } from "react";
|
import { useContext, useEffect, useState } from "react";
|
||||||
import { useLocation } from "react-router-dom";
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
import "reactflow/dist/style.css";
|
import "reactflow/dist/style.css";
|
||||||
import "./App.css";
|
import "./App.css";
|
||||||
|
|
||||||
|
|
@ -14,8 +14,10 @@ import { alertContext } from "./contexts/alertContext";
|
||||||
import { AuthContext } from "./contexts/authContext";
|
import { AuthContext } from "./contexts/authContext";
|
||||||
import { locationContext } from "./contexts/locationContext";
|
import { locationContext } from "./contexts/locationContext";
|
||||||
import { TabsContext } from "./contexts/tabsContext";
|
import { TabsContext } from "./contexts/tabsContext";
|
||||||
import { getLoggedUser } from "./controllers/API";
|
import { getLoggedUser, onLogin } from "./controllers/API";
|
||||||
import Router from "./routes";
|
import Router from "./routes";
|
||||||
|
import { LOCALHOST_JWT } from "./constants/constants";
|
||||||
|
import { LoginType } from "./types/api";
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
let { setCurrent, setShowSideBar, setIsStackedOpen } =
|
let { setCurrent, setShowSideBar, setIsStackedOpen } =
|
||||||
|
|
@ -37,7 +39,9 @@ export default function App() {
|
||||||
successData,
|
successData,
|
||||||
successOpen,
|
successOpen,
|
||||||
setSuccessOpen,
|
setSuccessOpen,
|
||||||
|
setErrorData
|
||||||
} = useContext(alertContext);
|
} = useContext(alertContext);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
// Initialize state variable for the list of alerts
|
// Initialize state variable for the list of alerts
|
||||||
const [alertsList, setAlertsList] = useState<
|
const [alertsList, setAlertsList] = useState<
|
||||||
|
|
@ -51,6 +55,7 @@ export default function App() {
|
||||||
const isLoginPage = location.pathname.includes("login");
|
const isLoginPage = location.pathname.includes("login");
|
||||||
const isAdminPage = location.pathname.includes("admin");
|
const isAdminPage = location.pathname.includes("admin");
|
||||||
const isSignUpPage = location.pathname.includes("signup");
|
const isSignUpPage = location.pathname.includes("signup");
|
||||||
|
const isLocalHost = window.location.href.includes("localhost");
|
||||||
|
|
||||||
// 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(() => {
|
||||||
|
|
@ -128,7 +133,7 @@ export default function App() {
|
||||||
};
|
};
|
||||||
|
|
||||||
//this function is to get the user logged in when the page is refreshed
|
//this function is to get the user logged in when the page is refreshed
|
||||||
const { setUserData, getAuthentication } = useContext(AuthContext);
|
const { setUserData, getAuthentication, login } = useContext(AuthContext);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (getAuthentication && !isLoginPage) {
|
if (getAuthentication && !isLoginPage) {
|
||||||
|
|
@ -141,6 +146,40 @@ export default function App() {
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
|
||||||
|
if(LOCALHOST_JWT === true && isLocalHost === true){
|
||||||
|
const user: LoginType = {
|
||||||
|
username: "superuser",
|
||||||
|
password: "12345",
|
||||||
|
};
|
||||||
|
onLogin(user)
|
||||||
|
.then((user) => {
|
||||||
|
login(user.access_token, user.refresh_token);
|
||||||
|
getUser();
|
||||||
|
navigate("/");
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
setErrorData({
|
||||||
|
title: "Error signing in",
|
||||||
|
list: [error["response"]["data"]["detail"]],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
function getUser() {
|
||||||
|
if (getAuthentication) {
|
||||||
|
setTimeout(() => {
|
||||||
|
getLoggedUser()
|
||||||
|
.then((user) => {
|
||||||
|
setUserData(user);
|
||||||
|
})
|
||||||
|
.catch((error) => {});
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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">
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,8 @@ export default function PaginatorComponent({
|
||||||
}: PaginatorComponentType) {
|
}: PaginatorComponentType) {
|
||||||
const [size, setPageSize] = useState(pageSize);
|
const [size, setPageSize] = useState(pageSize);
|
||||||
const [index, setPageIndex] = useState(pageIndex);
|
const [index, setPageIndex] = useState(pageIndex);
|
||||||
const [maxIndex, setMaxPageIndex] = useState(100);
|
const [maxIndex, setMaxPageIndex] = useState(Math.ceil(totalRowsCount/pageSize));
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
@ -31,7 +32,7 @@ export default function PaginatorComponent({
|
||||||
<Select
|
<Select
|
||||||
onValueChange={(pageSize: string) => {
|
onValueChange={(pageSize: string) => {
|
||||||
setPageSize(Number(pageSize));
|
setPageSize(Number(pageSize));
|
||||||
setMaxPageIndex(100);
|
setMaxPageIndex(Math.ceil(totalRowsCount/Number(pageSize)))
|
||||||
paginate(Number(pageSize), 0);
|
paginate(Number(pageSize), 0);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
@ -48,7 +49,7 @@ export default function PaginatorComponent({
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-[100px] items-center justify-center text-sm font-medium">
|
<div className="flex w-[100px] items-center justify-center text-sm font-medium">
|
||||||
Page {index + 1} of {maxIndex}
|
Page {currentPage} of {maxIndex}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -57,18 +58,25 @@ export default function PaginatorComponent({
|
||||||
className="hidden h-8 w-8 p-0 lg:flex"
|
className="hidden h-8 w-8 p-0 lg:flex"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setPageIndex(0);
|
setPageIndex(0);
|
||||||
|
setCurrentPage(1);
|
||||||
paginate(size, 0);
|
paginate(size, 0);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="sr-only">Go to first page</span>
|
<span className="sr-only">Go to first page</span>
|
||||||
<IconComponent name="ChevronsLeft" className="h-4 w-4" />
|
<IconComponent name="ChevronsLeft" className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
disabled={index <= 0}
|
disabled={index <= 0}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (index > 0) {
|
if (index > 0) {
|
||||||
setPageIndex(index - 1);
|
const pgIndex = size - index;
|
||||||
paginate(size, index - 1);
|
setCurrentPage(currentPage-1);
|
||||||
|
setPageIndex(pgIndex);
|
||||||
|
paginate(size, pgIndex);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
|
@ -77,11 +85,17 @@ export default function PaginatorComponent({
|
||||||
<span className="sr-only">Go to previous page</span>
|
<span className="sr-only">Go to previous page</span>
|
||||||
<IconComponent name="ChevronLeft" className="h-4 w-4" />
|
<IconComponent name="ChevronLeft" className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
disabled={maxIndex === index}
|
disabled={currentPage === maxIndex}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setPageIndex(index + 1);
|
const pgIndex = size + index;
|
||||||
paginate(size, index + 1);
|
setPageIndex(pgIndex);
|
||||||
|
setCurrentPage(currentPage+1);
|
||||||
|
paginate(size, pgIndex);
|
||||||
}}
|
}}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="h-8 w-8 p-0"
|
className="h-8 w-8 p-0"
|
||||||
|
|
@ -89,13 +103,18 @@ export default function PaginatorComponent({
|
||||||
<span className="sr-only">Go to next page</span>
|
<span className="sr-only">Go to next page</span>
|
||||||
<IconComponent name="ChevronRight" className="h-4 w-4" />
|
<IconComponent name="ChevronRight" className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
disabled={maxIndex === index}
|
disabled={currentPage === maxIndex}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="hidden h-8 w-8 p-0 lg:flex"
|
className="hidden h-8 w-8 p-0 lg:flex"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setPageIndex(maxIndex);
|
setPageIndex(maxIndex-1);
|
||||||
paginate(size, maxIndex);
|
setCurrentPage(maxIndex);
|
||||||
|
paginate(size, size);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="sr-only">Go to last page</span>
|
<span className="sr-only">Go to last page</span>
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,18 @@
|
||||||
import { useContext } from "react";
|
import { useContext } from "react";
|
||||||
import { Navigate } from "react-router-dom";
|
import { Navigate } from "react-router-dom";
|
||||||
import { AuthContext } from "../../contexts/authContext";
|
import { AuthContext } from "../../contexts/authContext";
|
||||||
|
import { LOCALHOST_JWT } from "../../constants/constants";
|
||||||
|
|
||||||
export const ProtectedLoginRoute = ({ children }) => {
|
export const ProtectedLoginRoute = ({ children }) => {
|
||||||
const { getAuthentication } = useContext(AuthContext);
|
const { getAuthentication } = useContext(AuthContext);
|
||||||
|
|
||||||
|
const isLocalHost = window.location.href.includes("localhost");
|
||||||
|
|
||||||
|
if(isLocalHost && LOCALHOST_JWT){
|
||||||
|
window.location.replace("/");
|
||||||
|
return <Navigate to="/" replace />;
|
||||||
|
}
|
||||||
|
|
||||||
if (getAuthentication()) {
|
if (getAuthentication()) {
|
||||||
window.location.replace("/");
|
window.location.replace("/");
|
||||||
return <Navigate to="/" replace />;
|
return <Navigate to="/" replace />;
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { useContext, useEffect, useState } from "react";
|
||||||
import { FaDiscord, FaGithub, FaTwitter } from "react-icons/fa";
|
import { FaDiscord, FaGithub, FaTwitter } from "react-icons/fa";
|
||||||
import { Link, useLocation, useNavigate } from "react-router-dom";
|
import { Link, useLocation, useNavigate } from "react-router-dom";
|
||||||
import AlertDropdown from "../../alerts/alertDropDown";
|
import AlertDropdown from "../../alerts/alertDropDown";
|
||||||
import { USER_PROJECTS_HEADER } from "../../constants/constants";
|
import { LOCALHOST_JWT, USER_PROJECTS_HEADER } from "../../constants/constants";
|
||||||
import { alertContext } from "../../contexts/alertContext";
|
import { alertContext } from "../../contexts/alertContext";
|
||||||
import { AuthContext } from "../../contexts/authContext";
|
import { AuthContext } from "../../contexts/authContext";
|
||||||
import { darkContext } from "../../contexts/darkContext";
|
import { darkContext } from "../../contexts/darkContext";
|
||||||
|
|
@ -22,6 +22,7 @@ export default function Header() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [stars, setStars] = useState(null);
|
const [stars, setStars] = useState(null);
|
||||||
|
const isLocalHost = window.location.href.includes("localhost");
|
||||||
|
|
||||||
// Get and set numbers of stars on header
|
// Get and set numbers of stars on header
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -34,19 +35,21 @@ export default function Header() {
|
||||||
return (
|
return (
|
||||||
<div className="header-arrangement">
|
<div className="header-arrangement">
|
||||||
<div className="header-start-display">
|
<div className="header-start-display">
|
||||||
<Link to="/">
|
<Link to="/">
|
||||||
<span className="ml-4 text-2xl">⛓️</span>
|
<span className="ml-4 text-2xl">⛓️</span>
|
||||||
</Link>
|
</Link>
|
||||||
<Button
|
{!isLocalHost || !LOCALHOST_JWT && (
|
||||||
onClick={() => {
|
<Button
|
||||||
logout();
|
onClick={() => {
|
||||||
navigate("/login");
|
logout();
|
||||||
}}
|
navigate("/login");
|
||||||
variant="outline"
|
}}
|
||||||
className=""
|
variant="outline"
|
||||||
>
|
className=""
|
||||||
Sign out
|
>
|
||||||
</Button>
|
Sign out
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
{flows.findIndex((f) => tabId === f.id) !== -1 && tabId !== "" && (
|
{flows.findIndex((f) => tabId === f.id) !== -1 && tabId !== "" && (
|
||||||
<MenuBar flows={flows} tabId={tabId} />
|
<MenuBar flows={flows} tabId={tabId} />
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ const Checkbox = React.forwardRef<
|
||||||
<CheckboxPrimitive.Indicator
|
<CheckboxPrimitive.Indicator
|
||||||
className={cn("flex items-center justify-center text-current")}
|
className={cn("flex items-center justify-center text-current")}
|
||||||
>
|
>
|
||||||
<IconComponent name="Check" className="h-3 w-3" />
|
<IconComponent name="Check" className="h-4 w-4" />
|
||||||
</CheckboxPrimitive.Indicator>
|
</CheckboxPrimitive.Indicator>
|
||||||
</CheckboxPrimitive.Root>
|
</CheckboxPrimitive.Root>
|
||||||
));
|
));
|
||||||
|
|
|
||||||
|
|
@ -524,7 +524,7 @@ export const CONTROL_LOGIN_STATE = {
|
||||||
export const CONTROL_NEW_USER = {
|
export const CONTROL_NEW_USER = {
|
||||||
username: "",
|
username: "",
|
||||||
password: "",
|
password: "",
|
||||||
is_disabled: false,
|
is_active: false,
|
||||||
is_superuser: false,
|
is_superuser: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -610,3 +610,5 @@ export function tabsArray(codes: string[], method: number) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BASE_URL_API = "http://localhost:7860/";
|
export const BASE_URL_API = "http://localhost:7860/";
|
||||||
|
|
||||||
|
export let LOCALHOST_JWT = true;
|
||||||
|
|
@ -32,13 +32,11 @@ export default function UserManagementModal({
|
||||||
const [password, setPassword] = useState(data?.password ?? "");
|
const [password, setPassword] = useState(data?.password ?? "");
|
||||||
const [username, setUserName] = useState(data?.username ?? "");
|
const [username, setUserName] = useState(data?.username ?? "");
|
||||||
const [confirmPassword, setConfirmPassword] = useState(data?.password ?? "");
|
const [confirmPassword, setConfirmPassword] = useState(data?.password ?? "");
|
||||||
const [isDisabled, setIsDisabled] = useState(data?.is_disabled ?? false);
|
const [isDisabled, setIsDisabled] = useState(data?.is_active ?? false);
|
||||||
const [isSuperUser, setIsSuperUser] = useState(data?.is_superuser ?? false);
|
const [isSuperUser, setIsSuperUser] = useState(data?.is_superuser ?? false);
|
||||||
const [inputState, setInputState] = useState<UserInputType>(CONTROL_NEW_USER);
|
const [inputState, setInputState] = useState<UserInputType>(CONTROL_NEW_USER);
|
||||||
const { userData } = useContext(AuthContext);
|
const { userData } = useContext(AuthContext);
|
||||||
|
|
||||||
console.log(userData);
|
|
||||||
|
|
||||||
function handleInput({
|
function handleInput({
|
||||||
target: { name, value },
|
target: { name, value },
|
||||||
}: inputHandlerEventType): void {
|
}: inputHandlerEventType): void {
|
||||||
|
|
@ -50,7 +48,7 @@ export default function UserManagementModal({
|
||||||
resetForm();
|
resetForm();
|
||||||
} else {
|
} else {
|
||||||
handleInput({ target: { name: "username", value: username } });
|
handleInput({ target: { name: "username", value: username } });
|
||||||
handleInput({ target: { name: "is_disabled", value: isDisabled } });
|
handleInput({ target: { name: "is_active", value: isDisabled } });
|
||||||
handleInput({ target: { name: "is_superuser", value: isSuperUser } });
|
handleInput({ target: { name: "is_superuser", value: isSuperUser } });
|
||||||
}
|
}
|
||||||
}, [open]);
|
}, [open]);
|
||||||
|
|
@ -230,7 +228,7 @@ export default function UserManagementModal({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-8">
|
<div className="flex gap-8">
|
||||||
<Form.Field name="is_disabled">
|
<Form.Field name="is_active">
|
||||||
<div>
|
<div>
|
||||||
<Form.Label className="data-[invalid]:label-invalid mr-3">
|
<Form.Label className="data-[invalid]:label-invalid mr-3">
|
||||||
Disabled
|
Disabled
|
||||||
|
|
@ -239,10 +237,10 @@ export default function UserManagementModal({
|
||||||
<Checkbox
|
<Checkbox
|
||||||
value={isDisabled}
|
value={isDisabled}
|
||||||
checked={isDisabled}
|
checked={isDisabled}
|
||||||
id="is_disabled"
|
id="is_active"
|
||||||
className="relative top-0.5"
|
className="relative top-0.5"
|
||||||
onCheckedChange={(value) => {
|
onCheckedChange={(value) => {
|
||||||
handleInput({ target: { name: "is_disabled", value } });
|
handleInput({ target: { name: "is_active", value } });
|
||||||
setIsDisabled(value);
|
setIsDisabled(value);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { cloneDeep } from "lodash";
|
||||||
import { X } from "lucide-react";
|
import { X } from "lucide-react";
|
||||||
import { useContext, useEffect, useRef, useState } from "react";
|
import { useContext, useEffect, useRef, useState } from "react";
|
||||||
import PaginatorComponent from "../../components/PaginatorComponent";
|
import PaginatorComponent from "../../components/PaginatorComponent";
|
||||||
|
|
@ -15,6 +16,7 @@ import {
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "../../components/ui/table";
|
} from "../../components/ui/table";
|
||||||
import { alertContext } from "../../contexts/alertContext";
|
import { alertContext } from "../../contexts/alertContext";
|
||||||
|
import { AuthContext } from "../../contexts/authContext";
|
||||||
import {
|
import {
|
||||||
addUser,
|
addUser,
|
||||||
deleteUser,
|
deleteUser,
|
||||||
|
|
@ -23,7 +25,6 @@ import {
|
||||||
} from "../../controllers/API";
|
} from "../../controllers/API";
|
||||||
import ConfirmationModal from "../../modals/ConfirmationModal";
|
import ConfirmationModal from "../../modals/ConfirmationModal";
|
||||||
import UserManagementModal from "../../modals/UserManagementModal";
|
import UserManagementModal from "../../modals/UserManagementModal";
|
||||||
import { AuthContext } from "../../contexts/authContext";
|
|
||||||
|
|
||||||
export default function AdminPage() {
|
export default function AdminPage() {
|
||||||
const [inputValue, setInputValue] = useState("");
|
const [inputValue, setInputValue] = useState("");
|
||||||
|
|
@ -49,9 +50,9 @@ export default function AdminPage() {
|
||||||
setLoadingUsers(true);
|
setLoadingUsers(true);
|
||||||
getUsersPage(index, size)
|
getUsersPage(index, size)
|
||||||
.then((users) => {
|
.then((users) => {
|
||||||
setTotalRowsCount(users['total_count']);
|
setTotalRowsCount(users["total_count"]);
|
||||||
userList.current = users['users'];
|
userList.current = users["users"];
|
||||||
setFilterUserList(users['users']);
|
setFilterUserList(users["users"]);
|
||||||
setLoadingUsers(false);
|
setLoadingUsers(false);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
|
@ -63,8 +64,9 @@ export default function AdminPage() {
|
||||||
setLoadingUsers(true);
|
setLoadingUsers(true);
|
||||||
getUsersPage(pageIndex, pageSize)
|
getUsersPage(pageIndex, pageSize)
|
||||||
.then((users) => {
|
.then((users) => {
|
||||||
userList.current = users;
|
setTotalRowsCount(users["total_count"]);
|
||||||
setFilterUserList(users);
|
userList.current = users["users"];
|
||||||
|
setFilterUserList(users["users"]);
|
||||||
setLoadingUsers(false);
|
setLoadingUsers(false);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
|
@ -123,6 +125,45 @@ export default function AdminPage() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleDisableUser(check, userId, user) {
|
||||||
|
const userEdit = cloneDeep(user);
|
||||||
|
userEdit.is_active = !check;
|
||||||
|
|
||||||
|
updateUser(userId, userEdit)
|
||||||
|
.then((res) => {
|
||||||
|
console.log(res);
|
||||||
|
|
||||||
|
resetFilter();
|
||||||
|
setSuccessData({
|
||||||
|
title: "Success! User edited!",
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
setErrorData({
|
||||||
|
title: "Error on edit user",
|
||||||
|
list: [error["response"]["data"]["detail"]],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSuperUserEdit(check, userId, user) {
|
||||||
|
const userEdit = cloneDeep(user);
|
||||||
|
userEdit.is_superuser = !check;
|
||||||
|
updateUser(userId, userEdit)
|
||||||
|
.then((res) => {
|
||||||
|
resetFilter();
|
||||||
|
setSuccessData({
|
||||||
|
title: "Success! User edited!",
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
setErrorData({
|
||||||
|
title: "Error on edit user",
|
||||||
|
list: [error["response"]["data"]["detail"]],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function handleNewUser(user) {
|
function handleNewUser(user) {
|
||||||
addUser(user)
|
addUser(user)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
|
|
@ -140,214 +181,246 @@ export default function AdminPage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
||||||
<>
|
<>
|
||||||
{
|
{userData && (
|
||||||
userData && (
|
|
||||||
|
|
||||||
|
|
||||||
<div className="main-page-panel">
|
<div className="main-page-panel">
|
||||||
<div className="m-auto flex h-full flex-row justify-center">
|
<div className="m-auto flex h-full flex-row justify-center">
|
||||||
<div className="basis-5/6">
|
<div className="basis-5/6">
|
||||||
<div className="m-auto flex h-full flex-col space-y-8 p-8 ">
|
<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 className="flex items-center justify-between space-y-2">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-2xl font-bold tracking-tight">
|
<h2 className="text-2xl font-bold tracking-tight">
|
||||||
Welcome back!
|
Welcome back!
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground">
|
||||||
Here's a list of all users!
|
Here's a list of all users!
|
||||||
</p>
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2"></div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-2"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{userList.current.length === 0 && !loadingUsers && (
|
{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 items-center justify-between">
|
||||||
<h2>There's no users registered :)</h2>
|
<div className="flex flex-1 items-center space-x-2">
|
||||||
</div>
|
<Input
|
||||||
</>
|
value={inputValue}
|
||||||
)}
|
placeholder="Filter users..."
|
||||||
<>
|
className="h-8 w-[150px] lg:w-[250px]"
|
||||||
<div className="flex items-center justify-between">
|
onChange={(e) => handleFilterUsers(e.target.value)}
|
||||||
<div className="flex flex-1 items-center space-x-2">
|
/>
|
||||||
<Input
|
{inputValue.length > 0 && (
|
||||||
value={inputValue}
|
<Button
|
||||||
placeholder="Filter users..."
|
onClick={() => {
|
||||||
className="h-8 w-[150px] lg:w-[250px]"
|
setInputValue("");
|
||||||
onChange={(e) => handleFilterUsers(e.target.value)}
|
setFilterUserList(userList.current);
|
||||||
/>
|
}}
|
||||||
{inputValue.length > 0 && (
|
variant="ghost"
|
||||||
<Button
|
className="h-8 px-2 lg:px-3"
|
||||||
onClick={() => {
|
>
|
||||||
setInputValue("");
|
Reset
|
||||||
setFilterUserList(userList.current);
|
<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);
|
||||||
}}
|
}}
|
||||||
variant="ghost"
|
|
||||||
className="h-8 px-2 lg:px-3"
|
|
||||||
>
|
>
|
||||||
Reset
|
<Button>New User</Button>
|
||||||
<X className="ml-2 h-4 w-4" />
|
</UserManagementModal>
|
||||||
</Button>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
{loadingUsers && (
|
||||||
<UserManagementModal
|
<div>
|
||||||
title="New User"
|
<strong>Loading...</strong>
|
||||||
titleHeader={"Add a new user"}
|
</div>
|
||||||
cancelText="Cancel"
|
)}
|
||||||
confirmationText="Save"
|
<div
|
||||||
icon={"UserPlus2"}
|
className={
|
||||||
onConfirm={(index, user) => {
|
"max-h-[26rem] overflow-scroll overflow-x-hidden rounded-md border-2 bg-muted custom-scroll" +
|
||||||
handleNewUser(user);
|
(loadingUsers ? " border-0" : "")
|
||||||
}}
|
}
|
||||||
>
|
>
|
||||||
<Button>New User</Button>
|
<Table className={"table-fixed bg-muted outline-1"}>
|
||||||
</UserManagementModal>
|
<TableHeader
|
||||||
</div>
|
className={
|
||||||
</div>
|
loadingUsers
|
||||||
{loadingUsers && (
|
? "hidden"
|
||||||
<div>
|
: "table-fixed bg-muted outline-1"
|
||||||
<strong>Loading...</strong>
|
}
|
||||||
</div>
|
>
|
||||||
)}
|
<TableRow>
|
||||||
<div
|
<TableHead className="h-10">Id</TableHead>
|
||||||
className={
|
<TableHead className="h-10">Username</TableHead>
|
||||||
"max-h-[26rem] overflow-scroll overflow-x-hidden rounded-md border-2 bg-muted custom-scroll" +
|
<TableHead className="h-10">Disabled</TableHead>
|
||||||
(loadingUsers ? " border-0" : "")
|
<TableHead className="h-10">Superuser</TableHead>
|
||||||
}
|
<TableHead className="h-10">Created At</TableHead>
|
||||||
>
|
<TableHead className="h-10">Updated At</TableHead>
|
||||||
<Table className={"table-fixed bg-muted outline-1"}>
|
<TableHead className="h-10 w-[100px] text-right"></TableHead>
|
||||||
<TableHeader
|
</TableRow>
|
||||||
className={
|
</TableHeader>
|
||||||
loadingUsers
|
{!loadingUsers && (
|
||||||
? "hidden"
|
<TableBody>
|
||||||
: "table-fixed bg-muted outline-1"
|
{filterUserList.map((user, index) => (
|
||||||
}
|
<TableRow key={index}>
|
||||||
>
|
<TableCell className="truncate py-2 font-medium">
|
||||||
<TableRow>
|
<ShadTooltip content={user.id}>
|
||||||
<TableHead className="h-10">Id</TableHead>
|
<span className="cursor-default">
|
||||||
<TableHead className="h-10">Username</TableHead>
|
{user.id}
|
||||||
<TableHead className="h-10">Disabled</TableHead>
|
</span>
|
||||||
<TableHead className="h-10">Superuser</TableHead>
|
</ShadTooltip>
|
||||||
<TableHead className="h-10">Created At</TableHead>
|
</TableCell>
|
||||||
<TableHead className="h-10">Updated At</TableHead>
|
<TableCell className="truncate py-2">
|
||||||
<TableHead className="h-10 w-[100px] text-right"></TableHead>
|
<ShadTooltip content={user.username}>
|
||||||
</TableRow>
|
<span className="cursor-default">
|
||||||
</TableHeader>
|
{user.username}
|
||||||
{!loadingUsers && (
|
</span>
|
||||||
<TableBody>
|
</ShadTooltip>
|
||||||
{filterUserList.map((user, index) => (
|
</TableCell>
|
||||||
<TableRow key={index}>
|
<TableCell className="relative left-5 truncate py-2 text-align-last-left">
|
||||||
<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">
|
|
||||||
<Checkbox
|
|
||||||
id="is_disabled"
|
|
||||||
checked={user.is_disabled}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="relative left-5 truncate py-2">
|
|
||||||
<Checkbox
|
|
||||||
id="is_superuser"
|
|
||||||
checked={user.is_superuser}
|
|
||||||
/>
|
|
||||||
</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="Edit"
|
|
||||||
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
|
<ConfirmationModal
|
||||||
title="Delete"
|
title="Edit"
|
||||||
titleHeader="Delete User"
|
titleHeader={`${user.username}`}
|
||||||
modalContentTitle="Attention!"
|
modalContentTitle="Attention!"
|
||||||
modalContent="Are you sure you want to delete this user? This action cannot be undone."
|
modalContent="Are you completely confident about the changes you are making to this user?"
|
||||||
cancelText="Cancel"
|
cancelText="Cancel"
|
||||||
confirmationText="Delete"
|
confirmationText="Confirm"
|
||||||
icon={"UserMinus2"}
|
icon={"UserCog2"}
|
||||||
data={user}
|
data={user}
|
||||||
index={index}
|
index={index}
|
||||||
onConfirm={(index, user) => {
|
onConfirm={(index, user) => {
|
||||||
handleDeleteUser(user);
|
handleDisableUser(
|
||||||
|
user.is_superuser,
|
||||||
|
user.id,
|
||||||
|
user
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ShadTooltip content="Delete" side="top">
|
<Checkbox
|
||||||
<IconComponent
|
id="is_active"
|
||||||
name="Trash2"
|
checked={user.is_active}
|
||||||
className="ml-2 h-4 w-4 cursor-pointer"
|
/>
|
||||||
/>
|
|
||||||
</ShadTooltip>
|
|
||||||
</ConfirmationModal>
|
</ConfirmationModal>
|
||||||
</div>
|
</TableCell>
|
||||||
</TableCell>
|
<TableCell className="relative left-5 truncate py-2 text-align-last-left">
|
||||||
</TableRow>
|
<ConfirmationModal
|
||||||
))}
|
title="Edit"
|
||||||
</TableBody>
|
titleHeader={`${user.username}`}
|
||||||
)}
|
modalContentTitle="Attention!"
|
||||||
</Table>
|
modalContent="Are you completely confident about the changes you are making to this user?"
|
||||||
</div>
|
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="Edit"
|
||||||
|
icon={"UserCog2"}
|
||||||
|
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>
|
||||||
|
|
||||||
<PaginatorComponent
|
<ConfirmationModal
|
||||||
pageIndex={index}
|
title="Delete"
|
||||||
pageSize={size}
|
titleHeader="Delete User"
|
||||||
totalRowsCount={filterUserList.length}
|
modalContentTitle="Attention!"
|
||||||
paginate={(pageIndex, pageSize) => {
|
modalContent="Are you sure you want to delete this user? This action cannot be undone."
|
||||||
handleChangePagination(pageSize, pageIndex);
|
cancelText="Cancel"
|
||||||
}}
|
confirmationText="Delete"
|
||||||
></PaginatorComponent>
|
icon={"UserMinus2"}
|
||||||
</>
|
data={user}
|
||||||
|
index={index}
|
||||||
|
onConfirm={(index, user) => {
|
||||||
|
handleDeleteUser(user);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ShadTooltip content="Delete" side="top">
|
||||||
|
<IconComponent
|
||||||
|
name="Trash2"
|
||||||
|
className="ml-2 h-4 w-4 cursor-pointer"
|
||||||
|
/>
|
||||||
|
</ShadTooltip>
|
||||||
|
</ConfirmationModal>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
)}
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<PaginatorComponent
|
||||||
|
pageIndex={index}
|
||||||
|
pageSize={size}
|
||||||
|
totalRowsCount={totalRowsCount}
|
||||||
|
paginate={(pageIndex, pageSize) => {
|
||||||
|
handleChangePagination(pageSize, pageIndex);
|
||||||
|
}}
|
||||||
|
></PaginatorComponent>
|
||||||
|
</>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import * as Form from "@radix-ui/react-form";
|
import * as Form from "@radix-ui/react-form";
|
||||||
import { useContext, useState } from "react";
|
import { useContext, useEffect, useState } from "react";
|
||||||
import { Link, useNavigate } from "react-router-dom";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
import InputComponent from "../../components/inputComponent";
|
import InputComponent from "../../components/inputComponent";
|
||||||
import { Button } from "../../components/ui/button";
|
import { Button } from "../../components/ui/button";
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,7 @@ export type LoginAuthType = {
|
||||||
export type Users = {
|
export type Users = {
|
||||||
id: string;
|
id: string;
|
||||||
username: string;
|
username: string;
|
||||||
is_disabled: boolean;
|
is_active: boolean;
|
||||||
is_superuser: boolean;
|
is_superuser: boolean;
|
||||||
create_at: Date;
|
create_at: Date;
|
||||||
updated_at: Date;
|
updated_at: Date;
|
||||||
|
|
|
||||||
|
|
@ -230,6 +230,6 @@ export type loginInputStateType = {
|
||||||
export type UserInputType = {
|
export type UserInputType = {
|
||||||
username: string;
|
username: string;
|
||||||
password: string;
|
password: string;
|
||||||
is_disabled: boolean;
|
is_active: boolean;
|
||||||
is_superuser: boolean;
|
is_superuser: boolean;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,7 @@ import {
|
||||||
Trash2,
|
Trash2,
|
||||||
Undo,
|
Undo,
|
||||||
Upload,
|
Upload,
|
||||||
|
UserCog2,
|
||||||
UserMinus2,
|
UserMinus2,
|
||||||
UserPlus2,
|
UserPlus2,
|
||||||
Users2,
|
Users2,
|
||||||
|
|
@ -292,4 +293,5 @@ export const nodeIconsLucide = {
|
||||||
FaApple,
|
FaApple,
|
||||||
EyeOff,
|
EyeOff,
|
||||||
Eye,
|
Eye,
|
||||||
|
UserCog2
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -201,6 +201,12 @@ module.exports = {
|
||||||
".dark .theme-attribution .react-flow__attribution a": {
|
".dark .theme-attribution .react-flow__attribution a": {
|
||||||
color: "black",
|
color: "black",
|
||||||
},
|
},
|
||||||
|
".text-align-last-left": {
|
||||||
|
"text-align-last": "left",
|
||||||
|
},
|
||||||
|
".text-align-last-right": {
|
||||||
|
"text-align-last": "right",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
require("@tailwindcss/typography"),
|
require("@tailwindcss/typography"),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue