enh/refac: show read only kbs

This commit is contained in:
Timothy Jaeryang Baek 2025-12-10 16:58:53 -05:00
parent a6ef82c5ed
commit 693636d971
4 changed files with 154 additions and 125 deletions

View file

@ -41,7 +41,11 @@ router = APIRouter()
############################ ############################
@router.get("/", response_model=list[KnowledgeUserResponse]) class KnowledgeAccessResponse(KnowledgeUserResponse):
write_access: Optional[bool] = False
@router.get("/", response_model=list[KnowledgeAccessResponse])
async def get_knowledge(user=Depends(get_verified_user)): async def get_knowledge(user=Depends(get_verified_user)):
# Return knowledge bases with read access # Return knowledge bases with read access
knowledge_bases = [] knowledge_bases = []
@ -51,27 +55,29 @@ async def get_knowledge(user=Depends(get_verified_user)):
knowledge_bases = Knowledges.get_knowledge_bases_by_user_id(user.id, "read") knowledge_bases = Knowledges.get_knowledge_bases_by_user_id(user.id, "read")
return [ return [
KnowledgeUserResponse( KnowledgeAccessResponse(
**knowledge_base.model_dump(), **knowledge_base.model_dump(),
files=Knowledges.get_file_metadatas_by_id(knowledge_base.id), files=Knowledges.get_file_metadatas_by_id(knowledge_base.id),
write_access=has_access(user.id, "write", knowledge_base.access_control),
) )
for knowledge_base in knowledge_bases for knowledge_base in knowledge_bases
] ]
@router.get("/list", response_model=list[KnowledgeUserResponse]) @router.get("/list", response_model=list[KnowledgeAccessResponse])
async def get_knowledge_list(user=Depends(get_verified_user)): async def get_knowledge_list(user=Depends(get_verified_user)):
# Return knowledge bases with write access # Return knowledge bases with write access
knowledge_bases = [] knowledge_bases = []
if user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL: if user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL:
knowledge_bases = Knowledges.get_knowledge_bases() knowledge_bases = Knowledges.get_knowledge_bases()
else: else:
knowledge_bases = Knowledges.get_knowledge_bases_by_user_id(user.id, "write") knowledge_bases = Knowledges.get_knowledge_bases_by_user_id(user.id, "read")
return [ return [
KnowledgeUserResponse( KnowledgeAccessResponse(
**knowledge_base.model_dump(), **knowledge_base.model_dump(),
files=Knowledges.get_file_metadatas_by_id(knowledge_base.id), files=Knowledges.get_file_metadatas_by_id(knowledge_base.id),
write_access=has_access(user.id, "write", knowledge_base.access_control),
) )
for knowledge_base in knowledge_bases for knowledge_base in knowledge_bases
] ]
@ -187,6 +193,7 @@ async def reindex_knowledge_files(request: Request, user=Depends(get_verified_us
class KnowledgeFilesResponse(KnowledgeResponse): class KnowledgeFilesResponse(KnowledgeResponse):
files: list[FileMetadataResponse] files: list[FileMetadataResponse]
write_access: Optional[bool] = False
@router.get("/{id}", response_model=Optional[KnowledgeFilesResponse]) @router.get("/{id}", response_model=Optional[KnowledgeFilesResponse])
@ -203,6 +210,7 @@ async def get_knowledge_by_id(id: str, user=Depends(get_verified_user)):
return KnowledgeFilesResponse( return KnowledgeFilesResponse(
**knowledge.model_dump(), **knowledge.model_dump(),
files=Knowledges.get_file_metadatas_by_id(knowledge.id), files=Knowledges.get_file_metadatas_by_id(knowledge.id),
write_access=has_access(user.id, "write", knowledge.access_control),
) )
else: else:
raise HTTPException( raise HTTPException(

View file

@ -196,76 +196,80 @@
<!-- The Aleph dreams itself into being, and the void learns its own name --> <!-- The Aleph dreams itself into being, and the void learns its own name -->
<div class=" my-2 px-3 grid grid-cols-1 lg:grid-cols-2 gap-2"> <div class=" my-2 px-3 grid grid-cols-1 lg:grid-cols-2 gap-2">
{#each filteredItems as item} {#each filteredItems as item}
<Tooltip content={item?.description ?? item.name}> <button
<button class=" flex space-x-4 cursor-pointer text-left w-full px-3 py-2.5 dark:hover:bg-gray-850/50 hover:bg-gray-50 transition rounded-2xl"
class=" flex space-x-4 cursor-pointer text-left w-full px-3 py-2.5 dark:hover:bg-gray-850/50 hover:bg-gray-50 transition rounded-2xl" on:click={() => {
on:click={() => { if (item?.meta?.document) {
if (item?.meta?.document) { toast.error(
toast.error( $i18n.t(
$i18n.t( 'Only collections can be edited, create a new knowledge base to edit/add documents.'
'Only collections can be edited, create a new knowledge base to edit/add documents.' )
) );
); } else {
} else { goto(`/workspace/knowledge/${item.id}`);
goto(`/workspace/knowledge/${item.id}`); }
} }}
}} >
> <div class=" w-full">
<div class=" w-full"> <div class=" self-center flex-1">
<div class=" self-center flex-1"> <div class="flex items-center justify-between -my-1">
<div class="flex items-center justify-between -my-1"> <div class=" flex gap-2 items-center justify-between w-full">
<div class=" flex gap-2 items-center"> <div>
<div> <Badge type="success" content={$i18n.t('Collection')} />
{#if item?.meta?.document} </div>
<Badge type="muted" content={$i18n.t('Document')} />
{:else}
<Badge type="success" content={$i18n.t('Collection')} />
{/if}
</div>
{#if !item?.write_access}
<div>
<Badge type="muted" content={$i18n.t('Read Only')} />
</div>
{/if}
</div>
<div class="flex items-center gap-2">
<div class=" flex self-center">
<ItemMenu
on:delete={() => {
selectedItem = item;
showDeleteConfirm = true;
}}
/>
</div>
</div>
</div>
<div class=" flex items-center gap-1 justify-between px-1.5">
<Tooltip content={item?.description ?? item.name}>
<div class=" flex items-center gap-2">
<div class=" text-sm font-medium line-clamp-1 capitalize">{item.name}</div>
</div>
</Tooltip>
<div class="flex items-center gap-2">
<Tooltip content={dayjs(item.updated_at * 1000).format('LLLL')}>
<div class=" text-xs text-gray-500 line-clamp-1"> <div class=" text-xs text-gray-500 line-clamp-1">
{$i18n.t('Updated')} {$i18n.t('Updated')}
{dayjs(item.updated_at * 1000).fromNow()} {dayjs(item.updated_at * 1000).fromNow()}
</div> </div>
</div> </Tooltip>
<div class="flex items-center gap-2"> <div class="text-xs text-gray-500">
<div class=" flex self-center"> <Tooltip
<ItemMenu content={item?.user?.email ?? $i18n.t('Deleted User')}
on:delete={() => { className="flex shrink-0"
selectedItem = item; placement="top-start"
showDeleteConfirm = true; >
}} {$i18n.t('By {{name}}', {
/> name: capitalizeFirstLetter(
</div> item?.user?.name ?? item?.user?.email ?? $i18n.t('Deleted User')
</div> )
</div> })}
</Tooltip>
<div class=" flex items-center gap-1 justify-between px-1.5">
<div class=" flex items-center gap-2">
<div class=" text-sm font-medium line-clamp-1 capitalize">{item.name}</div>
</div>
<div>
<div class="text-xs text-gray-500">
<Tooltip
content={item?.user?.email ?? $i18n.t('Deleted User')}
className="flex shrink-0"
placement="top-start"
>
{$i18n.t('By {{name}}', {
name: capitalizeFirstLetter(
item?.user?.name ?? item?.user?.email ?? $i18n.t('Deleted User')
)
})}
</Tooltip>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</button> </div>
</Tooltip> </button>
{/each} {/each}
</div> </div>
{:else} {:else}

View file

@ -429,7 +429,7 @@
}); });
if (res) { if (res) {
knowledge = res; fileItems = [];
toast.success($i18n.t('Knowledge reset successfully.')); toast.success($i18n.t('Knowledge reset successfully.'));
// Upload directory // Upload directory
@ -747,6 +747,7 @@
class="text-left w-full font-medium text-lg font-primary bg-transparent outline-hidden flex-1" class="text-left w-full font-medium text-lg font-primary bg-transparent outline-hidden flex-1"
bind:value={knowledge.name} bind:value={knowledge.name}
placeholder={$i18n.t('Knowledge Name')} placeholder={$i18n.t('Knowledge Name')}
disabled={!knowledge?.write_access}
on:input={() => { on:input={() => {
changeDebounceHandler(); changeDebounceHandler();
}} }}
@ -763,21 +764,27 @@
</div> </div>
</div> </div>
<div class="self-center shrink-0"> {#if knowledge?.write_access}
<button <div class="self-center shrink-0">
class="bg-gray-50 hover:bg-gray-100 text-black dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-white transition px-2 py-1 rounded-full flex gap-1 items-center" <button
type="button" class="bg-gray-50 hover:bg-gray-100 text-black dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-white transition px-2 py-1 rounded-full flex gap-1 items-center"
on:click={() => { type="button"
showAccessControlModal = true; on:click={() => {
}} showAccessControlModal = true;
> }}
<LockClosed strokeWidth="2.5" className="size-3.5" /> >
<LockClosed strokeWidth="2.5" className="size-3.5" />
<div class="text-sm font-medium shrink-0"> <div class="text-sm font-medium shrink-0">
{$i18n.t('Access')} {$i18n.t('Access')}
</div> </div>
</button> </button>
</div> </div>
{:else}
<div class="text-xs shrink-0 text-gray-500">
{$i18n.t('Read Only')}
</div>
{/if}
</div> </div>
<div class="flex w-full"> <div class="flex w-full">
@ -786,6 +793,7 @@
class="text-left text-xs w-full text-gray-500 bg-transparent outline-hidden" class="text-left text-xs w-full text-gray-500 bg-transparent outline-hidden"
bind:value={knowledge.description} bind:value={knowledge.description}
placeholder={$i18n.t('Knowledge Description')} placeholder={$i18n.t('Knowledge Description')}
disabled={!knowledge?.write_access}
on:input={() => { on:input={() => {
changeDebounceHandler(); changeDebounceHandler();
}} }}
@ -812,22 +820,24 @@
}} }}
/> />
<div> {#if knowledge?.write_access}
<AddContentMenu <div>
on:upload={(e) => { <AddContentMenu
if (e.detail.type === 'directory') { on:upload={(e) => {
uploadDirectoryHandler(); if (e.detail.type === 'directory') {
} else if (e.detail.type === 'text') { uploadDirectoryHandler();
showAddTextContentModal = true; } else if (e.detail.type === 'text') {
} else { showAddTextContentModal = true;
document.getElementById('files-input').click(); } else {
} document.getElementById('files-input').click();
}} }
on:sync={(e) => { }}
showSyncConfirmModal = true; on:sync={(e) => {
}} showSyncConfirmModal = true;
/> }}
</div> />
</div>
{/if}
</div> </div>
</div> </div>
@ -896,6 +906,7 @@
<div class=" flex overflow-y-auto h-full w-full scrollbar-hidden text-xs"> <div class=" flex overflow-y-auto h-full w-full scrollbar-hidden text-xs">
<Files <Files
files={fileItems} files={fileItems}
{knowledge}
{selectedFileId} {selectedFileId}
onClick={(fileId) => { onClick={(fileId) => {
selectedFileId = fileId; selectedFileId = fileId;
@ -959,28 +970,31 @@
{selectedFile?.meta?.name} {selectedFile?.meta?.name}
</div> </div>
<div> {#if knowledge?.write_access}
<button <div>
class="flex self-center w-fit text-sm py-1 px-2.5 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed" <button
disabled={isSaving} class="flex self-center w-fit text-sm py-1 px-2.5 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed"
on:click={() => { disabled={isSaving}
updateFileContentHandler(); on:click={() => {
}} updateFileContentHandler();
> }}
{$i18n.t('Save')} >
{#if isSaving} {$i18n.t('Save')}
<div class="ml-2 self-center"> {#if isSaving}
<Spinner /> <div class="ml-2 self-center">
</div> <Spinner />
{/if} </div>
</button> {/if}
</div> </button>
</div>
{/if}
</div> </div>
{#key selectedFile.id} {#key selectedFile.id}
<textarea <textarea
class="w-full h-full text-sm outline-none resize-none px-3 py-2" class="w-full h-full text-sm outline-none resize-none px-3 py-2"
bind:value={selectedFileContent} bind:value={selectedFileContent}
disabled={!knowledge?.write_access}
placeholder={$i18n.t('Add content here')} placeholder={$i18n.t('Add content here')}
/> />
{/key} {/key}

View file

@ -16,6 +16,7 @@
import XMark from '$lib/components/icons/XMark.svelte'; import XMark from '$lib/components/icons/XMark.svelte';
import Spinner from '$lib/components/common/Spinner.svelte'; import Spinner from '$lib/components/common/Spinner.svelte';
export let knowledge = null;
export let selectedFileId = null; export let selectedFileId = null;
export let files = []; export let files = [];
@ -79,19 +80,21 @@
</div> </div>
</button> </button>
<div class="flex items-center"> {#if knowledge?.write_access}
<Tooltip content={$i18n.t('Delete')}> <div class="flex items-center">
<button <Tooltip content={$i18n.t('Delete')}>
class="p-1 rounded-full hover:bg-gray-100 dark:hover:bg-gray-850 transition" <button
type="button" class="p-1 rounded-full hover:bg-gray-100 dark:hover:bg-gray-850 transition"
on:click={() => { type="button"
onDelete(file?.id ?? file?.tempId); on:click={() => {
}} onDelete(file?.id ?? file?.tempId);
> }}
<XMark /> >
</button> <XMark />
</Tooltip> </button>
</div> </Tooltip>
</div>
{/if}
</div> </div>
{/each} {/each}
</div> </div>