refac: user list sub-standard code

This commit is contained in:
Timothy Jaeryang Baek 2025-04-30 16:49:41 +04:00
parent db06a925fe
commit f9d238e850
5 changed files with 188 additions and 154 deletions

View File

@ -10,6 +10,8 @@ from open_webui.models.groups import Groups
from pydantic import BaseModel, ConfigDict from pydantic import BaseModel, ConfigDict
from sqlalchemy import BigInteger, Column, String, Text from sqlalchemy import BigInteger, Column, String, Text
from sqlalchemy import or_
#################### ####################
# User DB Schema # User DB Schema
@ -67,6 +69,11 @@ class UserModel(BaseModel):
#################### ####################
class UserListResponse(BaseModel):
users: list[UserModel]
total: int
class UserResponse(BaseModel): class UserResponse(BaseModel):
id: str id: str
name: str name: str
@ -161,25 +168,62 @@ class UsersTable:
def get_users( def get_users(
self, self,
filter: Optional[dict] = None,
skip: Optional[int] = None, skip: Optional[int] = None,
limit: Optional[int] = None, limit: Optional[int] = None,
query_key: Optional[int] = None ) -> UserListResponse:
) -> list[UserModel]:
with get_db() as db: with get_db() as db:
query = db.query(User)
if not query_key: if filter:
query = db.query(User).order_by(User.created_at.desc()) query_key = filter.get("query")
else: if query_key:
query = ( query = query.filter(
db.query(User)
.filter(
or_( or_(
User.name.ilike(f'%{query_key}%'), User.name.ilike(f"%{query_key}%"),
User.email.ilike(f'%{query_key}%') User.email.ilike(f"%{query_key}%"),
) )
) )
.order_by(User.created_at.desc())
) order_by = filter.get("order_by")
direction = filter.get("direction")
if 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())
else:
query = query.order_by(User.email.desc())
elif order_by == "created_at":
if direction == "asc":
query = query.order_by(User.created_at.asc())
else:
query = query.order_by(User.created_at.desc())
elif order_by == "last_active_at":
if direction == "asc":
query = query.order_by(User.last_active_at.asc())
else:
query = query.order_by(User.last_active_at.desc())
elif order_by == "updated_at":
if direction == "asc":
query = query.order_by(User.updated_at.asc())
else:
query = query.order_by(User.updated_at.desc())
elif order_by == "role":
if direction == "asc":
query = query.order_by(User.role.asc())
else:
query = query.order_by(User.role.desc())
else:
query = query.order_by(User.created_at.desc())
if skip: if skip:
query = query.offset(skip) query = query.offset(skip)
@ -187,8 +231,10 @@ class UsersTable:
query = query.limit(limit) query = query.limit(limit)
users = query.all() users = query.all()
return {
return [UserModel.model_validate(user) for user in users] "users": [UserModel.model_validate(user) for user in users],
"total": db.query(User).count(),
}
def get_users_by_user_ids(self, user_ids: list[str]) -> list[UserModel]: def get_users_by_user_ids(self, user_ids: list[str]) -> list[UserModel]:
with get_db() as db: with get_db() as db:

View File

