refac
This commit is contained in:
@@ -267,3 +267,31 @@ export const removeUserFromGroup = async (token: string, id: string, userIds: st
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const getGroupPreview = async (token: string, id: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/groups/id/${id}/preview`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
authorization: `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err.detail;
|
||||
console.error(err);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
@@ -550,3 +550,30 @@ export const getUserGroupsById = async (token: string, userId: string) => {
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const getUserPreview = async (token: string, userId: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/users/${userId}/preview`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
error = err.detail;
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte';
|
||||
import { getUserPreview } from '$lib/apis/users';
|
||||
import Modal from '$lib/components/common/Modal.svelte';
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let show = false;
|
||||
export let userId: string = '';
|
||||
export let userName: string = '';
|
||||
|
||||
let loading = true;
|
||||
let preview: any = null;
|
||||
let error: string = '';
|
||||
|
||||
$: if (show && userId) {
|
||||
loadPreview();
|
||||
}
|
||||
|
||||
const loadPreview = async () => {
|
||||
loading = true;
|
||||
error = '';
|
||||
try {
|
||||
preview = await getUserPreview(localStorage.token, userId);
|
||||
} catch (e) {
|
||||
error = String(e);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<Modal size="md" bind:show>
|
||||
<div>
|
||||
<div class=" flex justify-between dark:text-gray-100 px-5 pt-4 mb-1.5">
|
||||
<div class=" text-lg font-medium self-center font-primary">
|
||||
{$i18n.t('User Preview')}
|
||||
{#if userName}
|
||||
<span class="text-sm font-normal text-gray-500 ml-1">{userName}</span>
|
||||
{/if}
|
||||
</div>
|
||||
<button
|
||||
class="self-center"
|
||||
on:click={() => {
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
<XMark className={'size-5'} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col w-full px-5 pb-4">
|
||||
{#if loading}
|
||||
<div class="flex justify-center items-center py-8">
|
||||
<Spinner className="size-5" />
|
||||
</div>
|
||||
{:else if error}
|
||||
<div class="text-red-500 text-xs text-center py-4">{error}</div>
|
||||
{:else if preview}
|
||||
<div class="space-y-2">
|
||||
{#if preview.groups.length > 0}
|
||||
<div>
|
||||
<div class=" mb-2 text-sm font-medium">{$i18n.t('Groups')}</div>
|
||||
<div class="flex flex-col w-full">
|
||||
{#each preview.groups as group}
|
||||
<div class="flex w-full justify-between my-1">
|
||||
<div class=" self-center text-xs font-medium">{group.name}</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="border-gray-50 dark:border-gray-850/30 my-1" />
|
||||
{/if}
|
||||
|
||||
<div>
|
||||
<div class=" mb-2 text-sm font-medium">{$i18n.t('Models')}</div>
|
||||
<div class="flex flex-col w-full">
|
||||
{#if preview.models.items.length === 0}
|
||||
<div class="flex w-full justify-between my-1">
|
||||
<div class=" self-center text-xs text-gray-500">
|
||||
{$i18n.t('No models accessible')}
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
{#each preview.models.items as model}
|
||||
<div class="flex w-full justify-between my-1">
|
||||
<div class=" self-center text-xs font-medium">{model.name}</div>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
{#if preview.models.total > preview.models.items.length}
|
||||
<div class="flex w-full justify-between my-1">
|
||||
<div class=" self-center text-xs text-gray-500">
|
||||
{$i18n.t('{{count}} of {{total}} accessible', {
|
||||
count: preview.models.items.length,
|
||||
total: preview.models.total
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="border-gray-50 dark:border-gray-850/30 my-1" />
|
||||
|
||||
<div>
|
||||
<div class=" mb-2 text-sm font-medium">{$i18n.t('Knowledge')}</div>
|
||||
<div class="flex flex-col w-full">
|
||||
{#if preview.knowledge.items.length === 0}
|
||||
<div class="flex w-full justify-between my-1">
|
||||
<div class=" self-center text-xs text-gray-500">
|
||||
{$i18n.t('No knowledge bases accessible')}
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
{#each preview.knowledge.items as kb}
|
||||
<div class="flex w-full justify-between my-1">
|
||||
<div class=" self-center text-xs font-medium">{kb.name}</div>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
{#if preview.knowledge.total > preview.knowledge.items.length}
|
||||
<div class="flex w-full justify-between my-1">
|
||||
<div class=" self-center text-xs text-gray-500">
|
||||
{$i18n.t('{{count}} of {{total}} accessible', {
|
||||
count: preview.knowledge.items.length,
|
||||
total: preview.knowledge.total
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="border-gray-50 dark:border-gray-850/30 my-1" />
|
||||
|
||||
<div>
|
||||
<div class=" mb-2 text-sm font-medium">{$i18n.t('Tools')}</div>
|
||||
<div class="flex flex-col w-full">
|
||||
{#if preview.tools.items.length === 0}
|
||||
<div class="flex w-full justify-between my-1">
|
||||
<div class=" self-center text-xs text-gray-500">
|
||||
{$i18n.t('No tools accessible')}
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
{#each preview.tools.items as tool}
|
||||
<div class="flex w-full justify-between my-1">
|
||||
<div class=" self-center text-xs font-medium">{tool.name}</div>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
{#if preview.tools.total > preview.tools.items.length}
|
||||
<div class="flex w-full justify-between my-1">
|
||||
<div class=" self-center text-xs text-gray-500">
|
||||
{$i18n.t('{{count}} of {{total}} accessible', {
|
||||
count: preview.tools.items.length,
|
||||
total: preview.tools.total
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
@@ -8,6 +8,7 @@
|
||||
import General from './General.svelte';
|
||||
import Permissions from './Permissions.svelte';
|
||||
import Users from './Users.svelte';
|
||||
import GroupPreviewPanel from './GroupPreviewPanel.svelte';
|
||||
import { DEFAULT_PERMISSIONS } from '$lib/constants/permissions';
|
||||
import UserPlusSolid from '$lib/components/icons/UserPlusSolid.svelte';
|
||||
import WrenchSolid from '$lib/components/icons/WrenchSolid.svelte';
|
||||
@@ -195,6 +196,36 @@
|
||||
<div class=" self-center">{$i18n.t('Users')}</div>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
{#if tabs.includes('preview')}
|
||||
<button
|
||||
class="px-0.5 py-1 max-w-fit w-fit rounded-lg flex-1 lg:flex-none flex text-right transition {selectedTab ===
|
||||
'preview'
|
||||
? ''
|
||||
: ' text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'}"
|
||||
on:click={() => {
|
||||
selectedTab = 'preview';
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<div class=" self-center mr-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path d="M8 9.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z" />
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M1.38 8.28a.87.87 0 0 1 0-.566 7.003 7.003 0 0 1 13.238.006.87.87 0 0 1 0 .566A7.003 7.003 0 0 1 1.379 8.28ZM11 8a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center">{$i18n.t('Preview')}</div>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex-1 mt-1 lg:mt-1 lg:h-[30rem] lg:max-h-[30rem] flex flex-col">
|
||||
@@ -213,6 +244,8 @@
|
||||
<Permissions bind:permissions {defaultPermissions} />
|
||||
{:else if selectedTab == 'users'}
|
||||
<Users bind:userCount groupId={group?.id} />
|
||||
{:else if selectedTab == 'preview'}
|
||||
<GroupPreviewPanel groupId={group?.id} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -57,6 +57,7 @@
|
||||
edit
|
||||
{group}
|
||||
{defaultPermissions}
|
||||
tabs={['general', 'permissions', 'users', 'preview']}
|
||||
onSubmit={updateHandler}
|
||||
onDelete={deleteHandler}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte';
|
||||
import { getGroupPreview } from '$lib/apis/groups';
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let groupId: string = '';
|
||||
|
||||
let loading = true;
|
||||
let preview: any = null;
|
||||
let error: string = '';
|
||||
|
||||
$: if (groupId) {
|
||||
loadPreview();
|
||||
}
|
||||
|
||||
const loadPreview = async () => {
|
||||
loading = true;
|
||||
error = '';
|
||||
try {
|
||||
preview = await getGroupPreview(localStorage.token, groupId);
|
||||
} catch (e) {
|
||||
error = String(e);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="space-y-2">
|
||||
{#if loading}
|
||||
<div class="flex justify-center items-center py-8">
|
||||
<Spinner className="size-5" />
|
||||
</div>
|
||||
{:else if error}
|
||||
<div class="text-red-500 text-xs text-center py-4">{error}</div>
|
||||
{:else if preview}
|
||||
<div>
|
||||
<div class=" mb-2 text-sm font-medium">{$i18n.t('Models')}</div>
|
||||
|
||||
<div class="flex flex-col w-full">
|
||||
{#if preview.models.items.length === 0}
|
||||
<div class="flex w-full justify-between my-1">
|
||||
<div class=" self-center text-xs text-gray-500">
|
||||
{$i18n.t('No models accessible')}
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
{#each preview.models.items as model}
|
||||
<div class="flex w-full justify-between my-1">
|
||||
<div class=" self-center text-xs font-medium">{model.name}</div>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
{#if preview.models.total > preview.models.items.length}
|
||||
<div class="flex w-full justify-between my-1">
|
||||
<div class=" self-center text-xs text-gray-500">
|
||||
{$i18n.t('{{count}} of {{total}} accessible', {
|
||||
count: preview.models.items.length,
|
||||
total: preview.models.total
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="border-gray-50 dark:border-gray-850/30 my-1" />
|
||||
|
||||
<div>
|
||||
<div class=" mb-2 text-sm font-medium">{$i18n.t('Knowledge')}</div>
|
||||
|
||||
<div class="flex flex-col w-full">
|
||||
{#if preview.knowledge.items.length === 0}
|
||||
<div class="flex w-full justify-between my-1">
|
||||
<div class=" self-center text-xs text-gray-500">
|
||||
{$i18n.t('No knowledge bases accessible')}
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
{#each preview.knowledge.items as kb}
|
||||
<div class="flex w-full justify-between my-1">
|
||||
<div class=" self-center text-xs font-medium">{kb.name}</div>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
{#if preview.knowledge.total > preview.knowledge.items.length}
|
||||
<div class="flex w-full justify-between my-1">
|
||||
<div class=" self-center text-xs text-gray-500">
|
||||
{$i18n.t('{{count}} of {{total}} accessible', {
|
||||
count: preview.knowledge.items.length,
|
||||
total: preview.knowledge.total
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="border-gray-50 dark:border-gray-850/30 my-1" />
|
||||
|
||||
<div>
|
||||
<div class=" mb-2 text-sm font-medium">{$i18n.t('Tools')}</div>
|
||||
|
||||
<div class="flex flex-col w-full">
|
||||
{#if preview.tools.items.length === 0}
|
||||
<div class="flex w-full justify-between my-1">
|
||||
<div class=" self-center text-xs text-gray-500">
|
||||
{$i18n.t('No tools accessible')}
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
{#each preview.tools.items as tool}
|
||||
<div class="flex w-full justify-between my-1">
|
||||
<div class=" self-center text-xs font-medium">{tool.name}</div>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
{#if preview.tools.total > preview.tools.items.length}
|
||||
<div class="flex w-full justify-between my-1">
|
||||
<div class=" self-center text-xs text-gray-500">
|
||||
{$i18n.t('{{count}} of {{total}} accessible', {
|
||||
count: preview.tools.items.length,
|
||||
total: preview.tools.total
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -261,6 +261,7 @@
|
||||
<td class=" px-3 py-1">
|
||||
{dayjs(user.last_active_at * 1000).fromNow()}
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
import Markdown from '$lib/components/chat/Messages/Markdown.svelte';
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
import ProfilePreview from '$lib/components/channel/Messages/Message/ProfilePreview.svelte';
|
||||
import UserPreviewModal from '$lib/components/admin/UserPreviewModal.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
@@ -54,6 +55,7 @@
|
||||
|
||||
let showUserChatsModal = false;
|
||||
let showEditUserModal = false;
|
||||
let showUserPreviewModal = false;
|
||||
|
||||
const deleteUserHandler = async (id) => {
|
||||
const res = await deleteUserById(localStorage.token, id).catch((error) => {
|
||||
@@ -425,6 +427,39 @@
|
||||
</Tooltip>
|
||||
{/if}
|
||||
|
||||
{#if user.role !== 'admin'}
|
||||
<Tooltip content={$i18n.t('Preview Access')}>
|
||||
<button
|
||||
class="self-center w-fit text-sm px-2 py-2 hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
|
||||
aria-label={$i18n.t('Preview Access')}
|
||||
on:click={() => {
|
||||
selectedUser = user;
|
||||
showUserPreviewModal = true;
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
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>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
|
||||
<Tooltip content={$i18n.t('Edit User')}>
|
||||
<button
|
||||
class="self-center w-fit text-sm px-2 py-2 hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
|
||||
@@ -518,3 +553,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#if selectedUser}
|
||||
<UserPreviewModal bind:show={showUserPreviewModal} userId={selectedUser.id} userName={selectedUser.name} />
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user