From ba48bbe770e9642765a9471b43bf6dd44df8a2f5 Mon Sep 17 00:00:00 2001 From: Cristhian Zanforlin Lousa Date: Wed, 16 Aug 2023 16:38:13 -0300 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(App.tsx):=20remove=20unneces?= =?UTF-8?q?sary=20code=20block=20in=20useEffect=20=F0=9F=94=A7=20chore(con?= =?UTF-8?q?stants.ts):=20add=20CONTROL=5FNEW=5FAPI=5FKEY=20constant=20for?= =?UTF-8?q?=20consistency=20=E2=9C=A8=20feat(SecretKeyModal):=20add=20Secr?= =?UTF-8?q?etKeyModal=20component=20to=20handle=20secret=20key=20generatio?= =?UTF-8?q?n=20and=20copying=20=F0=9F=94=A7=20chore(UserManagementModal):?= =?UTF-8?q?=20rearrange=20buttons=20in=20UserManagementModal=20for=20bette?= =?UTF-8?q?r=20user=20experience?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🚀 feat(ApiKeysPage): add new page for managing API keys 🔧 chore(routes.tsx): add route for ApiKeysPage 🔧 chore(types): add ApiKeyType and ApiKeyInputType to improve type safety and readability of code 🔧 chore(utils): add Key icon from lucide-react to nodeIconsLucide to be used in styling --- src/frontend/src/App.tsx | 3 - src/frontend/src/constants/constants.ts | 4 + .../src/modals/SecretKeyModal/index.tsx | 191 ++++++++++ .../src/modals/UserManagementModal/index.tsx | 11 +- src/frontend/src/pages/ApiKeysPage/index.tsx | 349 ++++++++++++++++++ src/frontend/src/routes.tsx | 9 + src/frontend/src/types/components/index.ts | 15 + src/frontend/src/utils/styleUtils.ts | 4 +- 8 files changed, 578 insertions(+), 8 deletions(-) create mode 100644 src/frontend/src/modals/SecretKeyModal/index.tsx create mode 100644 src/frontend/src/pages/ApiKeysPage/index.tsx diff --git a/src/frontend/src/App.tsx b/src/frontend/src/App.tsx index 99901e3a0..25b21020c 100644 --- a/src/frontend/src/App.tsx +++ b/src/frontend/src/App.tsx @@ -150,9 +150,6 @@ export default function App() { }) .catch((error) => {}); } - else{ - navigate("/login"); - } }); }, 500); }, []); diff --git a/src/frontend/src/constants/constants.ts b/src/frontend/src/constants/constants.ts index a8881d7a5..b43954067 100644 --- a/src/frontend/src/constants/constants.ts +++ b/src/frontend/src/constants/constants.ts @@ -528,6 +528,10 @@ export const CONTROL_NEW_USER = { is_superuser: false, }; +export const CONTROL_NEW_API_KEY = { + apikeyname: "", +}; + export const tabsCode = []; export function tabsArray(codes: string[], method: number) { diff --git a/src/frontend/src/modals/SecretKeyModal/index.tsx b/src/frontend/src/modals/SecretKeyModal/index.tsx new file mode 100644 index 000000000..a5a3143f6 --- /dev/null +++ b/src/frontend/src/modals/SecretKeyModal/index.tsx @@ -0,0 +1,191 @@ +import * as Form from "@radix-ui/react-form"; +import { useContext, useEffect, useRef, useState } from "react"; +import IconComponent from "../../components/genericIconComponent"; +import { Button } from "../../components/ui/button"; +import { Input } from "../../components/ui/input"; +import { CONTROL_NEW_API_KEY } from "../../constants/constants"; +import { alertContext } from "../../contexts/alertContext"; +import { + ApiKeyInputType, + ApiKeyType, + inputHandlerEventType, +} from "../../types/components"; +import { nodeIconsLucide } from "../../utils/styleUtils"; +import BaseModal from "../baseModal"; + +export default function SecretKeyModal({ + title, + cancelText, + confirmationText, + children, + icon, + data, + index, + onConfirm, +}: ApiKeyType) { + const Icon: any = nodeIconsLucide[icon]; + const [open, setOpen] = useState(false); + const [apiKeyName, setApiKeyName] = useState(data?.apikeyname ?? ""); + const [apiKeyValue, setApiKeyValue] = useState("Value"); + const [inputState, setInputState] = + useState(CONTROL_NEW_API_KEY); + const [renderKey, setRenderKey] = useState(false); + const [textCopied, setTextCopied] = useState(true); + const { setSuccessData } = useContext(alertContext); + const inputRef = useRef(null); + + function handleInput({ + target: { name, value }, + }: inputHandlerEventType): void { + setInputState((prev) => ({ ...prev, [name]: value })); + } + + useEffect(() => { + if (open) { + setRenderKey(false); + resetForm(); + } + }, [open]); + + function resetForm() { + setApiKeyName(""); + setApiKeyValue("Value"); + } + + const handleCopyClick = async () => { + if (apiKeyValue) { + await navigator.clipboard.writeText(apiKeyValue); + inputRef.current.focus(); + inputRef.current.select(); + setSuccessData({ + title: "API Key copied!", + }); + setTextCopied(false); + + setTimeout(() => { + setTextCopied(true); + }, 3000); + } + }; + + return ( + + {children} + + {title} + + + {renderKey === true && ( + <> + + Please save this secret key somewhere safe and accessible. For + security reasons,{" "} + you won't be able to view it again through your + account. If you lose this secret key, you'll need to generate a + new one. + +
+
+ { + setApiKeyValue(event.target.value); + }} + readOnly={true} + value={apiKeyValue} + /> +
+ +
+ +
+
+ + )} + + { + setRenderKey(true); + event.preventDefault(); + }} + > + {renderKey === false && ( +
+ +
+ + Name (optional){" "} + +
+ + { + handleInput({ target: { name: "apikeyname", value } }); + setApiKeyName(value); + }} + value={apiKeyName} + className="primary-input" + placeholder="My key name" + /> + +
+
+ )} + {renderKey === false && ( +
+ + + + + +
+ )} + + {renderKey === true && ( +
+ +
+ )} +
+
+
+ ); +} diff --git a/src/frontend/src/modals/UserManagementModal/index.tsx b/src/frontend/src/modals/UserManagementModal/index.tsx index d370ef841..993ffb944 100644 --- a/src/frontend/src/modals/UserManagementModal/index.tsx +++ b/src/frontend/src/modals/UserManagementModal/index.tsx @@ -273,17 +273,20 @@ export default function UserManagementModal({
- - - - + + + + +
diff --git a/src/frontend/src/pages/ApiKeysPage/index.tsx b/src/frontend/src/pages/ApiKeysPage/index.tsx new file mode 100644 index 000000000..a08e155f4 --- /dev/null +++ b/src/frontend/src/pages/ApiKeysPage/index.tsx @@ -0,0 +1,349 @@ +import { cloneDeep } from "lodash"; +import { useContext, useEffect, useRef, useState } from "react"; +import ShadTooltip from "../../components/ShadTooltipComponent"; +import IconComponent from "../../components/genericIconComponent"; +import { Button } from "../../components/ui/button"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "../../components/ui/table"; +import { alertContext } from "../../contexts/alertContext"; +import { AuthContext } from "../../contexts/authContext"; +import { + addUser, + deleteUser, + getUsersPage, + updateUser, +} from "../../controllers/API"; +import ConfirmationModal from "../../modals/ConfirmationModal"; +import SecretKeyModal from "../../modals/SecretKeyModal"; +import { UserInputType } from "../../types/components"; + +export default function ApiKeysPage() { + const [inputValue, setInputValue] = useState(""); + + const [size, setPageSize] = useState(10); + const [index, setPageIndex] = useState(0); + const [loadingUsers, setLoadingUsers] = useState(true); + const { setErrorData, setSuccessData } = useContext(alertContext); + const { userData } = useContext(AuthContext); + const [totalRowsCount, setTotalRowsCount] = useState(0); + + const userList = useRef([]); + + useEffect(() => { + setTimeout(() => { + getUsers(); + }, 500); + }, []); + + const [filterUserList, setFilterUserList] = useState(userList.current); + + function getUsers() { + setLoadingUsers(true); + getUsersPage(index, size) + .then((users) => { + setTotalRowsCount(users["total_count"]); + userList.current = users["users"]; + setFilterUserList(users["users"]); + setLoadingUsers(false); + }) + .catch((error) => { + setLoadingUsers(false); + }); + } + + function handleChangePagination(pageIndex: number, pageSize: number) { + setLoadingUsers(true); + getUsersPage(pageIndex, pageSize) + .then((users) => { + setTotalRowsCount(users["total_count"]); + userList.current = users["users"]; + setFilterUserList(users["users"]); + setLoadingUsers(false); + }) + .catch((error) => { + setLoadingUsers(false); + }); + } + + function resetFilter() { + setPageIndex(0); + setPageSize(10); + getUsers(); + } + + function handleFilterUsers(input: string) { + setInputValue(input); + + if (input === "") { + setFilterUserList(userList.current); + } else { + const filteredList = userList.current.filter((user) => + user.username.toLowerCase().includes(input.toLowerCase()) + ); + setFilterUserList(filteredList); + } + } + + function handleDeleteUser(user) { + deleteUser(user.id) + .then((res) => { + resetFilter(); + setSuccessData({ + title: "Success! User deleted!", + }); + }) + .catch((error) => { + setErrorData({ + title: "Error on delete user", + list: [error["response"]["data"]["detail"]], + }); + }); + } + + function handleEditUser(userId, user) { + updateUser(userId, user) + .then((res) => { + resetFilter(); + setSuccessData({ + title: "Success! User edited!", + }); + }) + .catch((error) => { + setErrorData({ + title: "Error on edit user", + list: [error["response"]["data"]["detail"]], + }); + }); + } + + 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: UserInputType) { + addUser(user) + .then((res) => { + resetFilter(); + setSuccessData({ + title: "Success! New user added!", + }); + }) + .catch((error) => { + setErrorData({ + title: "Error on add new user", + list: [error["response"]["data"]["detail"]], + }); + }); + } + + function lastUsedMessage() { + return ( +
+ + The last time this key was used.

Accurate to within the hour + from the most recent usage. +
+
+ ); + } + + return ( + <> + {userData && ( +
+
+
+
+
+
+

+ API keys +

+

+ Your secret API keys are listed below. Please note that we + do not display your secret API keys again after you + generate them.

+ Do not share your API key with others, or expose it in the + browser or other client-side code. +

+
+
+
+ + {userList.current.length === 0 && !loadingUsers && ( + <> +
+

There's no users registered :)

+
+ + )} + <> + {loadingUsers && ( +
+ Loading... +
+ )} +
+ + + + Name + Key + Created + + Last Used + +
+ +
+
+
+ +
+
+ {!loadingUsers && ( + + {filterUserList.map((user, index) => ( + + + + + {user.id} + + + + + + + {user.username} + + + + + { + new Date(user.create_at) + .toISOString() + .split("T")[0] + } + + + { + new Date(user.updated_at) + .toISOString() + .split("T")[0] + } + + +
+ { + handleDeleteUser(user); + }} + > + + + + +
+
+
+ ))} +
+ )} +
+
+ +
+
+ { + handleNewUser(user); + }} + > + + +
+
+ +
+
+
+
+ )} + + ); +} diff --git a/src/frontend/src/routes.tsx b/src/frontend/src/routes.tsx index 81421d057..88d485155 100644 --- a/src/frontend/src/routes.tsx +++ b/src/frontend/src/routes.tsx @@ -11,6 +11,7 @@ import HomePage from "./pages/MainPage"; import DeleteAccountPage from "./pages/deleteAccountPage"; import LoginPage from "./pages/loginPage"; import SignUp from "./pages/signUpPage"; +import ApiKeysPage from "./pages/ApiKeysPage"; const Router = () => { return ( @@ -93,6 +94,14 @@ const Router = () => { } > + + + + } + > ); diff --git a/src/frontend/src/types/components/index.ts b/src/frontend/src/types/components/index.ts index ddac47d9f..053eb50df 100644 --- a/src/frontend/src/types/components/index.ts +++ b/src/frontend/src/types/components/index.ts @@ -233,3 +233,18 @@ export type UserInputType = { is_active?: boolean; is_superuser?: boolean; }; + +export type ApiKeyType = { + title: string; + cancelText: string; + confirmationText: string; + children: ReactElement; + icon: string; + data?: any; + index?: number; + onConfirm: (index, data) => void; +}; + +export type ApiKeyInputType = { + apikeyname: string; +}; \ No newline at end of file diff --git a/src/frontend/src/utils/styleUtils.ts b/src/frontend/src/utils/styleUtils.ts index 9f4ebe21d..0f82635f6 100644 --- a/src/frontend/src/utils/styleUtils.ts +++ b/src/frontend/src/utils/styleUtils.ts @@ -72,6 +72,7 @@ import { X, XCircle, Zap, + Key } from "lucide-react"; import { FaApple, FaGithub } from "react-icons/fa"; import { AirbyteIcon } from "../icons/Airbyte"; @@ -294,5 +295,6 @@ export const nodeIconsLucide = { FaApple, EyeOff, Eye, - UserCog2 + UserCog2, + Key };