feat: Display assigned user groups in Admin Panel

Description:
This PR adds the ability to view a user’s assigned groups in the Admin Panel when editing a user.

Backend Changes:
    Added a new endpoint:
    GET /api/v1/users/{user_id}/groups

        Returns the list of groups assigned to a specific user.
        Requires admin privileges.

Frontend Changes:
    Implemented getUserGroupsById API function to call the new backend endpoint, in lib/apis/users.

    Updated EditUserModal.svelte to:
        Load user groups asynchronously when the modal is opened.
        Display the groups inline in the form before the Save button.
        Show a loading state while fetching, and a “No groups assigned” message if none exist.

Result:
Admins can now see which groups a user belongs to directly from the edit user modal,
improving visibility and reducing the need to navigate away for group membership checks.
This commit is contained in:
Athanasios Oikonomou 2025-08-10 14:41:24 +03:00 committed by Athanasios Oikonomou
parent 397c1e7684
commit dc453efa5c
3 changed files with 68 additions and 1 deletions

View file

@ -501,3 +501,13 @@ async def delete_user_by_id(user_id: str, user=Depends(get_admin_user)):
status_code=status.HTTP_403_FORBIDDEN, status_code=status.HTTP_403_FORBIDDEN,
detail=ERROR_MESSAGES.ACTION_PROHIBITED, detail=ERROR_MESSAGES.ACTION_PROHIBITED,
) )
############################
# GetUserGroupsById
############################
@router.get("/{user_id}/groups")
async def get_user_groups_by_id(user_id: str, user=Depends(get_admin_user)):
return Groups.get_groups_by_member_id(user_id)

View file

@ -443,3 +443,30 @@ export const updateUserById = async (token: string, userId: string, user: UserUp
return res; return res;
}; };
export const getUserGroupsById = async (token: string, userId: string) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/users/${userId}/groups`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.error(err);
error = err.detail;
return null;
});
if (error) {
throw error;
}
return res;
};

View file

@ -4,7 +4,7 @@
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
import { onMount, getContext } from 'svelte'; import { onMount, getContext } from 'svelte';
import { updateUserById } from '$lib/apis/users'; import { updateUserById, getUserGroupsById } from '$lib/apis/users';
import Modal from '$lib/components/common/Modal.svelte'; import Modal from '$lib/components/common/Modal.svelte';
import localizedFormat from 'dayjs/plugin/localizedFormat'; import localizedFormat from 'dayjs/plugin/localizedFormat';
@ -27,6 +27,9 @@
password: '' password: ''
}; };
let _user_groups: any[] = [];
let loadingGroups = false;
const submitHandler = async () => { const submitHandler = async () => {
const res = await updateUserById(localStorage.token, selectedUser.id, _user).catch((error) => { const res = await updateUserById(localStorage.token, selectedUser.id, _user).catch((error) => {
toast.error(`${error}`); toast.error(`${error}`);
@ -38,10 +41,23 @@
} }
}; };
const loadUserGroups = async () => {
if (!selectedUser?.id) return;
loadingGroups = true;
try {
_user_groups = await getUserGroupsById(localStorage.token, selectedUser.id);
} catch (error) {
toast.error(`${error}`);
} finally {
loadingGroups = false;
}
};
onMount(() => { onMount(() => {
if (selectedUser) { if (selectedUser) {
_user = selectedUser; _user = selectedUser;
_user.password = ''; _user.password = '';
loadUserGroups();
} }
}); });
</script> </script>
@ -152,6 +168,20 @@
</div> </div>
</div> </div>
<div class="flex flex-col w-full">
<div class="mb-1 text-xs text-gray-500">{$i18n.t('Groups')}</div>
{#if loadingGroups}
<div class="text-sm font-medium text-white">{$i18n.t('Loading groups...')}</div>
{:else if _user_groups.length === 0}
<div class="text-sm font-medium text-white">{$i18n.t('No groups assigned')}</div>
{:else}
<div class="text-sm font-medium text-white">
{_user_groups.map(g => g.name).join(', ')}
</div>
{/if}
</div>
<div class="flex justify-end pt-3 text-sm font-medium"> <div class="flex justify-end pt-3 text-sm font-medium">
<button <button
class="px-3.5 py-1.5 text-sm font-medium bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full flex flex-row space-x-1 items-center" class="px-3.5 py-1.5 text-sm font-medium bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full flex flex-row space-x-1 items-center"