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;