mirror of
https://github.com/open-webui/open-webui.git
synced 2025-12-12 04:15:25 +00:00
Merge 886f313c0b into 3ed1df2e53
This commit is contained in:
commit
39c642acdd
3 changed files with 156 additions and 127 deletions
|
|
@ -78,7 +78,7 @@ async def create_new_group(form_data: GroupForm, user=Depends(get_admin_user)):
|
||||||
|
|
||||||
|
|
||||||
@router.get("/id/{id}", response_model=Optional[GroupResponse])
|
@router.get("/id/{id}", response_model=Optional[GroupResponse])
|
||||||
async def get_group_by_id(id: str, user=Depends(get_admin_user)):
|
async def get_group_by_id(id: str, user=Depends(get_verified_user)):
|
||||||
group = Groups.get_group_by_id(id)
|
group = Groups.get_group_by_id(id)
|
||||||
if group:
|
if group:
|
||||||
return GroupResponse(
|
return GroupResponse(
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,11 @@
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
import { getGroups } from '$lib/apis/groups';
|
|
||||||
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 Plus from '$lib/components/icons/Plus.svelte';
|
||||||
import UserCircleSolid from '$lib/components/icons/UserCircleSolid.svelte';
|
import UserCircleSolid from '$lib/components/icons/UserCircleSolid.svelte';
|
||||||
import XMark from '$lib/components/icons/XMark.svelte';
|
import XMark from '$lib/components/icons/XMark.svelte';
|
||||||
|
import MemberSelector from '$lib/components/workspace/common/MemberSelector.svelte';
|
||||||
import Badge from '$lib/components/common/Badge.svelte';
|
import Badge from '$lib/components/common/Badge.svelte';
|
||||||
|
|
||||||
export let onChange: Function = () => {};
|
export let onChange: Function = () => {};
|
||||||
|
|
@ -18,8 +18,6 @@
|
||||||
export let share = true;
|
export let share = true;
|
||||||
export let sharePublic = true;
|
export let sharePublic = true;
|
||||||
|
|
||||||
let selectedGroupId = '';
|
|
||||||
let groups = [];
|
|
||||||
|
|
||||||
$: if (!sharePublic && accessControl === null) {
|
$: if (!sharePublic && accessControl === null) {
|
||||||
initPublicAccess();
|
initPublicAccess();
|
||||||
|
|
@ -41,8 +39,8 @@
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
groups = await getGroups(localStorage.token, true);
|
|
||||||
|
|
||||||
if (accessControl === null) {
|
if (accessControl === null) {
|
||||||
initPublicAccess();
|
initPublicAccess();
|
||||||
|
|
@ -144,109 +142,21 @@
|
||||||
|
|
||||||
{#if share}
|
{#if share}
|
||||||
{#if accessControl !== null}
|
{#if accessControl !== null}
|
||||||
{@const accessGroups = groups.filter((group) =>
|
<div class="mt-4">
|
||||||
(accessControl?.read?.group_ids ?? []).includes(group.id)
|
|
||||||
)}
|
|
||||||
<div>
|
|
||||||
<div class="">
|
<div class="">
|
||||||
<div class="flex justify-between mb-2.5">
|
<div class="flex justify-between mb-2.5">
|
||||||
<div class="text-xs font-medium text-gray-500">
|
<div class="text-xs font-medium text-gray-500">
|
||||||
{$i18n.t('Groups')}
|
{$i18n.t('Add Members')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if accessGroups.length > 0}
|
|
||||||
<div class="flex flex-col gap-1.5 mb-2 px-0.5 mx-0.5">
|
|
||||||
{#each accessGroups as group}
|
|
||||||
<div class="flex items-center gap-3 justify-between text-sm w-full transition">
|
|
||||||
<div class="flex items-center gap-1.5 w-full">
|
|
||||||
<div>
|
|
||||||
{group.name} <span class="text-xs text-gray-500">{group?.member_count}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="w-full flex justify-end items-center gap-0.5">
|
|
||||||
<button
|
|
||||||
class=""
|
|
||||||
type="button"
|
|
||||||
on:click={() => {
|
|
||||||
if (accessRoles.includes('write')) {
|
|
||||||
if ((accessControl?.write?.group_ids ?? []).includes(group.id)) {
|
|
||||||
accessControl.write.group_ids = (
|
|
||||||
accessControl?.write?.group_ids ?? []
|
|
||||||
).filter((group_id) => group_id !== group.id);
|
|
||||||
} else {
|
|
||||||
accessControl.write.group_ids = [
|
|
||||||
...(accessControl?.write?.group_ids ?? []),
|
|
||||||
group.id
|
|
||||||
];
|
|
||||||
}
|
|
||||||
onChange(accessControl);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{#if (accessControl?.write?.group_ids ?? []).includes(group.id)}
|
|
||||||
<Badge type={'success'} content={$i18n.t('Write')} />
|
|
||||||
{:else}
|
|
||||||
<Badge type={'info'} content={$i18n.t('Read')} />
|
|
||||||
{/if}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
class=" rounded-full p-1 hover:bg-gray-100 dark:hover:bg-gray-850 transition"
|
|
||||||
type="button"
|
|
||||||
on:click={() => {
|
|
||||||
accessControl.read.group_ids = (
|
|
||||||
accessControl?.read?.group_ids ?? []
|
|
||||||
).filter((id) => id !== group.id);
|
|
||||||
accessControl.write.group_ids = (
|
|
||||||
accessControl?.write?.group_ids ?? []
|
|
||||||
).filter((id) => id !== group.id);
|
|
||||||
onChange(accessControl);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<XMark />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<!-- <div class="flex items-center justify-center">
|
|
||||||
<div class="text-gray-500 text-xs text-center py-2 px-10">
|
|
||||||
{$i18n.t('No groups with access, add a group to grant access')}
|
|
||||||
</div>
|
|
||||||
</div> -->
|
|
||||||
|
|
||||||
<div class="mb-1">
|
<div class="mb-1">
|
||||||
<div class="flex w-full">
|
<div class="flex w-full">
|
||||||
<div class="flex flex-1 items-center">
|
<div class="flex flex-1 items-center">
|
||||||
<div class="w-full px-0.5">
|
<div class="w-full px-0.5">
|
||||||
<select
|
<div class="">
|
||||||
class="dark:bg-gray-900 outline-hidden bg-transparent text-sm block w-full pr-10 max-w-full
|
<MemberSelector bind:accessControl bind:accessRoles {onChange} includeGroups={true} manageAccess={true} />
|
||||||
{selectedGroupId ? '' : 'text-gray-500'}
|
</div>
|
||||||
dark:placeholder-gray-500"
|
|
||||||
bind:value={selectedGroupId}
|
|
||||||
on:change={() => {
|
|
||||||
if (selectedGroupId !== '') {
|
|
||||||
accessControl.read.group_ids = [
|
|
||||||
...(accessControl?.read?.group_ids ?? []),
|
|
||||||
selectedGroupId
|
|
||||||
];
|
|
||||||
|
|
||||||
selectedGroupId = '';
|
|
||||||
onChange(accessControl);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<option class=" text-gray-700" value="" disabled selected
|
|
||||||
>{$i18n.t('Select a group')}</option
|
|
||||||
>
|
|
||||||
{#each groups.filter((group) => !(accessControl?.read?.group_ids ?? []).includes(group.id)) as group}
|
|
||||||
<option class=" text-gray-700" value={group.id}>{group.name}</option>
|
|
||||||
{/each}
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -16,13 +16,19 @@
|
||||||
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
|
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
|
||||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||||
import Checkbox from '$lib/components/common/Checkbox.svelte';
|
import Checkbox from '$lib/components/common/Checkbox.svelte';
|
||||||
import { getGroups } from '$lib/apis/groups';
|
import Badge from '$lib/components/common/Badge.svelte';
|
||||||
|
import { getGroups, getGroupById } from '$lib/apis/groups';
|
||||||
|
|
||||||
|
export let onChange: Function = () => {};
|
||||||
|
|
||||||
export let includeGroups = true;
|
export let includeGroups = true;
|
||||||
export let pagination = false;
|
export let pagination = false;
|
||||||
|
export let manageAccess = false;
|
||||||
|
|
||||||
export let groupIds = [];
|
export let groupIds = [];
|
||||||
export let userIds = [];
|
export let userIds = [];
|
||||||
|
export let accessRoles = ['read'];
|
||||||
|
export let accessControl = {};
|
||||||
|
|
||||||
let groups = null;
|
let groups = null;
|
||||||
let filteredGroups = [];
|
let filteredGroups = [];
|
||||||
|
|
@ -31,7 +37,7 @@
|
||||||
? groups.filter((group) => group.name.toLowerCase().includes(query.toLowerCase()))
|
? groups.filter((group) => group.name.toLowerCase().includes(query.toLowerCase()))
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
let selectedGroup = {};
|
let selectedGroups = {};
|
||||||
let selectedUsers = {};
|
let selectedUsers = {};
|
||||||
|
|
||||||
let page = 1;
|
let page = 1;
|
||||||
|
|
@ -66,6 +72,10 @@
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
groups = await getGroups(localStorage.token, true);
|
groups = await getGroups(localStorage.token, true);
|
||||||
|
if(manageAccess){
|
||||||
|
userIds = accessControl?.read?.user_ids ?? [];
|
||||||
|
groupIds = accessControl?.read?.group_ids ?? [];
|
||||||
|
}
|
||||||
if (userIds.length > 0) {
|
if (userIds.length > 0) {
|
||||||
userIds.forEach(async (id) => {
|
userIds.forEach(async (id) => {
|
||||||
const res = await getUserById(localStorage.token, id).catch((error) => {
|
const res = await getUserById(localStorage.token, id).catch((error) => {
|
||||||
|
|
@ -77,6 +87,27 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (groupIds.length > 0) {
|
||||||
|
groupIds.forEach(async (id) => {
|
||||||
|
const res = await getGroupById(localStorage.token, id).catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
if (res) {
|
||||||
|
selectedGroups[id] = res;
|
||||||
|
// Check if group with this ID already exists in groups array
|
||||||
|
const existingGroupIndex = groups.findIndex(group => group.id === res.id);
|
||||||
|
if (existingGroupIndex === -1) {
|
||||||
|
// If not found, add the new group
|
||||||
|
groups = [
|
||||||
|
...groups,
|
||||||
|
res
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log(selectedUsers)
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -86,7 +117,7 @@
|
||||||
<Spinner className="size-5" />
|
<Spinner className="size-5" />
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
{#if groupIds.length > 0}
|
{#if groupIds.length > 0 && !manageAccess}
|
||||||
<div class="mx-1 mb-1.5">
|
<div class="mx-1 mb-1.5">
|
||||||
<div class="text-xs text-gray-500 mx-0.5 mb-1">
|
<div class="text-xs text-gray-500 mx-0.5 mb-1">
|
||||||
{groupIds.length}
|
{groupIds.length}
|
||||||
|
|
@ -94,18 +125,18 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-1 flex-wrap">
|
<div class="flex gap-1 flex-wrap">
|
||||||
{#each groupIds as id}
|
{#each groupIds as id}
|
||||||
{#if selectedGroup[id]}
|
{#if selectedGroups[id]}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="inline-flex items-center space-x-1 px-2 py-1 bg-gray-100/50 dark:bg-gray-850 rounded-lg text-xs"
|
class="inline-flex items-center space-x-1 px-2 py-1 bg-gray-100/50 dark:bg-gray-850 rounded-lg text-xs"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
groupIds = groupIds.filter((gid) => gid !== id);
|
groupIds = groupIds.filter((gid) => gid !== id);
|
||||||
delete selectedGroup[id];
|
delete selectedGroups[id];
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
{selectedGroup[id].name}
|
{selectedGroups[id].name}
|
||||||
<span class="text-xs text-gray-500">{selectedGroup[id].member_count}</span>
|
<span class="text-xs text-gray-500">{selectedGroups[id].member_count}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -118,7 +149,7 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if userIds.length > 0}
|
{#if userIds.length > 0 && !manageAccess}
|
||||||
<div class="mx-1 mb-1.5">
|
<div class="mx-1 mb-1.5">
|
||||||
<div class="text-xs text-gray-500 mx-0.5 mb-1">
|
<div class="text-xs text-gray-500 mx-0.5 mb-1">
|
||||||
{userIds.length}
|
{userIds.length}
|
||||||
|
|
@ -190,12 +221,45 @@
|
||||||
class=" dark:border-gray-850 text-xs flex items-center justify-between w-full"
|
class=" dark:border-gray-850 text-xs flex items-center justify-between w-full"
|
||||||
type="button"
|
type="button"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
|
if (manageAccess && !accessRoles.includes('write')) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if ((groupIds ?? []).includes(group.id)) {
|
if ((groupIds ?? []).includes(group.id)) {
|
||||||
groupIds = groupIds.filter((id) => id !== group.id);
|
if (!manageAccess || (manageAccess && (accessControl?.write?.group_ids ?? []).includes(group.id))){
|
||||||
delete selectedGroup[group.id];
|
groupIds = groupIds.filter((id) => id !== group.id);
|
||||||
|
delete selectedGroups[group.id];
|
||||||
|
}
|
||||||
|
|
||||||
|
if(manageAccess){
|
||||||
|
if ((accessControl?.write?.group_ids ?? []).includes(group.id)) {
|
||||||
|
accessControl.write.group_ids = (
|
||||||
|
accessControl?.write?.group_ids ?? []
|
||||||
|
).filter((group_id) => group_id !== group.id);
|
||||||
|
accessControl.read.group_ids = (
|
||||||
|
accessControl?.read?.group_ids ?? []
|
||||||
|
).filter((group_id) => group_id !== group.id);
|
||||||
|
} else {
|
||||||
|
accessControl.write.group_ids = [
|
||||||
|
...(accessControl?.write?.group_ids ?? []),
|
||||||
|
group.id
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
groupIds = [...groupIds, group.id];
|
groupIds = [...groupIds, group.id];
|
||||||
selectedGroup[group.id] = group;
|
selectedGroups[group.id] = group;
|
||||||
|
if(manageAccess){
|
||||||
|
accessControl.read.group_ids = [
|
||||||
|
...(accessControl?.read?.group_ids ?? []),
|
||||||
|
group.id
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(manageAccess){
|
||||||
|
onChange(accessControl);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
@ -211,9 +275,15 @@
|
||||||
|
|
||||||
<div class="px-3 py-1">
|
<div class="px-3 py-1">
|
||||||
<div class=" translate-y-0.5">
|
<div class=" translate-y-0.5">
|
||||||
<Checkbox
|
{#if manageAccess && (accessControl?.write?.group_ids ?? []).includes(group.id)}
|
||||||
state={(groupIds ?? []).includes(group.id) ? 'checked' : 'unchecked'}
|
<Badge type={'success'} content={$i18n.t('Write')} />
|
||||||
/>
|
{:else if manageAccess && (accessControl?.read?.group_ids ?? []).includes(group.id)}
|
||||||
|
<Badge type={'info'} content={$i18n.t('Read')} />
|
||||||
|
{:else}
|
||||||
|
<Checkbox
|
||||||
|
state={(groupIds ?? []).includes(group.id) ? 'checked' : 'unchecked'}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -232,27 +302,70 @@
|
||||||
class=" dark:border-gray-850 text-xs flex items-center justify-between w-full"
|
class=" dark:border-gray-850 text-xs flex items-center justify-between w-full"
|
||||||
type="button"
|
type="button"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
if ((userIds ?? []).includes(user.id)) {
|
if (manageAccess && !accessRoles.includes('write')) {
|
||||||
userIds = userIds.filter((id) => id !== user.id);
|
return
|
||||||
delete selectedUsers[user.id];
|
}
|
||||||
} else {
|
|
||||||
userIds = [...userIds, user.id];
|
if ((userIds ?? []).includes(user.id)) {
|
||||||
selectedUsers[user.id] = user;
|
if (!manageAccess || (manageAccess && (accessControl?.write?.user_ids ?? []).includes(user.id))){
|
||||||
}
|
userIds = userIds.filter((id) => id !== user.id);
|
||||||
|
delete selectedUsers[user.id];
|
||||||
|
}
|
||||||
|
|
||||||
|
if(manageAccess){
|
||||||
|
if ((accessControl?.write?.user_ids ?? []).includes(user.id)) {
|
||||||
|
accessControl.write.user_ids = (
|
||||||
|
accessControl?.write?.user_ids ?? []
|
||||||
|
).filter((user_id) => user_id !== user.id);
|
||||||
|
accessControl.read.user_ids = (
|
||||||
|
accessControl?.read?.user_ids ?? []
|
||||||
|
).filter((user_id) => user_id !== user.id);
|
||||||
|
} else {
|
||||||
|
accessControl.write.user_ids = [
|
||||||
|
...(accessControl?.write?.user_ids ?? []),
|
||||||
|
user.id
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
userIds = [...userIds, user.id];
|
||||||
|
selectedUsers[user.id] = user;
|
||||||
|
if(manageAccess){
|
||||||
|
accessControl.read.user_ids = [
|
||||||
|
...(accessControl?.read?.user_ids ?? []),
|
||||||
|
user.id
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(manageAccess){
|
||||||
|
onChange(accessControl);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class="px-3 py-1.5 font-medium text-gray-900 dark:text-white flex-1">
|
<div class="px-3 py-1.5 font-medium text-gray-900 dark:text-white flex-1">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<ProfilePreview {user} side="right" align="center" sideOffset={6}>
|
{#if manageAccess}
|
||||||
<img
|
<img
|
||||||
class="rounded-2xl w-6 h-6 object-cover flex-shrink-0"
|
class="rounded-2xl w-6 h-6 object-cover flex-shrink-0"
|
||||||
src={`${WEBUI_API_BASE_URL}/users/${user.id}/profile/image`}
|
src={`${WEBUI_API_BASE_URL}/users/${user.id}/profile/image`}
|
||||||
alt="user"
|
alt="user"
|
||||||
/>
|
/>
|
||||||
</ProfilePreview>
|
<Tooltip content={user.email} placement="top-start">
|
||||||
<Tooltip content={user.email} placement="top-start">
|
<div class="font-medium truncate">{user.name} <span class="text-xs text-gray-500">{user?.email}</span></div>
|
||||||
<div class="font-medium truncate">{user.name}</div>
|
</Tooltip>
|
||||||
</Tooltip>
|
{:else}
|
||||||
|
<ProfilePreview {user} side="right" align="center" sideOffset={6}>
|
||||||
|
<img
|
||||||
|
class="rounded-2xl w-6 h-6 object-cover flex-shrink-0"
|
||||||
|
src={`${WEBUI_API_BASE_URL}/users/${user.id}/profile/image`}
|
||||||
|
alt="user"
|
||||||
|
/>
|
||||||
|
</ProfilePreview>
|
||||||
|
<Tooltip content={user.email} placement="top-start">
|
||||||
|
<div class="font-medium truncate">{user.name}</div>
|
||||||
|
</Tooltip>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if user?.is_active}
|
{#if user?.is_active}
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -270,9 +383,15 @@
|
||||||
|
|
||||||
<div class="px-3 py-1">
|
<div class="px-3 py-1">
|
||||||
<div class=" translate-y-0.5">
|
<div class=" translate-y-0.5">
|
||||||
<Checkbox
|
{#if manageAccess && (accessControl?.write?.user_ids ?? []).includes(user.id)}
|
||||||
state={(userIds ?? []).includes(user.id) ? 'checked' : 'unchecked'}
|
<Badge type={'success'} content={$i18n.t('Write')} />
|
||||||
/>
|
{:else if manageAccess && (accessControl?.read?.user_ids ?? []).includes(user.id)}
|
||||||
|
<Badge type={'info'} content={$i18n.t('Read')} />
|
||||||
|
{:else}
|
||||||
|
<Checkbox
|
||||||
|
state={(userIds ?? []).includes(user.id) ? 'checked' : 'unchecked'}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue