This commit is contained in:
Timothy Jaeryang Baek
2026-07-15 05:17:41 -04:00
parent a3908f1281
commit 3ce3c52936
24 changed files with 210 additions and 177 deletions
+1 -1
View File
@@ -18,7 +18,7 @@
</script>
{#if loaded}
<div class="w-full h-full pb-2 px-3">
<div class="w-full h-full pb-2">
<Dashboard />
</div>
{/if}
@@ -230,13 +230,10 @@
}
</script>
<!-- Header with title and period selector -->
<div
class="pt-0.5 pb-1 gap-1 flex flex-row justify-between items-center sticky top-0 z-10 bg-white dark:bg-gray-900"
>
<div class="text-lg font-normal px-0.5 shrink-0">
<div class="flex items-center justify-between mb-2 gap-2">
<h2 class="text-sm font-medium text-gray-900 dark:text-white shrink-0">
{$i18n.t('Analytics')}
</div>
</h2>
<div class="flex items-center gap-2 flex-wrap justify-end min-w-0">
{#if groups.length > 0}
<select
+15
View File
@@ -20,6 +20,7 @@
import WebSearch from './Settings/WebSearch.svelte';
import Evaluations from './Settings/Evaluations.svelte';
import Analytics from './Analytics.svelte';
import CodeExecution from './Settings/CodeExecution.svelte';
import Integrations from './Settings/Integrations.svelte';
import Subagents from './Settings/Subagents.svelte';
@@ -42,6 +43,7 @@
'models',
'subagents',
'evaluations',
'analytics',
'integrations',
'documents',
'web',
@@ -157,6 +159,12 @@
route: '/admin/settings/evaluations',
keywords: ['evaluations', 'feedback', 'rating', 'arena', 'leaderboard', 'preference']
},
{
id: 'analytics',
title: 'Analytics',
route: '/admin/settings/analytics',
keywords: ['analytics', 'usage', 'stats', 'dashboard', 'models', 'users', 'messages']
},
{
id: 'integrations',
title: 'Integrations',
@@ -289,6 +297,10 @@
const setFilteredSettings = () => {
filteredSettings = allSettings.filter((tab) => {
const searchTerm = search.toLowerCase().trim();
if (tab.id === 'analytics' && !($config?.features.enable_admin_analytics ?? true)) {
return false;
}
return (
search === '' ||
tab.title.toLowerCase().includes(searchTerm) ||
@@ -367,6 +379,7 @@
<!-- {$i18n.t('Models')} -->
<!-- {$i18n.t('Sub-agents')} -->
<!-- {$i18n.t('Evaluations')} -->
<!-- {$i18n.t('Analytics')} -->
<!-- {$i18n.t('Integrations')} -->
<!-- {$i18n.t('Documents')} -->
<!-- {$i18n.t('Web Search')} -->
@@ -416,6 +429,8 @@
<Subagents />
{:else if selectedTab === 'evaluations'}
<Evaluations />
{:else if selectedTab === 'analytics'}
<Analytics />
{:else if selectedTab === 'integrations'}
<Integrations />
{:else if selectedTab === 'documents'}
@@ -1,5 +1,6 @@
<script lang="ts">
import CodeBracket from '$lib/components/icons/CodeBracket.svelte';
import BarsArrowUp from '$lib/components/icons/BarsArrowUp.svelte';
import Computer from '$lib/components/icons/Computer.svelte';
import Database from '$lib/components/icons/Database.svelte';
import Document from '$lib/components/icons/Document.svelte';
@@ -66,6 +67,8 @@
<path stroke-linecap="round" stroke-linejoin="round" d="M12 20V4" />
<path stroke-linecap="round" stroke-linejoin="round" d="M19 20v-7" />
</svg>
{:else if id === 'analytics'}
<BarsArrowUp {className} {strokeWidth} />
{:else if id === 'integrations'}
<WrenchAlt {className} {strokeWidth} />
{:else if id === 'documents'}
+22 -10
View File
@@ -30,7 +30,6 @@
import ModelEditor from '$lib/components/workspace/Models/ModelEditor.svelte';
import { toast } from 'svelte-sonner';
import Badge from '$lib/components/common/Badge.svelte';
import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
import ModelSettingsModal from './Models/ModelSettingsModal.svelte';
import ManageModelsModal from './Models/ManageModelsModal.svelte';
@@ -85,14 +84,24 @@
const isSharedModel = (model) => (model?.access_grants ?? []).length > 0 && !isPublicModel(model);
const modelAccessBadge = (model) => {
const modelAccessLabel = (model) => {
if (isPublicModel(model)) {
return { type: 'success', content: $i18n.t('Public') };
return $i18n.t('Public');
}
if (isSharedModel(model)) {
return { type: 'info', content: $i18n.t('Shared') };
return $i18n.t('Shared');
}
return { type: 'muted', content: $i18n.t('Private') };
return $i18n.t('Private');
};
const modelAccessClass = (model) => {
if (isPublicModel(model)) {
return 'text-green-700/50 dark:text-green-300/50';
}
if (isSharedModel(model)) {
return 'text-blue-700/50 dark:text-blue-300/50';
}
return 'text-gray-500 dark:text-gray-400';
};
$: if (models) {
@@ -670,7 +679,7 @@
<img
src={`${WEBUI_API_BASE_URL}/models/model/profile/image?id=${model.id}&lang=${$i18n.language}`}
alt="modelfile profile"
class=" rounded-xl size-8 object-cover"
class=" rounded-xl size-7 object-cover"
loading="lazy"
decoding="async"
on:error={(e) => {
@@ -711,10 +720,13 @@
: model.id}
</span>
<Badge
type={modelAccessBadge(model).type}
content={modelAccessBadge(model).content}
/>
<span
class="shrink-0 text-[11px] font-normal leading-4 {modelAccessClass(
model
)}"
>
{modelAccessLabel(model)}
</span>
</div>
</Tooltip>
</div>
@@ -16,7 +16,6 @@
import Tooltip from '$lib/components/common/Tooltip.svelte';
import Checkbox from '$lib/components/common/Checkbox.svelte';
import Badge from '$lib/components/common/Badge.svelte';
import Search from '$lib/components/icons/Search.svelte';
import Pagination from '$lib/components/common/Pagination.svelte';
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
@@ -46,6 +45,16 @@
page = 1;
};
const roleClass = (role) => {
if (role === 'admin') {
return 'text-blue-700/50 dark:text-blue-300/50';
}
if (role === 'user') {
return 'text-green-700/50 dark:text-green-300/50';
}
return 'text-gray-500 dark:text-gray-400';
};
const getUserList = async () => {
try {
const res = await getUsers(localStorage.token, query, orderBy, direction, page).catch(
@@ -154,12 +163,12 @@
<th
scope="col"
class="px-2.5 py-2 cursor-pointer select-none"
on:click={() => setSortKey('role')}
on:click={() => setSortKey('name')}
>
<div class="flex gap-1.5 items-center">
{$i18n.t('Role')}
{$i18n.t('Name')}
{#if orderBy === 'role'}
{#if orderBy === 'name'}
<span class="font-normal"
>{#if direction === 'asc'}
<ChevronUp className="size-2" />
@@ -177,12 +186,12 @@
<th
scope="col"
class="px-2.5 py-2 cursor-pointer select-none"
on:click={() => setSortKey('name')}
on:click={() => setSortKey('role')}
>
<div class="flex gap-1.5 items-center">
{$i18n.t('Name')}
{$i18n.t('Role')}
{#if orderBy === 'name'}
{#if orderBy === 'role'}
<span class="font-normal"
>{#if direction === 'asc'}
<ChevronUp className="size-2" />
@@ -236,23 +245,11 @@
/>
</div>
</td>
<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-normal text-gray-900 dark:text-white max-w-48">
<Tooltip content={user.email} placement="top-start">
<div class="flex items-center">
<div class="flex items-center gap-2">
<img
class="rounded-full w-6 h-6 object-cover mr-2.5 flex-shrink-0"
class="rounded-full w-6 h-6 object-cover flex-shrink-0"
src={`${WEBUI_API_BASE_URL}/users/${user.id}/profile/image`}
alt="user"
/>
@@ -261,6 +258,11 @@
</div>
</Tooltip>
</td>
<td class="px-3 py-1 min-w-[5rem] w-20">
<span class="text-xs font-normal leading-4 capitalize {roleClass(user.role)}">
{$i18n.t(user.role)}
</span>
</td>
<td class=" px-3 py-1">
{dayjs(user.last_active_at * 1000).fromNow()}
+28 -22
View File
@@ -23,7 +23,6 @@
import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
import Badge from '$lib/components/common/Badge.svelte';
import Plus from '$lib/components/icons/Plus.svelte';
import ChevronUp from '$lib/components/icons/ChevronUp.svelte';
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
@@ -79,6 +78,16 @@
}
};
const roleClass = (role) => {
if (role === 'admin') {
return 'text-blue-700/50 dark:text-blue-300/50';
}
if (role === 'user') {
return 'text-green-700/50 dark:text-green-300/50';
}
return 'text-gray-500 dark:text-gray-400';
};
const getUserList = async () => {
try {
const res = await getUsers(localStorage.token, query, orderBy, direction, page).catch(
@@ -239,12 +248,12 @@
<th
scope="col"
class="px-2.5 py-2 cursor-pointer select-none"
on:click={() => setSortKey('role')}
on:click={() => setSortKey('name')}
>
<div class="flex gap-1.5 items-center">
{$i18n.t('Role')}
{$i18n.t('Name')}
{#if orderBy === 'role'}
{#if orderBy === 'name'}
<span class="font-normal"
>{#if direction === 'asc'}
<ChevronUp className="size-2" />
@@ -262,12 +271,12 @@
<th
scope="col"
class="px-2.5 py-2 cursor-pointer select-none"
on:click={() => setSortKey('name')}
on:click={() => setSortKey('role')}
>
<div class="flex gap-1.5 items-center">
{$i18n.t('Name')}
{$i18n.t('Role')}
{#if orderBy === 'name'}
{#if orderBy === 'role'}
<span class="font-normal"
>{#if direction === 'asc'}
<ChevronUp className="size-2" />
@@ -359,21 +368,6 @@
<tbody class="">
{#each users as user (user.id)}
<tr class="bg-white dark:bg-gray-900 dark:border-gray-850 text-xs">
<td class="px-3 py-1 min-w-[7rem] w-28">
<button
class=" translate-y-0.5"
aria-label={$i18n.t('Change User Role')}
on:click={() => {
selectedUser = user;
showEditUserModal = !showEditUserModal;
}}
>
<Badge
type={user.role === 'admin' ? 'info' : user.role === 'user' ? 'success' : 'muted'}
content={$i18n.t(user.role)}
/>
</button>
</td>
<td class="px-3 py-1 font-normal text-gray-900 dark:text-white max-w-48">
<div class="flex items-center gap-2">
<ProfilePreview {user} side="right" align="center" sideOffset={6}>
@@ -401,6 +395,18 @@
{/if}
</div>
</td>
<td class="px-3 py-1 min-w-[5rem] w-20">
<button
class="text-xs font-normal leading-4 capitalize transition {roleClass(user.role)}"
aria-label={$i18n.t('Change User Role')}
on:click={() => {
selectedUser = user;
showEditUserModal = !showEditUserModal;
}}
>
{$i18n.t(user.role)}
</button>
</td>
<td class=" px-3 py-1 max-w-48 truncate"> {user.email} </td>
<td class=" px-3 py-1">