refac
This commit is contained in:
@@ -7,6 +7,7 @@ from fastapi import APIRouter, Depends, HTTPException, Request, status
|
||||
from open_webui.config import CACHE_DIR
|
||||
from open_webui.constants import ERROR_MESSAGES
|
||||
from open_webui.internal.db import get_async_session
|
||||
from open_webui.models.access_grants import AccessGrants
|
||||
from open_webui.models.groups import (
|
||||
GroupForm,
|
||||
GroupInfoResponse,
|
||||
@@ -15,6 +16,9 @@ from open_webui.models.groups import (
|
||||
GroupUpdateForm,
|
||||
UserIdsForm,
|
||||
)
|
||||
from open_webui.models.knowledge import Knowledges
|
||||
from open_webui.models.models import Models
|
||||
from open_webui.models.tools import Tools
|
||||
from open_webui.models.users import UserInfoResponse, Users
|
||||
from open_webui.utils.auth import get_admin_user, get_verified_user
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
@@ -273,3 +277,88 @@ async def delete_group_by_id(id: str, user=Depends(get_admin_user), db: AsyncSes
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ERROR_MESSAGES.DEFAULT(e),
|
||||
)
|
||||
|
||||
|
||||
############################
|
||||
# PreviewGroupAccess
|
||||
############################
|
||||
|
||||
|
||||
@router.get('/id/{id}/preview')
|
||||
async def preview_group_access(
|
||||
id: str,
|
||||
user=Depends(get_admin_user),
|
||||
db: AsyncSession = Depends(get_async_session),
|
||||
):
|
||||
"""Show what resources a group can access (preview audit)."""
|
||||
group = await Groups.get_group_by_id(id, db=db)
|
||||
if not group:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||
)
|
||||
|
||||
group_ids = {group.id}
|
||||
|
||||
# Batch-check accessible resources using existing AccessGrants
|
||||
all_models = await Models.get_all_models(db=db)
|
||||
accessible_model_ids = await AccessGrants.get_accessible_resource_ids(
|
||||
user_id='',
|
||||
resource_type='model',
|
||||
resource_ids=[m.id for m in all_models],
|
||||
permission='read',
|
||||
user_group_ids=group_ids,
|
||||
db=db,
|
||||
)
|
||||
|
||||
all_knowledge = await Knowledges.get_knowledge_bases(db=db)
|
||||
accessible_knowledge_ids = await AccessGrants.get_accessible_resource_ids(
|
||||
user_id='',
|
||||
resource_type='knowledge',
|
||||
resource_ids=[k.id for k in all_knowledge],
|
||||
permission='read',
|
||||
user_group_ids=group_ids,
|
||||
db=db,
|
||||
)
|
||||
|
||||
all_tools = await Tools.get_tools(defer_content=True, db=db)
|
||||
accessible_tool_ids = await AccessGrants.get_accessible_resource_ids(
|
||||
user_id='',
|
||||
resource_type='tool',
|
||||
resource_ids=[t.id for t in all_tools],
|
||||
permission='read',
|
||||
user_group_ids=group_ids,
|
||||
db=db,
|
||||
)
|
||||
|
||||
active_models = [m for m in all_models if m.is_active]
|
||||
|
||||
return {
|
||||
'group': {'id': group.id, 'name': group.name},
|
||||
'models': {
|
||||
'items': [
|
||||
{'id': m.id, 'name': m.name}
|
||||
for m in active_models
|
||||
if m.id in accessible_model_ids
|
||||
],
|
||||
'total': len(active_models),
|
||||
},
|
||||
'knowledge': {
|
||||
'items': [
|
||||
{'id': k.id, 'name': k.name}
|
||||
for k in all_knowledge
|
||||
if k.id in accessible_knowledge_ids
|
||||
],
|
||||
'total': len(all_knowledge),
|
||||
},
|
||||
'tools': {
|
||||
'items': [
|
||||
{'id': t.id, 'name': t.name}
|
||||
for t in all_tools
|
||||
if t.id in accessible_tool_ids
|
||||
],
|
||||
'total': len(all_tools),
|
||||
},
|
||||
'permissions': group.permissions or {},
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ from __future__ import annotations
|
||||
import base64
|
||||
import io
|
||||
import logging
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
||||
@@ -25,6 +26,10 @@ from open_webui.models.users import (
|
||||
UserStatus,
|
||||
UserUpdateForm,
|
||||
)
|
||||
from open_webui.models.access_grants import AccessGrants
|
||||
from open_webui.models.knowledge import Knowledges
|
||||
from open_webui.models.models import Models
|
||||
from open_webui.models.tools import Tools
|
||||
from open_webui.socket.main import disconnect_user_sessions
|
||||
from open_webui.utils.access_control import get_permissions, has_permission
|
||||
from open_webui.utils.auth import (
|
||||
@@ -677,3 +682,88 @@ async def get_user_groups_by_id(
|
||||
user_id: str, user=Depends(get_admin_user), db: AsyncSession = Depends(get_async_session)
|
||||
):
|
||||
return await Groups.get_groups_by_member_id(user_id, db=db)
|
||||
|
||||
|
||||
############################
|
||||
# GetUserPreview
|
||||
############################
|
||||
|
||||
|
||||
@router.get('/{user_id}/preview')
|
||||
async def get_user_preview(
|
||||
user_id: str,
|
||||
user=Depends(get_admin_user),
|
||||
db: AsyncSession = Depends(get_async_session),
|
||||
):
|
||||
"""Show what resources a specific user can access across all their groups."""
|
||||
target_user = await Users.get_user_by_id(user_id, db=db)
|
||||
if not target_user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=ERROR_MESSAGES.USER_NOT_FOUND,
|
||||
)
|
||||
|
||||
# Get all group IDs this user belongs to
|
||||
user_groups = await Groups.get_groups_by_member_id(user_id, db=db)
|
||||
user_group_ids = {g.id for g in user_groups}
|
||||
|
||||
all_models = await Models.get_all_models(db=db)
|
||||
accessible_model_ids = await AccessGrants.get_accessible_resource_ids(
|
||||
user_id=user_id,
|
||||
resource_type='model',
|
||||
resource_ids=[m.id for m in all_models],
|
||||
permission='read',
|
||||
user_group_ids=user_group_ids,
|
||||
db=db,
|
||||
)
|
||||
|
||||
all_knowledge = await Knowledges.get_knowledge_bases(db=db)
|
||||
accessible_knowledge_ids = await AccessGrants.get_accessible_resource_ids(
|
||||
user_id=user_id,
|
||||
resource_type='knowledge',
|
||||
resource_ids=[k.id for k in all_knowledge],
|
||||
permission='read',
|
||||
user_group_ids=user_group_ids,
|
||||
db=db,
|
||||
)
|
||||
|
||||
all_tools = await Tools.get_tools(defer_content=True, db=db)
|
||||
accessible_tool_ids = await AccessGrants.get_accessible_resource_ids(
|
||||
user_id=user_id,
|
||||
resource_type='tool',
|
||||
resource_ids=[t.id for t in all_tools],
|
||||
permission='read',
|
||||
user_group_ids=user_group_ids,
|
||||
db=db,
|
||||
)
|
||||
|
||||
active_models = [m for m in all_models if m.is_active]
|
||||
|
||||
return {
|
||||
'user': {'id': target_user.id, 'name': target_user.name},
|
||||
'groups': [{'id': g.id, 'name': g.name} for g in user_groups],
|
||||
'models': {
|
||||
'items': [
|
||||
{'id': m.id, 'name': m.name}
|
||||
for m in active_models
|
||||
if m.id in accessible_model_ids
|
||||
],
|
||||
'total': len(active_models),
|
||||
},
|
||||
'knowledge': {
|
||||
'items': [
|
||||
{'id': k.id, 'name': k.name}
|
||||
for k in all_knowledge
|
||||
if k.id in accessible_knowledge_ids
|
||||
],
|
||||
'total': len(all_knowledge),
|
||||
},
|
||||
'tools': {
|
||||
'items': [
|
||||
{'id': t.id, 'name': t.name}
|
||||
for t in all_tools
|
||||
if t.id in accessible_tool_ids
|
||||
],
|
||||
'total': len(all_tools),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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