mirror of
https://github.com/open-webui/open-webui.git
synced 2025-12-12 12:25:20 +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.env import DATABASE_USER_ACTIVE_STATUS_UPDATE_INTERVAL
|
||||||
from open_webui.models.chats import Chats
|
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
|
from open_webui.utils.misc import throttle
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -95,8 +95,12 @@ class UpdateProfileForm(BaseModel):
|
||||||
date_of_birth: Optional[datetime.date] = None
|
date_of_birth: Optional[datetime.date] = None
|
||||||
|
|
||||||
|
|
||||||
|
class UserGroupIdsModel(UserModel):
|
||||||
|
group_ids: list[str] = []
|
||||||
|
|
||||||
|
|
||||||
class UserListResponse(BaseModel):
|
class UserListResponse(BaseModel):
|
||||||
users: list[UserModel]
|
users: list[UserGroupIdsModel]
|
||||||
total: int
|
total: int
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -222,7 +226,10 @@ class UsersTable:
|
||||||
limit: Optional[int] = None,
|
limit: Optional[int] = None,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
with get_db() as db:
|
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:
|
if filter:
|
||||||
query_key = filter.get("query")
|
query_key = filter.get("query")
|
||||||
|
|
@ -237,7 +244,16 @@ class UsersTable:
|
||||||
order_by = filter.get("order_by")
|
order_by = filter.get("order_by")
|
||||||
direction = filter.get("direction")
|
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":
|
if direction == "asc":
|
||||||
query = query.order_by(User.name.asc())
|
query = query.order_by(User.name.asc())
|
||||||
else:
|
else:
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,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,
|
||||||
|
UserGroupIdsModel,
|
||||||
UserListResponse,
|
UserListResponse,
|
||||||
UserInfoListResponse,
|
UserInfoListResponse,
|
||||||
UserIdNameListResponse,
|
UserIdNameListResponse,
|
||||||
|
|
@ -91,7 +92,25 @@ async def get_users(
|
||||||
if direction:
|
if direction:
|
||||||
filter["direction"] = 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)
|
@router.get("/all", response_model=UserInfoListResponse)
|
||||||
|
|
|
||||||
|
|
@ -160,3 +160,73 @@ export const deleteGroupById = async (token: string, id: string) => {
|
||||||
|
|
||||||
return res;
|
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">
|
<div class=" self-center mr-2">
|
||||||
<UserPlusSolid />
|
<UserPlusSolid />
|
||||||
</div>
|
</div>
|
||||||
<div class=" self-center">{$i18n.t('Users')} ({userCount})</div>
|
<div class=" self-center">{$i18n.t('Users')}</div>
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -232,7 +232,7 @@
|
||||||
{:else if selectedTab == 'permissions'}
|
{:else if selectedTab == 'permissions'}
|
||||||
<Permissions bind:permissions {defaultPermissions} />
|
<Permissions bind:permissions {defaultPermissions} />
|
||||||
{:else if selectedTab == 'users'}
|
{:else if selectedTab == 'users'}
|
||||||
<Users bind:userCount />
|
<Users bind:userCount groupId={group?.id} />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -2,18 +2,18 @@
|
||||||
import { getContext } from 'svelte';
|
import { getContext } from 'svelte';
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
|
import { getUsers } from '$lib/apis/users';
|
||||||
|
import { toast } from 'svelte-sonner';
|
||||||
|
|
||||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
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 Checkbox from '$lib/components/common/Checkbox.svelte';
|
||||||
import Badge from '$lib/components/common/Badge.svelte';
|
import Badge from '$lib/components/common/Badge.svelte';
|
||||||
import Search from '$lib/components/icons/Search.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 Pagination from '$lib/components/common/Pagination.svelte';
|
||||||
|
import { addUserToGroup, removeUserFromGroup } from '$lib/apis/groups';
|
||||||
|
|
||||||
|
export let groupId: string;
|
||||||
export let userCount = 0;
|
export let userCount = 0;
|
||||||
let userIds = [];
|
|
||||||
|
|
||||||
let users = [];
|
let users = [];
|
||||||
let total = 0;
|
let total = 0;
|
||||||
|
|
@ -23,7 +23,13 @@
|
||||||
|
|
||||||
const getUserList = async () => {
|
const getUserList = async () => {
|
||||||
try {
|
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}`);
|
toast.error(`${error}`);
|
||||||
return null;
|
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) {
|
$: if (page) {
|
||||||
getUserList();
|
getUserList();
|
||||||
}
|
}
|
||||||
|
|
@ -50,9 +73,9 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class=" max-h-full">
|
<div class=" max-h-full h-full w-full flex flex-col">
|
||||||
<div class="flex w-full">
|
<div class="w-full h-fit">
|
||||||
<div class="flex flex-1">
|
<div class="flex flex-1 h-fit">
|
||||||
<div class=" self-center mr-3">
|
<div class=" self-center mr-3">
|
||||||
<Search />
|
<Search />
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -64,20 +87,16 @@
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="flex flex-col gap-2.5">
|
||||||
{#if users.length > 0}
|
{#if users.length > 0}
|
||||||
{#each users as user, userIdx (user.id)}
|
{#each users as user, userIdx (user.id)}
|
||||||
<div class="flex flex-row items-center gap-3 w-full text-sm">
|
<div class="flex flex-row items-center gap-3 w-full text-sm">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
state={userIds.includes(user.id) ? 'checked' : 'unchecked'}
|
state={(user?.group_ids ?? []).includes(groupId) ? 'checked' : 'unchecked'}
|
||||||
on:change={(e) => {
|
on:change={(e) => {
|
||||||
if (e.detail === 'checked') {
|
toggleMember(user.id, e.detail);
|
||||||
userIds = [...userIds, user.id];
|
|
||||||
} else {
|
|
||||||
userIds = userIds.filter((id) => id !== user.id);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -89,20 +108,12 @@
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
{#if userIds.includes(user.id)}
|
{#if (user?.group_ids ?? []).includes(groupId)}
|
||||||
<Badge type="success" content="member" />
|
<Badge type="success" content="member" />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
{page}
|
|
||||||
|
|
||||||
{total}
|
|
||||||
|
|
||||||
{#if total > 30}
|
|
||||||
<Pagination bind:page count={total} perPage={30} />
|
|
||||||
{/if}
|
|
||||||
{:else}
|
{:else}
|
||||||
<div class="text-gray-500 text-xs text-center py-2 px-10">
|
<div class="text-gray-500 text-xs text-center py-2 px-10">
|
||||||
{$i18n.t('No users were found.')}
|
{$i18n.t('No users were found.')}
|
||||||
|
|
@ -110,4 +121,8 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{#if total > 30}
|
||||||
|
<Pagination bind:page count={total} perPage={30} />
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue