open-webui / src /lib /components /admin /Users /Groups.svelte
oki692's picture
Deploy Open WebUI
87a665c verified
<script>
import { toast } from 'svelte-sonner';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
dayjs.extend(relativeTime);
import { onMount, getContext } from 'svelte';
import { goto } from '$app/navigation';
import { WEBUI_NAME, config, user, showSidebar, knowledge } from '$lib/stores';
import { WEBUI_BASE_URL } from '$lib/constants';
import Tooltip from '$lib/components/common/Tooltip.svelte';
import Plus from '$lib/components/icons/Plus.svelte';
import UsersSolid from '$lib/components/icons/UsersSolid.svelte';
import ChevronRight from '$lib/components/icons/ChevronRight.svelte';
import Search from '$lib/components/icons/Search.svelte';
import EditGroupModal from './Groups/EditGroupModal.svelte';
import GroupItem from './Groups/GroupItem.svelte';
import XMark from '$lib/components/icons/XMark.svelte';
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
import Check from '$lib/components/icons/Check.svelte';
import Select from '$lib/components/common/Select.svelte';
import { createNewGroup, getGroups } from '$lib/apis/groups';
import {
getUserDefaultPermissions,
getAllUsers,
updateUserDefaultPermissions
} from '$lib/apis/users';
const i18n = getContext('i18n');
let loaded = false;
let groups = [];
let query = '';
let sortBy = 'members';
const sortItems = [
{ value: 'name', label: $i18n.t('Name') },
{ value: 'members', label: $i18n.t('Members') }
];
$: filteredGroups = groups
.filter((group) => {
if (query === '') {
return true;
} else {
let name = group.name.toLowerCase();
const q = query.toLowerCase();
return name.includes(q);
}
})
.sort((a, b) => {
if (sortBy === 'name') {
return a.name.localeCompare(b.name);
} else if (sortBy === 'members') {
return (b.member_count ?? 0) - (a.member_count ?? 0);
}
return 0;
});
let defaultPermissions = {};
let showAddGroupModal = false;
let showDefaultPermissionsModal = false;
const setGroups = async () => {
groups = await getGroups(localStorage.token);
};
const addGroupHandler = async (group) => {
const res = await createNewGroup(localStorage.token, group).catch((error) => {
toast.error(`${error}`);
return null;
});
if (res) {
toast.success($i18n.t('Group created successfully'));
groups = await getGroups(localStorage.token);
}
};
const updateDefaultPermissionsHandler = async (group) => {
console.debug(group.permissions);
const res = await updateUserDefaultPermissions(localStorage.token, group.permissions).catch(
(error) => {
toast.error(`${error}`);
return null;
}
);
if (res) {
toast.success($i18n.t('Default permissions updated successfully'));
defaultPermissions = await getUserDefaultPermissions(localStorage.token);
}
};
onMount(async () => {
if ($user?.role !== 'admin') {
await goto('/');
return;
}
defaultPermissions = await getUserDefaultPermissions(localStorage.token);
await setGroups();
loaded = true;
});
</script>
{#if loaded}
<EditGroupModal
bind:show={showAddGroupModal}
edit={false}
tabs={['general', 'permissions']}
permissions={defaultPermissions}
onSubmit={addGroupHandler}
/>
<div class="flex flex-col gap-1 px-1 mt-1.5 mb-3">
<div class="flex justify-between items-center">
<div class="flex items-center md:self-center text-xl font-medium px-0.5 gap-2 shrink-0">
<div>
{$i18n.t('Groups')}
</div>
<div class="text-lg font-medium text-gray-500 dark:text-gray-500">
{groups.length}
</div>
</div>
<div class="flex w-full justify-end gap-1.5">
<button
class="px-2 py-1.5 rounded-xl bg-black text-white dark:bg-white dark:text-black transition font-medium text-sm flex items-center"
on:click={() => {
showAddGroupModal = !showAddGroupModal;
}}
>
<Plus className="size-3" strokeWidth="2.5" />
<div class="hidden md:block md:ml-1 text-xs">{$i18n.t('New Group')}</div>
</button>
</div>
</div>
</div>
<div
class="py-2 bg-white dark:bg-gray-900 rounded-3xl border border-gray-100/30 dark:border-gray-850/30"
>
<div class="flex items-center w-full space-x-2 py-0.5 px-3.5">
<div class="flex flex-1">
<div class="self-center ml-1 mr-3">
<Search className="size-3.5" />
</div>
<input
class="w-full text-sm py-1 rounded-r-xl outline-hidden bg-transparent"
bind:value={query}
aria-label={$i18n.t('Search Groups')}
placeholder={$i18n.t('Search Groups')}
/>
{#if query}
<div class="self-center pl-1.5 translate-y-[0.5px] rounded-l-xl bg-transparent">
<button
class="p-0.5 rounded-full hover:bg-gray-100 dark:hover:bg-gray-900 transition"
aria-label={$i18n.t('Clear search')}
on:click={() => {
query = '';
}}
>
<XMark className="size-3" strokeWidth="2" />
</button>
</div>
{/if}
</div>
<Select
bind:value={sortBy}
items={sortItems}
placeholder={$i18n.t('Sort by')}
triggerClass="relative flex items-center gap-0.5 px-2.5 py-1.5 text-sm bg-gray-50 dark:bg-gray-850 rounded-xl shrink-0"
align="end"
>
<svelte:fragment slot="trigger" let:selectedLabel>
<span
class="inline-flex h-input px-0.5 outline-hidden bg-transparent truncate placeholder-gray-400 focus:outline-hidden"
>
{selectedLabel}
</span>
<ChevronDown className="size-3.5" strokeWidth="2.5" />
</svelte:fragment>
<svelte:fragment slot="item" let:item let:selected>
{item.label}
{#if selected}
<div class="ml-auto">
<Check />
</div>
{/if}
</svelte:fragment>
</Select>
</div>
{#if filteredGroups.length !== 0}
<div class="my-2 px-3 grid grid-cols-1 gap-1">
{#each filteredGroups as group}
<GroupItem {group} {setGroups} {defaultPermissions} />
{/each}
</div>
{:else}
<div class="w-full h-full flex flex-col justify-center items-center my-16 mb-24">
<div class="max-w-md text-center">
<div class="text-3xl mb-3">👥</div>
<div class="text-lg font-medium mb-1">{$i18n.t('No groups found')}</div>
<div class="text-gray-500 text-center text-xs">
{$i18n.t('Use groups to organize your users and assign permissions.')}
</div>
</div>
</div>
{/if}
</div>
<EditGroupModal
bind:show={showDefaultPermissionsModal}
tabs={['permissions']}
bind:permissions={defaultPermissions}
custom={false}
onSubmit={updateDefaultPermissionsHandler}
/>
<button
class="flex items-center justify-between rounded-lg w-full transition mt-4"
aria-haspopup="dialog"
on:click={() => {
showDefaultPermissionsModal = true;
}}
>
<div class="flex items-center gap-2.5">
<div class="p-1.5 bg-black/5 dark:bg-white/10 rounded-full">
<UsersSolid className="size-4" />
</div>
<div class="text-left">
<div class=" text-sm font-medium">{$i18n.t('Default permissions')}</div>
<div class="flex text-xs mt-0.5">
{$i18n.t('applies to all users with the "user" role')}
</div>
</div>
</div>
<div>
<ChevronRight strokeWidth="2.5" />
</div>
</button>
{/if}