@ -6,6 +6,7 @@ from open_webui.models.groups import Groups
from open_webui.models.chats import Chats from open_webui.models.chats import Chats
from open_webui.models.users import ( from open_webui.models.users import (
UserModel, UserModel,
UserListResponse,
UserRoleUpdateForm, UserRoleUpdateForm,
Users, Users,
UserSettings, UserSettings,
@ -33,23 +34,38 @@ router = APIRouter()
############################ ############################
@router.get("/", response_model=list[UserModel]) PAGE_ITEM_COUNT = 10
@router.get("/", response_model=UserListResponse)
async def get_users( async def get_users(
page: Optional[int] = None, query: Optional[str] = None,
limit: Optional[int] = None, order_by: Optional[str] = None,
q: Optional[str] = None, direction: Optional[str] = None,
page: Optional[int] = 1,
user=Depends(get_admin_user), user=Depends(get_admin_user),
): ):
if q: limit = PAGE_ITEM_COUNT
skip: Optional[int] = None
if page: page = max(1, page)
skip = (page - 1) * limit skip = (page - 1) * limit
return Users.get_users(skip=skip, limit=limit, query_key=q)
else: filter = {}
skip: Optional[int] = None if query:
if page: filter["query"] = query
skip = (page - 1) * limit if order_by:
return Users.get_users(skip=skip, limit=limit) filter["order_by"] = order_by
if direction:
filter["direction"] = direction
return Users.get_users(filter=filter, skip=skip, limit=limit)
@router.get("/all", response_model=UserListResponse)
async def get_all_users(
user=Depends(get_admin_user),
):
return Users.get_users()
############################ ############################

View File

@ -116,65 +116,54 @@ export const updateUserRole = async (token: string, id: string, role: string) =>
return res; return res;
}; };
export const getUsers = async (token: string, page?: number, limit: number = 10, q?: string) => { export const getUsers = async (
token: string,
query?: string,
orderBy?: string,
direction?: string,
page = 1
) => {
let error = null; let error = null;
let res = null; let res = null;
if (q !== undefined) {
res = await fetch(`${WEBUI_API_BASE_URL}/users/?q=${q}`, { let searchParams = new URLSearchParams();
method: 'GET',
headers: { searchParams.set('page', `${page}`);
'Content-Type': 'application/json',
Authorization: `Bearer ${token}` if (query) {
} searchParams.set('query', query);
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.log(err);
error = err.detail;
return null;
});
} else if (page !== undefined) {
res = await fetch(`${WEBUI_API_BASE_URL}/users/?page=${page}&limit=${limit}`, {
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.log(err);
error = err.detail;
return null;
});
} else {
res = await fetch(`${WEBUI_API_BASE_URL}/users/`, {
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.log(err);
error = err.detail;
return null;
});
} }
if (orderBy) {
searchParams.set('order_by', orderBy);
}
if (direction) {
searchParams.set('direction', direction);
}
res = await fetch(`${WEBUI_API_BASE_URL}/users/?${searchParams.toString()}`, {
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.log(err);
error = err.detail;
return null;
});
if (error) { if (error) {
throw error; throw error;
} }
return res ? res : [];
return res;
}; };
export const getUserSettings = async (token: string) => { export const getUserSettings = async (token: string) => {

View File

@ -5,34 +5,19 @@
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { user } from '$lib/stores'; import { user } from '$lib/stores';
import { getUsers } from '$lib/apis/users';
import UserList from './Users/UserList.svelte'; import UserList from './Users/UserList.svelte';
import Groups from './Users/Groups.svelte'; import Groups from './Users/Groups.svelte';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
let users = [];
let totalUsers = 0;
let selectedTab = 'overview'; let selectedTab = 'overview';
let loaded = false; let loaded = false;
$: if (selectedTab) {
getUsersHandler();
}
const getUsersHandler = async () => {
users = await getUsers(localStorage.token);
};
onMount(async () => { onMount(async () => {
if ($user?.role !== 'admin') { if ($user?.role !== 'admin') {
await goto('/'); await goto('/');
} else {
users = await getUsers(localStorage.token);
totalUsers = users.length;
} }
loaded = true; loaded = true;
const containerElement = document.getElementById('users-tabs-container'); const containerElement = document.getElementById('users-tabs-container');
@ -104,9 +89,9 @@
<div class="flex-1 mt-1 lg:mt-0 overflow-y-scroll"> <div class="flex-1 mt-1 lg:mt-0 overflow-y-scroll">
{#if selectedTab === 'overview'} {#if selectedTab === 'overview'}
<UserList {totalUsers}/> <UserList />
{:else if selectedTab === 'groups'} {:else if selectedTab === 'groups'}
<Groups {users} /> <Groups />
{/if} {/if}
</div> </div>
</div> </div>

View File

@ -33,14 +33,17 @@
const i18n = getContext('i18n'); const i18n = getContext('i18n');
export let totalUsers = 0;
let users = []
let search = '';
let selectedUser = null;
let page = 1; let page = 1;
let users = [];
let total = 0;
let query = '';
let orderBy = 'created_at'; // default sort key
let direction = 'asc'; // default sort order
let selectedUser = null;
let showDeleteConfirmDialog = false; let showDeleteConfirmDialog = false;
let showAddUserModal = false; let showAddUserModal = false;
@ -54,7 +57,7 @@
}); });
if (res) { if (res) {
users = await getUsers(localStorage.token); getUserList();
} }
}; };
@ -64,48 +67,43 @@
return null; return null;
}); });
if (res) { if (res) {
users = await getUsers(localStorage.token); getUserList();
} }
}; };
const fetchUserPage = async () => { const setSortKey = (key) => {
try { if (orderBy === key) {
users = await getUsers(localStorage.token, page); direction = direction === 'asc' ? 'desc' : 'asc';
} catch (err) {
console.error("Error fetching users: " + err);
}
};
let sortKey = 'created_at'; // default sort key
let sortOrder = 'asc'; // default sort order
function setSortKey(key) {
if (sortKey === key) {
sortOrder = sortOrder === 'asc' ? 'desc' : 'asc';
} else { } else {
sortKey = key; orderBy = key;
sortOrder = 'asc'; direction = 'asc';
} }
} };
const queryUser = async (q) => { const getUserList = async () => {
try { try {
const result = await getUsers(localStorage.token, undefined, 10, q); const res = await getUsers(localStorage.token, query, orderBy, direction, page).catch(
filteredUsers = result.slice((page - 1) * 10, page * 10); (error) => {
toast.error(`${error}`);
return null;
}
);
if (res) {
users = res.users;
total = res.total;
}
} catch (err) { } catch (err) {
console.error(err); console.error(err);
} }
}; };
let filteredUsers; $: if (page) {
$: if (search.trim() === '') { getUserList();
filteredUsers = users }
.sort((a, b) => {
if (a[sortKey] < b[sortKey]) return sortOrder === 'asc' ? -1 : 1; $: if (query !== null && orderBy && direction) {
if (a[sortKey] > b[sortKey]) return sortOrder === 'asc' ? 1 : -1; getUserList();
return 0;
})
} else {
queryUser(search);
} }
</script> </script>
@ -122,7 +120,7 @@
{selectedUser} {selectedUser}
sessionUser={$user} sessionUser={$user}
on:save={async () => { on:save={async () => {
users = await getUsers(localStorage.token); getUserList();
}} }}
/> />
{/key} {/key}
@ -130,7 +128,7 @@
<AddUserModal <AddUserModal
bind:show={showAddUserModal} bind:show={showAddUserModal}
on:save={async () => { on:save={async () => {
users = await getUsers(localStorage.token); getUserList();
}} }}
/> />
<UserChatsModal bind:show={showUserChatsModal} user={selectedUser} /> <UserChatsModal bind:show={showUserChatsModal} user={selectedUser} />
@ -193,7 +191,7 @@
</div> </div>
<input <input
class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-hidden bg-transparent" class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-hidden bg-transparent"
bind:value={search} bind:value={query}
placeholder={$i18n.t('Search')} placeholder={$i18n.t('Search')}
/> />
</div> </div>
@ -232,9 +230,9 @@
<div class="flex gap-1.5 items-center"> <div class="flex gap-1.5 items-center">
{$i18n.t('Role')} {$i18n.t('Role')}
{#if sortKey === 'role'} {#if orderBy === 'role'}
<span class="font-normal" <span class="font-normal"
>{#if sortOrder === 'asc'} >{#if direction === 'asc'}
<ChevronUp className="size-2" /> <ChevronUp className="size-2" />
{:else} {:else}
<ChevronDown className="size-2" /> <ChevronDown className="size-2" />
@ -255,9 +253,9 @@
<div class="flex gap-1.5 items-center"> <div class="flex gap-1.5 items-center">
{$i18n.t('Name')} {$i18n.t('Name')}
{#if sortKey === 'name'} {#if orderBy === 'name'}
<span class="font-normal" <span class="font-normal"
>{#if sortOrder === 'asc'} >{#if direction === 'asc'}
<ChevronUp className="size-2" /> <ChevronUp className="size-2" />
{:else} {:else}
<ChevronDown className="size-2" /> <ChevronDown className="size-2" />
@ -278,9 +276,9 @@
<div class="flex gap-1.5 items-center"> <div class="flex gap-1.5 items-center">
{$i18n.t('Email')} {$i18n.t('Email')}
{#if sortKey === 'email'} {#if orderBy === 'email'}
<span class="font-normal" <span class="font-normal"
>{#if sortOrder === 'asc'} >{#if direction === 'asc'}
<ChevronUp className="size-2" /> <ChevronUp className="size-2" />
{:else} {:else}
<ChevronDown className="size-2" /> <ChevronDown className="size-2" />
@ -302,9 +300,9 @@
<div class="flex gap-1.5 items-center"> <div class="flex gap-1.5 items-center">
{$i18n.t('Last Active')} {$i18n.t('Last Active')}
{#if sortKey === 'last_active_at'} {#if orderBy === 'last_active_at'}
<span class="font-normal" <span class="font-normal"
>{#if sortOrder === 'asc'} >{#if direction === 'asc'}
<ChevronUp className="size-2" /> <ChevronUp className="size-2" />
{:else} {:else}
<ChevronDown className="size-2" /> <ChevronDown className="size-2" />
@ -324,9 +322,9 @@
> >
<div class="flex gap-1.5 items-center"> <div class="flex gap-1.5 items-center">
{$i18n.t('Created at')} {$i18n.t('Created at')}
{#if sortKey === 'created_at'} {#if orderBy === 'created_at'}
<span class="font-normal" <span class="font-normal"
>{#if sortOrder === 'asc'} >{#if direction === 'asc'}
<ChevronUp className="size-2" /> <ChevronUp className="size-2" />
{:else} {:else}
<ChevronDown className="size-2" /> <ChevronDown className="size-2" />
@ -348,9 +346,9 @@
<div class="flex gap-1.5 items-center"> <div class="flex gap-1.5 items-center">
{$i18n.t('OAuth ID')} {$i18n.t('OAuth ID')}
{#if sortKey === 'oauth_sub'} {#if orderBy === 'oauth_sub'}
<span class="font-normal" <span class="font-normal"
>{#if sortOrder === 'asc'} >{#if direction === 'asc'}
<ChevronUp className="size-2" /> <ChevronUp className="size-2" />
{:else} {:else}
<ChevronDown className="size-2" /> <ChevronDown className="size-2" />
@ -368,7 +366,7 @@
</tr> </tr>
</thead> </thead>
<tbody class=""> <tbody class="">
{#each filteredUsers as user, userIdx} {#each users as user, userIdx}
<tr class="bg-white dark:bg-gray-900 dark:border-gray-850 text-xs"> <tr class="bg-white dark:bg-gray-900 dark:border-gray-850 text-xs">
<td class="px-3 py-1 min-w-[7rem] w-28"> <td class="px-3 py-1 min-w-[7rem] w-28">
<button <button
@ -495,10 +493,10 @@
{$i18n.t("Click on the user role button to change a user's role.")} {$i18n.t("Click on the user role button to change a user's role.")}
</div> </div>
<Pagination bind:page count={totalUsers} perPage={10}/> <Pagination bind:page count={total} perPage={10} />
{#if !$config?.license_metadata} {#if !$config?.license_metadata}
{#if totalUsers > 50} {#if total > 50}
<div class="text-sm"> <div class="text-sm">
<Markdown <Markdown
content={` content={`