mirror of
https://github.com/open-webui/open-webui.git
synced 2025-12-11 20:05:19 +00:00
refac: group members frontend integration
This commit is contained in:
parent
0a72d047ef
commit
73734b186b
5 changed files with 152 additions and 32 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -219,7 +219,7 @@
|
|||
<div class=" self-center mr-2">
|
||||
<UserPlusSolid />
|
||||
</div>
|
||||
<div class=" self-center">{$i18n.t('Users')} ({userCount})</div>
|
||||
<div class=" self-center">{$i18n.t('Users')}</div>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
@ -232,7 +232,7 @@
|
|||
{:else if selectedTab == 'permissions'}
|
||||
<Permissions bind:permissions {defaultPermissions} />
|
||||
{:else if selectedTab == 'users'}
|
||||
<Users bind:userCount />
|
||||
<Users bind:userCount groupId={group?.id} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<div class=" max-h-full">
|
||||
<div class="flex w-full">
|
||||
<div class="flex flex-1">
|
||||
<div class=" max-h-full h-full w-full flex flex-col">
|
||||
<div class="w-full h-fit">
|
||||
<div class="flex flex-1 h-fit">
|
||||
<div class=" self-center mr-3">
|
||||
<Search />
|
||||
</div>
|
||||
|
|
@ -64,20 +87,16 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 overflow-y-auto">
|
||||
<div class="mt-3 overflow-y-auto flex-1">
|
||||
<div class="flex flex-col gap-2.5">
|
||||
{#if users.length > 0}
|
||||
{#each users as user, userIdx (user.id)}
|
||||
<div class="flex flex-row items-center gap-3 w-full text-sm">
|
||||
<div class="flex items-center">
|
||||
<Checkbox
|
||||
state={userIds.includes(user.id) ? 'checked' : 'unchecked'}
|
||||
state={(user?.group_ids ?? []).includes(groupId) ? 'checked' : 'unchecked'}
|
||||
on:change={(e) => {
|
||||
if (e.detail === 'checked') {
|
||||
userIds = [...userIds, user.id];
|
||||
} else {
|
||||
userIds = userIds.filter((id) => id !== user.id);
|
||||
}
|
||||
toggleMember(user.id, e.detail);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -89,20 +108,12 @@
|
|||
</div>
|
||||
</Tooltip>
|
||||
|
||||
{#if userIds.includes(user.id)}
|
||||
{#if (user?.group_ids ?? []).includes(groupId)}
|
||||
<Badge type="success" content="member" />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
{page}
|
||||
|
||||
{total}
|
||||
|
||||
{#if total > 30}
|
||||
<Pagination bind:page count={total} perPage={30} />
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="text-gray-500 text-xs text-center py-2 px-10">
|
||||
{$i18n.t('No users were found.')}
|
||||
|
|
@ -110,4 +121,8 @@
|
|||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if total > 30}
|
||||
<Pagination bind:page count={total} perPage={30} />
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue