refac
This commit is contained in:
1 parent
f8ea15b84a
commit
727041da78
8 files changed
+302
-282
No files matched your search
@@ -1,7 +1,9 @@
|
|||||||
<script>
|
<script>
|
||||||
import { getContext, tick, onMount } from 'svelte';
|
import { getContext, onMount } from 'svelte';
|
||||||
import { goto } from '$app/navigation';
|
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
|
import { adminFeedbackCount, adminLeaderboardCount, models as _models } from '$lib/stores';
|
||||||
|
import { getFeedbackItems, getLeaderboard } from '$lib/apis/evaluations';
|
||||||
|
import { formatNumber } from '$lib/utils';
|
||||||
|
|
||||||
import Leaderboard from './Evaluations/Leaderboard.svelte';
|
import Leaderboard from './Evaluations/Leaderboard.svelte';
|
||||||
import Feedbacks from './Evaluations/Feedbacks.svelte';
|
import Feedbacks from './Evaluations/Feedbacks.svelte';
|
||||||
@@ -28,8 +30,34 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
let loaded = false;
|
let loaded = false;
|
||||||
|
$: formattedLeaderboardCount =
|
||||||
|
$adminLeaderboardCount === null ? null : formatNumber($adminLeaderboardCount);
|
||||||
|
$: formattedFeedbackCount =
|
||||||
|
$adminFeedbackCount === null ? null : formatNumber($adminFeedbackCount);
|
||||||
|
|
||||||
|
const getLeaderboardCount = (res) => {
|
||||||
|
const entries = res?.entries ?? [];
|
||||||
|
const modelMap = new Map(($_models ?? []).map((model) => [model.id, model]));
|
||||||
|
const activeModelCount = ($_models ?? []).filter(
|
||||||
|
(model) => model?.owned_by !== 'arena' && !model?.info?.meta?.hidden
|
||||||
|
).length;
|
||||||
|
const evaluatedModelCount = entries.filter((entry) => !modelMap.has(entry.model_id)).length;
|
||||||
|
|
||||||
|
return activeModelCount + evaluatedModelCount;
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadCounts = async () => {
|
||||||
|
const [leaderboardRes, feedbackRes] = await Promise.all([
|
||||||
|
getLeaderboard(localStorage.token).catch(() => null),
|
||||||
|
getFeedbackItems(localStorage.token, 'updated_at', 'desc', 1).catch(() => null)
|
||||||
|
]);
|
||||||
|
|
||||||
|
adminLeaderboardCount.set(getLeaderboardCount(leaderboardRes));
|
||||||
|
adminFeedbackCount.set(feedbackRes?.total ?? null);
|
||||||
|
};
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
|
await loadCounts();
|
||||||
loaded = true;
|
loaded = true;
|
||||||
|
|
||||||
const containerElement = document.getElementById('users-tabs-container');
|
const containerElement = document.getElementById('users-tabs-container');
|
||||||
@@ -52,30 +80,40 @@
|
|||||||
<div class="flex flex-col lg:flex-row w-full h-full pb-2 lg:space-x-4">
|
<div class="flex flex-col lg:flex-row w-full h-full pb-2 lg:space-x-4">
|
||||||
<div
|
<div
|
||||||
id="users-tabs-container"
|
id="users-tabs-container"
|
||||||
class="tabs mx-2 sm:mx-[16px] lg:mx-0 lg:px-[16px] flex flex-row overflow-x-auto gap-2.5 max-w-full lg:gap-1 lg:flex-col lg:flex-none lg:w-50 dark:text-gray-200 text-sm font-normal text-left scrollbar-none"
|
class="tabs mx-2 px-2 sm:mx-2.5 lg:mx-0 lg:px-2.5 flex flex-row overflow-x-auto gap-2.5 max-w-full lg:gap-0 lg:flex-col lg:flex-none lg:w-50 dark:text-gray-200 text-sm font-normal text-left scrollbar-none"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
id="leaderboard"
|
id="leaderboard"
|
||||||
href="/admin/evaluations/leaderboard"
|
href="/admin/evaluations/leaderboard"
|
||||||
draggable="false"
|
draggable="false"
|
||||||
class="px-0.5 py-1 min-w-fit rounded-lg lg:flex-none flex text-right transition select-none {selectedTab ===
|
class="px-0.5 py-1 min-w-fit rounded-lg lg:flex-none flex items-center gap-1.5 text-right transition select-none {selectedTab ===
|
||||||
'leaderboard'
|
'leaderboard'
|
||||||
? ''
|
? ''
|
||||||
: ' text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'}"
|
: ' text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'}"
|
||||||
>
|
>
|
||||||
<div class=" self-center">{$i18n.t('Leaderboard')}</div>
|
<div class=" self-center">{$i18n.t('Leaderboard')}</div>
|
||||||
|
{#if formattedLeaderboardCount !== null}
|
||||||
|
<div class="self-center text-sm opacity-60">
|
||||||
|
{formattedLeaderboardCount}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
id="feedback"
|
id="feedback"
|
||||||
href="/admin/evaluations/feedback"
|
href="/admin/evaluations/feedback"
|
||||||
draggable="false"
|
draggable="false"
|
||||||
class="px-0.5 py-1 min-w-fit rounded-lg lg:flex-none flex text-right transition select-none {selectedTab ===
|
class="px-0.5 py-1 min-w-fit rounded-lg lg:flex-none flex items-center gap-1.5 text-right transition select-none {selectedTab ===
|
||||||
'feedback'
|
'feedback'
|
||||||
? ''
|
? ''
|
||||||
: ' text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'}"
|
: ' text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'}"
|
||||||
>
|
>
|
||||||
<div class=" self-center">{$i18n.t('Feedback')}</div>
|
<div class=" self-center">{$i18n.t('Feedback')}</div>
|
||||||
|
{#if formattedFeedbackCount !== null}
|
||||||
|
<div class="self-center text-sm opacity-60">
|
||||||
|
{formattedFeedbackCount}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
import ChevronUp from '$lib/components/icons/ChevronUp.svelte';
|
import ChevronUp from '$lib/components/icons/ChevronUp.svelte';
|
||||||
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
|
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
|
||||||
import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
|
import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
|
||||||
import { config } from '$lib/stores';
|
import { adminFeedbackCount, config } from '$lib/stores';
|
||||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||||
import Select from '$lib/components/common/Select.svelte';
|
import Select from '$lib/components/common/Select.svelte';
|
||||||
import Check from '$lib/components/icons/Check.svelte';
|
import Check from '$lib/components/icons/Check.svelte';
|
||||||
@@ -90,6 +90,7 @@
|
|||||||
if (res) {
|
if (res) {
|
||||||
items = res.items;
|
items = res.items;
|
||||||
total = res.total;
|
total = res.total;
|
||||||
|
adminFeedbackCount.set(total);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
@@ -216,27 +217,61 @@
|
|||||||
<Spinner className="size-5" />
|
<Spinner className="size-5" />
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="flex flex-col gap-1 mt-0.5 mb-3">
|
<div class="space-y-1">
|
||||||
<div class="flex justify-between items-center">
|
{#if modelIds.length > 0 || total > 0}
|
||||||
<div class="flex items-center md:self-center text-xl font-normal px-0.5 gap-2 shrink-0">
|
<div class="flex h-8 flex-1 items-center w-full gap-2">
|
||||||
<div>
|
<div
|
||||||
{$i18n.t('Feedback History')}
|
class="flex min-w-0 flex-1 bg-transparent overflow-x-auto scrollbar-none"
|
||||||
|
on:wheel={(e) => {
|
||||||
|
if (e.deltaY !== 0) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.currentTarget.scrollLeft += e.deltaY;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{#if modelIds.length > 0}
|
||||||
|
<div
|
||||||
|
class="flex gap-0.5 w-fit text-center text-sm rounded-full bg-transparent whitespace-nowrap"
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
bind:value={selectedModelId}
|
||||||
|
items={[
|
||||||
|
{ value: '', label: $i18n.t('All') },
|
||||||
|
...modelIds.map((mid) => ({ value: mid, label: mid }))
|
||||||
|
]}
|
||||||
|
placeholder={$i18n.t('All')}
|
||||||
|
triggerClass="relative w-full flex items-center gap-0.5 px-2.5 py-1.5 bg-transparent rounded-xl text-[13px] font-normal text-gray-700 transition hover:text-gray-900 dark:text-gray-200 dark:hover:text-gray-100"
|
||||||
|
onChange={() => {
|
||||||
|
page = 1;
|
||||||
|
getFeedbacks();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svelte:fragment slot="trigger" let:selectedLabel>
|
||||||
|
<span
|
||||||
|
class="inline-flex h-input px-0.5 w-full 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}
|
||||||
|
<div class="ml-auto {selected ? '' : 'invisible'}">
|
||||||
|
<Check />
|
||||||
|
</div>
|
||||||
|
</svelte:fragment>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-lg font-normal text-gray-500 dark:text-gray-500">
|
|
||||||
{total}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex w-full justify-end gap-1.5">
|
|
||||||
{#if total > 0}
|
{#if total > 0}
|
||||||
<Dropdown align="end">
|
<Dropdown align="end">
|
||||||
<button
|
<button
|
||||||
class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-200 transition"
|
class="flex h-8 shrink-0 items-center gap-1 px-2 py-1.5 rounded-xl hover:bg-gray-100 dark:hover:bg-gray-850 dark:text-gray-200 transition text-xs"
|
||||||
>
|
>
|
||||||
<div class="self-center font-normal line-clamp-1">
|
{$i18n.t('Export')}
|
||||||
{$i18n.t('Export')}
|
|
||||||
</div>
|
|
||||||
<ChevronDown className="size-3" strokeWidth="2.5" />
|
<ChevronDown className="size-3" strokeWidth="2.5" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@@ -262,59 +297,9 @@
|
|||||||
</Dropdown>
|
</Dropdown>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</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"
|
|
||||||
>
|
|
||||||
{#if modelIds.length > 0}
|
|
||||||
<div
|
|
||||||
class="px-2.5 flex w-full bg-transparent overflow-x-auto scrollbar-none mb-1"
|
|
||||||
on:wheel={(e) => {
|
|
||||||
if (e.deltaY !== 0) {
|
|
||||||
e.preventDefault();
|
|
||||||
e.currentTarget.scrollLeft += e.deltaY;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="flex gap-0.5 w-fit text-center text-sm rounded-full bg-transparent whitespace-nowrap"
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
bind:value={selectedModelId}
|
|
||||||
items={[
|
|
||||||
{ value: '', label: $i18n.t('All') },
|
|
||||||
...modelIds.map((mid) => ({ value: mid, label: mid }))
|
|
||||||
]}
|
|
||||||
placeholder={$i18n.t('All')}
|
|
||||||
triggerClass="relative w-full flex items-center gap-0.5 px-2.5 py-1.5 bg-gray-50 dark:bg-gray-850 rounded-xl"
|
|
||||||
onChange={() => {
|
|
||||||
page = 1;
|
|
||||||
getFeedbacks();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<svelte:fragment slot="trigger" let:selectedLabel>
|
|
||||||
<span
|
|
||||||
class="inline-flex h-input px-0.5 w-full 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}
|
|
||||||
<div class="ml-auto {selected ? '' : 'invisible'}">
|
|
||||||
<Check />
|
|
||||||
</div>
|
|
||||||
</svelte:fragment>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="scrollbar-hidden relative whitespace-nowrap overflow-x-auto max-w-full px-2">
|
<div class="scrollbar-hidden relative whitespace-nowrap overflow-x-auto max-w-full">
|
||||||
{#if (items ?? []).length === 0}
|
{#if (items ?? []).length === 0}
|
||||||
<div class="w-full h-full flex flex-col justify-center items-center my-16 mb-24">
|
<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="max-w-md text-center">
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount, getContext } from 'svelte';
|
import { getContext } from 'svelte';
|
||||||
import { models } from '$lib/stores';
|
import { adminLeaderboardCount, models } from '$lib/stores';
|
||||||
import { getLeaderboard } from '$lib/apis/evaluations';
|
import { getLeaderboard } from '$lib/apis/evaluations';
|
||||||
import ModelModal from './LeaderboardModal.svelte';
|
import ModelModal from './LeaderboardModal.svelte';
|
||||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
import Search from '$lib/components/icons/Search.svelte';
|
import Search from '$lib/components/icons/Search.svelte';
|
||||||
|
import XMark from '$lib/components/icons/XMark.svelte';
|
||||||
import ChevronUp from '$lib/components/icons/ChevronUp.svelte';
|
import ChevronUp from '$lib/components/icons/ChevronUp.svelte';
|
||||||
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
|
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
|
||||||
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
||||||
@@ -83,6 +84,7 @@
|
|||||||
if (b.rating === '-') return -1;
|
if (b.rating === '-') return -1;
|
||||||
return b.rating - a.rating;
|
return b.rating - a.rating;
|
||||||
});
|
});
|
||||||
|
adminLeaderboardCount.set(rankedModels.length);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Leaderboard load failed:', err);
|
console.error('Leaderboard load failed:', err);
|
||||||
}
|
}
|
||||||
@@ -120,118 +122,133 @@
|
|||||||
|
|
||||||
<ModelModal bind:show={showModal} model={selectedModel} onClose={closeModal} />
|
<ModelModal bind:show={showModal} model={selectedModel} onClose={closeModal} />
|
||||||
|
|
||||||
<div
|
<div class="space-y-1">
|
||||||
class="pt-0.5 pb-1 gap-1 flex flex-col md:flex-row justify-between sticky top-0 z-10 bg-white dark:bg-gray-900"
|
<div class="pt-0.5 pb-1 sticky top-0 z-10 bg-white dark:bg-gray-900">
|
||||||
>
|
<div class="flex h-8 flex-1 items-center w-full gap-2">
|
||||||
<div class="flex items-center text-xl font-normal px-0.5 gap-2 shrink-0">
|
<div class="flex min-w-0 flex-1 items-center">
|
||||||
{$i18n.t('Leaderboard')}
|
<div class="self-center ml-1 mr-3">
|
||||||
<span class="text-lg text-gray-500">{rankedModels.length}</span>
|
<Search className="size-3.5" />
|
||||||
</div>
|
</div>
|
||||||
<Tooltip content={$i18n.t('Re-rank models by topic similarity')}>
|
<input
|
||||||
<div class="flex flex-1">
|
class="w-full text-sm pr-4 py-1 rounded-r-xl outline-hidden bg-transparent"
|
||||||
<Search className="size-3 ml-1 mr-3 self-center" />
|
bind:value={query}
|
||||||
<input
|
aria-label={$i18n.t('Search')}
|
||||||
class="w-full text-sm pr-4 py-1 rounded-r-xl outline-hidden bg-transparent"
|
placeholder={$i18n.t('Search')}
|
||||||
bind:value={query}
|
/>
|
||||||
placeholder={$i18n.t('Search')}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
{#if query}
|
||||||
class="scrollbar-hidden relative whitespace-nowrap overflow-x-auto max-w-full rounded-sm min-h-[100px]"
|
<div class="self-center pl-1.5 translate-y-[0.5px] rounded-l-xl bg-transparent">
|
||||||
>
|
<button
|
||||||
{#if loading}
|
class="p-0.5 rounded-full hover:bg-gray-100 dark:hover:bg-gray-900 transition"
|
||||||
<div
|
aria-label={$i18n.t('Clear search')}
|
||||||
class="absolute inset-0 flex items-center justify-center z-10 bg-white/50 dark:bg-gray-900/50"
|
on:click={() => {
|
||||||
>
|
query = '';
|
||||||
<Spinner className="size-5" />
|
}}
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if !rankedModels.length && !loading}
|
|
||||||
<div class="text-center text-xs text-gray-500 py-1">{$i18n.t('No models found')}</div>
|
|
||||||
{:else if rankedModels.length}
|
|
||||||
<table
|
|
||||||
class="w-full text-sm text-left text-gray-500 dark:text-gray-400 {loading
|
|
||||||
? 'opacity-20'
|
|
||||||
: ''}"
|
|
||||||
>
|
|
||||||
<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/30">
|
|
||||||
{#each [{ key: 'rating', label: 'RK', class: 'w-3' }, { key: 'name', label: 'Model', class: '' }, { key: 'rating', label: 'Rating', class: 'text-right w-fit' }, { key: 'won', label: 'Won', class: 'text-right w-5' }, { key: 'lost', label: 'Lost', class: 'text-right w-5' }] as col}
|
|
||||||
<th
|
|
||||||
scope="col"
|
|
||||||
class="px-2.5 py-2 cursor-pointer select-none {col.class}"
|
|
||||||
on:click={() => toggleSort(col.key)}
|
|
||||||
>
|
>
|
||||||
<div
|
<XMark className="size-3" strokeWidth="2" />
|
||||||
class="flex gap-1.5 items-center {col.class.includes('right') ? 'justify-end' : ''}"
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="scrollbar-hidden relative whitespace-nowrap overflow-x-auto max-w-full rounded-sm min-h-[100px]"
|
||||||
|
>
|
||||||
|
{#if loading}
|
||||||
|
<div
|
||||||
|
class="absolute inset-0 flex items-center justify-center z-10 bg-white/50 dark:bg-gray-900/50"
|
||||||
|
>
|
||||||
|
<Spinner className="size-5" />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if !rankedModels.length && !loading}
|
||||||
|
<div class="text-center text-xs text-gray-500 py-1">{$i18n.t('No models found')}</div>
|
||||||
|
{:else if rankedModels.length}
|
||||||
|
<table
|
||||||
|
class="w-full text-sm text-left text-gray-500 dark:text-gray-400 {loading
|
||||||
|
? 'opacity-20'
|
||||||
|
: ''}"
|
||||||
|
>
|
||||||
|
<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/30">
|
||||||
|
{#each [{ key: 'rating', label: 'RK', class: 'w-3' }, { key: 'name', label: 'Model', class: '' }, { key: 'rating', label: 'Rating', class: 'text-right w-fit' }, { key: 'won', label: 'Won', class: 'text-right w-5' }, { key: 'lost', label: 'Lost', class: 'text-right w-5' }] as col}
|
||||||
|
<th
|
||||||
|
scope="col"
|
||||||
|
class="px-2.5 py-2 cursor-pointer select-none {col.class}"
|
||||||
|
on:click={() => toggleSort(col.key)}
|
||||||
>
|
>
|
||||||
{$i18n.t(col.label)}
|
<div
|
||||||
{#if orderBy === col.key}
|
class="flex gap-1.5 items-center {col.class.includes('right')
|
||||||
{#if direction === 'asc'}<ChevronUp className="size-2" />{:else}<ChevronDown
|
? 'justify-end'
|
||||||
className="size-2"
|
: ''}"
|
||||||
/>{/if}
|
|
||||||
{:else}
|
|
||||||
<span class="invisible"><ChevronUp className="size-2" /></span>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</th>
|
|
||||||
{/each}
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{#each sortedModels as model, idx (model.id)}
|
|
||||||
<tr
|
|
||||||
class="bg-white dark:bg-gray-900 text-xs group cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-850/50 transition"
|
|
||||||
on:click={() => openModal(model)}
|
|
||||||
>
|
|
||||||
<td class="px-3 py-1.5 font-normal text-gray-900 dark:text-white">
|
|
||||||
{model.rating !== '-' ? idx + 1 : '-'}
|
|
||||||
</td>
|
|
||||||
<td class="px-3 py-1.5">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<img
|
|
||||||
src="{WEBUI_API_BASE_URL}/models/model/profile/image?id={model.id}"
|
|
||||||
alt={model.name}
|
|
||||||
class="size-5 rounded-full object-cover shrink-0"
|
|
||||||
on:error={(e) => {
|
|
||||||
e.target.src = '/favicon.png';
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Tooltip content={`${model.name} (${model.id})`} placement="top-start">
|
|
||||||
<span class="font-normal text-gray-800 dark:text-gray-200 line-clamp-1"
|
|
||||||
>{model.name}</span
|
|
||||||
>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="px-3 py-1.5 text-right font-normal text-gray-900 dark:text-white">
|
|
||||||
{model.rating}
|
|
||||||
</td>
|
|
||||||
<td class="px-3 py-1.5 text-right font-normal text-green-500 w-10">
|
|
||||||
{#if model.stats.won === '-'}-{:else}
|
|
||||||
<span class="hidden group-hover:inline"
|
|
||||||
>{((Number(model.stats.won) / model.stats.count) * 100).toFixed(1)}%</span
|
|
||||||
>
|
>
|
||||||
<span class="group-hover:hidden">{model.stats.won}</span>
|
{$i18n.t(col.label)}
|
||||||
{/if}
|
{#if orderBy === col.key}
|
||||||
</td>
|
{#if direction === 'asc'}<ChevronUp className="size-2" />{:else}<ChevronDown
|
||||||
<td class="px-3 py-1.5 text-right font-normal text-red-500 w-10">
|
className="size-2"
|
||||||
{#if model.stats.lost === '-'}-{:else}
|
/>{/if}
|
||||||
<span class="hidden group-hover:inline"
|
{:else}
|
||||||
>{((Number(model.stats.lost) / model.stats.count) * 100).toFixed(1)}%</span
|
<span class="invisible"><ChevronUp className="size-2" /></span>
|
||||||
>
|
{/if}
|
||||||
<span class="group-hover:hidden">{model.stats.lost}</span>
|
</div>
|
||||||
{/if}
|
</th>
|
||||||
</td>
|
{/each}
|
||||||
</tr>
|
</tr>
|
||||||
{/each}
|
</thead>
|
||||||
</tbody>
|
<tbody>
|
||||||
</table>
|
{#each sortedModels as model, idx (model.id)}
|
||||||
{/if}
|
<tr
|
||||||
|
class="bg-white dark:bg-gray-900 text-xs group cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-850/50 transition"
|
||||||
|
on:click={() => openModal(model)}
|
||||||
|
>
|
||||||
|
<td class="px-3 py-1.5 font-normal text-gray-900 dark:text-white">
|
||||||
|
{model.rating !== '-' ? idx + 1 : '-'}
|
||||||
|
</td>
|
||||||
|
<td class="px-3 py-1.5">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<img
|
||||||
|
src="{WEBUI_API_BASE_URL}/models/model/profile/image?id={model.id}"
|
||||||
|
alt={model.name}
|
||||||
|
class="size-5 rounded-full object-cover shrink-0"
|
||||||
|
on:error={(e) => {
|
||||||
|
e.target.src = '/favicon.png';
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Tooltip content={`${model.name} (${model.id})`} placement="top-start">
|
||||||
|
<span class="font-normal text-gray-800 dark:text-gray-200 line-clamp-1"
|
||||||
|
>{model.name}</span
|
||||||
|
>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-3 py-1.5 text-right font-normal text-gray-900 dark:text-white">
|
||||||
|
{model.rating}
|
||||||
|
</td>
|
||||||
|
<td class="px-3 py-1.5 text-right font-normal text-green-500 w-10">
|
||||||
|
{#if model.stats.won === '-'}-{:else}
|
||||||
|
<span class="hidden group-hover:inline"
|
||||||
|
>{((Number(model.stats.won) / model.stats.count) * 100).toFixed(1)}%</span
|
||||||
|
>
|
||||||
|
<span class="group-hover:hidden">{model.stats.won}</span>
|
||||||
|
{/if}
|
||||||
|
</td>
|
||||||
|
<td class="px-3 py-1.5 text-right font-normal text-red-500 w-10">
|
||||||
|
{#if model.stats.lost === '-'}-{:else}
|
||||||
|
<span class="hidden group-hover:inline"
|
||||||
|
>{((Number(model.stats.lost) / model.stats.count) * 100).toFixed(1)}%</span
|
||||||
|
>
|
||||||
|
<span class="group-hover:hidden">{model.stats.lost}</span>
|
||||||
|
{/if}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-gray-500 text-xs mt-1.5 w-full flex justify-end">
|
<div class="text-gray-500 text-xs mt-1.5 w-full flex justify-end">
|
||||||
|
|||||||
@@ -2,8 +2,11 @@
|
|||||||
import { getContext, onMount } from 'svelte';
|
import { getContext, onMount } from 'svelte';
|
||||||
|
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { user } from '$lib/stores';
|
import { adminGroupCount, adminUserCount, config, user } from '$lib/stores';
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
|
import { getGroups } from '$lib/apis/groups';
|
||||||
|
import { getUsers } from '$lib/apis/users';
|
||||||
|
import { formatNumber } from '$lib/utils';
|
||||||
|
|
||||||
import UserList from './Users/UserList.svelte';
|
import UserList from './Users/UserList.svelte';
|
||||||
import Groups from './Users/Groups.svelte';
|
import Groups from './Users/Groups.svelte';
|
||||||
@@ -30,12 +33,32 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
let loaded = false;
|
let loaded = false;
|
||||||
|
$: usersSeatLimit = $config?.license_metadata?.seats ?? null;
|
||||||
|
$: usersCountExceeded = usersSeatLimit !== null && ($adminUserCount ?? 0) > usersSeatLimit;
|
||||||
|
$: formattedUserCount =
|
||||||
|
$adminUserCount === null
|
||||||
|
? null
|
||||||
|
: usersSeatLimit !== null
|
||||||
|
? `${formatNumber($adminUserCount)} of ${formatNumber(usersSeatLimit)}`
|
||||||
|
: formatNumber($adminUserCount);
|
||||||
|
$: formattedGroupCount = $adminGroupCount === null ? null : formatNumber($adminGroupCount);
|
||||||
|
|
||||||
|
const loadCounts = async () => {
|
||||||
|
const [usersRes, groupsRes] = await Promise.all([
|
||||||
|
getUsers(localStorage.token, undefined, 'created_at', 'asc', 1).catch(() => null),
|
||||||
|
getGroups(localStorage.token).catch(() => null)
|
||||||
|
]);
|
||||||
|
|
||||||
|
adminUserCount.set(usersRes?.total ?? null);
|
||||||
|
adminGroupCount.set(Array.isArray(groupsRes) ? groupsRes.length : null);
|
||||||
|
};
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
if ($user?.role !== 'admin') {
|
if ($user?.role !== 'admin') {
|
||||||
await goto('/');
|
await goto('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await loadCounts();
|
||||||
loaded = true;
|
loaded = true;
|
||||||
|
|
||||||
const containerElement = document.getElementById('users-tabs-container');
|
const containerElement = document.getElementById('users-tabs-container');
|
||||||
@@ -55,33 +78,47 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if loaded}
|
{#if loaded}
|
||||||
<div class="flex flex-col lg:flex-row w-full h-full pb-2 lg:space-x-4">
|
<div class="flex flex-col lg:flex-row w-full h-full pb-2">
|
||||||
<div
|
<div
|
||||||
id="users-tabs-container"
|
id="users-tabs-container"
|
||||||
class="tabs mx-2 sm:mx-[16px] lg:mx-0 lg:px-[16px] flex flex-row overflow-x-auto gap-2.5 max-w-full lg:gap-1 lg:flex-col lg:flex-none lg:w-50 dark:text-gray-200 text-sm font-normal text-left scrollbar-none"
|
class="tabs mx-2 px-2 sm:mx-2.5 lg:mx-0 lg:px-2.5 flex flex-row overflow-x-auto gap-2.5 max-w-full lg:gap-0 lg:flex-col lg:flex-none lg:w-50 dark:text-gray-200 text-sm font-normal text-left scrollbar-none"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
id="overview"
|
id="overview"
|
||||||
href="/admin/users/overview"
|
href="/admin/users/overview"
|
||||||
draggable="false"
|
draggable="false"
|
||||||
class="px-0.5 py-1 min-w-fit rounded-lg lg:flex-none flex text-right transition select-none {selectedTab ===
|
class="px-0.5 py-1 min-w-fit rounded-lg lg:flex-none flex items-center gap-1.5 text-right transition select-none {selectedTab ===
|
||||||
'overview'
|
'overview'
|
||||||
? ''
|
? ''
|
||||||
: ' text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'}"
|
: ' text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'}"
|
||||||
>
|
>
|
||||||
<div class=" self-center">{$i18n.t('Overview')}</div>
|
<div class=" self-center">{$i18n.t('Overview')}</div>
|
||||||
|
{#if formattedUserCount !== null}
|
||||||
|
<div
|
||||||
|
class="self-center text-sm {usersCountExceeded
|
||||||
|
? `text-red-500 ${selectedTab === 'overview' ? '' : 'opacity-50'}`
|
||||||
|
: 'opacity-60'}"
|
||||||
|
>
|
||||||
|
{formattedUserCount}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
id="groups"
|
id="groups"
|
||||||
href="/admin/users/groups"
|
href="/admin/users/groups"
|
||||||
draggable="false"
|
draggable="false"
|
||||||
class="px-0.5 py-1 min-w-fit rounded-lg lg:flex-none flex text-right transition select-none {selectedTab ===
|
class="px-0.5 py-1 min-w-fit rounded-lg lg:flex-none flex items-center gap-1.5 text-right transition select-none {selectedTab ===
|
||||||
'groups'
|
'groups'
|
||||||
? ''
|
? ''
|
||||||
: ' text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'}"
|
: ' text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'}"
|
||||||
>
|
>
|
||||||
<div class=" self-center">{$i18n.t('Groups')}</div>
|
<div class=" self-center">{$i18n.t('Groups')}</div>
|
||||||
|
{#if formattedGroupCount !== null}
|
||||||
|
<div class="self-center text-sm opacity-60">
|
||||||
|
{formattedGroupCount}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { onMount, getContext } from 'svelte';
|
import { onMount, getContext } from 'svelte';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
|
|
||||||
import { user } from '$lib/stores';
|
import { adminGroupCount, user } from '$lib/stores';
|
||||||
|
|
||||||
import Plus from '$lib/components/icons/Plus.svelte';
|
import Plus from '$lib/components/icons/Plus.svelte';
|
||||||
import Search from '$lib/components/icons/Search.svelte';
|
import Search from '$lib/components/icons/Search.svelte';
|
||||||
@@ -55,6 +55,7 @@
|
|||||||
|
|
||||||
const setGroups = async () => {
|
const setGroups = async () => {
|
||||||
groups = await getGroups(localStorage.token);
|
groups = await getGroups(localStorage.token);
|
||||||
|
adminGroupCount.set(groups.length);
|
||||||
};
|
};
|
||||||
|
|
||||||
const addGroupHandler = async (group) => {
|
const addGroupHandler = async (group) => {
|
||||||
@@ -65,7 +66,7 @@
|
|||||||
|
|
||||||
if (res) {
|
if (res) {
|
||||||
toast.success($i18n.t('Group created successfully'));
|
toast.success($i18n.t('Group created successfully'));
|
||||||
groups = await getGroups(localStorage.token);
|
await setGroups();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -106,50 +107,9 @@
|
|||||||
onSubmit={addGroupHandler}
|
onSubmit={addGroupHandler}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="flex flex-col gap-1 mt-0.5 mb-3">
|
<div class="space-y-1">
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex h-8 flex-1 items-center w-full gap-2">
|
||||||
<div class="flex items-center md:self-center text-xl font-normal px-0.5 gap-2 shrink-0">
|
<div class="flex min-w-0 flex-1 items-center">
|
||||||
<div>
|
|
||||||
{$i18n.t('Groups')}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="text-lg font-normal text-gray-500 dark:text-gray-500">
|
|
||||||
{filteredGroups.length}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex w-full justify-end gap-1.5">
|
|
||||||
<button
|
|
||||||
class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-200 transition"
|
|
||||||
aria-haspopup="dialog"
|
|
||||||
on:click={() => {
|
|
||||||
showDefaultPermissionsModal = true;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div class="self-center font-normal line-clamp-1">
|
|
||||||
{$i18n.t('Default permissions')}
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
class="px-2 py-1.5 rounded-xl bg-black text-white dark:bg-white dark:text-black transition font-normal 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="px-2.5 flex h-8 flex-1 items-center w-full gap-2">
|
|
||||||
<div class="flex min-w-0 flex-1">
|
|
||||||
<div class="self-center ml-1 mr-3">
|
<div class="self-center ml-1 mr-3">
|
||||||
<Search className="size-3.5" />
|
<Search className="size-3.5" />
|
||||||
</div>
|
</div>
|
||||||
@@ -198,10 +158,30 @@
|
|||||||
</div>
|
</div>
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="flex h-8 shrink-0 items-center px-2 py-1.5 rounded-xl hover:bg-gray-100 dark:hover:bg-gray-850 dark:text-gray-200 transition text-xs"
|
||||||
|
aria-haspopup="dialog"
|
||||||
|
on:click={() => {
|
||||||
|
showDefaultPermissionsModal = true;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{$i18n.t('Default permissions')}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="h-8 shrink-0 p-2 rounded-xl hover:bg-gray-100 dark:hover:bg-gray-850 transition font-normal text-sm flex items-center"
|
||||||
|
aria-label={$i18n.t('New Group')}
|
||||||
|
on:click={() => {
|
||||||
|
showAddGroupModal = !showAddGroupModal;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Plus className="size-3.5" strokeWidth="2.5" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if filteredGroups.length !== 0}
|
{#if filteredGroups.length !== 0}
|
||||||
<div class="mt-1 grid grid-cols-1 px-2">
|
<div class="mt-1 grid grid-cols-1">
|
||||||
{#each filteredGroups as group}
|
{#each filteredGroups as group}
|
||||||
<GroupItem {group} {setGroups} {defaultPermissions} />
|
<GroupItem {group} {setGroups} {defaultPermissions} />
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
@@ -121,7 +121,7 @@
|
|||||||
const shouldShowSettingGroup = (tabIds: string[], index: number) =>
|
const shouldShowSettingGroup = (tabIds: string[], index: number) =>
|
||||||
index === 0 || settingGroupTitle(tabIds[index]) !== settingGroupTitle(tabIds[index - 1]);
|
index === 0 || settingGroupTitle(tabIds[index]) !== settingGroupTitle(tabIds[index - 1]);
|
||||||
const settingGroupHeadingClass = (first: boolean) =>
|
const settingGroupHeadingClass = (first: boolean) =>
|
||||||
`hidden md:block text-[0.625rem] text-gray-400 dark:text-gray-600 px-2 ${
|
`hidden md:block shrink-0 text-[0.625rem] text-gray-400 dark:text-gray-600 px-2 ${
|
||||||
first ? 'mt-0.5' : 'mt-2'
|
first ? 'mt-0.5' : 'mt-2'
|
||||||
} mb-0.5`;
|
} mb-0.5`;
|
||||||
|
|
||||||
@@ -789,7 +789,7 @@
|
|||||||
const tabButtonClass = (active: boolean) =>
|
const tabButtonClass = (active: boolean) =>
|
||||||
`flex items-center gap-1.5 h-7 px-2 md:w-full shrink-0 rounded-lg text-xs text-left transition-colors duration-75 ${
|
`flex items-center gap-1.5 h-7 px-2 md:w-full shrink-0 rounded-lg text-xs text-left transition-colors duration-75 ${
|
||||||
active
|
active
|
||||||
? 'font-medium text-gray-900 dark:text-white bg-gray-100 dark:bg-white/6'
|
? 'font-medium text-gray-900 dark:text-white bg-gray-50 dark:bg-white/[0.04]'
|
||||||
: 'text-gray-500 hover:text-gray-700 dark:hover:text-gray-300'
|
: 'text-gray-500 hover:text-gray-700 dark:hover:text-gray-300'
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
@@ -1019,9 +1019,10 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if $user?.role === 'admin' && filteredAdminSettings.length > 0}
|
{#if $user?.role === 'admin' && filteredAdminSettings.length > 0}
|
||||||
<span
|
<div
|
||||||
class="hidden md:block border-t border-gray-300/80 dark:border-white/15 text-[0.625rem] text-gray-400 dark:text-gray-600 px-2 pt-2 mt-2 mb-0.5"
|
class="hidden md:block shrink-0 self-stretch h-px mx-1 my-2 bg-gray-100/40 dark:bg-white/[0.025]"
|
||||||
>
|
></div>
|
||||||
|
<span class="hidden md:block text-[0.625rem] text-gray-400 dark:text-gray-600 px-2 mb-0.5">
|
||||||
{$i18n.t('Admin')}
|
{$i18n.t('Admin')}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
|||||||
@@ -90,6 +90,9 @@ export const workspaceCounts: Writable<Record<WorkspaceSection, number | null>>
|
|||||||
});
|
});
|
||||||
export const workspaceActions: Writable<WorkspaceAction[]> = writable([]);
|
export const workspaceActions: Writable<WorkspaceAction[]> = writable([]);
|
||||||
export const adminUserCount: Writable<number | null> = writable(null);
|
export const adminUserCount: Writable<number | null> = writable(null);
|
||||||
|
export const adminGroupCount: Writable<number | null> = writable(null);
|
||||||
|
export const adminLeaderboardCount: Writable<number | null> = writable(null);
|
||||||
|
export const adminFeedbackCount: Writable<number | null> = writable(null);
|
||||||
|
|
||||||
export const toolServers = writable([]);
|
export const toolServers = writable([]);
|
||||||
export const terminalServers = writable([]);
|
export const terminalServers = writable([]);
|
||||||
|
|||||||
@@ -2,40 +2,15 @@
|
|||||||
import { onMount, getContext } from 'svelte';
|
import { onMount, getContext } from 'svelte';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
|
|
||||||
import {
|
import { WEBUI_NAME, config, mobile, showSettings, showSidebar, user } from '$lib/stores';
|
||||||
WEBUI_NAME,
|
|
||||||
adminUserCount,
|
|
||||||
config,
|
|
||||||
mobile,
|
|
||||||
showSettings,
|
|
||||||
showSidebar,
|
|
||||||
user
|
|
||||||
} from '$lib/stores';
|
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
|
|
||||||
import Sidebar from '$lib/components/icons/Sidebar.svelte';
|
import Sidebar from '$lib/components/icons/Sidebar.svelte';
|
||||||
import { formatNumber } from '$lib/utils';
|
|
||||||
import { getUsers } from '$lib/apis/users';
|
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
let loaded = false;
|
let loaded = false;
|
||||||
$: usersCountVisible = $page.url.pathname.includes('/admin');
|
|
||||||
$: usersTabSelected = $page.url.pathname.includes('/admin/users');
|
|
||||||
$: usersSeatLimit = $config?.license_metadata?.seats ?? null;
|
|
||||||
$: usersCountExceeded =
|
|
||||||
usersCountVisible && usersSeatLimit !== null && ($adminUserCount ?? 0) > usersSeatLimit;
|
|
||||||
$: if (loaded && usersCountVisible) {
|
|
||||||
loadUserCount();
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadUserCount = async () => {
|
|
||||||
const res = await getUsers(localStorage.token, undefined, 'created_at', 'asc', 1).catch(
|
|
||||||
() => null
|
|
||||||
);
|
|
||||||
adminUserCount.set(res?.total ?? null);
|
|
||||||
};
|
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
if ($user?.role !== 'admin') {
|
if ($user?.role !== 'admin') {
|
||||||
@@ -92,26 +67,10 @@
|
|||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
draggable="false"
|
draggable="false"
|
||||||
class="min-w-fit p-1.5 flex items-center gap-1.5 {$page.url.pathname.includes(
|
class="min-w-fit p-1.5 {$page.url.pathname.includes('/admin/users')
|
||||||
'/admin/users'
|
|
||||||
)
|
|
||||||
? ''
|
? ''
|
||||||
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition select-none"
|
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition select-none"
|
||||||
href="/admin"
|
href="/admin">{$i18n.t('Users')}</a
|
||||||
><span>{$i18n.t('Users')}</span>
|
|
||||||
{#if usersCountVisible}
|
|
||||||
<span
|
|
||||||
class="text-sm {usersCountExceeded
|
|
||||||
? `text-red-500 ${usersTabSelected ? '' : 'opacity-50'}`
|
|
||||||
: 'opacity-60'}"
|
|
||||||
>
|
|
||||||
{#if usersSeatLimit !== null}
|
|
||||||
{formatNumber($adminUserCount ?? 0)} of {formatNumber(usersSeatLimit)}
|
|
||||||
{:else}
|
|
||||||
{formatNumber($adminUserCount ?? 0)}
|
|
||||||
{/if}
|
|
||||||
</span>
|
|
||||||
{/if}</a
|
|
||||||
>
|
>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
|
|||||||
Reference in new issue
Block a user