Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ export function Admin() {
)}
</div>

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

<div className='flex flex-col gap-2'>
<p className='text-[var(--text-secondary)] text-sm'>
Expand Down Expand Up @@ -231,10 +231,10 @@ export function Admin() {
)}
</div>

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

<div className='flex flex-col gap-3'>
<p className='font-medium text-[var(--text-primary)] text-sm'>User Management</p>
<p className='font-medium text-[var(--text-muted)] text-small'>User Management</p>
<div className='flex gap-2'>
<ChipInput
icon={Search}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,21 @@ import { createLogger } from '@sim/logger'
import { formatDate } from '@sim/utils/formatting'
import { Info, Plus } from 'lucide-react'
import { useParams } from 'next/navigation'
import { Chip, ChipConfirmModal, ChipInput, Search, Switch, Tooltip } from '@/components/emcn'
import {
Chip,
ChipConfirmModal,
ChipInput,
chipVariants,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
MoreHorizontal,
Search,
Switch,
Tooltip,
toast,
} from '@/components/emcn'
import { useSession } from '@/lib/auth/auth-client'
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
import { SettingsSection } from '@/app/workspace/[workspaceId]/settings/components/settings-section/settings-section'
Expand All @@ -20,6 +34,51 @@ import { CreateApiKeyModal } from './components'

const logger = createLogger('ApiKeys')

/** Copies an API key's name and confirms with a toast. */
function copyKeyName(name: string) {
void navigator.clipboard.writeText(name)
toast.success('Copied name to clipboard')
}

interface ApiKeyRowMenuProps {
keyName: string
onDelete: () => void
/** When false, the Delete item is disabled (e.g. non-admins on workspace keys). */
canDelete?: boolean
}

/**
* Trailing `...` actions menu for an API key row. Mirrors the Secrets /
* Teammates row menu so the settings experience is consistent.
*/
function ApiKeyRowMenu({ keyName, onDelete, canDelete = true }: ApiKeyRowMenuProps) {
return (
<div className='flex-shrink-0'>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
type='button'
aria-label='API key actions'
className={chipVariants({ flush: true })}
>
<MoreHorizontal className='size-[14px] flex-shrink-0 text-[var(--text-icon)]' />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align='end'>
<DropdownMenuItem onSelect={() => copyKeyName(keyName)}>Copy name</DropdownMenuItem>
<DropdownMenuItem
className='text-[var(--text-error)]'
onSelect={onDelete}
disabled={!canDelete}
>
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
)
}

