mirror of
https://github.com/open-webui/open-webui.git
synced 2025-12-12 04:15:25 +00:00
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.
220 lines
6.1 KiB
Svelte
220 lines
6.1 KiB
Svelte
<script lang="ts">
|
|
import { toast } from 'svelte-sonner';
|
|
import dayjs from 'dayjs';
|
|
import { createEventDispatcher } from 'svelte';
|
|
import { onMount, getContext } from 'svelte';
|
|
|
|
import { updateUserById, getUserGroupsById } from '$lib/apis/users';
|
|
|
|
import Modal from '$lib/components/common/Modal.svelte';
|
|
import localizedFormat from 'dayjs/plugin/localizedFormat';
|
|
import XMark from '$lib/components/icons/XMark.svelte';
|
|
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
|
|
|
|
const i18n = getContext('i18n');
|
|
const dispatch = createEventDispatcher();
|
|
dayjs.extend(localizedFormat);
|
|
|
|
export let show = false;
|
|
export let selectedUser;
|
|
export let sessionUser;
|
|
|
|
let _user = {
|
|
profile_image_url: '',
|
|
role: 'pending',
|
|
name: '',
|
|
email: '',
|
|
password: ''
|
|
};
|
|
|
|
let _user_groups: any[] = [];
|
|
let loadingGroups = false;
|
|
|
|
const submitHandler = async () => {
|
|
const res = await updateUserById(localStorage.token, selectedUser.id, _user).catch((error) => {
|
|
toast.error(`${error}`);
|
|
});
|
|
|
|
if (res) {
|
|
dispatch('save');
|
|
show = false;
|
|
}
|
|
};
|
|
|
|
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(() => {
|
|
if (selectedUser) {
|
|
_user = selectedUser;
|
|
_user.password = '';
|
|
loadUserGroups();
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<Modal size="sm" bind:show>
|
|
<div>
|
|
<div class=" flex justify-between dark:text-gray-300 px-5 pt-4 pb-2">
|
|
<div class=" text-lg font-medium self-center">{$i18n.t('Edit User')}</div>
|
|
<button
|
|
class="self-center"
|
|
on:click={() => {
|
|
show = false;
|
|
}}
|
|
>
|
|
<XMark className={'size-5'} />
|
|
</button>
|
|
</div>
|
|
|
|
<div class="flex flex-col md:flex-row w-full md:space-x-4 dark:text-gray-200">
|
|
<div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
|
|
<form
|
|
class="flex flex-col w-full"
|
|
on:submit|preventDefault={() => {
|
|
submitHandler();
|
|
}}
|
|
>
|
|
<div class=" flex items-center rounded-md px-5 py-2 w-full">
|
|
<div class=" self-center mr-5">
|
|
<img
|
|
src={selectedUser.profile_image_url}
|
|
class=" max-w-[55px] object-cover rounded-full"
|
|
alt="User profile"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<div class=" self-center capitalize font-semibold">{selectedUser.name}</div>
|
|
|
|
<div class="text-xs text-gray-500">
|
|
{$i18n.t('Created at')}
|
|
{dayjs(selectedUser.created_at * 1000).format('LL')}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class=" px-5 pt-3 pb-5">
|
|
<div class=" flex flex-col space-y-1.5">
|
|
<div class="flex flex-col w-full">
|
|
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Role')}</div>
|
|
|
|
<div class="flex-1">
|
|
<select
|
|
class="w-full dark:bg-gray-900 text-sm bg-transparent disabled:text-gray-500 dark:disabled:text-gray-500 outline-hidden"
|
|
bind:value={_user.role}
|
|
disabled={_user.id == sessionUser.id}
|
|
required
|
|
>
|
|
<option value="admin">{$i18n.t('Admin')}</option>
|
|
<option value="user">{$i18n.t('User')}</option>
|
|
<option value="pending">{$i18n.t('Pending')}</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex flex-col w-full">
|
|
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Email')}</div>
|
|
|
|
<div class="flex-1">
|
|
<input
|
|
class="w-full text-sm bg-transparent disabled:text-gray-500 dark:disabled:text-gray-500 outline-hidden"
|
|
type="email"
|
|
bind:value={_user.email}
|
|
placeholder={$i18n.t('Enter Your Email')}
|
|
autocomplete="off"
|
|
required
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex flex-col w-full">
|
|
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Name')}</div>
|
|
|
|
<div class="flex-1">
|
|
<input
|
|
class="w-full text-sm bg-transparent outline-hidden"
|
|
type="text"
|
|
bind:value={_user.name}
|
|
placeholder={$i18n.t('Enter Your Name')}
|
|
autocomplete="off"
|
|
required
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex flex-col w-full">
|
|
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('New Password')}</div>
|
|
|
|
<div class="flex-1">
|
|
<SensitiveInput
|
|
class="w-full text-sm bg-transparent outline-hidden"
|
|
type="password"
|
|
placeholder={$i18n.t('Enter New Password')}
|
|
bind:value={_user.password}
|
|
autocomplete="new-password"
|
|
required={false}
|
|
/>
|
|
</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">
|
|
<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"
|
|
type="submit"
|
|
>
|
|
{$i18n.t('Save')}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Modal>
|
|
|
|
<style>
|
|
input::-webkit-outer-spin-button,
|
|
input::-webkit-inner-spin-button {
|
|
/* display: none; <- Crashes Chrome on hover */
|
|
-webkit-appearance: none;
|
|
margin: 0; /* <-- Apparently some margin are still there even though it's hidden */
|
|
}
|
|
|
|
.tabs::-webkit-scrollbar {
|
|
display: none; /* for Chrome, Safari and Opera */
|
|
}
|
|
|
|
.tabs {
|
|
-ms-overflow-style: none; /* IE and Edge */
|
|
scrollbar-width: none; /* Firefox */
|
|
}
|
|
|
|
input[type='number'] {
|
|
-moz-appearance: textfield; /* Firefox */
|
|
}
|
|
</style>
|