enh: group members selector

This commit is contained in:
Timothy Jaeryang Baek 2025-11-23 18:56:50 -05:00
parent 103f92c8dd
commit 46bbf760e8

View file

@ -2,6 +2,12 @@
import { getContext } from 'svelte'; import { getContext } from 'svelte';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import localizedFormat from 'dayjs/plugin/localizedFormat';
dayjs.extend(relativeTime);
dayjs.extend(localizedFormat);
import { getUsers } from '$lib/apis/users'; import { getUsers } from '$lib/apis/users';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
@ -11,6 +17,9 @@
import Search from '$lib/components/icons/Search.svelte'; import Search from '$lib/components/icons/Search.svelte';
import Pagination from '$lib/components/common/Pagination.svelte'; import Pagination from '$lib/components/common/Pagination.svelte';
import { addUserToGroup, removeUserFromGroup } from '$lib/apis/groups'; import { addUserToGroup, removeUserFromGroup } from '$lib/apis/groups';
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
import ChevronUp from '$lib/components/icons/ChevronUp.svelte';
import { WEBUI_API_BASE_URL } from '$lib/constants';
export let groupId: string; export let groupId: string;
export let userCount = 0; export let userCount = 0;
@ -19,20 +28,28 @@
let total = 0; let total = 0;
let query = ''; let query = '';
let orderBy = `group_id:${groupId}`; // default sort key
let direction = 'desc'; // default sort order
let page = 1; let page = 1;
const setSortKey = (key) => {
if (orderBy === key) {
direction = direction === 'asc' ? 'desc' : 'asc';
} else {
orderBy = key;
direction = 'asc';
}
};
const getUserList = async () => { const getUserList = async () => {
try { try {
const res = await getUsers( const res = await getUsers(localStorage.token, query, orderBy, direction, page).catch(
localStorage.token, (error) => {
query,
`group_id:${groupId}`,
null,
page
).catch((error) => {
toast.error(`${error}`); toast.error(`${error}`);
return null; return null;
}); }
);
if (res) { if (res) {
users = res.users; users = res.users;
@ -60,11 +77,7 @@
getUserList(); getUserList();
}; };
$: if (page) { $: if (page !== null && query !== null && orderBy !== null && direction !== null) {
getUserList();
}
$: if (query !== null) {
getUserList(); getUserList();
} }
@ -87,12 +100,114 @@
</div> </div>
</div> </div>
<div class="flex-1 overflow-y-auto scrollbar-hidden">
<div class="flex flex-col gap-2.5">
{#if users.length > 0} {#if users.length > 0}
{#each users as user, userIdx (user.id)} <div class="scrollbar-hidden relative whitespace-nowrap overflow-x-auto max-w-full">
<div class="flex flex-row items-center gap-3 w-full text-sm"> <table
<div class="flex items-center"> class="w-full text-sm text-left text-gray-500 dark:text-gray-400 table-auto max-w-full"
>
<thead class="text-xs text-gray-800 uppercase bg-transparent dark:text-gray-200">
<tr class=" border-b-[1.5px] border-gray-50 dark:border-gray-850">
<th
scope="col"
class="px-2.5 py-2 cursor-pointer text-left w-8"
on:click={() => setSortKey(`group_id:${groupId}`)}
>
<div class="flex gap-1.5 items-center">
{$i18n.t('MBR')}
{#if orderBy === `group_id:${groupId}`}
<span class="font-normal"
>{#if direction === 'asc'}
<ChevronUp className="size-2" />
{:else}
<ChevronDown className="size-2" />
{/if}
</span>
{:else}
<span class="invisible">
<ChevronUp className="size-2" />
</span>
{/if}
</div>
</th>
<th
scope="col"
class="px-2.5 py-2 cursor-pointer select-none"
on:click={() => setSortKey('role')}
>
<div class="flex gap-1.5 items-center">
{$i18n.t('Role')}
{#if orderBy === 'role'}
<span class="font-normal"
>{#if direction === 'asc'}
<ChevronUp className="size-2" />
{:else}
<ChevronDown className="size-2" />
{/if}
</span>
{:else}
<span class="invisible">
<ChevronUp className="size-2" />
</span>
{/if}
</div>
</th>
<th
scope="col"
class="px-2.5 py-2 cursor-pointer select-none"
on:click={() => setSortKey('name')}
>
<div class="flex gap-1.5 items-center">
{$i18n.t('Name')}
{#if orderBy === 'name'}
<span class="font-normal"
>{#if direction === 'asc'}
<ChevronUp className="size-2" />
{:else}
<ChevronDown className="size-2" />
{/if}
</span>
{:else}
<span class="invisible">
<ChevronUp className="size-2" />
</span>
{/if}
</div>
</th>
<th
scope="col"
class="px-2.5 py-2 cursor-pointer select-none"
on:click={() => setSortKey('last_active_at')}
>
<div class="flex gap-1.5 items-center">
{$i18n.t('Last Active')}
{#if orderBy === 'last_active_at'}
<span class="font-normal"
>{#if direction === 'asc'}
<ChevronUp className="size-2" />
{:else}
<ChevronDown className="size-2" />
{/if}
</span>
{:else}
<span class="invisible">
<ChevronUp className="size-2" />
</span>
{/if}
</div>
</th>
</tr>
</thead>
<tbody class="">
{#each users as user, userIdx}
<tr class="bg-white dark:bg-gray-900 dark:border-gray-850 text-xs">
<td class=" px-3 py-1 w-8">
<div class="flex w-full justify-center">
<Checkbox <Checkbox
state={(user?.group_ids ?? []).includes(groupId) ? 'checked' : 'unchecked'} state={(user?.group_ids ?? []).includes(groupId) ? 'checked' : 'unchecked'}
on:change={(e) => { on:change={(e) => {
@ -100,27 +215,46 @@
}} }}
/> />
</div> </div>
</td>
<div class="flex w-full items-center justify-between overflow-hidden"> <td class="px-3 py-1 min-w-[7rem] w-28">
<div class=" translate-y-0.5">
<Badge
type={user.role === 'admin'
? 'info'
: user.role === 'user'
? 'success'
: 'muted'}
content={$i18n.t(user.role)}
/>
</div>
</td>
<td class="px-3 py-1 font-medium text-gray-900 dark:text-white max-w-48">
<Tooltip content={user.email} placement="top-start"> <Tooltip content={user.email} placement="top-start">
<div class="flex"> <div class="flex items-center">
<div class=" font-medium self-center truncate">{user.name}</div> <img
class="rounded-full w-6 h-6 object-cover mr-2.5 flex-shrink-0"
src={`${WEBUI_API_BASE_URL}/users/${user.id}/profile/image`}
alt="user"
/>
<div class="font-medium truncate">{user.name}</div>
</div> </div>
</Tooltip> </Tooltip>
</td>
{#if (user?.group_ids ?? []).includes(groupId)} <td class=" px-3 py-1">
<Badge type="success" content="member" /> {dayjs(user.last_active_at * 1000).fromNow()}
{/if} </td>
</div> </tr>
</div>
{/each} {/each}
</tbody>
</table>
</div>
{: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.')}
</div> </div>
{/if} {/if}
</div>
</div>
{#if total > 30} {#if total > 30}
<Pagination bind:page count={total} perPage={30} /> <Pagination bind:page count={total} perPage={30} />