mirror of
https://github.com/open-webui/open-webui
synced 2025-06-26 18:26:48 +00:00
wip: frontend
This commit is contained in:
148
src/lib/components/admin/Users/Groups/AddGroupModal.svelte
Normal file
148
src/lib/components/admin/Users/Groups/AddGroupModal.svelte
Normal file
@@ -0,0 +1,148 @@
|
||||
<script lang="ts">
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { getContext, onMount } from 'svelte';
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
import Modal from '$lib/components/common/Modal.svelte';
|
||||
import Textarea from '$lib/components/common/Textarea.svelte';
|
||||
export let onSubmit: Function = () => {};
|
||||
export let show = false;
|
||||
|
||||
let name = '';
|
||||
let description = '';
|
||||
let userIds = [];
|
||||
|
||||
let loading = false;
|
||||
|
||||
const submitHandler = async () => {
|
||||
loading = true;
|
||||
|
||||
const group = {
|
||||
name,
|
||||
description,
|
||||
user_ids: userIds
|
||||
};
|
||||
|
||||
await onSubmit(group);
|
||||
|
||||
loading = false;
|
||||
show = false;
|
||||
|
||||
name = '';
|
||||
description = '';
|
||||
userIds = [];
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
console.log('mounted');
|
||||
});
|
||||
</script>
|
||||
|
||||
<Modal size="sm" bind:show>
|
||||
<div>
|
||||
<div class=" flex justify-between dark:text-gray-100 px-5 pt-4 mb-1.5">
|
||||
<div class=" text-lg font-medium self-center font-primary">
|
||||
{$i18n.t('Add User Group')}
|
||||
</div>
|
||||
<button
|
||||
class="self-center"
|
||||
on:click={() => {
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<path
|
||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col md:flex-row w-full px-4 pb-4 md:space-x-4 dark:text-gray-200">
|
||||
<div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
|
||||
<form
|
||||
class="flex flex-col w-full"
|
||||
on:submit={(e) => {
|
||||
e.preventDefault();
|
||||
submitHandler();
|
||||
}}
|
||||
>
|
||||
<div class="flex gap-2">
|
||||
<div class="flex flex-col w-full">
|
||||
<div class=" mb-0.5 text-xs text-gray-500">{$i18n.t('Name')}</div>
|
||||
|
||||
<div class="flex-1">
|
||||
<input
|
||||
class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none"
|
||||
type="text"
|
||||
bind:value={name}
|
||||
placeholder={$i18n.t('Group Name')}
|
||||
autocomplete="off"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col w-full mt-2">
|
||||
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Description')}</div>
|
||||
|
||||
<div class="flex-1">
|
||||
<Textarea
|
||||
className="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none resize-none"
|
||||
rows={2}
|
||||
bind:value={description}
|
||||
placeholder={$i18n.t('Group Description')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end pt-3 text-sm font-medium gap-1.5">
|
||||
<button
|
||||
class="px-3.5 py-1.5 text-sm font-medium bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full flex flex-row space-x-1 items-center {loading
|
||||
? ' cursor-not-allowed'
|
||||
: ''}"
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
>
|
||||
{$i18n.t('Create')}
|
||||
|
||||
{#if loading}
|
||||
<div class="ml-2 self-center">
|
||||
<svg
|
||||
class=" w-4 h-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
><style>
|
||||
.spinner_ajPY {
|
||||
transform-origin: center;
|
||||
animation: spinner_AtaB 0.75s infinite linear;
|
||||
}
|
||||
@keyframes spinner_AtaB {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style><path
|
||||
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
|
||||
opacity=".25"
|
||||
/><path
|
||||
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
|
||||
class="spinner_ajPY"
|
||||
/></svg
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
@@ -19,8 +19,14 @@
|
||||
|
||||
export let show = false;
|
||||
export let edit = false;
|
||||
|
||||
export let users = [];
|
||||
export let group = null;
|
||||
|
||||
export let custom = true;
|
||||
|
||||
export let tabs = ['display', 'permissions', 'users'];
|
||||
|
||||
let selectedTab = 'display';
|
||||
|
||||
let name = '';
|
||||
@@ -28,6 +34,7 @@
|
||||
|
||||
let permissions = {};
|
||||
let userIds = [];
|
||||
let adminIds = [];
|
||||
|
||||
let loading = false;
|
||||
|
||||
@@ -65,18 +72,24 @@
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
console.log(tabs);
|
||||
selectedTab = tabs[0];
|
||||
init();
|
||||
});
|
||||
</script>
|
||||
|
||||
<Modal size="sm" bind:show>
|
||||
<div>
|
||||
<div class=" flex justify-between dark:text-gray-100 px-5 pt-4 mb-1">
|
||||
<div class=" flex justify-between dark:text-gray-100 px-5 pt-4 mb-1.5">
|
||||
<div class=" text-lg font-medium self-center font-primary">
|
||||
{#if edit}
|
||||
{$i18n.t('Edit User Group')}
|
||||
{#if custom}
|
||||
{#if edit}
|
||||
{$i18n.t('Edit User Group')}
|
||||
{:else}
|
||||
{$i18n.t('Add User Group')}
|
||||
{/if}
|
||||
{:else}
|
||||
{$i18n.t('Add User Group')}
|
||||
{$i18n.t('Edit Default Permissions')}
|
||||
{/if}
|
||||
</div>
|
||||
<button
|
||||
@@ -108,54 +121,60 @@
|
||||
}}
|
||||
>
|
||||
<div
|
||||
class=" tabs flex flex-row overflow-x-auto gap-2.5 text-sm font-medium mb-3 border-b border-b-gray-800 scrollbar-hidden"
|
||||
class=" tabs flex flex-row overflow-x-auto gap-2.5 text-sm font-medium border-b border-b-gray-800 scrollbar-hidden"
|
||||
>
|
||||
<button
|
||||
class="px-0.5 pb-1.5 min-w-fit flex text-right transition border-b-2 {selectedTab ===
|
||||
'display'
|
||||
? ' dark:border-white'
|
||||
: 'border-transparent text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'}"
|
||||
on:click={() => {
|
||||
selectedTab = 'display';
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{$i18n.t('Display')}
|
||||
</button>
|
||||
{#if tabs.includes('display')}
|
||||
<button
|
||||
class="px-0.5 pb-1.5 min-w-fit flex text-right transition border-b-2 {selectedTab ===
|
||||
'display'
|
||||
? ' dark:border-white'
|
||||
: 'border-transparent text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'}"
|
||||
on:click={() => {
|
||||
selectedTab = 'display';
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{$i18n.t('Display')}
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
class="px-0.5 pb-1.5 min-w-fit flex text-right transition border-b-2 {selectedTab ===
|
||||
'permissions'
|
||||
? ' dark:border-white'
|
||||
: 'border-transparent text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'}"
|
||||
on:click={() => {
|
||||
selectedTab = 'permissions';
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{$i18n.t('Permissions')}
|
||||
</button>
|
||||
{#if tabs.includes('permissions')}
|
||||
<button
|
||||
class="px-0.5 pb-1.5 min-w-fit flex text-right transition border-b-2 {selectedTab ===
|
||||
'permissions'
|
||||
? ' dark:border-white'
|
||||
: 'border-transparent text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'}"
|
||||
on:click={() => {
|
||||
selectedTab = 'permissions';
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{$i18n.t('Permissions')}
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
class="px-0.5 pb-1.5 min-w-fit flex text-right transition border-b-2 {selectedTab ===
|
||||
'users'
|
||||
? ' dark:border-white'
|
||||
: ' border-transparent text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'}"
|
||||
on:click={() => {
|
||||
selectedTab = 'users';
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{$i18n.t('Users')} ({userIds.length})
|
||||
</button>
|
||||
{#if tabs.includes('users')}
|
||||
<button
|
||||
class="px-0.5 pb-1.5 min-w-fit flex text-right transition border-b-2 {selectedTab ===
|
||||
'users'
|
||||
? ' dark:border-white'
|
||||
: ' border-transparent text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'}"
|
||||
on:click={() => {
|
||||
selectedTab = 'users';
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{$i18n.t('Users')} ({userIds.length})
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="px-1 h-96 lg:max-h-96 overflow-y-auto scrollbar-hidden">
|
||||
<div class="px-1 h-96 lg:max-h-96 overflow-y-auto scrollbar-hidden mt-2.5">
|
||||
{#if selectedTab == 'display'}
|
||||
<Display bind:name bind:description />
|
||||
{:else if selectedTab == 'permissions'}
|
||||
<Permissions bind:permissions />
|
||||
<Permissions bind:permissions {custom} />
|
||||
{:else if selectedTab == 'users'}
|
||||
<Users bind:userIds />
|
||||
<Users bind:userIds bind:adminIds {users} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
45
src/lib/components/admin/Users/Groups/GroupItem.svelte
Normal file
45
src/lib/components/admin/Users/Groups/GroupItem.svelte
Normal file
@@ -0,0 +1,45 @@
|
||||
<script>
|
||||
import Pencil from '$lib/components/icons/Pencil.svelte';
|
||||
import User from '$lib/components/icons/User.svelte';
|
||||
|
||||
import UserCircleSolid from '$lib/components/icons/UserCircleSolid.svelte';
|
||||
import GroupModal from './EditGroupModal.svelte';
|
||||
|
||||
export let users = [];
|
||||
export let group = {
|
||||
name: 'Admins',
|
||||
user_ids: [1, 2, 3]
|
||||
};
|
||||
|
||||
let showEdit = false;
|
||||
</script>
|
||||
|
||||
<GroupModal bind:show={showEdit} edit {group} {users} />
|
||||
|
||||
<div class="flex items-center gap-3 justify-between px-1 text-xs w-full transition">
|
||||
<div class="flex items-center gap-1.5 w-full font-medium">
|
||||
<div>
|
||||
<UserCircleSolid className="size-4" />
|
||||
</div>
|
||||
{group.name}
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-1.5 w-full font-medium">
|
||||
{group.user_ids.length}
|
||||
|
||||
<div>
|
||||
<User className="size-3.5" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-full flex justify-end">
|
||||
<button
|
||||
class=" rounded-lg p-1 hover:bg-gray-100 dark:hover:bg-gray-850 transition"
|
||||
on:click={() => {
|
||||
showEdit = true;
|
||||
}}
|
||||
>
|
||||
<Pencil />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -8,6 +8,7 @@
|
||||
import Plus from '$lib/components/icons/Plus.svelte';
|
||||
|
||||
export let permissions = {};
|
||||
export let custom = true;
|
||||
|
||||
let defaultModelId = '';
|
||||
|
||||
@@ -135,17 +136,34 @@
|
||||
<div class=" mb-2 text-sm font-medium">{$i18n.t('Workspace Permissions')}</div>
|
||||
|
||||
<div class=" flex w-full justify-between my-2 pr-2">
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Allow Models Access')}</div>
|
||||
<div class=" self-center text-xs font-medium">
|
||||
{#if custom}
|
||||
{$i18n.t('Admins')}:
|
||||
{/if}
|
||||
{$i18n.t('Models Access')}
|
||||
</div>
|
||||
<Switch bind:state={workspaceModelsAccess} />
|
||||
</div>
|
||||
|
||||
<div class=" flex w-full justify-between my-2 pr-2">
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Allow Knowledge Access')}</div>
|
||||
<div class=" self-center text-xs font-medium">
|
||||
{#if custom}
|
||||
{$i18n.t('Admins')}:
|
||||
{/if}
|
||||
|
||||
{$i18n.t('Knowledge Access')}
|
||||
</div>
|
||||
<Switch bind:state={workspaceKnowledgeAccess} />
|
||||
</div>
|
||||
|
||||
<div class=" flex w-full justify-between my-2 pr-2">
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Allow Prompts Access')}</div>
|
||||
<div class=" self-center text-xs font-medium">
|
||||
{#if custom}
|
||||
{$i18n.t('Admins')}:
|
||||
{/if}
|
||||
|
||||
{$i18n.t('Prompts Access')}
|
||||
</div>
|
||||
<Switch bind:state={workspacePromptsAccess} />
|
||||
</div>
|
||||
|
||||
@@ -166,19 +184,34 @@
|
||||
<div class=" mb-2 text-sm font-medium">{$i18n.t('Chat Permissions')}</div>
|
||||
|
||||
<div class=" flex w-full justify-between my-2 pr-2">
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Allow Chat Deletion')}</div>
|
||||
<div class=" self-center text-xs font-medium">
|
||||
{#if custom}
|
||||
{$i18n.t('Members')}:
|
||||
{/if}
|
||||
{$i18n.t('Allow Chat Deletion')}
|
||||
</div>
|
||||
|
||||
<Switch bind:state={chatDeletion} />
|
||||
</div>
|
||||
|
||||
<div class=" flex w-full justify-between my-2 pr-2">
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Allow Chat Editing')}</div>
|
||||
<div class=" self-center text-xs font-medium">
|
||||
{#if custom}
|
||||
{$i18n.t('Members')}:
|
||||
{/if}
|
||||
{$i18n.t('Allow Chat Editing')}
|
||||
</div>
|
||||
|
||||
<Switch bind:state={chatEdit} />
|
||||
</div>
|
||||
|
||||
<div class=" flex w-full justify-between my-2 pr-2">
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Allow Temporary Chat')}</div>
|
||||
<div class=" self-center text-xs font-medium">
|
||||
{#if custom}
|
||||
{$i18n.t('Members')}:
|
||||
{/if}
|
||||
{$i18n.t('Allow Temporary Chat')}
|
||||
</div>
|
||||
|
||||
<Switch bind:state={chatTemporary} />
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,140 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte';
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
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';
|
||||
|
||||
export let users = [];
|
||||
export let userIds = [];
|
||||
export let adminIds = [];
|
||||
|
||||
let filteredUsers = [];
|
||||
|
||||
$: filteredUsers = users
|
||||
.filter((user) => {
|
||||
if (user?.role === 'admin') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (query === '') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (
|
||||
user.name.toLowerCase().includes(query.toLowerCase()) ||
|
||||
user.email.toLowerCase().includes(query.toLowerCase())
|
||||
);
|
||||
})
|
||||
.sort((a, b) => {
|
||||
const aIsAdmin = adminIds.includes(a.id);
|
||||
const bIsAdmin = adminIds.includes(b.id);
|
||||
const aUserIndex = userIds.indexOf(a.id);
|
||||
const bUserIndex = userIds.indexOf(b.id);
|
||||
|
||||
// Admin users should come first
|
||||
if (aIsAdmin && !bIsAdmin) return -1; // Place 'a' first if it's admin
|
||||
if (!aIsAdmin && bIsAdmin) return 1; // Place 'b' first if it's admin
|
||||
|
||||
// Neither are admin, compare based on userIds or fall back to alphabetical order
|
||||
if (aUserIndex !== -1 && bUserIndex === -1) return -1; // 'a' has valid userId -> prioritize
|
||||
if (bUserIndex !== -1 && aUserIndex === -1) return 1; // 'b' has valid userId -> prioritize
|
||||
|
||||
// Both a and b are either in the userIds array or not, so we'll sort them by their indices
|
||||
if (aUserIndex !== -1 && bUserIndex !== -1) return aUserIndex - bUserIndex;
|
||||
|
||||
// If both are not in the userIds, fallback to alphabetical sorting by name
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
|
||||
let query = '';
|
||||
</script>
|
||||
|
||||
<div>
|
||||
{JSON.stringify(userIds)}
|
||||
<div class="flex w-full">
|
||||
<div class="flex flex-1">
|
||||
<div class=" self-center mr-3">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<input
|
||||
class=" w-full text-sm pr-4 rounded-r-xl outline-none bg-transparent"
|
||||
bind:value={query}
|
||||
placeholder={$i18n.t('Search')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 max-h-[22rem] overflow-y-auto scrollbar-hidden">
|
||||
<div class="flex flex-col gap-2.5">
|
||||
{#if filteredUsers.length > 0}
|
||||
{#each filteredUsers 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'}
|
||||
on:change={(e) => {
|
||||
if (e.detail === 'checked') {
|
||||
userIds = [...userIds, user.id];
|
||||
} else {
|
||||
userIds = userIds.filter((id) => id !== user.id);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex w-full items-center justify-between">
|
||||
<div class="flex">
|
||||
<img
|
||||
class=" rounded-full w-6 h-6 object-cover mr-2.5"
|
||||
src={user.profile_image_url.startsWith(WEBUI_BASE_URL) ||
|
||||
user.profile_image_url.startsWith('https://www.gravatar.com/avatar/') ||
|
||||
user.profile_image_url.startsWith('data:')
|
||||
? user.profile_image_url
|
||||
: `/user.png`}
|
||||
alt="user"
|
||||
/>
|
||||
|
||||
<div class=" font-medium self-center">{user.name}</div>
|
||||
</div>
|
||||
|
||||
{#if userIds.includes(user.id)}
|
||||
<button
|
||||
on:click={() => {
|
||||
adminIds = adminIds.includes(user.id)
|
||||
? adminIds.filter((id) => id !== user.id)
|
||||
: [...adminIds, user.id];
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{#if adminIds.includes(user.id)}
|
||||
<Badge type="info" content="admin" />
|
||||
{:else}
|
||||
<Badge type="success" content="member" />
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{:else}
|
||||
<div class="text-gray-500 text-xs text-center py-2 px-10">
|
||||
{$i18n.t('No users were found.')}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user