This commit is contained in:
Timothy Jaeryang Baek 2026-10-07 11:06:15 +04:00
parent 0f5a58f5fb
commit 27808f6a02
2 changed files with 85 additions and 46 deletions

View file

@ -315,7 +315,7 @@
{/if}
</div>
<div class="flex-1 mt-1 lg:mt-1 lg:h-[30rem] lg:max-h-[30rem] flex flex-col">
<div class="flex-1 min-w-0 mt-1 lg:mt-1 lg:h-[30rem] lg:max-h-[30rem] flex flex-col">
<div class="w-full h-full overflow-y-auto scrollbar-hidden">
{#if selectedTab == 'general'}
<General
@ -343,16 +343,22 @@
defaultPermissions={custom ? inheritedPermissions : defaultPermissions}
/>
{:else if selectedTab == 'users'}
<div class="flex gap-3 mb-3 text-sm">
<div class="flex flex-wrap items-center gap-x-4 gap-y-1 mb-3">
<button
type="button"
class:underline={membershipTab === 'direct'}
aria-pressed={membershipTab === 'direct'}
class="py-1 text-xs font-normal transition {membershipTab === 'direct'
? 'text-gray-900 dark:text-gray-100'
: 'text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200'}"
on:click={() => (membershipTab = 'direct')}
>{$i18n.t('Direct members')}</button
>
<button
type="button"
class:underline={membershipTab === 'inherited'}
aria-pressed={membershipTab === 'inherited'}
class="py-1 text-xs font-normal transition {membershipTab === 'inherited'
? 'text-gray-900 dark:text-gray-100'
: 'text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200'}"
on:click={() => (membershipTab = 'inherited')}
>{$i18n.t('Inherited members')}</button
>

View file

@ -1,7 +1,12 @@
<script lang="ts">
import { getContext } from 'svelte';
import { getGroupMembers } from '$lib/apis/groups';
const i18n = getContext<any>('i18n');
import { WEBUI_API_BASE_URL } from '$lib/constants';
import Pagination from '$lib/components/common/Pagination.svelte';
import Spinner from '$lib/components/common/Spinner.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte';
import Search from '$lib/components/icons/Search.svelte';
const i18n = getContext<import('svelte/store').Writable<import('i18next').i18n>>('i18n');
export let groupId: string;
export let groups: import('../Groups.svelte').GroupListItem[] = [];
let query = '';
@ -9,54 +14,82 @@
$: members = getGroupMembers(localStorage.token, groupId, 'inherited', query, page);
</script>
<div class="space-y-3 text-sm">
<p class="text-xs text-gray-500">
{$i18n.t('Inherited members are managed in their directly assigned groups.')}
</p>
<input
class="w-full rounded-lg border border-gray-200 dark:border-gray-700 bg-transparent px-2 py-1"
aria-label={$i18n.t('Search inherited members')}
placeholder={$i18n.t('Search inherited members')}
bind:value={query}
on:input={() => {
page = 1;
}}
/>
<div class="flex w-full min-w-0 flex-col text-sm">
<div class="mb-1.5 flex items-center">
<div class="self-center mr-3">
<Search />
</div>
<input
class="w-full min-w-0 text-sm pr-4 rounded-r-xl outline-hidden bg-transparent"
aria-label={$i18n.t('Search inherited members')}
placeholder={$i18n.t('Search')}
bind:value={query}
on:input={() => {
page = 1;
}}
/>
</div>
{#await members}
<p>{$i18n.t('Loading...')}</p>
<div class="my-10" role="status" aria-label={$i18n.t('Loading...')}>
<Spinner className="size-5" />
</div>
{:then result}
<p class="text-xs text-gray-500">
<p class="my-2 text-xs text-gray-500 dark:text-gray-400">
{$i18n.t('Inherited members are managed in their directly assigned groups.')}
</p>
{#if result.items.length > 0}
<table class="w-full table-fixed text-left text-sm text-gray-500 dark:text-gray-400">
<thead class="text-xs text-gray-800 uppercase dark:text-gray-200">
<tr class="border-b-[1.5px] border-gray-50/50 dark:border-gray-800/10">
<th scope="col" class="w-1/2 px-2.5 py-1.5">{$i18n.t('Name')}</th>
<th scope="col" class="px-2.5 py-1.5">{$i18n.t('Inherited via')}</th>
</tr>
</thead>
<tbody>
{#each result.items as member (member.id)}
<tr class="text-xs">
<td class="px-3 py-1 font-normal text-gray-900 dark:text-white">
<Tooltip content={member.email} placement="top-start">
<div class="flex min-w-0 items-center gap-2">
<img
class="size-6 shrink-0 rounded-full object-cover"
src={`${WEBUI_API_BASE_URL}/users/${member.id}/profile/image`}
alt=""
/>
<div class="truncate">{member.name}</div>
</div>
</Tooltip>
</td>
<td class="px-3 py-1">
{#each member.via_group_ids as id}
<a
class="block truncate rounded-sm transition hover:text-gray-700 hover:underline dark:hover:text-gray-200"
href={`/admin/users/groups?id=${id}`}
title={groups.find((group) => group.id === id)?.path || id}
>
{groups.find((group) => group.id === id)?.path || id}
</a>
{/each}
</td>
</tr>
{/each}
</tbody>
</table>
{:else}
<div class="px-10 py-2 text-center text-xs text-gray-500">
{$i18n.t('No inherited members')}
</div>
{/if}
<p class="mt-3 text-xs text-gray-500 dark:text-gray-400">
{$i18n.t(
'Direct: {{direct}} · Inherited: {{inherited}} · Total: {{effective}}',
result.counts
)}
</p>
{#each result.items as member (member.id)}
<div class="border-b border-gray-100 dark:border-gray-800 py-2">
<div>{member.name} <span class="text-gray-500">{member.email}</span></div>
<div class="text-xs text-gray-500">
{$i18n.t('Inherited via')}
{#each member.via_group_ids as id, index}
{index ? ', ' : ''}<a class="underline" href={`/admin/users/groups?id=${id}`}
>{groups.find((group) => group.id === id)?.path || id}</a
>
{/each}
</div>
</div>
{:else}<p class="text-gray-500">{$i18n.t('No inherited members')}</p>{/each}
<div class="flex items-center justify-between">
<button type="button" disabled={page <= 1} class="disabled:opacity-40" on:click={() => page--}
>{$i18n.t('Previous')}</button
>
<span>{page} / {Math.max(1, Math.ceil(result.total / 30))}</span>
<button
type="button"
disabled={page * 30 >= result.total}
class="disabled:opacity-40"
on:click={() => page++}>{$i18n.t('Next')}</button
>
</div>
{#if result.total > 30}
<Pagination bind:page count={result.total} perPage={30} />
{/if}
{:catch error}
<p role="alert" class="text-red-500">{String(error)}</p>
<p role="alert" class="py-4 text-center text-xs text-red-500">{String(error)}</p>
{/await}
</div>