* feat: Add read-only access support for Models
- Backend: Add write_access field to ModelAccessResponse
- Backend: Update /models/list to return ModelAccessListResponse
- Frontend: Display Read Only badge in Models list
- Frontend: Disable inputs and save button when no write access
- Frontend: Hide action buttons for read-only models
* fix: Handle ModelAccessListResponse format in getModels API
- Backend returns {items, total} instead of {data}
- Update getModels API to handle both formats for backward compatibility
* fix: Show read-only shared models in workspace list
- Backend: Change search_models permission from 'write' to 'read' to include shared models
- Backend: Keep user_id filter to only show owned/shared models (not all public)
- Frontend: Handle ModelAccessListResponse format in getModels API
* fix: Align Read Only badge inline with model name
* fix: Correct badge placement and fix syntax error
* fix: Resolve badge truncation in Models list
- Add w-full to flex container for proper spacing
- Wrap Badge in div to prevent truncation
- Match Knowledge.svelte badge pattern
* fix: Align Read Only badge with Knowledge.svelte pattern
- Match Knowledge.svelte structure for badge placement
- Actions only show when write_access or admin
- Remove w-full from container to prevent right-overflow
* fix: Return write_access from getModelById endpoint
fix: Return write_access from getModelById endpoint
- Use ModelAccessResponse instead of raw dict
- Remove inefficient getModels call in edit page
* revert
* fix
* fix
* fix
This commit is contained in:
@@ -130,6 +130,10 @@ class ModelUserResponse(ModelModel):
|
|||||||
user: Optional[UserResponse] = None
|
user: Optional[UserResponse] = None
|
||||||
|
|
||||||
|
|
||||||
|
class ModelAccessResponse(ModelUserResponse):
|
||||||
|
write_access: Optional[bool] = False
|
||||||
|
|
||||||
|
|
||||||
class ModelResponse(ModelModel):
|
class ModelResponse(ModelModel):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -139,6 +143,11 @@ class ModelListResponse(BaseModel):
|
|||||||
total: int
|
total: int
|
||||||
|
|
||||||
|
|
||||||
|
class ModelAccessListResponse(BaseModel):
|
||||||
|
items: list[ModelAccessResponse]
|
||||||
|
total: int
|
||||||
|
|
||||||
|
|
||||||
class ModelForm(BaseModel):
|
class ModelForm(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
base_model_id: Optional[str] = None
|
base_model_id: Optional[str] = None
|
||||||
@@ -292,7 +301,7 @@ class ModelsTable:
|
|||||||
db,
|
db,
|
||||||
query,
|
query,
|
||||||
filter,
|
filter,
|
||||||
permission="write",
|
permission="read",
|
||||||
)
|
)
|
||||||
|
|
||||||
tag = filter.get("tag")
|
tag = filter.get("tag")
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ from open_webui.models.models import (
|
|||||||
ModelModel,
|
ModelModel,
|
||||||
ModelResponse,
|
ModelResponse,
|
||||||
ModelListResponse,
|
ModelListResponse,
|
||||||
|
ModelAccessListResponse,
|
||||||
|
ModelAccessResponse,
|
||||||
Models,
|
Models,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -51,7 +53,7 @@ PAGE_ITEM_COUNT = 30
|
|||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/list", response_model=ModelListResponse
|
"/list", response_model=ModelAccessListResponse
|
||||||
) # do NOT use "/" as path, conflicts with main.py
|
) # do NOT use "/" as path, conflicts with main.py
|
||||||
async def get_models(
|
async def get_models(
|
||||||
query: Optional[str] = None,
|
query: Optional[str] = None,
|
||||||
@@ -88,7 +90,21 @@ async def get_models(
|
|||||||
|
|
||||||
filter["user_id"] = user.id
|
filter["user_id"] = user.id
|
||||||
|
|
||||||
return Models.search_models(user.id, filter=filter, skip=skip, limit=limit, db=db)
|
result = Models.search_models(user.id, filter=filter, skip=skip, limit=limit, db=db)
|
||||||
|
return ModelAccessListResponse(
|
||||||
|
items=[
|
||||||
|
ModelAccessResponse(
|
||||||
|
**model.model_dump(),
|
||||||
|
write_access=(
|
||||||
|
(user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL)
|
||||||
|
or user.id == model.user_id
|
||||||
|
or has_access(user.id, "write", model.access_control, db=db)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
for model in result.items
|
||||||
|
],
|
||||||
|
total=result.total,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
###########################
|
###########################
|
||||||
@@ -290,7 +306,7 @@ class ModelIdForm(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
# Note: We're not using the typical url path param here, but instead using a query parameter to allow '/' in the id
|
# Note: We're not using the typical url path param here, but instead using a query parameter to allow '/' in the id
|
||||||
@router.get("/model", response_model=Optional[ModelResponse])
|
@router.get("/model", response_model=Optional[ModelAccessResponse])
|
||||||
async def get_model_by_id(
|
async def get_model_by_id(
|
||||||
id: str, user=Depends(get_verified_user), db: Session = Depends(get_session)
|
id: str, user=Depends(get_verified_user), db: Session = Depends(get_session)
|
||||||
):
|
):
|
||||||
@@ -301,7 +317,14 @@ async def get_model_by_id(
|
|||||||
or model.user_id == user.id
|
or model.user_id == user.id
|
||||||
or has_access(user.id, "read", model.access_control, db=db)
|
or has_access(user.id, "read", model.access_control, db=db)
|
||||||
):
|
):
|
||||||
return model
|
return ModelAccessResponse(
|
||||||
|
**model.model_dump(),
|
||||||
|
write_access=(
|
||||||
|
(user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL)
|
||||||
|
or user.id == model.user_id
|
||||||
|
or has_access(user.id, "write", model.access_control, db=db)
|
||||||
|
),
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
|||||||
@@ -44,6 +44,7 @@
|
|||||||
import ViewSelector from './common/ViewSelector.svelte';
|
import ViewSelector from './common/ViewSelector.svelte';
|
||||||
import TagSelector from './common/TagSelector.svelte';
|
import TagSelector from './common/TagSelector.svelte';
|
||||||
import Pagination from '../common/Pagination.svelte';
|
import Pagination from '../common/Pagination.svelte';
|
||||||
|
import Badge from '$lib/components/common/Badge.svelte';
|
||||||
|
|
||||||
let shiftKey = false;
|
let shiftKey = false;
|
||||||
|
|
||||||
@@ -457,14 +458,10 @@
|
|||||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||||
<div
|
<div
|
||||||
class=" flex cursor-pointer dark:hover:bg-gray-850/50 hover:bg-gray-50 transition rounded-2xl w-full p-2.5"
|
class="flex transition rounded-2xl w-full p-2.5 {model.write_access ? 'cursor-pointer dark:hover:bg-gray-850/50 hover:bg-gray-50' : 'cursor-not-allowed opacity-60'}"
|
||||||
id="model-item-{model.id}"
|
id="model-item-{model.id}"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
if (
|
if (model.write_access) {
|
||||||
$user?.role === 'admin' ||
|
|
||||||
model.user_id === $user?.id ||
|
|
||||||
model.access_control.write.group_ids.some((wg) => groupIds.includes(wg))
|
|
||||||
) {
|
|
||||||
goto(`/workspace/models/edit?id=${encodeURIComponent(model.id)}`);
|
goto(`/workspace/models/edit?id=${encodeURIComponent(model.id)}`);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@@ -499,91 +496,95 @@
|
|||||||
</a>
|
</a>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<div class=" flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<div
|
{#if !model.write_access}
|
||||||
class="flex justify-end w-full {model.is_active ? '' : 'text-gray-500'}"
|
<div>
|
||||||
>
|
<Badge type="muted" content={$i18n.t('Read Only')} />
|
||||||
<div class="flex justify-between items-center w-full">
|
</div>
|
||||||
<div class=""></div>
|
{/if}
|
||||||
<div class="flex flex-row gap-0.5 items-center">
|
|
||||||
{#if shiftKey}
|
|
||||||
<Tooltip
|
|
||||||
content={model?.meta?.hidden
|
|
||||||
? $i18n.t('Show')
|
|
||||||
: $i18n.t('Hide')}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
class="self-center w-fit text-sm p-1.5 dark:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
|
|
||||||
type="button"
|
|
||||||
on:click={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
hideModelHandler(model);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{#if model?.meta?.hidden}
|
|
||||||
<EyeSlash />
|
|
||||||
{:else}
|
|
||||||
<Eye />
|
|
||||||
{/if}
|
|
||||||
</button>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<Tooltip content={$i18n.t('Delete')}>
|
{#if model.write_access || $user?.role === 'admin'}
|
||||||
<button
|
<div class="flex {model.is_active ? '' : 'text-gray-500'}">
|
||||||
class="self-center w-fit text-sm p-1.5 dark:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
|
<div class="flex items-center gap-0.5">
|
||||||
type="button"
|
{#if shiftKey}
|
||||||
on:click={(e) => {
|
<Tooltip
|
||||||
e.stopPropagation();
|
content={model?.meta?.hidden
|
||||||
deleteModelHandler(model);
|
? $i18n.t('Show')
|
||||||
}}
|
: $i18n.t('Hide')}
|
||||||
>
|
>
|
||||||
<GarbageBin />
|
<button
|
||||||
</button>
|
class="self-center w-fit text-sm p-1.5 dark:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
|
||||||
</Tooltip>
|
type="button"
|
||||||
{:else}
|
on:click={(e) => {
|
||||||
<ModelMenu
|
e.stopPropagation();
|
||||||
user={$user}
|
|
||||||
{model}
|
|
||||||
editHandler={() => {
|
|
||||||
goto(
|
|
||||||
`/workspace/models/edit?id=${encodeURIComponent(model.id)}`
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
shareHandler={() => {
|
|
||||||
shareModelHandler(model);
|
|
||||||
}}
|
|
||||||
cloneHandler={() => {
|
|
||||||
cloneModelHandler(model);
|
|
||||||
}}
|
|
||||||
exportHandler={() => {
|
|
||||||
exportModelHandler(model);
|
|
||||||
}}
|
|
||||||
hideHandler={() => {
|
|
||||||
hideModelHandler(model);
|
hideModelHandler(model);
|
||||||
}}
|
}}
|
||||||
pinModelHandler={() => {
|
|
||||||
pinModelHandler(model.id);
|
|
||||||
}}
|
|
||||||
copyLinkHandler={() => {
|
|
||||||
copyLinkHandler(model);
|
|
||||||
}}
|
|
||||||
deleteHandler={() => {
|
|
||||||
selectedModel = model;
|
|
||||||
showModelDeleteConfirm = true;
|
|
||||||
}}
|
|
||||||
onClose={() => {}}
|
|
||||||
>
|
>
|
||||||
<div
|
{#if model?.meta?.hidden}
|
||||||
class="self-center w-fit p-1 text-sm dark:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
|
<EyeSlash />
|
||||||
>
|
{:else}
|
||||||
<EllipsisHorizontal className="size-5" />
|
<Eye />
|
||||||
</div>
|
{/if}
|
||||||
</ModelMenu>
|
</button>
|
||||||
{/if}
|
</Tooltip>
|
||||||
</div>
|
|
||||||
|
<Tooltip content={$i18n.t('Delete')}>
|
||||||
|
<button
|
||||||
|
class="self-center w-fit text-sm p-1.5 dark:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
|
||||||
|
type="button"
|
||||||
|
on:click={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
deleteModelHandler(model);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<GarbageBin />
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
{:else}
|
||||||
|
<ModelMenu
|
||||||
|
user={$user}
|
||||||
|
{model}
|
||||||
|
editHandler={() => {
|
||||||
|
goto(
|
||||||
|
`/workspace/models/edit?id=${encodeURIComponent(model.id)}`
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
shareHandler={() => {
|
||||||
|
shareModelHandler(model);
|
||||||
|
}}
|
||||||
|
cloneHandler={() => {
|
||||||
|
cloneModelHandler(model);
|
||||||
|
}}
|
||||||
|
exportHandler={() => {
|
||||||
|
exportModelHandler(model);
|
||||||
|
}}
|
||||||
|
hideHandler={() => {
|
||||||
|
hideModelHandler(model);
|
||||||
|
}}
|
||||||
|
pinModelHandler={() => {
|
||||||
|
pinModelHandler(model.id);
|
||||||
|
}}
|
||||||
|
copyLinkHandler={() => {
|
||||||
|
copyLinkHandler(model);
|
||||||
|
}}
|
||||||
|
deleteHandler={() => {
|
||||||
|
selectedModel = model;
|
||||||
|
showModelDeleteConfirm = true;
|
||||||
|
}}
|
||||||
|
onClose={() => {}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="self-center w-fit p-1 text-sm dark:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
|
||||||
|
>
|
||||||
|
<EllipsisHorizontal className="size-5" />
|
||||||
|
</div>
|
||||||
|
</ModelMenu>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if model.write_access}
|
||||||
<button
|
<button
|
||||||
on:click={(e) => {
|
on:click={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -607,6 +608,7 @@
|
|||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</button>
|
</button>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user