mirror of
https://github.com/open-webui/open-webui.git
synced 2025-12-15 13:55:19 +00:00
enh/refac: show read only kbs
This commit is contained in:
parent
a6ef82c5ed
commit
693636d971
4 changed files with 154 additions and 125 deletions
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue