diff --git a/contrib/auth/acl/controller.go b/contrib/auth/acl/controller.go index 2917f5b1a8b..57a40909a48 100644 --- a/contrib/auth/acl/controller.go +++ b/contrib/auth/acl/controller.go @@ -151,6 +151,7 @@ func (c *Controller) ListGroupMembers(w http.ResponseWriter, r *http.Request, gr Username: u.Username, CreationDate: u.CreatedAt.Unix(), Email: u.Email, + FriendlyName: u.FriendlyName, }) } writeResponse(w, http.StatusOK, response) diff --git a/pkg/api/controller.go b/pkg/api/controller.go index 7004db34abe..89133c4d407 100644 --- a/pkg/api/controller.go +++ b/pkg/api/controller.go @@ -1112,6 +1112,7 @@ func (c *Controller) ListGroupMembers(w http.ResponseWriter, r *http.Request, gr Id: u.Username, Email: u.Email, CreationDate: u.CreatedAt.Unix(), + FriendlyName: u.FriendlyName, }) } writeResponse(w, r, http.StatusOK, response) diff --git a/webui/src/lib/components/auth/forms.jsx b/webui/src/lib/components/auth/forms.jsx index e5771961b53..12608461c78 100644 --- a/webui/src/lib/components/auth/forms.jsx +++ b/webui/src/lib/components/auth/forms.jsx @@ -9,15 +9,8 @@ import {SearchIcon} from "@primer/octicons-react"; import {useAPI} from "../../hooks/api"; import {Checkbox, DataTable, DebouncedFormControl, AlertError, Loading} from "../controls"; -const resolveEntityDisplayName = (ent) => { - // for users - if (ent?.email?.length) return ent.email; - // for groups - if (ent?.name?.length) return ent.name; - return ent.id; -} - -export const AttachModal = ({ show, searchFn, onAttach, onHide, addText = "Add", + +export const AttachModal = ({ show, searchFn, resolveEntityFn = (ent => ent.id), onAttach, onHide , addText = "Add", emptyState = 'No matches', modalTitle = 'Add', headers = ['Select', 'ID'], filterPlaceholder = 'Filter...'}) => { const search = useRef(null); @@ -49,7 +42,7 @@ export const AttachModal = ({ show, searchFn, onAttach, onHide, addText = "Add", onAdd={() => setSelected([...selected, ent])} onRemove={() => setSelected(selected.filter(selectedEnt => selectedEnt.id !== ent.id))} name={'selected'}/>, - {resolveEntityDisplayName(ent)} + {resolveEntityFn(ent)} ]}/>
@@ -58,7 +51,7 @@ export const AttachModal = ({ show, searchFn, onAttach, onHide, addText = "Add", Selected: {(selected.map(item => ( - {resolveEntityDisplayName(item)} + {resolveEntityFn(item)} )))}

diff --git a/webui/src/lib/components/auth/users.jsx b/webui/src/lib/components/auth/users.jsx new file mode 100644 index 00000000000..8f317148359 --- /dev/null +++ b/webui/src/lib/components/auth/users.jsx @@ -0,0 +1,20 @@ +import {auth, MAX_LISTING_AMOUNT} from "../../api"; + +export const allUsersFromLakeFS = async (resolveUserDisplayNameFN = (user => user.id)) => { + let after = "" + let hasMore = true + let usersList = [] + try { + do { + const results = await auth.listUsers("", after, MAX_LISTING_AMOUNT); + usersList = usersList.concat(results.results); + after = results.pagination.next_offset; + hasMore = results.pagination.has_more; + } while (hasMore); + usersList.sort((a, b) => resolveUserDisplayNameFN(a).localeCompare(resolveUserDisplayNameFN(b))); + return usersList; + } catch (error) { + console.error("Error fetching users:", error); + return []; + } +} \ No newline at end of file diff --git a/webui/src/lib/utils.ts b/webui/src/lib/utils.ts index 3be59c67120..266dfbd4817 100644 --- a/webui/src/lib/utils.ts +++ b/webui/src/lib/utils.ts @@ -4,7 +4,7 @@ interface User { friendly_name: string; } -export const resolveDisplayName = (user: User): string => { +export const resolveUserDisplayName = (user: User): string => { if (!user) return ""; if (user?.email?.length) return user.email; if (user?.friendly_name?.length) return user.friendly_name; diff --git a/webui/src/pages/auth/credentials.jsx b/webui/src/pages/auth/credentials.jsx index 344ab46c146..33dab61cc7c 100644 --- a/webui/src/pages/auth/credentials.jsx +++ b/webui/src/pages/auth/credentials.jsx @@ -12,7 +12,7 @@ import {auth} from "../../lib/api"; import {useState} from "react"; import {CredentialsShowModal, CredentialsTable} from "../../lib/components/auth/credentials"; import {useRouter} from "../../lib/hooks/router"; -import {resolveDisplayName} from "../../lib/utils"; +import {resolveUserDisplayName} from "../../lib/utils"; const CredentialsContainer = () => { const router = useRouter(); @@ -40,7 +40,7 @@ const CredentialsContainer = () => { Create a new Access Key for user {resolveDisplayName(user)}?} + msg={Create a new Access Key for user {resolveUserDisplayName(user)}?} onConfirm={hide => { createKey() .then(key => { setCreatedKey(key) }) diff --git a/webui/src/pages/auth/groups/group/members.jsx b/webui/src/pages/auth/groups/group/members.jsx index f8cab5ed014..3cdef8a439d 100644 --- a/webui/src/pages/auth/groups/group/members.jsx +++ b/webui/src/pages/auth/groups/group/members.jsx @@ -19,22 +19,32 @@ import { } from "../../../../lib/components/controls"; import {useRouter} from "../../../../lib/hooks/router"; import {Link} from "../../../../lib/components/nav"; -import {resolveDisplayName} from "../../../../lib/utils"; +import {resolveUserDisplayName} from "../../../../lib/utils"; +import {allUsersFromLakeFS} from "../../../../lib/components/auth/users"; const GroupMemberList = ({ groupId, after, onPaginate }) => { const [refresh, setRefresh] = useState(false); const [showAddModal, setShowAddModal] = useState(false); const [attachError, setAttachError] = useState(null); - + const [allUsers, setAllUsers] = useState([]); const {results, loading, error, nextPage} = useAPIWithPagination(() => { return auth.listGroupMembers(groupId, after); }, [groupId, after, refresh]); - useEffect(() => { setAttachError(null); }, [refresh]); + + const searchUsers = async (prefix, maxResults, resolveUserDisplayNameFN = (user => user.id)) => { + let allUsersList = allUsers; + if (allUsersList.length == 0) { + allUsersList = await allUsersFromLakeFS(resolveUserDisplayNameFN) + setAllUsers(allUsersList) + } + let filteredUsers = allUsersList.filter(user => resolveUserDisplayNameFN(user).startsWith(prefix)); + return filteredUsers.slice(0, maxResults); + }; let content; if (loading) content = ; else if (error) content= ; @@ -45,7 +55,7 @@ const GroupMemberList = ({ groupId, after, onPaginate }) => { user.id} rowFn={user => [ - {resolveDisplayName(user)}, + {resolveUserDisplayName(user)}, ]} headers={['User ID', 'Created At']} @@ -54,7 +64,7 @@ const GroupMemberList = ({ groupId, after, onPaginate }) => { buttonFn: user => Are you sure you{'\''}d like to remove user {resolveDisplayName(user)} from group {groupId}?} + msg={Are you sure you{'\''}d like to remove user {resolveUserDisplayName(user)} from group {groupId}?} onConfirm={() => { auth.removeUserFromGroup(user.id, groupId) .catch(error => alert(error)) @@ -75,7 +85,8 @@ const GroupMemberList = ({ groupId, after, onPaginate }) => { filterPlaceholder={'Find User...'} modalTitle={'Add to Group'} addText={'Add to Group'} - searchFn={prefix => auth.listUsers(prefix, "", 5).then(res => res.results)} + resolveEntityFn={resolveUserDisplayName} + searchFn={prefix => searchUsers(prefix, 5, resolveUserDisplayName).then(res => res)} onHide={() => setShowAddModal(false)} onAttach={(selected) => { Promise.all(selected.map(user => auth.addUserToGroup(user.id, groupId))) diff --git a/webui/src/pages/auth/users/index.jsx b/webui/src/pages/auth/users/index.jsx index da45a7218dd..969f58064b5 100644 --- a/webui/src/pages/auth/users/index.jsx +++ b/webui/src/pages/auth/users/index.jsx @@ -24,7 +24,7 @@ import { } from "../../../lib/components/controls"; import validator from "validator/es"; import { disallowPercentSign, INVALID_USER_NAME_ERROR_MESSAGE } from "../validation"; -import { resolveDisplayName } from "../../../lib/utils"; +import { resolveUserDisplayName } from "../../../lib/utils"; const USER_NOT_FOUND = "unknown"; export const GetUserEmailByIdContext = createContext(); @@ -119,7 +119,7 @@ const UsersContainer = ({nextPage, refresh, setRefresh, error, loading, userList onRemove={() => setSelected(selected.filter(u => u !== user))} />, - { resolveDisplayName(user) } + { resolveUserDisplayName(user) } , ]}/> diff --git a/webui/src/pages/auth/users/user/groups.jsx b/webui/src/pages/auth/users/user/groups.jsx index 7e2fa6fc94a..bb8b0adc540 100644 --- a/webui/src/pages/auth/users/user/groups.jsx +++ b/webui/src/pages/auth/users/user/groups.jsx @@ -21,6 +21,12 @@ import { ConfirmationButton } from "../../../../lib/components/modals"; import { useRouter } from "../../../../lib/hooks/router"; import { Link } from "../../../../lib/components/nav"; +const resolveGroupDisplayName = (group) => { + if(!group) return ""; + if (group?.name?.length) return group.name; + return group.id; +} + const UserGroupsList = ({ userId, after, onPaginate }) => { const [refresh, setRefresh] = useState(false); const [showAddModal, setShowAddModal] = useState(false); @@ -97,6 +103,7 @@ const UserGroupsList = ({ userId, after, onPaginate }) => { searchFn={(prefix) => auth.listGroups(prefix, "", 5).then((res) => res.results) } + resolveEntityFn={resolveGroupDisplayName} onHide={() => setShowAddModal(false)} onAttach={(selected) => { Promise.all( @@ -112,7 +119,8 @@ const UserGroupsList = ({ userId, after, onPaginate }) => { .finally(() => { setShowAddModal(false); }); - }} + } + } /> )}