chore: remove dead admin Analytics ModelUsage and UserUsage components (#27574)
Nothing in the tree imports either component; the admin Analytics tab
renders its own inline copies of both tables directly from
Dashboard.svelte. Both files landed with the dashboard in a4ad34841
(feat: analytics frontend dashboard) but were never wired into it.
The remaining name matches elsewhere (the getUserUsage API and
UserUsage* types in src/lib/apis/users/index.ts, consumed by
chat/Settings/Usage.svelte, plus the backend usage endpoints) belong to
the unrelated per-user usage feature and are untouched.
This commit is contained in:
@@ -1,173 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { onMount, getContext } from 'svelte';
|
||||
import { models } from '$lib/stores';
|
||||
import { getModelAnalytics } from '$lib/apis/analytics';
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
import ChevronUp from '$lib/components/icons/ChevronUp.svelte';
|
||||
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
|
||||
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
let modelStats: Array<{ model_id: string; count: number; name?: string }> = [];
|
||||
let loading = true;
|
||||
let orderBy = 'count';
|
||||
let direction: 'asc' | 'desc' = 'desc';
|
||||
|
||||
const toggleSort = (key: string) => {
|
||||
if (orderBy === key) {
|
||||
direction = direction === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
orderBy = key;
|
||||
direction = key === 'name' ? 'asc' : 'desc';
|
||||
}
|
||||
};
|
||||
|
||||
const loadAnalytics = async () => {
|
||||
loading = true;
|
||||
try {
|
||||
const result = await getModelAnalytics(localStorage.token);
|
||||
const modelsMap = new Map($models.map((m) => [m.id, m.name || m.id]));
|
||||
|
||||
modelStats = (result?.models ?? []).map((entry) => ({
|
||||
...entry,
|
||||
name: modelsMap.get(entry.model_id) || entry.model_id
|
||||
}));
|
||||
} catch (err) {
|
||||
console.error('Analytics load failed:', err);
|
||||
}
|
||||
loading = false;
|
||||
};
|
||||
|
||||
$: sortedModels = [...modelStats].sort((a, b) => {
|
||||
if (orderBy === 'name') {
|
||||
return direction === 'asc' ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name);
|
||||
}
|
||||
return direction === 'asc' ? a.count - b.count : b.count - a.count;
|
||||
});
|
||||
|
||||
$: totalMessages = modelStats.reduce((sum, m) => sum + m.count, 0);
|
||||
|
||||
onMount(loadAnalytics);
|
||||
</script>
|
||||
|
||||
<div
|
||||
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="flex items-center text-xl font-normal px-0.5 gap-2 shrink-0">
|
||||
{$i18n.t('Model Usage')}
|
||||
<span class="text-lg text-gray-500">{totalMessages} {$i18n.t('messages')}</span>
|
||||
</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 !modelStats.length && !loading}
|
||||
<div class="text-center text-xs text-gray-500 py-1">{$i18n.t('No data found')}</div>
|
||||
{:else if modelStats.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">
|
||||
<th scope="col" class="px-2.5 py-2 w-8">#</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-2.5 py-2 cursor-pointer select-none"
|
||||
on:click={() => toggleSort('name')}
|
||||
>
|
||||
<div class="flex gap-1.5 items-center">
|
||||
{$i18n.t('Model')}
|
||||
{#if orderBy === 'name'}
|
||||
{#if direction === 'asc'}<ChevronUp className="size-2" />{:else}<ChevronDown
|
||||
className="size-2"
|
||||
/>{/if}
|
||||
{: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 text-right"
|
||||
on:click={() => toggleSort('count')}
|
||||
>
|
||||
<div class="flex gap-1.5 items-center justify-end">
|
||||
{$i18n.t('Messages')}
|
||||
{#if orderBy === 'count'}
|
||||
{#if direction === 'asc'}<ChevronUp className="size-2" />{:else}<ChevronDown
|
||||
className="size-2"
|
||||
/>{/if}
|
||||
{: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 text-right w-24"
|
||||
on:click={() => toggleSort('percentage')}
|
||||
>
|
||||
<div class="flex gap-1.5 items-center justify-end">
|
||||
{$i18n.t('Share')}
|
||||
{#if orderBy === 'percentage'}
|
||||
{#if direction === 'asc'}<ChevronUp className="size-2" />{:else}<ChevronDown
|
||||
className="size-2"
|
||||
/>{/if}
|
||||
{:else}
|
||||
<span class="invisible"><ChevronUp className="size-2" /></span>
|
||||
{/if}
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each sortedModels as model, idx (model.model_id)}
|
||||
<tr
|
||||
class="bg-white dark:bg-gray-900 text-xs hover:bg-gray-50 dark:hover:bg-gray-850/50 transition"
|
||||
>
|
||||
<td class="px-3 py-1.5 font-normal text-gray-900 dark:text-white">
|
||||
{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.model_id}"
|
||||
alt={model.name}
|
||||
class="size-5 rounded-full object-cover shrink-0"
|
||||
on:error={(e) => {
|
||||
e.target.src = '/favicon.png';
|
||||
}}
|
||||
/>
|
||||
<span class="font-normal text-gray-800 dark:text-gray-200">{model.name}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-3 py-1.5 text-right font-normal text-gray-900 dark:text-white">
|
||||
{model.count.toLocaleString()}
|
||||
</td>
|
||||
<td class="px-3 py-1.5 text-right font-normal text-blue-500">
|
||||
{((model.count / totalMessages) * 100).toFixed(1)}%
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="text-gray-500 text-xs mt-1.5 w-full flex justify-end">
|
||||
<div class="text-right">
|
||||
ⓘ {$i18n.t('Message counts are based on assistant responses.')}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,160 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { onMount, getContext } from 'svelte';
|
||||
import { getUserAnalytics } from '$lib/apis/analytics';
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
import ChevronUp from '$lib/components/icons/ChevronUp.svelte';
|
||||
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
let userStats: Array<{ user_id: string; count: number }> = [];
|
||||
let loading = true;
|
||||
let orderBy = 'count';
|
||||
let direction: 'asc' | 'desc' = 'desc';
|
||||
|
||||
const toggleSort = (key: string) => {
|
||||
if (orderBy === key) {
|
||||
direction = direction === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
orderBy = key;
|
||||
direction = key === 'user_id' ? 'asc' : 'desc';
|
||||
}
|
||||
};
|
||||
|
||||
const loadAnalytics = async () => {
|
||||
loading = true;
|
||||
try {
|
||||
const result = await getUserAnalytics(localStorage.token, null, null, 100);
|
||||
userStats = result?.users ?? [];
|
||||
} catch (err) {
|
||||
console.error('User analytics load failed:', err);
|
||||
}
|
||||
loading = false;
|
||||
};
|
||||
|
||||
$: sortedUsers = [...userStats].sort((a, b) => {
|
||||
if (orderBy === 'user_id') {
|
||||
return direction === 'asc'
|
||||
? a.user_id.localeCompare(b.user_id)
|
||||
: b.user_id.localeCompare(a.user_id);
|
||||
}
|
||||
return direction === 'asc' ? a.count - b.count : b.count - a.count;
|
||||
});
|
||||
|
||||
$: totalMessages = userStats.reduce((sum, u) => sum + u.count, 0);
|
||||
|
||||
onMount(loadAnalytics);
|
||||
</script>
|
||||
|
||||
<div
|
||||
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="flex items-center text-xl font-normal px-0.5 gap-2 shrink-0">
|
||||
{$i18n.t('User Activity')}
|
||||
<span class="text-lg text-gray-500">{userStats.length} {$i18n.t('users')}</span>
|
||||
</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 !userStats.length && !loading}
|
||||
<div class="text-center text-xs text-gray-500 py-1">{$i18n.t('No data found')}</div>
|
||||
{:else if userStats.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">
|
||||
<th scope="col" class="px-2.5 py-2 w-8">#</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-2.5 py-2 cursor-pointer select-none"
|
||||
on:click={() => toggleSort('user_id')}
|
||||
>
|
||||
<div class="flex gap-1.5 items-center">
|
||||
{$i18n.t('User')}
|
||||
{#if orderBy === 'user_id'}
|
||||
{#if direction === 'asc'}<ChevronUp className="size-2" />{:else}<ChevronDown
|
||||
className="size-2"
|
||||
/>{/if}
|
||||
{: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 text-right"
|
||||
on:click={() => toggleSort('count')}
|
||||
>
|
||||
<div class="flex gap-1.5 items-center justify-end">
|
||||
{$i18n.t('Messages')}
|
||||
{#if orderBy === 'count'}
|
||||
{#if direction === 'asc'}<ChevronUp className="size-2" />{:else}<ChevronDown
|
||||
className="size-2"
|
||||
/>{/if}
|
||||
{: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 text-right w-24"
|
||||
on:click={() => toggleSort('percentage')}
|
||||
>
|
||||
<div class="flex gap-1.5 items-center justify-end">
|
||||
{$i18n.t('Share')}
|
||||
{#if orderBy === 'percentage'}
|
||||
{#if direction === 'asc'}<ChevronUp className="size-2" />{:else}<ChevronDown
|
||||
className="size-2"
|
||||
/>{/if}
|
||||
{:else}
|
||||
<span class="invisible"><ChevronUp className="size-2" /></span>
|
||||
{/if}
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each sortedUsers as user, idx (user.user_id)}
|
||||
<tr
|
||||
class="bg-white dark:bg-gray-900 text-xs hover:bg-gray-50 dark:hover:bg-gray-850/50 transition"
|
||||
>
|
||||
<td class="px-3 py-1.5 font-normal text-gray-900 dark:text-white">
|
||||
{idx + 1}
|
||||
</td>
|
||||
<td class="px-3 py-1.5">
|
||||
<span class="font-normal text-gray-800 dark:text-gray-200 font-mono text-xs">
|
||||
{user.user_id.substring(0, 8)}...
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-3 py-1.5 text-right font-normal text-gray-900 dark:text-white">
|
||||
{user.count.toLocaleString()}
|
||||
</td>
|
||||
<td class="px-3 py-1.5 text-right font-normal text-blue-500">
|
||||
{((user.count / totalMessages) * 100).toFixed(1)}%
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="text-gray-500 text-xs mt-1.5 w-full flex justify-end">
|
||||
<div class="text-right">
|
||||
ⓘ {$i18n.t('Showing all messages (user + assistant) per user.')}
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user