This commit is contained in:
Timothy Jaeryang Baek 2026-10-09 17:19:10 +04:00
parent db1ca1afab
commit b3ce7de8d3
12 changed files with 199 additions and 7 deletions

View file

@ -1,4 +1,6 @@
<script lang="ts">
import WorkspaceAccessModal from './common/WorkspaceAccessModal.svelte';
let accessModal: WorkspaceAccessModal;
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
dayjs.extend(relativeTime);
@ -296,6 +298,8 @@
});
</script>
<WorkspaceAccessModal bind:this={accessModal} resourceType="knowledge" onUpdated={init} />
<svelte:head>
<!-- LICENSE covers this Open WebUI browser-title identifier.
Do not alter, remove, obscure, or replace it except as LICENSE permits:
@ -524,6 +528,7 @@
{#if item?.write_access || $user?.role === 'admin'}
<div class="ml-2 flex shrink-0 flex-row items-center self-center">
<ItemMenu
accessHandler={() => accessModal.open(item.id)}
onExport={$user?.role === 'admin'
? () => {
exportHandler(item);

View file

@ -1,4 +1,5 @@
<script lang="ts">
import LockClosed from '$lib/components/icons/LockClosed.svelte';
import { getContext, createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
@ -9,9 +10,10 @@
import Download from '$lib/components/icons/Download.svelte';
import EllipsisHorizontal from '$lib/components/icons/EllipsisHorizontal.svelte';
const i18n = getContext('i18n');
const i18n = getContext<typeof import('$lib/i18n').default>('i18n');
export let onExport: null | Function = null;
export let accessHandler: (() => void) | null = null;
export let onClose: Function = () => {};
let show = false;
@ -50,6 +52,20 @@
<div slot="content">
<DropdownMenu className="min-w-[10.625rem]">
{#if accessHandler}
<button
type="button"
class="select-none flex h-[1.6875rem] w-full cursor-pointer items-center gap-2 rounded-xl bg-transparent px-2 text-[0.8125rem] hover:text-gray-900 dark:hover:text-gray-100"
on:click={() => {
accessHandler?.();
closeMenu();
}}
>
<LockClosed className="size-3.5" />
<div class="flex items-center">{$i18n.t('Access')}</div>
</button>
{/if}
{#if onExport}
<button
class="select-none flex h-[1.6875rem] w-full cursor-pointer items-center gap-2 rounded-xl bg-transparent px-2 text-[0.8125rem] hover:text-gray-900 dark:hover:text-gray-100"

View file

@ -1,4 +1,6 @@
<script lang="ts">
import WorkspaceAccessModal from './common/WorkspaceAccessModal.svelte';
let accessModal: WorkspaceAccessModal;
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import { toast } from 'svelte-sonner';
@ -506,6 +508,8 @@
});
</script>
<WorkspaceAccessModal bind:this={accessModal} resourceType="models" onUpdated={getModelList} />
<svelte:head>
<!-- LICENSE covers this Open WebUI browser-title identifier.
Do not alter, remove, obscure, or replace it except as LICENSE permits:
@ -924,6 +928,7 @@
{:else}
<div class="flex shrink-0 flex-row items-center gap-1 self-center">
<ModelMenu
accessHandler={() => accessModal.open(model.id)}
user={$user}
{model}
writeAccess={model.write_access}

View file

@ -1,4 +1,5 @@
<script lang="ts">
import LockClosed from '$lib/components/icons/LockClosed.svelte';
import { getContext } from 'svelte';
import Dropdown from '$lib/components/common/Dropdown.svelte';
@ -33,6 +34,7 @@
export let hideHandler: Function;
export let pinModelHandler: Function;
export let deleteHandler: Function;
export let accessHandler: (() => void) | null = null;
export let onClose: Function;
export let writeAccess: boolean = true;
@ -80,6 +82,18 @@
<div class="flex items-center">{$i18n.t('Edit')}</div>
</button>
{/if}
{#if accessHandler && writeAccess}
<button
type="button"
class="select-none flex h-[1.6875rem] w-full cursor-pointer items-center gap-2 rounded-xl bg-transparent px-2 text-[0.8125rem] hover:text-gray-900 dark:hover:text-gray-100"
on:click={() => {
runAndClose(() => accessHandler?.());
}}
>
<LockClosed className="size-3.5" />
<div class="flex items-center">{$i18n.t('Access')}</div>
</button>
{/if}
{#if writeAccess}
<button

View file

@ -1,4 +1,6 @@
<script lang="ts">
import WorkspaceAccessModal from './common/WorkspaceAccessModal.svelte';
let accessModal: WorkspaceAccessModal;
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import { toast } from 'svelte-sonner';
@ -375,6 +377,8 @@
});
</script>
<WorkspaceAccessModal bind:this={accessModal} resourceType="prompts" onUpdated={getPromptList} />
<svelte:head>
<!-- LICENSE covers this Open WebUI browser-title identifier.
Do not alter, remove, obscure, or replace it except as LICENSE permits:
@ -688,6 +692,7 @@
<div class="ml-0.5 flex shrink-0 flex-row items-center gap-1.5 self-center">
<PromptMenu
accessHandler={() => accessModal.open(prompt.id)}
writeAccess={prompt.write_access}
show={openPromptMenuId === prompt.id}
editHandler={() => {

View file

@ -1,4 +1,5 @@
<script lang="ts">
import LockClosed from '$lib/components/icons/LockClosed.svelte';
import { getContext } from 'svelte';
import { config, user } from '$lib/stores';
@ -10,7 +11,7 @@
import DocumentDuplicate from '$lib/components/icons/DocumentDuplicate.svelte';
import Download from '$lib/components/icons/Download.svelte';
const i18n = getContext('i18n');
const i18n = getContext<typeof import('$lib/i18n').default>('i18n');
export let writeAccess = true;
@ -19,6 +20,7 @@
export let cloneHandler: Function;
export let exportHandler: Function;
export let deleteHandler: Function;
export let accessHandler: (() => void) | null = null;
export let onClose: Function;
export let show = false;
@ -69,6 +71,19 @@
<div class="flex items-center">{$i18n.t('Edit')}</div>
</button>
{#if accessHandler && writeAccess}
<button
type="button"
class="select-none flex h-[1.6875rem] w-full cursor-pointer items-center gap-2 rounded-xl bg-transparent px-2 text-[0.8125rem] hover:text-gray-900 dark:hover:text-gray-100"
on:click={() => {
accessHandler?.();
closeMenu();
}}
>
<LockClosed className="size-3.5" />
<div class="flex items-center">{$i18n.t('Access')}</div>
</button>
{/if}
{#if $config.features.enable_community_sharing}
<button

View file

@ -1,4 +1,6 @@
<script lang="ts">
import WorkspaceAccessModal from './common/WorkspaceAccessModal.svelte';
let accessModal: WorkspaceAccessModal;
import { resolveLocalizedResource } from '$lib/utils/localizedContent';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
@ -282,6 +284,8 @@
});
</script>
<WorkspaceAccessModal bind:this={accessModal} resourceType="skills" onUpdated={loadSkillItems} />
<svelte:head>
<!-- LICENSE covers this Open WebUI browser-title identifier.
Do not alter, remove, obscure, or replace it except as LICENSE permits:
@ -535,6 +539,7 @@
{:else}
<div class="flex shrink-0 flex-row items-center gap-1.5 self-center">
<SkillMenu
accessHandler={() => accessModal.open(skill.id)}
show={openSkillMenuId === skill.id}
editHandler={() => {
goto(`/workspace/skills/edit?id=${encodeURIComponent(skill.id)}`);

View file

@ -1,4 +1,5 @@
<script lang="ts">
import LockClosed from '$lib/components/icons/LockClosed.svelte';
import { getContext } from 'svelte';
import Dropdown from '$lib/components/common/Dropdown.svelte';
@ -9,12 +10,13 @@
import Download from '$lib/components/icons/Download.svelte';
import { user } from '$lib/stores';
const i18n = getContext('i18n');
const i18n = getContext<typeof import('$lib/i18n').default>('i18n');
export let editHandler: Function;
export let cloneHandler: Function;
export let exportHandler: Function;
export let deleteHandler: Function;
export let accessHandler: (() => void) | null = null;
export let onClose: Function;
export let show = false;
@ -63,6 +65,19 @@
<div class="flex items-center">{$i18n.t('Edit')}</div>
</button>
{#if accessHandler}
<button
type="button"
class="select-none flex h-[1.6875rem] w-full cursor-pointer items-center gap-2 rounded-xl bg-transparent px-2 text-[0.8125rem] hover:text-gray-900 dark:hover:text-gray-100"
on:click={() => {
accessHandler?.();
closeMenu();
}}
>
<LockClosed className="size-3.5" />
<div class="flex items-center">{$i18n.t('Access')}</div>
</button>
{/if}
<button
class="select-none flex h-[1.6875rem] w-full cursor-pointer items-center gap-2 rounded-xl bg-transparent px-2 text-[0.8125rem] hover:text-gray-900 dark:hover:text-gray-100"

View file

@ -1,4 +1,6 @@
<script lang="ts">
import WorkspaceAccessModal from './common/WorkspaceAccessModal.svelte';
let accessModal: WorkspaceAccessModal;
import { resolveLocalizedResource } from '$lib/utils/localizedContent';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
@ -294,6 +296,8 @@
});
</script>
<WorkspaceAccessModal bind:this={accessModal} resourceType="tools" onUpdated={init} />
<svelte:head>
<!-- LICENSE covers this Open WebUI browser-title identifier.
Do not alter, remove, obscure, or replace it except as LICENSE permits:
@ -587,6 +591,7 @@
</Tooltip>
<ToolMenu
accessHandler={() => accessModal.open(tool.id)}
show={openToolMenuId === tool.id}
editHandler={() => {
goto(`/workspace/tools/edit?id=${encodeURIComponent(tool.id)}`);

View file

@ -1,4 +1,5 @@
<script lang="ts">
import LockClosed from '$lib/components/icons/LockClosed.svelte';
import { getContext } from 'svelte';
import Dropdown from '$lib/components/common/Dropdown.svelte';
@ -11,13 +12,14 @@
import Download from '$lib/components/icons/Download.svelte';
import { config, user } from '$lib/stores';
const i18n = getContext('i18n');
const i18n = getContext<typeof import('$lib/i18n').default>('i18n');
export let editHandler: Function;
export let shareHandler: Function;
export let cloneHandler: Function;
export let exportHandler: Function;
export let deleteHandler: Function;
export let accessHandler: (() => void) | null = null;
export let onClose: Function;
export let show = false;
@ -67,6 +69,19 @@
<div class="flex items-center">{$i18n.t('Edit')}</div>
</button>
{#if accessHandler}
<button
type="button"
class="select-none flex h-[1.6875rem] w-full cursor-pointer items-center gap-2 rounded-xl bg-transparent px-2 text-[0.8125rem] hover:text-gray-900 dark:hover:text-gray-100"
on:click={() => {
accessHandler?.();
closeMenu();
}}
>
<LockClosed className="size-3.5" />
<div class="flex items-center">{$i18n.t('Access')}</div>
</button>
{/if}
{#if $config.features.enable_community_sharing}
<button

View file

@ -1,6 +1,6 @@
<script lang="ts">
import { getContext } from 'svelte';
const i18n = getContext('i18n');
const i18n = getContext<typeof import('$lib/i18n').default>('i18n');
import Modal from '$lib/components/common/Modal.svelte';
import AccessControl from './AccessControl.svelte';
@ -14,6 +14,7 @@
};
export let show = false;
export let disabled = false;
export let accessGrants: AccessGrant[] = [];
export let accessControl: any = undefined;
export let accessRoles = ['read'];
@ -24,7 +25,7 @@
export let shareUsers = true;
export let allowGroups = true;
export let onChange = () => {};
export let onChange: (grants: AccessGrant[]) => void | Promise<void> = () => {};
</script>
<Modal size="sm" bind:show>
@ -43,7 +44,7 @@
</button>
</div>
<div class="w-full px-4 pb-3 dark:text-white">
<div class="w-full px-4 pb-3 dark:text-white" inert={disabled} aria-busy={disabled}>
<AccessControl
bind:accessGrants
bind:accessControl

View file

@ -0,0 +1,91 @@
<script lang="ts">
import { getContext } from 'svelte';
import { toast } from 'svelte-sonner';
import { user } from '$lib/stores';
import { getModelById, updateModelAccessGrants } from '$lib/apis/models';
import { getKnowledgeById, updateKnowledgeAccessGrants } from '$lib/apis/knowledge';
import { getPromptById, updatePromptAccessGrants } from '$lib/apis/prompts';
import { getSkillById, updateSkillAccessGrants } from '$lib/apis/skills';
import { getToolById, updateToolAccessGrants } from '$lib/apis/tools';
import AccessControlModal from './AccessControlModal.svelte';
export let resourceType: 'models' | 'knowledge' | 'prompts' | 'skills' | 'tools';
export let onUpdated: () => void | Promise<void> = () => {};
const i18n = getContext<any>('i18n');
const loaders = {
models: getModelById,
knowledge: getKnowledgeById,
prompts: getPromptById,
skills: getSkillById,
tools: getToolById
};
const savers = {
knowledge: updateKnowledgeAccessGrants,
prompts: updatePromptAccessGrants,
skills: updateSkillAccessGrants,
tools: updateToolAccessGrants
};
let show = false;
let saving = false;
let resource: any = null;
let accessGrants: any[] = [];
let savedGrants: any[] = [];
let requestId = 0;
const reportError = (error: any) => toast.error(`${error?.detail ?? error}`);
export const open = async (id: string) => {
const request = ++requestId;
show = false;
try {
const result = await loaders[resourceType](localStorage.token, id);
if (request !== requestId) return;
resource = result;
accessGrants = structuredClone(result.access_grants ?? []);
savedGrants = structuredClone(accessGrants);
saving = false;
show = true;
} catch (error) {
if (request === requestId) reportError(error);
}
};
const save = async (grants: any[]) => {
if (saving || !resource) return;
const request = requestId;
saving = true;
try {
const updated =
resourceType === 'models'
? await updateModelAccessGrants(localStorage.token, resource.id, resource.name, grants)
: await savers[resourceType](localStorage.token, resource.id, grants);
if (request === requestId) {
accessGrants = structuredClone(updated.access_grants ?? []);
savedGrants = structuredClone(accessGrants);
}
toast.success($i18n.t('Saved'));
} catch (error) {
if (request === requestId) accessGrants = structuredClone(savedGrants);
reportError(error);
return;
} finally {
if (request === requestId) saving = false;
}
try {
await onUpdated();
} catch (error) {
reportError(error);
}
};
</script>
<AccessControlModal
bind:show
bind:accessGrants
disabled={saving}
accessRoles={resourceType === 'models' && !resource?.base_model_id ? ['read'] : ['read', 'write']}
share={$user?.permissions?.sharing?.[resourceType] || $user?.role === 'admin'}
sharePublic={$user?.permissions?.sharing?.[`public_${resourceType}`] || $user?.role === 'admin'}
shareUsers={($user?.permissions?.access_grants?.allow_users ?? true) || $user?.role === 'admin'}
allowGroups={($user?.permissions?.access_grants?.allow_groups ?? true) || $user?.role === 'admin'}
onChange={save}
/>