22
33import { useCallback , useEffect , useMemo , useRef , useState } from 'react'
44import { createLogger } from '@sim/logger'
5- import { Check , Clipboard , Key , Search } from 'lucide-react'
5+ import { Check , Clipboard , Info , Key , Search , Shield , UserPlus } from 'lucide-react'
66import { useParams , useRouter } from 'next/navigation'
77import {
88 Avatar ,
@@ -1188,44 +1188,78 @@ export function CredentialsManager() {
11881188 </ div >
11891189 ) }
11901190
1191- < div className = 'flex flex-col gap-1.5 border-[var(--border)] border-t pt-4' >
1192- < Label > Members ({ activeMembers . length } )</ Label >
1191+ < div className = 'flex flex-col gap-0 overflow-hidden rounded-lg border border-[var(--border)]' >
1192+ { /* Header */ }
1193+ < div className = 'flex items-start gap-3 border-b border-[var(--border)] bg-[var(--surface-1)] px-4 py-3' >
1194+ < div className = 'flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-md bg-[var(--surface-4)]' >
1195+ < Shield className = 'h-4 w-4 text-[var(--text-secondary)]' />
1196+ </ div >
1197+ < div className = 'min-w-0 flex-1' >
1198+ < div className = 'flex items-center gap-2' >
1199+ < p className = 'font-medium text-[var(--text-primary)] text-sm' >
1200+ Access Control
1201+ </ p >
1202+ < Badge variant = 'gray-secondary' size = 'sm' >
1203+ { activeMembers . length } { activeMembers . length === 1 ? 'member' : 'members' }
1204+ </ Badge >
1205+ </ div >
1206+ < p className = 'mt-0.5 text-[var(--text-tertiary)] text-caption' >
1207+ Only workspace members listed below can view and use this secret in their
1208+ workflows. Admins can manage access; members can only use the secret.
1209+ </ p >
1210+ </ div >
1211+ </ div >
11931212
1213+ { /* Member list */ }
11941214 { membersLoading ? (
1195- < div className = 'flex flex-col gap-2' >
1196- < Skeleton className = 'h-[44px] w-full rounded-lg' />
1197- < Skeleton className = 'h-[44px] w-full rounded-lg' />
1215+ < div className = 'flex flex-col gap-0 px-4' >
1216+ < div className = 'flex items-center gap-3 py-3' >
1217+ < Skeleton className = 'h-8 w-8 rounded-full' />
1218+ < div className = 'flex-1' >
1219+ < Skeleton className = 'mb-1 h-3.5 w-[120px]' />
1220+ < Skeleton className = 'h-3 w-[180px]' />
1221+ </ div >
1222+ < Skeleton className = 'h-7 w-[80px] rounded-md' />
1223+ </ div >
1224+ < div className = 'flex items-center gap-3 border-t border-[var(--border)] py-3' >
1225+ < Skeleton className = 'h-8 w-8 rounded-full' />
1226+ < div className = 'flex-1' >
1227+ < Skeleton className = 'mb-1 h-3.5 w-[100px]' />
1228+ < Skeleton className = 'h-3 w-[160px]' />
1229+ </ div >
1230+ < Skeleton className = 'h-7 w-[80px] rounded-md' />
1231+ </ div >
11981232 </ div >
11991233 ) : (
1200- < div className = 'flex flex-col gap-2 ' >
1201- { activeMembers . map ( ( member ) => (
1234+ < div className = 'flex flex-col' >
1235+ { activeMembers . map ( ( member , index ) => (
12021236 < div
12031237 key = { member . id }
1204- className = 'grid grid-cols-[1fr_120px_72px] items-center gap-2'
1238+ className = { `flex items-center gap-3 px-4 py-2.5 ${
1239+ index > 0 ? 'border-t border-[var(--border)]' : ''
1240+ } `}
12051241 >
1206- < div className = 'flex min-w-0 items-center gap-2.5' >
1207- < Avatar className = 'h-8 w-8 flex-shrink-0' >
1208- < AvatarFallback
1209- style = { {
1210- background : getUserColor ( member . userId || member . userEmail || '' ) ,
1211- } }
1212- className = 'border-0 text-small text-white'
1213- >
1214- { ( member . userName || member . userEmail || '?' ) . charAt ( 0 ) . toUpperCase ( ) }
1215- </ AvatarFallback >
1216- </ Avatar >
1217- < div className = 'min-w-0' >
1218- < p className = 'truncate font-medium text-[var(--text-primary)] text-sm' >
1219- { member . userName || member . userEmail || member . userId }
1220- </ p >
1221- < p className = 'truncate text-[var(--text-tertiary)] text-caption' >
1222- { member . userEmail || member . userId }
1223- </ p >
1224- </ div >
1242+ < Avatar className = 'h-8 w-8 flex-shrink-0' >
1243+ < AvatarFallback
1244+ style = { {
1245+ background : getUserColor ( member . userId || member . userEmail || '' ) ,
1246+ } }
1247+ className = 'border-0 text-small text-white'
1248+ >
1249+ { ( member . userName || member . userEmail || '?' ) . charAt ( 0 ) . toUpperCase ( ) }
1250+ </ AvatarFallback >
1251+ </ Avatar >
1252+ < div className = 'min-w-0 flex-1' >
1253+ < p className = 'truncate font-medium text-[var(--text-primary)] text-sm' >
1254+ { member . userName || member . userEmail || member . userId }
1255+ </ p >
1256+ < p className = 'truncate text-[var(--text-tertiary)] text-caption' >
1257+ { member . userEmail || member . userId }
1258+ </ p >
12251259 </ div >
12261260
12271261 { isSelectedAdmin ? (
1228- < >
1262+ < div className = 'flex flex-shrink-0 items-center gap-1.5' >
12291263 < Combobox
12301264 options = { ROLE_OPTIONS . map ( ( option ) => ( {
12311265 value : option . value ,
@@ -1250,55 +1284,85 @@ export function CredentialsManager() {
12501284 variant = 'ghost'
12511285 onClick = { ( ) => handleRemoveMember ( member . userId ) }
12521286 disabled = { member . role === 'admin' && adminMemberCount <= 1 }
1253- className = 'w-full justify-end '
1287+ className = 'h-7 px-2 text-[var(--text-tertiary)] text-caption hover-hover:text-[var(--text-error)] '
12541288 >
12551289 Remove
12561290 </ Button >
1257- </ >
1291+ </ div >
12581292 ) : (
1259- < >
1260- < Badge variant = 'gray-secondary' > { member . role } </ Badge >
1261- < div />
1262- </ >
1293+ < Badge variant = 'gray-secondary' size = 'sm' >
1294+ { member . role === 'admin' ? 'Admin' : 'Member' }
1295+ </ Badge >
12631296 ) }
12641297 </ div >
12651298 ) ) }
1299+
1300+ { /* Add member row */ }
12661301 { isSelectedAdmin && (
1267- < div className = 'grid grid-cols-[1fr_120px_72px] items-center gap-2 border-[var(--border)] border-t pt-2' >
1268- < Combobox
1269- options = { workspaceUserOptions }
1270- value = {
1271- workspaceUserOptions . find ( ( option ) => option . value === memberUserId )
1272- ?. label || ''
1273- }
1274- selectedValue = { memberUserId }
1275- onChange = { setMemberUserId }
1276- placeholder = 'Add member...'
1277- searchable
1278- searchPlaceholder = 'Search members...'
1279- size = 'sm'
1280- />
1281- < Combobox
1282- options = { ROLE_OPTIONS . map ( ( option ) => ( {
1283- value : option . value ,
1284- label : option . label ,
1285- } ) ) }
1286- value = {
1287- ROLE_OPTIONS . find ( ( option ) => option . value === memberRole ) ?. label || ''
1288- }
1289- selectedValue = { memberRole }
1290- onChange = { ( value ) => setMemberRole ( value as WorkspaceCredentialRole ) }
1291- placeholder = 'Role'
1292- size = 'sm'
1293- />
1294- < Button
1295- variant = 'ghost'
1296- onClick = { handleAddMember }
1297- disabled = { ! memberUserId || upsertMember . isPending }
1298- className = 'w-full justify-end'
1299- >
1300- Add
1301- </ Button >
1302+ < div className = 'flex flex-col gap-2 border-t border-[var(--border)] bg-[var(--surface-1)] px-4 py-3' >
1303+ < div className = 'flex items-center gap-1.5' >
1304+ < UserPlus className = 'h-3.5 w-3.5 text-[var(--text-tertiary)]' />
1305+ < p className = 'text-[var(--text-secondary)] text-caption font-medium' >
1306+ Grant access to a workspace member
1307+ </ p >
1308+ </ div >
1309+ < div className = 'flex items-center gap-2' >
1310+ < div className = 'flex-1' >
1311+ < Combobox
1312+ options = { workspaceUserOptions }
1313+ value = {
1314+ workspaceUserOptions . find ( ( option ) => option . value === memberUserId )
1315+ ?. label || ''
1316+ }
1317+ selectedValue = { memberUserId }
1318+ onChange = { setMemberUserId }
1319+ placeholder = 'Select workspace member...'
1320+ searchable
1321+ searchPlaceholder = 'Search workspace members...'
1322+ emptyMessage = 'No workspace members available. Invite members to the workspace first.'
1323+ size = 'sm'
1324+ />
1325+ </ div >
1326+ < div className = 'w-[110px] flex-shrink-0' >
1327+ < Combobox
1328+ options = { ROLE_OPTIONS . map ( ( option ) => ( {
1329+ value : option . value ,
1330+ label : option . label ,
1331+ } ) ) }
1332+ value = {
1333+ ROLE_OPTIONS . find ( ( option ) => option . value === memberRole ) ?. label ||
1334+ ''
1335+ }
1336+ selectedValue = { memberRole }
1337+ onChange = { ( value ) => setMemberRole ( value as WorkspaceCredentialRole ) }
1338+ placeholder = 'Role'
1339+ size = 'sm'
1340+ />
1341+ </ div >
1342+ < Button
1343+ variant = 'primary'
1344+ onClick = { handleAddMember }
1345+ disabled = { ! memberUserId || upsertMember . isPending }
1346+ className = 'h-7 flex-shrink-0 px-3'
1347+ >
1348+ { upsertMember . isPending ? 'Adding...' : 'Add' }
1349+ </ Button >
1350+ </ div >
1351+ < p className = 'flex items-start gap-1 text-[var(--text-muted)] text-[11px]' >
1352+ < Info className = 'mt-0.5 h-3 w-3 flex-shrink-0' />
1353+ Only members of this workspace appear here. To add someone new, invite
1354+ them to the workspace first.
1355+ </ p >
1356+ </ div >
1357+ ) }
1358+
1359+ { /* Non-admin notice */ }
1360+ { ! isSelectedAdmin && (
1361+ < div className = 'flex items-center gap-2 border-t border-[var(--border)] bg-[var(--surface-1)] px-4 py-2.5' >
1362+ < Info className = 'h-3.5 w-3.5 flex-shrink-0 text-[var(--text-muted)]' />
1363+ < p className = 'text-[var(--text-muted)] text-caption' >
1364+ Only admins of this secret can manage access control.
1365+ </ p >
13021366 </ div >
13031367 ) }
13041368 </ div >
@@ -1307,7 +1371,7 @@ export function CredentialsManager() {
13071371 </ div >
13081372 </ div >
13091373
1310- < div className = 'mt-auto flex items-center justify-end border-[var(--border)] border-t pt-2.5' >
1374+ < div className = 'mt-auto flex items-center justify-end pt-2.5' >
13111375 < div className = 'flex items-center gap-2' >
13121376 < Button onClick = { handleBackAttempt } variant = 'default' >
13131377 Back
0 commit comments