refac
This commit is contained in:
@@ -3,10 +3,11 @@ import { WEBUI_API_BASE_URL } from '$lib/constants';
|
||||
type ChannelForm = {
|
||||
type?: string;
|
||||
name: string;
|
||||
is_private?: boolean;
|
||||
is_private?: boolean | null;
|
||||
data?: object;
|
||||
meta?: object;
|
||||
access_control?: object;
|
||||
access_grants?: object[];
|
||||
group_ids?: string[];
|
||||
user_ids?: string[];
|
||||
};
|
||||
|
||||
|
||||
@@ -99,6 +99,38 @@ export const getGroupById = async (token: string, id: string) => {
|
||||
return res;
|
||||
};
|
||||
|
||||
export const getGroupInfoById = async (token: string, id: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/groups/id/${id}/info`, {
|
||||
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();
|
||||
})
|
||||
.then((json) => {
|
||||
return json;
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err.detail;
|
||||
|
||||
console.error(err);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const updateGroupById = async (token: string, id: string, group: object) => {
|
||||
let error = null;
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ export const createNewKnowledge = async (
|
||||
token: string,
|
||||
name: string,
|
||||
description: string,
|
||||
accessControl: null | object
|
||||
accessGrants: object[]
|
||||
) => {
|
||||
let error = null;
|
||||
|
||||
@@ -18,7 +18,7 @@ export const createNewKnowledge = async (
|
||||
body: JSON.stringify({
|
||||
name: name,
|
||||
description: description,
|
||||
access_control: accessControl
|
||||
access_grants: accessGrants
|
||||
})
|
||||
})
|
||||
.then(async (res) => {
|
||||
@@ -248,7 +248,7 @@ type KnowledgeUpdateForm = {
|
||||
name?: string;
|
||||
description?: string;
|
||||
data?: object;
|
||||
access_control?: null | object;
|
||||
access_grants?: object[];
|
||||
};
|
||||
|
||||
export const updateKnowledgeById = async (token: string, id: string, form: KnowledgeUpdateForm) => {
|
||||
@@ -265,7 +265,7 @@ export const updateKnowledgeById = async (token: string, id: string, form: Knowl
|
||||
name: form?.name ? form.name : undefined,
|
||||
description: form?.description ? form.description : undefined,
|
||||
data: form?.data ? form.data : undefined,
|
||||
access_control: form.access_control
|
||||
access_grants: form.access_grants
|
||||
})
|
||||
})
|
||||
.then(async (res) => {
|
||||
|
||||
@@ -5,7 +5,7 @@ type NoteItem = {
|
||||
title: string;
|
||||
data: object;
|
||||
meta?: null | object;
|
||||
access_control?: null | object;
|
||||
access_grants?: object[];
|
||||
};
|
||||
|
||||
export const createNewNote = async (token: string, note: NoteItem) => {
|
||||
|
||||
@@ -7,7 +7,7 @@ type PromptItem = {
|
||||
content: string;
|
||||
data?: object | null;
|
||||
meta?: object | null;
|
||||
access_control?: null | object;
|
||||
access_grants?: object[];
|
||||
version_id?: string | null; // Active version
|
||||
commit_message?: string | null; // For history tracking
|
||||
is_production?: boolean; // Whether to set new version as production
|
||||
@@ -23,7 +23,7 @@ type PromptHistoryItem = {
|
||||
command: string;
|
||||
data: object;
|
||||
meta: object;
|
||||
access_control: object | null;
|
||||
access_grants: object[];
|
||||
};
|
||||
user_id: string;
|
||||
commit_message: string | null;
|
||||
@@ -42,7 +42,7 @@ type PromptDiff = {
|
||||
to_snapshot: object;
|
||||
content_diff: string[];
|
||||
name_changed: boolean;
|
||||
access_control_changed: boolean;
|
||||
access_grants_changed: boolean;
|
||||
};
|
||||
|
||||
export const createNewPrompt = async (token: string, prompt: PromptItem) => {
|
||||
@@ -611,4 +611,3 @@ export const getPromptDiff = async (
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
|
||||
@@ -327,6 +327,33 @@ export const getUserById = async (token: string, userId: string) => {
|
||||
return res;
|
||||
};
|
||||
|
||||
export const getUserInfoById = async (token: string, userId: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/users/${userId}/info`, {
|
||||
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;
|
||||
};
|
||||
|
||||
export const updateUserStatus = async (token: string, formData: object) => {
|
||||
let error = null;
|
||||
|
||||
|
||||
@@ -15,13 +15,32 @@
|
||||
import AddMembersModal from './ChannelInfoModal/AddMembersModal.svelte';
|
||||
|
||||
export let show = false;
|
||||
export let channel = null;
|
||||
export let channel: any = null;
|
||||
|
||||
export let onUpdate = () => {};
|
||||
|
||||
let showAddMembersModal = false;
|
||||
const submitHandler = async () => {};
|
||||
|
||||
const hasPublicReadGrant = (grants: any) =>
|
||||
Array.isArray(grants) &&
|
||||
grants.some(
|
||||
(grant) =>
|
||||
grant?.principal_type === 'user' &&
|
||||
grant?.principal_id === '*' &&
|
||||
grant?.permission === 'read'
|
||||
);
|
||||
|
||||
const isPublicChannel = (channel: any): boolean => {
|
||||
if (channel?.type === 'group') {
|
||||
if (typeof channel?.is_private === 'boolean') {
|
||||
return !channel.is_private;
|
||||
}
|
||||
return hasPublicReadGrant(channel?.access_grants);
|
||||
}
|
||||
return hasPublicReadGrant(channel?.access_grants);
|
||||
};
|
||||
|
||||
const removeMemberHandler = async (userId) => {
|
||||
const res = await removeMembersById(localStorage.token, channel.id, {
|
||||
user_ids: [userId]
|
||||
@@ -62,7 +81,7 @@
|
||||
</div>
|
||||
{:else}
|
||||
<div class=" size-4 justify-center flex items-center">
|
||||
{#if channel?.type === 'group' ? !channel?.is_private : channel?.access_control === null}
|
||||
{#if isPublicChannel(channel)}
|
||||
<Hashtag className="size-3.5" strokeWidth="2.5" />
|
||||
{:else}
|
||||
<Lock className="size-5.5" strokeWidth="2" />
|
||||
|
||||
@@ -129,6 +129,25 @@
|
||||
onDestroy(() => {
|
||||
window.removeEventListener('keydown', keydownListener);
|
||||
});
|
||||
|
||||
const hasPublicReadGrant = (grants: any) =>
|
||||
Array.isArray(grants) &&
|
||||
grants.some(
|
||||
(grant) =>
|
||||
grant?.principal_type === 'user' &&
|
||||
grant?.principal_id === '*' &&
|
||||
grant?.permission === 'read'
|
||||
);
|
||||
|
||||
const isPublicChannel = (channel: any): boolean => {
|
||||
if (channel?.type === 'group') {
|
||||
if (typeof channel?.is_private === 'boolean') {
|
||||
return !channel.is_private;
|
||||
}
|
||||
return hasPublicReadGrant(channel?.access_grants);
|
||||
}
|
||||
return hasPublicReadGrant(channel?.access_grants);
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if filteredItems.length}
|
||||
@@ -165,7 +184,7 @@
|
||||
>
|
||||
{#if item.type === 'channel'}
|
||||
<div class=" size-4 justify-center flex items-center mr-0.5">
|
||||
{#if item?.data?.access_control === null}
|
||||
{#if isPublicChannel(item?.data)}
|
||||
<Hashtag className="size-3" strokeWidth="2.5" />
|
||||
{:else}
|
||||
<Lock className="size-[15px]" strokeWidth="2" />
|
||||
|
||||
@@ -26,6 +26,25 @@
|
||||
let showChannelPinnedMessagesModal = false;
|
||||
let showChannelInfoModal = false;
|
||||
|
||||
const hasPublicReadGrant = (grants: any) =>
|
||||
Array.isArray(grants) &&
|
||||
grants.some(
|
||||
(grant) =>
|
||||
grant?.principal_type === 'user' &&
|
||||
grant?.principal_id === '*' &&
|
||||
grant?.permission === 'read'
|
||||
);
|
||||
|
||||
const isPublicChannel = (channel: any): boolean => {
|
||||
if (channel?.type === 'group') {
|
||||
if (typeof channel?.is_private === 'boolean') {
|
||||
return !channel.is_private;
|
||||
}
|
||||
return hasPublicReadGrant(channel?.access_grants);
|
||||
}
|
||||
return hasPublicReadGrant(channel?.access_grants);
|
||||
};
|
||||
|
||||
export let channel;
|
||||
|
||||
export let onPin = (messageId, pinned) => {};
|
||||
@@ -112,7 +131,7 @@
|
||||
{/if}
|
||||
{:else}
|
||||
<div class=" size-4.5 justify-center flex items-center">
|
||||
{#if channel?.type === 'group' ? !channel?.is_private : channel?.access_control === null}
|
||||
{#if isPublicChannel(channel)}
|
||||
<Hashtag className="size-3.5" strokeWidth="2.5" />
|
||||
{:else}
|
||||
<Lock className="size-5" strokeWidth="2" />
|
||||
|
||||
@@ -151,7 +151,7 @@
|
||||
return {
|
||||
...file,
|
||||
user: undefined,
|
||||
access_control: undefined
|
||||
access_grants: undefined
|
||||
};
|
||||
}),
|
||||
selectedToolIds,
|
||||
|
||||
@@ -560,7 +560,8 @@
|
||||
|
||||
<ChannelModal
|
||||
bind:show={showCreateChannel}
|
||||
onSubmit={async ({ type, name, is_private, access_control, group_ids, user_ids }) => {
|
||||
onSubmit={async (payload: any) => {
|
||||
let { type, name, is_private, access_grants, group_ids, user_ids } = payload ?? {};
|
||||
name = name?.trim();
|
||||
|
||||
if (type === 'dm') {
|
||||
@@ -579,7 +580,7 @@
|
||||
type: type,
|
||||
name: name,
|
||||
is_private: is_private,
|
||||
access_control: access_control,
|
||||
access_grants: access_grants,
|
||||
group_ids: group_ids,
|
||||
user_ids: user_ids
|
||||
}).catch((error) => {
|
||||
|
||||
@@ -25,6 +25,25 @@
|
||||
let showEditChannelModal = false;
|
||||
|
||||
let itemElement;
|
||||
|
||||
const hasPublicReadGrant = (grants: any) =>
|
||||
Array.isArray(grants) &&
|
||||
grants.some(
|
||||
(grant) =>
|
||||
grant?.principal_type === 'user' &&
|
||||
grant?.principal_id === '*' &&
|
||||
grant?.permission === 'read'
|
||||
);
|
||||
|
||||
const isPublicChannel = (channel: any): boolean => {
|
||||
if (channel?.type === 'group') {
|
||||
if (typeof channel?.is_private === 'boolean') {
|
||||
return !channel.is_private;
|
||||
}
|
||||
return hasPublicReadGrant(channel?.access_grants);
|
||||
}
|
||||
return hasPublicReadGrant(channel?.access_grants);
|
||||
};
|
||||
</script>
|
||||
|
||||
<ChannelModal
|
||||
@@ -32,11 +51,12 @@
|
||||
{channel}
|
||||
edit={true}
|
||||
{onUpdate}
|
||||
onSubmit={async ({ name, is_private, access_control, group_ids, user_ids }) => {
|
||||
onSubmit={async (payload: any) => {
|
||||
const { name, is_private, access_grants, group_ids, user_ids } = payload ?? {};
|
||||
const res = await updateChannelById(localStorage.token, channel.id, {
|
||||
name,
|
||||
is_private,
|
||||
access_control,
|
||||
access_grants,
|
||||
group_ids,
|
||||
user_ids
|
||||
}).catch((error) => {
|
||||
@@ -123,7 +143,7 @@
|
||||
{/if}
|
||||
{:else}
|
||||
<div class=" size-4 justify-center flex items-center ml-1">
|
||||
{#if channel?.type === 'group' ? !channel?.is_private : channel?.access_control === null}
|
||||
{#if isPublicChannel(channel)}
|
||||
<Hashtag className="size-3.5" strokeWidth="2.5" />
|
||||
{:else}
|
||||
<Lock className="size-[15px]" strokeWidth="2" />
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
export let onSubmit: Function = () => {};
|
||||
export let onUpdate: Function = () => {};
|
||||
|
||||
export let channel = null;
|
||||
export let channel: any = null;
|
||||
export let edit = false;
|
||||
|
||||
let channelTypes = ['group', 'dm'];
|
||||
@@ -31,7 +31,7 @@
|
||||
let name = '';
|
||||
|
||||
let isPrivate = null;
|
||||
let accessControl = {};
|
||||
let accessGrants = [];
|
||||
|
||||
let groupIds = [];
|
||||
let userIds = [];
|
||||
@@ -65,8 +65,8 @@
|
||||
await onSubmit({
|
||||
type: type,
|
||||
name: name.replace(/\s/g, '-'),
|
||||
is_private: type === 'group' ? isPrivate : null,
|
||||
access_control: type === '' ? accessControl : {},
|
||||
is_private: type === 'group' ? (isPrivate ?? true) : null,
|
||||
access_grants: type === '' ? accessGrants : [],
|
||||
group_ids: groupIds,
|
||||
user_ids: userIds
|
||||
});
|
||||
@@ -85,8 +85,12 @@
|
||||
|
||||
if (channel) {
|
||||
name = channel?.name ?? '';
|
||||
isPrivate = channel?.is_private ?? null;
|
||||
accessControl = channel.access_control;
|
||||
if (type === 'group') {
|
||||
isPrivate = typeof channel?.is_private === 'boolean' ? channel.is_private : true;
|
||||
} else {
|
||||
isPrivate = null;
|
||||
}
|
||||
accessGrants = channel?.access_grants ?? [];
|
||||
userIds = channel?.user_ids ?? [];
|
||||
}
|
||||
};
|
||||
@@ -102,8 +106,14 @@
|
||||
|
||||
const deleteHandler = async () => {
|
||||
showDeleteConfirmDialog = false;
|
||||
if (!channel?.id) {
|
||||
show = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await deleteChannelById(localStorage.token, channel.id).catch((error) => {
|
||||
const channelId = channel.id;
|
||||
|
||||
const res = await deleteChannelById(localStorage.token, channelId).catch((error) => {
|
||||
toast.error(error.message);
|
||||
});
|
||||
|
||||
@@ -111,7 +121,7 @@
|
||||
toast.success($i18n.t('Channel deleted successfully'));
|
||||
onUpdate();
|
||||
|
||||
if ($page.url.pathname === `/channels/${channel.id}`) {
|
||||
if ($page.url.pathname === `/channels/${channelId}`) {
|
||||
goto('/');
|
||||
}
|
||||
}
|
||||
@@ -122,7 +132,7 @@
|
||||
const resetHandler = () => {
|
||||
type = '';
|
||||
name = '';
|
||||
accessControl = {};
|
||||
accessGrants = [];
|
||||
userIds = [];
|
||||
loading = false;
|
||||
};
|
||||
@@ -226,11 +236,11 @@
|
||||
{#if type !== 'dm'}
|
||||
<div class="-mx-2 mb-1 mt-2.5 px-2">
|
||||
{#if type === ''}
|
||||
<AccessControl bind:accessControl accessRoles={['read', 'write']} />
|
||||
<AccessControl bind:accessGrants accessRoles={['read', 'write']} />
|
||||
{:else if type === 'group'}
|
||||
<Visibility
|
||||
state={isPrivate ? 'private' : 'public'}
|
||||
onChange={(value) => {
|
||||
onChange={(value: string) => {
|
||||
if (value === 'private') {
|
||||
isPrivate = true;
|
||||
} else {
|
||||
|
||||
@@ -108,9 +108,18 @@
|
||||
},
|
||||
// pages: [], // TODO: Implement pages for notes to allow users to create multiple pages in a note
|
||||
meta: null,
|
||||
access_control: {}
|
||||
access_grants: []
|
||||
};
|
||||
|
||||
const hasPublicReadGrant = (grants) =>
|
||||
Array.isArray(grants) &&
|
||||
grants.some(
|
||||
(grant) =>
|
||||
grant?.principal_type === 'user' &&
|
||||
grant?.principal_id === '*' &&
|
||||
grant?.permission === 'read'
|
||||
);
|
||||
|
||||
let files = [];
|
||||
let messages = [];
|
||||
|
||||
@@ -161,6 +170,9 @@
|
||||
|
||||
if (res) {
|
||||
note = res;
|
||||
if (!Array.isArray(note?.access_grants)) {
|
||||
note.access_grants = [];
|
||||
}
|
||||
files = res.data.files || [];
|
||||
|
||||
if (note?.write_access) {
|
||||
@@ -193,7 +205,7 @@
|
||||
data: {
|
||||
files: files
|
||||
},
|
||||
access_control: note?.access_control
|
||||
access_grants: note?.access_grants ?? []
|
||||
}).catch((e) => {
|
||||
toast.error(`${e}`);
|
||||
});
|
||||
@@ -765,8 +777,8 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings,
|
||||
console.log('noteEventHandler', _note);
|
||||
if (_note.id !== id) return;
|
||||
|
||||
if (_note.access_control && _note.access_control !== note.access_control) {
|
||||
note.access_control = _note.access_control;
|
||||
if (_note.access_grants && _note.access_grants !== note.access_grants) {
|
||||
note.access_grants = _note.access_grants;
|
||||
}
|
||||
|
||||
if (_note.data && _note.data.files) {
|
||||
@@ -851,7 +863,7 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings,
|
||||
{#if note}
|
||||
<AccessControlModal
|
||||
bind:show={showAccessControlModal}
|
||||
bind:accessControl={note.access_control}
|
||||
bind:accessGrants={note.access_grants}
|
||||
accessRoles={['read', 'write']}
|
||||
onChange={() => {
|
||||
changeDebounceHandler();
|
||||
@@ -1114,7 +1126,11 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings,
|
||||
}}
|
||||
disabled={note?.user_id !== $user?.id && $user?.role !== 'admin'}
|
||||
>
|
||||
<span> {note?.access_control ? $i18n.t('Private') : $i18n.t('Everyone')} </span>
|
||||
<span>
|
||||
{hasPublicReadGrant(note?.access_grants)
|
||||
? $i18n.t('Everyone')
|
||||
: $i18n.t('Private')}
|
||||
</span>
|
||||
</button>
|
||||
{:else}
|
||||
<div>
|
||||
|
||||
@@ -129,7 +129,7 @@
|
||||
}
|
||||
},
|
||||
meta: null,
|
||||
access_control: {}
|
||||
access_grants: []
|
||||
}).catch((error) => {
|
||||
toast.error(`${error}`);
|
||||
return null;
|
||||
|
||||
@@ -120,7 +120,7 @@ export const createNoteHandler = async (title: string, md?: string, html?: strin
|
||||
}
|
||||
},
|
||||
meta: null,
|
||||
access_control: {}
|
||||
access_grants: []
|
||||
}).catch((error) => {
|
||||
toast.error(`${error}`);
|
||||
return null;
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
let name = '';
|
||||
let description = '';
|
||||
let accessControl = {};
|
||||
let accessGrants = [];
|
||||
|
||||
const submitHandler = async () => {
|
||||
loading = true;
|
||||
@@ -32,7 +32,7 @@
|
||||
localStorage.token,
|
||||
name,
|
||||
description,
|
||||
accessControl
|
||||
accessGrants
|
||||
).catch((e) => {
|
||||
toast.error(`${e}`);
|
||||
});
|
||||
@@ -114,7 +114,7 @@
|
||||
|
||||
<div class="mt-2">
|
||||
<AccessControl
|
||||
bind:accessControl
|
||||
bind:accessGrants
|
||||
accessRoles={['read', 'write']}
|
||||
share={$user?.permissions?.sharing?.knowledge || $user?.role === 'admin'}
|
||||
sharePublic={$user?.permissions?.sharing?.public_knowledge || $user?.role === 'admin'}
|
||||
|
||||
@@ -75,6 +75,8 @@
|
||||
file_ids: string[];
|
||||
};
|
||||
files: any[];
|
||||
access_grants?: any[];
|
||||
write_access?: boolean;
|
||||
};
|
||||
|
||||
let id = null;
|
||||
@@ -614,7 +616,7 @@
|
||||
...knowledge,
|
||||
name: knowledge.name,
|
||||
description: knowledge.description,
|
||||
access_control: knowledge.access_control
|
||||
access_grants: knowledge.access_grants ?? []
|
||||
}).catch((e) => {
|
||||
toast.error(`${e}`);
|
||||
});
|
||||
@@ -745,6 +747,9 @@
|
||||
|
||||
if (res) {
|
||||
knowledge = res;
|
||||
if (!Array.isArray(knowledge?.access_grants)) {
|
||||
knowledge.access_grants = [];
|
||||
}
|
||||
knowledgeId = knowledge?.id;
|
||||
} else {
|
||||
goto('/workspace/knowledge');
|
||||
@@ -828,9 +833,11 @@
|
||||
{#if id && knowledge}
|
||||
<AccessControlModal
|
||||
bind:show={showAccessControlModal}
|
||||
bind:accessControl={knowledge.access_control}
|
||||
bind:accessGrants={knowledge.access_grants}
|
||||
share={$user?.permissions?.sharing?.knowledge || $user?.role === 'admin'}
|
||||
sharePublic={$user?.permissions?.sharing?.public_knowledge || $user?.role === 'admin'}
|
||||
sharePublic={$user?.permissions?.sharing?.public_knowledge ||
|
||||
$user?.role === 'admin' ||
|
||||
knowledge?.write_access}
|
||||
onChange={() => {
|
||||
changeDebounceHandler();
|
||||
}}
|
||||
|
||||
@@ -108,7 +108,7 @@
|
||||
let builtinTools = {};
|
||||
|
||||
let actionIds = [];
|
||||
let accessControl = {};
|
||||
let accessGrants = [];
|
||||
let tts = { voice: '' };
|
||||
|
||||
const submitHandler = async () => {
|
||||
@@ -140,7 +140,7 @@
|
||||
|
||||
info.params = { ...info.params, ...params };
|
||||
|
||||
info.access_control = accessControl;
|
||||
info.access_grants = accessGrants;
|
||||
info.meta.capabilities = capabilities;
|
||||
|
||||
if (enableDescription) {
|
||||
@@ -301,14 +301,7 @@
|
||||
builtinTools = model?.meta?.builtinTools ?? {};
|
||||
tts = { voice: model?.meta?.tts?.voice ?? '' };
|
||||
|
||||
if ('access_control' in model) {
|
||||
accessControl = model.access_control;
|
||||
} else {
|
||||
accessControl = {};
|
||||
}
|
||||
|
||||
console.log(model?.access_control);
|
||||
console.log(accessControl);
|
||||
accessGrants = model?.access_grants ?? [];
|
||||
|
||||
info = {
|
||||
...info,
|
||||
@@ -334,10 +327,10 @@
|
||||
{#if loaded}
|
||||
<AccessControlModal
|
||||
bind:show={showAccessControlModal}
|
||||
bind:accessControl
|
||||
bind:accessGrants
|
||||
accessRoles={['read', 'write']}
|
||||
share={$user?.permissions?.sharing?.models || $user?.role === 'admin'}
|
||||
sharePublic={$user?.permissions?.sharing?.public_models || $user?.role === 'admin'}
|
||||
sharePublic={$user?.permissions?.sharing?.public_models || $user?.role === 'admin' || edit}
|
||||
/>
|
||||
|
||||
{#if onBack}
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
let commitMessage = '';
|
||||
let isProduction = true;
|
||||
|
||||
let accessControl = {};
|
||||
let accessGrants = [];
|
||||
let showAccessControlModal = false;
|
||||
let hasManualEdit = false;
|
||||
|
||||
@@ -87,7 +87,7 @@
|
||||
command,
|
||||
content,
|
||||
tags: tags.map((tag) => tag.name),
|
||||
access_control: accessControl,
|
||||
access_grants: accessGrants,
|
||||
commit_message: commitMessage || undefined,
|
||||
is_production: isProduction
|
||||
});
|
||||
@@ -259,7 +259,7 @@
|
||||
command = prompt.command.at(0) === '/' ? prompt.command.slice(1) : prompt.command;
|
||||
content = prompt.content;
|
||||
tags = (prompt.tags || []).map((tag) => ({ name: tag }));
|
||||
accessControl = prompt?.access_control === undefined ? {} : prompt?.access_control;
|
||||
accessGrants = prompt?.access_grants === undefined ? [] : prompt?.access_grants;
|
||||
|
||||
// Store originals for revert on collision
|
||||
originalName = name;
|
||||
@@ -286,10 +286,10 @@
|
||||
|
||||
<AccessControlModal
|
||||
bind:show={showAccessControlModal}
|
||||
bind:accessControl
|
||||
bind:accessGrants
|
||||
accessRoles={['read', 'write']}
|
||||
share={$user?.permissions?.sharing?.prompts || $user?.role === 'admin'}
|
||||
sharePublic={$user?.permissions?.sharing?.public_prompts || $user?.role === 'admin'}
|
||||
sharePublic={$user?.permissions?.sharing?.public_prompts || $user?.role === 'admin' || edit}
|
||||
/>
|
||||
|
||||
<!-- Edit Modal -->
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
description: ''
|
||||
};
|
||||
export let content = '';
|
||||
export let accessControl = {};
|
||||
export let accessGrants = [];
|
||||
|
||||
let _content = '';
|
||||
|
||||
@@ -161,7 +161,7 @@ class Tools:
|
||||
name,
|
||||
meta,
|
||||
content,
|
||||
access_control: accessControl
|
||||
access_grants: accessGrants
|
||||
});
|
||||
};
|
||||
|
||||
@@ -187,10 +187,10 @@ class Tools:
|
||||
|
||||
<AccessControlModal
|
||||
bind:show={showAccessControlModal}
|
||||
bind:accessControl
|
||||
bind:accessGrants
|
||||
accessRoles={['read', 'write']}
|
||||
share={$user?.permissions?.sharing?.tools || $user?.role === 'admin'}
|
||||
sharePublic={$user?.permissions?.sharing?.public_tools || $user?.role === 'admin'}
|
||||
sharePublic={$user?.permissions?.sharing?.public_tools || $user?.role === 'admin' || edit}
|
||||
/>
|
||||
|
||||
<div class=" flex flex-col justify-between w-full overflow-y-auto h-full">
|
||||
|
||||
@@ -3,75 +3,430 @@
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
import { getGroups } from '$lib/apis/groups';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import Plus from '$lib/components/icons/Plus.svelte';
|
||||
import UserCircleSolid from '$lib/components/icons/UserCircleSolid.svelte';
|
||||
import { getGroups, getGroupById, getGroupInfoById } from '$lib/apis/groups';
|
||||
import { getUserById, getUserInfoById } from '$lib/apis/users';
|
||||
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
import Badge from '$lib/components/common/Badge.svelte';
|
||||
import GlobeAlt from '$lib/components/icons/GlobeAlt.svelte';
|
||||
import Plus from '$lib/components/icons/Plus.svelte';
|
||||
import AddAccessModal from './AddAccessModal.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
|
||||
type AccessGrant = {
|
||||
id?: string;
|
||||
principal_type: 'user' | 'group';
|
||||
principal_id: string;
|
||||
permission: 'read' | 'write';
|
||||
};
|
||||
|
||||
type LegacyAccessControl = {
|
||||
read: { group_ids: string[]; user_ids: string[] };
|
||||
write: { group_ids: string[]; user_ids: string[] };
|
||||
};
|
||||
|
||||
export let onChange: Function = () => {};
|
||||
|
||||
export let accessRoles = ['read'];
|
||||
export let accessControl = {};
|
||||
export let accessGrants: AccessGrant[] | any = [];
|
||||
export let accessControl: any = undefined;
|
||||
|
||||
export let share = true;
|
||||
export let sharePublic = true;
|
||||
|
||||
let selectedGroupId = '';
|
||||
let groups = [];
|
||||
let groups: any[] = [];
|
||||
const resolvingGroupIds = new Set<string>();
|
||||
let userById: Record<string, any> = {};
|
||||
const resolvingUserIds = new Set<string>();
|
||||
|
||||
$: if (!sharePublic && accessControl === null) {
|
||||
initPublicAccess();
|
||||
}
|
||||
let showAddAccessModal = false;
|
||||
|
||||
const initPublicAccess = () => {
|
||||
if (!sharePublic && accessControl === null) {
|
||||
accessControl = {
|
||||
read: {
|
||||
group_ids: [],
|
||||
user_ids: []
|
||||
},
|
||||
write: {
|
||||
group_ids: [],
|
||||
user_ids: []
|
||||
const dedupeAccessGrants = (grants: AccessGrant[] | null | undefined): AccessGrant[] => {
|
||||
if (!Array.isArray(grants)) return [];
|
||||
const map = new Map<string, AccessGrant>();
|
||||
for (const grant of grants) {
|
||||
if (!grant) continue;
|
||||
const key = `${grant.principal_type}:${grant.principal_id}:${grant.permission}`;
|
||||
if (!grant.principal_type || !grant.principal_id || !grant.permission) continue;
|
||||
map.set(key, {
|
||||
id: grant.id,
|
||||
principal_type: grant.principal_type,
|
||||
principal_id: grant.principal_id,
|
||||
permission: grant.permission
|
||||
});
|
||||
}
|
||||
return Array.from(map.values());
|
||||
};
|
||||
|
||||
const legacyAccessControlToGrants = (accessControl: any): AccessGrant[] => {
|
||||
if (accessControl === null) {
|
||||
return [
|
||||
{
|
||||
principal_type: 'user',
|
||||
principal_id: '*',
|
||||
permission: 'read'
|
||||
}
|
||||
};
|
||||
onChange(accessControl);
|
||||
];
|
||||
}
|
||||
|
||||
if (!accessControl || typeof accessControl !== 'object') {
|
||||
return [];
|
||||
}
|
||||
|
||||
const grants: AccessGrant[] = [];
|
||||
for (const permission of ['read', 'write'] as const) {
|
||||
const entry = accessControl?.[permission] ?? {};
|
||||
for (const groupId of entry?.group_ids ?? []) {
|
||||
grants.push({
|
||||
principal_type: 'group',
|
||||
principal_id: groupId,
|
||||
permission
|
||||
});
|
||||
}
|
||||
for (const userId of entry?.user_ids ?? []) {
|
||||
grants.push({
|
||||
principal_type: 'user',
|
||||
principal_id: userId,
|
||||
permission
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return dedupeAccessGrants(grants);
|
||||
};
|
||||
|
||||
const grantsToLegacyAccessControl = (grants: AccessGrant[]): null | LegacyAccessControl => {
|
||||
const normalized = dedupeAccessGrants(grants);
|
||||
if (hasPublicReadGrant(normalized)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const result: LegacyAccessControl = {
|
||||
read: { group_ids: [], user_ids: [] },
|
||||
write: { group_ids: [], user_ids: [] }
|
||||
};
|
||||
|
||||
for (const grant of normalized) {
|
||||
if (!['read', 'write'].includes(grant.permission)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (grant.principal_type === 'group') {
|
||||
if (!result[grant.permission].group_ids.includes(grant.principal_id)) {
|
||||
result[grant.permission].group_ids = [
|
||||
...result[grant.permission].group_ids,
|
||||
grant.principal_id
|
||||
];
|
||||
}
|
||||
} else if (grant.principal_type === 'user' && grant.principal_id !== '*') {
|
||||
if (!result[grant.permission].user_ids.includes(grant.principal_id)) {
|
||||
result[grant.permission].user_ids = [
|
||||
...result[grant.permission].user_ids,
|
||||
grant.principal_id
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
const normalizeInputToGrants = (value: any): AccessGrant[] => {
|
||||
if (value === null) {
|
||||
return legacyAccessControlToGrants(null);
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return dedupeAccessGrants(value);
|
||||
}
|
||||
if (value && typeof value === 'object' && ('read' in value || 'write' in value)) {
|
||||
return legacyAccessControlToGrants(value);
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
const stableStringify = (value: any): string => {
|
||||
try {
|
||||
return JSON.stringify(value ?? null);
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
const hasPublicReadGrant = (grants: AccessGrant[]): boolean =>
|
||||
grants.some(
|
||||
(grant) =>
|
||||
grant.principal_type === 'user' && grant.principal_id === '*' && grant.permission === 'read'
|
||||
);
|
||||
|
||||
const currentGrants = (): AccessGrant[] =>
|
||||
Array.isArray(accessGrants) ? (accessGrants as AccessGrant[]) : [];
|
||||
|
||||
const getPrincipalIdsByPermission = (
|
||||
principalType: 'user' | 'group',
|
||||
permission: 'read' | 'write'
|
||||
): string[] =>
|
||||
Array.from(
|
||||
new Set(
|
||||
currentGrants()
|
||||
.filter(
|
||||
(grant) => grant.principal_type === principalType && grant.permission === permission
|
||||
)
|
||||
.map((grant) => grant.principal_id)
|
||||
)
|
||||
);
|
||||
|
||||
const hasPrincipalGrant = (
|
||||
principalType: 'user' | 'group',
|
||||
principalId: string,
|
||||
permission: 'read' | 'write'
|
||||
): boolean =>
|
||||
currentGrants().some(
|
||||
(grant) =>
|
||||
grant.principal_type === principalType &&
|
||||
grant.principal_id === principalId &&
|
||||
grant.permission === permission
|
||||
);
|
||||
|
||||
const commitAccessGrants = (nextGrants: AccessGrant[]) => {
|
||||
accessGrants = dedupeAccessGrants(nextGrants);
|
||||
onChange(accessGrants);
|
||||
};
|
||||
|
||||
const setPublic = (isPublic: boolean) => {
|
||||
const filtered = currentGrants().filter(
|
||||
(grant) =>
|
||||
!(
|
||||
grant.principal_type === 'user' &&
|
||||
grant.principal_id === '*' &&
|
||||
grant.permission === 'read'
|
||||
)
|
||||
);
|
||||
if (isPublic) {
|
||||
filtered.push({
|
||||
principal_type: 'user',
|
||||
principal_id: '*',
|
||||
permission: 'read'
|
||||
});
|
||||
}
|
||||
commitAccessGrants(filtered);
|
||||
};
|
||||
|
||||
const upsertPrincipalGrant = (
|
||||
principalType: 'user' | 'group',
|
||||
principalId: string,
|
||||
permission: 'read' | 'write',
|
||||
grants: AccessGrant[]
|
||||
): AccessGrant[] => {
|
||||
if (
|
||||
grants.some(
|
||||
(grant) =>
|
||||
grant.principal_type === principalType &&
|
||||
grant.principal_id === principalId &&
|
||||
grant.permission === permission
|
||||
)
|
||||
) {
|
||||
return grants;
|
||||
}
|
||||
return [
|
||||
...grants,
|
||||
{
|
||||
principal_type: principalType,
|
||||
principal_id: principalId,
|
||||
permission
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
const removePrincipalGrant = (
|
||||
principalType: 'user' | 'group',
|
||||
principalId: string,
|
||||
permission: 'read' | 'write',
|
||||
grants: AccessGrant[]
|
||||
): AccessGrant[] =>
|
||||
grants.filter(
|
||||
(grant) =>
|
||||
!(
|
||||
grant.principal_type === principalType &&
|
||||
grant.principal_id === principalId &&
|
||||
grant.permission === permission
|
||||
)
|
||||
);
|
||||
|
||||
const removePrincipal = (principalType: 'user' | 'group', principalId: string) => {
|
||||
let next = [...currentGrants()];
|
||||
next = removePrincipalGrant(principalType, principalId, 'read', next);
|
||||
next = removePrincipalGrant(principalType, principalId, 'write', next);
|
||||
commitAccessGrants(next);
|
||||
};
|
||||
|
||||
const togglePrincipalWrite = (principalType: 'user' | 'group', principalId: string) => {
|
||||
let next = [...currentGrants()];
|
||||
const hasWrite = hasPrincipalGrant(principalType, principalId, 'write');
|
||||
if (hasWrite) {
|
||||
next = removePrincipalGrant(principalType, principalId, 'write', next);
|
||||
} else {
|
||||
next = upsertPrincipalGrant(principalType, principalId, 'read', next);
|
||||
next = upsertPrincipalGrant(principalType, principalId, 'write', next);
|
||||
}
|
||||
commitAccessGrants(next);
|
||||
};
|
||||
|
||||
const ensureUsersByIds = async (userIds: string[]) => {
|
||||
const pendingIds = userIds.filter((id) => !userById[id] && !resolvingUserIds.has(id));
|
||||
if (!pendingIds.length) return;
|
||||
|
||||
for (const id of pendingIds) {
|
||||
resolvingUserIds.add(id);
|
||||
}
|
||||
|
||||
const fetched = await Promise.all(
|
||||
pendingIds.map(async (id) => {
|
||||
const user = await getUserInfoById(localStorage.token, id).catch((error) => {
|
||||
console.error(error);
|
||||
return null;
|
||||
});
|
||||
return { id, user };
|
||||
})
|
||||
);
|
||||
|
||||
const nextUserById = { ...userById };
|
||||
for (const item of fetched) {
|
||||
if (item.user?.id) {
|
||||
nextUserById[item.id] = item.user;
|
||||
}
|
||||
resolvingUserIds.delete(item.id);
|
||||
}
|
||||
userById = nextUserById;
|
||||
};
|
||||
|
||||
const handleAddAccess = ({ userIds, groupIds }: { userIds: string[]; groupIds: string[] }) => {
|
||||
let next = [...currentGrants()];
|
||||
|
||||
for (const groupId of groupIds) {
|
||||
next = upsertPrincipalGrant('group', groupId, 'read', next);
|
||||
}
|
||||
for (const userId of userIds) {
|
||||
next = upsertPrincipalGrant('user', userId, 'read', next);
|
||||
}
|
||||
commitAccessGrants(next);
|
||||
};
|
||||
|
||||
// NOTE: We must reference `accessGrants` directly in each reactive
|
||||
// expression so Svelte tracks the dependency.
|
||||
const ensureGroupsByIds = async (groupIds: string[]) => {
|
||||
const pendingIds = groupIds.filter(
|
||||
(id) => !groups.find((g) => g.id === id) && !resolvingGroupIds.has(id)
|
||||
);
|
||||
if (!pendingIds.length) return;
|
||||
|
||||
for (const id of pendingIds) {
|
||||
resolvingGroupIds.add(id);
|
||||
}
|
||||
|
||||
const fetched = await Promise.all(
|
||||
pendingIds.map(async (id) => {
|
||||
const group = await getGroupInfoById(localStorage.token, id).catch((error) => {
|
||||
console.error(error);
|
||||
return null;
|
||||
});
|
||||
return group;
|
||||
})
|
||||
);
|
||||
|
||||
const newGroups = fetched.filter((g) => g);
|
||||
if (newGroups.length > 0) {
|
||||
groups = [...groups, ...newGroups].filter(
|
||||
(g, index, self) => index === self.findIndex((t) => t.id === g.id)
|
||||
);
|
||||
}
|
||||
|
||||
for (const id of pendingIds) {
|
||||
resolvingGroupIds.delete(id);
|
||||
}
|
||||
};
|
||||
|
||||
$: if (readGroupIds.length > 0 || writeGroupIds.length > 0) {
|
||||
void ensureGroupsByIds([...readGroupIds, ...writeGroupIds]);
|
||||
}
|
||||
$: readGroupIds = (accessGrants, getPrincipalIdsByPermission('group', 'read'));
|
||||
$: writeGroupIds = (accessGrants, getPrincipalIdsByPermission('group', 'write'));
|
||||
$: readUserIds =
|
||||
(accessGrants, getPrincipalIdsByPermission('user', 'read').filter((id) => id !== '*'));
|
||||
$: writeUserIds =
|
||||
(accessGrants, getPrincipalIdsByPermission('user', 'write').filter((id) => id !== '*'));
|
||||
|
||||
$: selectedUserIds = Array.from(new Set([...readUserIds, ...writeUserIds]));
|
||||
|
||||
$: selectedUsers = selectedUserIds
|
||||
.map((id) => {
|
||||
return userById[id] ?? { id, name: id, email: '' };
|
||||
})
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
$: accessGroups = groups
|
||||
.filter((group) => readGroupIds.includes(group.id) || writeGroupIds.includes(group.id))
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
$: if (selectedUserIds.length > 0) {
|
||||
void ensureUsersByIds(selectedUserIds);
|
||||
}
|
||||
|
||||
$: {
|
||||
if (accessControl !== undefined) {
|
||||
const normalizedGrants = normalizeInputToGrants(accessControl);
|
||||
if (stableStringify(normalizedGrants) !== stableStringify(accessGrants)) {
|
||||
accessGrants = normalizedGrants;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$: {
|
||||
const normalizedGrants = normalizeInputToGrants(accessGrants);
|
||||
if (stableStringify(normalizedGrants) !== stableStringify(accessGrants)) {
|
||||
accessGrants = normalizedGrants;
|
||||
}
|
||||
|
||||
if (accessControl !== undefined) {
|
||||
const nextAccessControl = grantsToLegacyAccessControl(normalizedGrants);
|
||||
if (stableStringify(nextAccessControl) !== stableStringify(accessControl)) {
|
||||
accessControl = nextAccessControl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
groups = await getGroups(localStorage.token, true).catch((error) => {
|
||||
console.log('AccessControl mounted', { accessGrants, accessControl });
|
||||
const res = await getGroups(localStorage.token, true).catch((error) => {
|
||||
console.error(error);
|
||||
return [];
|
||||
});
|
||||
|
||||
if (accessControl === null) {
|
||||
initPublicAccess();
|
||||
} else {
|
||||
accessControl = {
|
||||
read: {
|
||||
group_ids: accessControl?.read?.group_ids ?? [],
|
||||
user_ids: accessControl?.read?.user_ids ?? []
|
||||
},
|
||||
write: {
|
||||
group_ids: accessControl?.write?.group_ids ?? [],
|
||||
user_ids: accessControl?.write?.user_ids ?? []
|
||||
}
|
||||
};
|
||||
}
|
||||
console.log('getGroups res', res);
|
||||
|
||||
groups = [...groups, ...res].filter(
|
||||
(g, index, self) => index === self.findIndex((t) => t.id === g.id)
|
||||
);
|
||||
});
|
||||
|
||||
$: console.log('AccessControl state', {
|
||||
accessGrants,
|
||||
readGroupIds,
|
||||
writeGroupIds,
|
||||
selectedUserIds,
|
||||
groups,
|
||||
accessGroups,
|
||||
selectedUsers
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class=" rounded-lg flex flex-col gap-2">
|
||||
<div class="">
|
||||
<div class=" text-xs font-medium mb-2.5 text-gray-500">{$i18n.t('Visibility')}</div>
|
||||
<AddAccessModal bind:show={showAddAccessModal} onAdd={handleAddAccess} />
|
||||
|
||||
<div class="flex gap-2.5 items-center mb-1">
|
||||
<div class=" rounded-lg flex flex-col gap-1">
|
||||
<div class="py-2">
|
||||
<div class="flex gap-2.5 items-center">
|
||||
<div>
|
||||
<div class=" p-2 bg-black/5 dark:bg-white/5 rounded-full">
|
||||
{#if accessControl !== null}
|
||||
{#if !hasPublicReadGrant(accessGrants ?? [])}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
@@ -106,36 +461,28 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<select
|
||||
id="models"
|
||||
class="dark:bg-gray-900 outline-hidden bg-transparent text-sm font-medium block w-fit pr-10 max-w-full placeholder-gray-400"
|
||||
value={accessControl !== null ? 'private' : 'public'}
|
||||
on:change={(e) => {
|
||||
if (e.target.value === 'public') {
|
||||
accessControl = null;
|
||||
} else {
|
||||
accessControl = {
|
||||
read: {
|
||||
group_ids: [],
|
||||
user_ids: []
|
||||
},
|
||||
write: {
|
||||
group_ids: [],
|
||||
user_ids: []
|
||||
}
|
||||
};
|
||||
}
|
||||
onChange(accessControl);
|
||||
}}
|
||||
<Tooltip
|
||||
content={!(share && sharePublic) && !hasPublicReadGrant(accessGrants ?? [])
|
||||
? $i18n.t('You do not have permission to make this public')
|
||||
: ''}
|
||||
>
|
||||
<option class=" text-gray-700" value="private" selected>{$i18n.t('Private')}</option>
|
||||
{#if share && sharePublic}
|
||||
<option class=" text-gray-700" value="public" selected>{$i18n.t('Public')}</option>
|
||||
{/if}
|
||||
</select>
|
||||
<select
|
||||
id="models"
|
||||
class="dark:bg-gray-900 outline-none bg-transparent text-sm font-medium block w-fit pr-10 max-w-full placeholder-gray-400"
|
||||
value={!hasPublicReadGrant(accessGrants ?? []) ? 'private' : 'public'}
|
||||
on:change={(e) => {
|
||||
setPublic((e.target as HTMLSelectElement).value === 'public');
|
||||
}}
|
||||
>
|
||||
<option class=" text-gray-700" value="private">{$i18n.t('Private')}</option>
|
||||
{#if (share && sharePublic) || hasPublicReadGrant(accessGrants ?? [])}
|
||||
<option class=" text-gray-700" value="public">{$i18n.t('Public')}</option>
|
||||
{/if}
|
||||
</select>
|
||||
</Tooltip>
|
||||
|
||||
<div class=" text-xs text-gray-400 font-medium">
|
||||
{#if accessControl !== null}
|
||||
{#if !hasPublicReadGrant(accessGrants ?? [])}
|
||||
{$i18n.t('Only select users and groups with permission can access')}
|
||||
{:else}
|
||||
{$i18n.t('Accessible to all users')}
|
||||
@@ -146,116 +493,126 @@
|
||||
</div>
|
||||
|
||||
{#if share}
|
||||
{#if accessControl !== null}
|
||||
{@const accessGroups = groups.filter((group) =>
|
||||
(accessControl?.read?.group_ids ?? []).includes(group.id)
|
||||
)}
|
||||
<div class="flex items-center justify-between text-xs font-medium text-gray-500 my-1">
|
||||
<div>
|
||||
<div class="">
|
||||
<div class="flex justify-between mb-2.5">
|
||||
<div class="text-xs font-medium text-gray-500">
|
||||
{$i18n.t('Groups')}
|
||||
{$i18n.t('Access List')}
|
||||
</div>
|
||||
<div class="flex gap-1">
|
||||
<button
|
||||
class="px-2 py-1 bg-transparent hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition text-xs font-medium flex items-center gap-1"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
showAddAccessModal = true;
|
||||
}}
|
||||
>
|
||||
<Plus className="size-3" />
|
||||
{$i18n.t('Add Access')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- List -->
|
||||
<div class="flex flex-col gap-2">
|
||||
<!-- Groups -->
|
||||
{#each accessGroups as group}
|
||||
<div class="flex items-center gap-3 justify-between text-sm w-full transition pb-1">
|
||||
<div class="flex items-center gap-2 w-full flex-1">
|
||||
<!-- Placeholder for group icon vs user icon -->
|
||||
<div
|
||||
class="size-5 rounded-full bg-gray-100 dark:bg-gray-850 flex items-center justify-center text-xs"
|
||||
>
|
||||
{group.name.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
|
||||
<div class="truncate text-sm flex items-center gap-2">
|
||||
{group.name}
|
||||
<span class="text-xs text-gray-400 font-normal"
|
||||
>{group?.member_count} {$i18n.t('members')}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if accessGroups.length > 0}
|
||||
<div class="flex flex-col gap-1.5 mb-2 px-0.5 mx-0.5">
|
||||
{#each accessGroups as group}
|
||||
<div class="flex items-center gap-3 justify-between text-sm w-full transition">
|
||||
<div class="flex items-center gap-1.5 w-full">
|
||||
<div>
|
||||
{group.name} <span class="text-xs text-gray-500">{group?.member_count}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full flex justify-end items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => {
|
||||
if (accessRoles.includes('write')) {
|
||||
togglePrincipalWrite('group', group.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#if writeGroupIds.includes(group.id)}
|
||||
<Badge type={'success'} content={$i18n.t('Write')} />
|
||||
{:else}
|
||||
<Badge type={'info'} content={$i18n.t('Read')} />
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
<div class="w-full flex justify-end items-center gap-0.5">
|
||||
<button
|
||||
class=""
|
||||
type="button"
|
||||
on:click={() => {
|
||||
if (accessRoles.includes('write')) {
|
||||
if ((accessControl?.write?.group_ids ?? []).includes(group.id)) {
|
||||
accessControl.write.group_ids = (
|
||||
accessControl?.write?.group_ids ?? []
|
||||
).filter((group_id) => group_id !== group.id);
|
||||
} else {
|
||||
accessControl.write.group_ids = [
|
||||
...(accessControl?.write?.group_ids ?? []),
|
||||
group.id
|
||||
];
|
||||
}
|
||||
onChange(accessControl);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#if (accessControl?.write?.group_ids ?? []).includes(group.id)}
|
||||
<Badge type={'success'} content={$i18n.t('Write')} />
|
||||
{:else}
|
||||
<Badge type={'info'} content={$i18n.t('Read')} />
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
<button
|
||||
class=" rounded-full p-1 hover:bg-gray-100 dark:hover:bg-gray-850 transition"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
accessControl.read.group_ids = (
|
||||
accessControl?.read?.group_ids ?? []
|
||||
).filter((id) => id !== group.id);
|
||||
accessControl.write.group_ids = (
|
||||
accessControl?.write?.group_ids ?? []
|
||||
).filter((id) => id !== group.id);
|
||||
onChange(accessControl);
|
||||
}}
|
||||
>
|
||||
<XMark />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- <div class="flex items-center justify-center">
|
||||
<div class="text-gray-500 text-xs text-center py-2 px-10">
|
||||
{$i18n.t('No groups with access, add a group to grant access')}
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<div class="mb-1">
|
||||
<div class="flex w-full">
|
||||
<div class="flex flex-1 items-center">
|
||||
<div class="w-full px-0.5">
|
||||
<select
|
||||
class=" outline-hidden bg-transparent text-sm block w-full pr-10 max-w-full
|
||||
{selectedGroupId ? '' : 'text-gray-500'}
|
||||
dark:placeholder-gray-500"
|
||||
bind:value={selectedGroupId}
|
||||
on:change={() => {
|
||||
if (selectedGroupId !== '') {
|
||||
accessControl.read.group_ids = [
|
||||
...(accessControl?.read?.group_ids ?? []),
|
||||
selectedGroupId
|
||||
];
|
||||
|
||||
selectedGroupId = '';
|
||||
onChange(accessControl);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<option class=" text-gray-700" value="" disabled selected
|
||||
>{$i18n.t('Select a group')}</option
|
||||
>
|
||||
{#each groups.filter((group) => !(accessControl?.read?.group_ids ?? []).includes(group.id)) as group}
|
||||
<option class=" text-gray-700" value={group.id}>{group.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class=" rounded-full p-1 hover:bg-gray-100 dark:hover:bg-gray-850 transition"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
removePrincipal('group', group.id);
|
||||
}}
|
||||
>
|
||||
<XMark className="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
<!-- Users -->
|
||||
{#each selectedUsers as user}
|
||||
<div
|
||||
class="flex items-center gap-3 justify-between text-sm w-full transition border-b border-gray-50 dark:border-gray-850 pb-2 last:border-0"
|
||||
>
|
||||
<div class="flex items-center gap-2 w-full flex-1">
|
||||
<img
|
||||
class="rounded-full size-5 object-cover"
|
||||
src={`${WEBUI_API_BASE_URL}/users/${user.id}/profile/image`}
|
||||
alt={user.name ?? user.id}
|
||||
/>
|
||||
<div class="w-full">
|
||||
<Tooltip content={user.email} placement="top-start">
|
||||
<div class="truncate text-sm">{user.name ?? user.id}</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-full flex justify-end items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => {
|
||||
if (accessRoles.includes('write')) {
|
||||
togglePrincipalWrite('user', user.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#if writeUserIds.includes(user.id)}
|
||||
<Badge type={'success'} content={$i18n.t('Write')} />
|
||||
{:else}
|
||||
<Badge type={'info'} content={$i18n.t('Read')} />
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
<button
|
||||
class=" rounded-full p-1 hover:bg-gray-100 dark:hover:bg-gray-850 transition"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
removePrincipal('user', user.id);
|
||||
}}
|
||||
>
|
||||
<XMark className="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
{#if !hasPublicReadGrant(accessGrants ?? []) && accessGroups.length === 0 && selectedUsers.length === 0}
|
||||
<div class="text-xs text-gray-500 text-center py-4">
|
||||
{$i18n.t('No access grants. Private to you.')}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte';
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
@@ -6,8 +6,16 @@
|
||||
import AccessControl from './AccessControl.svelte';
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
|
||||
type AccessGrant = {
|
||||
id?: string;
|
||||
principal_type: 'user' | 'group';
|
||||
principal_id: string;
|
||||
permission: 'read' | 'write';
|
||||
};
|
||||
|
||||
export let show = false;
|
||||
export let accessControl = {};
|
||||
export let accessGrants: AccessGrant[] = [];
|
||||
export let accessControl: any = undefined;
|
||||
export let accessRoles = ['read'];
|
||||
|
||||
export let share = true;
|
||||
@@ -33,7 +41,14 @@
|
||||
</div>
|
||||
|
||||
<div class="w-full px-5 pb-4 dark:text-white">
|
||||
<AccessControl bind:accessControl {onChange} {accessRoles} {share} {sharePublic} />
|
||||
<AccessControl
|
||||
bind:accessGrants
|
||||
bind:accessControl
|
||||
{onChange}
|
||||
{accessRoles}
|
||||
{share}
|
||||
{sharePublic}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
content: updatedPrompt.content,
|
||||
version_id: updatedPrompt.version_id,
|
||||
tags: updatedPrompt.tags,
|
||||
access_control: updatedPrompt?.access_control === undefined ? {} : updatedPrompt?.access_control
|
||||
access_grants: updatedPrompt?.access_grants === undefined ? [] : updatedPrompt?.access_grants
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -59,7 +59,7 @@
|
||||
content: _prompt.content,
|
||||
version_id: _prompt.version_id,
|
||||
tags: _prompt.tags,
|
||||
access_control: _prompt?.access_control === undefined ? {} : _prompt?.access_control
|
||||
access_grants: _prompt?.access_grants === undefined ? [] : _prompt?.access_grants
|
||||
};
|
||||
} else {
|
||||
goto('/workspace/prompts');
|
||||
|
||||
@@ -10,10 +10,11 @@
|
||||
import PromptEditor from '$lib/components/workspace/Prompts/PromptEditor.svelte';
|
||||
|
||||
let prompt: {
|
||||
title: string;
|
||||
name: string;
|
||||
command: string;
|
||||
content: string;
|
||||
access_control: any | null;
|
||||
tags: string[];
|
||||
access_grants: any[];
|
||||
} | null = null;
|
||||
|
||||
let clone = false;
|
||||
@@ -50,7 +51,7 @@
|
||||
command: _prompt.command,
|
||||
content: _prompt.content,
|
||||
tags: _prompt.tags || [],
|
||||
access_control: _prompt.access_control !== undefined ? _prompt.access_control : {}
|
||||
access_grants: _prompt.access_grants !== undefined ? _prompt.access_grants : []
|
||||
};
|
||||
});
|
||||
|
||||
@@ -70,7 +71,7 @@
|
||||
command: _prompt.command,
|
||||
content: _prompt.content,
|
||||
tags: _prompt.tags || [],
|
||||
access_control: _prompt.access_control !== undefined ? _prompt.access_control : {}
|
||||
access_grants: _prompt.access_grants !== undefined ? _prompt.access_grants : []
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
name: data.name,
|
||||
meta: data.meta,
|
||||
content: data.content,
|
||||
access_control: data.access_control
|
||||
access_grants: data.access_grants
|
||||
}).catch((error) => {
|
||||
toast.error(`${error}`);
|
||||
return null;
|
||||
@@ -87,7 +87,7 @@
|
||||
name={tool?.name ?? ''}
|
||||
meta={tool?.meta ?? { description: '' }}
|
||||
content={tool?.content ?? ''}
|
||||
accessControl={tool?.access_control !== undefined ? tool.access_control : {}}
|
||||
accessGrants={tool?.access_grants !== undefined ? tool.access_grants : []}
|
||||
{clone}
|
||||
onSave={(value) => {
|
||||
saveHandler(value);
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
name: data.name,
|
||||
meta: data.meta,
|
||||
content: data.content,
|
||||
access_control: data.access_control
|
||||
access_grants: data.access_grants
|
||||
}).catch((error) => {
|
||||
toast.error(`${error}`);
|
||||
return null;
|
||||
@@ -83,7 +83,7 @@
|
||||
name={tool.name}
|
||||
meta={tool.meta}
|
||||
content={tool.content}
|
||||
accessControl={tool.access_control}
|
||||
accessGrants={tool.access_grants ?? []}
|
||||
onSave={(value) => {
|
||||
saveHandler(value);
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user