1
1
import { useFormikContext } from "formik" ;
2
- import { useCallback , useEffect , useState , useMemo } from "react" ;
2
+ import { useCallback , useState , useEffect } from "react" ;
3
3
import { Switch } from "../../../ui/FormControls/Switch" ;
4
- import FormikCheckbox from "../../Forms/Formik/FormikCheckbox" ;
5
4
import { OBJECTS , getActionsForObject } from "../tokenUtils" ;
6
5
import { TokenFormValues } from "./CreateTokenForm" ;
7
6
8
7
const ScopeOptions = [ "Read" , "Write" , "Admin" , "Custom" ] as const ;
9
8
type ScopeType = ( typeof ScopeOptions ) [ number ] ;
10
9
10
+ const ObjectPermissionOptions = [ "None" , "Read" , "Write" , "Admin" ] as const ;
11
+ const McpPermissionOptions = [ "None" , "Admin" ] as const ;
12
+ type ObjectPermissionType = ( typeof ObjectPermissionOptions ) [ number ] ;
13
+ type McpPermissionType = ( typeof McpPermissionOptions ) [ number ] ;
14
+
15
+ // Component for individual object permission selection
16
+ type ObjectPermissionSwitchProps = {
17
+ object : string ;
18
+ isMcpSetup ?: boolean ;
19
+ } ;
20
+
21
+ function ObjectPermissionSwitch ( {
22
+ object,
23
+ isMcpSetup
24
+ } : ObjectPermissionSwitchProps ) {
25
+ const { setFieldValue, values } = useFormikContext < TokenFormValues > ( ) ;
26
+
27
+ // Determine current permission level for this object
28
+ const getCurrentPermission = ( ) : ObjectPermissionType | McpPermissionType => {
29
+ const objectActions = values . objectActions ;
30
+
31
+ // Special handling for MCP setup mode
32
+ if ( isMcpSetup && object === "mcp" ) {
33
+ return "Admin" ; // Pre-select Admin (which maps to mcp:*) for MCP setup
34
+ }
35
+
36
+ if ( object === "mcp" ) {
37
+ return objectActions [ "mcp:*" ] ? "Admin" : "None" ;
38
+ }
39
+
40
+ if ( object === "playbooks" ) {
41
+ const playbookActions = [
42
+ "playbook:run" ,
43
+ "playbook:approve" ,
44
+ "playbook:cancel"
45
+ ] ;
46
+ const hasAllPlaybookActions = playbookActions . every (
47
+ ( action ) => objectActions [ `${ object } :${ action } ` ]
48
+ ) ;
49
+ const hasCrud = [ "read" , "create" , "update" , "delete" ] . every (
50
+ ( action ) => objectActions [ `${ object } :${ action } ` ]
51
+ ) ;
52
+
53
+ if ( hasCrud && hasAllPlaybookActions ) return "Admin" ;
54
+ if ( objectActions [ `${ object } :read` ] && objectActions [ `${ object } :create` ] )
55
+ return "Write" ;
56
+ if ( objectActions [ `${ object } :read` ] ) return "Read" ;
57
+ return "None" ;
58
+ }
59
+
60
+ // For other objects (non-mcp, non-playbooks)
61
+ const hasCrud = [ "read" , "create" , "update" , "delete" ] . every (
62
+ ( action ) => objectActions [ `${ object } :${ action } ` ]
63
+ ) ;
64
+ if ( hasCrud ) return "Admin" ;
65
+ if ( objectActions [ `${ object } :read` ] && objectActions [ `${ object } :create` ] )
66
+ return "Write" ;
67
+ if ( objectActions [ `${ object } :read` ] ) return "Read" ;
68
+ return "None" ;
69
+ } ;
70
+
71
+ const [ selectedPermission , setSelectedPermission ] = useState ( ( ) =>
72
+ getCurrentPermission ( )
73
+ ) ;
74
+
75
+ // Handle initial MCP setup - apply the mcp:* permission when component mounts
76
+ useEffect ( ( ) => {
77
+ if ( isMcpSetup && object === "mcp" && selectedPermission === "Admin" ) {
78
+ setFieldValue (
79
+ "objectActions" ,
80
+ ( currentObjectActions : Record < string , boolean > ) => {
81
+ const newObjectActions = { ...currentObjectActions } ;
82
+ newObjectActions [ "mcp:*" ] = true ;
83
+ return newObjectActions ;
84
+ }
85
+ ) ;
86
+ }
87
+ } , [ isMcpSetup , object , selectedPermission , setFieldValue ] ) ;
88
+
89
+ const handlePermissionChange = useCallback (
90
+ ( permission : string ) => {
91
+ const newPermission = permission as
92
+ | ObjectPermissionType
93
+ | McpPermissionType ;
94
+ setSelectedPermission ( newPermission ) ;
95
+
96
+ // Update the objectActions in form state
97
+ setFieldValue (
98
+ "objectActions" ,
99
+ ( currentObjectActions : Record < string , boolean > ) => {
100
+ const newObjectActions = { ...currentObjectActions } ;
101
+ const actions = getActionsForObject ( object ) ;
102
+
103
+ // Reset all actions for this object first
104
+ actions . forEach ( ( action ) => {
105
+ newObjectActions [ `${ object } :${ action } ` ] = false ;
106
+ } ) ;
107
+
108
+ // Apply the selected permission level
109
+ if ( newPermission !== "None" ) {
110
+ if ( object === "mcp" ) {
111
+ // MCP only has Admin option (maps to *)
112
+ if ( newPermission === "Admin" ) {
113
+ newObjectActions [ "mcp:*" ] = true ;
114
+ }
115
+ } else if ( object === "playbooks" ) {
116
+ if ( newPermission === "Read" ) {
117
+ newObjectActions [ `${ object } :read` ] = true ;
118
+ } else if ( newPermission === "Write" ) {
119
+ newObjectActions [ `${ object } :read` ] = true ;
120
+ newObjectActions [ `${ object } :create` ] = true ;
121
+ } else if ( newPermission === "Admin" ) {
122
+ // For playbooks Admin: CRUD + all 3 specific playbook actions
123
+ [ "read" , "create" , "update" , "delete" ] . forEach ( ( action ) => {
124
+ newObjectActions [ `${ object } :${ action } ` ] = true ;
125
+ } ) ;
126
+ [ "playbook:run" , "playbook:approve" , "playbook:cancel" ] . forEach (
127
+ ( action ) => {
128
+ newObjectActions [ `${ object } :${ action } ` ] = true ;
129
+ }
130
+ ) ;
131
+ }
132
+ } else {
133
+ // For other objects
134
+ if ( newPermission === "Read" ) {
135
+ newObjectActions [ `${ object } :read` ] = true ;
136
+ } else if ( newPermission === "Write" ) {
137
+ newObjectActions [ `${ object } :read` ] = true ;
138
+ newObjectActions [ `${ object } :create` ] = true ;
139
+ } else if ( newPermission === "Admin" ) {
140
+ [ "read" , "create" , "update" , "delete" ] . forEach ( ( action ) => {
141
+ newObjectActions [ `${ object } :${ action } ` ] = true ;
142
+ } ) ;
143
+ }
144
+ }
145
+ }
146
+
147
+ return newObjectActions ;
148
+ }
149
+ ) ;
150
+ } ,
151
+ [ object , setFieldValue ]
152
+ ) ;
153
+
154
+ // Get the appropriate options based on object type
155
+ const getOptionsForObject = ( ) => {
156
+ if ( object === "mcp" ) {
157
+ return McpPermissionOptions as unknown as string [ ] ;
158
+ }
159
+ return ObjectPermissionOptions as unknown as string [ ] ;
160
+ } ;
161
+
162
+ return (
163
+ < div className = "flex flex-row items-center space-x-4" >
164
+ < label className = "w-20 flex-shrink-0 text-sm font-medium text-gray-800" >
165
+ { object }
166
+ </ label >
167
+ < div className = "flex flex-row" >
168
+ < Switch
169
+ options = { getOptionsForObject ( ) }
170
+ defaultValue = "None"
171
+ value = { selectedPermission as string }
172
+ onChange = { handlePermissionChange }
173
+ />
174
+ </ div >
175
+ </ div >
176
+ ) ;
177
+ }
178
+
11
179
// Pre-calculate scope mappings outside component to avoid recalculation
12
180
const SCOPE_MAPPINGS = {
13
181
Read : ( ( ) => {
@@ -55,23 +223,14 @@ const SCOPE_MAPPINGS = {
55
223
} ) ( )
56
224
} ;
57
225
58
- // Pre-calculate object actions to avoid function calls in render
59
- const OBJECT_ACTIONS = OBJECTS . reduce (
60
- ( acc , object ) => {
61
- acc [ object ] = getActionsForObject ( object ) ;
62
- return acc ;
63
- } ,
64
- { } as Record < string , string [ ] >
65
- ) ;
66
-
67
226
type TokenScopeFieldsGroupProps = {
68
227
isMcpSetup ?: boolean ;
69
228
} ;
70
229
71
230
export default function TokenScopeFieldsGroup ( {
72
231
isMcpSetup = false
73
232
} : TokenScopeFieldsGroupProps ) {
74
- const { setFieldValue, values } = useFormikContext < TokenFormValues > ( ) ;
233
+ const { setFieldValue } = useFormikContext < TokenFormValues > ( ) ;
75
234
76
235
const [ selectedScope , setSelectedScope ] = useState < ScopeType > ( ( ) => {
77
236
if ( isMcpSetup ) {
@@ -80,41 +239,36 @@ export default function TokenScopeFieldsGroup({
80
239
return "Read" ;
81
240
} ) ;
82
241
83
- // Memoize the keys from objectActions to avoid dependency on the whole object
84
- const objectActionKeys = useMemo ( ( ) => {
85
- return Object . keys ( values . objectActions ) ;
86
- } , [ values . objectActions ] ) ;
242
+ const handleScopeChange = useCallback (
243
+ ( scope : string ) => {
244
+ const newScope = scope as ScopeType ;
245
+ setSelectedScope ( newScope ) ;
87
246
88
- const applyScopePreset = useCallback (
89
- ( scope : ScopeType ) => {
90
- let newObjectActions : Record < string , boolean > = { } ;
247
+ if ( newScope !== "Custom" ) {
248
+ // Use setFieldValue with a function to get current values
249
+ setFieldValue (
250
+ "objectActions" ,
251
+ ( currentObjectActions : Record < string , boolean > ) => {
252
+ const newObjectActions : Record < string , boolean > = { } ;
91
253
92
- // Reset all scopes first
93
- objectActionKeys . forEach ( ( key ) => {
94
- newObjectActions [ key ] = false ;
95
- } ) ;
254
+ // Reset all scopes first
255
+ Object . keys ( currentObjectActions ) . forEach ( ( key ) => {
256
+ newObjectActions [ key ] = false ;
257
+ } ) ;
96
258
97
- if ( scope !== "Custom" ) {
98
- // Use pre-calculated scope mappings
99
- newObjectActions = { ...newObjectActions , ...SCOPE_MAPPINGS [ scope ] } ;
100
- } else if ( isMcpSetup ) {
101
- // Pre-select MCP * action for MCP setup
102
- newObjectActions [ "mcp:*" ] = true ;
103
- }
259
+ // Apply the selected preset
260
+ Object . assign ( newObjectActions , SCOPE_MAPPINGS [ newScope ] ) ;
104
261
105
- setFieldValue ( "objectActions" , newObjectActions ) ;
262
+ return newObjectActions ;
263
+ }
264
+ ) ;
265
+ }
266
+ // Note: For Custom mode, individual ObjectPermissionSwitch components handle their own state
267
+ // For MCP setup, the individual ObjectPermissionSwitch for mcp will handle the pre-selection
106
268
} ,
107
- [ objectActionKeys , setFieldValue , isMcpSetup ]
269
+ [ setFieldValue ]
108
270
) ;
109
271
110
- useEffect ( ( ) => {
111
- applyScopePreset ( selectedScope ) ;
112
- } , [ selectedScope , applyScopePreset ] ) ;
113
-
114
- const handleScopeChange = useCallback ( ( scope : string ) => {
115
- setSelectedScope ( scope as ScopeType ) ;
116
- } , [ ] ) ;
117
-
118
272
return (
119
273
< div className = "space-y-3" >
120
274
< div className = "text-sm font-medium text-gray-700" >
@@ -125,7 +279,6 @@ export default function TokenScopeFieldsGroup({
125
279
</ div >
126
280
127
281
< div className = "flex flex-col space-y-2" >
128
- < label className = "text-sm font-semibold" > Permission Level</ label >
129
282
< div className = "flex w-full flex-row" >
130
283
< Switch
131
284
options = { ScopeOptions as unknown as string [ ] }
@@ -139,49 +292,14 @@ export default function TokenScopeFieldsGroup({
139
292
{ selectedScope === "Custom" && (
140
293
< div className = "max-h-64 space-y-4 overflow-y-auto rounded-md border bg-gray-50 p-4" >
141
294
{ OBJECTS . map ( ( object ) => (
142
- < div key = { object } className = "space-y-2" >
143
- < div className = "text-sm font-medium text-gray-800" > { object } </ div >
144
- < div className = "grid grid-cols-4 gap-2 pl-4" >
145
- { OBJECT_ACTIONS [ object ] . map ( ( action ) => {
146
- const scopeKey = `${ object } :${ action } ` ;
147
- return (
148
- < FormikCheckbox
149
- key = { scopeKey }
150
- name = { `objectActions.${ scopeKey } ` }
151
- label = { action }
152
- labelClassName = "text-sm font-normal text-gray-600"
153
- inline = { true }
154
- />
155
- ) ;
156
- } ) }
157
- </ div >
158
- </ div >
295
+ < ObjectPermissionSwitch
296
+ key = { object }
297
+ object = { object }
298
+ isMcpSetup = { isMcpSetup }
299
+ />
159
300
) ) }
160
301
</ div >
161
302
) }
162
-
163
- { selectedScope !== "Custom" && (
164
- < div className = "rounded-md border bg-blue-50 p-3" >
165
- < div className = "text-sm text-blue-800" >
166
- < strong > { selectedScope } </ strong > permissions selected:
167
- < ul className = "mt-1 list-inside list-disc text-xs" >
168
- { selectedScope === "Read" && < li > Read access to all objects</ li > }
169
- { selectedScope === "Write" && (
170
- < >
171
- < li > Read access to all objects</ li >
172
- < li > Create access to all objects</ li >
173
- </ >
174
- ) }
175
- { selectedScope === "Admin" && (
176
- < >
177
- < li > Full CRUD access to all objects</ li >
178
- < li > Playbook execution permissions</ li >
179
- </ >
180
- ) }
181
- </ ul >
182
- </ div >
183
- </ div >
184
- ) }
185
303
</ div >
186
304
) ;
187
305
}
0 commit comments