From 938731ce2c320a5198e05fdae63652fcc23c30b1 Mon Sep 17 00:00:00 2001 From: Chen Wang Date: Fri, 1 Dec 2023 15:13:00 -0600 Subject: [PATCH 01/17] add admin to the right model so it's reflect in the return object --- backend/app/models/users.py | 2 +- frontend/src/components/Layout.tsx | 20 ++++++- .../{ApiKeys => apikeys}/ApiKey.tsx | 0 .../CreateApiKeyModal.tsx | 0 frontend/src/components/users/ManageUsers.tsx | 57 +++++++++++++++++++ frontend/src/components/users/Profile.tsx | 4 +- frontend/src/openapi/v2/models/UserIn.ts | 1 + frontend/src/openapi/v2/models/UserOut.ts | 11 ++-- frontend/src/reducers/user.ts | 3 +- frontend/src/routes.tsx | 9 +++ 10 files changed, 98 insertions(+), 9 deletions(-) rename frontend/src/components/{ApiKeys => apikeys}/ApiKey.tsx (100%) rename frontend/src/components/{ApiKeys => apikeys}/CreateApiKeyModal.tsx (100%) create mode 100644 frontend/src/components/users/ManageUsers.tsx diff --git a/backend/app/models/users.py b/backend/app/models/users.py index beee19b36..4d29fb4d3 100644 --- a/backend/app/models/users.py +++ b/backend/app/models/users.py @@ -12,6 +12,7 @@ class UserBase(BaseModel): email: EmailStr first_name: str last_name: str + admin: bool class UserIn(UserBase): @@ -31,7 +32,6 @@ class Settings: class UserDB(UserDoc): hashed_password: str = Field() keycloak_id: Optional[str] = None - admin: bool def verify_password(self, password): return pwd_context.verify(password, self.hashed_password) diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index f7e2887ed..91d1b8750 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -18,7 +18,7 @@ import ListItemIcon from "@mui/material/ListItemIcon"; import ListItemText from "@mui/material/ListItemText"; import { Link, Menu, MenuItem, MenuList, Typography } from "@mui/material"; import { Link as RouterLink, useLocation } from "react-router-dom"; -import { useSelector } from "react-redux"; +import { useDispatch, useSelector } from "react-redux"; import { RootState } from "../types/data"; import { AddBox, Explore } from "@material-ui/icons"; import HistoryIcon from "@mui/icons-material/History"; @@ -30,6 +30,8 @@ import { getCurrEmail } from "../utils/common"; import VpnKeyIcon from "@mui/icons-material/VpnKey"; import LogoutIcon from "@mui/icons-material/Logout"; import { EmbeddedSearch } from "./search/EmbeddedSearch"; +import ManageAccountsIcon from "@mui/icons-material/ManageAccounts"; +import { fetchUserProfile } from "../actions/user"; const drawerWidth = 240; @@ -103,6 +105,10 @@ export default function PersistentDrawerLeft(props) { const [embeddedSearchHidden, setEmbeddedSearchHidden] = React.useState(false); const [anchorEl, setAnchorEl] = React.useState(null); const isMenuOpen = Boolean(anchorEl); + const user = useSelector((state: RootState) => state.user); + const profile = user["profile"]; + const dispatch = useDispatch(); + const fetchProfile = () => dispatch(fetchUserProfile()); const handleDrawerOpen = () => { setOpen(true); @@ -128,6 +134,7 @@ export default function PersistentDrawerLeft(props) { } else { setEmbeddedSearchHidden(false); } + fetchProfile(); }, [location]); const loggedOut = useSelector((state: RootState) => state.error.loggedOut); @@ -217,6 +224,17 @@ export default function PersistentDrawerLeft(props) { User Profile + {profile.admin ? ( + <> + + + + + Manage Users + + + + ) : null} diff --git a/frontend/src/components/ApiKeys/ApiKey.tsx b/frontend/src/components/apikeys/ApiKey.tsx similarity index 100% rename from frontend/src/components/ApiKeys/ApiKey.tsx rename to frontend/src/components/apikeys/ApiKey.tsx diff --git a/frontend/src/components/ApiKeys/CreateApiKeyModal.tsx b/frontend/src/components/apikeys/CreateApiKeyModal.tsx similarity index 100% rename from frontend/src/components/ApiKeys/CreateApiKeyModal.tsx rename to frontend/src/components/apikeys/CreateApiKeyModal.tsx diff --git a/frontend/src/components/users/ManageUsers.tsx b/frontend/src/components/users/ManageUsers.tsx new file mode 100644 index 000000000..1ccfd215f --- /dev/null +++ b/frontend/src/components/users/ManageUsers.tsx @@ -0,0 +1,57 @@ +import React, { useEffect } from "react"; +import { RootState } from "../../types/data"; +import { useDispatch, useSelector } from "react-redux"; +import Layout from "../Layout"; +import Table from "@mui/material/Table"; +import TableBody from "@mui/material/TableBody"; +import TableCell from "@mui/material/TableCell"; +import TableContainer from "@mui/material/TableContainer"; +import TableHead from "@mui/material/TableHead"; +import TableRow from "@mui/material/TableRow"; +import Paper from "@mui/material/Paper"; +import { fetchUserProfile } from "../../actions/user"; + +export const ManageUsers = (): JSX.Element => { + const dispatch = useDispatch(); + const user = useSelector((state: RootState) => state.user); + const profile = user["profile"]; + const fetchProfile = () => dispatch(fetchUserProfile()); + // component did mount + useEffect(() => { + fetchProfile(); + }, []); + + if (profile != null) { + return ( + + + + + + Name + Email + Admin + + + + + + + {profile.first_name} {profile.last_name} + + {profile.email} + + {profile.admin ? "True" : "False"} + + + +
+
+
+ ); + } else { + return

nothing yet

; + } +}; diff --git a/frontend/src/components/users/Profile.tsx b/frontend/src/components/users/Profile.tsx index ebed22427..002fec4f0 100644 --- a/frontend/src/components/users/Profile.tsx +++ b/frontend/src/components/users/Profile.tsx @@ -42,7 +42,9 @@ export const Profile = (): JSX.Element => { {profile.first_name} {profile.last_name} {profile.email} - {"false"} + + {profile.admin ? "True" : "False"} + diff --git a/frontend/src/openapi/v2/models/UserIn.ts b/frontend/src/openapi/v2/models/UserIn.ts index f7e7cbfa4..2796f1e9f 100644 --- a/frontend/src/openapi/v2/models/UserIn.ts +++ b/frontend/src/openapi/v2/models/UserIn.ts @@ -6,5 +6,6 @@ export type UserIn = { email: string; first_name: string; last_name: string; + admin: boolean; password: string; } diff --git a/frontend/src/openapi/v2/models/UserOut.ts b/frontend/src/openapi/v2/models/UserOut.ts index 42c7657d5..f11812942 100644 --- a/frontend/src/openapi/v2/models/UserOut.ts +++ b/frontend/src/openapi/v2/models/UserOut.ts @@ -16,8 +16,9 @@ * - [UpdateMethods](https://roman-right.github.io/beanie/api/interfaces/#aggregatemethods) */ export type UserOut = { - email: string; - first_name: string; - last_name: string; - id?: string; -} + email: string; + first_name: string; + last_name: string; + admin: boolean; + id?: string; +}; diff --git a/frontend/src/reducers/user.ts b/frontend/src/reducers/user.ts index 2a83e5984..badc97424 100644 --- a/frontend/src/reducers/user.ts +++ b/frontend/src/reducers/user.ts @@ -11,6 +11,7 @@ import { } from "../actions/user"; import { UserState } from "../types/data"; import { DataAction } from "../types/action"; +import { UserOut } from "../openapi/v2"; const defaultState: UserState = { Authorization: null, @@ -19,7 +20,7 @@ const defaultState: UserState = { errorMsg: "", hashedKey: "", apiKeys: [], - profile: null, + profile: {}, }; const user = (state = defaultState, action: DataAction) => { diff --git a/frontend/src/routes.tsx b/frontend/src/routes.tsx index 2cdd433ef..ee1bb9d45 100644 --- a/frontend/src/routes.tsx +++ b/frontend/src/routes.tsx @@ -29,6 +29,7 @@ import { PageNotFound } from "./components/errors/PageNotFound"; import { Forbidden } from "./components/errors/Forbidden"; import { ApiKeys } from "./components/ApiKeys/ApiKey"; import { Profile } from "./components/users/Profile"; +import { ManageUsers } from "./components/users/ManageUsers"; import config from "./app.config"; import { MetadataDefinitions } from "./components/metadata/MetadataDefinitions"; import { MetadataDefinitionEntry } from "./components/metadata/MetadataDefinitionEntry"; @@ -114,6 +115,14 @@ export const AppRoutes = (): JSX.Element => { } /> + + + + } + /> Date: Fri, 1 Dec 2023 15:44:37 -0600 Subject: [PATCH 02/17] populate manage users page --- frontend/src/components/users/ManageUsers.tsx | 74 +++++++++---------- frontend/src/components/users/Profile.tsx | 64 ++++++++-------- 2 files changed, 65 insertions(+), 73 deletions(-) diff --git a/frontend/src/components/users/ManageUsers.tsx b/frontend/src/components/users/ManageUsers.tsx index 1ccfd215f..dacfd2654 100644 --- a/frontend/src/components/users/ManageUsers.tsx +++ b/frontend/src/components/users/ManageUsers.tsx @@ -9,49 +9,47 @@ import TableContainer from "@mui/material/TableContainer"; import TableHead from "@mui/material/TableHead"; import TableRow from "@mui/material/TableRow"; import Paper from "@mui/material/Paper"; -import { fetchUserProfile } from "../../actions/user"; +import { fetchAllUsers as fetchAllUsersAction } from "../../actions/user"; export const ManageUsers = (): JSX.Element => { const dispatch = useDispatch(); - const user = useSelector((state: RootState) => state.user); - const profile = user["profile"]; - const fetchProfile = () => dispatch(fetchUserProfile()); + const users = useSelector((state: RootState) => state.group.users); + const fetchAllUsers = () => dispatch(fetchAllUsersAction()); // component did mount useEffect(() => { - fetchProfile(); + fetchAllUsers(); }, []); - if (profile != null) { - return ( - - - - - - Name - Email - Admin - - - - - - - {profile.first_name} {profile.last_name} - - {profile.email} - - {profile.admin ? "True" : "False"} - - - -
-
-
- ); - } else { - return

nothing yet

; - } + return ( + + + + + + Name + Email + Admin + + + + {users.map((profile) => { + return ( + + + {profile.first_name} {profile.last_name} + + {profile.email} + + {profile.admin ? "True" : "False"} + + + ); + })} + +
+
+
+ ); }; diff --git a/frontend/src/components/users/Profile.tsx b/frontend/src/components/users/Profile.tsx index 002fec4f0..edcc177ec 100644 --- a/frontend/src/components/users/Profile.tsx +++ b/frontend/src/components/users/Profile.tsx @@ -13,45 +13,39 @@ import { fetchUserProfile } from "../../actions/user"; export const Profile = (): JSX.Element => { const dispatch = useDispatch(); - const user = useSelector((state: RootState) => state.user); - const profile = user["profile"]; + const profile = useSelector((state: RootState) => state.user.profile); const fetchProfile = () => dispatch(fetchUserProfile()); // component did mount useEffect(() => { fetchProfile(); }, []); - if (profile != null) { - return ( - - - - - - Name - Email - Admin - - - - - - - {profile.first_name} {profile.last_name} - - {profile.email} - - {profile.admin ? "True" : "False"} - - - -
-
-
- ); - } else { - return

nothing yet

; - } + return ( + + + + + + Name + Email + Admin + + + + + + {profile.first_name} {profile.last_name} + + {profile.email} + + {profile.admin ? "True" : "False"} + + + +
+
+
+ ); }; From 457c0e213c9ea0b6c95d749a0d0020fcb173bdfc Mon Sep 17 00:00:00 2001 From: Chen Wang Date: Fri, 1 Dec 2023 15:53:10 -0600 Subject: [PATCH 03/17] put admin to correct place; update faker script --- backend/app/models/users.py | 3 ++- .../develop/populate_fake_data/official.csv | 20 +++++++++++++++++++ .../populate_fake_data/populate_fake_data.py | 20 +++++++++---------- 3 files changed, 32 insertions(+), 11 deletions(-) create mode 100644 scripts/develop/populate_fake_data/official.csv diff --git a/backend/app/models/users.py b/backend/app/models/users.py index 4d29fb4d3..fd168d69f 100644 --- a/backend/app/models/users.py +++ b/backend/app/models/users.py @@ -12,7 +12,6 @@ class UserBase(BaseModel): email: EmailStr first_name: str last_name: str - admin: bool class UserIn(UserBase): @@ -25,6 +24,8 @@ class UserLogin(BaseModel): class UserDoc(Document, UserBase): + admin: bool + class Settings: name = "users" diff --git a/scripts/develop/populate_fake_data/official.csv b/scripts/develop/populate_fake_data/official.csv new file mode 100644 index 000000000..e489e2ef3 --- /dev/null +++ b/scripts/develop/populate_fake_data/official.csv @@ -0,0 +1,20 @@ +"Andrew Adams","1528 Smith Valley +Johnsonshire, AR 11008" +"Heidi Coffey","056 Meadows Mountain +Lauramouth, DE 68115" +"Juan Lloyd","934 Kyle Island Apt. 173 +North Samantha, KS 86274" +"Kimberly Johnson","368 Pineda Pines +Richardsonport, AS 40439" +"Mike Griffith","23377 Garza Fields Suite 166 +Christopherburgh, NJ 58724" +"Amy Wright","74031 Michael Court Apt. 521 +New James, WY 99475" +"Monica Nelson","68595 Laura Creek Suite 840 +West Chelseaborough, MH 13306" +"Daniel Parks","9646 Ryan Crescent Apt. 173 +North Cindyborough, GA 95518" +"Brenda Hernandez","307 Stephens Islands Suite 504 +Jillside, CA 38888" +"Jason Hunt","5290 Taylor Trafficway Suite 641 +Ashleystad, TN 93116" diff --git a/scripts/develop/populate_fake_data/populate_fake_data.py b/scripts/develop/populate_fake_data/populate_fake_data.py index 710628fda..8eb323a09 100755 --- a/scripts/develop/populate_fake_data/populate_fake_data.py +++ b/scripts/develop/populate_fake_data/populate_fake_data.py @@ -13,11 +13,11 @@ def upload_file( - api: str, - headers: dict, - dataset_id: str, - filename: str, - content: str, + api: str, + headers: dict, + dataset_id: str, + filename: str, + content: str, ): """Uploads a dummy file (optionally with custom name/content) to a dataset and returns the JSON.""" with open(filename, "w") as tempf: @@ -34,11 +34,11 @@ def upload_file( def upload_image( - api: str, - headers: dict, - dataset_id: str, - filename: str, - content: bytes, + api: str, + headers: dict, + dataset_id: str, + filename: str, + content: bytes, ): """Uploads a dummy file (optionally with custom name/content) to a dataset and returns the JSON.""" with open(filename, "wb") as tempf: From 46fba7ce06a752a140550ad5e5255615d3b52796 Mon Sep 17 00:00:00 2001 From: Chen Wang Date: Fri, 1 Dec 2023 15:57:37 -0600 Subject: [PATCH 04/17] add faker output file to gitignore --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index 2e3b15af5..986f39ae1 100644 --- a/.gitignore +++ b/.gitignore @@ -78,3 +78,7 @@ backend/app/tests/* !clowder-theme.tgz *clowder2-software-dev.yaml secrets.yaml + +# faker +official.csv +fact.png From da6b4f952aedc267d24a276f31949d0a720033b4 Mon Sep 17 00:00:00 2001 From: Chen Wang Date: Fri, 1 Dec 2023 16:19:13 -0600 Subject: [PATCH 05/17] add pagination --- frontend/src/components/users/ManageUsers.tsx | 58 ++++++++++++++++++- 1 file changed, 55 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/users/ManageUsers.tsx b/frontend/src/components/users/ManageUsers.tsx index dacfd2654..550f2af3c 100644 --- a/frontend/src/components/users/ManageUsers.tsx +++ b/frontend/src/components/users/ManageUsers.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from "react"; +import React, { useEffect, useState } from "react"; import { RootState } from "../../types/data"; import { useDispatch, useSelector } from "react-redux"; import Layout from "../Layout"; @@ -10,16 +10,54 @@ import TableHead from "@mui/material/TableHead"; import TableRow from "@mui/material/TableRow"; import Paper from "@mui/material/Paper"; import { fetchAllUsers as fetchAllUsersAction } from "../../actions/user"; +import { Box, Button, ButtonGroup } from "@mui/material"; +import { ArrowBack, ArrowForward } from "@material-ui/icons"; export const ManageUsers = (): JSX.Element => { + const [currPageNum, setCurrPageNum] = useState(0); + const [limit, setLimit] = useState(5); + const [skip, setSkip] = useState(0); + const [prevDisabled, setPrevDisabled] = useState(true); + const [nextDisabled, setNextDisabled] = useState(false); + const [searchTerm, setSearchTerm] = useState(""); + const dispatch = useDispatch(); const users = useSelector((state: RootState) => state.group.users); - const fetchAllUsers = () => dispatch(fetchAllUsersAction()); + const fetchAllUsers = (skip: number, limit: number) => + dispatch(fetchAllUsersAction(skip, limit)); + // component did mount useEffect(() => { - fetchAllUsers(); + fetchAllUsers(skip, limit); }, []); + useEffect(() => { + // disable flipping if reaches the last page + if (users.length < limit) setNextDisabled(true); + else setNextDisabled(false); + }, [users]); + + useEffect(() => { + if (skip !== null && skip !== undefined) { + fetchAllUsers(skip, limit); + if (skip === 0) setPrevDisabled(true); + else setPrevDisabled(false); + } + }, [skip]); + + const previous = () => { + if (currPageNum - 1 >= 0) { + setSkip((currPageNum - 1) * limit); + setCurrPageNum(currPageNum - 1); + } + }; + const next = () => { + if (users.length === limit) { + setSkip((currPageNum + 1) * limit); + setCurrPageNum(currPageNum + 1); + } + }; + return ( @@ -49,6 +87,20 @@ export const ManageUsers = (): JSX.Element => { })} + + + + + + ); From 85f64b6bdbd5ce0ae02dbc1246d0b804dcc2a006 Mon Sep 17 00:00:00 2001 From: Chen Wang Date: Mon, 4 Dec 2023 09:43:31 -0600 Subject: [PATCH 06/17] set and revoke logic works now --- backend/app/routers/authentication.py | 31 ++++++++++++---- frontend/src/actions/user.js | 36 +++++++++++++++++++ frontend/src/components/users/ManageUsers.tsx | 35 ++++++++++++++++-- frontend/src/openapi/v2/models/UserIn.ts | 1 - frontend/src/openapi/v2/models/UserOut.ts | 12 +++---- .../src/openapi/v2/services/LoginService.ts | 23 ++++++++++++ frontend/src/reducers/group.ts | 19 +++++++++- 7 files changed, 140 insertions(+), 17 deletions(-) diff --git a/backend/app/routers/authentication.py b/backend/app/routers/authentication.py index 7513b488a..2e8d9b0d2 100644 --- a/backend/app/routers/authentication.py +++ b/backend/app/routers/authentication.py @@ -1,5 +1,6 @@ import json +from beanie import PydanticObjectId from fastapi import APIRouter, HTTPException, Depends from keycloak.exceptions import ( KeycloakAuthenticationError, @@ -8,8 +9,6 @@ ) from passlib.hash import bcrypt -from beanie import PydanticObjectId - from app.keycloak_auth import create_user, get_current_user from app.keycloak_auth import keycloak_openid from app.models.datasets import DatasetDB @@ -95,14 +94,14 @@ async def authenticate_user(email: str, password: str): async def get_admin(dataset_id: str = None, current_username=Depends(get_current_user)): if ( - current_user := await UserDB.find_one(UserDB.email == current_username.email) + current_user := await UserDB.find_one(UserDB.email == current_username.email) ) is not None: if current_user.admin: return current_user.admin elif ( - dataset_id - and (dataset_db := await DatasetDB.get(PydanticObjectId(dataset_id))) - is not None + dataset_id + and (dataset_db := await DatasetDB.get(PydanticObjectId(dataset_id))) + is not None ): return dataset_db.creator.email == current_username.email else: @@ -111,7 +110,7 @@ async def get_admin(dataset_id: str = None, current_username=Depends(get_current @router.post("/users/set_admin/{useremail}", response_model=UserOut) async def set_admin( - useremail: str, current_username=Depends(get_current_user), admin=Depends(get_admin) + useremail: str, current_username=Depends(get_current_user), admin=Depends(get_admin) ): if admin: if (user := await UserDB.find_one(UserDB.email == useremail)) is not None: @@ -125,3 +124,21 @@ async def set_admin( status_code=403, detail=f"User {current_username.email} is not an admin. Only admin can make others admin.", ) + + +@router.post("/users/revoke_admin/{useremail}", response_model=UserOut) +async def revoke_admin( + useremail: str, current_username=Depends(get_current_user), admin=Depends(get_admin) +): + if admin: + if (user := await UserDB.find_one(UserDB.email == useremail)) is not None: + user.admin = False + await user.replace() + return user.dict() + else: + raise HTTPException(status_code=404, detail=f"User {useremail} not found") + else: + raise HTTPException( + status_code=403, + detail=f"User {current_username.email} is not an admin. Only admin can revoke admin access.", + ) diff --git a/frontend/src/actions/user.js b/frontend/src/actions/user.js index 9e46b03e8..a4eca2fa6 100644 --- a/frontend/src/actions/user.js +++ b/frontend/src/actions/user.js @@ -231,3 +231,39 @@ export function fetchUserProfile() { }); }; } + +export const SET_ADMIN = "SET_ADMIN"; + +export function setAdmin(email) { + return (dispatch) => { + return V2.LoginService.setAdminApiV2UsersSetAdminUseremailPost(email) + .then((json) => { + dispatch({ + type: SET_ADMIN, + profile: json, + receivedAt: Date.now(), + }); + }) + .catch((reason) => { + dispatch(handleErrors(reason, setAdmin(email))); + }); + }; +} + +export const REVOKE_ADMIN = "REVOKE_ADMIN"; + +export function revokeAdmin(email) { + return (dispatch) => { + return V2.LoginService.revokeAdminApiV2UsersRevokeAdminUseremailPost(email) + .then((json) => { + dispatch({ + type: REVOKE_ADMIN, + profile: json, + receivedAt: Date.now(), + }); + }) + .catch((reason) => { + dispatch(handleErrors(reason, revokeAdmin(email))); + }); + }; +} diff --git a/frontend/src/components/users/ManageUsers.tsx b/frontend/src/components/users/ManageUsers.tsx index 550f2af3c..4331b5485 100644 --- a/frontend/src/components/users/ManageUsers.tsx +++ b/frontend/src/components/users/ManageUsers.tsx @@ -9,7 +9,11 @@ import TableContainer from "@mui/material/TableContainer"; import TableHead from "@mui/material/TableHead"; import TableRow from "@mui/material/TableRow"; import Paper from "@mui/material/Paper"; -import { fetchAllUsers as fetchAllUsersAction } from "../../actions/user"; +import { + fetchAllUsers as fetchAllUsersAction, + revokeAdmin as revokeAdminAction, + setAdmin as setAdminAction, +} from "../../actions/user"; import { Box, Button, ButtonGroup } from "@mui/material"; import { ArrowBack, ArrowForward } from "@material-ui/icons"; @@ -26,6 +30,9 @@ export const ManageUsers = (): JSX.Element => { const fetchAllUsers = (skip: number, limit: number) => dispatch(fetchAllUsersAction(skip, limit)); + const setAdmin = (email: string) => dispatch(setAdminAction(email)); + const revokeAdmin = (email: string) => dispatch(revokeAdminAction(email)); + // component did mount useEffect(() => { fetchAllUsers(skip, limit); @@ -67,6 +74,7 @@ export const ManageUsers = (): JSX.Element => { Name Email Admin + @@ -80,7 +88,30 @@ export const ManageUsers = (): JSX.Element => { {profile.email} - {profile.admin ? "True" : "False"} + {profile.admin !== undefined && profile.admin + ? "True" + : "False"} + + + {profile.admin ? ( + + ) : ( + + )} ); diff --git a/frontend/src/openapi/v2/models/UserIn.ts b/frontend/src/openapi/v2/models/UserIn.ts index 2796f1e9f..f7e7cbfa4 100644 --- a/frontend/src/openapi/v2/models/UserIn.ts +++ b/frontend/src/openapi/v2/models/UserIn.ts @@ -6,6 +6,5 @@ export type UserIn = { email: string; first_name: string; last_name: string; - admin: boolean; password: string; } diff --git a/frontend/src/openapi/v2/models/UserOut.ts b/frontend/src/openapi/v2/models/UserOut.ts index f11812942..f624b5122 100644 --- a/frontend/src/openapi/v2/models/UserOut.ts +++ b/frontend/src/openapi/v2/models/UserOut.ts @@ -16,9 +16,9 @@ * - [UpdateMethods](https://roman-right.github.io/beanie/api/interfaces/#aggregatemethods) */ export type UserOut = { - email: string; - first_name: string; - last_name: string; - admin: boolean; - id?: string; -}; + email: string; + first_name: string; + last_name: string; + id?: string; + admin: boolean; +} diff --git a/frontend/src/openapi/v2/services/LoginService.ts b/frontend/src/openapi/v2/services/LoginService.ts index 7d982cc2d..58d601260 100644 --- a/frontend/src/openapi/v2/services/LoginService.ts +++ b/frontend/src/openapi/v2/services/LoginService.ts @@ -72,4 +72,27 @@ export class LoginService { }); } + /** + * Revoke Admin + * @param useremail + * @param datasetId + * @returns UserOut Successful Response + * @throws ApiError + */ + public static revokeAdminApiV2UsersRevokeAdminUseremailPost( + useremail: string, + datasetId?: string, + ): CancelablePromise { + return __request({ + method: 'POST', + path: `/api/v2/users/revoke_admin/${useremail}`, + query: { + 'dataset_id': datasetId, + }, + errors: { + 422: `Validation Error`, + }, + }); + } + } \ No newline at end of file diff --git a/frontend/src/reducers/group.ts b/frontend/src/reducers/group.ts index 691b55b3f..339d4f30a 100644 --- a/frontend/src/reducers/group.ts +++ b/frontend/src/reducers/group.ts @@ -13,7 +13,12 @@ import { RECEIVE_GROUP_ROLE } from "../actions/authorization"; import { DataAction } from "../types/action"; import { GroupState } from "../types/data"; import { GroupOut, RoleType } from "../openapi/v2"; -import { LIST_USERS, PREFIX_SEARCH_USERS } from "../actions/user"; +import { + LIST_USERS, + PREFIX_SEARCH_USERS, + REVOKE_ADMIN, + SET_ADMIN, +} from "../actions/user"; const defaultState: GroupState = { groups: [], @@ -44,6 +49,18 @@ const group = (state = defaultState, action: DataAction) => { return Object.assign({}, state, { about: action.about }); case LIST_USERS: return Object.assign({}, state, { users: action.users }); + case SET_ADMIN: + return Object.assign({}, state, { + users: state.users.map((user) => + user.email === action.profile.email ? action.profile : user + ), + }); + case REVOKE_ADMIN: + return Object.assign({}, state, { + users: state.users.map((user) => + user.email === action.profile.email ? action.profile : user + ), + }); case PREFIX_SEARCH_USERS: return Object.assign({}, state, { users: action.users }); case ASSIGN_GROUP_MEMBER_ROLE: From c7acf6730cba8489bbf2383dc60d3ec0a3d882e2 Mon Sep 17 00:00:00 2001 From: Chen Wang Date: Mon, 4 Dec 2023 10:19:53 -0600 Subject: [PATCH 07/17] add error modal --- backend/app/routers/authentication.py | 19 +++++++++++++------ frontend/src/components/users/ManageUsers.tsx | 4 ++++ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/backend/app/routers/authentication.py b/backend/app/routers/authentication.py index 2e8d9b0d2..3fab6afc5 100644 --- a/backend/app/routers/authentication.py +++ b/backend/app/routers/authentication.py @@ -112,7 +112,7 @@ async def get_admin(dataset_id: str = None, current_username=Depends(get_current async def set_admin( useremail: str, current_username=Depends(get_current_user), admin=Depends(get_admin) ): - if admin: + if admin and current_username.admin if (user := await UserDB.find_one(UserDB.email == useremail)) is not None: user.admin = True await user.replace() @@ -130,13 +130,20 @@ async def set_admin( async def revoke_admin( useremail: str, current_username=Depends(get_current_user), admin=Depends(get_admin) ): + if admin: - if (user := await UserDB.find_one(UserDB.email == useremail)) is not None: - user.admin = False - await user.replace() - return user.dict() + if current_username.email == useremail: + raise HTTPException( + status_code=403, + detail=f"You are currently and admin. Admin cannot revoke their own admin access.", + ) else: - raise HTTPException(status_code=404, detail=f"User {useremail} not found") + if (user := await UserDB.find_one(UserDB.email == useremail)) is not None: + user.admin = False + await user.replace() + return user.dict() + else: + raise HTTPException(status_code=404, detail=f"User {useremail} not found") else: raise HTTPException( status_code=403, diff --git a/frontend/src/components/users/ManageUsers.tsx b/frontend/src/components/users/ManageUsers.tsx index 4331b5485..2120760ab 100644 --- a/frontend/src/components/users/ManageUsers.tsx +++ b/frontend/src/components/users/ManageUsers.tsx @@ -16,6 +16,7 @@ import { } from "../../actions/user"; import { Box, Button, ButtonGroup } from "@mui/material"; import { ArrowBack, ArrowForward } from "@material-ui/icons"; +import { ErrorModal } from "../errors/ErrorModal"; export const ManageUsers = (): JSX.Element => { const [currPageNum, setCurrPageNum] = useState(0); @@ -23,6 +24,7 @@ export const ManageUsers = (): JSX.Element => { const [skip, setSkip] = useState(0); const [prevDisabled, setPrevDisabled] = useState(true); const [nextDisabled, setNextDisabled] = useState(false); + const [errorOpen, setErrorOpen] = useState(false); const [searchTerm, setSearchTerm] = useState(""); const dispatch = useDispatch(); @@ -67,6 +69,8 @@ export const ManageUsers = (): JSX.Element => { return ( + {/*Error Message dialogue*/} + From be06e4e7b346eeca65c35828a66e2d105c2ed11a Mon Sep 17 00:00:00 2001 From: Chen Wang Date: Mon, 4 Dec 2023 10:22:23 -0600 Subject: [PATCH 08/17] update revoke admin logic --- backend/app/routers/authentication.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/backend/app/routers/authentication.py b/backend/app/routers/authentication.py index 3fab6afc5..31b6d1383 100644 --- a/backend/app/routers/authentication.py +++ b/backend/app/routers/authentication.py @@ -112,7 +112,7 @@ async def get_admin(dataset_id: str = None, current_username=Depends(get_current async def set_admin( useremail: str, current_username=Depends(get_current_user), admin=Depends(get_admin) ): - if admin and current_username.admin + if admin and current_username.admin: if (user := await UserDB.find_one(UserDB.email == useremail)) is not None: user.admin = True await user.replace() @@ -130,12 +130,11 @@ async def set_admin( async def revoke_admin( useremail: str, current_username=Depends(get_current_user), admin=Depends(get_admin) ): - if admin: if current_username.email == useremail: raise HTTPException( status_code=403, - detail=f"You are currently and admin. Admin cannot revoke their own admin access.", + detail=f"You are currently an admin. Admin cannot revoke their own admin access.", ) else: if (user := await UserDB.find_one(UserDB.email == useremail)) is not None: From 3bba34b38a8c5e5308cbda84c4e01198eff8d88e Mon Sep 17 00:00:00 2001 From: Chen Wang Date: Mon, 4 Dec 2023 10:25:29 -0600 Subject: [PATCH 09/17] disable button that is the current user --- frontend/src/components/users/ManageUsers.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/frontend/src/components/users/ManageUsers.tsx b/frontend/src/components/users/ManageUsers.tsx index 2120760ab..50cad9778 100644 --- a/frontend/src/components/users/ManageUsers.tsx +++ b/frontend/src/components/users/ManageUsers.tsx @@ -11,6 +11,7 @@ import TableRow from "@mui/material/TableRow"; import Paper from "@mui/material/Paper"; import { fetchAllUsers as fetchAllUsersAction, + fetchUserProfile, revokeAdmin as revokeAdminAction, setAdmin as setAdminAction, } from "../../actions/user"; @@ -29,8 +30,11 @@ export const ManageUsers = (): JSX.Element => { const dispatch = useDispatch(); const users = useSelector((state: RootState) => state.group.users); + const currentUser = useSelector((state: RootState) => state.user.profile); + const fetchAllUsers = (skip: number, limit: number) => dispatch(fetchAllUsersAction(skip, limit)); + const fetchCurrentUser = () => dispatch(fetchUserProfile()); const setAdmin = (email: string) => dispatch(setAdminAction(email)); const revokeAdmin = (email: string) => dispatch(revokeAdminAction(email)); @@ -38,6 +42,7 @@ export const ManageUsers = (): JSX.Element => { // component did mount useEffect(() => { fetchAllUsers(skip, limit); + fetchCurrentUser(); }, []); useEffect(() => { @@ -103,6 +108,7 @@ export const ManageUsers = (): JSX.Element => { onClick={() => { revokeAdmin(profile.email); }} + disabled={profile.email === currentUser.email} > Revoke @@ -112,6 +118,7 @@ export const ManageUsers = (): JSX.Element => { onClick={() => { setAdmin(profile.email); }} + disabled={profile.email === currentUser.email} > Set Admin From baaee70dabb53c5892e1785a13fbff2e0db2c121 Mon Sep 17 00:00:00 2001 From: Chen Wang Date: Mon, 4 Dec 2023 10:54:31 -0600 Subject: [PATCH 10/17] wire in the search user --- frontend/src/components/users/ManageUsers.tsx | 173 +++++++++++------- 1 file changed, 105 insertions(+), 68 deletions(-) diff --git a/frontend/src/components/users/ManageUsers.tsx b/frontend/src/components/users/ManageUsers.tsx index 50cad9778..d45c8651a 100644 --- a/frontend/src/components/users/ManageUsers.tsx +++ b/frontend/src/components/users/ManageUsers.tsx @@ -12,12 +12,14 @@ import Paper from "@mui/material/Paper"; import { fetchAllUsers as fetchAllUsersAction, fetchUserProfile, + prefixSearchAllUsers as prefixSearchAllUsersAction, revokeAdmin as revokeAdminAction, setAdmin as setAdminAction, } from "../../actions/user"; -import { Box, Button, ButtonGroup } from "@mui/material"; +import { Box, Button, ButtonGroup, Grid } from "@mui/material"; import { ArrowBack, ArrowForward } from "@material-ui/icons"; import { ErrorModal } from "../errors/ErrorModal"; +import { GenericSearchBox } from "../search/GenericSearchBox"; export const ManageUsers = (): JSX.Element => { const [currPageNum, setCurrPageNum] = useState(0); @@ -35,6 +37,8 @@ export const ManageUsers = (): JSX.Element => { const fetchAllUsers = (skip: number, limit: number) => dispatch(fetchAllUsersAction(skip, limit)); const fetchCurrentUser = () => dispatch(fetchUserProfile()); + const prefixSearchAllUsers = (text: string, skip: number, limit: number) => + dispatch(prefixSearchAllUsersAction(text, skip, limit)); const setAdmin = (email: string) => dispatch(setAdminAction(email)); const revokeAdmin = (email: string) => dispatch(revokeAdminAction(email)); @@ -72,78 +76,111 @@ export const ManageUsers = (): JSX.Element => { } }; + const searchUsers = (searchTerm: string) => { + prefixSearchAllUsers(searchTerm, skip, limit); + setSearchTerm(searchTerm); + }; + + // search while typing + useEffect(() => { + if (searchTerm !== "") prefixSearchAllUsers(searchTerm, skip, limit); + else fetchAllUsers(skip, limit); + }, [searchTerm]); + return ( {/*Error Message dialogue*/} - -
- - - Name - Email - Admin - - - - - {users.map((profile) => { - return ( - - - {profile.first_name} {profile.last_name} - - {profile.email} - - {profile.admin !== undefined && profile.admin - ? "True" - : "False"} - - - {profile.admin ? ( - - ) : ( - - )} - + + + + + + +
+ + + Name + Email + Admin + - ); - })} - -
- - - - - - -
+ + + {users.map((profile) => { + return ( + + + {profile.first_name} {profile.last_name} + + {profile.email} + + {profile.admin !== undefined && profile.admin + ? "True" + : "False"} + + + {profile.admin ? ( + + ) : ( + + )} + + + ); + })} + + + + + + + + + + +
); }; From eb2b7768e533261de7764142b2a936e6c5cfe3f1 Mon Sep 17 00:00:00 2001 From: Chen Wang Date: Mon, 4 Dec 2023 10:59:19 -0600 Subject: [PATCH 11/17] add padding --- frontend/src/components/groups/Groups.tsx | 233 +++++++++--------- .../metadata/MetadataDefinitions.tsx | 232 ++++++++--------- .../components/search/GenericSearchBox.tsx | 1 - frontend/src/components/users/ManageUsers.tsx | 180 +++++++------- 4 files changed, 327 insertions(+), 319 deletions(-) diff --git a/frontend/src/components/groups/Groups.tsx b/frontend/src/components/groups/Groups.tsx index 5ed93bec5..e8ae5cced 100644 --- a/frontend/src/components/groups/Groups.tsx +++ b/frontend/src/components/groups/Groups.tsx @@ -97,128 +97,129 @@ export function Groups() { {/*Error Message dialogue*/} - - {/*create new group*/} - { - setCreateGroupOpen(false); - }} - fullWidth={true} - maxWidth="md" - aria-labelledby="form-dialog" - > - Create New Group - - - - - - - - - - - - - +
+ {/*create new group*/} + { + setCreateGroupOpen(false); + }} + fullWidth={true} + maxWidth="md" + aria-labelledby="form-dialog" + > + Create New Group + + + + + + + + + - - - - - - - Group Name - - - Description - - - - - - - - {groups.map((group) => { - return ( - + + + + + +
+ + + - - - - - {group.description} - - - {group.users !== undefined ? group.users.length : 0} - - - ); - })} - -
- - - - + + + {group.description} + + + {group.users !== undefined ? group.users.length : 0} + + + ); + })} + + + + - Next - - - -
+ + + + + +
- +
); } diff --git a/frontend/src/components/metadata/MetadataDefinitions.tsx b/frontend/src/components/metadata/MetadataDefinitions.tsx index 0ba28a7a4..5db91cab4 100644 --- a/frontend/src/components/metadata/MetadataDefinitions.tsx +++ b/frontend/src/components/metadata/MetadataDefinitions.tsx @@ -8,7 +8,7 @@ import { DialogTitle, Grid, IconButton, - InputBase, Snackbar, + Snackbar, } from "@mui/material"; import { RootState } from "../../types/data"; import { useDispatch, useSelector } from "react-redux"; @@ -66,7 +66,7 @@ export function MetadataDefinitions() { const [selectedMetadataDefinition, setSelectedMetadataDefinition] = useState(); - // snack bar + // snack bar const [snackBarOpen, setSnackBarOpen] = useState(false); const [snackBarMessage, setSnackBarMessage] = useState(""); @@ -163,124 +163,128 @@ export function MetadataDefinitions() { /> - - - - +
+ + + + + - -
- - - - - - - - - - - Metadata Definition - - - Description - - - - - - {metadataDefinitions.map((mdd) => { - return ( - + + + + + + +
+ + + - - - - - {mdd.description} - - + + Description + + + + + + {metadataDefinitions.map((mdd) => { + return ( + - { - setSelectedMetadataDefinition(mdd.id); - setDeleteMetadataDefinitionConfirmOpen(true); - }} + + + + - - - - - ); - })} - -
- - - - - - -
+ + + + + +
- +
); } diff --git a/frontend/src/components/search/GenericSearchBox.tsx b/frontend/src/components/search/GenericSearchBox.tsx index 51a7fbe79..305df6b60 100644 --- a/frontend/src/components/search/GenericSearchBox.tsx +++ b/frontend/src/components/search/GenericSearchBox.tsx @@ -40,7 +40,6 @@ export function GenericSearchBox(props: GenericSearchBoxProps) { sx={{ border: "1px solid #ced4da", borderRadius: "6px", - mb: 2, p: "2px 4px", display: "flex", alignItems: "left", diff --git a/frontend/src/components/users/ManageUsers.tsx b/frontend/src/components/users/ManageUsers.tsx index d45c8651a..65c8382ad 100644 --- a/frontend/src/components/users/ManageUsers.tsx +++ b/frontend/src/components/users/ManageUsers.tsx @@ -91,96 +91,100 @@ export const ManageUsers = (): JSX.Element => { {/*Error Message dialogue*/} - - - - - - - - - - Name - Email - Admin - - - - - {users.map((profile) => { - return ( - - - {profile.first_name} {profile.last_name} - - {profile.email} - - {profile.admin !== undefined && profile.admin - ? "True" - : "False"} - - - {profile.admin ? ( - - ) : ( - - )} - - - ); - })} - -
- - - - - - -
+ + + + + +
-
+ ); }; From f1aa00bd10df0a5d8d9052b075dbfb5839d70196 Mon Sep 17 00:00:00 2001 From: Chen Wang Date: Mon, 4 Dec 2023 11:52:39 -0600 Subject: [PATCH 12/17] add gravatar --- frontend/src/components/users/ManageUsers.tsx | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/frontend/src/components/users/ManageUsers.tsx b/frontend/src/components/users/ManageUsers.tsx index 65c8382ad..70b67fb68 100644 --- a/frontend/src/components/users/ManageUsers.tsx +++ b/frontend/src/components/users/ManageUsers.tsx @@ -20,6 +20,8 @@ import { Box, Button, ButtonGroup, Grid } from "@mui/material"; import { ArrowBack, ArrowForward } from "@material-ui/icons"; import { ErrorModal } from "../errors/ErrorModal"; import { GenericSearchBox } from "../search/GenericSearchBox"; +import Gravatar from "react-gravatar"; +import PersonIcon from "@mui/icons-material/Person"; export const ManageUsers = (): JSX.Element => { const [currPageNum, setCurrPageNum] = useState(0); @@ -124,6 +126,26 @@ export const ManageUsers = (): JSX.Element => { }} > + {profile.email !== undefined ? ( + + ) : ( + + )} {profile.first_name} {profile.last_name} {profile.email} From 1c8152f050bc91a9fe78672583a707c9ef561f24 Mon Sep 17 00:00:00 2001 From: Chen Wang Date: Mon, 4 Dec 2023 11:53:12 -0600 Subject: [PATCH 13/17] black format --- backend/app/routers/authentication.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/backend/app/routers/authentication.py b/backend/app/routers/authentication.py index 31b6d1383..f3e82736d 100644 --- a/backend/app/routers/authentication.py +++ b/backend/app/routers/authentication.py @@ -94,14 +94,14 @@ async def authenticate_user(email: str, password: str): async def get_admin(dataset_id: str = None, current_username=Depends(get_current_user)): if ( - current_user := await UserDB.find_one(UserDB.email == current_username.email) + current_user := await UserDB.find_one(UserDB.email == current_username.email) ) is not None: if current_user.admin: return current_user.admin elif ( - dataset_id - and (dataset_db := await DatasetDB.get(PydanticObjectId(dataset_id))) - is not None + dataset_id + and (dataset_db := await DatasetDB.get(PydanticObjectId(dataset_id))) + is not None ): return dataset_db.creator.email == current_username.email else: @@ -110,7 +110,7 @@ async def get_admin(dataset_id: str = None, current_username=Depends(get_current @router.post("/users/set_admin/{useremail}", response_model=UserOut) async def set_admin( - useremail: str, current_username=Depends(get_current_user), admin=Depends(get_admin) + useremail: str, current_username=Depends(get_current_user), admin=Depends(get_admin) ): if admin and current_username.admin: if (user := await UserDB.find_one(UserDB.email == useremail)) is not None: @@ -128,7 +128,7 @@ async def set_admin( @router.post("/users/revoke_admin/{useremail}", response_model=UserOut) async def revoke_admin( - useremail: str, current_username=Depends(get_current_user), admin=Depends(get_admin) + useremail: str, current_username=Depends(get_current_user), admin=Depends(get_admin) ): if admin: if current_username.email == useremail: @@ -142,7 +142,9 @@ async def revoke_admin( await user.replace() return user.dict() else: - raise HTTPException(status_code=404, detail=f"User {useremail} not found") + raise HTTPException( + status_code=404, detail=f"User {useremail} not found" + ) else: raise HTTPException( status_code=403, From 75e2e74e141b6733d001c104d407aa5f77882d22 Mon Sep 17 00:00:00 2001 From: Chen Wang Date: Mon, 4 Dec 2023 12:00:24 -0600 Subject: [PATCH 14/17] linting --- .../populate_fake_data/populate_fake_data.py | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/scripts/develop/populate_fake_data/populate_fake_data.py b/scripts/develop/populate_fake_data/populate_fake_data.py index 8eb323a09..710628fda 100755 --- a/scripts/develop/populate_fake_data/populate_fake_data.py +++ b/scripts/develop/populate_fake_data/populate_fake_data.py @@ -13,11 +13,11 @@ def upload_file( - api: str, - headers: dict, - dataset_id: str, - filename: str, - content: str, + api: str, + headers: dict, + dataset_id: str, + filename: str, + content: str, ): """Uploads a dummy file (optionally with custom name/content) to a dataset and returns the JSON.""" with open(filename, "w") as tempf: @@ -34,11 +34,11 @@ def upload_file( def upload_image( - api: str, - headers: dict, - dataset_id: str, - filename: str, - content: bytes, + api: str, + headers: dict, + dataset_id: str, + filename: str, + content: bytes, ): """Uploads a dummy file (optionally with custom name/content) to a dataset and returns the JSON.""" with open(filename, "wb") as tempf: From 9fc2fc88a477f1e4a497040bcaa85a7769292d8f Mon Sep 17 00:00:00 2001 From: Chen Wang Date: Thu, 7 Dec 2023 12:29:54 -0600 Subject: [PATCH 15/17] update use switch --- frontend/src/components/users/ManageUsers.tsx | 39 ++++++------------- 1 file changed, 12 insertions(+), 27 deletions(-) diff --git a/frontend/src/components/users/ManageUsers.tsx b/frontend/src/components/users/ManageUsers.tsx index 70b67fb68..6968f8200 100644 --- a/frontend/src/components/users/ManageUsers.tsx +++ b/frontend/src/components/users/ManageUsers.tsx @@ -16,7 +16,7 @@ import { revokeAdmin as revokeAdminAction, setAdmin as setAdminAction, } from "../../actions/user"; -import { Box, Button, ButtonGroup, Grid } from "@mui/material"; +import { Box, Button, ButtonGroup, Grid, Switch } from "@mui/material"; import { ArrowBack, ArrowForward } from "@material-ui/icons"; import { ErrorModal } from "../errors/ErrorModal"; import { GenericSearchBox } from "../search/GenericSearchBox"; @@ -113,8 +113,7 @@ export const ManageUsers = (): JSX.Element => { Name Email - Admin - + Admin @@ -149,33 +148,19 @@ export const ManageUsers = (): JSX.Element => { {profile.first_name} {profile.last_name} {profile.email} - - {profile.admin !== undefined && profile.admin - ? "True" - : "False"} - - {profile.admin ? ( - - ) : ( - - )} + } + }} + disabled={profile.email === currentUser.email} + /> ); From 7c7207bed7c58c90655746aeaad63de79ee19b3d Mon Sep 17 00:00:00 2001 From: Luigi Marini Date: Tue, 12 Dec 2023 10:36:26 -0600 Subject: [PATCH 16/17] Drop metadata definition in mongo-delete.js. --- scripts/develop/cleanup/mongo-delete.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/scripts/develop/cleanup/mongo-delete.js b/scripts/develop/cleanup/mongo-delete.js index 3e15c2505..2986afaea 100644 --- a/scripts/develop/cleanup/mongo-delete.js +++ b/scripts/develop/cleanup/mongo-delete.js @@ -1,6 +1,6 @@ -db.datasets.drop() -db.files.drop() -db.file_versions.drop() -db.folders.drop() -db.metadata.drop() - +db.datasets.drop(); +db.files.drop(); +db.file_versions.drop(); +db.folders.drop(); +db.metadata.drop(); +db.metadata.definitions.drop(); From acb174c59bccdec6e8b62265cd3630f7b68e6bdd Mon Sep 17 00:00:00 2001 From: Chen Wang Date: Thu, 14 Dec 2023 15:42:20 -0600 Subject: [PATCH 17/17] move to sidebar --- frontend/src/components/Layout.tsx | 31 ++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index 91d1b8750..448c6f670 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -30,8 +30,8 @@ import { getCurrEmail } from "../utils/common"; import VpnKeyIcon from "@mui/icons-material/VpnKey"; import LogoutIcon from "@mui/icons-material/Logout"; import { EmbeddedSearch } from "./search/EmbeddedSearch"; -import ManageAccountsIcon from "@mui/icons-material/ManageAccounts"; import { fetchUserProfile } from "../actions/user"; +import ManageAccountsIcon from "@mui/icons-material/ManageAccounts"; const drawerWidth = 240; @@ -105,8 +105,7 @@ export default function PersistentDrawerLeft(props) { const [embeddedSearchHidden, setEmbeddedSearchHidden] = React.useState(false); const [anchorEl, setAnchorEl] = React.useState(null); const isMenuOpen = Boolean(anchorEl); - const user = useSelector((state: RootState) => state.user); - const profile = user["profile"]; + const profile = useSelector((state: RootState) => state.user.profile); const dispatch = useDispatch(); const fetchProfile = () => dispatch(fetchUserProfile()); @@ -224,17 +223,6 @@ export default function PersistentDrawerLeft(props) { User Profile
- {profile.admin ? ( - <> - - - - - Manage Users - - - - ) : null} @@ -296,6 +284,21 @@ export default function PersistentDrawerLeft(props) { + {profile.admin ? ( + <> + + + + + + + + + + + + + ) : null}