From 73734b186bdcf79cb689df1a145473b18c294875 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Tue, 18 Nov 2025 03:44:26 -0500 Subject: [PATCH] refac: group members frontend integration --- backend/open_webui/models/users.py | 24 +++++-- backend/open_webui/routers/users.py | 21 +++++- src/lib/apis/groups/index.ts | 70 +++++++++++++++++++ .../admin/Users/Groups/EditGroupModal.svelte | 4 +- .../admin/Users/Groups/Users.svelte | 65 ++++++++++------- 5 files changed, 152 insertions(+), 32 deletions(-) diff --git a/backend/open_webui/models/users.py b/backend/open_webui/models/users.py index c80e8f645a..c1a4e9c3f5 100644 --- a/backend/open_webui/models/users.py +++ b/backend/open_webui/models/users.py @@ -6,7 +6,7 @@ from open_webui.internal.db import Base, JSONField, get_db from open_webui.env import DATABASE_USER_ACTIVE_STATUS_UPDATE_INTERVAL from open_webui.models.chats import Chats -from open_webui.models.groups import Groups +from open_webui.models.groups import Groups, GroupMember from open_webui.utils.misc import throttle @@ -95,8 +95,12 @@ class UpdateProfileForm(BaseModel): date_of_birth: Optional[datetime.date] = None +class UserGroupIdsModel(UserModel): + group_ids: list[str] = [] + + class UserListResponse(BaseModel): - users: list[UserModel] + users: list[UserGroupIdsModel] total: int @@ -222,7 +226,10 @@ class UsersTable: limit: Optional[int] = None, ) -> dict: with get_db() as db: - query = db.query(User) + # Join GroupMember so we can order by group_id when requested + query = db.query(User).outerjoin( + GroupMember, GroupMember.user_id == User.id + ) if filter: query_key = filter.get("query") @@ -237,7 +244,16 @@ class UsersTable: order_by = filter.get("order_by") direction = filter.get("direction") - if order_by == "name": + if order_by and order_by.startswith("group_id:"): + group_id = order_by.split(":", 1)[1] + + if direction == "asc": + query = query.order_by((GroupMember.group_id == group_id).asc()) + else: + query = query.order_by( + (GroupMember.group_id == group_id).desc() + ) + elif order_by == "name": if direction == "asc": query = query.order_by(User.name.asc()) else: diff --git a/backend/open_webui/routers/users.py b/backend/open_webui/routers/users.py index 9ee3f9f88c..9d95c3d71a 100644 --- a/backend/open_webui/routers/users.py +++ b/backend/open_webui/routers/users.py @@ -16,6 +16,7 @@ from open_webui.models.groups import Groups from open_webui.models.chats import Chats from open_webui.models.users import ( UserModel, + UserGroupIdsModel, UserListResponse, UserInfoListResponse, UserIdNameListResponse, @@ -91,7 +92,25 @@ async def get_users( if direction: filter["direction"] = direction - return Users.get_users(filter=filter, skip=skip, limit=limit) + result = Users.get_users(filter=filter, skip=skip, limit=limit) + + users = result["users"] + total = result["total"] + + return { + "users": [ + UserGroupIdsModel( + **{ + **user.model_dump(), + "group_ids": [ + group.id for group in Groups.get_groups_by_member_id(user.id) + ], + } + ) + for user in users + ], + "total": total, + } @router.get("/all", response_model=UserInfoListResponse) diff --git a/src/lib/apis/groups/index.ts b/src/lib/apis/groups/index.ts index c55f477af5..51b49bf4d9 100644 --- a/src/lib/apis/groups/index.ts +++ b/src/lib/apis/groups/index.ts @@ -160,3 +160,73 @@ export const deleteGroupById = async (token: string, id: string) => { return res; }; + +export const addUserToGroup = async (token: string, id: string, userIds: string[]) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/groups/id/${id}/users/add`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + user_ids: userIds + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const removeUserFromGroup = async (token: string, id: string, userIds: string[]) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/groups/id/${id}/users/remove`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + user_ids: userIds + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; diff --git a/src/lib/components/admin/Users/Groups/EditGroupModal.svelte b/src/lib/components/admin/Users/Groups/EditGroupModal.svelte index 7f753c537c..af5e732499 100644 --- a/src/lib/components/admin/Users/Groups/EditGroupModal.svelte +++ b/src/lib/components/admin/Users/Groups/EditGroupModal.svelte @@ -219,7 +219,7 @@
-
{$i18n.t('Users')} ({userCount})
+
{$i18n.t('Users')}
{/if} @@ -232,7 +232,7 @@ {:else if selectedTab == 'permissions'} {:else if selectedTab == 'users'} - + {/if} diff --git a/src/lib/components/admin/Users/Groups/Users.svelte b/src/lib/components/admin/Users/Groups/Users.svelte index a82fb9228d..1a9a3afac2 100644 --- a/src/lib/components/admin/Users/Groups/Users.svelte +++ b/src/lib/components/admin/Users/Groups/Users.svelte @@ -2,18 +2,18 @@ import { getContext } from 'svelte'; const i18n = getContext('i18n'); + import { getUsers } from '$lib/apis/users'; + import { toast } from 'svelte-sonner'; + import Tooltip from '$lib/components/common/Tooltip.svelte'; - import Plus from '$lib/components/icons/Plus.svelte'; - import { WEBUI_BASE_URL } from '$lib/constants'; import Checkbox from '$lib/components/common/Checkbox.svelte'; import Badge from '$lib/components/common/Badge.svelte'; import Search from '$lib/components/icons/Search.svelte'; - import { getUsers } from '$lib/apis/users'; - import { toast } from 'svelte-sonner'; import Pagination from '$lib/components/common/Pagination.svelte'; + import { addUserToGroup, removeUserFromGroup } from '$lib/apis/groups'; + export let groupId: string; export let userCount = 0; - let userIds = []; let users = []; let total = 0; @@ -23,7 +23,13 @@ const getUserList = async () => { try { - const res = await getUsers(localStorage.token, query, null, null, page).catch((error) => { + const res = await getUsers( + localStorage.token, + query, + `group_id:${groupId}`, + null, + page + ).catch((error) => { toast.error(`${error}`); return null; }); @@ -37,6 +43,23 @@ } }; + const toggleMember = async (userId, state) => { + if (state === 'checked') { + await addUserToGroup(localStorage.token, groupId, [userId]).catch((error) => { + toast.error(`${error}`); + return null; + }); + } else { + await removeUserFromGroup(localStorage.token, groupId, [userId]).catch((error) => { + toast.error(`${error}`); + return null; + }); + } + + page = 1; + getUserList(); + }; + $: if (page) { getUserList(); } @@ -50,9 +73,9 @@ } -
-
-
+
+
+
@@ -64,20 +87,16 @@
-
+
{#if users.length > 0} {#each users as user, userIdx (user.id)}
{ - if (e.detail === 'checked') { - userIds = [...userIds, user.id]; - } else { - userIds = userIds.filter((id) => id !== user.id); - } + toggleMember(user.id, e.detail); }} />
@@ -89,20 +108,12 @@
- {#if userIds.includes(user.id)} + {#if (user?.group_ids ?? []).includes(groupId)} {/if}
{/each} - - {page} - - {total} - - {#if total > 30} - - {/if} {:else}
{$i18n.t('No users were found.')} @@ -110,4 +121,8 @@ {/if}
+ + {#if total > 30} + + {/if}