11import { useCallback , useEffect , useMemo , useRef , useState } from "react" ;
2- import { useAtomSet , useAtomValue , Result } from "@effect-atom/atom-react" ;
2+ import { useAtomSet , useAtomValue , useAtomRefresh , Result } from "@effect-atom/atom-react" ;
33
44import {
55 useScope ,
66 SecretPicker ,
77 secretsAtom ,
8+ setSecret ,
89 type SecretPickerSecret ,
910} from "@executor/react" ;
11+ import { SecretId } from "@executor/sdk" ;
1012import { Badge } from "@executor/ui/components/badge" ;
1113import { Button } from "@executor/ui/components/button" ;
1214import {
@@ -24,6 +26,159 @@ import {
2426 startGoogleDiscoveryOAuth ,
2527} from "./atoms" ;
2628
29+ // ---------------------------------------------------------------------------
30+ // Inline secret creation
31+ // ---------------------------------------------------------------------------
32+
33+ function InlineCreateSecret ( props : {
34+ headerName : string ;
35+ suggestedId : string ;
36+ onCreated : ( secretId : string ) => void ;
37+ onCancel : ( ) => void ;
38+ } ) {
39+ const [ secretId , setSecretIdValue ] = useState ( props . suggestedId ) ;
40+ const [ secretName , setSecretName ] = useState ( props . headerName ) ;
41+ const [ secretValue , setSecretValue ] = useState ( "" ) ;
42+ const [ saving , setSaving ] = useState ( false ) ;
43+ const [ error , setError ] = useState < string | null > ( null ) ;
44+ const scopeId = useScope ( ) ;
45+ const doSet = useAtomSet ( setSecret , { mode : "promise" } ) ;
46+ const refreshSecrets = useAtomRefresh ( secretsAtom ( scopeId ) ) ;
47+
48+ const handleSave = async ( ) => {
49+ if ( ! secretId . trim ( ) || ! secretValue . trim ( ) ) return ;
50+ setSaving ( true ) ;
51+ setError ( null ) ;
52+ try {
53+ await doSet ( {
54+ path : { scopeId } ,
55+ payload : {
56+ id : SecretId . make ( secretId . trim ( ) ) ,
57+ name : secretName . trim ( ) || secretId . trim ( ) ,
58+ value : secretValue . trim ( ) ,
59+ purpose : `Google OAuth: ${ props . headerName } ` ,
60+ } ,
61+ } ) ;
62+ refreshSecrets ( ) ;
63+ props . onCreated ( secretId . trim ( ) ) ;
64+ } catch ( e ) {
65+ setError ( e instanceof Error ? e . message : "Failed to save secret" ) ;
66+ setSaving ( false ) ;
67+ }
68+ } ;
69+
70+ return (
71+ < div className = "rounded-lg border border-primary/20 bg-primary/[0.02] p-3 space-y-2.5" >
72+ < p className = "text-[11px] font-semibold text-primary tracking-wide uppercase" > New secret</ p >
73+ < div className = "grid grid-cols-2 gap-2" >
74+ < div className = "space-y-1" >
75+ < Label className = "text-[10px] uppercase tracking-wider text-muted-foreground" > ID</ Label >
76+ < Input
77+ value = { secretId }
78+ onChange = { ( e ) => setSecretIdValue ( ( e . target as HTMLInputElement ) . value ) }
79+ placeholder = "google-client-secret"
80+ className = "h-8 text-xs font-mono"
81+ />
82+ </ div >
83+ < div className = "space-y-1" >
84+ < Label className = "text-[10px] uppercase tracking-wider text-muted-foreground" > Label</ Label >
85+ < Input
86+ value = { secretName }
87+ onChange = { ( e ) => setSecretName ( ( e . target as HTMLInputElement ) . value ) }
88+ placeholder = "Client Secret"
89+ className = "h-8 text-xs"
90+ />
91+ </ div >
92+ </ div >
93+ < div className = "space-y-1" >
94+ < Label className = "text-[10px] uppercase tracking-wider text-muted-foreground" > Value</ Label >
95+ < Input
96+ type = "password"
97+ value = { secretValue }
98+ onChange = { ( e ) => setSecretValue ( ( e . target as HTMLInputElement ) . value ) }
99+ placeholder = "paste your client secret…"
100+ className = "h-8 text-xs font-mono"
101+ />
102+ </ div >
103+ { error && < p className = "text-[11px] text-destructive" > { error } </ p > }
104+ < div className = "flex gap-1.5 pt-0.5" >
105+ < Button variant = "outline" size = "xs" onClick = { props . onCancel } >
106+ Cancel
107+ </ Button >
108+ < Button
109+ size = "xs"
110+ onClick = { handleSave }
111+ disabled = { ! secretId . trim ( ) || ! secretValue . trim ( ) || saving }
112+ >
113+ { saving ? "Saving…" : "Create & use" }
114+ </ Button >
115+ </ div >
116+ </ div >
117+ ) ;
118+ }
119+
120+ // ---------------------------------------------------------------------------
121+ // Client secret field with inline creation
122+ // ---------------------------------------------------------------------------
123+
124+ function ClientSecretField ( props : {
125+ clientSecretSecretId : string | null ;
126+ onSelect : ( secretId : string | null ) => void ;
127+ secretList : readonly SecretPickerSecret [ ] ;
128+ } ) {
129+ const [ creating , setCreating ] = useState ( false ) ;
130+ const { clientSecretSecretId, onSelect, secretList } = props ;
131+
132+ if ( creating ) {
133+ return (
134+ < div className = "space-y-2" >
135+ < Label > OAuth Client Secret</ Label >
136+ < InlineCreateSecret
137+ headerName = "Client Secret"
138+ suggestedId = "google-oauth-client-secret"
139+ onCreated = { ( id ) => {
140+ onSelect ( id ) ;
141+ setCreating ( false ) ;
142+ } }
143+ onCancel = { ( ) => setCreating ( false ) }
144+ />
145+ </ div >
146+ ) ;
147+ }
148+
149+ return (
150+ < div className = "space-y-2" >
151+ < Label > OAuth Client Secret</ Label >
152+ < div className = "flex items-center gap-2" >
153+ < div className = "flex-1 min-w-0" >
154+ < SecretPicker
155+ value = { clientSecretSecretId }
156+ onSelect = { onSelect }
157+ secrets = { secretList }
158+ placeholder = "Optional for confidential clients"
159+ />
160+ </ div >
161+ < Button
162+ variant = "outline"
163+ size = "sm"
164+ className = "shrink-0"
165+ onClick = { ( ) => setCreating ( true ) }
166+ >
167+ + New
168+ </ Button >
169+ { clientSecretSecretId && (
170+ < Button
171+ variant = "outline"
172+ onClick = { ( ) => onSelect ( null ) }
173+ >
174+ Clear
175+ </ Button >
176+ ) }
177+ </ div >
178+ </ div >
179+ ) ;
180+ }
181+
27182type GoogleDiscoveryTemplate = {
28183 id : string ;
29184 name : string ;
@@ -576,27 +731,11 @@ export default function AddGoogleDiscoverySource(props: {
576731 placeholder = "1234567890-abc.apps.googleusercontent.com"
577732 />
578733 </ div >
579- < div className = "space-y-2" >
580- < Label > OAuth Client Secret</ Label >
581- < div className = "flex items-center gap-2" >
582- < div className = "flex-1 min-w-0" >
583- < SecretPicker
584- value = { clientSecretSecretId }
585- onSelect = { setClientSecretSecretId }
586- secrets = { secretList }
587- placeholder = "Optional for confidential clients"
588- />
589- </ div >
590- { clientSecretSecretId && (
591- < Button
592- variant = "outline"
593- onClick = { ( ) => setClientSecretSecretId ( null ) }
594- >
595- Clear
596- </ Button >
597- ) }
598- </ div >
599- </ div >
734+ < ClientSecretField
735+ clientSecretSecretId = { clientSecretSecretId }
736+ onSelect = { setClientSecretSecretId }
737+ secretList = { secretList }
738+ />
600739 < Collapsible
601740 open = { showScopes }
602741 onOpenChange = { setShowScopes }
0 commit comments