refac
This commit is contained in:
1 parent
3ce3c52936
commit
f8ea15b84a
7 files changed
+215
-165
No files matched your search
@@ -96,10 +96,10 @@
|
|||||||
|
|
||||||
const modelAccessClass = (model) => {
|
const modelAccessClass = (model) => {
|
||||||
if (isPublicModel(model)) {
|
if (isPublicModel(model)) {
|
||||||
return 'text-green-700/50 dark:text-green-300/50';
|
return 'text-[#4f7a5a] dark:text-[#8db395]';
|
||||||
}
|
}
|
||||||
if (isSharedModel(model)) {
|
if (isSharedModel(model)) {
|
||||||
return 'text-blue-700/50 dark:text-blue-300/50';
|
return 'text-[#4f6f93] dark:text-[#8ba6c6]';
|
||||||
}
|
}
|
||||||
return 'text-gray-500 dark:text-gray-400';
|
return 'text-gray-500 dark:text-gray-400';
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -47,10 +47,10 @@
|
|||||||
|
|
||||||
const roleClass = (role) => {
|
const roleClass = (role) => {
|
||||||
if (role === 'admin') {
|
if (role === 'admin') {
|
||||||
return 'text-blue-700/50 dark:text-blue-300/50';
|
return 'text-[#4f6f93] dark:text-[#8ba6c6]';
|
||||||
}
|
}
|
||||||
if (role === 'user') {
|
if (role === 'user') {
|
||||||
return 'text-green-700/50 dark:text-green-300/50';
|
return 'text-[#4f7a5a] dark:text-[#8db395]';
|
||||||
}
|
}
|
||||||
return 'text-gray-500 dark:text-gray-400';
|
return 'text-gray-500 dark:text-gray-400';
|
||||||
};
|
};
|
||||||
@@ -138,7 +138,7 @@
|
|||||||
<tr class=" border-b-[1.5px] border-gray-50/50 dark:border-gray-800/10">
|
<tr class=" border-b-[1.5px] border-gray-50/50 dark:border-gray-800/10">
|
||||||
<th
|
<th
|
||||||
scope="col"
|
scope="col"
|
||||||
class="px-2.5 py-2 cursor-pointer text-left w-8"
|
class="px-2.5 py-1.5 cursor-pointer text-left w-8"
|
||||||
on:click={() => setSortKey(`group_id:${groupId}`)}
|
on:click={() => setSortKey(`group_id:${groupId}`)}
|
||||||
>
|
>
|
||||||
<div class="flex gap-1.5 items-center">
|
<div class="flex gap-1.5 items-center">
|
||||||
@@ -162,7 +162,7 @@
|
|||||||
|
|
||||||
<th
|
<th
|
||||||
scope="col"
|
scope="col"
|
||||||
class="px-2.5 py-2 cursor-pointer select-none"
|
class="px-2.5 py-1.5 cursor-pointer select-none"
|
||||||
on:click={() => setSortKey('name')}
|
on:click={() => setSortKey('name')}
|
||||||
>
|
>
|
||||||
<div class="flex gap-1.5 items-center">
|
<div class="flex gap-1.5 items-center">
|
||||||
@@ -185,7 +185,7 @@
|
|||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
scope="col"
|
scope="col"
|
||||||
class="px-2.5 py-2 cursor-pointer select-none"
|
class="px-2.5 py-1.5 cursor-pointer select-none"
|
||||||
on:click={() => setSortKey('role')}
|
on:click={() => setSortKey('role')}
|
||||||
>
|
>
|
||||||
<div class="flex gap-1.5 items-center">
|
<div class="flex gap-1.5 items-center">
|
||||||
@@ -209,7 +209,7 @@
|
|||||||
|
|
||||||
<th
|
<th
|
||||||
scope="col"
|
scope="col"
|
||||||
class="px-2.5 py-2 cursor-pointer select-none"
|
class="px-2.5 py-1.5 cursor-pointer select-none"
|
||||||
on:click={() => setSortKey('last_active_at')}
|
on:click={() => setSortKey('last_active_at')}
|
||||||
>
|
>
|
||||||
<div class="flex gap-1.5 items-center">
|
<div class="flex gap-1.5 items-center">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
||||||
import { config, user } from '$lib/stores';
|
import { adminUserCount, config, user } from '$lib/stores';
|
||||||
import { getContext, onDestroy } from 'svelte';
|
import { getContext, onDestroy } from 'svelte';
|
||||||
|
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
@@ -15,6 +15,9 @@
|
|||||||
|
|
||||||
import Pagination from '$lib/components/common/Pagination.svelte';
|
import Pagination from '$lib/components/common/Pagination.svelte';
|
||||||
import ChatBubbles from '$lib/components/icons/ChatBubbles.svelte';
|
import ChatBubbles from '$lib/components/icons/ChatBubbles.svelte';
|
||||||
|
import EditPencil from '$lib/components/icons/EditPencil.svelte';
|
||||||
|
import Eye from '$lib/components/icons/Eye.svelte';
|
||||||
|
import Trash from '$lib/components/icons/Trash.svelte';
|
||||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
|
|
||||||
import EditUserModal from '$lib/components/admin/Users/UserList/EditUserModal.svelte';
|
import EditUserModal from '$lib/components/admin/Users/UserList/EditUserModal.svelte';
|
||||||
@@ -24,10 +27,11 @@
|
|||||||
import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
||||||
|
|
||||||
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 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 Banner from '$lib/components/common/Banner.svelte';
|
import Banner from '$lib/components/common/Banner.svelte';
|
||||||
import Markdown from '$lib/components/chat/Messages/Markdown.svelte';
|
|
||||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||||
import ProfilePreview from '$lib/components/channel/Messages/Message/ProfilePreview.svelte';
|
import ProfilePreview from '$lib/components/channel/Messages/Message/ProfilePreview.svelte';
|
||||||
import UserPreviewModal from '$lib/components/admin/UserPreviewModal.svelte';
|
import UserPreviewModal from '$lib/components/admin/UserPreviewModal.svelte';
|
||||||
@@ -80,10 +84,10 @@
|
|||||||
|
|
||||||
const roleClass = (role) => {
|
const roleClass = (role) => {
|
||||||
if (role === 'admin') {
|
if (role === 'admin') {
|
||||||
return 'text-blue-700/50 dark:text-blue-300/50';
|
return 'text-[#4f6f93] dark:text-[#8ba6c6]';
|
||||||
}
|
}
|
||||||
if (role === 'user') {
|
if (role === 'user') {
|
||||||
return 'text-green-700/50 dark:text-green-300/50';
|
return 'text-[#4f7a5a] dark:text-[#8db395]';
|
||||||
}
|
}
|
||||||
return 'text-gray-500 dark:text-gray-400';
|
return 'text-gray-500 dark:text-gray-400';
|
||||||
};
|
};
|
||||||
@@ -100,6 +104,7 @@
|
|||||||
if (res) {
|
if (res) {
|
||||||
users = res.users;
|
users = res.users;
|
||||||
total = res.total;
|
total = res.total;
|
||||||
|
adminUserCount.set(total);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
@@ -172,72 +177,46 @@
|
|||||||
<Spinner className="size-5" />
|
<Spinner className="size-5" />
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div
|
<div class="pt-0.5 pb-1 sticky top-0 z-10 bg-white dark:bg-gray-900">
|
||||||
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 h-8 flex-1 items-center w-full gap-2">
|
||||||
>
|
<div class="flex min-w-0 flex-1 items-center">
|
||||||
<div class="flex items-center md:self-center text-xl font-normal px-0.5 gap-2 shrink-0">
|
<div class="self-center ml-1 mr-3">
|
||||||
<div class="flex-shrink-0">
|
<Search className="size-3.5" />
|
||||||
{$i18n.t('Users')}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
{#if ($config?.license_metadata?.seats ?? null) !== null}
|
|
||||||
{#if total > $config?.license_metadata?.seats}
|
|
||||||
<span class="text-lg font-normal text-red-500"
|
|
||||||
>{total} of {$config?.license_metadata?.seats}
|
|
||||||
<span class="text-sm font-normal">{$i18n.t('available users')}</span></span
|
|
||||||
>
|
|
||||||
{:else}
|
|
||||||
<span class="text-lg font-normal text-gray-500 dark:text-gray-300"
|
|
||||||
>{total} of {$config?.license_metadata?.seats}
|
|
||||||
<span class="text-sm font-normal">{$i18n.t('available users')}</span></span
|
|
||||||
>
|
|
||||||
{/if}
|
|
||||||
{:else}
|
|
||||||
<span class="text-lg font-normal text-gray-500 dark:text-gray-300">{total}</span>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex gap-1">
|
|
||||||
<div class="flex w-full space-x-2">
|
|
||||||
<div class="flex flex-1">
|
|
||||||
<div class=" self-center ml-1 mr-3">
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
class="w-4 h-4"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-hidden bg-transparent"
|
|
||||||
bind:value={query}
|
|
||||||
on:input={handleSearchInput}
|
|
||||||
aria-label={$i18n.t('Search')}
|
|
||||||
placeholder={$i18n.t('Search')}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
<input
|
||||||
|
class="w-full text-sm pr-4 py-1 rounded-r-xl outline-hidden bg-transparent"
|
||||||
|
bind:value={query}
|
||||||
|
on:input={handleSearchInput}
|
||||||
|
aria-label={$i18n.t('Search')}
|
||||||
|
placeholder={$i18n.t('Search')}
|
||||||
|
/>
|
||||||
|
|
||||||
<div>
|
{#if query}
|
||||||
<Tooltip content={$i18n.t('Add User')}>
|
<div class="self-center pl-1.5 translate-y-[0.5px] rounded-l-xl bg-transparent">
|
||||||
<button
|
<button
|
||||||
class=" p-2 rounded-xl hover:bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-850 transition font-normal text-sm flex items-center space-x-1"
|
class="p-0.5 rounded-full hover:bg-gray-100 dark:hover:bg-gray-900 transition"
|
||||||
|
aria-label={$i18n.t('Clear search')}
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
showAddUserModal = !showAddUserModal;
|
query = '';
|
||||||
|
handleSearchInput();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Plus className="size-3.5" />
|
<XMark className="size-3" strokeWidth="2" />
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</div>
|
||||||
</div>
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Tooltip content={$i18n.t('Add User')}>
|
||||||
|
<button
|
||||||
|
class="p-2 rounded-xl hover:bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-850 transition font-normal text-sm flex items-center"
|
||||||
|
on:click={() => {
|
||||||
|
showAddUserModal = !showAddUserModal;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Plus className="size-3.5" />
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -247,7 +226,7 @@
|
|||||||
<tr class=" border-b-[1.5px] border-gray-50 dark:border-gray-850/30">
|
<tr class=" border-b-[1.5px] border-gray-50 dark:border-gray-850/30">
|
||||||
<th
|
<th
|
||||||
scope="col"
|
scope="col"
|
||||||
class="px-2.5 py-2 cursor-pointer select-none"
|
class="px-2.5 py-1.5 cursor-pointer select-none"
|
||||||
on:click={() => setSortKey('name')}
|
on:click={() => setSortKey('name')}
|
||||||
>
|
>
|
||||||
<div class="flex gap-1.5 items-center">
|
<div class="flex gap-1.5 items-center">
|
||||||
@@ -270,7 +249,7 @@
|
|||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
scope="col"
|
scope="col"
|
||||||
class="px-2.5 py-2 cursor-pointer select-none"
|
class="px-2.5 py-1.5 cursor-pointer select-none"
|
||||||
on:click={() => setSortKey('role')}
|
on:click={() => setSortKey('role')}
|
||||||
>
|
>
|
||||||
<div class="flex gap-1.5 items-center">
|
<div class="flex gap-1.5 items-center">
|
||||||
@@ -293,7 +272,7 @@
|
|||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
scope="col"
|
scope="col"
|
||||||
class="px-2.5 py-2 cursor-pointer select-none"
|
class="px-2.5 py-1.5 cursor-pointer select-none"
|
||||||
on:click={() => setSortKey('email')}
|
on:click={() => setSortKey('email')}
|
||||||
>
|
>
|
||||||
<div class="flex gap-1.5 items-center">
|
<div class="flex gap-1.5 items-center">
|
||||||
@@ -317,7 +296,7 @@
|
|||||||
|
|
||||||
<th
|
<th
|
||||||
scope="col"
|
scope="col"
|
||||||
class="px-2.5 py-2 cursor-pointer select-none"
|
class="px-2.5 py-1.5 cursor-pointer select-none"
|
||||||
on:click={() => setSortKey('last_active_at')}
|
on:click={() => setSortKey('last_active_at')}
|
||||||
>
|
>
|
||||||
<div class="flex gap-1.5 items-center">
|
<div class="flex gap-1.5 items-center">
|
||||||
@@ -341,7 +320,7 @@
|
|||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
scope="col"
|
scope="col"
|
||||||
class="px-2.5 py-2 cursor-pointer select-none"
|
class="px-2.5 py-1.5 cursor-pointer select-none"
|
||||||
on:click={() => setSortKey('created_at')}
|
on:click={() => setSortKey('created_at')}
|
||||||
>
|
>
|
||||||
<div class="flex gap-1.5 items-center">
|
<div class="flex gap-1.5 items-center">
|
||||||
@@ -362,7 +341,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
|
|
||||||
<th scope="col" class="px-2.5 py-2 text-right"></th>
|
<th scope="col" class="px-2.5 py-1.5 text-right"></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="">
|
<tbody class="">
|
||||||
@@ -372,7 +351,7 @@
|
|||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<ProfilePreview {user} side="right" align="center" sideOffset={6}>
|
<ProfilePreview {user} side="right" align="center" sideOffset={6}>
|
||||||
<img
|
<img
|
||||||
class="rounded-full w-6 min-w-6 h-6 object-cover mr-0.5 flex-shrink-0"
|
class="rounded-full w-6 min-w-6 h-6 object-cover flex-shrink-0"
|
||||||
src={`${WEBUI_API_BASE_URL}/users/${user.id}/profile/image`}
|
src={`${WEBUI_API_BASE_URL}/users/${user.id}/profile/image`}
|
||||||
alt="user"
|
alt="user"
|
||||||
on:error={(e) => {
|
on:error={(e) => {
|
||||||
@@ -422,14 +401,14 @@
|
|||||||
{#if $config.features.enable_admin_chat_access && user.role !== 'admin'}
|
{#if $config.features.enable_admin_chat_access && user.role !== 'admin'}
|
||||||
<Tooltip content={$i18n.t('Chats')}>
|
<Tooltip content={$i18n.t('Chats')}>
|
||||||
<button
|
<button
|
||||||
class="self-center w-fit text-sm px-2 py-2 hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
|
class="self-center w-fit p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg"
|
||||||
aria-label={$i18n.t('Chats')}
|
aria-label={$i18n.t('Chats')}
|
||||||
on:click={async () => {
|
on:click={async () => {
|
||||||
showUserChatsModal = !showUserChatsModal;
|
showUserChatsModal = !showUserChatsModal;
|
||||||
selectedUser = user;
|
selectedUser = user;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ChatBubbles />
|
<ChatBubbles className="size-3.5" />
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -437,86 +416,42 @@
|
|||||||
{#if user.role !== 'admin'}
|
{#if user.role !== 'admin'}
|
||||||
<Tooltip content={$i18n.t('Preview Access')}>
|
<Tooltip content={$i18n.t('Preview Access')}>
|
||||||
<button
|
<button
|
||||||
class="self-center w-fit text-sm px-2 py-2 hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
|
class="self-center w-fit p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg"
|
||||||
aria-label={$i18n.t('Preview Access')}
|
aria-label={$i18n.t('Preview Access')}
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
selectedUser = user;
|
selectedUser = user;
|
||||||
showUserPreviewModal = true;
|
showUserPreviewModal = true;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<svg
|
<Eye className="size-3.5" />
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke-width="1.5"
|
|
||||||
stroke="currentColor"
|
|
||||||
class="w-4 h-4"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<Tooltip content={$i18n.t('Edit User')}>
|
<Tooltip content={$i18n.t('Edit User')}>
|
||||||
<button
|
<button
|
||||||
class="self-center w-fit text-sm px-2 py-2 hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
|
class="self-center w-fit p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg"
|
||||||
aria-label={$i18n.t('Edit User')}
|
aria-label={$i18n.t('Edit User')}
|
||||||
on:click={async () => {
|
on:click={async () => {
|
||||||
showEditUserModal = !showEditUserModal;
|
showEditUserModal = !showEditUserModal;
|
||||||
selectedUser = user;
|
selectedUser = user;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<svg
|
<EditPencil className="size-3.5" />
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke-width="1.5"
|
|
||||||
stroke="currentColor"
|
|
||||||
class="w-4 h-4"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L6.832 19.82a4.5 4.5 0 0 1-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 0 1 1.13-1.897L16.863 4.487Zm0 0L19.5 7.125"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
{#if user.role !== 'admin'}
|
{#if user.role !== 'admin'}
|
||||||
<Tooltip content={$i18n.t('Delete User')}>
|
<Tooltip content={$i18n.t('Delete User')}>
|
||||||
<button
|
<button
|
||||||
class="self-center w-fit text-sm px-2 py-2 hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
|
class="self-center w-fit p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg"
|
||||||
aria-label={$i18n.t('Delete User')}
|
aria-label={$i18n.t('Delete User')}
|
||||||
on:click={async () => {
|
on:click={async () => {
|
||||||
showDeleteConfirmDialog = true;
|
showDeleteConfirmDialog = true;
|
||||||
selectedUser = user;
|
selectedUser = user;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<svg
|
<Trash className="size-3.5" />
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke-width="1.5"
|
|
||||||
stroke="currentColor"
|
|
||||||
class="w-4 h-4"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -539,24 +474,48 @@
|
|||||||
|
|
||||||
{#if !$config?.license_metadata}
|
{#if !$config?.license_metadata}
|
||||||
{#if total > 50}
|
{#if total > 50}
|
||||||
<div class="text-sm">
|
<div class="mt-3 mb-3 pb-1 text-gray-700 dark:text-gray-300">
|
||||||
<Markdown
|
<div class="max-w-3xl text-xs leading-5">
|
||||||
content={`
|
<div class="text-gray-900 dark:text-gray-100">
|
||||||
> [!NOTE]
|
{$i18n.t('Running Open WebUI for a team?')}
|
||||||
> # **Hey there! 👋**
|
</div>
|
||||||
>
|
<div class="mt-2 space-y-2">
|
||||||
> It looks like you have over 50 users, that usually falls under organizational usage.
|
<p>
|
||||||
>
|
{$i18n.t(
|
||||||
> Open WebUI is completely free to use as-is, with no restrictions or hidden limits, and we'd love to keep it that way. 🌱
|
'You have more than 50 users, which often means this workspace is supporting organizational use. Open WebUI is free to use as-is, with no restrictions or hidden limits, and we want to keep it that way.'
|
||||||
>
|
)}
|
||||||
> By supporting the project through sponsorship or an enterprise license, you’re not only helping us stay independent, you’re also helping us ship new features faster, improve stability, and grow the project for the long haul. With an *enterprise license*, you also get additional perks like dedicated support, customization options, and more, all at a fraction of what it would cost to build and maintain internally.
|
</p>
|
||||||
>
|
<p class="text-gray-500 dark:text-gray-400">
|
||||||
> Your support helps us stay independent and continue building great tools for everyone. 💛
|
{$i18n.t(
|
||||||
>
|
'By supporting the project through sponsorship or an enterprise license, you help us stay independent, ship new features faster, improve stability, and grow Open WebUI for the long haul.'
|
||||||
> - 👉 **[Click here to learn more about enterprise licensing](https://docs.openwebui.com/enterprise)**
|
)}
|
||||||
> - 👉 *[Click here to sponsor the project on GitHub](https://github.com/sponsors/open-webui)*
|
</p>
|
||||||
`}
|
<p class="text-gray-500 dark:text-gray-400">
|
||||||
/>
|
{$i18n.t(
|
||||||
|
'Enterprise licenses also include dedicated support, customization options, and more, at a fraction of the cost of building and maintaining this stack internally.'
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-2 flex items-center gap-3">
|
||||||
|
<a
|
||||||
|
class="text-xs text-gray-700 underline transition hover:text-gray-900 dark:text-gray-300 dark:hover:text-gray-100"
|
||||||
|
href="https://docs.openwebui.com/enterprise"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
{$i18n.t('Enterprise licensing')}
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
class="text-xs text-gray-500 underline transition hover:text-gray-900 dark:text-gray-500 dark:hover:text-gray-100"
|
||||||
|
href="https://github.com/sponsors/open-webui"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
{$i18n.t('Sponsor on GitHub')}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -85,6 +85,45 @@
|
|||||||
const isAdminTab = (tabId: string) => tabId.startsWith('admin:');
|
const isAdminTab = (tabId: string) => tabId.startsWith('admin:');
|
||||||
const adminTabSegment = (tabId: string) => tabId.replace('admin:', '');
|
const adminTabSegment = (tabId: string) => tabId.replace('admin:', '');
|
||||||
const adminTabPanelId = (tabId: string) => `tab-${tabId.replace(':', '-')}`;
|
const adminTabPanelId = (tabId: string) => `tab-${tabId.replace(':', '-')}`;
|
||||||
|
const personalSettingGroups: Record<string, string> = {
|
||||||
|
general: 'Basics',
|
||||||
|
interface: 'Basics',
|
||||||
|
shortcuts: 'Basics',
|
||||||
|
connections: 'Services',
|
||||||
|
tools: 'Services',
|
||||||
|
personalization: 'Preferences',
|
||||||
|
audio: 'Preferences',
|
||||||
|
data_controls: 'Data',
|
||||||
|
archived_chats: 'Data',
|
||||||
|
account: 'Profile',
|
||||||
|
about: 'Profile'
|
||||||
|
};
|
||||||
|
const adminSettingGroups: Record<string, string> = {
|
||||||
|
'admin:general': 'System',
|
||||||
|
'admin:authentication': 'System',
|
||||||
|
'admin:connections': 'AI',
|
||||||
|
'admin:models': 'AI',
|
||||||
|
'admin:subagents': 'AI',
|
||||||
|
'admin:evaluations': 'Quality',
|
||||||
|
'admin:analytics': 'Quality',
|
||||||
|
'admin:integrations': 'Tools',
|
||||||
|
'admin:documents': 'Tools',
|
||||||
|
'admin:web': 'Tools',
|
||||||
|
'admin:code-execution': 'Tools',
|
||||||
|
'admin:pipelines': 'Tools',
|
||||||
|
'admin:interface': 'Experience',
|
||||||
|
'admin:audio': 'Experience',
|
||||||
|
'admin:images': 'Experience',
|
||||||
|
'admin:db': 'Data'
|
||||||
|
};
|
||||||
|
const settingGroupTitle = (tabId: string) =>
|
||||||
|
(isAdminTab(tabId) ? adminSettingGroups[tabId] : personalSettingGroups[tabId]) ?? 'General';
|
||||||
|
const shouldShowSettingGroup = (tabIds: string[], index: number) =>
|
||||||
|
index === 0 || settingGroupTitle(tabIds[index]) !== settingGroupTitle(tabIds[index - 1]);
|
||||||
|
const settingGroupHeadingClass = (first: boolean) =>
|
||||||
|
`hidden md:block text-[0.625rem] text-gray-400 dark:text-gray-600 px-2 ${
|
||||||
|
first ? 'mt-0.5' : 'mt-2'
|
||||||
|
} mb-0.5`;
|
||||||
|
|
||||||
const allSettings: SettingsTab[] = [
|
const allSettings: SettingsTab[] = [
|
||||||
{
|
{
|
||||||
@@ -628,6 +667,11 @@
|
|||||||
title: 'Code Execution',
|
title: 'Code Execution',
|
||||||
keywords: ['code execution', 'python', 'sandbox', 'compiler', 'jupyter', 'interpreter']
|
keywords: ['code execution', 'python', 'sandbox', 'compiler', 'jupyter', 'interpreter']
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'admin:pipelines',
|
||||||
|
title: 'Pipelines',
|
||||||
|
keywords: ['pipelines', 'workflows', 'filters', 'valves', 'middleware']
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'admin:interface',
|
id: 'admin:interface',
|
||||||
title: 'Interface',
|
title: 'Interface',
|
||||||
@@ -643,19 +687,12 @@
|
|||||||
title: 'Images',
|
title: 'Images',
|
||||||
keywords: ['images', 'generation', 'dalle', 'stable diffusion', 'comfyui', 'automatic1111']
|
keywords: ['images', 'generation', 'dalle', 'stable diffusion', 'comfyui', 'automatic1111']
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 'admin:pipelines',
|
|
||||||
title: 'Pipelines',
|
|
||||||
keywords: ['pipelines', 'workflows', 'filters', 'valves', 'middleware']
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'admin:db',
|
id: 'admin:db',
|
||||||
title: 'Database',
|
title: 'Database',
|
||||||
keywords: ['database', 'export', 'import', 'backup', 'chats', 'users']
|
keywords: ['database', 'export', 'import', 'backup', 'chats', 'users']
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
const allKnownSettings = [...allSettings, ...adminSettings];
|
|
||||||
|
|
||||||
let availableSettings: SettingsTab[] = [];
|
let availableSettings: SettingsTab[] = [];
|
||||||
let filteredSettings: string[] = [];
|
let filteredSettings: string[] = [];
|
||||||
let filteredPersonalSettings: string[] = [];
|
let filteredPersonalSettings: string[] = [];
|
||||||
@@ -823,7 +860,13 @@
|
|||||||
</span>
|
</span>
|
||||||
|
|
||||||
{#if filteredPersonalSettings.length > 0}
|
{#if filteredPersonalSettings.length > 0}
|
||||||
{#each filteredPersonalSettings as tabId (tabId)}
|
{#each filteredPersonalSettings as tabId, index (tabId)}
|
||||||
|
{#if shouldShowSettingGroup(filteredPersonalSettings, index)}
|
||||||
|
<span class={settingGroupHeadingClass(index === 0)}>
|
||||||
|
{$i18n.t(settingGroupTitle(tabId))}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if tabId === 'general'}
|
{#if tabId === 'general'}
|
||||||
<button
|
<button
|
||||||
role="tab"
|
role="tab"
|
||||||
@@ -977,12 +1020,18 @@
|
|||||||
|
|
||||||
{#if $user?.role === 'admin' && filteredAdminSettings.length > 0}
|
{#if $user?.role === 'admin' && filteredAdminSettings.length > 0}
|
||||||
<span
|
<span
|
||||||
class="hidden md:block text-[0.625rem] text-gray-400 dark:text-gray-600 px-2 mt-2 mb-0.5"
|
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"
|
||||||
>
|
>
|
||||||
{$i18n.t('Admin')}
|
{$i18n.t('Admin')}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{#each filteredAdminSettings as tabId (tabId)}
|
{#each filteredAdminSettings as tabId, index (tabId)}
|
||||||
|
{#if shouldShowSettingGroup(filteredAdminSettings, index)}
|
||||||
|
<span class={settingGroupHeadingClass(index === 0)}>
|
||||||
|
{$i18n.t(settingGroupTitle(tabId))}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{@const tab = adminSettings.find((setting) => setting.id === tabId)}
|
{@const tab = adminSettings.find((setting) => setting.id === tabId)}
|
||||||
{#if tab}
|
{#if tab}
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ export const workspaceCounts: Writable<Record<WorkspaceSection, number | null>>
|
|||||||
tools: null
|
tools: null
|
||||||
});
|
});
|
||||||
export const workspaceActions: Writable<WorkspaceAction[]> = writable([]);
|
export const workspaceActions: Writable<WorkspaceAction[]> = writable([]);
|
||||||
|
export const adminUserCount: Writable<number | null> = writable(null);
|
||||||
|
|
||||||
export const toolServers = writable([]);
|
export const toolServers = writable([]);
|
||||||
export const terminalServers = writable([]);
|
export const terminalServers = writable([]);
|
||||||
|
|||||||
@@ -2,15 +2,40 @@
|
|||||||
import { onMount, getContext } from 'svelte';
|
import { onMount, getContext } from 'svelte';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
|
|
||||||
import { WEBUI_NAME, config, mobile, showSettings, showSidebar, user } from '$lib/stores';
|
import {
|
||||||
|
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') {
|
||||||
@@ -67,10 +92,26 @@
|
|||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
draggable="false"
|
draggable="false"
|
||||||
class="min-w-fit p-1.5 {$page.url.pathname.includes('/admin/users')
|
class="min-w-fit p-1.5 flex items-center gap-1.5 {$page.url.pathname.includes(
|
||||||
|
'/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">{$i18n.t('Users')}</a
|
href="/admin"
|
||||||
|
><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
|
||||||
|
|||||||
@@ -156,7 +156,7 @@
|
|||||||
href="/workspace/models"
|
href="/workspace/models"
|
||||||
>
|
>
|
||||||
<span>{$i18n.t('Models')}</span>
|
<span>{$i18n.t('Models')}</span>
|
||||||
<span class="text-sm text-gray-500 dark:text-gray-500">
|
<span class="text-sm opacity-60">
|
||||||
{formatCount($workspaceCounts.models)}
|
{formatCount($workspaceCounts.models)}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
@@ -173,7 +173,7 @@
|
|||||||
href="/workspace/knowledge"
|
href="/workspace/knowledge"
|
||||||
>
|
>
|
||||||
<span>{$i18n.t('Knowledge')}</span>
|
<span>{$i18n.t('Knowledge')}</span>
|
||||||
<span class="text-sm text-gray-500 dark:text-gray-500">
|
<span class="text-sm opacity-60">
|
||||||
{formatCount($workspaceCounts.knowledge)}
|
{formatCount($workspaceCounts.knowledge)}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
@@ -190,7 +190,7 @@
|
|||||||
href="/workspace/prompts"
|
href="/workspace/prompts"
|
||||||
>
|
>
|
||||||
<span>{$i18n.t('Prompts')}</span>
|
<span>{$i18n.t('Prompts')}</span>
|
||||||
<span class="text-sm text-gray-500 dark:text-gray-500">
|
<span class="text-sm opacity-60">
|
||||||
{formatCount($workspaceCounts.prompts)}
|
{formatCount($workspaceCounts.prompts)}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
@@ -207,7 +207,7 @@
|
|||||||
href="/workspace/skills"
|
href="/workspace/skills"
|
||||||
>
|
>
|
||||||
<span>{$i18n.t('Skills')}</span>
|
<span>{$i18n.t('Skills')}</span>
|
||||||
<span class="text-sm text-gray-500 dark:text-gray-500">
|
<span class="text-sm opacity-60">
|
||||||
{formatCount($workspaceCounts.skills)}
|
{formatCount($workspaceCounts.skills)}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
@@ -224,7 +224,7 @@
|
|||||||
href="/workspace/tools"
|
href="/workspace/tools"
|
||||||
>
|
>
|
||||||
<span>{$i18n.t('Tools')}</span>
|
<span>{$i18n.t('Tools')}</span>
|
||||||
<span class="text-sm text-gray-500 dark:text-gray-500">
|
<span class="text-sm opacity-60">
|
||||||
{formatCount($workspaceCounts.tools)}
|
{formatCount($workspaceCounts.tools)}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
Reference in new issue
Block a user