github.com/treeverse/lakefs@v1.24.1-0.20240520134607-95648127bfb0/webui/src/pages/auth/users/index.jsx (about) 1 import React, {createContext, useCallback, useEffect, useState} from "react"; 2 import {Outlet} from "react-router-dom"; 3 import { useOutletContext } from "react-router-dom"; 4 5 import Button from "react-bootstrap/Button"; 6 7 import {useAPI, useAPIWithPagination} from "../../../lib/hooks/api"; 8 import {auth} from "../../../lib/api"; 9 import useUser from "../../../lib/hooks/user"; 10 import {ConfirmationButton} from "../../../lib/components/modals"; 11 import {EntityActionModal} from "../../../lib/components/auth/forms"; 12 import {Paginator} from "../../../lib/components/pagination"; 13 import {useRouter} from "../../../lib/hooks/router"; 14 import {Link} from "../../../lib/components/nav"; 15 import { 16 ActionGroup, 17 ActionsBar, 18 Checkbox, 19 DataTable, 20 AlertError, 21 FormattedDate, 22 Loading, 23 RefreshButton 24 } from "../../../lib/components/controls"; 25 import validator from "validator/es"; 26 import { disallowPercentSign, INVALID_USER_NAME_ERROR_MESSAGE } from "../validation"; 27 import { resolveDisplayName } from "../../../lib/utils"; 28 29 const USER_NOT_FOUND = "unknown"; 30 export const GetUserEmailByIdContext = createContext(); 31 32 33 const UsersContainer = ({nextPage, refresh, setRefresh, error, loading, userListResults}) => { 34 const { user } = useUser(); 35 const currentUser = user; 36 37 const router = useRouter(); 38 const after = (router.query.after) ? router.query.after : ""; 39 const [selected, setSelected] = useState([]); 40 const [deleteError, setDeleteError] = useState(null); 41 const [showCreate, setShowCreate] = useState(false); 42 const [showInvite, setShowInvite] = useState(false); 43 44 45 46 useEffect(() => { setSelected([]); }, [refresh, after]); 47 48 const authCapabilities = useAPI(() => auth.getAuthCapabilities()); 49 if (error) return <AlertError error={error}/>; 50 if (loading) return <Loading/>; 51 if (authCapabilities.loading) return <Loading/>; 52 53 const canInviteUsers = !authCapabilities.error && authCapabilities.response && authCapabilities.response.invite_user; 54 55 return ( 56 <> 57 <ActionsBar> 58 <UserActionsActionGroup canInviteUsers={canInviteUsers} selected={selected} 59 onClickInvite={() => setShowInvite(true)} onClickCreate={() => setShowCreate(true)} 60 onConfirmDelete={() => { 61 auth.deleteUsers(selected.map(u => u.id)) 62 .catch(err => setDeleteError(err)) 63 .then(() => { 64 setSelected([]); 65 setRefresh(!refresh); 66 })}}/> 67 <ActionGroup orientation="right"> 68 <RefreshButton onClick={() => setRefresh(!refresh)}/> 69 </ActionGroup> 70 </ActionsBar> 71 <div className="auth-learn-more"> 72 Users are entities that access and use lakeFS. <a href="https://docs.lakefs.io/reference/authentication.html" target="_blank" rel="noopener noreferrer">Learn more.</a> 73 </div> 74 75 {(!!deleteError) && <AlertError error={deleteError}/>} 76 77 <EntityActionModal 78 show={showCreate} 79 onHide={() => setShowCreate(false)} 80 onAction={userId => { 81 return auth.createUser(userId).then(() => { 82 setSelected([]); 83 setShowCreate(false); 84 setRefresh(!refresh); 85 }); 86 }} 87 title={canInviteUsers ? "Create API User" : "Create User"} 88 placeholder={canInviteUsers ? "Name (e.g. Spark)" : "Username (e.g. 'jane.doe')"} 89 actionName={"Create"} 90 validationFunction={disallowPercentSign(INVALID_USER_NAME_ERROR_MESSAGE)} 91 /> 92 93 <EntityActionModal 94 show={showInvite} 95 onHide={() => setShowInvite(false)} 96 onAction={async (userEmail) => { 97 if (!validator.isEmail(userEmail)) { 98 throw new Error("Invalid email address"); 99 } 100 await auth.createUser(userEmail, true); 101 setSelected([]); 102 setShowInvite(false); 103 setRefresh(!refresh); 104 }} 105 title={"Invite User"} 106 placeholder={"Email"} 107 actionName={"Invite"} 108 /> 109 110 <DataTable 111 results={userListResults} 112 headers={['', 'User ID', 'Created At']} 113 keyFn={user => user.id} 114 rowFn={user => [ 115 <Checkbox 116 disabled={(!!currentUser && currentUser.id === user.id)} 117 name={user.id} 118 onAdd={() => setSelected([...selected, user])} 119 onRemove={() => setSelected(selected.filter(u => u !== user))} 120 />, 121 <Link href={{pathname: '/auth/users/:userId', params: {userId: user.id}}}> 122 { resolveDisplayName(user) } 123 </Link>, 124 <FormattedDate dateValue={user.creation_date}/> 125 ]}/> 126 127 <Paginator 128 nextPage={nextPage} 129 after={after} 130 onPaginate={after => router.push({pathname: '/auth/users', query: {after}})} 131 /> 132 </> 133 ); 134 }; 135 136 const UserActionsActionGroup = ({canInviteUsers, selected, onClickInvite, onClickCreate, onConfirmDelete }) => { 137 138 return ( 139 <ActionGroup orientation="left"> 140 <Button 141 hidden={!canInviteUsers} 142 variant="primary" 143 onClick={onClickInvite}> 144 Invite User 145 </Button> 146 147 <Button 148 variant="success" 149 onClick={onClickCreate}> 150 {canInviteUsers ? "Create API User" : "Create User"} 151 </Button> 152 <ConfirmationButton 153 onConfirm={onConfirmDelete} 154 disabled={(selected.length === 0)} 155 variant="danger" 156 msg={`Are you sure you'd like to delete ${selected.length} users?`}> 157 Delete Selected 158 </ConfirmationButton> 159 </ActionGroup> 160 ); 161 } 162 163 export const UsersPage = () => { 164 const { setActiveTab, refresh, loading, error, nextPage, setRefresh, usersList } = useOutletContext(); 165 useEffect(() => setActiveTab("users"), [setActiveTab]); 166 return ( 167 <UsersContainer 168 refresh={refresh} 169 loading={loading} 170 error={error} 171 nextPage={nextPage} 172 setRefresh={setRefresh} 173 userListResults={usersList} 174 /> 175 ); 176 }; 177 178 const UsersIndexPage = () => { 179 const [setActiveTab] = useOutletContext(); 180 const [refresh, setRefresh] = useState(false); 181 const [usersList, setUsersList] = useState([]); 182 const router = useRouter(); 183 const after = (router.query.after) ? router.query.after : ""; 184 const { results, loading, error, nextPage } = useAPIWithPagination(() => { 185 return auth.listUsers('', after); 186 }, [after, refresh]); 187 188 useEffect(() => { 189 setUsersList(results); 190 }, [results, refresh]); 191 192 const getUserEmailById = useCallback((id) => { 193 const userRecord = usersList.find(user => user.id === id); 194 // return something, so we don't completely break the state 195 // this can help us track down issues later on 196 if (!userRecord) { 197 return USER_NOT_FOUND; 198 } 199 200 return userRecord.email || userRecord.id; 201 }, [usersList]); 202 203 return ( 204 <GetUserEmailByIdContext.Provider value={getUserEmailById}> 205 <Outlet context={{setActiveTab, refresh, loading, error, nextPage, setRefresh, usersList, getUserEmailById}} /> 206 </GetUserEmailByIdContext.Provider> 207 ) 208 } 209 210 export default UsersIndexPage;