| <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} |
|
|