🤝 refactor: Clarify labels for Sharing Permissions

This commit is contained in:
Danny Avila 2025-08-14 22:55:51 -04:00
parent 6af7efd0f4
commit d711fc7852
No known key found for this signature in database
GPG key ID: BF31EEB2C5CA0956
6 changed files with 27 additions and 366 deletions

View file

@ -109,7 +109,7 @@ const AdminSettings = () => {
const labelControllerData = [ const labelControllerData = [
{ {
promptPerm: Permissions.SHARED_GLOBAL, promptPerm: Permissions.SHARED_GLOBAL,
label: localize('com_ui_prompts_allow_share_global'), label: localize('com_ui_prompts_allow_share'),
}, },
{ {
promptPerm: Permissions.CREATE, promptPerm: Permissions.CREATE,

View file

@ -1,350 +0,0 @@
import React, { useState, useEffect } from 'react';
import { AccessRoleIds, ResourceType } from 'librechat-data-provider';
import { Settings, Users, UserCheck, Trash2, Shield } from 'lucide-react';
import {
useGetResourcePermissionsQuery,
useUpdateResourcePermissionsMutation,
} from 'librechat-data-provider/react-query';
import type { TPrincipal } from 'librechat-data-provider';
import {
Button,
Spinner,
OGDialog,
OGDialogTitle,
OGDialogClose,
OGDialogContent,
OGDialogTrigger,
useToastContext,
} from '@librechat/client';
import { SelectedPrincipalsList } from './PeoplePicker';
import PublicSharingToggle from './PublicSharingToggle';
import { cn, removeFocusOutlines } from '~/utils';
import { useLocalize } from '~/hooks';
export default function ManagePermissionsDialog({
agentName,
resourceType = ResourceType.AGENT,
agentDbId,
onUpdatePermissions,
}: {
agentDbId: string;
agentName?: string;
resourceType?: ResourceType;
onUpdatePermissions?: (
shares: TPrincipal[],
isPublic: boolean,
publicRole: AccessRoleIds,
) => void;
}) {
const localize = useLocalize();
const { showToast } = useToastContext();
const {
data: permissionsData,
isLoading: isLoadingPermissions,
error: permissionsError,
} = useGetResourcePermissionsQuery(resourceType, agentDbId, {
enabled: !!agentDbId,
});
const updatePermissionsMutation = useUpdateResourcePermissionsMutation();
const [managedShares, setManagedShares] = useState<TPrincipal[]>([]);
const [managedIsPublic, setManagedIsPublic] = useState(false);
const [managedPublicRole, setManagedPublicRole] = useState<AccessRoleIds>(
AccessRoleIds.AGENT_VIEWER,
);
const [isModalOpen, setIsModalOpen] = useState(false);
const [hasChanges, setHasChanges] = useState(false);
const currentShares: TPrincipal[] = permissionsData?.principals || [];
const isPublic = permissionsData?.public || false;
const publicRole = permissionsData?.publicAccessRoleId || AccessRoleIds.AGENT_VIEWER;
useEffect(() => {
if (permissionsData) {
const shares = permissionsData.principals || [];
const isPublicValue = permissionsData.public || false;
const publicRoleValue = permissionsData.publicAccessRoleId || AccessRoleIds.AGENT_VIEWER;
setManagedShares(shares);
setManagedIsPublic(isPublicValue);
setManagedPublicRole(publicRoleValue);
setHasChanges(false);
}
}, [permissionsData, isModalOpen]);
if (!agentDbId) {
return null;
}
if (permissionsError) {
return <div className="text-sm text-red-600">{localize('com_ui_permissions_failed_load')}</div>;
}
const handleRemoveShare = (idOnTheSource: string) => {
setManagedShares(managedShares.filter((s) => s.idOnTheSource !== idOnTheSource));
setHasChanges(true);
};
const handleRoleChange = (idOnTheSource: string, newRole: AccessRoleIds) => {
setManagedShares(
managedShares.map((s) =>
s.idOnTheSource === idOnTheSource ? { ...s, accessRoleId: newRole } : s,
),
);
setHasChanges(true);
};
const handleSaveChanges = async () => {
try {
const originalSharesMap = new Map(
currentShares.map((share) => [`${share.type}-${share.idOnTheSource}`, share]),
);
const managedSharesMap = new Map(
managedShares.map((share) => [`${share.type}-${share.idOnTheSource}`, share]),
);
const updated = managedShares.filter((share) => {
const key = `${share.type}-${share.idOnTheSource}`;
const original = originalSharesMap.get(key);
return !original || original.accessRoleId !== share.accessRoleId;
});
const removed = currentShares.filter((share) => {
const key = `${share.type}-${share.idOnTheSource}`;
return !managedSharesMap.has(key);
});
await updatePermissionsMutation.mutateAsync({
resourceType,
resourceId: agentDbId,
data: {
updated,
removed,
public: managedIsPublic,
publicAccessRoleId: managedIsPublic ? managedPublicRole : undefined,
},
});
if (onUpdatePermissions) {
onUpdatePermissions(managedShares, managedIsPublic, managedPublicRole);
}
showToast({
message: localize('com_ui_permissions_updated_success'),
status: 'success',
});
setIsModalOpen(false);
} catch (error) {
console.error('Error updating permissions:', error);
showToast({
message: localize('com_ui_permissions_failed_update'),
status: 'error',
});
}
};
const handleCancel = () => {
setManagedShares(currentShares);
setManagedIsPublic(isPublic);
setManagedPublicRole(publicRole);
setIsModalOpen(false);
};
const handleRevokeAll = () => {
setManagedShares([]);
setManagedIsPublic(false);
setHasChanges(true);
};
const handlePublicToggle = (isPublic: boolean) => {
setManagedIsPublic(isPublic);
setHasChanges(true);
if (!isPublic) {
setManagedPublicRole(AccessRoleIds.AGENT_VIEWER);
}
};
const handlePublicRoleChange = (role: AccessRoleIds) => {
setManagedPublicRole(role);
setHasChanges(true);
};
const totalShares = managedShares.length + (managedIsPublic ? 1 : 0);
const originalTotalShares = currentShares.length + (isPublic ? 1 : 0);
/** Check if there's at least one owner (user, group, or public with owner role) */
const hasAtLeastOneOwner =
managedShares.some((share) => share.accessRoleId === AccessRoleIds.AGENT_OWNER) ||
(managedIsPublic && managedPublicRole === AccessRoleIds.AGENT_OWNER);
let peopleLabel = localize('com_ui_people');
if (managedShares.length === 1) {
peopleLabel = localize('com_ui_person');
}
let buttonAriaLabel = localize('com_ui_manage_permissions_for') + ' agent';
if (agentName != null && agentName !== '') {
buttonAriaLabel = localize('com_ui_manage_permissions_for') + ` "${agentName}"`;
}
let dialogTitle = localize('com_ui_manage_permissions_for') + ' Agent';
if (agentName != null && agentName !== '') {
dialogTitle = localize('com_ui_manage_permissions_for') + ` "${agentName}"`;
}
let publicSuffix = '';
if (managedIsPublic) {
publicSuffix = localize('com_ui_and_public');
}
return (
<OGDialog open={isModalOpen} onOpenChange={setIsModalOpen}>
<OGDialogTrigger asChild>
<button
className={cn(
'btn btn-neutral border-token-border-light relative h-9 rounded-lg font-medium',
removeFocusOutlines,
)}
aria-label={buttonAriaLabel}
type="button"
>
<div className="flex items-center justify-center gap-2 text-blue-500">
<Settings className="icon-md h-4 w-4" />
<span className="hidden sm:inline">{localize('com_ui_manage')}</span>
{originalTotalShares > 0 && `(${originalTotalShares})`}
</div>
</button>
</OGDialogTrigger>
<OGDialogContent className="max-h-[90vh] w-11/12 overflow-y-auto md:max-w-3xl">
<OGDialogTitle>
<div className="flex items-center gap-2">
<Shield className="h-5 w-5 text-blue-500" />
{dialogTitle}
</div>
</OGDialogTitle>
<div className="space-y-6 p-2">
<div className="rounded-lg bg-surface-tertiary p-4">
<div className="flex items-center justify-between">
<div>
<h3 className="text-sm font-medium text-text-primary">
{localize('com_ui_current_access')}
</h3>
<p className="text-xs text-text-secondary">
{(() => {
if (totalShares === 0) {
return localize('com_ui_no_users_groups_access');
}
return localize('com_ui_shared_with_count', {
0: managedShares.length,
1: peopleLabel,
2: publicSuffix,
});
})()}
</p>
</div>
{(managedShares.length > 0 || managedIsPublic) && (
<Button
variant="outline"
size="sm"
onClick={handleRevokeAll}
className="text-red-600 hover:text-red-700"
>
<Trash2 className="mr-2 h-4 w-4" />
{localize('com_ui_revoke_all')}
</Button>
)}
</div>
</div>
{(() => {
if (isLoadingPermissions) {
return (
<div className="flex items-center justify-center p-8">
<Spinner className="h-6 w-6" />
<span className="ml-2 text-sm text-text-secondary">
{localize('com_ui_loading_permissions')}
</span>
</div>
);
}
if (managedShares.length > 0) {
return (
<div>
<h3 className="mb-3 flex items-center gap-2 text-sm font-medium text-text-primary">
<UserCheck className="h-4 w-4" />
{localize('com_ui_user_group_permissions')} ({managedShares.length})
</h3>
<SelectedPrincipalsList
principles={managedShares}
onRemoveHandler={handleRemoveShare}
resourceType={resourceType}
onRoleChange={(id, newRole) => handleRoleChange(id, newRole)}
/>
</div>
);
}
return (
<div className="rounded-lg border-2 border-dashed border-border-light p-8 text-center">
<Users className="mx-auto h-8 w-8 text-text-secondary" />
<p className="mt-2 text-sm text-text-secondary">
{localize('com_ui_no_individual_access')}
</p>
</div>
);
})()}
<PublicSharingToggle
isPublic={managedIsPublic}
publicRole={managedPublicRole}
onPublicToggle={handlePublicToggle}
onPublicRoleChange={handlePublicRoleChange}
/>
<div className="flex justify-end gap-3 border-t pt-4">
<OGDialogClose asChild>
<Button variant="outline" onClick={handleCancel}>
{localize('com_ui_cancel')}
</Button>
</OGDialogClose>
<Button
onClick={handleSaveChanges}
disabled={
updatePermissionsMutation.isLoading ||
!hasChanges ||
isLoadingPermissions ||
!hasAtLeastOneOwner
}
className="min-w-[120px]"
>
{updatePermissionsMutation.isLoading ? (
<div className="flex items-center gap-2">
<Spinner className="h-4 w-4" />
{localize('com_ui_saving')}
</div>
) : (
localize('com_ui_save_changes')
)}
</Button>
</div>
{hasChanges && (
<div className="text-xs text-orange-600 dark:text-orange-400">
* {localize('com_ui_unsaved_changes')}
</div>
)}
{!hasAtLeastOneOwner && hasChanges && (
<div className="text-xs text-red-600 dark:text-red-400">
* {localize('com_ui_at_least_one_owner_required')}
</div>
)}
</div>
</OGDialogContent>
</OGDialog>
);
}

View file

@ -16,6 +16,11 @@ interface PublicSharingToggleProps {
className?: string; className?: string;
} }
const accessDescriptions: Record<ResourceType, 'com_ui_agent' | 'com_ui_prompt'> = {
[ResourceType.AGENT]: 'com_ui_agent',
[ResourceType.PROMPTGROUP]: 'com_ui_prompt',
};
export default function PublicSharingToggle({ export default function PublicSharingToggle({
isPublic, isPublic,
publicRole, publicRole,
@ -49,19 +54,25 @@ export default function PublicSharingToggle({
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Label <Label
htmlFor="public-access-toggle" htmlFor="share-everyone-toggle"
className="cursor-pointer text-sm font-medium text-text-primary" className="cursor-pointer text-sm font-medium text-text-primary"
> >
{localize('com_ui_public_access')} {localize('com_ui_share_everyone')}
</Label> </Label>
<InfoHoverCard side={ESide.Top} text={localize('com_ui_public_access_description')} /> <InfoHoverCard
side={ESide.Top}
text={localize('com_ui_share_everyone_description_var', {
resource:
localize(accessDescriptions[resourceType]) || localize('com_ui_resource'),
})}
/>
</div> </div>
</div> </div>
<Switch <Switch
id="public-access-toggle" id="share-everyone-toggle"
checked={isPublic} checked={isPublic}
onCheckedChange={handleToggle} onCheckedChange={handleToggle}
aria-label={localize('com_ui_public_access')} aria-label={localize('com_ui_share_everyone')}
/> />
</div> </div>
</div> </div>
@ -94,7 +105,7 @@ export default function PublicSharingToggle({
</div> </div>
<div className="flex flex-col gap-0.5"> <div className="flex flex-col gap-0.5">
<Label htmlFor="permission-level" className="text-sm font-medium text-text-primary"> <Label htmlFor="permission-level" className="text-sm font-medium text-text-primary">
{localize('com_ui_public_permission_level')} {localize('com_ui_everyone_permission_level')}
</Label> </Label>
</div> </div>
</div> </div>

View file

@ -1,5 +1,4 @@
export { default as AccessRolesPicker } from './AccessRolesPicker';
export { default as GenericGrantAccessDialog } from './GenericGrantAccessDialog';
export { default as ManagePermissionsDialog } from './ManagePermissionsDialog';
export { default as PrincipalAvatar } from './PrincipalAvatar'; export { default as PrincipalAvatar } from './PrincipalAvatar';
export { default as AccessRolesPicker } from './AccessRolesPicker';
export { default as PublicSharingToggle } from './PublicSharingToggle'; export { default as PublicSharingToggle } from './PublicSharingToggle';
export { default as GenericGrantAccessDialog } from './GenericGrantAccessDialog';

View file

@ -114,7 +114,7 @@ const AdminSettings = () => {
const labelControllerData = [ const labelControllerData = [
{ {
agentPerm: Permissions.SHARED_GLOBAL, agentPerm: Permissions.SHARED_GLOBAL,
label: localize('com_ui_agents_allow_share_global'), label: localize('com_ui_agents_allow_share'),
}, },
{ {
agentPerm: Permissions.CREATE, agentPerm: Permissions.CREATE,

View file

@ -586,7 +586,7 @@
"com_ui_agent_version_unknown_date": "Unknown date", "com_ui_agent_version_unknown_date": "Unknown date",
"com_ui_agents": "Agents", "com_ui_agents": "Agents",
"com_ui_agents_allow_create": "Allow creating Agents", "com_ui_agents_allow_create": "Allow creating Agents",
"com_ui_agents_allow_share_global": "Allow sharing Agents to all users", "com_ui_agents_allow_share": "Allow sharing Agents",
"com_ui_agents_allow_use": "Allow using Agents", "com_ui_agents_allow_use": "Allow using Agents",
"com_ui_people_picker": "People Picker", "com_ui_people_picker": "People Picker",
"com_ui_people_picker_allow_view_users": "Allow viewing users", "com_ui_people_picker_allow_view_users": "Allow viewing users",
@ -981,7 +981,7 @@
"com_ui_prompt_update_error": "There was an error updating the prompt", "com_ui_prompt_update_error": "There was an error updating the prompt",
"com_ui_prompts": "Prompts", "com_ui_prompts": "Prompts",
"com_ui_prompts_allow_create": "Allow creating Prompts", "com_ui_prompts_allow_create": "Allow creating Prompts",
"com_ui_prompts_allow_share_global": "Allow sharing Prompts to all users", "com_ui_prompts_allow_share": "Allow sharing Prompts",
"com_ui_prompts_allow_use": "Allow using Prompts", "com_ui_prompts_allow_use": "Allow using Prompts",
"com_ui_provider": "Provider", "com_ui_provider": "Provider",
"com_ui_quality": "Quality", "com_ui_quality": "Quality",
@ -1167,6 +1167,7 @@
"com_ui_select_options": "Select options...", "com_ui_select_options": "Select options...",
"com_ui_no_results_found": "No results found", "com_ui_no_results_found": "No results found",
"com_ui_try_adjusting_search": "Try adjusting your search terms", "com_ui_try_adjusting_search": "Try adjusting your search terms",
"com_ui_resource": "resource",
"com_ui_role_viewer": "Viewer", "com_ui_role_viewer": "Viewer",
"com_ui_role_editor": "Editor", "com_ui_role_editor": "Editor",
"com_ui_role_manager": "Manager", "com_ui_role_manager": "Manager",
@ -1189,11 +1190,11 @@
"com_ui_loading_permissions": "Loading permissions...", "com_ui_loading_permissions": "Loading permissions...",
"com_ui_user_group_permissions": "User & Group Permissions", "com_ui_user_group_permissions": "User & Group Permissions",
"com_ui_no_individual_access": "No individual users or groups have access to this agent", "com_ui_no_individual_access": "No individual users or groups have access to this agent",
"com_ui_public_access": "Public Access", "com_ui_share_everyone": "Share with everyone",
"com_ui_public_access_description": "Anyone can access this resource publicly", "com_ui_share_everyone_description_var": "This {{resource}} will be available to everyone. Please make sure the {{resource}} is really meant to be shared with everyone. Be careful with your data.",
"com_ui_save_changes": "Save Changes", "com_ui_save_changes": "Save Changes",
"com_ui_unsaved_changes": "You have unsaved changes", "com_ui_unsaved_changes": "You have unsaved changes",
"com_ui_public_permission_level": "Public permission level", "com_ui_everyone_permission_level": "Everyone's permission level",
"com_ui_at_least_one_owner_required": "At least one owner is required", "com_ui_at_least_one_owner_required": "At least one owner is required",
"com_agents_marketplace": "Agent Marketplace", "com_agents_marketplace": "Agent Marketplace",
"com_agents_all": "All Agents", "com_agents_all": "All Agents",