Skip to content

Commit 33ee7da

Browse files
committed
feat(access-control): page-based permission groups, tool-level deny-list, settings row-action consistency
- Replace the cramped configure modal with a full-surface tabbed Access Control page (General/Model Providers/Blocks/Platform) with a sticky save bar - Add deniedTools denylist to permission groups: deny individual tools within an allowed integration; enforced at the universal executeTool chokepoint via ToolNotAllowedError, and hidden from the operation dropdown for governed users - Add per-section Select/Deselect All on the Blocks tab and expandable per-tool deny rows (mirrors Providers->Models) - Standardize every settings list row on the canonical "..." DropdownMenu (custom-tools, mcp, workflow-mcp-servers, api-keys, secrets, credential-sets) and align badges (ChipTag), avatars (MemberAvatar), inputs (ChipInput), and the mothership env picker (ChipSelect)
1 parent 7640af0 commit 33ee7da

19 files changed

Lines changed: 2358 additions & 1844 deletions

File tree

apps/sim/app/workspace/[workspaceId]/settings/components/admin/admin.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ export function Admin() {
196196
)}
197197
</div>
198198

199-
<div className='h-px bg-[var(--border-secondary)]' />
199+
<div className='h-px bg-[var(--border)]' />
200200

201201
<div className='flex flex-col gap-2'>
202202
<p className='text-[var(--text-secondary)] text-sm'>
@@ -231,10 +231,10 @@ export function Admin() {
231231
)}
232232
</div>
233233

234-
<div className='h-px bg-[var(--border-secondary)]' />
234+
<div className='h-px bg-[var(--border)]' />
235235

236236
<div className='flex flex-col gap-3'>
237-
<p className='font-medium text-[var(--text-primary)] text-sm'>User Management</p>
237+
<p className='font-medium text-[var(--text-muted)] text-small'>User Management</p>
238238
<div className='flex gap-2'>
239239
<ChipInput
240240
icon={Search}

apps/sim/app/workspace/[workspaceId]/settings/components/api-keys/api-keys.tsx

Lines changed: 74 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,21 @@ import { createLogger } from '@sim/logger'
55
import { formatDate } from '@sim/utils/formatting'
66
import { Info, Plus } from 'lucide-react'
77
import { useParams } from 'next/navigation'
8-
import { Chip, ChipConfirmModal, ChipInput, Search, Switch, Tooltip } from '@/components/emcn'
8+
import {
9+
Chip,
10+
ChipConfirmModal,
11+
ChipInput,
12+
chipVariants,
13+
DropdownMenu,
14+
DropdownMenuContent,
15+
DropdownMenuItem,
16+
DropdownMenuTrigger,
17+
MoreHorizontal,
18+
Search,
19+
Switch,
20+
Tooltip,
21+
toast,
22+
} from '@/components/emcn'
923
import { useSession } from '@/lib/auth/auth-client'
1024
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
1125
import { SettingsSection } from '@/app/workspace/[workspaceId]/settings/components/settings-section/settings-section'
@@ -20,6 +34,51 @@ import { CreateApiKeyModal } from './components'
2034

2135
const logger = createLogger('ApiKeys')
2236