export function ApiKeys() {
const { data: session } = useSession()
const userId = session?.user?.id
Expand Down Expand Up @@ -164,16 +223,14 @@ export function ApiKeys() {
{key.displayKey || key.key}
</p>
</div>
<Chip
className='flex-shrink-0'
onClick={() => {
<ApiKeyRowMenu
keyName={key.name}
onDelete={() => {
setDeleteKey(key)
setShowDeleteDialog(true)
}}
disabled={!canManageWorkspaceKeys}
>
Delete
</Chip>
canDelete={canManageWorkspaceKeys}
/>
</div>
))}
</div>
Expand All @@ -197,16 +254,14 @@ export function ApiKeys() {
{key.displayKey || key.key}
</p>
</div>
<Chip
className='flex-shrink-0'
onClick={() => {
<ApiKeyRowMenu
keyName={key.name}
onDelete={() => {
setDeleteKey(key)
setShowDeleteDialog(true)
}}
disabled={!canManageWorkspaceKeys}
>
Delete
</Chip>
canDelete={canManageWorkspaceKeys}
/>
</div>
))}
</div>
Expand Down Expand Up @@ -235,15 +290,13 @@ export function ApiKeys() {
{key.displayKey || key.key}
</p>
</div>
<Chip
className='flex-shrink-0'
onClick={() => {
<ApiKeyRowMenu
keyName={key.name}
onDelete={() => {
setDeleteKey(key)
setShowDeleteDialog(true)
}}
>
Delete
</Chip>
/>
</div>
{isConflict && (
<div className='text-[var(--text-error)] text-small leading-tight'>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,13 @@ import {
ChipModalField,
ChipModalFooter,
ChipModalHeader,
chipVariants,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
type FileInputOptions,
MoreHorizontal,
Search,
TagInput,
type TagItem,
Expand Down Expand Up @@ -516,13 +522,26 @@ export function CredentialSets() {
</div>

<div className='ml-4 flex items-center gap-1'>
<Chip
variant='destructive'
onClick={() => handleRemoveMember(member.id)}
disabled={removeMember.isPending}
>
Remove
</Chip>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
type='button'
aria-label='Member actions'
className={chipVariants({ flush: true })}
>
<MoreHorizontal className='size-[14px] flex-shrink-0 text-[var(--text-icon)]' />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align='end'>
<DropdownMenuItem
className='text-[var(--text-error)]'
onSelect={() => handleRemoveMember(member.id)}
disabled={removeMember.isPending}
>
Remove
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
)
Expand Down Expand Up @@ -561,25 +580,41 @@ export function CredentialSets() {
</div>

<div className='ml-4 flex items-center gap-1'>
<Chip
onClick={() => handleResendInvitation(invitation.id, email)}
disabled={
resendingInvitations.has(invitation.id) ||
(resendCooldowns[invitation.id] ?? 0) > 0
}
>
{resendingInvitations.has(invitation.id)
? 'Sending...'
: resendCooldowns[invitation.id]
? `Resend (${resendCooldowns[invitation.id]}s)`
: 'Resend'}
</Chip>
<Chip
onClick={() => handleCancelInvitation(invitation.id)}
disabled={cancellingInvitations.has(invitation.id)}
>
{cancellingInvitations.has(invitation.id) ? 'Cancelling...' : 'Cancel'}
</Chip>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
type='button'
aria-label='Invitation actions'
className={chipVariants({ flush: true })}
>
<MoreHorizontal className='size-[14px] flex-shrink-0 text-[var(--text-icon)]' />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align='end'>
<DropdownMenuItem
onSelect={() => handleResendInvitation(invitation.id, email)}
disabled={
resendingInvitations.has(invitation.id) ||
(resendCooldowns[invitation.id] ?? 0) > 0
}
>
{resendingInvitations.has(invitation.id)
? 'Sending...'
: resendCooldowns[invitation.id]
? `Resend (${resendCooldowns[invitation.id]}s)`
: 'Resend'}
</DropdownMenuItem>
<DropdownMenuItem
className='text-[var(--text-error)]'
onSelect={() => handleCancelInvitation(invitation.id)}
disabled={cancellingInvitations.has(invitation.id)}
>
{cancellingInvitations.has(invitation.id)
? 'Cancelling...'
: 'Cancel'}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
)
Expand Down Expand Up @@ -729,14 +764,30 @@ export function CredentialSets() {
</span>
</div>
</div>
<div className='flex items-center gap-2'>
<Chip onClick={() => setViewingSet(set)}>Details</Chip>
<Chip
onClick={() => handleDeleteClick(set)}
disabled={deletingSetIds.has(set.id)}
>
{deletingSetIds.has(set.id) ? 'Deleting...' : 'Delete'}
</Chip>
<div className='flex items-center gap-1'>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
type='button'
aria-label='Group actions'
className={chipVariants({ flush: true })}
>
<MoreHorizontal className='size-[14px] flex-shrink-0 text-[var(--text-icon)]' />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align='end'>
<DropdownMenuItem onSelect={() => setViewingSet(set)}>
Details
</DropdownMenuItem>
<DropdownMenuItem
className='text-[var(--text-error)]'
onSelect={() => handleDeleteClick(set)}
disabled={deletingSetIds.has(set.id)}
>
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
))}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,18 @@ import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { Plus } from 'lucide-react'
import { useParams } from 'next/navigation'
import { Chip, ChipConfirmModal, ChipInput, Search } from '@/components/emcn'
import {
Chip,
ChipConfirmModal,
ChipInput,
chipVariants,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
MoreHorizontal,
Search,
} from '@/components/emcn'
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'
import { useCustomTools, useDeleteCustomTool } from '@/hooks/queries/custom-tools'

Expand Down Expand Up @@ -134,14 +145,30 @@ export function CustomTools() {
</p>
)}
</div>
<div className='flex flex-shrink-0 items-center gap-2'>
<Chip onClick={() => setEditingTool(tool.id)}>Edit</Chip>
<Chip
onClick={() => handleDeleteClick(tool.id)}
disabled={deletingTools.has(tool.id)}
>
{deletingTools.has(tool.id) ? 'Deleting...' : 'Delete'}
</Chip>
<div className='flex flex-shrink-0 items-center gap-1'>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
type='button'
aria-label='Tool actions'
className={chipVariants({ flush: true })}
>
<MoreHorizontal className='size-[14px] flex-shrink-0 text-[var(--text-icon)]' />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align='end'>
<DropdownMenuItem onSelect={() => setEditingTool(tool.id)}>
Edit
</DropdownMenuItem>
<DropdownMenuItem
className='text-[var(--text-error)]'
onSelect={() => handleDeleteClick(tool.id)}
disabled={deletingTools.has(tool.id)}
>
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
))}
Expand Down
Loading
Loading