diff --git a/backend/open_webui/models/users.py b/backend/open_webui/models/users.py index 445eb51128..9779731a47 100644 --- a/backend/open_webui/models/users.py +++ b/backend/open_webui/models/users.py @@ -11,8 +11,8 @@ from open_webui.utils.misc import throttle from pydantic import BaseModel, ConfigDict -from sqlalchemy import BigInteger, Column, String, Text, Date -from sqlalchemy import or_ +from sqlalchemy import BigInteger, Column, String, Text, Date, exists, select +from sqlalchemy import or_, case import datetime @@ -243,20 +243,30 @@ class UsersTable: direction = filter.get("direction") if order_by and order_by.startswith("group_id:"): - query = query.outerjoin(GroupMember, GroupMember.user_id == User.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() + # Subquery that checks if the user belongs to the group + membership_exists = exists( + select(GroupMember.id).where( + GroupMember.user_id == User.id, + GroupMember.group_id == group_id, ) + ) + + # CASE: user in group → 1, user not in group → 0 + group_sort = case((membership_exists, 1), else_=0) + + if direction == "asc": + query = query.order_by(group_sort.asc(), User.name.asc()) + else: + query = query.order_by(group_sort.desc(), User.name.asc()) + elif order_by == "name": if direction == "asc": query = query.order_by(User.name.asc()) else: query = query.order_by(User.name.desc()) + elif order_by == "email": if direction == "asc": query = query.order_by(User.email.asc()) diff --git a/src/lib/components/admin/Users/Groups/Users.svelte b/src/lib/components/admin/Users/Groups/Users.svelte index 298611a655..e017187677 100644 --- a/src/lib/components/admin/Users/Groups/Users.svelte +++ b/src/lib/components/admin/Users/Groups/Users.svelte @@ -11,21 +11,23 @@ import { getUsers } from '$lib/apis/users'; import { toast } from 'svelte-sonner'; + import { addUserToGroup, removeUserFromGroup } from '$lib/apis/groups'; + import { WEBUI_API_BASE_URL } from '$lib/constants'; + import Tooltip from '$lib/components/common/Tooltip.svelte'; 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 Pagination from '$lib/components/common/Pagination.svelte'; - import { addUserToGroup, removeUserFromGroup } from '$lib/apis/groups'; import ChevronDown from '$lib/components/icons/ChevronDown.svelte'; import ChevronUp from '$lib/components/icons/ChevronUp.svelte'; - import { WEBUI_API_BASE_URL } from '$lib/constants'; + import Spinner from '$lib/components/common/Spinner.svelte'; export let groupId: string; export let userCount = 0; - let users = []; - let total = 0; + let users = null; + let total = null; let query = ''; let orderBy = `group_id:${groupId}`; // default sort key @@ -100,163 +102,169 @@ - {#if users.length > 0} -
- - - - - - - - - - - - - {#each users as user, userIdx} - - - - - - - - {/each} - -
setSortKey(`group_id:${groupId}`)} - > -
- {$i18n.t('MBR')} - - {#if orderBy === `group_id:${groupId}`} - {#if direction === 'asc'} - - {:else} - - {/if} - - {:else} - - {/if} -
-
setSortKey('role')} - > -
- {$i18n.t('Role')} - - {#if orderBy === 'role'} - {#if direction === 'asc'} - - {:else} - - {/if} - - {:else} - - {/if} -
-
setSortKey('name')} - > -
- {$i18n.t('Name')} - - {#if orderBy === 'name'} - {#if direction === 'asc'} - - {:else} - - {/if} - - {:else} - - {/if} -
-
setSortKey('last_active_at')} - > -
- {$i18n.t('Last Active')} - - {#if orderBy === 'last_active_at'} - {#if direction === 'asc'} - - {:else} - - {/if} - - {:else} - - {/if} -
-
-
- { - toggleMember(user.id, e.detail); - }} - /> -
-
-
- -
-
- -
- user - -
{user.name}
-
-
-
- {dayjs(user.last_active_at * 1000).fromNow()} -
+ {#if users === null || total === null} +
+
{:else} -
- {$i18n.t('No users were found.')} -
- {/if} + {#if users.length > 0} +
+ + + + + + + + + + + + + {#each users as user, userIdx (user?.id ?? userIdx)} + + + + + + + + {/each} + +
setSortKey(`group_id:${groupId}`)} + > +
+ {$i18n.t('MBR')} - {#if total > 30} - + {#if orderBy === `group_id:${groupId}`} + {#if direction === 'asc'} + + {:else} + + {/if} + + {:else} + + {/if} +
+
setSortKey('role')} + > +
+ {$i18n.t('Role')} + + {#if orderBy === 'role'} + {#if direction === 'asc'} + + {:else} + + {/if} + + {:else} + + {/if} +
+
setSortKey('name')} + > +
+ {$i18n.t('Name')} + + {#if orderBy === 'name'} + {#if direction === 'asc'} + + {:else} + + {/if} + + {:else} + + {/if} +
+
setSortKey('last_active_at')} + > +
+ {$i18n.t('Last Active')} + + {#if orderBy === 'last_active_at'} + {#if direction === 'asc'} + + {:else} + + {/if} + + {:else} + + {/if} +
+
+
+ { + toggleMember(user.id, e.detail); + }} + /> +
+
+
+ +
+
+ +
+ user + +
{user.name}
+
+
+
+ {dayjs(user.last_active_at * 1000).fromNow()} +
+
+ {:else} +
+ {$i18n.t('No users were found.')} +
+ {/if} + + {#if total > 30} + + {/if} {/if}