37+
/** Copies an API key's name and confirms with a toast. */
38+
function copyKeyName(name: string) {
39+
void navigator.clipboard.writeText(name)
40+
toast.success('Copied name to clipboard')
41+
}
42+
43+
interface ApiKeyRowMenuProps {
44+
keyName: string
45+
onDelete: () => void
46+
/** When false, the Delete item is disabled (e.g. non-admins on workspace keys). */
47+
canDelete?: boolean
48+
}
49+
50+
/**
51+
* Trailing `...` actions menu for an API key row. Mirrors the Secrets /
52+
* Teammates row menu so the settings experience is consistent.
53+
*/
54+
function ApiKeyRowMenu({ keyName, onDelete, canDelete = true }: ApiKeyRowMenuProps) {
55+
return (
56+
<div className='flex-shrink-0'>
57+
<DropdownMenu>
58+
<DropdownMenuTrigger asChild>
59+
<button
60+
type='button'
61+
aria-label='API key actions'
62+
className={chipVariants({ flush: true })}
63+
>
64+
<MoreHorizontal className='size-[14px] flex-shrink-0 text-[var(--text-icon)]' />
65+
</button>
66+
</DropdownMenuTrigger>
67+
<DropdownMenuContent align='end'>
68+
<DropdownMenuItem onSelect={() => copyKeyName(keyName)}>Copy name</DropdownMenuItem>
69+
<DropdownMenuItem
70+
className='text-[var(--text-error)]'
71+
onSelect={onDelete}
72+
disabled={!canDelete}
73+
>
74+
Delete
75+
</DropdownMenuItem>
76+
</DropdownMenuContent>
77+
</DropdownMenu>
78+
</div>
79+
)
80+
}
81+
2382
export function ApiKeys() {
2483
const { data: session } = useSession()
2584
const userId = session?.user?.id
@@ -164,16 +223,14 @@ export function ApiKeys() {
164223
{key.displayKey || key.key}
165224
</p>
166225
</div>
167-
<Chip
168-
className='flex-shrink-0'
169-
onClick={() => {
226+
<ApiKeyRowMenu
227+
keyName={key.name}
228+
onDelete={() => {
170229
setDeleteKey(key)
171230
setShowDeleteDialog(true)
172231
}}
173-
disabled={!canManageWorkspaceKeys}
174-
>
175-
Delete
176-
</Chip>
232+
canDelete={canManageWorkspaceKeys}
233+
/>
177234
</div>
178235
))}
179236
</div>
@@ -197,16 +254,14 @@ export function ApiKeys() {
197254
{key.displayKey || key.key}
198255
</p>
199256
</div>
200-
<Chip
201-
className='flex-shrink-0'
202-
onClick={() => {
257+
<ApiKeyRowMenu
258+
keyName={key.name}
259+
onDelete={() => {
203260
setDeleteKey(key)
204261
setShowDeleteDialog(true)
205262
}}
206-
disabled={!canManageWorkspaceKeys}
207-
>
208-
Delete
209-
</Chip>
263+
canDelete={canManageWorkspaceKeys}
264+
/>
210265
</div>
211266
))}
212267
</div>
@@ -235,15 +290,13 @@ export function ApiKeys() {
235290
{key.displayKey || key.key}
236291
</p>
237292
</div>
238-
<Chip
239-
className='flex-shrink-0'
240-
onClick={() => {
293+
<ApiKeyRowMenu
294+
keyName={key.name}
295+
onDelete={() => {
241296
setDeleteKey(key)
242297
setShowDeleteDialog(true)
243298
}}
244-
>
245-
Delete
246-
</Chip>
299+
/>
247300
</div>
248301
{isConflict && (
249302
<div className='text-[var(--text-error)] text-small leading-tight'>

apps/sim/app/workspace/[workspaceId]/settings/components/credential-sets/credential-sets.tsx

Lines changed: 85 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,13 @@ import {
2020
ChipModalField,
2121
ChipModalFooter,
2222
ChipModalHeader,
23+
chipVariants,
24+
DropdownMenu,
25+
DropdownMenuContent,
26+
DropdownMenuItem,
27+
DropdownMenuTrigger,
2328
type FileInputOptions,
29+
MoreHorizontal,
2430
Search,
2531
TagInput,
2632
type TagItem,
@@ -516,13 +522,26 @@ export function CredentialSets() {
516522
</div>
517523

518524
<div className='ml-4 flex items-center gap-1'>
519-
<Chip
520-
variant='destructive'
521-
onClick={() => handleRemoveMember(member.id)}
522-
disabled={removeMember.isPending}
523-
>
524-
Remove
525-
</Chip>
525+
<DropdownMenu>
526+
<DropdownMenuTrigger asChild>
527+
<button
528+
type='button'
529+
aria-label='Member actions'
530+
className={chipVariants({ flush: true })}
531+
>
532+
<MoreHorizontal className='size-[14px] flex-shrink-0 text-[var(--text-icon)]' />
533+
</button>
534+
</DropdownMenuTrigger>
535+
<DropdownMenuContent align='end'>
536+
<DropdownMenuItem
537+
className='text-[var(--text-error)]'
538+
onSelect={() => handleRemoveMember(member.id)}
539+
disabled={removeMember.isPending}
540+
>
541+
Remove
542+
</DropdownMenuItem>
543+
</DropdownMenuContent>
544+
</DropdownMenu>
526545
</div>
527546
</div>
528547
)
@@ -561,25 +580,41 @@ export function CredentialSets() {
561580
</div>
562581

563582
<div className='ml-4 flex items-center gap-1'>
564-
<Chip
565-
onClick={() => handleResendInvitation(invitation.id, email)}
566-
disabled={
567-
resendingInvitations.has(invitation.id) ||
568-
(resendCooldowns[invitation.id] ?? 0) > 0
569-
}
570-
>
571-
{resendingInvitations.has(invitation.id)
572-
? 'Sending...'
573-
: resendCooldowns[invitation.id]
574-
? `Resend (${resendCooldowns[invitation.id]}s)`
575-
: 'Resend'}
576-
</Chip>
577-
<Chip
578-
onClick={() => handleCancelInvitation(invitation.id)}
579-
disabled={cancellingInvitations.has(invitation.id)}
580-
>
581-
{cancellingInvitations.has(invitation.id) ? 'Cancelling...' : 'Cancel'}
582-
</Chip>
583+
<DropdownMenu>
584+
<DropdownMenuTrigger asChild>
585+
<button
586+
type='button'
587+
aria-label='Invitation actions'
588+
className={chipVariants({ flush: true })}
589+
>
590+
<MoreHorizontal className='size-[14px] flex-shrink-0 text-[var(--text-icon)]' />
591+
</button>
592+
</DropdownMenuTrigger>
593+
<DropdownMenuContent align='end'>
594+
<DropdownMenuItem
595+
onSelect={() => handleResendInvitation(invitation.id, email)}
596+
disabled={
597+
resendingInvitations.has(invitation.id) ||
598+
(resendCooldowns[invitation.id] ?? 0) > 0
599+
}
600+
>
601+
{resendingInvitations.has(invitation.id)
602+
? 'Sending...'
603+
: resendCooldowns[invitation.id]
604+
? `Resend (${resendCooldowns[invitation.id]}s)`
605+
: 'Resend'}
606+
</DropdownMenuItem>
607+
<DropdownMenuItem
608+
className='text-[var(--text-error)]'
609+
onSelect={() => handleCancelInvitation(invitation.id)}
610+
disabled={cancellingInvitations.has(invitation.id)}
611+
>
612+
{cancellingInvitations.has(invitation.id)
613+
? 'Cancelling...'
614+
: 'Cancel'}
615+
</DropdownMenuItem>
616+
</DropdownMenuContent>
617+
</DropdownMenu>
583618
</div>
584619
</div>
585620
)
@@ -729,14 +764,30 @@ export function CredentialSets() {
729764
</span>
730765
</div>
731766
</div>
732-
<div className='flex items-center gap-2'>
733-
<Chip onClick={() => setViewingSet(set)}>Details</Chip>
734-
<Chip
735-
onClick={() => handleDeleteClick(set)}
736-
disabled={deletingSetIds.has(set.id)}
737-
>
738-
{deletingSetIds.has(set.id) ? 'Deleting...' : 'Delete'}
739-
</Chip>
767+
<div className='flex items-center gap-1'>
768+
<DropdownMenu>
769+
<DropdownMenuTrigger asChild>
770+
<button
771+
type='button'
772+
aria-label='Group actions'
773+
className={chipVariants({ flush: true })}
774+
>
775+
<MoreHorizontal className='size-[14px] flex-shrink-0 text-[var(--text-icon)]' />
776+
</button>
777+
</DropdownMenuTrigger>
778+
<DropdownMenuContent align='end'>
779+
<DropdownMenuItem onSelect={() => setViewingSet(set)}>
780+
Details
781+
</DropdownMenuItem>
782+
<DropdownMenuItem
783+
className='text-[var(--text-error)]'
784+
onSelect={() => handleDeleteClick(set)}
785+
disabled={deletingSetIds.has(set.id)}
786+
>
787+
Delete
788+
</DropdownMenuItem>
789+
</DropdownMenuContent>
790+
</DropdownMenu>
740791
</div>
741792
</div>
742793
))}

apps/sim/app/workspace/[workspaceId]/settings/components/custom-tools/custom-tools.tsx

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,18 @@ import { createLogger } from '@sim/logger'
55
import { getErrorMessage } from '@sim/utils/errors'
66
import { Plus } from 'lucide-react'
77
import { useParams } from 'next/navigation'
8-
import { Chip, ChipConfirmModal, ChipInput, Search } from '@/components/emcn'
8+
import {
9+
Chip,
10+
ChipConfirmModal,
11+
ChipInput,
12+
chipVariants,
13+
DropdownMenu,
14+
DropdownMenuContent,
15+
DropdownMenuItem,
16+
DropdownMenuTrigger,
17+
MoreHorizontal,
18+
Search,
19+
} from '@/components/emcn'
920
import { CustomToolModal } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/custom-tool-modal/custom-tool-modal'
1021
import { useCustomTools, useDeleteCustomTool } from '@/hooks/queries/custom-tools'
1122

@@ -134,14 +145,30 @@ export function CustomTools() {
134145
</p>
135146
)}
136147
</div>
137-
<div className='flex flex-shrink-0 items-center gap-2'>
138-
<Chip onClick={() => setEditingTool(tool.id)}>Edit</Chip>
139-
<Chip
140-
onClick={() => handleDeleteClick(tool.id)}
141-
disabled={deletingTools.has(tool.id)}
142-
>
143-
{deletingTools.has(tool.id) ? 'Deleting...' : 'Delete'}
144-
</Chip>
148+
<div className='flex flex-shrink-0 items-center gap-1'>
149+
<DropdownMenu>
150+
<DropdownMenuTrigger asChild>
151+
<button
152+
type='button'
153+
aria-label='Tool actions'
154+
className={chipVariants({ flush: true })}
155+
>
156+
<MoreHorizontal className='size-[14px] flex-shrink-0 text-[var(--text-icon)]' />
157+
</button>
158+
</DropdownMenuTrigger>
159+
<DropdownMenuContent align='end'>
160+
<DropdownMenuItem onSelect={() => setEditingTool(tool.id)}>
161+
Edit
162+
</DropdownMenuItem>
163+
<DropdownMenuItem
164+
className='text-[var(--text-error)]'
165+
onSelect={() => handleDeleteClick(tool.id)}
166+
disabled={deletingTools.has(tool.id)}
167+
>
168+
Delete
169+
</DropdownMenuItem>
170+
</DropdownMenuContent>
171+
</DropdownMenu>
145172
</div>
146173
</div>
147174
))}

0 commit comments

Comments
 (0)